blob: c70933cdf1335ebcbb2d6bee6b6100ec31ff3a6d [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2019 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Hannes Erven <hannes@erven.at> - Bug 293841 - [FieldAssist] NumLock keyDown event should not close the proposal popup [with patch]
*******************************************************************************/
package org.eclipse.jface.fieldassist;
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.preference.JFacePreferences;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.swt.SWT;
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.
*
* @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;
@Override
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(() -> {
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;
}
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.
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
@Override
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.
*/
@Override
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() {
@Override
public void focusGained(FocusEvent event) {
ContentProposalPopup.this.close();
}
});
return text;
}
/*
* Adjust the bounds so that we appear adjacent to our parent shell
*/
@Override
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);
}
@Override
protected Color getForeground() {
return control.getDisplay().
getSystemColor(SWT.COLOR_INFO_FOREGROUND);
}
@Override
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;
}
@Override
protected Color getForeground() {
return JFaceResources.getColorRegistry().get(
JFacePreferences.CONTENT_ASSIST_FOREGROUND_COLOR);
}
@Override
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>.
*/
@Override
protected final Control createDialogArea(final Composite parent) {
proposalTable = new Table(parent, SWT.H_SCROLL | SWT.V_SCROLL | SWT.VIRTUAL);
Listener listener = event -> handleSetData(event);
proposalTable.addListener(SWT.SetData, listener);
// set the proposals to force population of the table.
setProposals(filterProposals(proposals, filterText));
proposalTable.setHeaderVisible(false);
proposalTable.addSelectionListener(new SelectionListener() {
@Override
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.
@Override
public void widgetDefaultSelected(SelectionEvent e) {
acceptCurrentProposal();
}
});
return proposalTable;
}
@Override
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();
}
int dir = proposalTable.getTextDirection();
if (dir == SWT.RIGHT_TO_LEFT) {
initialX = initialX - popupSize.x;
}
// 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, 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;
// Set and clear the virtual table. Data will be
// provided in the SWT.SetData event handler.
proposalTable.setItemCount(newSize);
proposalTable.clearAll();
// 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()
*/
@Override
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
*/
@Override
public boolean close() {
popupCloser.removeListeners();
if (infoPopup != null) {
infoPopup.close();
}
boolean ret = super.close();
notifyPopupClosed();
return ret;
}
/**
* Asynchronously recompute proposals.
*/
private void refresh() {
asyncRecomputeProposals(filterText);
}
/*
* 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 = () -> {
pendingDescriptionUpdate = true;
try {
Thread.sleep(POPUP_DELAY);
} catch (InterruptedException e) {
}
if (!isValid()) {
return;
}
getShell().getDisplay().syncExec(() -> {
// 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(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(() -> {
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<IContentProposal> list = new ArrayList<>();
for (IContentProposal proposal : proposals) {
String string = getString(proposal);
if (string.length() >= filterString.length()
&& string.substring(0, filterString.length())
.equalsIgnoreCase(filterString)) {
list.add(proposal);
}
}
return 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}
*/
@Deprecated
public static final int FILTER_CUMULATIVE = 3;
/*
* 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<IContentProposalListener> proposalListeners = new ListenerList<>();
/*
* The list of IContentProposalListener2 listeners.
*/
private ListenerList<IContentProposalListener2> 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.
*
* @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.
*
* @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.
*
* @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.
*
* @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() {
@Override
public void handleEvent(Event e) {
if (!isEnabled) {
return;
}
switch (e.type) {
case SWT.Traverse:
case SWT.KeyDown:
if (DEBUG) {
StringBuilder sb;
if (e.type == SWT.Traverse) {
sb = new StringBuilder("Traverse"); //$NON-NLS-1$
} else {
sb = new StringBuilder("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) {
StringBuilder sb;
if (e.type == SWT.Traverse) {
sb = new StringBuilder("Traverse"); //$NON-NLS-1$
} else {
sb = new StringBuilder("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) {
// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=520372
// The popup is null so record tab as a means to interrupt
// any autoactivation that is pending due to autoactivation
// delay.
if (popup == null) {
switch (e.detail) {
case SWT.TRAVERSE_TAB_NEXT:
case SWT.TRAVERSE_TAB_PREVIOUS:
receivedKeyDown = true;
break;
}
}
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;
}
// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=520372
// mimic close cases of popup in TargetControlListener
if (popup == null) {
switch (e.character) {
case SWT.CR:
case SWT.LF:
case SWT.ESC:
// Interrupt any autoactivation that is pending due to
// autoactivation delay.
receivedKeyDown = true;
break;
}
}
}
} 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) {
StringBuilder sb = new StringBuilder(
"--- [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 == null)
return;
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(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 = () -> {
receivedKeyDown = false;
try {
Thread.sleep(autoActivationDelay);
} catch (InterruptedException e) {
}
if (!isValid() || receivedKeyDown) {
return;
}
getControl().getDisplay().syncExec(() -> 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(() -> {
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$
}
for (IContentProposalListener l : proposalListeners) {
l.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$
}
for (IContentProposalListener2 l : proposalListeners2) {
l.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$
}
for (IContentProposalListener2 l : proposalListeners2) {
l.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;
}
/**
* Reloads the proposals from the content provider and fills them into the
* proposal pop-up, if the pop-up is currently open.
*
* @since 3.15
*/
public void refresh() {
if (isProposalPopupOpen()) {
popup.refresh();
}
}
}