/******************************************************************************* * Copyright (c) 2012 Pivotal Software, Inc. * 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: * Pivotal Software, Inc. - initial API and implementation *******************************************************************************/ package org.springsource.ide.eclipse.commons.frameworks.ui.internal.contentassist; import java.util.ArrayList; import org.eclipse.core.runtime.Assert; import org.eclipse.core.runtime.ListenerList; import org.eclipse.jface.bindings.keys.KeyStroke; import org.eclipse.jface.dialogs.PopupDialog; import org.eclipse.jface.fieldassist.IContentProposal; import org.eclipse.jface.fieldassist.IContentProposalListener; import org.eclipse.jface.fieldassist.IContentProposalProvider; import org.eclipse.jface.fieldassist.IControlContentAdapter; import org.eclipse.jface.fieldassist.IControlContentAdapter2; import org.eclipse.jface.fieldassist.SimpleContentProposalProvider; import org.eclipse.jface.preference.JFacePreferences; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.util.Util; import org.eclipse.jface.viewers.ILabelProvider; import org.eclipse.swt.SWT; import org.eclipse.swt.events.DisposeEvent; import org.eclipse.swt.events.DisposeListener; import org.eclipse.swt.events.FocusAdapter; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.widgets.Combo; 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.swt.widgets.ScrollBar; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableItem; import org.eclipse.swt.widgets.Text; /** * ContentProposalAdapter can be used to attach content proposal behavior to a * control. This behavior includes obtaining proposals, opening a popup dialog, * managing the content of the control relative to the selections in the popup, * and optionally opening up a secondary popup to further describe proposals. * <p> * A number of configurable options are provided to determine how the control * content is altered when a proposal is chosen, how the content proposal popup * is activated, and whether any filtering should be done on the proposals as * the user types characters. * <p> * This class provides some overridable methods to allow clients to manually * control the popup. However, most of the implementation remains private. * @author Kris De Volder * @author Nieraj Singh * @since 3.2 */ public class ContentProposalAdapter { /* * The lightweight popup used to show content proposals for a text field. If * additional information exists for a proposal, then selecting that * proposal will result in the information being displayed in a secondary * popup. */ class ContentProposalPopup extends PopupDialog { /* * The listener we install on the popup and related controls to * determine when to close the popup. Some events (move, resize, close, * deactivate) trigger closure as soon as they are received, simply * because one of the registered listeners received them. Other events * depend on additional circumstances. */ private final class PopupCloserListener implements Listener { private boolean scrollbarClicked = false; public void handleEvent(final Event e) { // If focus is leaving an important widget or the field's // shell is deactivating if (e.type == SWT.FocusOut) { scrollbarClicked = false; /* * Ignore this event if it's only happening because focus is * moving between the popup shells, their controls, or a * scrollbar. Do this in an async since the focus is not * actually switched when this event is received. */ e.display.asyncExec(new Runnable() { public void run() { if (isValid()) { if (scrollbarClicked || hasFocus()) { return; } // Workaround a problem on X and Mac, whereby at // this point, the focus control is not known. // This can happen, for example, when resizing // the popup shell on the Mac. // Check the active shell. Shell activeShell = e.display.getActiveShell(); if (activeShell == getShell() || (infoPopup != null && infoPopup .getShell() == activeShell)) { return; } /* * System.out.println(e); * System.out.println(e.display.getFocusControl()); * System.out.println(e.display.getActiveShell()); */ close(); } } }); return; } // Scroll bar has been clicked. Remember this for focus event // processing. if (e.type == SWT.Selection) { scrollbarClicked = true; return; } // For all other events, merely getting them dictates closure. /* STS Change: new code: */ e.display.asyncExec(new Runnable() { public void run() { close(); } }); /* STS Change: old code: close(); */ } // Install the listeners for events that need to be monitored for // popup closure. void installListeners() { // Listeners on this popup's table and scroll bar proposalTable.addListener(SWT.FocusOut, this); ScrollBar scrollbar = proposalTable.getVerticalBar(); if (scrollbar != null) { scrollbar.addListener(SWT.Selection, this); } // Listeners on this popup's shell getShell().addListener(SWT.Deactivate, this); getShell().addListener(SWT.Close, this); // Listeners on the target control control.addListener(SWT.MouseDoubleClick, this); control.addListener(SWT.MouseDown, this); control.addListener(SWT.Dispose, this); control.addListener(SWT.FocusOut, this); // Listeners on the target control's shell Shell controlShell = control.getShell(); controlShell.addListener(SWT.Move, this); controlShell.addListener(SWT.Resize, this); } // Remove installed listeners void removeListeners() { if (isValid()) { proposalTable.removeListener(SWT.FocusOut, this); ScrollBar scrollbar = proposalTable.getVerticalBar(); if (scrollbar != null) { scrollbar.removeListener(SWT.Selection, this); } getShell().removeListener(SWT.Deactivate, this); getShell().removeListener(SWT.Close, this); } if (control != null && !control.isDisposed()) { control.removeListener(SWT.MouseDoubleClick, this); control.removeListener(SWT.MouseDown, this); control.removeListener(SWT.Dispose, this); control.removeListener(SWT.FocusOut, this); Shell controlShell = control.getShell(); controlShell.removeListener(SWT.Move, this); controlShell.removeListener(SWT.Resize, this); } } } /* * The listener we will install on the target control. */ private final class TargetControlListener implements Listener { // Key events from the control public void handleEvent(Event e) { if (!isValid()) { return; } char key = e.character; // Traverse events are handled depending on whether the // event has a character. if (e.type == SWT.Traverse) { // If the traverse event contains a legitimate character, // then we must set doit false so that the widget will // receive the key event. We return immediately so that // the character is handled only in the key event. // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=132101 if (key != 0) { e.doit = false; return; } // Traversal does not contain a character. Set doit true // to indicate TRAVERSE_NONE will occur and that no key // event will be triggered. We will check for navigation // keys below. e.detail = SWT.TRAVERSE_NONE; e.doit = true; } else { // Default is to only propagate when configured that way. // Some keys will always set doit to false anyway. e.doit = propagateKeys; } // No character. Check for navigation keys. if (key == 0) { int newSelection = proposalTable.getSelectionIndex(); int visibleRows = (proposalTable.getSize().y / proposalTable .getItemHeight()) - 1; switch (e.keyCode) { case SWT.ARROW_UP: newSelection -= 1; if (newSelection < 0) { newSelection = proposalTable.getItemCount() - 1; } // Not typical - usually we get this as a Traverse and // therefore it never propagates. Added for consistency. if (e.type == SWT.KeyDown) { // don't propagate to control e.doit = false; } break; case SWT.ARROW_DOWN: newSelection += 1; if (newSelection > proposalTable.getItemCount() - 1) { newSelection = 0; } // Not typical - usually we get this as a Traverse and // therefore it never propagates. Added for consistency. if (e.type == SWT.KeyDown) { // don't propagate to control e.doit = false; } break; case SWT.PAGE_DOWN: newSelection += visibleRows; if (newSelection >= proposalTable.getItemCount()) { newSelection = proposalTable.getItemCount() - 1; } if (e.type == SWT.KeyDown) { // don't propagate to control e.doit = false; } break; case SWT.PAGE_UP: newSelection -= visibleRows; if (newSelection < 0) { newSelection = 0; } if (e.type == SWT.KeyDown) { // don't propagate to control e.doit = false; } break; case SWT.HOME: newSelection = 0; if (e.type == SWT.KeyDown) { // don't propagate to control e.doit = false; } break; case SWT.END: newSelection = proposalTable.getItemCount() - 1; if (e.type == SWT.KeyDown) { // don't propagate to control e.doit = false; } break; // If received as a Traverse, these should propagate // to the control as keydown. If received as a keydown, // proposals should be recomputed since the cursor // position has changed. case SWT.ARROW_LEFT: case SWT.ARROW_RIGHT: if (e.type == SWT.Traverse) { e.doit = false; } else { e.doit = true; String contents = getControlContentAdapter() .getControlContents(getControl()); // If there are no contents, changes in cursor // position have no effect. Note also that we do // not affect the filter text on ARROW_LEFT as // we would with BS. if (contents.length() > 0) { asyncRecomputeProposals(filterText); } } break; // Any unknown keycodes will cause the popup to close. // Modifier keys are explicitly checked and ignored because // they are not complete yet (no character). default: if (e.keyCode != SWT.CAPS_LOCK && e.keyCode != SWT.NUM_LOCK && e.keyCode != SWT.MOD1 && e.keyCode != SWT.MOD2 && e.keyCode != SWT.MOD3 && e.keyCode != SWT.MOD4) { close(); } return; } // If any of these navigation events caused a new selection, // then handle that now and return. if (newSelection >= 0) { selectProposal(newSelection); } return; } // key != 0 // Check for special keys involved in cancelling, accepting, or // filtering the proposals. switch (key) { case SWT.ESC: e.doit = false; close(); break; case SWT.LF: case SWT.CR: e.doit = false; Object p = getSelectedProposal(); if (p != null) { acceptCurrentProposal(); } else { close(); } break; case SWT.TAB: e.doit = false; getShell().setFocus(); return; case SWT.BS: // Backspace should back out of any stored filter text if (filterStyle != FILTER_NONE) { // We have no filter to back out of, so do nothing if (filterText.length() == 0) { return; } // There is filter to back out of filterText = filterText.substring(0, filterText .length() - 1); asyncRecomputeProposals(filterText); return; } // There is no filtering provided by us, but some // clients provide their own filtering based on content. // Recompute the proposals if the cursor position // will change (is not at 0). int pos = getControlContentAdapter().getCursorPosition( getControl()); // We rely on the fact that the contents and pos do not yet // reflect the result of the BS. If the contents were // already empty, then BS should not cause // a recompute. if (pos > 0) { asyncRecomputeProposals(filterText); } break; default: // If the key is a defined unicode character, and not one of // the special cases processed above, update the filter text // and filter the proposals. if (Character.isDefined(key)) { if (filterStyle == FILTER_CUMULATIVE) { filterText = filterText + String.valueOf(key); } else if (filterStyle == FILTER_CHARACTER) { filterText = String.valueOf(key); } // Recompute proposals after processing this event. asyncRecomputeProposals(filterText); } break; } } } /* * Internal class used to implement the secondary popup. */ private class InfoPopupDialog extends PopupDialog { /* * The text control that displays the text. */ private Text text; /* * The String shown in the popup. */ private String contents = EMPTY; /* * Construct an info-popup with the specified parent. */ InfoPopupDialog(Shell parent) { super(parent, PopupDialog.HOVER_SHELLSTYLE, false, false, false, false, false, null, null); } /* * Create a text control for showing the info about a proposal. */ protected Control createDialogArea(Composite parent) { text = new Text(parent, SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.NO_FOCUS); // Use the compact margins employed by PopupDialog. GridData gd = new GridData(GridData.BEGINNING | GridData.FILL_BOTH); gd.horizontalIndent = PopupDialog.POPUP_HORIZONTALSPACING; gd.verticalIndent = PopupDialog.POPUP_VERTICALSPACING; text.setLayoutData(gd); text.setText(contents); // since SWT.NO_FOCUS is only a hint... text.addFocusListener(new FocusAdapter() { public void focusGained(FocusEvent event) { ContentProposalPopup.this.close(); } }); return text; } /* * Adjust the bounds so that we appear adjacent to our parent shell */ protected void adjustBounds() { Rectangle parentBounds = getParentShell().getBounds(); Rectangle proposedBounds; // Try placing the info popup to the right Rectangle rightProposedBounds = new Rectangle(parentBounds.x + parentBounds.width + PopupDialog.POPUP_HORIZONTALSPACING, parentBounds.y + PopupDialog.POPUP_VERTICALSPACING, parentBounds.width, parentBounds.height); rightProposedBounds = getConstrainedShellBounds(rightProposedBounds); // If it won't fit on the right, try the left if (rightProposedBounds.intersects(parentBounds)) { Rectangle leftProposedBounds = new Rectangle(parentBounds.x - parentBounds.width - POPUP_HORIZONTALSPACING - 1, parentBounds.y, parentBounds.width, parentBounds.height); leftProposedBounds = getConstrainedShellBounds(leftProposedBounds); // If it won't fit on the left, choose the proposed bounds // that fits the best if (leftProposedBounds.intersects(parentBounds)) { if (rightProposedBounds.x - parentBounds.x >= parentBounds.x - leftProposedBounds.x) { rightProposedBounds.x = parentBounds.x + parentBounds.width + PopupDialog.POPUP_HORIZONTALSPACING; proposedBounds = rightProposedBounds; } else { leftProposedBounds.width = parentBounds.x - POPUP_HORIZONTALSPACING - leftProposedBounds.x; proposedBounds = leftProposedBounds; } } else { // use the proposed bounds on the left proposedBounds = leftProposedBounds; } } else { // use the proposed bounds on the right proposedBounds = rightProposedBounds; } getShell().setBounds(proposedBounds); } /* * (non-Javadoc) * @see org.eclipse.jface.dialogs.PopupDialog#getForeground() */ protected Color getForeground() { return control.getDisplay(). getSystemColor(SWT.COLOR_INFO_FOREGROUND); } /* * (non-Javadoc) * @see org.eclipse.jface.dialogs.PopupDialog#getBackground() */ protected Color getBackground() { return control.getDisplay(). getSystemColor(SWT.COLOR_INFO_BACKGROUND); } /* * Set the text contents of the popup. */ void setContents(String newContents) { if (newContents == null) { newContents = EMPTY; } this.contents = newContents; if (text != null && !text.isDisposed()) { text.setText(contents); } } /* * Return whether the popup has focus. */ boolean hasFocus() { if (text == null || text.isDisposed()) { return false; } return text.getShell().isFocusControl() || text.isFocusControl(); } } /* * The listener installed on the target control. */ private Listener targetControlListener; /* * The listener installed in order to close the popup. */ private PopupCloserListener popupCloser; /* * The table used to show the list of proposals. */ private Table proposalTable; /* * The proposals to be shown (cached to avoid repeated requests). */ private IContentProposal[] proposals; /* * Secondary popup used to show detailed information about the selected * proposal.. */ private InfoPopupDialog infoPopup; /* * Flag indicating whether there is a pending secondary popup update. */ private boolean pendingDescriptionUpdate = false; /* * Filter text - tracked while popup is open, only if we are told to * filter */ private String filterText = EMPTY; /** * Constructs a new instance of this popup, specifying the control for * which this popup is showing content, and how the proposals should be * obtained and displayed. * * @param infoText * Text to be shown in a lower info area, or * <code>null</code> if there is no info area. */ ContentProposalPopup(String infoText, IContentProposal[] proposals) { // IMPORTANT: Use of SWT.ON_TOP is critical here for ensuring // that the target control retains focus on Mac and Linux. Without // it, the focus will disappear, keystrokes will not go to the // popup, and the popup closer will wrongly close the popup. // On platforms where SWT.ON_TOP overrides SWT.RESIZE, we will live // with this. // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=126138 super(control.getShell(), SWT.RESIZE | SWT.ON_TOP, false, false, false, false, false, null, infoText); this.proposals = proposals; } /* * (non-Javadoc) * @see org.eclipse.jface.dialogs.PopupDialog#getForeground() */ protected Color getForeground() { return JFaceResources.getColorRegistry().get( JFacePreferences.CONTENT_ASSIST_FOREGROUND_COLOR); } /* * (non-Javadoc) * @see org.eclipse.jface.dialogs.PopupDialog#getBackground() */ protected Color getBackground() { return JFaceResources.getColorRegistry().get( JFacePreferences.CONTENT_ASSIST_BACKGROUND_COLOR); } /* * Creates the content area for the proposal popup. This creates a table * and places it inside the composite. The table will contain a list of * all the proposals. * * @param parent The parent composite to contain the dialog area; must * not be <code>null</code>. */ protected final Control createDialogArea(final Composite parent) { // Use virtual where appropriate (see flag definition). if (USE_VIRTUAL) { proposalTable = new Table(parent, SWT.H_SCROLL | SWT.V_SCROLL | SWT.VIRTUAL); Listener listener = new Listener() { public void handleEvent(Event event) { handleSetData(event); } }; proposalTable.addListener(SWT.SetData, listener); } else { proposalTable = new Table(parent, SWT.H_SCROLL | SWT.V_SCROLL); } // set the proposals to force population of the table. setProposals(filterProposals(proposals, filterText)); proposalTable.setHeaderVisible(false); proposalTable.addSelectionListener(new SelectionListener() { public void widgetSelected(SelectionEvent e) { // If a proposal has been selected, show it in the secondary // popup. Otherwise close the popup. if (e.item == null) { if (infoPopup != null) { infoPopup.close(); } } else { showProposalDescription(); } } // Default selection was made. Accept the current proposal. public void widgetDefaultSelected(SelectionEvent e) { acceptCurrentProposal(); } }); return proposalTable; } /* * (non-Javadoc) * * @see org.eclipse.jface.dialogs.PopupDialog.adjustBounds() */ protected void adjustBounds() { // Get our control's location in display coordinates. Point location = control.getDisplay().map(control.getParent(), null, control.getLocation()); int initialX = location.x + POPUP_OFFSET; int initialY = location.y + control.getSize().y + POPUP_OFFSET; // If we are inserting content, use the cursor position to // position the control. if (getProposalAcceptanceStyle() == PROPOSAL_INSERT) { Rectangle insertionBounds = controlContentAdapter .getInsertionBounds(control); initialX = initialX + insertionBounds.x; initialY = location.y + insertionBounds.y + insertionBounds.height; } // If there is no specified size, force it by setting // up a layout on the table. if (popupSize == null) { GridData data = new GridData(GridData.FILL_BOTH); data.heightHint = proposalTable.getItemHeight() * POPUP_CHAR_HEIGHT; data.widthHint = Math.max(control.getSize().x, POPUP_MINIMUM_WIDTH); proposalTable.setLayoutData(data); getShell().pack(); popupSize = getShell().getSize(); } // Constrain to the display Rectangle constrainedBounds = getConstrainedShellBounds(new Rectangle(initialX, initialY, popupSize.x, popupSize.y)); // If there has been an adjustment causing the popup to overlap // with the control, then put the popup above the control. if (constrainedBounds.y < initialY) getShell().setBounds(initialX, location.y - popupSize.y, popupSize.x, popupSize.y); else getShell().setBounds(initialX, initialY, popupSize.x, popupSize.y); // Now set up a listener to monitor any changes in size. getShell().addListener(SWT.Resize, new Listener() { public void handleEvent(Event e) { popupSize = getShell().getSize(); if (infoPopup != null) { infoPopup.adjustBounds(); } } }); } /* * Handle the set data event. Set the item data of the requested item to * the corresponding proposal in the proposal cache. */ private void handleSetData(Event event) { TableItem item = (TableItem) event.item; int index = proposalTable.indexOf(item); if (0 <= index && index < proposals.length) { IContentProposal current = proposals[index]; item.setText(getString(current)); item.setImage(getImage(current)); item.setData(current); } else { // this should not happen, but does on win32 } } /* * Caches the specified proposals and repopulates the table if it has * been created. */ private void setProposals(IContentProposal[] newProposals) { if (newProposals == null || newProposals.length == 0) { newProposals = getEmptyProposalArray(); } this.proposals = newProposals; // If there is a table if (isValid()) { final int newSize = newProposals.length; if (USE_VIRTUAL) { // Set and clear the virtual table. Data will be // provided in the SWT.SetData event handler. proposalTable.setItemCount(newSize); proposalTable.clearAll(); } else { // Populate the table manually proposalTable.setRedraw(false); proposalTable.setItemCount(newSize); TableItem[] items = proposalTable.getItems(); for (int i = 0; i < items.length; i++) { TableItem item = items[i]; IContentProposal proposal = newProposals[i]; item.setText(getString(proposal)); item.setImage(getImage(proposal)); item.setData(proposal); } proposalTable.setRedraw(true); } // Default to the first selection if there is content. if (newProposals.length > 0) { selectProposal(0); } else { // No selection, close the secondary popup if it was open if (infoPopup != null) { infoPopup.close(); } } } } /* * Get the string for the specified proposal. Always return a String of * some kind. */ private String getString(IContentProposal proposal) { if (proposal == null) { return EMPTY; } if (labelProvider == null) { return proposal.getLabel() == null ? proposal.getContent() : proposal.getLabel(); } return labelProvider.getText(proposal); } /* * Get the image for the specified proposal. If there is no image * available, return null. */ private Image getImage(IContentProposal proposal) { if (proposal == null || labelProvider == null) { return null; } return labelProvider.getImage(proposal); } /* * Return an empty array. Used so that something always shows in the * proposal popup, even if no proposal provider was specified. */ private IContentProposal[] getEmptyProposalArray() { return new IContentProposal[0]; } /* * Answer true if the popup is valid, which means the table has been * created and not disposed. */ private boolean isValid() { return proposalTable != null && !proposalTable.isDisposed(); } /* * Return whether the receiver has focus. Since 3.4, this includes a * check for whether the info popup has focus. */ private boolean hasFocus() { if (!isValid()) { return false; } if (getShell().isFocusControl() || proposalTable.isFocusControl()) { return true; } if (infoPopup != null && infoPopup.hasFocus()) { return true; } return false; } /* * Return the current selected proposal. */ private IContentProposal getSelectedProposal() { if (isValid()) { int i = proposalTable.getSelectionIndex(); if (proposals == null || i < 0 || i >= proposals.length) { return null; } return proposals[i]; } return null; } /* * Select the proposal at the given index. */ private void selectProposal(int index) { Assert .isTrue(index >= 0, "Proposal index should never be negative"); //$NON-NLS-1$ if (!isValid() || proposals == null || index >= proposals.length) { return; } proposalTable.setSelection(index); proposalTable.showSelection(); showProposalDescription(); } /** * Opens this ContentProposalPopup. This method is extended in order to * add the control listener when the popup is opened and to invoke the * secondary popup if applicable. * * @return the return code * * @see org.eclipse.jface.window.Window#open() */ public int open() { int value = super.open(); if (popupCloser == null) { popupCloser = new PopupCloserListener(); } popupCloser.installListeners(); IContentProposal p = getSelectedProposal(); if (p != null) { showProposalDescription(); } return value; } /** * Closes this popup. This method is extended to remove the control * listener. * * @return <code>true</code> if the window is (or was already) closed, * and <code>false</code> if it is still open */ public boolean close() { popupCloser.removeListeners(); if (infoPopup != null) { infoPopup.close(); } boolean ret = super.close(); notifyPopupClosed(); return ret; } /* * Show the currently selected proposal's description in a secondary * popup. */ private void showProposalDescription() { // If we do not already have a pending update, then // create a thread now that will show the proposal description if (!pendingDescriptionUpdate) { // Create a thread that will sleep for the specified delay // before creating the popup. We do not use Jobs since this // code must be able to run independently of the Eclipse // runtime. Runnable runnable = new Runnable() { public void run() { pendingDescriptionUpdate = true; try { Thread.sleep(POPUP_DELAY); } catch (InterruptedException e) { } if (!isValid()) { return; } getShell().getDisplay().syncExec(new Runnable() { public void run() { // Query the current selection since we have // been delayed IContentProposal p = getSelectedProposal(); if (p != null) { String description = p.getDescription(); if (description != null) { if (infoPopup == null) { infoPopup = new InfoPopupDialog( getShell()); infoPopup.open(); infoPopup .getShell() .addDisposeListener( new DisposeListener() { public void widgetDisposed( DisposeEvent event) { infoPopup = null; } }); } infoPopup.setContents(p .getDescription()); } else if (infoPopup != null) { infoPopup.close(); } pendingDescriptionUpdate = false; } } }); } }; Thread t = new Thread(runnable); t.start(); } } /* * Accept the current proposal. */ private void acceptCurrentProposal() { // Close before accepting the proposal. This is important // so that the cursor position can be properly restored at // acceptance, which does not work without focus on some controls. // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=127108 IContentProposal proposal = getSelectedProposal(); close(); proposalAccepted(proposal); } /* * Request the proposals from the proposal provider, and recompute any * caches. Repopulate the popup if it is open. */ private void recomputeProposals(String filterText) { IContentProposal[] allProposals = getProposals(); if (allProposals == null) allProposals = getEmptyProposalArray(); // If the non-filtered proposal list is empty, we should // close the popup. // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=147377 if (allProposals.length == 0) { proposals = allProposals; close(); } else { // Keep the popup open, but filter by any provided filter text setProposals(filterProposals(allProposals, filterText)); } } /* * In an async block, request the proposals. This is used when clients * are in the middle of processing an event that affects the widget * content. By using an async, we ensure that the widget content is up * to date with the event. */ private void asyncRecomputeProposals(final String filterText) { if (isValid()) { control.getDisplay().asyncExec(new Runnable() { public void run() { recordCursorPosition(); recomputeProposals(filterText); } }); } else { recomputeProposals(filterText); } } /* * Filter the provided list of content proposals according to the filter * text. */ private IContentProposal[] filterProposals( IContentProposal[] proposals, String filterString) { if (filterString.length() == 0) { return proposals; } // Check each string for a match. Use the string displayed to the // user, not the proposal content. ArrayList list = new ArrayList(); for (int i = 0; i < proposals.length; i++) { String string = getString(proposals[i]); if (string.length() >= filterString.length() && string.substring(0, filterString.length()) .equalsIgnoreCase(filterString)) { list.add(proposals[i]); } } return (IContentProposal[]) list.toArray(new IContentProposal[list .size()]); } Listener getTargetControlListener() { if (targetControlListener == null) { targetControlListener = new TargetControlListener(); } return targetControlListener; } } /** * Flag that controls the printing of debug info. */ public static final boolean DEBUG = false; /** * Indicates that a chosen proposal should be inserted into the field. */ public static final int PROPOSAL_INSERT = 1; /** * Indicates that a chosen proposal should replace the entire contents of * the field. */ public static final int PROPOSAL_REPLACE = 2; /** * Indicates that the contents of the control should not be modified when a * proposal is chosen. This is typically used when a client needs more * specialized behavior when a proposal is chosen. In this case, clients * typically register an IContentProposalListener so that they are notified * when a proposal is chosen. */ public static final int PROPOSAL_IGNORE = 3; /** * Indicates that there should be no filter applied as keys are typed in the * popup. */ public static final int FILTER_NONE = 1; /** * Indicates that a single character filter applies as keys are typed in the * popup. */ public static final int FILTER_CHARACTER = 2; /** * Indicates that a cumulative filter applies as keys are typed in the * popup. That is, each character typed will be added to the filter. * * @deprecated As of 3.4, filtering that is sensitive to changes in the * control content should be performed by the supplied * {@link IContentProposalProvider}, such as that performed by * {@link SimpleContentProposalProvider} */ public static final int FILTER_CUMULATIVE = 3; /* * Set to <code>true</code> to use a Table with SWT.VIRTUAL. This is a * workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=98585#c40 * The corresponding SWT bug is * https://bugs.eclipse.org/bugs/show_bug.cgi?id=90321 */ private static final boolean USE_VIRTUAL = !Util.isMotif(); /* * The delay before showing a secondary popup. */ private static final int POPUP_DELAY = 750; /* * The character height hint for the popup. May be overridden by using * setInitialPopupSize. */ private static final int POPUP_CHAR_HEIGHT = 10; /* * The minimum pixel width for the popup. May be overridden by using * setInitialPopupSize. */ private static final int POPUP_MINIMUM_WIDTH = 300; /* * The pixel offset of the popup from the bottom corner of the control. */ private static final int POPUP_OFFSET = 3; /* * Empty string. */ private static final String EMPTY = ""; //$NON-NLS-1$ /* * The object that provides content proposals. */ private IContentProposalProvider proposalProvider; /* * A label provider used to display proposals in the popup, and to extract * Strings from non-String proposals. */ private ILabelProvider labelProvider; /* * The control for which content proposals are provided. */ private Control control; /* * The adapter used to extract the String contents from an arbitrary * control. */ private IControlContentAdapter controlContentAdapter; /* * The popup used to show proposals. */ private ContentProposalPopup popup; /* * The keystroke that signifies content proposals should be shown. */ private KeyStroke triggerKeyStroke; /* * The String containing characters that auto-activate the popup. */ private String autoActivateString; /* * Integer that indicates how an accepted proposal should affect the * control. One of PROPOSAL_IGNORE, PROPOSAL_INSERT, or PROPOSAL_REPLACE. * Default value is PROPOSAL_INSERT. */ private int proposalAcceptanceStyle = PROPOSAL_INSERT; /* * A boolean that indicates whether key events received while the proposal * popup is open should also be propagated to the control. Default value is * true. */ private boolean propagateKeys = true; /* * Integer that indicates the filtering style. One of FILTER_CHARACTER, * FILTER_CUMULATIVE, FILTER_NONE. */ private int filterStyle = FILTER_NONE; /* * The listener we install on the control. */ private Listener controlListener; /* * The list of IContentProposalListener listeners. */ private ListenerList proposalListeners = new ListenerList(); /* * 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; /* * The desired size in pixels of the proposal popup. */ private Point popupSize; /* * The remembered position of the insertion position. Not all controls will * restore the insertion position if the proposal popup gets focus, so we * need to remember it. */ private int insertionPos = -1; /* * The remembered selection range. Not all controls will restore the * selection position if the proposal popup gets focus, so we need to * remember it. */ private Point selectionRange = new Point(-1, -1); /* * A flag that indicates that we are watching modify events */ private boolean watchModify = false; /** * Construct a content proposal adapter that can assist the user with * choosing content for the field. * * @param control * the control for which the adapter is providing content assist. * May not be <code>null</code>. * @param controlContentAdapter * the <code>IControlContentAdapter</code> used to obtain and * update the control's contents as proposals are accepted. May * not be <code>null</code>. * @param proposalProvider * the <code>IContentProposalProvider</code> used to obtain * content proposals for this control, or <code>null</code> if * no content proposal is available. * @param keyStroke * the keystroke that will invoke the content proposal popup. If * this value is <code>null</code>, then proposals will be * activated automatically when any of the auto activation * characters are typed. * @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 parameter is * <code>null</code>, then all alphanumeric characters will * auto-activate content proposal. */ public ContentProposalAdapter(Control control, IControlContentAdapter controlContentAdapter, IContentProposalProvider proposalProvider, KeyStroke keyStroke, char[] autoActivationCharacters) { super(); // We always assume the control and content adapter are valid. Assert.isNotNull(control); Assert.isNotNull(controlContentAdapter); this.control = control; this.controlContentAdapter = controlContentAdapter; // The rest of these may be null this.proposalProvider = proposalProvider; this.triggerKeyStroke = keyStroke; if (autoActivationCharacters != null) { this.autoActivateString = new String(autoActivationCharacters); } addControlListener(control); } /** * 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; } /** * Get the label provider that is used to show proposals. * * @return the {@link ILabelProvider} used to show proposals, or * <code>null</code> if one has not been installed. */ public ILabelProvider getLabelProvider() { return labelProvider; } /** * 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; } /** * Set the label provider that is used to show proposals. The lifecycle of * the specified label provider is not managed by this adapter. Clients must * dispose the label provider when it is no longer needed. * * @param labelProvider * the (@link ILabelProvider} used to show proposals. */ public void setLabelProvider(ILabelProvider labelProvider) { this.labelProvider = labelProvider; } /** * Return the proposal provider that provides content proposals given the * current content of the field. A value of <code>null</code> indicates * that there are no content proposals available for the field. * * @return the {@link IContentProposalProvider} used to show proposals. May * be <code>null</code>. */ public IContentProposalProvider getContentProposalProvider() { return proposalProvider; } /** * Set the content proposal provider that is used to show proposals. * * @param proposalProvider * the {@link IContentProposalProvider} used to show proposals */ public void setContentProposalProvider( IContentProposalProvider proposalProvider) { this.proposalProvider = proposalProvider; } /** * 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; } /** * Get the integer style that indicates how an accepted proposal affects the * control's content. * * @return a constant indicating how an accepted proposal should affect the * control's content. Should be one of <code>PROPOSAL_INSERT</code>, * <code>PROPOSAL_REPLACE</code>, or <code>PROPOSAL_IGNORE</code>. * (Default is <code>PROPOSAL_INSERT</code>). */ public int getProposalAcceptanceStyle() { return proposalAcceptanceStyle; } /** * Set the integer style that indicates how an accepted proposal affects the * control's content. * * @param acceptance * a constant indicating how an accepted proposal should affect * the control's content. Should be one of * <code>PROPOSAL_INSERT</code>, <code>PROPOSAL_REPLACE</code>, * or <code>PROPOSAL_IGNORE</code> */ public void setProposalAcceptanceStyle(int acceptance) { proposalAcceptanceStyle = acceptance; } /** * Return the integer style that indicates how keystrokes affect the content * of the proposal popup while it is open. * * @return a constant indicating how keystrokes in the proposal popup affect * filtering of the proposals shown. <code>FILTER_NONE</code> * specifies that no filtering will occur in the content proposal * list as keys are typed. <code>FILTER_CHARACTER</code> specifies * the content of the popup will be filtered by the most recently * typed character. <code>FILTER_CUMULATIVE</code> is deprecated * and no longer recommended. It specifies that the content of the * popup will be filtered by a string containing all the characters * typed since the popup has been open. The default is * <code>FILTER_NONE</code>. */ public int getFilterStyle() { return filterStyle; } /** * Set the integer style that indicates how keystrokes affect the content of * the proposal popup while it is open. Popup-based filtering is useful for * narrowing and navigating the list of proposals provided once the popup is * open. Filtering of the proposals will occur even when the control content * is not affected by user typing. Note that automatic filtering is not used * to achieve content-sensitive filtering such as auto-completion. Filtering * that is sensitive to changes in the control content should be performed * by the supplied {@link IContentProposalProvider}. * * @param filterStyle * a constant indicating how keystrokes received in the proposal * popup affect filtering of the proposals shown. * <code>FILTER_NONE</code> specifies that no automatic * filtering of the content proposal list will occur as keys are * typed in the popup. <code>FILTER_CHARACTER</code> specifies * that the content of the popup will be filtered by the most * recently typed character. <code>FILTER_CUMULATIVE</code> is * deprecated and no longer recommended. It specifies that the * content of the popup will be filtered by a string containing * all the characters typed since the popup has been open. */ public void setFilterStyle(int filterStyle) { this.filterStyle = filterStyle; } /** * Return the size, in pixels, of the content proposal popup. * * @return a Point specifying the last width and height, in pixels, of the * content proposal popup. */ public Point getPopupSize() { return popupSize; } /** * Set the size, in pixels, of the content proposal popup. This size will be * used the next time the content proposal popup is opened. * * @param size * a Point specifying the desired width and height, in pixels, of * the content proposal popup. */ public void setPopupSize(Point size) { popupSize = size; } /** * Get the boolean that indicates whether key events (including * auto-activation characters) received by the content proposal popup should * also be propagated to the adapted control when the proposal popup is * open. * * @return a boolean that indicates whether key events (including * auto-activation characters) should be propagated to the adapted * control when the proposal popup is open. Default value is * <code>true</code>. */ public boolean getPropagateKeys() { return propagateKeys; } /** * Set the boolean that indicates whether key events (including * auto-activation characters) received by the content proposal popup should * also be propagated to the adapted control when the proposal popup is * open. * * @param propagateKeys * a boolean that indicates whether key events (including * auto-activation characters) should be propagated to the * adapted control when the proposal popup is open. */ public void setPropagateKeys(boolean propagateKeys) { this.propagateKeys = propagateKeys; } /** * Return the content adapter that can get or retrieve the text contents * from the adapter's control. This method is used when a client, such as a * content proposal listener, needs to update the control's contents * manually. * * @return the {@link IControlContentAdapter} which can update the control * text. */ public IControlContentAdapter getControlContentAdapter() { return controlContentAdapter; } /** * 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 content proposals are chosen. * </p> * * @param listener * the IContentProposalListener 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. * * @see org.eclipse.jface.fieldassist.IContentProposalListener */ public void addContentProposalListener(IContentProposalListener listener) { proposalListeners.add(listener); } /** * Removes the specified listener from the list of content proposal * listeners that are notified when content proposals are chosen. * </p> * * @param listener * the IContentProposalListener 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.IContentProposalListener */ public void removeContentProposalListener(IContentProposalListener listener) { proposalListeners.remove(listener); } /** * 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()) { if (popup == null) { // Check whether there are any proposals to be shown. recordCursorPosition(); // must be done before getting proposals IContentProposal[] proposals = getProposals(); if (proposals.length > 0) { if (DEBUG) { System.out.println("POPUP OPENED BY PRECEDING EVENT"); //$NON-NLS-1$ } recordCursorPosition(); popup = new ContentProposalPopup(null, proposals); popup.open(); popup.getShell().addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent event) { popup = null; } }); internalPopupOpened(); notifyPopupOpened(); } else if (!autoActivated) { getControl().getDisplay().beep(); } } } } /** * 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); } /** * Close the proposal popup without accepting a proposal. This method * returns immediately, and has no effect if the proposal popup was not * open. This method is used by subclasses to explicitly close the popup * based on additional logic. * * @since 3.3 */ protected void closeProposalPopup() { if (popup != null) { popup.close(); } } /* * A content proposal has been accepted. Update the control contents * accordingly and notify any listeners. * * @param proposal the accepted proposal */ private void proposalAccepted(IContentProposal proposal) { switch (proposalAcceptanceStyle) { case (PROPOSAL_REPLACE): setControlContent(proposal.getContent(), proposal .getCursorPosition()); break; case (PROPOSAL_INSERT): insertControlContent(proposal.getContent(), proposal .getCursorPosition()); break; default: // do nothing. Typically a listener is installed to handle this in // a custom way. break; } // In all cases, notify listeners of an accepted proposal. notifyProposalAccepted(proposal); } /* * Set the text content of the control to the specified text, setting the * cursorPosition at the desired location within the new contents. */ private void setControlContent(String text, int cursorPosition) { if (isValid()) { // should already be false, but just in case. watchModify = false; controlContentAdapter.setControlContents(control, text, cursorPosition); } } /* * Insert the specified text into the control content, setting the * cursorPosition at the desired location within the new contents. */ private void insertControlContent(String text, int cursorPosition) { if (isValid()) { // should already be false, but just in case. watchModify = false; // Not all controls preserve their selection index when they lose // focus, so we must set it explicitly here to what it was before // the popup opened. // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=127108 // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=139063 if (controlContentAdapter instanceof IControlContentAdapter2 && selectionRange.x != -1) { ((IControlContentAdapter2) controlContentAdapter).setSelection( control, selectionRange); } else if (insertionPos != -1) { controlContentAdapter.setCursorPosition(control, insertionPos); } controlContentAdapter.insertControlContents(control, text, cursorPosition); } } /* * Check that the control and content adapter are valid. */ private boolean isValid() { return control != null && !control.isDisposed() && controlContentAdapter != null; } /* * Record the control's cursor position. */ private void recordCursorPosition() { if (isValid()) { IControlContentAdapter adapter = getControlContentAdapter(); insertionPos = adapter.getCursorPosition(control); // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=139063 if (adapter instanceof IControlContentAdapter2) { selectionRange = ((IControlContentAdapter2) adapter) .getSelection(control); } } } /* * Get the proposals from the proposal provider. Gets all of the proposals * without doing any filtering. */ private IContentProposal[] getProposals() { if (proposalProvider == null || !isValid()) { return null; } if (DEBUG) { System.out.println(">>> obtaining proposals from provider"); //$NON-NLS-1$ } int position = insertionPos; if (position == -1) { position = getControlContentAdapter().getCursorPosition( getControl()); } String contents = getControlContentAdapter().getControlContents( getControl()); IContentProposal[] proposals = proposalProvider.getProposals(contents, position); return proposals; } /** * 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); } } }); } } /* * A proposal has been accepted. Notify interested listeners. */ private void notifyProposalAccepted(IContentProposal proposal) { if (DEBUG) { System.out.println("Notify listeners - proposal accepted."); //$NON-NLS-1$ } final Object[] listenerArray = proposalListeners.getListeners(); for (int i = 0; i < listenerArray.length; i++) { ((IContentProposalListener) listenerArray[i]) .proposalAccepted(proposal); } } /* * 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(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++) { ((IContentProposalListener2) listenerArray[i]) .proposalPopupClosed(this); } } /** * Returns whether the content proposal popup has the focus. This includes * both the primary popup and any secondary info popup that may have focus. * * @return <code>true</code> if the proposal popup or its secondary info * popup has the focus * @since 3.4 */ public boolean hasProposalPopupFocus() { return popup != null && popup.hasFocus(); } /* * Return whether the control content is empty */ private boolean isControlContentEmpty() { return getControlContentAdapter().getControlContents(getControl()) .length() == 0; } /* * The popup has just opened, but listeners have not yet * been notified. Perform any cleanup that is needed. */ private void internalPopupOpened() { // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=243612 if (control instanceof Combo) { ((Combo)control).setListVisible(false); } } /* * Return whether a proposal popup should remain open. * If it was autoactivated by specific characters, and * none of those characters remain, then it should not remain * open. This method should not be used to determine * whether autoactivation has occurred or should occur, only whether * the circumstances would dictate that a popup remain open. */ private boolean shouldPopupRemainOpen() { // If we always autoactivate or never autoactivate, it should remain open if (autoActivateString == null || autoActivateString.length() == 0) return true; String content = getControlContentAdapter().getControlContents(getControl()); for (int i=0; i<autoActivateString.length(); i++) { if (content.indexOf(autoActivateString.charAt(i)) >= 0) return true; } return false; } /* * Return whether this adapter is configured for autoactivation, by * specific characters or by any characters. */ private boolean allowsAutoActivate() { return (autoActivateString != null && autoActivateString.length() > 0) // there are specific autoactivation chars supplied || (autoActivateString == null && triggerKeyStroke == null); // we autoactivate on everything } /** * Sets focus to the proposal popup. If the proposal popup is not opened, * this method is ignored. If the secondary popup has focus, focus is * returned to the main proposal popup. * * @since 3.6 */ public void setProposalPopupFocus() { if (isValid() && popup != null) popup.getShell().setFocus(); } /** * 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 != null) return true; return false; } }