Browse Source

Introducing CompletionProposalAdapter to delegate completion proposal request to the content assistant of the source viewer instead of registering a content assist handler via extension point. Enter and Escape keys are now handled with respect to the completion proposal popup state.

paphko@gmail.com 14 years ago
parent
commit
176c8662f6

+ 4 - 1
de.itemis.xtext.utils/plugins/de.itemis.xtext.utils.jface/META-INF/MANIFEST.MF

@@ -12,6 +12,9 @@ Require-Bundle: org.eclipse.core.runtime;bundle-version="[3.6.0,4.0.0)",
  org.eclipse.jface.text;bundle-version="[3.6.0,4.0.0)",
  org.eclipse.emf.common;bundle-version="[2.6.0,3.0.0)",
  org.eclipse.xtext.ui;bundle-version="[2.0.0,3.0.0)"
-Export-Package: de.itemis.xtext.utils.jface.viewers,
+Export-Package: de.itemis.utils.jface.viewers,
+ de.itemis.xtext.utils.jface.fieldassist,
+ de.itemis.xtext.utils.jface.viewers,
  de.itemis.xtext.utils.jface.viewers.util
 Bundle-ActivationPolicy: lazy
+Bundle-ClassPath: .

+ 0 - 16
de.itemis.xtext.utils/plugins/de.itemis.xtext.utils.jface/plugin.xml

@@ -1,26 +1,10 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <?eclipse version="3.4"?>
 <plugin>
-   <extension
-         point="org.eclipse.ui.handlers">
-      <handler
-            class="de.itemis.xtext.utils.jface.internal.handlers.ContentAssistHandler"
-            commandId="org.eclipse.ui.edit.text.contentAssist.proposals">
-         <activeWhen>
-            <with
-                  variable="activeFocusControlId">
-               <equals
-                     value="org.eclipse.swt.custom.StyledText">
-               </equals>
-            </with>
-         </activeWhen>
-      </handler>
-   </extension>
    <extension
          point="org.eclipse.ui.startup">
       <startup
             class="de.itemis.xtext.utils.jface.viewers.util.ActiveEditorTracker">
       </startup>
    </extension>
-
 </plugin>

+ 9 - 1
de.itemis.xtext.utils/plugins/de.itemis.xtext.utils.jface/src/de/itemis/xtext/utils/jface/viewers/StyledTextCellEditor.java

@@ -10,7 +10,7 @@
  *     IBM Corporation - initial API and implementation
  *     Tom Eicher <eclipse@tom.eicher.name> - fix minimum width
  *******************************************************************************/
-package de.itemis.xtext.utils.jface.viewers;
+package de.itemis.utils.jface.viewers;
 
 import java.text.MessageFormat;
 
@@ -309,6 +309,10 @@ public class StyledTextCellEditor extends CellEditor {
 		if (text == null || text.isDisposed()) {
 			return false;
 		}
+		/*
+		 * TODO: check that semantics remains the same as original:
+		 * text.getCaretPosition()
+		 */
 		return text.getSelectionCount() > 0
 				|| text.getCaretOffset() < text.getCharCount();
 	}
