/*
* Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* * Neither the name of Business Objects nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/
/*
* AutoCompletePopupMenu.java
* Creation date: Dec 10th 2002
* By: Ken Wong
*/
package org.openquark.gems.client;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.JTextComponent;
import org.openquark.cal.compiler.ModuleName;
import org.openquark.cal.compiler.ModuleNameResolver.ResolutionResult;
import org.openquark.cal.compiler.ScopedEntityNamingPolicy.UnqualifiedUnlessAmbiguous;
import org.openquark.cal.services.AutoCompleteHelper;
import org.openquark.cal.services.GemEntity;
import org.openquark.cal.services.MetaModule;
import org.openquark.cal.services.Perspective;
import org.openquark.gems.client.AutoburnLogic.AutoburnUnifyStatus;
import org.openquark.gems.client.IntellicutListModelAdapter.IntellicutListEntry;
import org.openquark.gems.client.IntellicutManager.IntellicutInfo;
import org.openquark.gems.client.IntellicutManager.IntellicutMode;
import org.openquark.gems.client.utilities.MouseClickDragAdapter;
import org.openquark.util.Pair;
/**
* A popupmenu that assists the entry of unqualifiedNames by providing a sequential-search styled access to the
* unqualified names of gems
* @author Ken Wong
* Creation Date: December 3rd 2002
*/
public class AutoCompletePopupMenu extends JPopupMenu {
private static final long serialVersionUID = -7001891717459680744L;
/** The exception that is thrown when no autocomplete entry is found */
public static class AutoCompleteException extends Exception {
private static final long serialVersionUID = -5115617110940452405L;
/**
* Default constructor
* @param msg
*/
AutoCompleteException (String msg) {
super(msg);
}
}
/**
* The adapter used by the IntellicutList for the auto-complete manager.
* It includes all entities that can be substituted to complete/replace a
* portion of typed text. If the typed text is preceded by a module name,
* only the valid entities from the specific module will be shown as completions.
*
* @author Iulian Radu
*/
static class AutoCompleteIntellicutAdapter extends IntellicutListModelAdapter {
/**
* Module which is searched for matching entities for completion.
* If null, entities are retrieved from all modules via the perspective.
*/
private ResolutionResult moduleResolutions = null;
/**
* Perspective to use for retrieving entities which are not
* qualified to a module.
*/
private final Perspective perspective;
/**
* Constructor
* @param perspective
*/
public AutoCompleteIntellicutAdapter(Perspective perspective) {
this.perspective = perspective;
}
/**
* @see org.openquark.gems.client.IntellicutListModelAdapter#getDataObjects()
*/
@Override
protected Set<GemEntity> getDataObjects() {
if (moduleResolutions == null) {
setNamingPolicy(new UnqualifiedUnlessAmbiguous(perspective.getWorkingModuleTypeInfo()));
return getVisibleGemsFromPerspective(perspective);
} else {
// setNamingPolicy(ScopedEntityNamingPolicy.UNQUALIFIED);
ModuleName[] matches = moduleResolutions.getPotentialMatches();
HashSet<GemEntity> results = new HashSet<GemEntity>();
for(int i = 0; i < matches.length; ++i){
ModuleName moduleName = matches[i];
MetaModule metaModule = (perspective.isVisibleModule(moduleName)? perspective.getMetaModule(moduleName) : null);
if (metaModule != null) {
results.addAll(perspective.getVisibleGemEntities(metaModule));
}
}
return results;
}
}
/**
* Sets the module to retrieve list objects from.
* If null, objects are retrieved from all imported modules via the perspective.
* @param moduleName
*/
public void setModule(ResolutionResult moduleName) {
this.moduleResolutions = moduleName;
}
/**
* @see org.openquark.gems.client.IntellicutListModelAdapter#getIntellicutInfo(org.openquark.gems.client.IntellicutListModelAdapter.IntellicutListEntry)
*/
@Override
protected IntellicutInfo getIntellicutInfo(IntellicutListEntry listEntry) {
return new IntellicutInfo(AutoburnUnifyStatus.NOT_NECESSARY, -1, 1);
}
}
/** A list of the gems that are currently being displayed in the panel */
private IntellicutList visibleGems;
/** The intellicut adapter used for modeling the auto-completion list */
private AutoCompleteIntellicutAdapter listAdapter;
/** The whether or not the user commited */
private boolean userCommited;
/** The text field that the user types in */
private JTextComponent textComponent;
/** The context within the workspace */
private Perspective perspective;
/** The panel that stores the various UI components */
private final JPanel mainPanel;
/** all the listeners that were removed from the preview panel (add back when we close this popup) */
private KeyListener[] oldKeyListeners;
/** The length of the word that we're trying to complete */
private int userInputWordLength;
/** The length of the symbol that is being auto-completed. */
private int userInputQualifiedNameLength;
/** The AutoCompleteManager that handles this instance */
private final AutoCompleteManager autoCompleteManager;
/** The scrollpane that contains the autocomplete list*/
private final JScrollPane scrollPane;
/** The listener used to change the suggestion list that is shown. */
private final DocumentListener documentListener = new DocumentListener() {
/**
* @see javax.swing.event.DocumentListener#insertUpdate(DocumentEvent)
*/
public void insertUpdate(DocumentEvent e) {
refreshVisibleList(textComponent.getCaretPosition() + e.getLength());
}
/**
* @see javax.swing.event.DocumentListener#removeUpdate(DocumentEvent)
*/
public void removeUpdate(DocumentEvent e) {
refreshVisibleList(textComponent.getCaretPosition() - e.getLength());
}
/**
* @see javax.swing.event.DocumentListener#changedUpdate(DocumentEvent)
*/
public void changedUpdate(DocumentEvent e) {
refreshVisibleList(textComponent.getCaretPosition() + e.getLength());
}
};
/**
* The key listener that deals with the accept or cancel gestures
*/
private final KeyListener cancelAcceptKeyListener = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
if (keyCode == KeyEvent.VK_ESCAPE || keyCode == KeyEvent.VK_SPACE) {
userCommited = false;
autoCompleteManager.closeAutoCompletePopup();
e.consume();
} else if (keyCode == KeyEvent.VK_ENTER) {
userCommited = true;
autoCompleteManager.closeAutoCompletePopup();
e.consume();
}
}
};
/**
* The listener used to ensure that the up/down arrow keys still work when focus is on the textarea
* */
private final KeyListener previewFieldListener = new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int keyCode = e.getKeyCode();
int selectedIndex = visibleGems.getSelectedIndex();
if (keyCode == KeyEvent.VK_UP) {
selectedIndex--;
e.consume();
} else if (keyCode == KeyEvent.VK_DOWN) {
selectedIndex++;
e.consume();
} else if (keyCode == KeyEvent.VK_PAGE_UP) {
selectedIndex -= 10;
if (selectedIndex < visibleGems.getModel().getSize()) {
selectedIndex = 0;
}
e.consume();
} else if (keyCode == KeyEvent.VK_PAGE_DOWN) {
selectedIndex += 10;
if (selectedIndex > visibleGems.getModel().getSize()) {
selectedIndex = visibleGems.getModel().getSize() - 1;
}
e.consume();
} else if (keyCode == KeyEvent.VK_LEFT) {
if (textComponent.getCaretPosition() > 0) {
refreshVisibleList(textComponent.getCaretPosition() - 1);
repositionPopup(textComponent.getCaretPosition() - 1);
}
} else if (keyCode == KeyEvent.VK_RIGHT) {
if (textComponent.getCaretPosition() < textComponent.getText().length()) {
refreshVisibleList(textComponent.getCaretPosition() + 1);
repositionPopup(textComponent.getCaretPosition() + 1);
}
}
if (selectedIndex >= 0 && selectedIndex < visibleGems.getModel().getSize()) {
visibleGems.setSelectedIndex(selectedIndex);
visibleGems.ensureIndexIsVisible(selectedIndex);
}
}
};
/**
* MouseListener we use to listen for left double clicks to commit the user's choice.
*/
private class DoubleClickMouseListener extends MouseClickDragAdapter {
@Override
public boolean mouseReallyClicked(MouseEvent e){
boolean doubleClicked = super.mouseReallyClicked(e);
if (doubleClicked && SwingUtilities.isLeftMouseButton(e)) {
userCommited = true;
autoCompleteManager.closeAutoCompletePopup();
}
return doubleClicked;
}
}
/**
* Constructor for AutoCompletePopupMenu.
*/
public AutoCompletePopupMenu(AutoCompleteManager autoCompleteManager) {
this.autoCompleteManager = autoCompleteManager;
// initialize the mainPanel
mainPanel = new JPanel(new BorderLayout());
// layout stuff
setSize(200, 200);
userCommited = false;
// create a scroll pane to display the list
scrollPane = new JScrollPane();
// layout stuff...
mainPanel.add(scrollPane, BorderLayout.CENTER);
add(mainPanel);
scrollPane.setPreferredSize(new Dimension(200,150));
scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
}
/**
* Refresh the visible list to reflect the user input so far
*/
private void refreshVisibleList(int caretPosition) {
// Get the text that was in the text component
String entireTextField = textComponent.getText();
// Since MS Windows uses two characters for linefeed instead of one. (Unlike every other
// OS in existence) We have to get rid of line feeds all together to get rid of parsing errors
String windowsLineSeparator = "\r\n";
entireTextField = entireTextField.replaceAll(windowsLineSeparator, "\n");
final String textField = entireTextField;
final AutoCompleteHelper ach = new AutoCompleteHelper(new AutoCompleteHelper.Document() {
public char getChar(int offset) {
return textField.charAt(offset);
}
public String get(int startIndex, int length) {
return textField.substring(startIndex, startIndex + length);
}
});
// Find the text we are completing (ex: no in "Prelude.no")
String userInput = ach.getLastIncompleteIdentifier(caretPosition);
userInputWordLength = userInput.length();
final Pair<String, List<Integer>> scopingAndOffset = ach.getIdentifierScoping(caretPosition);
final String moduleNameString = scopingAndOffset.fst();
final List<Integer> componentPositions = scopingAndOffset.snd();
final int startOfModuleName = (componentPositions.get(0)).intValue();
// Now if we are completing a qualification, find the module (ex: Prelude in "Prelude.no")
final boolean qualification = moduleNameString.length() > 0;
if (!qualification) {
listAdapter.setModule(null);
listAdapter.clear();
visibleGems.refreshList(userInput);
userInputQualifiedNameLength = userInputWordLength;
} else {
userInputQualifiedNameLength = caretPosition - startOfModuleName;
try{
final ModuleName moduleName = ModuleName.make(moduleNameString);
final ResolutionResult resolution = perspective.getWorkingModuleTypeInfo().getModuleNameResolver().resolve(moduleName);
listAdapter.setModule(resolution);
}
catch(IllegalArgumentException e){
// if the module name is not valid then there are no suggestions available.
}
listAdapter.clear();
visibleGems.refreshList(userInput);
}
if (visibleGems.getModel().getSize() == 0) {
userCommited = false;
autoCompleteManager.closeAutoCompletePopup();
}
if (visibleGems.getSelectedIndex() == -1) {
visibleGems.setSelectedIndex(0);
}
}
/**
* Reposition the list below the specified caret position.
* @param caretPos the caret position
*/
private void repositionPopup(int caretPos) {
try {
// reposition the popup below the cursor position
Point location = textComponent.getUI().modelToView(textComponent, caretPos).getLocation();
int height = textComponent.getFontMetrics(textComponent.getFont()).getHeight();
show(textComponent, location.x, location.y + height);
textComponent.requestFocus();
} catch (BadLocationException ex) {
// This shouldn't happen
}
}
/**
* Display the popup menu.
* @param perspective the perspective to load gems from
* @param invoker the component that invoked the popup menu
* @param location at which location to display the popup menu (in invoker's coordinate space)
*/
public void start(Perspective perspective, JTextComponent invoker, Point location) throws AutoCompleteException{
userCommited = false;
textComponent = invoker;
oldKeyListeners = textComponent.getKeyListeners();
this.perspective = perspective;
for (final KeyListener oldKeyListener : oldKeyListeners) {
textComponent.removeKeyListener(oldKeyListener);
}
listAdapter = new AutoCompleteIntellicutAdapter(perspective);
IntellicutListModel listModel = new IntellicutListModel(listAdapter);
visibleGems = new IntellicutList(IntellicutMode.NOTHING);
visibleGems.setModel(listModel);
listModel.load();
// We don't want the list to ever have focus, focus should stay with the editor
visibleGems.setFocusable(false);
setFocusable(false);
// Add all the requisite key listeners
textComponent.addKeyListener(cancelAcceptKeyListener);
textComponent.getDocument().addDocumentListener(documentListener);
textComponent.addKeyListener(previewFieldListener);
visibleGems.addMouseListener(new DoubleClickMouseListener());
scrollPane.setViewportView(visibleGems);
refreshVisibleList(textComponent.getCaretPosition());
if (visibleGems.getModel().getSize() == 1) {
// If there is only one option available, we automatically just choose it for the user.
userCommited = true;
visibleGems.setSelectedIndex(0);
autoCompleteManager.closeAutoCompletePopup();
} else if (visibleGems.getModel().getSize() == 0) {
// If there are no options available, we display an error message.
userCommited = false;
throw (new AutoCompleteException("No Valid Autocomplete Entry"));
} else {
// If there are valid choices, then show the list.
show(invoker, location.x, location.y);
textComponent.requestFocus();
}
}
/**
* Return the user selected string. If the user committed, then the result is the selected GemEntity.
* if the user cancelled, then the returned value is null.
* @return IntellicutListEntry
*/
IntellicutListEntry getSelected() {
return (userCommited) ? visibleGems.getSelected() : null;
}
/**
* Returns the length of the word that we want to complete
* @return int
*/
int getUserInputWordLength() {
return userInputWordLength;
}
/**
* Returns the length of the symbol that we want to complete
* @return int
*/
int getUserInputQualifiedNameLength() {
return userInputQualifiedNameLength;
}
/** We override this to restore the key listeners we removed from the editor pane.
* @see java.awt.Component#setVisible(boolean)
*/
@Override
public void setVisible(boolean show) {
super.setVisible(show);
if (!show) {
textComponent.getDocument().removeDocumentListener(documentListener);
textComponent.removeKeyListener(cancelAcceptKeyListener);
textComponent.removeKeyListener(previewFieldListener);
if (oldKeyListeners != null) {
for (int i = 0; i < oldKeyListeners.length; i++) {
if (!Arrays.asList(textComponent.getKeyListeners()).contains(oldKeyListeners[i])){
textComponent.addKeyListener(oldKeyListeners[i]);
}
}
}
}
}
}