diff --git a/src/main/java/org/fife/ui/autocomplete/AutoCompleteDescWindow.java b/src/main/java/org/fife/ui/autocomplete/AutoCompleteDescWindow.java index f873898..d449226 100644 --- a/src/main/java/org/fife/ui/autocomplete/AutoCompleteDescWindow.java +++ b/src/main/java/org/fife/ui/autocomplete/AutoCompleteDescWindow.java @@ -205,8 +205,14 @@ public void paintBorder(Component c, Graphics g, int x, int y, int w, int h) { } - - /** + @Override + public void dispose() + { + ac = null; + super.dispose(); + } + + /** * Sets the currently displayed description and updates the history. * * @param historyItem The item to add to the history. diff --git a/src/main/java/org/fife/ui/autocomplete/AutoCompletePopupWindow.java b/src/main/java/org/fife/ui/autocomplete/AutoCompletePopupWindow.java index b7b9911..fdc84b8 100644 --- a/src/main/java/org/fife/ui/autocomplete/AutoCompletePopupWindow.java +++ b/src/main/java/org/fife/ui/autocomplete/AutoCompletePopupWindow.java @@ -187,8 +187,18 @@ public AutoCompletePopupWindow(Window parent, final AutoCompletion ac) { } - - @Override + @Override + public void dispose() + { + ac = null; + if (descWindow != null) { + descWindow.dispose(); + descWindow = null; + } + super.dispose(); + } + + @Override public void caretUpdate(CaretEvent e) { if (isVisible()) { // Should always be true int line = ac.getLineOfCaret(); diff --git a/src/main/java/org/fife/ui/autocomplete/AutoCompletion.java b/src/main/java/org/fife/ui/autocomplete/AutoCompletion.java index 4f8adc6..db64b76 100644 --- a/src/main/java/org/fife/ui/autocomplete/AutoCompletion.java +++ b/src/main/java/org/fife/ui/autocomplete/AutoCompletion.java @@ -8,9 +8,13 @@ */ package org.fife.ui.autocomplete; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; +import sun.management.snmp.jvminstr.JvmThreadInstanceEntryImpl; + import java.awt.*; import java.awt.event.*; import java.beans.*; +import java.util.ArrayList; import java.util.List; import javax.swing.*; import javax.swing.Timer; @@ -145,6 +149,11 @@ public class AutoCompletion { */ private KeyStroke trigger; + /** + * The keystroke to show method parameter tooltip + */ + private KeyStroke paramTrigger; + /** * The previous key in the text component's InputMap for the * trigger key. @@ -157,7 +166,26 @@ public class AutoCompletion { */ private Action oldTriggerAction; - /** + /** + * The previous key in the text component's InputMap for the + * paramTrigger key. + */ + private Object oldParamTriggerKey; + + /** + * The action previously assigned to {@link #paramTrigger}, so we can reset it if + * the user disables auto-completion. + */ + private Action oldParamTriggerAction; + + /** + * List of tooltip windows in case user presses Ctrl-P. If multiple ParameterizedCompletion is returned from the + * provider, we show multiple tool windows. This holds reference, so we can remove it. + */ + private List parameterTooltipWindows = new ArrayList(); + + private ParameterTooltipListener parameterTooltipListener; + /** * The previous key in the text component's InputMap for the * parameter completion trigger key. */ @@ -222,6 +250,11 @@ public class AutoCompletion { */ private static final String PARAM_TRIGGER_KEY = "AutoComplete"; + /** + * The key used in the input map for the AutoComplete action. + */ + private static final String SHOW_PARAM_TRIGGER_KEY = "AutoCompleteParams"; + /** * Key used in the input map for the parameter completion action. */ @@ -252,6 +285,7 @@ public AutoCompletion(CompletionProvider provider) { setCompletionProvider(provider); setTriggerKey(getDefaultTriggerKey()); + setParamTrigger(getDefaultParamTriggerKey()); setAutoCompleteEnabled(true); setAutoCompleteSingleChoices(true); setAutoActivationEnabled(false); @@ -369,6 +403,10 @@ public static KeyStroke getDefaultTriggerKey() { return KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, mask); } + public static KeyStroke getDefaultParamTriggerKey() { + int mask = InputEvent.CTRL_MASK; + return KeyStroke.getKeyStroke(KeyEvent.VK_P, mask); + } /** * Returns the handler to use when an external URL is clicked in the @@ -545,6 +583,7 @@ protected boolean hidePopupWindow() { return true; } } + hideParameterTooltips(); return false; } @@ -629,6 +668,7 @@ public void install(JTextComponent c) { this.textComponent = c; installTriggerKey(getTriggerKey()); + installParamTriggerKey(getParamTrigger()); // Install the function completion key, if there is one. // NOTE: We cannot do this if the start char is ' ' (e.g. just a space @@ -678,6 +718,69 @@ private void installTriggerKey(KeyStroke ks) { } + /** + * Installs a "trigger key" action onto the current text component. + * + * @param ks The keystroke that should trigger the action. + * @see #uninstallTriggerKey() + */ + private void installParamTriggerKey(KeyStroke ks) { + InputMap im = textComponent.getInputMap(); + oldParamTriggerKey = im.get(ks); + im.put(ks, SHOW_PARAM_TRIGGER_KEY); + ActionMap am = textComponent.getActionMap(); + oldParamTriggerAction = am.get(SHOW_PARAM_TRIGGER_KEY); + am.put(SHOW_PARAM_TRIGGER_KEY, showParameterTooltip()); + } + + private void hideParameterTooltips() { + if (parameterTooltipWindows.size() > 0) { + for (ParameterizedCompletionDescriptionToolTip tip : parameterTooltipWindows) { + tip.setVisible(false); + } + parameterTooltipWindows.clear(); + } + if (parameterTooltipListener != null) { + parameterTooltipListener.uninstall(textComponent); + } + } + + private Action showParameterTooltip() { + return new AbstractAction() + { + @Override + public void actionPerformed(ActionEvent e) + { + hideParameterTooltips(); + if (parameterTooltipListener == null) { + parameterTooltipListener = new ParameterTooltipListener(); + } + parameterTooltipListener.install(textComponent); + List completions = provider.getParameterizedCompletions(textComponent); + + if (completions != null && completions.size() > 0) { + for (int i = 0;i < completions.size();i++) { + ParameterizedCompletion pc = completions.get(i); + ParameterizedCompletionDescriptionToolTip tip = new ParameterizedCompletionDescriptionToolTip(parentWindow, new ParameterizedCompletionContext(parentWindow, AutoCompletion.this, pc), AutoCompletion.this, pc); + try { + int dot = textComponent.getCaretPosition(); + Rectangle r = textComponent.modelToView(dot); + Point p = new Point(r.x, r.y); + SwingUtilities.convertPointToScreen(p, textComponent); + r.x = p.x; + r.y = p.y - (24 * (completions.size() - (i + 1))); + tip.setLocationRelativeTo(r); + tip.setVisible(true); + parameterTooltipWindows.add(tip); + } + catch (Exception ex) { + } + } + } + } + }; + } + /** * Creates and returns the action to call when the user presses the * auto-completion trigger key (e.g. ctrl+space). This is a hook for @@ -785,6 +888,8 @@ protected int refreshPopupWindow() { return getLineOfCaret(); } + hideParameterTooltips(); + // If the popup is currently visible, and they type a space (or any // character that resets the completion list to "all completions"), // the popup window should be hidden instead of being reset to show @@ -1218,10 +1323,14 @@ public void uninstall() { UIManager.removePropertyChangeListener(lafListener); - textComponent = null; popupWindowListener.uninstall(popupWindow); + // release system resources + if (popupWindow != null) popupWindow.dispose(); popupWindow = null; + hideParameterTooltips(); + + textComponent = null; } } @@ -1531,5 +1640,131 @@ public void removeFrom(JTextComponent tc) { } - + public KeyStroke getParamTrigger() + { + return paramTrigger; + } + + public void setParamTrigger(KeyStroke paramTrigger) + { + this.paramTrigger = paramTrigger; + } + + /** + * Listener used to handle focus/caret update and ESC key if the Ctrl-P parameter hint tooltip is visible + */ + private class ParameterTooltipListener implements FocusListener, CaretListener { + private int minPos; + private int maxPos; + private Object oldEscapeKey; + private Action oldEscapeAction; + private static final String IM_KEY_ESCAPE = "ParamTooltipEscape"; + + public void install(JTextComponent textComponent) { + if (textComponent instanceof RSyntaxTextArea) { + RSyntaxTextArea t = (RSyntaxTextArea) textComponent; + + // set default to the whole line + minPos = t.getLineStartOffsetOfCurrentLine(); + maxPos = t.getLineEndOffsetOfCurrentLine(); + + // determine the first ( and the last ) within the method call and set it as boundaries of Parameter + // tooltip + int parenCounter = 0; + Document doc = textComponent.getDocument(); + + Element root = doc.getDefaultRootElement(); + int index = root.getElementIndex(textComponent.getCaretPosition()); + Element elem = root.getElement(index); + int start = elem.getStartOffset(); + int len = elem.getEndOffset() - start; + Segment seg = new Segment(new char[len], 0, 0); + try { + doc.getText(start, len, seg); + + // go from current caret position backwards, till we find an opening ( without a closing ) + int i = textComponent.getCaretPosition() - elem.getStartOffset(); + while (i >= 0) { + if (seg.charAt(i) == ')') parenCounter++; + else if (seg.charAt(i) == '(' && parenCounter > 0) parenCounter--; + else if (seg.charAt(i) == '(' && parenCounter == 0) { + minPos = i + 1 + elem.getStartOffset(); + break; + } + i--; + } + + // now go from current caret position towards the end of the line trying to find a closing ) without opening ( + i = textComponent.getCaretPosition() - elem.getStartOffset(); + while (i < seg.length()) { + if (seg.charAt(i) == '(') parenCounter++; + else if (seg.charAt(i) == ')' && parenCounter > 0) parenCounter--; + else if (seg.charAt(i) == ')' && parenCounter == 0) { + maxPos = i + 1 + elem.getStartOffset(); + break; + } + i++; + } + } + catch (Exception ble) { + ble.printStackTrace(); + } + } + textComponent.addCaretListener(this); + textComponent.addFocusListener(this); + + installKeyBindings(textComponent); + } + + public void uninstall(JTextComponent textComponent) { + textComponent.removeCaretListener(this); + textComponent.removeFocusListener(this); + + uninstallKeyBindings(textComponent); + } + + private void installKeyBindings(JTextComponent textComponent) { + InputMap im = textComponent.getInputMap(); + ActionMap am = textComponent.getActionMap(); + + KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); + oldEscapeKey = im.get(ks); + im.put(ks, IM_KEY_ESCAPE); + oldEscapeAction = am.get(IM_KEY_ESCAPE); + am.put(IM_KEY_ESCAPE, new AbstractAction() { + @Override + public void actionPerformed(ActionEvent e) { + hideParameterTooltips(); + } + }); + } + + private void uninstallKeyBindings(JTextComponent textComponent) { + InputMap im = textComponent.getInputMap(); + ActionMap am = textComponent.getActionMap(); + + KeyStroke ks = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); + im.put(ks, oldEscapeKey); + am.put(IM_KEY_ESCAPE, oldEscapeAction); + } + + @Override + public void caretUpdate(CaretEvent e) { + int dot = e.getDot(); + if (dot < minPos || dot >= maxPos) { + hideParameterTooltips(); + return; + } + } + + @Override + public void focusGained(FocusEvent e) { + // do nothing + } + + @Override + public void focusLost(FocusEvent e) { + hideParameterTooltips(); + } + } } \ No newline at end of file diff --git a/src/main/java/org/fife/ui/autocomplete/ParameterizedCompletionChoicesWindow.java b/src/main/java/org/fife/ui/autocomplete/ParameterizedCompletionChoicesWindow.java index 0e08c32..91c34c5 100644 --- a/src/main/java/org/fife/ui/autocomplete/ParameterizedCompletionChoicesWindow.java +++ b/src/main/java/org/fife/ui/autocomplete/ParameterizedCompletionChoicesWindow.java @@ -116,8 +116,14 @@ public void mouseClicked(MouseEvent e) { } - - /** + @Override + public void dispose() + { + ac = null; + super.dispose(); + } + + /** * Returns the selected value. * * @return The selected value, or null if nothing is @@ -133,14 +139,21 @@ public String getSelectedChoice() { * Changes the selected index. * * @param amount The amount by which to change the selected index. + * @param cyclic true if the selection should go around, false, if it should stop at first or last item */ - public void incSelection(int amount) { + public void incSelection(int amount, boolean cyclic) { int selection = list.getSelectedIndex(); selection += amount; - if (selection<0) { + if (selection < 0 && cyclic) { // Account for nothing selected yet selection = model.getSize()-1;//+= model.getSize(); } + else if (selection < 0) { + selection = 0; + } + else if (selection >= model.getSize() && !cyclic) { + selection = model.getSize() - 1; + } else { selection %= model.getSize(); } @@ -225,6 +238,7 @@ public void setParameter(int param, String prefix) { model.clear(); List temp = new ArrayList(); + List prefixParts = Util.getTextParts(prefix); if (choicesListList!=null && param>=0 && paramscreenBounds.x+screenBounds.width) { // completion * @see #isVisible() */ public void setVisible(boolean visible) { + if (tooltip == null) initTooltipWindow(); tooltip.setVisible(visible); + // if not visible, dispose window resources + if (!visible) { + tooltip.dispose(); + tooltip = null; + } } @@ -157,6 +172,7 @@ public void setVisible(boolean visible) { * @return Whether the text needed to be updated. */ public boolean updateText(int selectedParam) { + if (tooltip == null) initTooltipWindow(); StringBuilder sb = new StringBuilder(""); int paramCount = pc.getParamCount(); diff --git a/src/main/java/org/fife/ui/autocomplete/Util.java b/src/main/java/org/fife/ui/autocomplete/Util.java index c563e17..d0a8e90 100644 --- a/src/main/java/org/fife/ui/autocomplete/Util.java +++ b/src/main/java/org/fife/ui/autocomplete/Util.java @@ -16,6 +16,8 @@ import java.lang.reflect.Method; import java.net.URI; import java.security.AccessControlException; +import java.util.ArrayList; +import java.util.List; import java.util.regex.Pattern; import javax.swing.JLabel; @@ -319,5 +321,56 @@ public static String stripHtml(String text) { } - + /** + * Return text parts list for given text split at uppercase characters + * + * @param text + * @return + */ + public static List getTextParts(String text) { + List textParts = new ArrayList(); + if (text == null || "".equals(text)) return textParts; + StringBuilder sb = new StringBuilder(); + for (int i = 0;i < text.length();i++) { + if (Character.isUpperCase(text.charAt(i))) { + if (sb.length() > 0) { + textParts.add(sb.toString()); + sb = new StringBuilder(); + } + } + sb.append(text.charAt(i)); + } + if (sb.length() > 0) textParts.add(sb.toString()); + + return textParts; + } + + /** + * Compares two list of string if they share the same Uppercase segments. + * + * @param enteredText the text Parts for the entered text + * @param toBeMatched the text Parts for method, field, classname, etc. + * @return true if they match, false otherwise + */ + public static boolean matchTextParts(List enteredText, List toBeMatched) { + if (toBeMatched.size() < enteredText.size()) return false; + for (int i = 0;i < enteredText.size();i++) { + if (!toBeMatched.get(i).startsWith(enteredText.get(i))) { + return false; + } + } + return true; + } + + /** + * Compares two string if they share the same Uppercase segments. This method will + * create the string list for both parameter to be matched + * + * @param enteredText + * @param toBeMatched + * @return + */ + public static boolean matchTextParts(String enteredText, String toBeMatched) { + return matchTextParts(getTextParts(enteredText), getTextParts(toBeMatched)); + } } \ No newline at end of file