1 /*******************************************************************************
2  * Copyright (c) 2005, 2019 IBM Corporation and others.
3  *
4  * This program and the accompanying materials
5  * are made available under the terms of the Eclipse Public License 2.0
6  * which accompanies this distribution, and is available at
7  * https://www.eclipse.org/legal/epl-2.0/
8  *
9  * SPDX-License-Identifier: EPL-2.0
10  *
11  * Contributors:
12  *     IBM Corporation - initial API and implementation
13  *     Hannes Erven <hannes@erven.at> - Bug 293841 - [FieldAssist] NumLock keyDown event should not close the proposal popup [with patch]
14  *******************************************************************************/
15 package org.eclipse.jface.fieldassist;
16 
17 import java.util.ArrayList;
18 
19 import org.eclipse.core.runtime.Assert;
20 import org.eclipse.core.runtime.ListenerList;
21 import org.eclipse.jface.bindings.keys.KeyStroke;
22 import org.eclipse.jface.dialogs.PopupDialog;
23 import org.eclipse.jface.preference.JFacePreferences;
24 import org.eclipse.jface.resource.JFaceResources;
25 import org.eclipse.jface.viewers.ILabelProvider;
26 import org.eclipse.swt.SWT;
27 import org.eclipse.swt.events.FocusAdapter;
28 import org.eclipse.swt.events.FocusEvent;
29 import org.eclipse.swt.events.SelectionEvent;
30 import org.eclipse.swt.events.SelectionListener;
31 import org.eclipse.swt.graphics.Color;
32 import org.eclipse.swt.graphics.Image;
33 import org.eclipse.swt.graphics.Point;
34 import org.eclipse.swt.graphics.Rectangle;
35 import org.eclipse.swt.layout.GridData;
36 import org.eclipse.swt.widgets.Combo;
37 import org.eclipse.swt.widgets.Composite;
38 import org.eclipse.swt.widgets.Control;
39 import org.eclipse.swt.widgets.Event;
40 import org.eclipse.swt.widgets.Listener;
41 import org.eclipse.swt.widgets.ScrollBar;
42 import org.eclipse.swt.widgets.Shell;
43 import org.eclipse.swt.widgets.Table;
44 import org.eclipse.swt.widgets.TableItem;
45 import org.eclipse.swt.widgets.Text;
46 
47 /**
48  * ContentProposalAdapter can be used to attach content proposal behavior to a
49  * control. This behavior includes obtaining proposals, opening a popup dialog,
50  * managing the content of the control relative to the selections in the popup,
51  * and optionally opening up a secondary popup to further describe proposals.
52  * <p>
53  * A number of configurable options are provided to determine how the control
54  * content is altered when a proposal is chosen, how the content proposal popup
55  * is activated, and whether any filtering should be done on the proposals as
56  * the user types characters.
57  * <p>
58  * This class provides some overridable methods to allow clients to manually
59  * control the popup. However, most of the implementation remains private.
60  *
61  * @since 3.2
62  */
63 public class ContentProposalAdapter {
64 
65 	/*
66 	 * The lightweight popup used to show content proposals for a text field. If
67 	 * additional information exists for a proposal, then selecting that
68 	 * proposal will result in the information being displayed in a secondary
69 	 * popup.
70 	 */
71 	class ContentProposalPopup extends PopupDialog {
72 		/*
73 		 * The listener we install on the popup and related controls to
74 		 * determine when to close the popup. Some events (move, resize, close,
75 		 * deactivate) trigger closure as soon as they are received, simply
76 		 * because one of the registered listeners received them. Other events
77 		 * depend on additional circumstances.
78 		 */
79 		private final class PopupCloserListener implements Listener {
80 			private boolean scrollbarClicked = false;
81 
82 			@Override
handleEvent(final Event e)83 			public void handleEvent(final Event e) {
84 
85 				// If focus is leaving an important widget or the field's
86 				// shell is deactivating
87 				if (e.type == SWT.FocusOut) {
88 					scrollbarClicked = false;
89 					/*
90 					 * Ignore this event if it's only happening because focus is
91 					 * moving between the popup shells, their controls, or a
92 					 * scrollbar. Do this in an async since the focus is not
93 					 * actually switched when this event is received.
94 					 */
95 					e.display.asyncExec(() -> {
96 						if (isValid()) {
97 							if (scrollbarClicked || hasFocus()) {
98 								return;
99 							}
100 							// Workaround a problem on X and Mac, whereby at
101 							// this point, the focus control is not known.
102 							// This can happen, for example, when resizing
103 							// the popup shell on the Mac.
104 							// Check the active shell.
105 							Shell activeShell = e.display.getActiveShell();
106 							if (activeShell == getShell()
107 									|| (infoPopup != null && infoPopup
108 											.getShell() == activeShell)) {
109 								return;
110 							}
111 							close();
112 						}
113 					});
114 					return;
115 				}
116 
117 				// Scroll bar has been clicked. Remember this for focus event
118 				// processing.
119 				if (e.type == SWT.Selection) {
120 					scrollbarClicked = true;
121 					return;
122 				}
123 				// For all other events, merely getting them dictates closure.
124 				close();
125 			}
126 
127 			// Install the listeners for events that need to be monitored for
128 			// popup closure.
installListeners()129 			void installListeners() {
130 				// Listeners on this popup's table and scroll bar
131 				proposalTable.addListener(SWT.FocusOut, this);
132 				ScrollBar scrollbar = proposalTable.getVerticalBar();
133 				if (scrollbar != null) {
134 					scrollbar.addListener(SWT.Selection, this);
135 				}
136 
137 				// Listeners on this popup's shell
138 				getShell().addListener(SWT.Deactivate, this);
139 				getShell().addListener(SWT.Close, this);
140 
141 				// Listeners on the target control
142 				control.addListener(SWT.MouseDoubleClick, this);
143 				control.addListener(SWT.MouseDown, this);
144 				control.addListener(SWT.Dispose, this);
145 				control.addListener(SWT.FocusOut, this);
146 				// Listeners on the target control's shell
147 				Shell controlShell = control.getShell();
148 				controlShell.addListener(SWT.Move, this);
149 				controlShell.addListener(SWT.Resize, this);
150 
151 			}
152 
153 			// Remove installed listeners
removeListeners()154 			void removeListeners() {
155 				if (isValid()) {
156 					proposalTable.removeListener(SWT.FocusOut, this);
157 					ScrollBar scrollbar = proposalTable.getVerticalBar();
158 					if (scrollbar != null) {
159 						scrollbar.removeListener(SWT.Selection, this);
160 					}
161 
162 					getShell().removeListener(SWT.Deactivate, this);
163 					getShell().removeListener(SWT.Close, this);
164 				}
165 
166 				if (control != null && !control.isDisposed()) {
167 
168 					control.removeListener(SWT.MouseDoubleClick, this);
169 					control.removeListener(SWT.MouseDown, this);
170 					control.removeListener(SWT.Dispose, this);
171 					control.removeListener(SWT.FocusOut, this);
172 
173 					Shell controlShell = control.getShell();
174 					controlShell.removeListener(SWT.Move, this);
175 					controlShell.removeListener(SWT.Resize, this);
176 				}
177 			}
178 		}
179 
180 		/*
181 		 * The listener we will install on the target control.
182 		 */
183 		private final class TargetControlListener implements Listener {
184 			// Key events from the control
185 			@Override
handleEvent(Event e)186 			public void handleEvent(Event e) {
187 				if (!isValid()) {
188 					return;
189 				}
190 
191 				char key = e.character;
192 
193 				// Traverse events are handled depending on whether the
194 				// event has a character.
195 				if (e.type == SWT.Traverse) {
196 					// If the traverse event contains a legitimate character,
197 					// then we must set doit false so that the widget will
198 					// receive the key event. We return immediately so that
199 					// the character is handled only in the key event.
200 					// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=132101
201 					if (key != 0) {
202 						e.doit = false;
203 						return;
204 					}
205 					// Traversal does not contain a character. Set doit true
206 					// to indicate TRAVERSE_NONE will occur and that no key
207 					// event will be triggered. We will check for navigation
208 					// keys below.
209 					e.detail = SWT.TRAVERSE_NONE;
210 					e.doit = true;
211 				} else {
212 					// Default is to only propagate when configured that way.
213 					// Some keys will always set doit to false anyway.
214 					e.doit = propagateKeys;
215 				}
216 
217 				// No character. Check for navigation keys.
218 
219 				if (key == 0) {
220 					int newSelection = proposalTable.getSelectionIndex();
221 					int visibleRows = (proposalTable.getSize().y / proposalTable
222 							.getItemHeight()) - 1;
223 					switch (e.keyCode) {
224 					case SWT.ARROW_UP:
225 						newSelection -= 1;
226 						if (newSelection < 0) {
227 							newSelection = proposalTable.getItemCount() - 1;
228 						}
229 						// Not typical - usually we get this as a Traverse and
230 						// therefore it never propagates. Added for consistency.
231 						if (e.type == SWT.KeyDown) {
232 							// don't propagate to control
233 							e.doit = false;
234 						}
235 
236 						break;
237 
238 					case SWT.ARROW_DOWN:
239 						newSelection += 1;
240 						if (newSelection > proposalTable.getItemCount() - 1) {
241 							newSelection = 0;
242 						}
243 						// Not typical - usually we get this as a Traverse and
244 						// therefore it never propagates. Added for consistency.
245 						if (e.type == SWT.KeyDown) {
246 							// don't propagate to control
247 							e.doit = false;
248 						}
249 
250 						break;
251 
252 					case SWT.PAGE_DOWN:
253 						newSelection += visibleRows;
254 						if (newSelection >= proposalTable.getItemCount()) {
255 							newSelection = proposalTable.getItemCount() - 1;
256 						}
257 						if (e.type == SWT.KeyDown) {
258 							// don't propagate to control
259 							e.doit = false;
260 						}
261 						break;
262 
263 					case SWT.PAGE_UP:
264 						newSelection -= visibleRows;
265 						if (newSelection < 0) {
266 							newSelection = 0;
267 						}
268 						if (e.type == SWT.KeyDown) {
269 							// don't propagate to control
270 							e.doit = false;
271 						}
272 						break;
273 
274 					case SWT.HOME:
275 						newSelection = 0;
276 						if (e.type == SWT.KeyDown) {
277 							// don't propagate to control
278 							e.doit = false;
279 						}
280 						break;
281 
282 					case SWT.END:
283 						newSelection = proposalTable.getItemCount() - 1;
284 						if (e.type == SWT.KeyDown) {
285 							// don't propagate to control
286 							e.doit = false;
287 						}
288 						break;
289 
290 					// If received as a Traverse, these should propagate
291 					// to the control as keydown. If received as a keydown,
292 					// proposals should be recomputed since the cursor
293 					// position has changed.
294 					case SWT.ARROW_LEFT:
295 					case SWT.ARROW_RIGHT:
296 						if (e.type == SWT.Traverse) {
297 							e.doit = false;
298 						} else {
299 							e.doit = true;
300 							String contents = getControlContentAdapter()
301 									.getControlContents(getControl());
302 							// If there are no contents, changes in cursor
303 							// position have no effect. Note also that we do
304 							// not affect the filter text on ARROW_LEFT as
305 							// we would with BS.
306 							if (contents.length() > 0) {
307 								asyncRecomputeProposals(filterText);
308 							}
309 						}
310 						break;
311 
312 					// Any unknown keycodes will cause the popup to close.
313 					// Modifier keys are explicitly checked and ignored because
314 					// they are not complete yet (no character).
315 					default:
316 						if (e.keyCode != SWT.CAPS_LOCK && e.keyCode != SWT.NUM_LOCK
317 								&& e.keyCode != SWT.MOD1
318 								&& e.keyCode != SWT.MOD2
319 								&& e.keyCode != SWT.MOD3
320 								&& e.keyCode != SWT.MOD4) {
321 							close();
322 						}
323 						return;
324 					}
325 
326 					// If any of these navigation events caused a new selection,
327 					// then handle that now and return.
328 					if (newSelection >= 0) {
329 						selectProposal(newSelection);
330 					}
331 					return;
332 				}
333 
334 				// key != 0
335 				// Check for special keys involved in cancelling, accepting, or
336 				// filtering the proposals.
337 				switch (key) {
338 				case SWT.ESC:
339 					e.doit = false;
340 					close();
341 					break;
342 
343 				case SWT.LF:
344 				case SWT.CR:
345 					e.doit = false;
346 					Object p = getSelectedProposal();
347 					if (p != null) {
348 						acceptCurrentProposal();
349 					} else {
350 						close();
351 					}
352 					break;
353 
354 				case SWT.TAB:
355 					e.doit = false;
356 					getShell().setFocus();
357 					return;
358 
359 				case SWT.BS:
360 					// Backspace should back out of any stored filter text
361 					if (filterStyle != FILTER_NONE) {
362 						// We have no filter to back out of, so do nothing
363 						if (filterText.length() == 0) {
364 							return;
365 						}
366 						// There is filter to back out of
367 						filterText = filterText.substring(0, filterText
368 								.length() - 1);
369 						asyncRecomputeProposals(filterText);
370 						return;
371 					}
372 					// There is no filtering provided by us, but some
373 					// clients provide their own filtering based on content.
374 					// Recompute the proposals if the cursor position
375 					// will change (is not at 0).
376 					int pos = getControlContentAdapter().getCursorPosition(
377 							getControl());
378 					// We rely on the fact that the contents and pos do not yet
379 					// reflect the result of the BS. If the contents were
380 					// already empty, then BS should not cause
381 					// a recompute.
382 					if (pos > 0) {
383 						asyncRecomputeProposals(filterText);
384 					}
385 					break;
386 
387 				default:
388 					// If the key is a defined unicode character, and not one of
389 					// the special cases processed above, update the filter text
390 					// and filter the proposals.
391 					if (Character.isDefined(key)) {
392 						if (filterStyle == FILTER_CUMULATIVE) {
393 							filterText = filterText + key;
394 						} else if (filterStyle == FILTER_CHARACTER) {
395 							filterText = String.valueOf(key);
396 						}
397 						// Recompute proposals after processing this event.
398 						asyncRecomputeProposals(filterText);
399 					}
400 					break;
401 				}
402 			}
403 		}
404 
405 		/*
406 		 * Internal class used to implement the secondary popup.
407 		 */
408 		private class InfoPopupDialog extends PopupDialog {
409 
410 			/*
411 			 * The text control that displays the text.
412 			 */
413 			private Text text;
414 
415 			/*
416 			 * The String shown in the popup.
417 			 */
418 			private String contents = EMPTY;
419 
420 			/*
421 			 * Construct an info-popup with the specified parent.
422 			 */
InfoPopupDialog(Shell parent)423 			InfoPopupDialog(Shell parent) {
424 				super(parent, PopupDialog.HOVER_SHELLSTYLE, false, false, false,
425 						false, false, null, null);
426 			}
427 
428 			/*
429 			 * Create a text control for showing the info about a proposal.
430 			 */
431 			@Override
createDialogArea(Composite parent)432 			protected Control createDialogArea(Composite parent) {
433 				text = new Text(parent, SWT.MULTI | SWT.READ_ONLY | SWT.WRAP | SWT.NO_FOCUS);
434 
435 				// Use the compact margins employed by PopupDialog.
436 				GridData gd = new GridData(GridData.BEGINNING | GridData.FILL_BOTH);
437 				gd.horizontalIndent = PopupDialog.POPUP_HORIZONTALSPACING;
438 				gd.verticalIndent = PopupDialog.POPUP_VERTICALSPACING;
439 				text.setLayoutData(gd);
440 				text.setText(contents);
441 
442 				// since SWT.NO_FOCUS is only a hint...
443 				text.addFocusListener(new FocusAdapter() {
444 					@Override
445 					public void focusGained(FocusEvent event) {
446 						ContentProposalPopup.this.close();
447 					}
448 				});
449 				return text;
450 			}
451 
452 			/*
453 			 * Adjust the bounds so that we appear adjacent to our parent shell
454 			 */
455 			@Override
adjustBounds()456 			protected void adjustBounds() {
457 				Rectangle parentBounds = getParentShell().getBounds();
458 				Rectangle proposedBounds;
459 				// Try placing the info popup to the right
460 				Rectangle rightProposedBounds = new Rectangle(parentBounds.x
461 						+ parentBounds.width
462 						+ PopupDialog.POPUP_HORIZONTALSPACING, parentBounds.y
463 						+ PopupDialog.POPUP_VERTICALSPACING,
464 						parentBounds.width, parentBounds.height);
465 				rightProposedBounds = getConstrainedShellBounds(rightProposedBounds);
466 				// If it won't fit on the right, try the left
467 				if (rightProposedBounds.intersects(parentBounds)) {
468 					Rectangle leftProposedBounds = new Rectangle(parentBounds.x
469 							- parentBounds.width - POPUP_HORIZONTALSPACING - 1,
470 							parentBounds.y, parentBounds.width,
471 							parentBounds.height);
472 					leftProposedBounds = getConstrainedShellBounds(leftProposedBounds);
473 					// If it won't fit on the left, choose the proposed bounds
474 					// that fits the best
475 					if (leftProposedBounds.intersects(parentBounds)) {
476 						if (rightProposedBounds.x - parentBounds.x >= parentBounds.x
477 								- leftProposedBounds.x) {
478 							rightProposedBounds.x = parentBounds.x
479 									+ parentBounds.width
480 									+ PopupDialog.POPUP_HORIZONTALSPACING;
481 							proposedBounds = rightProposedBounds;
482 						} else {
483 							leftProposedBounds.width = parentBounds.x
484 									- POPUP_HORIZONTALSPACING
485 									- leftProposedBounds.x;
486 							proposedBounds = leftProposedBounds;
487 						}
488 					} else {
489 						// use the proposed bounds on the left
490 						proposedBounds = leftProposedBounds;
491 					}
492 				} else {
493 					// use the proposed bounds on the right
494 					proposedBounds = rightProposedBounds;
495 				}
496 				getShell().setBounds(proposedBounds);
497 			}
498 
499 			@Override
getForeground()500 			protected Color getForeground() {
501 				return control.getDisplay().
502 						getSystemColor(SWT.COLOR_INFO_FOREGROUND);
503 			}
504 
505 			@Override
getBackground()506 			protected Color getBackground() {
507 				return control.getDisplay().
508 						getSystemColor(SWT.COLOR_INFO_BACKGROUND);
509 			}
510 
511 			/*
512 			 * Set the text contents of the popup.
513 			 */
setContents(String newContents)514 			void setContents(String newContents) {
515 				if (newContents == null) {
516 					newContents = EMPTY;
517 				}
518 				this.contents = newContents;
519 				if (text != null && !text.isDisposed()) {
520 					text.setText(contents);
521 				}
522 			}
523 
524 			/*
525 			 * Return whether the popup has focus.
526 			 */
hasFocus()527 			boolean hasFocus() {
528 				if (text == null || text.isDisposed()) {
529 					return false;
530 				}
531 				return text.getShell().isFocusControl()
532 						|| text.isFocusControl();
533 			}
534 		}
535 
536 		/*
537 		 * The listener installed on the target control.
538 		 */
539 		private Listener targetControlListener;
540 
541 		/*
542 		 * The listener installed in order to close the popup.
543 		 */
544 		private PopupCloserListener popupCloser;
545 
546 		/*
547 		 * The table used to show the list of proposals.
548 		 */
549 		private Table proposalTable;
550 
551 		/*
552 		 * The proposals to be shown (cached to avoid repeated requests).
553 		 */
554 		private IContentProposal[] proposals;
555 
556 		/*
557 		 * Secondary popup used to show detailed information about the selected
558 		 * proposal..
559 		 */
560 		private InfoPopupDialog infoPopup;
561 
562 		/*
563 		 * Flag indicating whether there is a pending secondary popup update.
564 		 */
565 		private boolean pendingDescriptionUpdate = false;
566 
567 		/*
568 		 * Filter text - tracked while popup is open, only if we are told to
569 		 * filter
570 		 */
571 		private String filterText = EMPTY;
572 
573 		/**
574 		 * Constructs a new instance of this popup, specifying the control for
575 		 * which this popup is showing content, and how the proposals should be
576 		 * obtained and displayed.
577 		 *
578 		 * @param infoText
579 		 *            Text to be shown in a lower info area, or
580 		 *            <code>null</code> if there is no info area.
581 		 */
ContentProposalPopup(String infoText, IContentProposal[] proposals)582 		ContentProposalPopup(String infoText, IContentProposal[] proposals) {
583 			// IMPORTANT: Use of SWT.ON_TOP is critical here for ensuring
584 			// that the target control retains focus on Mac and Linux. Without
585 			// it, the focus will disappear, keystrokes will not go to the
586 			// popup, and the popup closer will wrongly close the popup.
587 			// On platforms where SWT.ON_TOP overrides SWT.RESIZE, we will live
588 			// with this.
589 			// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=126138
590 			super(control.getShell(), SWT.RESIZE | SWT.ON_TOP, false, false, false,
591 					false, false, null, infoText);
592 			this.proposals = proposals;
593 		}
594 
595 		@Override
getForeground()596 		protected Color getForeground() {
597 			return JFaceResources.getColorRegistry().get(
598 					JFacePreferences.CONTENT_ASSIST_FOREGROUND_COLOR);
599 		}
600 
601 		@Override
getBackground()602 		protected Color getBackground() {
603 			return JFaceResources.getColorRegistry().get(
604 					JFacePreferences.CONTENT_ASSIST_BACKGROUND_COLOR);
605 		}
606 
607 		/*
608 		 * Creates the content area for the proposal popup. This creates a table
609 		 * and places it inside the composite. The table will contain a list of
610 		 * all the proposals.
611 		 *
612 		 * @param parent The parent composite to contain the dialog area; must
613 		 * not be <code>null</code>.
614 		 */
615 		@Override
createDialogArea(final Composite parent)616 		protected final Control createDialogArea(final Composite parent) {
617 			proposalTable = new Table(parent, SWT.H_SCROLL | SWT.V_SCROLL | SWT.VIRTUAL);
618 
619 			Listener listener = event -> handleSetData(event);
620 			proposalTable.addListener(SWT.SetData, listener);
621 			// set the proposals to force population of the table.
622 			setProposals(filterProposals(proposals, filterText));
623 			proposalTable.setTextDirection(SWT.AUTO_TEXT_DIRECTION);
624 
625 			proposalTable.setHeaderVisible(false);
626 			proposalTable.addSelectionListener(new SelectionListener() {
627 
628 				@Override
629 				public void widgetSelected(SelectionEvent e) {
630 					// If a proposal has been selected, show it in the secondary
631 					// popup. Otherwise close the popup.
632 					if (e.item == null) {
633 						if (infoPopup != null) {
634 							infoPopup.close();
635 						}
636 					} else {
637 						showProposalDescription();
638 					}
639 				}
640 
641 				// Default selection was made. Accept the current proposal.
642 				@Override
643 				public void widgetDefaultSelected(SelectionEvent e) {
644 					acceptCurrentProposal();
645 				}
646 			});
647 			return proposalTable;
648 		}
649 
650 		@Override
adjustBounds()651 		protected void adjustBounds() {
652 			// Get our control's location in display coordinates.
653 			Point location = control.getDisplay().map(control.getParent(), null, control.getLocation());
654 			int initialX = location.x + POPUP_OFFSET;
655 			int initialY = location.y + control.getSize().y + POPUP_OFFSET;
656 			// If we are inserting content, use the cursor position to
657 			// position the control.
658 			if (getProposalAcceptanceStyle() == PROPOSAL_INSERT) {
659 				Rectangle insertionBounds = controlContentAdapter
660 						.getInsertionBounds(control);
661 				initialX = initialX + insertionBounds.x;
662 				initialY = location.y + insertionBounds.y
663 						+ insertionBounds.height;
664 			}
665 
666 			// If there is no specified size, force it by setting
667 			// up a layout on the table.
668 			if (popupSize == null) {
669 				GridData data = new GridData(GridData.FILL_BOTH);
670 				data.heightHint = proposalTable.getItemHeight()
671 						* POPUP_CHAR_HEIGHT;
672 				data.widthHint = Math.max(control.getSize().x,
673 						POPUP_MINIMUM_WIDTH);
674 				proposalTable.setLayoutData(data);
675 				getShell().pack();
676 				popupSize = getShell().getSize();
677 			}
678 
679 			int dir = proposalTable.getTextDirection();
680 			if (dir == SWT.RIGHT_TO_LEFT) {
681 				initialX = initialX - popupSize.x;
682 			}
683 
684 			// Constrain to the display
685 			Rectangle constrainedBounds = getConstrainedShellBounds(new Rectangle(initialX, initialY, popupSize.x, popupSize.y));
686 
687 			// If there has been an adjustment causing the popup to overlap
688 			// with the control, then put the popup above the control.
689 			if (constrainedBounds.y < initialY)
690 				getShell().setBounds(initialX, location.y - popupSize.y, popupSize.x, popupSize.y);
691 			else
692 				getShell().setBounds(initialX, initialY, popupSize.x, popupSize.y);
693 
694 			// Now set up a listener to monitor any changes in size.
695 			getShell().addListener(SWT.Resize, e -> {
696 				popupSize = getShell().getSize();
697 				if (infoPopup != null) {
698 					infoPopup.adjustBounds();
699 				}
700 			});
701 		}
702 
703 		/*
704 		 * Handle the set data event. Set the item data of the requested item to
705 		 * the corresponding proposal in the proposal cache.
706 		 */
handleSetData(Event event)707 		private void handleSetData(Event event) {
708 			TableItem item = (TableItem) event.item;
709 			int index = proposalTable.indexOf(item);
710 
711 			if (0 <= index && index < proposals.length) {
712 				IContentProposal current = proposals[index];
713 				item.setText(getString(current));
714 				item.setImage(getImage(current));
715 				item.setData(current);
716 			}
717 		}
718 
719 		/*
720 		 * Caches the specified proposals and repopulates the table if it has
721 		 * been created.
722 		 */
setProposals(IContentProposal[] newProposals)723 		private void setProposals(IContentProposal[] newProposals) {
724 			if (newProposals == null || newProposals.length == 0) {
725 				newProposals = getEmptyProposalArray();
726 			}
727 			this.proposals = newProposals;
728 
729 			// If there is a table
730 			if (isValid()) {
731 				final int newSize = newProposals.length;
732 					// Set and clear the virtual table. Data will be
733 					// provided in the SWT.SetData event handler.
734 				proposalTable.setItemCount(newSize);
735 				proposalTable.clearAll();
736 				// Default to the first selection if there is content.
737 				if (newProposals.length > 0) {
738 					selectProposal(0);
739 				} else {
740 					// No selection, close the secondary popup if it was open
741 					if (infoPopup != null) {
742 						infoPopup.close();
743 					}
744 
745 				}
746 			}
747 		}
748 
749 		/*
750 		 * Get the string for the specified proposal. Always return a String of
751 		 * some kind.
752 		 */
getString(IContentProposal proposal)753 		private String getString(IContentProposal proposal) {
754 			if (proposal == null) {
755 				return EMPTY;
756 			}
757 			if (labelProvider == null) {
758 				return proposal.getLabel() == null ? proposal.getContent()
759 						: proposal.getLabel();
760 			}
761 			return labelProvider.getText(proposal);
762 		}
763 
764 		/*
765 		 * Get the image for the specified proposal. If there is no image
766 		 * available, return null.
767 		 */
getImage(IContentProposal proposal)768 		private Image getImage(IContentProposal proposal) {
769 			if (proposal == null || labelProvider == null) {
770 				return null;
771 			}
772 			return labelProvider.getImage(proposal);
773 		}
774 
775 		/*
776 		 * Return an empty array. Used so that something always shows in the
777 		 * proposal popup, even if no proposal provider was specified.
778 		 */
getEmptyProposalArray()779 		private IContentProposal[] getEmptyProposalArray() {
780 			return new IContentProposal[0];
781 		}
782 
783 		/*
784 		 * Answer true if the popup is valid, which means the table has been
785 		 * created and not disposed.
786 		 */
isValid()787 		private boolean isValid() {
788 			return proposalTable != null && !proposalTable.isDisposed();
789 		}
790 
791 		/*
792 		 * Return whether the receiver has focus. Since 3.4, this includes a
793 		 * check for whether the info popup has focus.
794 		 */
hasFocus()795 		private boolean hasFocus() {
796 			if (!isValid()) {
797 				return false;
798 			}
799 			if (getShell().isFocusControl() || proposalTable.isFocusControl()) {
800 				return true;
801 			}
802 			if (infoPopup != null && infoPopup.hasFocus()) {
803 				return true;
804 			}
805 			return false;
806 		}
807 
808 		/*
809 		 * Return the current selected proposal.
810 		 */
getSelectedProposal()811 		private IContentProposal getSelectedProposal() {
812 			if (isValid()) {
813 				int i = proposalTable.getSelectionIndex();
814 				if (proposals == null || i < 0 || i >= proposals.length) {
815 					return null;
816 				}
817 				return proposals[i];
818 			}
819 			return null;
820 		}
821 
822 		/*
823 		 * Select the proposal at the given index.
824 		 */
selectProposal(int index)825 		private void selectProposal(int index) {
826 			Assert
827 					.isTrue(index >= 0,
828 							"Proposal index should never be negative"); //$NON-NLS-1$
829 			if (!isValid() || proposals == null || index >= proposals.length) {
830 				return;
831 			}
832 			proposalTable.setSelection(index);
833 			proposalTable.showSelection();
834 
835 			showProposalDescription();
836 		}
837 
838 		/**
839 		 * Opens this ContentProposalPopup. This method is extended in order to
840 		 * add the control listener when the popup is opened and to invoke the
841 		 * secondary popup if applicable.
842 		 *
843 		 * @return the return code
844 		 *
845 		 * @see org.eclipse.jface.window.Window#open()
846 		 */
847 		@Override
open()848 		public int open() {
849 			int value = super.open();
850 			if (popupCloser == null) {
851 				popupCloser = new PopupCloserListener();
852 			}
853 			popupCloser.installListeners();
854 			IContentProposal p = getSelectedProposal();
855 			if (p != null) {
856 				showProposalDescription();
857 			}
858 			return value;
859 		}
860 
861 		/**
862 		 * Closes this popup. This method is extended to remove the control
863 		 * listener.
864 		 *
865 		 * @return <code>true</code> if the window is (or was already) closed,
866 		 *         and <code>false</code> if it is still open
867 		 */
868 		@Override
close()869 		public boolean close() {
870 			popupCloser.removeListeners();
871 			if (infoPopup != null) {
872 				infoPopup.close();
873 			}
874 			boolean ret = super.close();
875 			notifyPopupClosed();
876 			return ret;
877 		}
878 
879 		/**
880 		 * Asynchronously recompute proposals.
881 		 */
refresh()882 		private void refresh() {
883 			asyncRecomputeProposals(filterText);
884 		}
885 
886 		/*
887 		 * Show the currently selected proposal's description in a secondary
888 		 * popup.
889 		 */
showProposalDescription()890 		private void showProposalDescription() {
891 			// If we do not already have a pending update, then
892 			// create a thread now that will show the proposal description
893 			if (!pendingDescriptionUpdate) {
894 				// Create a thread that will sleep for the specified delay
895 				// before creating the popup. We do not use Jobs since this
896 				// code must be able to run independently of the Eclipse
897 				// runtime.
898 				Runnable runnable = () -> {
899 					pendingDescriptionUpdate = true;
900 					try {
901 						Thread.sleep(POPUP_DELAY);
902 					} catch (InterruptedException e) {
903 					}
904 					if (!isValid()) {
905 						return;
906 					}
907 					getShell().getDisplay().syncExec(() -> {
908 						// Query the current selection since we have
909 						// been delayed
910 						IContentProposal p = getSelectedProposal();
911 						if (p != null) {
912 							String description = p.getDescription();
913 							if (description != null) {
914 								if (infoPopup == null) {
915 									infoPopup = new InfoPopupDialog(getShell());
916 									infoPopup.open();
917 									infoPopup.getShell()
918 											.addDisposeListener(event -> infoPopup = null);
919 								}
920 								infoPopup.setContents(p.getDescription());
921 							} else if (infoPopup != null) {
922 								infoPopup.close();
923 							}
924 							pendingDescriptionUpdate = false;
925 
926 						}
927 					});
928 				};
929 				Thread t = new Thread(runnable);
930 				t.start();
931 			}
932 		}
933 
934 		/*
935 		 * Accept the current proposal.
936 		 */
acceptCurrentProposal()937 		private void acceptCurrentProposal() {
938 			// Close before accepting the proposal. This is important
939 			// so that the cursor position can be properly restored at
940 			// acceptance, which does not work without focus on some controls.
941 			// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=127108
942 			IContentProposal proposal = getSelectedProposal();
943 			close();
944 			proposalAccepted(proposal);
945 		}
946 
947 		/*
948 		 * Request the proposals from the proposal provider, and recompute any
949 		 * caches. Repopulate the popup if it is open.
950 		 */
recomputeProposals(String filterText)951 		private void recomputeProposals(String filterText) {
952 			IContentProposal[] allProposals = getProposals();
953 			if (allProposals == null)
954 				 allProposals = getEmptyProposalArray();
955 			// If the non-filtered proposal list is empty, we should
956 			// close the popup.
957 			// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=147377
958 			if (allProposals.length == 0) {
959 				proposals = allProposals;
960 				close();
961 			} else {
962 				// Keep the popup open, but filter by any provided filter text
963 				setProposals(filterProposals(allProposals, filterText));
964 			}
965 		}
966 
967 		/*
968 		 * In an async block, request the proposals. This is used when clients
969 		 * are in the middle of processing an event that affects the widget
970 		 * content. By using an async, we ensure that the widget content is up
971 		 * to date with the event.
972 		 */
asyncRecomputeProposals(final String filterText)973 		private void asyncRecomputeProposals(final String filterText) {
974 			if (isValid()) {
975 				control.getDisplay().asyncExec(() -> {
976 					recordCursorPosition();
977 					recomputeProposals(filterText);
978 				});
979 			} else {
980 				recomputeProposals(filterText);
981 			}
982 		}
983 
984 		/*
985 		 * Filter the provided list of content proposals according to the filter
986 		 * text.
987 		 */
filterProposals( IContentProposal[] proposals, String filterString)988 		private IContentProposal[] filterProposals(
989 				IContentProposal[] proposals, String filterString) {
990 			if (filterString.length() == 0) {
991 				return proposals;
992 			}
993 
994 			// Check each string for a match. Use the string displayed to the
995 			// user, not the proposal content.
996 			ArrayList<IContentProposal> list = new ArrayList<>();
997 			for (IContentProposal proposal : proposals) {
998 				String string = getString(proposal);
999 				if (string.length() >= filterString.length()
1000 						&& string.substring(0, filterString.length())
1001 								.equalsIgnoreCase(filterString)) {
1002 					list.add(proposal);
1003 				}
1004 
1005 			}
1006 			return list.toArray(new IContentProposal[list
1007 					.size()]);
1008 		}
1009 
getTargetControlListener()1010 		Listener getTargetControlListener() {
1011 			if (targetControlListener == null) {
1012 				targetControlListener = new TargetControlListener();
1013 			}
1014 			return targetControlListener;
1015 		}
1016 	}
1017 
1018 	/**
1019 	 * Flag that controls the printing of debug info.
1020 	 */
1021 	public static final boolean DEBUG = false;
1022 
1023 	/**
1024 	 * Indicates that a chosen proposal should be inserted into the field.
1025 	 */
1026 	public static final int PROPOSAL_INSERT = 1;
1027 
1028 	/**
1029 	 * Indicates that a chosen proposal should replace the entire contents of
1030 	 * the field.
1031 	 */
1032 	public static final int PROPOSAL_REPLACE = 2;
1033 
1034 	/**
1035 	 * Indicates that the contents of the control should not be modified when a
1036 	 * proposal is chosen. This is typically used when a client needs more
1037 	 * specialized behavior when a proposal is chosen. In this case, clients
1038 	 * typically register an IContentProposalListener so that they are notified
1039 	 * when a proposal is chosen.
1040 	 */
1041 	public static final int PROPOSAL_IGNORE = 3;
1042 
1043 	/**
1044 	 * Indicates that there should be no filter applied as keys are typed in the
1045 	 * popup.
1046 	 */
1047 	public static final int FILTER_NONE = 1;
1048 
1049 	/**
1050 	 * Indicates that a single character filter applies as keys are typed in the
1051 	 * popup.
1052 	 */
1053 	public static final int FILTER_CHARACTER = 2;
1054 
1055 	/**
1056 	 * Indicates that a cumulative filter applies as keys are typed in the
1057 	 * popup. That is, each character typed will be added to the filter.
1058 	 *
1059 	 * @deprecated As of 3.4, filtering that is sensitive to changes in the
1060 	 *             control content should be performed by the supplied
1061 	 *             {@link IContentProposalProvider}, such as that performed by
1062 	 *             {@link SimpleContentProposalProvider}
1063 	 */
1064 	@Deprecated
1065 	public static final int FILTER_CUMULATIVE = 3;
1066 
1067 	/*
1068 	 * The delay before showing a secondary popup.
1069 	 */
1070 	private static final int POPUP_DELAY = 750;
1071 
1072 	/*
1073 	 * The character height hint for the popup. May be overridden by using
1074 	 * setInitialPopupSize.
1075 	 */
1076 	private static final int POPUP_CHAR_HEIGHT = 10;
1077 
1078 	/*
1079 	 * The minimum pixel width for the popup. May be overridden by using
1080 	 * setInitialPopupSize.
1081 	 */
1082 	private static final int POPUP_MINIMUM_WIDTH = 300;
1083 
1084 	/*
1085 	 * The pixel offset of the popup from the bottom corner of the control.
1086 	 */
1087 	private static final int POPUP_OFFSET = 3;
1088 
1089 	/*
1090 	 * Empty string.
1091 	 */
1092 	private static final String EMPTY = ""; //$NON-NLS-1$
1093 
1094 	/*
1095 	 * The object that provides content proposals.
1096 	 */
1097 	private IContentProposalProvider proposalProvider;
1098 
1099 	/*
1100 	 * A label provider used to display proposals in the popup, and to extract
1101 	 * Strings from non-String proposals.
1102 	 */
1103 	private ILabelProvider labelProvider;
1104 
1105 	/*
1106 	 * The control for which content proposals are provided.
1107 	 */
1108 	private Control control;
1109 
1110 	/*
1111 	 * The adapter used to extract the String contents from an arbitrary
1112 	 * control.
1113 	 */
1114 	private IControlContentAdapter controlContentAdapter;
1115 
1116 	/*
1117 	 * The popup used to show proposals.
1118 	 */
1119 	private ContentProposalPopup popup;
1120 
1121 	/*
1122 	 * The keystroke that signifies content proposals should be shown.
1123 	 */
1124 	private KeyStroke triggerKeyStroke;
1125 
1126 	/*
1127 	 * The String containing characters that auto-activate the popup.
1128 	 */
1129 	private String autoActivateString;
1130 
1131 	/*
1132 	 * Integer that indicates how an accepted proposal should affect the
1133 	 * control. One of PROPOSAL_IGNORE, PROPOSAL_INSERT, or PROPOSAL_REPLACE.
1134 	 * Default value is PROPOSAL_INSERT.
1135 	 */
1136 	private int proposalAcceptanceStyle = PROPOSAL_INSERT;
1137 
1138 	/*
1139 	 * A boolean that indicates whether key events received while the proposal
1140 	 * popup is open should also be propagated to the control. Default value is
1141 	 * true.
1142 	 */
1143 	private boolean propagateKeys = true;
1144 
1145 	/*
1146 	 * Integer that indicates the filtering style. One of FILTER_CHARACTER,
1147 	 * FILTER_CUMULATIVE, FILTER_NONE.
1148 	 */
1149 	private int filterStyle = FILTER_NONE;
1150 
1151 	/*
1152 	 * The listener we install on the control.
1153 	 */
1154 	private Listener controlListener;
1155 
1156 	/*
1157 	 * The list of IContentProposalListener listeners.
1158 	 */
1159 	private ListenerList<IContentProposalListener> proposalListeners = new ListenerList<>();
1160 
1161 	/*
1162 	 * The list of IContentProposalListener2 listeners.
1163 	 */
1164 	private ListenerList<IContentProposalListener2> proposalListeners2 = new ListenerList<>();
1165 
1166 	/*
1167 	 * Flag that indicates whether the adapter is enabled. In some cases,
1168 	 * adapters may be installed but depend upon outside state.
1169 	 */
1170 	private boolean isEnabled = true;
1171 
1172 	/*
1173 	 * The delay in milliseconds used when autoactivating the popup.
1174 	 */
1175 	private int autoActivationDelay = 0;
1176 
1177 	/*
1178 	 * A boolean indicating whether a keystroke has been received. Used to see
1179 	 * if an autoactivation delay was interrupted by a keystroke.
1180 	 */
1181 	private boolean receivedKeyDown;
1182 
1183 	/*
1184 	 * The desired size in pixels of the proposal popup.
1185 	 */
1186 	private Point popupSize;
1187 
1188 	/*
1189 	 * The remembered position of the insertion position. Not all controls will
1190 	 * restore the insertion position if the proposal popup gets focus, so we
1191 	 * need to remember it.
1192 	 */
1193 	private int insertionPos = -1;
1194 
1195 	/*
1196 	 * The remembered selection range. Not all controls will restore the
1197 	 * selection position if the proposal popup gets focus, so we need to
1198 	 * remember it.
1199 	 */
1200 	private Point selectionRange = new Point(-1, -1);
1201 
1202 	/*
1203 	 * A flag that indicates that we are watching modify events
1204 	 */
1205 	private boolean watchModify = false;
1206 
1207 	/**
1208 	 * Construct a content proposal adapter that can assist the user with
1209 	 * choosing content for the field.
1210 	 *
1211 	 * @param control
1212 	 *            the control for which the adapter is providing content assist.
1213 	 *            May not be <code>null</code>.
1214 	 * @param controlContentAdapter
1215 	 *            the <code>IControlContentAdapter</code> used to obtain and
1216 	 *            update the control's contents as proposals are accepted. May
1217 	 *            not be <code>null</code>.
1218 	 * @param proposalProvider
1219 	 *            the <code>IContentProposalProvider</code> used to obtain
1220 	 *            content proposals for this control, or <code>null</code> if
1221 	 *            no content proposal is available.
1222 	 * @param keyStroke
1223 	 *            the keystroke that will invoke the content proposal popup. If
1224 	 *            this value is <code>null</code>, then proposals will be
1225 	 *            activated automatically when any of the auto activation
1226 	 *            characters are typed.
1227 	 * @param autoActivationCharacters
1228 	 *            An array of characters that trigger auto-activation of content
1229 	 *            proposal. If specified, these characters will trigger
1230 	 *            auto-activation of the proposal popup, regardless of whether
1231 	 *            an explicit invocation keyStroke was specified. If this
1232 	 *            parameter is <code>null</code>, then only a specified
1233 	 *            keyStroke will invoke content proposal. If this parameter is
1234 	 *            <code>null</code> and the keyStroke parameter is
1235 	 *            <code>null</code>, then all alphanumeric characters will
1236 	 *            auto-activate content proposal.
1237 	 */
ContentProposalAdapter(Control control, IControlContentAdapter controlContentAdapter, IContentProposalProvider proposalProvider, KeyStroke keyStroke, char[] autoActivationCharacters)1238 	public ContentProposalAdapter(Control control,
1239 			IControlContentAdapter controlContentAdapter,
1240 			IContentProposalProvider proposalProvider, KeyStroke keyStroke,
1241 			char[] autoActivationCharacters) {
1242 		super();
1243 		// We always assume the control and content adapter are valid.
1244 		Assert.isNotNull(control);
1245 		Assert.isNotNull(controlContentAdapter);
1246 		this.control = control;
1247 		this.controlContentAdapter = controlContentAdapter;
1248 
1249 		// The rest of these may be null
1250 		this.proposalProvider = proposalProvider;
1251 		this.triggerKeyStroke = keyStroke;
1252 		if (autoActivationCharacters != null) {
1253 			this.autoActivateString = new String(autoActivationCharacters);
1254 		}
1255 		addControlListener(control);
1256 	}
1257 
1258 	/**
1259 	 * Get the control on which the content proposal adapter is installed.
1260 	 *
1261 	 * @return the control on which the proposal adapter is installed.
1262 	 */
getControl()1263 	public Control getControl() {
1264 		return control;
1265 	}
1266 
1267 	/**
1268 	 * Get the label provider that is used to show proposals.
1269 	 *
1270 	 * @return the {@link ILabelProvider} used to show proposals, or
1271 	 *         <code>null</code> if one has not been installed.
1272 	 */
getLabelProvider()1273 	public ILabelProvider getLabelProvider() {
1274 		return labelProvider;
1275 	}
1276 
1277 	/**
1278 	 * Return a boolean indicating whether the receiver is enabled.
1279 	 *
1280 	 * @return <code>true</code> if the adapter is enabled, and
1281 	 *         <code>false</code> if it is not.
1282 	 */
isEnabled()1283 	public boolean isEnabled() {
1284 		return isEnabled;
1285 	}
1286 
1287 	/**
1288 	 * Set the label provider that is used to show proposals. The lifecycle of
1289 	 * the specified label provider is not managed by this adapter. Clients must
1290 	 * dispose the label provider when it is no longer needed.
1291 	 *
1292 	 * @param labelProvider
1293 	 *            the {@link ILabelProvider} used to show proposals.
1294 	 */
setLabelProvider(ILabelProvider labelProvider)1295 	public void setLabelProvider(ILabelProvider labelProvider) {
1296 		this.labelProvider = labelProvider;
1297 	}
1298 
1299 	/**
1300 	 * Return the proposal provider that provides content proposals given the
1301 	 * current content of the field. A value of <code>null</code> indicates
1302 	 * that there are no content proposals available for the field.
1303 	 *
1304 	 * @return the {@link IContentProposalProvider} used to show proposals. May
1305 	 *         be <code>null</code>.
1306 	 */
getContentProposalProvider()1307 	public IContentProposalProvider getContentProposalProvider() {
1308 		return proposalProvider;
1309 	}
1310 
1311 	/**
1312 	 * Set the content proposal provider that is used to show proposals.
1313 	 *
1314 	 * @param proposalProvider
1315 	 *            the {@link IContentProposalProvider} used to show proposals
1316 	 */
setContentProposalProvider( IContentProposalProvider proposalProvider)1317 	public void setContentProposalProvider(
1318 			IContentProposalProvider proposalProvider) {
1319 		this.proposalProvider = proposalProvider;
1320 	}
1321 
1322 	/**
1323 	 * Return the array of characters on which the popup is autoactivated.
1324 	 *
1325 	 * @return An array of characters that trigger auto-activation of content
1326 	 *         proposal. If specified, these characters will trigger
1327 	 *         auto-activation of the proposal popup, regardless of whether an
1328 	 *         explicit invocation keyStroke was specified. If this parameter is
1329 	 *         <code>null</code>, then only a specified keyStroke will invoke
1330 	 *         content proposal. If this value is <code>null</code> and the
1331 	 *         keyStroke value is <code>null</code>, then all alphanumeric
1332 	 *         characters will auto-activate content proposal.
1333 	 */
getAutoActivationCharacters()1334 	public char[] getAutoActivationCharacters() {
1335 		if (autoActivateString == null) {
1336 			return null;
1337 		}
1338 		return autoActivateString.toCharArray();
1339 	}
1340 
1341 	/**
1342 	 * Set the array of characters that will trigger autoactivation of the
1343 	 * popup.
1344 	 *
1345 	 * @param autoActivationCharacters
1346 	 *            An array of characters that trigger auto-activation of content
1347 	 *            proposal. If specified, these characters will trigger
1348 	 *            auto-activation of the proposal popup, regardless of whether
1349 	 *            an explicit invocation keyStroke was specified. If this
1350 	 *            parameter is <code>null</code>, then only a specified
1351 	 *            keyStroke will invoke content proposal. If this parameter is
1352 	 *            <code>null</code> and the keyStroke value is
1353 	 *            <code>null</code>, then all alphanumeric characters will
1354 	 *            auto-activate content proposal.
1355 	 *
1356 	 */
setAutoActivationCharacters(char[] autoActivationCharacters)1357 	public void setAutoActivationCharacters(char[] autoActivationCharacters) {
1358 		if (autoActivationCharacters == null) {
1359 			this.autoActivateString = null;
1360 		} else {
1361 			this.autoActivateString = new String(autoActivationCharacters);
1362 		}
1363 	}
1364 
1365 	/**
1366 	 * Set the delay, in milliseconds, used before any autoactivation is
1367 	 * triggered.
1368 	 *
1369 	 * @return the time in milliseconds that will pass before a popup is
1370 	 *         automatically opened
1371 	 */
getAutoActivationDelay()1372 	public int getAutoActivationDelay() {
1373 		return autoActivationDelay;
1374 
1375 	}
1376 
1377 	/**
1378 	 * Set the delay, in milliseconds, used before autoactivation is triggered.
1379 	 *
1380 	 * @param delay
1381 	 *            the time in milliseconds that will pass before a popup is
1382 	 *            automatically opened
1383 	 */
setAutoActivationDelay(int delay)1384 	public void setAutoActivationDelay(int delay) {
1385 		autoActivationDelay = delay;
1386 
1387 	}
1388 
1389 	/**
1390 	 * Get the integer style that indicates how an accepted proposal affects the
1391 	 * control's content.
1392 	 *
1393 	 * @return a constant indicating how an accepted proposal should affect the
1394 	 *         control's content. Should be one of <code>PROPOSAL_INSERT</code>,
1395 	 *         <code>PROPOSAL_REPLACE</code>, or <code>PROPOSAL_IGNORE</code>.
1396 	 *         (Default is <code>PROPOSAL_INSERT</code>).
1397 	 */
getProposalAcceptanceStyle()1398 	public int getProposalAcceptanceStyle() {
1399 		return proposalAcceptanceStyle;
1400 	}
1401 
1402 	/**
1403 	 * Set the integer style that indicates how an accepted proposal affects the
1404 	 * control's content.
1405 	 *
1406 	 * @param acceptance
1407 	 *            a constant indicating how an accepted proposal should affect
1408 	 *            the control's content. Should be one of
1409 	 *            <code>PROPOSAL_INSERT</code>, <code>PROPOSAL_REPLACE</code>,
1410 	 *            or <code>PROPOSAL_IGNORE</code>
1411 	 */
setProposalAcceptanceStyle(int acceptance)1412 	public void setProposalAcceptanceStyle(int acceptance) {
1413 		proposalAcceptanceStyle = acceptance;
1414 	}
1415 
1416 	/**
1417 	 * Return the integer style that indicates how keystrokes affect the content
1418 	 * of the proposal popup while it is open.
1419 	 *
1420 	 * @return a constant indicating how keystrokes in the proposal popup affect
1421 	 *         filtering of the proposals shown. <code>FILTER_NONE</code>
1422 	 *         specifies that no filtering will occur in the content proposal
1423 	 *         list as keys are typed. <code>FILTER_CHARACTER</code> specifies
1424 	 *         the content of the popup will be filtered by the most recently
1425 	 *         typed character. <code>FILTER_CUMULATIVE</code> is deprecated
1426 	 *         and no longer recommended. It specifies that the content of the
1427 	 *         popup will be filtered by a string containing all the characters
1428 	 *         typed since the popup has been open. The default is
1429 	 *         <code>FILTER_NONE</code>.
1430 	 */
getFilterStyle()1431 	public int getFilterStyle() {
1432 		return filterStyle;
1433 	}
1434 
1435 	/**
1436 	 * Set the integer style that indicates how keystrokes affect the content of
1437 	 * the proposal popup while it is open. Popup-based filtering is useful for
1438 	 * narrowing and navigating the list of proposals provided once the popup is
1439 	 * open. Filtering of the proposals will occur even when the control content
1440 	 * is not affected by user typing. Note that automatic filtering is not used
1441 	 * to achieve content-sensitive filtering such as auto-completion. Filtering
1442 	 * that is sensitive to changes in the control content should be performed
1443 	 * by the supplied {@link IContentProposalProvider}.
1444 	 *
1445 	 * @param filterStyle
1446 	 *            a constant indicating how keystrokes received in the proposal
1447 	 *            popup affect filtering of the proposals shown.
1448 	 *            <code>FILTER_NONE</code> specifies that no automatic
1449 	 *            filtering of the content proposal list will occur as keys are
1450 	 *            typed in the popup. <code>FILTER_CHARACTER</code> specifies
1451 	 *            that the content of the popup will be filtered by the most
1452 	 *            recently typed character. <code>FILTER_CUMULATIVE</code> is
1453 	 *            deprecated and no longer recommended. It specifies that the
1454 	 *            content of the popup will be filtered by a string containing
1455 	 *            all the characters typed since the popup has been open.
1456 	 */
setFilterStyle(int filterStyle)1457 	public void setFilterStyle(int filterStyle) {
1458 		this.filterStyle = filterStyle;
1459 	}
1460 
1461 	/**
1462 	 * Return the size, in pixels, of the content proposal popup.
1463 	 *
1464 	 * @return a Point specifying the last width and height, in pixels, of the
1465 	 *         content proposal popup.
1466 	 */
getPopupSize()1467 	public Point getPopupSize() {
1468 		return popupSize;
1469 	}
1470 
1471 	/**
1472 	 * Set the size, in pixels, of the content proposal popup. This size will be
1473 	 * used the next time the content proposal popup is opened.
1474 	 *
1475 	 * @param size
1476 	 *            a Point specifying the desired width and height, in pixels, of
1477 	 *            the content proposal popup.
1478 	 */
setPopupSize(Point size)1479 	public void setPopupSize(Point size) {
1480 		popupSize = size;
1481 	}
1482 
1483 	/**
1484 	 * Get the boolean that indicates whether key events (including
1485 	 * auto-activation characters) received by the content proposal popup should
1486 	 * also be propagated to the adapted control when the proposal popup is
1487 	 * open.
1488 	 *
1489 	 * @return a boolean that indicates whether key events (including
1490 	 *         auto-activation characters) should be propagated to the adapted
1491 	 *         control when the proposal popup is open. Default value is
1492 	 *         <code>true</code>.
1493 	 */
getPropagateKeys()1494 	public boolean getPropagateKeys() {
1495 		return propagateKeys;
1496 	}
1497 
1498 	/**
1499 	 * Set the boolean that indicates whether key events (including
1500 	 * auto-activation characters) received by the content proposal popup should
1501 	 * also be propagated to the adapted control when the proposal popup is
1502 	 * open.
1503 	 *
1504 	 * @param propagateKeys
1505 	 *            a boolean that indicates whether key events (including
1506 	 *            auto-activation characters) should be propagated to the
1507 	 *            adapted control when the proposal popup is open.
1508 	 */
setPropagateKeys(boolean propagateKeys)1509 	public void setPropagateKeys(boolean propagateKeys) {
1510 		this.propagateKeys = propagateKeys;
1511 	}
1512 
1513 	/**
1514 	 * Return the content adapter that can get or retrieve the text contents
1515 	 * from the adapter's control. This method is used when a client, such as a
1516 	 * content proposal listener, needs to update the control's contents
1517 	 * manually.
1518 	 *
1519 	 * @return the {@link IControlContentAdapter} which can update the control
1520 	 *         text.
1521 	 */
getControlContentAdapter()1522 	public IControlContentAdapter getControlContentAdapter() {
1523 		return controlContentAdapter;
1524 	}
1525 
1526 	/**
1527 	 * Set the boolean flag that determines whether the adapter is enabled.
1528 	 *
1529 	 * @param enabled
1530 	 *            <code>true</code> if the adapter is enabled and responding
1531 	 *            to user input, <code>false</code> if it is ignoring user
1532 	 *            input.
1533 	 *
1534 	 */
setEnabled(boolean enabled)1535 	public void setEnabled(boolean enabled) {
1536 		// If we are disabling it while it's proposing content, close the
1537 		// content proposal popup.
1538 		if (isEnabled && !enabled) {
1539 			if (popup != null) {
1540 				popup.close();
1541 			}
1542 		}
1543 		isEnabled = enabled;
1544 	}
1545 
1546 	/**
1547 	 * Add the specified listener to the list of content proposal listeners that
1548 	 * are notified when content proposals are chosen.
1549 	 *
1550 	 * @param listener
1551 	 *            the IContentProposalListener to be added as a listener. Must
1552 	 *            not be <code>null</code>. If an attempt is made to register
1553 	 *            an instance which is already registered with this instance,
1554 	 *            this method has no effect.
1555 	 *
1556 	 * @see org.eclipse.jface.fieldassist.IContentProposalListener
1557 	 */
addContentProposalListener(IContentProposalListener listener)1558 	public void addContentProposalListener(IContentProposalListener listener) {
1559 		proposalListeners.add(listener);
1560 	}
1561 
1562 	/**
1563 	 * Removes the specified listener from the list of content proposal
1564 	 * listeners that are notified when content proposals are chosen.
1565 	 *
1566 	 * @param listener
1567 	 *            the IContentProposalListener to be removed as a listener. Must
1568 	 *            not be <code>null</code>. If the listener has not already
1569 	 *            been registered, this method has no effect.
1570 	 *
1571 	 * @since 3.3
1572 	 * @see org.eclipse.jface.fieldassist.IContentProposalListener
1573 	 */
removeContentProposalListener(IContentProposalListener listener)1574 	public void removeContentProposalListener(IContentProposalListener listener) {
1575 		proposalListeners.remove(listener);
1576 	}
1577 
1578 	/**
1579 	 * Add the specified listener to the list of content proposal listeners that
1580 	 * are notified when a content proposal popup is opened or closed.
1581 	 *
1582 	 * @param listener
1583 	 *            the IContentProposalListener2 to be added as a listener. Must
1584 	 *            not be <code>null</code>. If an attempt is made to register
1585 	 *            an instance which is already registered with this instance,
1586 	 *            this method has no effect.
1587 	 *
1588 	 * @since 3.3
1589 	 * @see org.eclipse.jface.fieldassist.IContentProposalListener2
1590 	 */
addContentProposalListener(IContentProposalListener2 listener)1591 	public void addContentProposalListener(IContentProposalListener2 listener) {
1592 		proposalListeners2.add(listener);
1593 	}
1594 
1595 	/**
1596 	 * Remove the specified listener from the list of content proposal listeners
1597 	 * that are notified when a content proposal popup is opened or closed.
1598 	 *
1599 	 * @param listener
1600 	 *            the IContentProposalListener2 to be removed as a listener.
1601 	 *            Must not be <code>null</code>. If the listener has not
1602 	 *            already been registered, this method has no effect.
1603 	 *
1604 	 * @since 3.3
1605 	 * @see org.eclipse.jface.fieldassist.IContentProposalListener2
1606 	 */
removeContentProposalListener(IContentProposalListener2 listener)1607 	public void removeContentProposalListener(IContentProposalListener2 listener) {
1608 		proposalListeners2.remove(listener);
1609 	}
1610 
1611 	/*
1612 	 * Add our listener to the control. Debug information to be left in until
1613 	 * this support is stable on all platforms.
1614 	 */
addControlListener(Control control)1615 	private void addControlListener(Control control) {
1616 		if (DEBUG) {
1617 			System.out
1618 					.println("ContentProposalListener#installControlListener()"); //$NON-NLS-1$
1619 		}
1620 
1621 		if (controlListener != null) {
1622 			return;
1623 		}
1624 		controlListener = new Listener() {
1625 			@Override
1626 			public void handleEvent(Event e) {
1627 				if (!isEnabled) {
1628 					return;
1629 				}
1630 
1631 				switch (e.type) {
1632 				case SWT.Traverse:
1633 				case SWT.KeyDown:
1634 					if (DEBUG) {
1635 						StringBuilder sb;
1636 						if (e.type == SWT.Traverse) {
1637 							sb = new StringBuilder("Traverse"); //$NON-NLS-1$
1638 						} else {
1639 							sb = new StringBuilder("KeyDown"); //$NON-NLS-1$
1640 						}
1641 						sb.append(" received by adapter"); //$NON-NLS-1$
1642 						dump(sb.toString(), e);
1643 					}
1644 					// If the popup is open, it gets first shot at the
1645 					// keystroke and should set the doit flags appropriately.
1646 					if (popup != null) {
1647 						popup.getTargetControlListener().handleEvent(e);
1648 						if (DEBUG) {
1649 							StringBuilder sb;
1650 							if (e.type == SWT.Traverse) {
1651 								sb = new StringBuilder("Traverse"); //$NON-NLS-1$
1652 							} else {
1653 								sb = new StringBuilder("KeyDown"); //$NON-NLS-1$
1654 							}
1655 							sb.append(" after being handled by popup"); //$NON-NLS-1$
1656 							dump(sb.toString(), e);
1657 						}
1658 						// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=192633
1659 						// If the popup is open and this is a valid character, we
1660 						// want to watch for the modified text.
1661 						if (propagateKeys && e.character != 0)
1662 							watchModify = true;
1663 
1664 						return;
1665 					}
1666 
1667 					// We were only listening to traverse events for the popup
1668 					if (e.type == SWT.Traverse) {
1669 						// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=520372
1670 						// The popup is null so record tab as a means to interrupt
1671 						// any autoactivation that is pending due to autoactivation
1672 						// delay.
1673 						if (popup == null) {
1674 							switch (e.detail) {
1675 							case SWT.TRAVERSE_TAB_NEXT:
1676 							case SWT.TRAVERSE_TAB_PREVIOUS:
1677 								receivedKeyDown = true;
1678 								break;
1679 							}
1680 						}
1681 						return;
1682 					}
1683 
1684 					// The popup is not open. We are looking at keydown events
1685 					// for a trigger to open the popup.
1686 					if (triggerKeyStroke != null) {
1687 						// Either there are no modifiers for the trigger and we
1688 						// check the character field...
1689 						if ((triggerKeyStroke.getModifierKeys() == KeyStroke.NO_KEY && triggerKeyStroke
1690 								.getNaturalKey() == e.character)
1691 								||
1692 								// ...or there are modifiers, in which case the
1693 								// keycode and state must match
1694 								(triggerKeyStroke.getNaturalKey() == e.keyCode && ((triggerKeyStroke
1695 										.getModifierKeys() & e.stateMask) == triggerKeyStroke
1696 										.getModifierKeys()))) {
1697 							// We never propagate the keystroke for an explicit
1698 							// keystroke invocation of the popup
1699 							e.doit = false;
1700 							openProposalPopup(false);
1701 							return;
1702 						}
1703 					}
1704 					/*
1705 					 * The triggering keystroke was not invoked. If a character
1706 					 * was typed, compare it to the autoactivation characters.
1707 					 */
1708 					if (e.character != 0) {
1709 						if (autoActivateString != null) {
1710 							if (autoActivateString.indexOf(e.character) >= 0) {
1711 								autoActivate();
1712 							} else {
1713 								// No autoactivation occurred, so record the key
1714 								// down as a means to interrupt any
1715 								// autoactivation that is pending due to
1716 								// autoactivation delay.
1717 								receivedKeyDown = true;
1718 								// watch the modify so we can close the popup in
1719 								// cases where there is no longer a trigger
1720 								// character in the content
1721 								watchModify = true;
1722 							}
1723 						} else {
1724 							// The autoactivate string is null. If the trigger
1725 							// is also null, we want to act on any modification
1726 							// to the content. Set a flag so we'll catch this
1727 							// in the modify event.
1728 							if (triggerKeyStroke == null) {
1729 								watchModify = true;
1730 							}
1731 
1732 							// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=520372
1733 							// mimic close cases of popup in TargetControlListener
1734 							if (popup == null) {
1735 								switch (e.character) {
1736 								case SWT.CR:
1737 								case SWT.LF:
1738 								case SWT.ESC:
1739 									// Interrupt any autoactivation that is pending due to
1740 									// autoactivation delay.
1741 									receivedKeyDown = true;
1742 									break;
1743 								}
1744 							}
1745 						}
1746 					} else {
1747 						// A non-character key has been pressed. Interrupt any
1748 						// autoactivation that is pending due to autoactivation delay.
1749 						receivedKeyDown = true;
1750 					}
1751 					break;
1752 
1753 
1754 					// There are times when we want to monitor content changes
1755 					// rather than individual keystrokes to determine whether
1756 					// the popup should be closed or opened based on the entire
1757 					// content of the control.
1758 					// The watchModify flag ensures that we don't autoactivate if
1759 					// the content change was caused by something other than typing.
1760 					// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=183650
1761 					case SWT.Modify:
1762 						if (allowsAutoActivate() && watchModify) {
1763 							if (DEBUG) {
1764 								dump("Modify event triggers popup open or close", e); //$NON-NLS-1$
1765 							}
1766 							watchModify = false;
1767 							// We are in autoactivation mode, either for specific
1768 							// characters or for all characters. In either case,
1769 							// we should close the proposal popup when there is no
1770 							// content in the control.
1771 							if (isControlContentEmpty()) {
1772 								// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=192633
1773 								closeProposalPopup();
1774 							} else {
1775 								// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=147377
1776 								// Given that we will close the popup when there are
1777 								// no valid proposals, we must consider reopening it on any
1778 								// content change when there are no particular autoActivation
1779 								// characters
1780 								if (autoActivateString == null) {
1781 									autoActivate();
1782 								} else {
1783 									// Autoactivation characters are defined, but this
1784 									// modify event does not involve one of them.  See
1785 									// if any of the autoactivation characters are left
1786 									// in the content and close the popup if none remain.
1787 									if (!shouldPopupRemainOpen())
1788 										closeProposalPopup();
1789 								}
1790 							}
1791 						}
1792 						break;
1793 				default:
1794 					break;
1795 				}
1796 			}
1797 
1798 			/**
1799 			 * Dump the given events to "standard" output.
1800 			 *
1801 			 * @param who
1802 			 *            who is dumping the event
1803 			 * @param e
1804 			 *            the event
1805 			 */
1806 			private void dump(String who, Event e) {
1807 				StringBuilder sb = new StringBuilder(
1808 						"--- [ContentProposalAdapter]\n"); //$NON-NLS-1$
1809 				sb.append(who);
1810 				sb.append(" - e: keyCode=" + e.keyCode + hex(e.keyCode)); //$NON-NLS-1$
1811 				sb.append("; character=" + e.character + hex(e.character)); //$NON-NLS-1$
1812 				sb.append("; stateMask=" + e.stateMask + hex(e.stateMask)); //$NON-NLS-1$
1813 				sb.append("; doit=" + e.doit); //$NON-NLS-1$
1814 				sb.append("; detail=" + e.detail + hex(e.detail)); //$NON-NLS-1$
1815 				sb.append("; widget=" + e.widget); //$NON-NLS-1$
1816 				System.out.println(sb);
1817 			}
1818 
1819 			private String hex(int i) {
1820 				return "[0x" + Integer.toHexString(i) + ']'; //$NON-NLS-1$
1821 			}
1822 		};
1823 		control.addListener(SWT.KeyDown, controlListener);
1824 		control.addListener(SWT.Traverse, controlListener);
1825 		control.addListener(SWT.Modify, controlListener);
1826 
1827 		if (DEBUG) {
1828 			System.out
1829 					.println("ContentProposalAdapter#installControlListener() - installed"); //$NON-NLS-1$
1830 		}
1831 	}
1832 
1833 	/**
1834 	 * Open the proposal popup and display the proposals provided by the
1835 	 * proposal provider. If there are no proposals to be shown, do not show the
1836 	 * popup. This method returns immediately. That is, it does not wait for the
1837 	 * popup to open or a proposal to be selected.
1838 	 *
1839 	 * @param autoActivated
1840 	 *            a boolean indicating whether the popup was autoactivated. If
1841 	 *            false, a beep will sound when no proposals can be shown.
1842 	 */
openProposalPopup(boolean autoActivated)1843 	private void openProposalPopup(boolean autoActivated) {
1844 		if (isValid()) {
1845 			if (popup == null) {
1846 				// Check whether there are any proposals to be shown.
1847 				recordCursorPosition(); // must be done before getting proposals
1848 				IContentProposal[] proposals = getProposals();
1849 				if (proposals == null)
1850 					return;
1851 				if (proposals.length > 0) {
1852 					if (DEBUG) {
1853 						System.out.println("POPUP OPENED BY PRECEDING EVENT"); //$NON-NLS-1$
1854 					}
1855 					recordCursorPosition();
1856 					popup = new ContentProposalPopup(null, proposals);
1857 					popup.open();
1858 					popup.getShell().addDisposeListener(event -> popup = null);
1859 					internalPopupOpened();
1860 					notifyPopupOpened();
1861 				} else if (!autoActivated) {
1862 					getControl().getDisplay().beep();
1863 				}
1864 			}
1865 		}
1866 	}
1867 
1868 	/**
1869 	 * Open the proposal popup and display the proposals provided by the
1870 	 * proposal provider. This method returns immediately. That is, it does not
1871 	 * wait for a proposal to be selected. This method is used by subclasses to
1872 	 * explicitly invoke the opening of the popup. If there are no proposals to
1873 	 * show, the popup will not open and a beep will be sounded.
1874 	 */
openProposalPopup()1875 	protected void openProposalPopup() {
1876 		openProposalPopup(false);
1877 	}
1878 
1879 	/**
1880 	 * Close the proposal popup without accepting a proposal. This method
1881 	 * returns immediately, and has no effect if the proposal popup was not
1882 	 * open. This method is used by subclasses to explicitly close the popup
1883 	 * based on additional logic.
1884 	 *
1885 	 * @since 3.3
1886 	 */
closeProposalPopup()1887 	protected void closeProposalPopup() {
1888 		if (popup != null) {
1889 			popup.close();
1890 		}
1891 	}
1892 
1893 	/*
1894 	 * A content proposal has been accepted. Update the control contents
1895 	 * accordingly and notify any listeners.
1896 	 *
1897 	 * @param proposal the accepted proposal
1898 	 */
proposalAccepted(IContentProposal proposal)1899 	private void proposalAccepted(IContentProposal proposal) {
1900 		switch (proposalAcceptanceStyle) {
1901 		case (PROPOSAL_REPLACE):
1902 			setControlContent(proposal.getContent(), proposal
1903 					.getCursorPosition());
1904 			break;
1905 		case (PROPOSAL_INSERT):
1906 			insertControlContent(proposal.getContent(), proposal
1907 					.getCursorPosition());
1908 			break;
1909 		default:
1910 			// do nothing. Typically a listener is installed to handle this in
1911 			// a custom way.
1912 			break;
1913 		}
1914 
1915 		// In all cases, notify listeners of an accepted proposal.
1916 		notifyProposalAccepted(proposal);
1917 	}
1918 
1919 	/*
1920 	 * Set the text content of the control to the specified text, setting the
1921 	 * cursorPosition at the desired location within the new contents.
1922 	 */
setControlContent(String text, int cursorPosition)1923 	private void setControlContent(String text, int cursorPosition) {
1924 		if (isValid()) {
1925 			// should already be false, but just in case.
1926 			watchModify = false;
1927 			controlContentAdapter.setControlContents(control, text,
1928 					cursorPosition);
1929 		}
1930 	}
1931 
1932 	/*
1933 	 * Insert the specified text into the control content, setting the
1934 	 * cursorPosition at the desired location within the new contents.
1935 	 */
insertControlContent(String text, int cursorPosition)1936 	private void insertControlContent(String text, int cursorPosition) {
1937 		if (isValid()) {
1938 			// should already be false, but just in case.
1939 			watchModify = false;
1940 			// Not all controls preserve their selection index when they lose
1941 			// focus, so we must set it explicitly here to what it was before
1942 			// the popup opened.
1943 			// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=127108
1944 			// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=139063
1945 			if (controlContentAdapter instanceof IControlContentAdapter2
1946 					&& selectionRange.x != -1) {
1947 				((IControlContentAdapter2) controlContentAdapter).setSelection(
1948 						control, selectionRange);
1949 			} else if (insertionPos != -1) {
1950 				controlContentAdapter.setCursorPosition(control, insertionPos);
1951 			}
1952 			controlContentAdapter.insertControlContents(control, text,
1953 					cursorPosition);
1954 		}
1955 	}
1956 
1957 	/*
1958 	 * Check that the control and content adapter are valid.
1959 	 */
isValid()1960 	private boolean isValid() {
1961 		return control != null && !control.isDisposed()
1962 				&& controlContentAdapter != null;
1963 	}
1964 
1965 	/*
1966 	 * Record the control's cursor position.
1967 	 */
recordCursorPosition()1968 	private void recordCursorPosition() {
1969 		if (isValid()) {
1970 			IControlContentAdapter adapter = getControlContentAdapter();
1971 			insertionPos = adapter.getCursorPosition(control);
1972 			// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=139063
1973 			if (adapter instanceof IControlContentAdapter2) {
1974 				selectionRange = ((IControlContentAdapter2) adapter)
1975 						.getSelection(control);
1976 			}
1977 
1978 		}
1979 	}
1980 
1981 	/*
1982 	 * Get the proposals from the proposal provider. Gets all of the proposals
1983 	 * without doing any filtering.
1984 	 */
getProposals()1985 	private IContentProposal[] getProposals() {
1986 		if (proposalProvider == null || !isValid()) {
1987 			return null;
1988 		}
1989 		if (DEBUG) {
1990 			System.out.println(">>> obtaining proposals from provider"); //$NON-NLS-1$
1991 		}
1992 		int position = insertionPos;
1993 		if (position == -1) {
1994 			position = getControlContentAdapter().getCursorPosition(
1995 					getControl());
1996 		}
1997 		String contents = getControlContentAdapter().getControlContents(
1998 				getControl());
1999 		return proposalProvider.getProposals(contents,
2000 				position);
2001 	}
2002 
2003 	/**
2004 	 * Autoactivation has been triggered. Open the popup using any specified
2005 	 * delay.
2006 	 */
autoActivate()2007 	private void autoActivate() {
2008 		if (autoActivationDelay > 0) {
2009 			Runnable runnable = () -> {
2010 				receivedKeyDown = false;
2011 				try {
2012 					Thread.sleep(autoActivationDelay);
2013 				} catch (InterruptedException e) {
2014 				}
2015 				if (!isValid() || receivedKeyDown) {
2016 					return;
2017 				}
2018 				getControl().getDisplay().syncExec(() -> openProposalPopup(true));
2019 			};
2020 			Thread t = new Thread(runnable);
2021 			t.start();
2022 		} else {
2023 			// Since we do not sleep, we must open the popup
2024 			// in an async exec. This is necessary because
2025 			// this method may be called in the middle of handling
2026 			// some event that will cause the cursor position or
2027 			// other important info to change as a result of this
2028 			// event occurring.
2029 			getControl().getDisplay().asyncExec(() -> {
2030 				if (isValid()) {
2031 					openProposalPopup(true);
2032 				}
2033 			});
2034 		}
2035 	}
2036 
2037 	/*
2038 	 * A proposal has been accepted. Notify interested listeners.
2039 	 */
notifyProposalAccepted(IContentProposal proposal)2040 	private void notifyProposalAccepted(IContentProposal proposal) {
2041 		if (DEBUG) {
2042 			System.out.println("Notify listeners - proposal accepted."); //$NON-NLS-1$
2043 		}
2044 		for (IContentProposalListener l : proposalListeners) {
2045 			l.proposalAccepted(proposal);
2046 		}
2047 	}
2048 
2049 	/*
2050 	 * The proposal popup has opened. Notify interested listeners.
2051 	 */
notifyPopupOpened()2052 	private void notifyPopupOpened() {
2053 		if (DEBUG) {
2054 			System.out.println("Notify listeners - popup opened."); //$NON-NLS-1$
2055 		}
2056 		for (IContentProposalListener2 l : proposalListeners2) {
2057 			l.proposalPopupOpened(this);
2058 		}
2059 	}
2060 
2061 	/*
2062 	 * The proposal popup has closed. Notify interested listeners.
2063 	 */
notifyPopupClosed()2064 	private void notifyPopupClosed() {
2065 		if (DEBUG) {
2066 			System.out.println("Notify listeners - popup closed."); //$NON-NLS-1$
2067 		}
2068 		for (IContentProposalListener2 l : proposalListeners2) {
2069 			l.proposalPopupClosed(this);
2070 		}
2071 	}
2072 
2073 	/**
2074 	 * Returns whether the content proposal popup has the focus. This includes
2075 	 * both the primary popup and any secondary info popup that may have focus.
2076 	 *
2077 	 * @return <code>true</code> if the proposal popup or its secondary info
2078 	 *         popup has the focus
2079 	 * @since 3.4
2080 	 */
hasProposalPopupFocus()2081 	public boolean hasProposalPopupFocus() {
2082 		return popup != null && popup.hasFocus();
2083 	}
2084 
2085 	/*
2086 	 * Return whether the control content is empty
2087 	 */
isControlContentEmpty()2088 	private boolean isControlContentEmpty() {
2089 		return getControlContentAdapter().getControlContents(getControl())
2090 				.length() == 0;
2091 	}
2092 
2093 	/*
2094 	 * The popup has just opened, but listeners have not yet
2095 	 * been notified.  Perform any cleanup that is needed.
2096 	 */
internalPopupOpened()2097 	private void internalPopupOpened() {
2098 		// see https://bugs.eclipse.org/bugs/show_bug.cgi?id=243612
2099 		if (control instanceof Combo) {
2100 			((Combo)control).setListVisible(false);
2101 		}
2102 	}
2103 
2104 	/*
2105 	 * Return whether a proposal popup should remain open.
2106 	 * If it was autoactivated by specific characters, and
2107 	 * none of those characters remain, then it should not remain
2108 	 * open.  This method should not be used to determine
2109 	 * whether autoactivation has occurred or should occur, only whether
2110 	 * the circumstances would dictate that a popup remain open.
2111 	 */
shouldPopupRemainOpen()2112 	private boolean shouldPopupRemainOpen() {
2113 		// If we always autoactivate or never autoactivate, it should remain open
2114 		if (autoActivateString == null || autoActivateString.length() == 0)
2115 			return true;
2116 		String content = getControlContentAdapter().getControlContents(getControl());
2117 		for (int i=0; i<autoActivateString.length(); i++) {
2118 			if (content.indexOf(autoActivateString.charAt(i)) >= 0)
2119 				return true;
2120 		}
2121 		return false;
2122 	}
2123 
2124 	/*
2125 	 * Return whether this adapter is configured for autoactivation, by
2126 	 * specific characters or by any characters.
2127 	 */
allowsAutoActivate()2128 	private boolean allowsAutoActivate() {
2129 		return (autoActivateString != null && autoActivateString.length() > 0) // there are specific autoactivation chars supplied
2130 			|| (autoActivateString == null && triggerKeyStroke == null);    // we autoactivate on everything
2131 	}
2132 
2133 	/**
2134 	 * Sets focus to the proposal popup. If the proposal popup is not opened,
2135 	 * this method is ignored. If the secondary popup has focus, focus is
2136 	 * returned to the main proposal popup.
2137 	 *
2138 	 * @since 3.6
2139 	 */
setProposalPopupFocus()2140 	public void setProposalPopupFocus() {
2141 		if (isValid() && popup != null)
2142 			popup.getShell().setFocus();
2143 	}
2144 
2145 	/**
2146 	 * Answers a boolean indicating whether the main proposal popup is open.
2147 	 *
2148 	 * @return <code>true</code> if the proposal popup is open, and
2149 	 *         <code>false</code> if it is not.
2150 	 *
2151 	 * @since 3.6
2152 	 */
isProposalPopupOpen()2153 	public boolean isProposalPopupOpen() {
2154 		if (isValid() && popup != null)
2155 			return true;
2156 		return false;
2157 	}
2158 
2159 	/**
2160 	 * Reloads the proposals from the content provider and fills them into the
2161 	 * proposal pop-up, if the pop-up is currently open.
2162 	 *
2163 	 * @since 3.15
2164 	 */
refresh()2165 	public void refresh() {
2166 		if (isProposalPopupOpen()) {
2167 			popup.refresh();
2168 		}
2169 	}
2170 
2171 }
2172