/** * Copyright (c) 2015 committers of YAKINDU and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * Contributors: * committers of YAKINDU - initial API and implementation * */ package org.yakindu.base.xtext.utils.jface.fieldassist; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import org.eclipse.core.runtime.ListenerList; import org.eclipse.jface.bindings.keys.KeyStroke; import org.eclipse.jface.fieldassist.ContentProposalAdapter; import org.eclipse.jface.text.contentassist.CompletionProposal; import org.eclipse.jface.text.contentassist.ContentAssistEvent; import org.eclipse.jface.text.contentassist.ContentAssistant; import org.eclipse.jface.text.contentassist.ICompletionListener; import org.eclipse.jface.text.contentassist.ICompletionProposal; import org.eclipse.jface.text.contentassist.IContentAssistant; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; /** * This is a stripped copy from {@link ContentProposalAdapter} that delegates * the popup to a {@link CompletionProposal} managed by a * {@link IContentAssistant}. * * @author patrick.koenemann@itemis.de * */ public class CompletionProposalAdapter implements ICompletionListener { private final IContentAssistant contentAssistant; /** * <p> * This adapter installs listener on the given control and delegates the * completion proposal popup request to the given {@link IContentAssistant}. * </p> * * <p> * FIXME: Parameter <code>autoActivationCharacters</code> is untested. * </p> * * @param control * @param contentAssistant * @param keyStroke * @param autoActivationCharacters */ public CompletionProposalAdapter(Control control, IContentAssistant contentAssistant, KeyStroke keyStroke, char[] autoActivationCharacters) { this.control = control; this.triggerKeyStroke = keyStroke; if (autoActivationCharacters != null) { this.autoActivateString = new String(autoActivationCharacters); } this.contentAssistant = contentAssistant; addControlListener(control); } /** * Flag that controls the printing of debug info. */ public static final boolean DEBUG = false; /* * The control for which content proposals are provided. */ private Control control; /* * The keystroke that signifies content proposals should be shown. */ private KeyStroke triggerKeyStroke; /* * The String containing characters that auto-activate the popup. */ private String autoActivateString; /* * The listener we install on the control. */ private Listener controlListener; /* * The list of IContentProposalListener2 listeners. */ private ListenerList proposalListeners2 = new ListenerList(); /* * Flag that indicates whether the adapter is enabled. In some cases, * adapters may be installed but depend upon outside state. */ private boolean isEnabled = true; /* * The delay in milliseconds used when autoactivating the popup. */ private int autoActivationDelay = 0; /* * A boolean indicating whether a keystroke has been received. Used to see * if an autoactivation delay was interrupted by a keystroke. */ private boolean receivedKeyDown; /** * Get the control on which the content proposal adapter is installed. * * @return the control on which the proposal adapter is installed. */ public Control getControl() { return control; } /** * Return a boolean indicating whether the receiver is enabled. * * @return <code>true</code> if the adapter is enabled, and * <code>false</code> if it is not. */ public boolean isEnabled() { return isEnabled; } /** * Return the array of characters on which the popup is autoactivated. * * @return An array of characters that trigger auto-activation of content * proposal. If specified, these characters will trigger * auto-activation of the proposal popup, regardless of whether an * explicit invocation keyStroke was specified. If this parameter is * <code>null</code>, then only a specified keyStroke will invoke * content proposal. If this value is <code>null</code> and the * keyStroke value is <code>null</code>, then all alphanumeric * characters will auto-activate content proposal. */ public char[] getAutoActivationCharacters() { if (autoActivateString == null) { return null; } return autoActivateString.toCharArray(); } /** * Set the array of characters that will trigger autoactivation of the * popup. * * @param autoActivationCharacters * An array of characters that trigger auto-activation of content * proposal. If specified, these characters will trigger * auto-activation of the proposal popup, regardless of whether * an explicit invocation keyStroke was specified. If this * parameter is <code>null</code>, then only a specified * keyStroke will invoke content proposal. If this parameter is * <code>null</code> and the keyStroke value is <code>null</code> * , then all alphanumeric characters will auto-activate content * proposal. * */ public void setAutoActivationCharacters(char[] autoActivationCharacters) { if (autoActivationCharacters == null) { this.autoActivateString = null; } else { this.autoActivateString = new String(autoActivationCharacters); } } /** * Set the delay, in milliseconds, used before any autoactivation is * triggered. * * @return the time in milliseconds that will pass before a popup is * automatically opened */ public int getAutoActivationDelay() { return autoActivationDelay; } /** * Set the delay, in milliseconds, used before autoactivation is triggered. * * @param delay * the time in milliseconds that will pass before a popup is * automatically opened */ public void setAutoActivationDelay(int delay) { autoActivationDelay = delay; } /** * Set the boolean flag that determines whether the adapter is enabled. * * @param enabled * <code>true</code> if the adapter is enabled and responding to * user input, <code>false</code> if it is ignoring user input. * */ public void setEnabled(boolean enabled) { // If we are disabling it while it's proposing content, close the // content proposal popup. if (isEnabled && !enabled) { // if (popup != null) { // popup.close(); // } } isEnabled = enabled; } /** * Add the specified listener to the list of content proposal listeners that * are notified when a content proposal popup is opened or closed. */ public void addContentProposalListener(ICompletionProposalListener listener) { proposalListeners2.add(listener); } /** * Remove the specified listener from the list of content proposal listeners * that are notified when a content proposal popup is opened or closed. */ public void removeContentProposalListener( ICompletionProposalListener listener) { proposalListeners2.remove(listener); } /* * Add our listener to the control. Debug information to be left in until * this support is stable on all platforms. */ private void addControlListener(Control control) { if (DEBUG) { System.out .println("ContentProposalListener#installControlListener()"); //$NON-NLS-1$ } if (controlListener != null) { return; } controlListener = new Listener() { public void handleEvent(Event e) { if (!isEnabled) { return; } switch (e.type) { case SWT.Traverse: case SWT.KeyDown: if (DEBUG) { StringBuffer sb; if (e.type == SWT.Traverse) { sb = new StringBuffer("Traverse"); //$NON-NLS-1$ } else { sb = new StringBuffer("KeyDown"); //$NON-NLS-1$ } sb.append(" received by adapter"); //$NON-NLS-1$ dump(sb.toString(), e); } // If the popup is open, it gets first shot at the // keystroke and should set the doit flags appropriately. // if (popup != null) { // popup.getTargetControlListener().handleEvent(e); // if (DEBUG) { // StringBuffer sb; // if (e.type == SWT.Traverse) { // sb = new StringBuffer("Traverse"); //$NON-NLS-1$ // } else { // sb = new StringBuffer("KeyDown"); //$NON-NLS-1$ // } // sb.append(" after being handled by popup"); //$NON-NLS-1$ // dump(sb.toString(), e); // } // // See // https://bugs.eclipse.org/bugs/show_bug.cgi?id=192633 // // If the popup is open and this is a valid character, we // // want to watch for the modified text. // if (propagateKeys && e.character != 0) // watchModify = true; // // return; // } // We were only listening to traverse events for the popup if (e.type == SWT.Traverse) { return; } // The popup is not open. We are looking at keydown events // for a trigger to open the popup. if (triggerKeyStroke != null) { // Either there are no modifiers for the trigger and we // check the character field... if ((triggerKeyStroke.getModifierKeys() == KeyStroke.NO_KEY && triggerKeyStroke .getNaturalKey() == e.character) || // ...or there are modifiers, in which case the // keycode and state must match (triggerKeyStroke.getNaturalKey() == e.keyCode && ((triggerKeyStroke .getModifierKeys() & e.stateMask) == triggerKeyStroke .getModifierKeys()))) { // We never propagate the keystroke for an explicit // keystroke invocation of the popup e.doit = false; openProposalPopup(false); return; } } /* * The triggering keystroke was not invoked. If a character * was typed, compare it to the autoactivation characters. */ if (e.character != 0) { if (autoActivateString != null) { if (autoActivateString.indexOf(e.character) >= 0) { autoActivate(); } else { // No autoactivation occurred, so record the key // down as a means to interrupt any // autoactivation that is pending due to // autoactivation delay. receivedKeyDown = true; // watch the modify so we can close the popup in // cases where there is no longer a trigger // character in the content // watchModify = true; } } else { // The autoactivate string is null. If the trigger // is also null, we want to act on any modification // to the content. Set a flag so we'll catch this // in the modify event. if (triggerKeyStroke == null) { // watchModify = true; } } } else { // A non-character key has been pressed. Interrupt any // autoactivation that is pending due to autoactivation // delay. receivedKeyDown = true; } break; // There are times when we want to monitor content changes // rather than individual keystrokes to determine whether // the popup should be closed or opened based on the entire // content of the control. // The watchModify flag ensures that we don't autoactivate if // the content change was caused by something other than typing. // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=183650 // case SWT.Modify: // if (allowsAutoActivate() && watchModify) { // if (DEBUG) { // dump("Modify event triggers popup open or close", e); //$NON-NLS-1$ // } // watchModify = false; // // We are in autoactivation mode, either for specific // // characters or for all characters. In either case, // // we should close the proposal popup when there is no // // content in the control. // if (isControlContentEmpty()) { // // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=192633 // closeProposalPopup(); // } else { // // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=147377 // // Given that we will close the popup when there are // // no valid proposals, we must consider reopening it on any // // content change when there are no particular autoActivation // // characters // if (autoActivateString == null) { // autoActivate(); // } else { // // Autoactivation characters are defined, but this // // modify event does not involve one of them. See // // if any of the autoactivation characters are left // // in the content and close the popup if none remain. // if (!shouldPopupRemainOpen()) // closeProposalPopup(); // } // } // } // break; default: break; } } /** * Dump the given events to "standard" output. * * @param who * who is dumping the event * @param e * the event */ private void dump(String who, Event e) { StringBuffer sb = new StringBuffer( "--- [ContentProposalAdapter]\n"); //$NON-NLS-1$ sb.append(who); sb.append(" - e: keyCode=" + e.keyCode + hex(e.keyCode)); //$NON-NLS-1$ sb.append("; character=" + e.character + hex(e.character)); //$NON-NLS-1$ sb.append("; stateMask=" + e.stateMask + hex(e.stateMask)); //$NON-NLS-1$ sb.append("; doit=" + e.doit); //$NON-NLS-1$ sb.append("; detail=" + e.detail + hex(e.detail)); //$NON-NLS-1$ sb.append("; widget=" + e.widget); //$NON-NLS-1$ System.out.println(sb); } private String hex(int i) { return "[0x" + Integer.toHexString(i) + ']'; //$NON-NLS-1$ } }; control.addListener(SWT.KeyDown, controlListener); control.addListener(SWT.Traverse, controlListener); control.addListener(SWT.Modify, controlListener); if (DEBUG) { System.out .println("ContentProposalAdapter#installControlListener() - installed"); //$NON-NLS-1$ } } /** * Open the proposal popup and display the proposals provided by the * proposal provider. If there are no proposals to be shown, do not show the * popup. This method returns immediately. That is, it does not wait for the * popup to open or a proposal to be selected. * * @param autoActivated * a boolean indicating whether the popup was autoactivated. If * false, a beep will sound when no proposals can be shown. */ private void openProposalPopup(boolean autoActivated) { if (isValid() && isEnabled()) { // XXX here we delegate the request! contentAssistant.showPossibleCompletions(); ((ContentAssistant) contentAssistant).addCompletionListener(this); } } /** * Open the proposal popup and display the proposals provided by the * proposal provider. This method returns immediately. That is, it does not * wait for a proposal to be selected. This method is used by subclasses to * explicitly invoke the opening of the popup. If there are no proposals to * show, the popup will not open and a beep will be sounded. */ protected void openProposalPopup() { openProposalPopup(false); } /* * Check that the control and content adapter are valid. */ private boolean isValid() { return control != null && !control.isDisposed(); } /** * Autoactivation has been triggered. Open the popup using any specified * delay. */ private void autoActivate() { if (autoActivationDelay > 0) { Runnable runnable = new Runnable() { public void run() { receivedKeyDown = false; try { Thread.sleep(autoActivationDelay); } catch (InterruptedException e) { } if (!isValid() || receivedKeyDown) { return; } getControl().getDisplay().syncExec(new Runnable() { public void run() { openProposalPopup(true); } }); } }; Thread t = new Thread(runnable); t.start(); } else { // Since we do not sleep, we must open the popup // in an async exec. This is necessary because // this method may be called in the middle of handling // some event that will cause the cursor position or // other important info to change as a result of this // event occurring. getControl().getDisplay().asyncExec(new Runnable() { public void run() { if (isValid()) { openProposalPopup(true); } } }); } } /* * The proposal popup has opened. Notify interested listeners. */ private void notifyPopupOpened() { if (DEBUG) { System.out.println("Notify listeners - popup opened."); //$NON-NLS-1$ } final Object[] listenerArray = proposalListeners2.getListeners(); for (int i = 0; i < listenerArray.length; i++) { ((ICompletionProposalListener) listenerArray[i]) .proposalPopupOpened(this); } } /* * The proposal popup has closed. Notify interested listeners. */ private void notifyPopupClosed() { if (DEBUG) { System.out.println("Notify listeners - popup closed."); //$NON-NLS-1$ } final Object[] listenerArray = proposalListeners2.getListeners(); for (int i = 0; i < listenerArray.length; i++) { ((ICompletionProposalListener) listenerArray[i]) .proposalPopupClosed(this); } } /** * Answers a boolean indicating whether the main proposal popup is open. * * @return <code>true</code> if the proposal popup is open, and * <code>false</code> if it is not. * * @since 3.6 */ public boolean isProposalPopupOpen() { if (isValid() && isProposalPopupActive()) return true; return false; } /** * @return <code>true</code> if the content assistant has the completion * proposal popup open; <code>false</code> otherwise. */ private boolean isProposalPopupActive() { /* * Unfortunately, the method is protected so we use java reflection to * access it. */ try { final Method m = ContentAssistant.class .getDeclaredMethod("isProposalPopupActive"); m.setAccessible(true); try { final Object result = m.invoke(contentAssistant); if (result != null && result instanceof Boolean) { return (Boolean) result; } else { throw new IllegalStateException( "Method is expected to return boolean!"); } } catch (InvocationTargetException e) { throw e.getCause(); // cause was thrown by method m. } } catch (Throwable e) { e.printStackTrace(); return false; } } public void assistSessionStarted(ContentAssistEvent event) { notifyPopupOpened(); } public void assistSessionEnded(ContentAssistEvent event) { notifyPopupClosed(); } public void selectionChanged(ICompletionProposal proposal, boolean smartToggle) { // do nothing } }