comparison dwtx/jface/fieldassist/ContentProposalAdapter.d @ 29:f12d40e7da8f

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