Mercurial > projects > dwt-addons
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 } |