@@ -423,6 +427,10 @@ public class StyledTextCellEditor extends CellEditor {
 			text.insert(""); //$NON-NLS-1$
 		} else {
 			// remove the next character
+			/*
+			 * TODO: check that semantics remains the same as original:
+			 * text.getCaretPosition()
+			 */
 			int pos = text.getCaretOffset();
 			if (pos < text.getCharCount()) {
 				text.setSelection(pos, pos + 1);

+ 589 - 0
de.itemis.xtext.utils/plugins/de.itemis.xtext.utils.jface/src/de/itemis/xtext/utils/jface/fieldassist/CompletionProposalAdapter.java

@@ -0,0 +1,589 @@
+package de.itemis.xtext.utils.jface.fieldassist;
+
+import org.eclipse.core.runtime.ListenerList;
+import org.eclipse.jface.bindings.keys.KeyStroke;
+import org.eclipse.jface.fieldassist.ContentProposalAdapter;
+import org.eclipse.jface.fieldassist.IContentProposalListener2;
+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 koenemann
+ * 
+ */
+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;
+
+	/*
+	 * Indicates whether the content proposal is currently open or not.
+	 */
+	private boolean popup;
+	
+	/*
+	 * 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.
+	 * </p>
+	 * 
+	 * @param listener
+	 *            the IContentProposalListener2 to be added as a listener. Must
+	 *            not be <code>null</code>. If an attempt is made to register
+	 *            an instance which is already registered with this instance,
+	 *            this method has no effect.
+	 * 
+	 * @since 3.3
+	 * @see org.eclipse.jface.fieldassist.IContentProposalListener2
+	 */
+	public void addContentProposalListener(IContentProposalListener2 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.
+	 * </p>
+	 * 
+	 * @param listener
+	 *            the IContentProposalListener2 to be removed as a listener.
+	 *            Must not be <code>null</code>. If the listener has not
+	 *            already been registered, this method has no effect.
+	 * 
+	 * @since 3.3
+	 * @see org.eclipse.jface.fieldassist.IContentProposalListener2
+	 */
+	public void removeContentProposalListener(IContentProposalListener2 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++) {
+			((IContentProposalListener2) listenerArray[i])
+					.proposalPopupOpened(null);
+		}
+	}
+
+	/*
+	 * 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++) {
+			((IContentProposalListener2) listenerArray[i])
+					.proposalPopupClosed(null);
+		}
+	}
+
+	/**
+	 * 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() && popup)
+			return true;
+		return false;
+	}
+
+	public void assistSessionStarted(ContentAssistEvent event) {
+		if (!popup) {
+			popup = true;
+			notifyPopupOpened();
+		}
+	}
+
+	public void assistSessionEnded(ContentAssistEvent event) {
+		if (popup) {
+			popup = false;
+			notifyPopupClosed();
+		}
+	}
+
+	public void selectionChanged(ICompletionProposal proposal,
+			boolean smartToggle) {
+		// do nothing
+	}
+
+}

+ 28 - 0
de.itemis.xtext.utils/plugins/de.itemis.xtext.utils.jface/src/de/itemis/xtext/utils/jface/fieldassist/ICompletionProposalListener.java

@@ -0,0 +1,28 @@
+package de.itemis.xtext.utils.jface.fieldassist;
+
+import org.eclipse.jface.fieldassist.ContentProposalAdapter;
+
+/**
+ * This interface is used to listen to additional notifications from a
+ * {@link CompletionProposalAdapter}.
+ *
+ */
+public interface ICompletionProposalListener {
+	/**
+	 * A completion proposal popup has been opened.
+	 * 
+	 * @param adapter
+	 *            the CompletionProposalAdapter which is adapting content proposal
+	 *            behavior to a control
+	 */
+	public void proposalPopupOpened(ContentProposalAdapter adapter);
+
+	/**
+	 * A completion proposal popup has been closed.
+	 * 
+	 * @param adapter
+	 *            the CompletionProposalAdapter which is adapting content proposal
+	 *            behavior to a control
+	 */
+	public void proposalPopupClosed(ContentProposalAdapter adapter);
+}

+ 0 - 25
de.itemis.xtext.utils/plugins/de.itemis.xtext.utils.jface/src/de/itemis/xtext/utils/jface/internal/handlers/ContentAssistHandler.java

@@ -1,25 +0,0 @@
-package de.itemis.xtext.utils.jface.internal.handlers;
-
-import org.eclipse.core.commands.AbstractHandler;
-import org.eclipse.core.commands.ExecutionEvent;
-import org.eclipse.core.commands.ExecutionException;
-import org.eclipse.jface.text.source.ISourceViewer;
-import org.eclipse.swt.widgets.Control;
-import org.eclipse.swt.widgets.Display;
-
-import de.itemis.xtext.utils.jface.viewers.XtextStyledText;
-
-public class ContentAssistHandler extends AbstractHandler {
-	
-	public Object execute(ExecutionEvent event) throws ExecutionException {
-		Control control = Display.getCurrent().getFocusControl();
-		if (control != null) {
-			final XtextStyledText xtextStyledText = (XtextStyledText) control.getData(XtextStyledText.class.getCanonicalName());
-			if (xtextStyledText != null) {
-				xtextStyledText.getSourceviewer().doOperation(ISourceViewer.CONTENTASSIST_PROPOSALS);
-			}
-		}
-		return null;
-	}
-
-}

+ 226 - 0
de.itemis.xtext.utils/plugins/de.itemis.xtext.utils.jface/src/de/itemis/xtext/utils/jface/viewers/SnippetTextCellEditorWithContentProposal.java

@@ -0,0 +1,226 @@
+package de.itemis.xtext.utils.jface.viewers;
+
+import org.eclipse.jface.bindings.keys.IKeyLookup;
+import org.eclipse.jface.bindings.keys.KeyLookupFactory;
+import org.eclipse.jface.bindings.keys.KeyStroke;
+import org.eclipse.jface.fieldassist.ContentProposalAdapter;
+import org.eclipse.jface.fieldassist.IContentProposalListener2;
+import org.eclipse.jface.fieldassist.IContentProposalProvider;
+import org.eclipse.jface.fieldassist.SimpleContentProposalProvider;
+import org.eclipse.jface.fieldassist.TextContentAdapter;
+import org.eclipse.jface.viewers.ArrayContentProvider;
+import org.eclipse.jface.viewers.CellEditor;
+import org.eclipse.jface.viewers.ColumnLabelProvider;
+import org.eclipse.jface.viewers.ColumnViewerEditor;
+import org.eclipse.jface.viewers.ColumnViewerEditorActivationEvent;
+import org.eclipse.jface.viewers.ColumnViewerEditorActivationStrategy;
+import org.eclipse.jface.viewers.EditingSupport;
+import org.eclipse.jface.viewers.FocusCellHighlighter;
+import org.eclipse.jface.viewers.FocusCellOwnerDrawHighlighter;
+import org.eclipse.jface.viewers.TableViewer;
+import org.eclipse.jface.viewers.TableViewerColumn;
+import org.eclipse.jface.viewers.TableViewerEditor;
+import org.eclipse.jface.viewers.TableViewerFocusCellManager;
+import org.eclipse.jface.viewers.TextCellEditor;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.layout.FillLayout;
+import org.eclipse.swt.widgets.Composite;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Shell;
+import org.eclipse.swt.widgets.Table;
+
+/**
+ * Shows how to enable content assist in a text cell editor.
+ * 
+ * @author Mario Winterer
+ */
+public class SnippetTextCellEditorWithContentProposal {
+	private static class Color {
+		public String name;
+
+		public Color(String name) {
+			this.name = name;
+		}
+
+		@Override
+		public String toString() {
+			return name;
+		}
+	}
+
+	public static class TextCellEditorWithContentProposal extends
+			TextCellEditor {
+
+		private ContentProposalAdapter contentProposalAdapter;
+		private boolean popupOpen = false; // true, iff popup is currently open
+
+		public TextCellEditorWithContentProposal(Composite parent,
+				IContentProposalProvider contentProposalProvider,
+				KeyStroke keyStroke, char[] autoActivationCharacters) {
+			super(parent);
+
+			enableContentProposal(contentProposalProvider, keyStroke,
+					autoActivationCharacters);
+		}
+
+		private void enableContentProposal(
+				IContentProposalProvider contentProposalProvider,
+				KeyStroke keyStroke, char[] autoActivationCharacters) {
+			contentProposalAdapter = new ContentProposalAdapter(text,
+					new TextContentAdapter(), contentProposalProvider,
+					keyStroke, autoActivationCharacters);
+
+			// Listen for popup open/close events to be able to handle focus
+			// events correctly
+			contentProposalAdapter
+					.addContentProposalListener(new IContentProposalListener2() {
+
+						public void proposalPopupClosed(
+								ContentProposalAdapter adapter) {
+							popupOpen = false;
+						}
+
+						public void proposalPopupOpened(
+								ContentProposalAdapter adapter) {
+							popupOpen = true;
+						}
+					});
+		}
+
+		/**
+		 * Return the {@link ContentProposalAdapter} of this cell editor.
+		 * 
+		 * @return the {@link ContentProposalAdapter}
+		 */
+		public ContentProposalAdapter getContentProposalAdapter() {
+			return contentProposalAdapter;
+		}
+
+		@Override
+		protected void focusLost() {
+			if (!popupOpen) {
+				// Focus lost deactivates the cell editor.
+				// This must not happen if focus lost was caused by activating
+				// the completion proposal popup.
+				super.focusLost();
+			}
+		}
+
+		@Override
+		protected boolean dependsOnExternalFocusListener() {
+			// Always return false;
+			// Otherwise, the ColumnViewerEditor will install an additional
+			// focus listener
+			// that cancels cell editing on focus lost, even if focus gets lost
+			// due to
+			// activation of the completion proposal popup. See also bug 58777.
+			return false;
+		}
+	}
+
+	private static class ColorNameEditingSupport extends EditingSupport {
+		private TextCellEditorWithContentProposal cellEditor;
+
+		public ColorNameEditingSupport(TableViewer viewer) {
+			super(viewer);
+
+			IContentProposalProvider contentProposalProvider = new SimpleContentProposalProvider(
+					new String[] { "red", "green", "blue" });
+			cellEditor = new TextCellEditorWithContentProposal(
+					viewer.getTable(), contentProposalProvider, null, null);
+		}
+
+		@Override
+		protected boolean canEdit(Object element) {
+			return (element instanceof Color);
+		}
+
+		@Override
+		protected CellEditor getCellEditor(Object element) {
+			return cellEditor;
+		}
+
+		@Override
+		protected Object getValue(Object element) {
+			return ((Color) element).name;
+		}
+
+		@Override
+		protected void setValue(Object element, Object value) {
+			((Color) element).name = value.toString();
+			getViewer().update(element, null);
+		}
+
+	}
+
+	public SnippetTextCellEditorWithContentProposal(Shell shell) {
+		final TableViewer viewer = new TableViewer(shell, SWT.BORDER
+				| SWT.FULL_SELECTION);
+		final Table table = viewer.getTable();
+		table.setLinesVisible(true);
+		table.setHeaderVisible(true);
+
+		final TableViewerColumn colorColumn = new TableViewerColumn(viewer,
+				SWT.LEFT);
+		colorColumn.getColumn().setText("Color name");
+		colorColumn.getColumn().setWidth(200);
+		colorColumn.setLabelProvider(new ColumnLabelProvider());
+		colorColumn.setEditingSupport(new ColorNameEditingSupport(viewer));
+
+		viewer.setContentProvider(new ArrayContentProvider());
+
+		ColumnViewerEditorActivationStrategy activationSupport = new ColumnViewerEditorActivationStrategy(
+				viewer) {
+			@Override
+			protected boolean isEditorActivationEvent(
+					ColumnViewerEditorActivationEvent event) {
+				return event.eventType == ColumnViewerEditorActivationEvent.TRAVERSAL
+						|| event.eventType == ColumnViewerEditorActivationEvent.MOUSE_DOUBLE_CLICK_SELECTION
+						|| event.eventType == ColumnViewerEditorActivationEvent.PROGRAMMATIC
+						|| (event.eventType == ColumnViewerEditorActivationEvent.KEY_PRESSED && event.keyCode == KeyLookupFactory
+								.getDefault().formalKeyLookup(
+										IKeyLookup.ENTER_NAME));
+			}
+		};
+		activationSupport.setEnableEditorActivationWithKeyboard(true);
+
+		/*
+		 * Without focus highlighter, keyboard events will not be delivered to
+		 * ColumnViewerEditorActivationStragety#isEditorActivationEvent(...)
+		 * (see above)
+		 */
+		FocusCellHighlighter focusCellHighlighter = new FocusCellOwnerDrawHighlighter(
+				viewer);
+		TableViewerFocusCellManager focusCellManager = new TableViewerFocusCellManager(
+				viewer, focusCellHighlighter);
+
+		TableViewerEditor.create(viewer, focusCellManager, activationSupport,
+				ColumnViewerEditor.TABBING_VERTICAL
+						| ColumnViewerEditor.KEYBOARD_ACTIVATION);
+
+		viewer.setInput(createModel());
+	}
+
+	private Color[] createModel() {
+		return new Color[] { new Color("red"), new Color("green") };
+	}
+
+	/**
+	 * @param args
+	 */
+	public static void main(String[] args) {
+		Display display = new Display();
+
+		Shell shell = new Shell(display);
+		shell.setLayout(new FillLayout());
+		new SnippetTextCellEditorWithContentProposal(shell);
+		shell.open();
+
+		while (!shell.isDisposed()) {
+			if (!display.readAndDispatch())
+				display.sleep();
+		}
+
+		display.dispose();
+	}
+}

+ 46 - 27
de.itemis.xtext.utils/plugins/de.itemis.xtext.utils.jface/src/de/itemis/xtext/utils/jface/viewers/XtextCellEditor.java

@@ -14,21 +14,29 @@ package de.itemis.xtext.utils.jface.viewers;
 import java.util.List;
 
 import org.eclipse.emf.ecore.resource.Resource;
+import org.eclipse.jface.bindings.keys.KeyStroke;
+import org.eclipse.jface.text.contentassist.IContentAssistant;
 import org.eclipse.jface.text.source.SourceViewer;
 import org.eclipse.jface.viewers.CellEditor;
 import org.eclipse.jface.viewers.StructuredViewer;
+import org.eclipse.swt.SWT;
 import org.eclipse.swt.custom.StyledText;
 import org.eclipse.swt.events.FocusAdapter;
 import org.eclipse.swt.events.FocusEvent;
-import org.eclipse.swt.events.KeyEvent;
-import org.eclipse.swt.events.KeyListener;
 import org.eclipse.swt.widgets.Composite;
 import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
 import org.eclipse.xtext.ui.editor.XtextEditor;
+import org.eclipse.xtext.ui.editor.XtextSourceViewer;
 import org.eclipse.xtext.validation.Issue;
 
 import com.google.inject.Injector;
 
+import de.itemis.utils.jface.viewers.StyledTextCellEditor;
+import de.itemis.xtext.utils.jface.fieldassist.CompletionProposalAdapter;
+
+
 /**
  * This class integrates Xtext features into a {@link CellEditor} and can be used e.g. in 
  * jFace {@link StructuredViewer}s or in GMF EditParts via DirectEditManager.
@@ -52,29 +60,6 @@ public class XtextCellEditor extends StyledTextCellEditor {
 	
 	private Resource context;
 	
-	/**
-	 * Key listener for updating, applying, and canceling cell editor.
-	 */
-	private final KeyListener keyListener = new KeyListener() {
-
-		public void keyPressed(KeyEvent e) {
-//			if (e.keyCode == SWT.CR && ((text.getStyle() & SWT.MULTI) == 0 || 
-//					(e.stateMask & SWT.CTRL) != 0)) {
-//				// apply value to cell editor and finish editing
-//				XtextCellEditor.this.fireApplyEditorValue();
-//			} else if (e.keyCode == SWT.ESC) {
-//				// cancel editor
-//				XtextCellEditor.this.fireCancelEditor();
-//			} else {
-//				// notify cell editor about changes
-//				XtextCellEditor.this.valueChanged(true, true);
-//			}
-		}
-
-		public void keyReleased(KeyEvent e) {
-		}
-	};
-
 	/**
 	 * C'tor to create a new Instance.
 	 * 
@@ -111,26 +96,60 @@ public class XtextCellEditor extends StyledTextCellEditor {
 	protected Control createControl(Composite parent) {
 		xtextWidget = createXtextWidget(parent, getStyle(), injector, context);
 		text = xtextWidget.getStyledText();
-		text.addKeyListener(keyListener);
 		text.addFocusListener(new FocusAdapter() {
 			public void focusLost(FocusEvent e) {
 				XtextCellEditor.this.focusLost();
 			}
 		});
+		
+		final XtextSourceViewer sourceViewer = xtextWidget.getSourceviewer();
+		final IContentAssistant contentAssistant = sourceViewer.getContentAssistant();
+		new CompletionProposalAdapter(text, contentAssistant, KeyStroke.getInstance(SWT.CTRL, SWT.SPACE), null);
+
+		if ((text.getStyle() & SWT.SINGLE) != 0) {
+			
+			// The regular key down event is too late (after popup is closed again).
+			// when using the StyledText.VerifyKey event (3005), we get the event early enough!
+			text.addListener(3005, new Listener() {
+				public void handleEvent(Event event) {
+					if (event.character == SWT.CR && !xtextWidget.isProposalPopupActive()) {
+//						System.err.println("handle event (CR), " + event.doit + " -> " + event);
+						XtextCellEditor.this.fireApplyEditorValue();
+					} else if (event.character == SWT.ESC && !xtextWidget.isProposalPopupActive()) {
+//						System.err.println("handle event (ESC), " + event.doit + " -> " + event);
+						XtextCellEditor.this.fireCancelEditor();
+					}
+				}
+			});
+		}
+		
 		return text;
 	}
 
+	@Override
+	protected void focusLost() {
+		if (!xtextWidget.isProposalPopupActive())
+			super.focusLost();
+	}
+	
 	protected XtextStyledText createXtextWidget(Composite parent, int style, Injector injector, Resource context) {
 		return new XtextStyledText(parent, style, injector, context);
 	}
 
 	@Override
 	public void dispose() {
-		text.removeKeyListener(keyListener);
 		xtextWidget.dispose();
 		super.dispose();
 	}
 
+	/**
+	 * This is damn important! If we don't return false here, the ColumnEditorViewer calls applyEditorValue on FocusLostEvents!
+	 */
+	@Override
+	protected boolean dependsOnExternalFocusListener() {
+		return false;
+	}
+	
 	public void setContext(Resource context) {
 		xtextWidget.setContextResource(context);
 	}

+ 42 - 5
de.itemis.xtext.utils/plugins/de.itemis.xtext.utils.jface/src/de/itemis/xtext/utils/jface/viewers/XtextStyledText.java

@@ -1,5 +1,7 @@
 package de.itemis.xtext.utils.jface.viewers;
 
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -9,7 +11,10 @@ import org.eclipse.emf.common.util.URI;
 import org.eclipse.emf.ecore.resource.Resource;
 import org.eclipse.emf.ecore.resource.ResourceSet;
 import org.eclipse.emf.ecore.util.EcoreUtil;
+import org.eclipse.jface.bindings.keys.KeyStroke;
 import org.eclipse.jface.text.IDocumentPartitioner;
+import org.eclipse.jface.text.contentassist.ContentAssistant;
+import org.eclipse.jface.text.contentassist.IContentAssistant;
 import org.eclipse.jface.text.source.AnnotationModel;
 import org.eclipse.jface.text.source.ICharacterPairMatcher;
 import org.eclipse.jface.text.source.SourceViewer;
@@ -52,6 +57,7 @@ import com.google.inject.Injector;
 import com.google.inject.Provider;
 import com.google.inject.name.Named;
 
+import de.itemis.xtext.utils.jface.fieldassist.CompletionProposalAdapter;
 import de.itemis.xtext.utils.jface.viewers.util.ActiveEditorTracker;
 
 /**
@@ -243,13 +249,17 @@ public class XtextStyledText {
 		styledText.setBackground(parent.getBackground());
 		styledText.setText("");
 		
+		final IContentAssistant contentAssistant = sourceviewer.getContentAssistant();
+		new CompletionProposalAdapter(styledText, contentAssistant, KeyStroke.getInstance(SWT.CTRL, SWT.SPACE), null);
+
 		if ((styledText.getStyle() & SWT.SINGLE) != 0) {
-			// Add listener to send DefaultSelection event when ENTER is pressed.
-			// This will emulate the behavior of the Text widget.
-			styledText.addListener(SWT.KeyDown, new Listener() {
-				
+			
+			// The regular key down event is too late (after popup is closed).
+			// when using the StyledText.VerifyKey event (3005), we get the event early enough!
+			styledText.addListener(3005, new Listener() {
 				public void handleEvent(Event event) {
-					if (event.keyCode == SWT.CR) {
+					if (event.character == SWT.CR && !isProposalPopupActive()) {
+//						System.err.println("handle event, " + event.doit + " -> " + event);
 						Event selectionEvent = new Event();
 						selectionEvent.type = SWT.DefaultSelection;
 						selectionEvent.widget = event.widget;
@@ -413,4 +423,31 @@ public class XtextStyledText {
 	public void setVisibleRegion(int start, int length) {
 		sourceviewer.setVisibleRegion(start, length);
 	}
+	
+	/**
+	 * @return <code>true</code> if the content assistant has the completion proposal popup open; <code>false</code> otherwise.
+	 */
+	public boolean isProposalPopupActive() {
+		/*
+		 * Unfortunately, the method is protected so we use java reflection to access it.
+		 */
+		final IContentAssistant contentAssistant = sourceviewer.getContentAssistant();
+		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;
+		}
+	}
 }