KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > eclipse > jface > fieldassist > ContentProposalAdapter


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  *******************************************************************************/

11 package org.eclipse.jface.fieldassist;
12
13 import java.util.ArrayList JavaDoc;
14
15 import org.eclipse.core.runtime.ListenerList;
16 import org.eclipse.jface.bindings.keys.KeyStroke;
17 import org.eclipse.jface.dialogs.PopupDialog;
18 import org.eclipse.core.runtime.Assert;
19 import org.eclipse.jface.viewers.ILabelProvider;
20 import org.eclipse.swt.SWT;
21 import org.eclipse.swt.events.DisposeEvent;
22 import org.eclipse.swt.events.DisposeListener;
23 import org.eclipse.swt.events.FocusAdapter;
24 import org.eclipse.swt.events.FocusEvent;
25 import org.eclipse.swt.events.SelectionEvent;
26 import org.eclipse.swt.events.SelectionListener;
27 import org.eclipse.swt.graphics.Image;
28 import org.eclipse.swt.graphics.Point;
29 import org.eclipse.swt.graphics.Rectangle;
30 import org.eclipse.swt.layout.GridData;
31 import org.eclipse.swt.widgets.Composite;
32 import org.eclipse.swt.widgets.Control;
33 import org.eclipse.swt.widgets.Event;
34 import org.eclipse.swt.widgets.Listener;
35 import org.eclipse.swt.widgets.ScrollBar;
36 import org.eclipse.swt.widgets.Shell;
37 import org.eclipse.swt.widgets.Table;
38 import org.eclipse.swt.widgets.TableItem;
39 import org.eclipse.swt.widgets.Text;
40
41 /**
42  * ContentProposalAdapter can be used to attach content proposal behavior to a
43  * control. This behavior includes obtaining proposals, opening a popup dialog,
44  * managing the content of the control relative to the selections in the popup,
45  * and optionally opening up a secondary popup to further describe proposals.
46  * <p>
47  * A number of configurable options are provided to determine how the control
48  * content is altered when a proposal is chosen, how the content proposal popup
49  * is activated, and whether any filtering should be done on the proposals as
50  * the user types characters.
51  * <p>
52  * This class is not intended to be subclassed.
53  *
54  * @since 3.2
55  */

56 public class ContentProposalAdapter {
57
58     /*
59      * The lightweight popup used to show content proposals for a text field. If
60      * additional information exists for a proposal, then selecting that
61      * proposal will result in the information being displayed in a secondary
62      * popup.
63      */

64     class ContentProposalPopup extends PopupDialog {
65         /*
66          * The listener we install on the popup and related controls to
67          * determine when to close the popup. Some events (move, resize, close,
68          * deactivate) trigger closure as soon as they are received, simply
69          * because one of the registered listeners received them. Other events
70          * depend on additional circumstances.
71          */

72         private final class PopupCloserListener implements Listener {
73             private boolean scrollbarClicked = false;
74
75             public void handleEvent(final Event e) {
76
77                 // If focus is leaving an important widget or the field's
78
// shell is deactivating
79
if (e.type == SWT.FocusOut) {
80                     scrollbarClicked = false;
81                     /*
82                      * Ignore this event if it's only happening because focus is
83                      * moving between the popup shells, their controls, or a
84                      * scrollbar. Do this in an async since the focus is not
85                      * actually switched when this event is received.
86                      */

87                     e.display.asyncExec(new Runnable JavaDoc() {
88                         public void run() {
89                             if (isValid()) {
90                                 if (scrollbarClicked
91                                         || hasFocus()
92                                         || (infoPopup != null && infoPopup
93                                                 .hasFocus())) {
94                                     return;
95                                 }
96                                 // Workaround a problem on X and Mac, whereby at
97
// this point, the focus control is not known.
98
// This can happen, for example, when resizing
99
// the popup shell on the Mac.
100
// Check the active shell.
101
Shell activeShell = e.display.getActiveShell();
102                                 if (activeShell == getShell()
103                                         || (infoPopup != null && infoPopup
104                                                 .getShell() == activeShell)) {
105                                     return;
106                                 }
107                                 /*
108                                  * System.out.println(e);
109                                  * System.out.println(e.display.getFocusControl());
110                                  * System.out.println(e.display.getActiveShell());
111                                  */

112                                 close();
113                             }
114                         }
115                     });
116                     return;
117                 }
118
119                 // Scroll bar has been clicked. Remember this for focus event
120
// processing.
121
if (e.type == SWT.Selection) {
122                     scrollbarClicked = true;
123                     return;
124                 }
125                 // For all other events, merely getting them dictates closure.
126
close();
127             }
128
129             // Install the listeners for events that need to be monitored for
130
// popup closure.
131
void installListeners() {
132                 // Listeners on this popup's table and scroll bar
133
proposalTable.addListener(SWT.FocusOut, this);
134                 ScrollBar scrollbar = proposalTable.getVerticalBar();
135                 if (scrollbar != null) {
136                     scrollbar.addListener(SWT.Selection, this);
137                 }
138
139                 // Listeners on this popup's shell
140
getShell().addListener(SWT.Deactivate, this);
141                 getShell().addListener(SWT.Close, this);
142
143                 // Listeners on the target control
144
control.addListener(SWT.MouseDoubleClick, this);
145                 control.addListener(SWT.MouseDown, this);
146                 control.addListener(SWT.Dispose, this);
147                 control.addListener(SWT.FocusOut, this);
148                 // Listeners on the target control's shell
149
Shell controlShell = control.getShell();
150                 controlShell.addListener(SWT.Move, this);
151                 controlShell.addListener(SWT.Resize, this);
152
153             }
154
155             // Remove installed listeners
156
void removeListeners() {
157                 if (isValid()) {
158                     proposalTable.removeListener(SWT.FocusOut, this);
159                     ScrollBar scrollbar = proposalTable.getVerticalBar();
160                     if (scrollbar != null) {
161                         scrollbar.removeListener(SWT.Selection, this);
162                     }
163
164                     getShell().removeListener(SWT.Deactivate, this);
165                     getShell().removeListener(SWT.Close, this);
166                 }
167
168                 if (control != null && !control.isDisposed()) {
169
170                     control.removeListener(SWT.MouseDoubleClick, this);
171                     control.removeListener(SWT.MouseDown, this);
172                     control.removeListener(SWT.Dispose, this);
173                     control.removeListener(SWT.FocusOut, this);
174
175                     Shell controlShell = control.getShell();
176                     controlShell.removeListener(SWT.Move, this);
177                     controlShell.removeListener(SWT.Resize, this);
178                 }
179             }
180         }
181
182         /*
183          * The listener we will install on the target control.
184          */

185         private final class TargetControlListener implements Listener {
186             // Key events from the control
187
public void handleEvent(Event e) {
188                 if (!isValid()) {
189                     return;
190                 }
191
192                 char key = e.character;
193
194                 // Traverse events are handled depending on whether the
195
// event has a character.
196
if (e.type == SWT.Traverse) {
197                     // If the traverse event contains a legitimate character,
198
// then we must set doit false so that the widget will
199
// receive the key event. We return immediately so that
200
// the character is handled only in the key event.
201
// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=132101
202
if (key != 0) {
203                         e.doit = false;
204                         return;
205                     }
206                     // Traversal does not contain a character. Set doit true
207
// to indicate TRAVERSE_NONE will occur and that no key
208
// event will be triggered. We will check for navigation
209
// keys below.
210
e.detail = SWT.TRAVERSE_NONE;
211                     e.doit = true;
212                 } else {
213                     // Default is to only propagate when configured that way.
214
// Some keys will always set doit to false anyway.
215
e.doit = propagateKeys;
216                 }
217
218                 // No character. Check for navigation keys.
219

220                 if (key == 0) {
221                     int newSelection = proposalTable.getSelectionIndex();
222                     int visibleRows = (proposalTable.getSize().y / proposalTable
223                             .getItemHeight()) - 1;
224                     switch (e.keyCode) {
225                     case SWT.ARROW_UP:
226                         newSelection -= 1;
227                         if (newSelection < 0) {
228                             newSelection = proposalTable.getItemCount() - 1;
229                         }
230                         // Not typical - usually we get this as a Traverse and
231
// therefore it never propagates. Added for consistency.
232
if (e.type == SWT.KeyDown) {
233                             // don't propagate to control
234
e.doit = false;
235                         }
236
237                         break;
238
239                     case SWT.ARROW_DOWN:
240                         newSelection += 1;
241                         if (newSelection > proposalTable.getItemCount() - 1) {
242                             newSelection = 0;
243                         }
244                         // Not typical - usually we get this as a Traverse and
245
// therefore it never propagates. Added for consistency.
246
if (e.type == SWT.KeyDown) {
247                             // don't propagate to control
248
e.doit = false;
249                         }
250
251                         break;
252
253                     case SWT.PAGE_DOWN:
254                         newSelection += visibleRows;
255                         if (newSelection >= proposalTable.getItemCount()) {
256                             newSelection = proposalTable.getItemCount() - 1;
257                         }
258                         if (e.type == SWT.KeyDown) {
259                             // don't propagate to control
260
e.doit = false;
261                         }
262                         break;
263
264                     case SWT.PAGE_UP:
265                         newSelection -= visibleRows;
266                         if (newSelection < 0) {
267                             newSelection = 0;
268                         }
269                         if (e.type == SWT.KeyDown) {
270                             // don't propagate to control
271
e.doit = false;
272                         }
273                         break;
274
275                     case SWT.HOME:
276                         newSelection = 0;
277                         if (e.type == SWT.KeyDown) {
278                             // don't propagate to control
279
e.doit = false;
280                         }
281                         break;
282
283                     case SWT.END:
284                         newSelection = proposalTable.getItemCount() - 1;
285                         if (e.type == SWT.KeyDown) {
286                             // don't propagate to control
287
e.doit = false;
288                         }
289                         break;
290
291                     // If received as a Traverse, these should propagate
292
// to the control as keydown. If received as a keydown,
293
// proposals should be recomputed since the cursor
294
// position has changed.
295
case SWT.ARROW_LEFT:
296                     case SWT.ARROW_RIGHT:
297                         if (e.type == SWT.Traverse) {
298                             e.doit = false;
299                         } else {
300                             e.doit = true;
301                             String JavaDoc contents = getControlContentAdapter()
302                                     .getControlContents(getControl());
303                             // If there are no contents, changes in cursor
304
// position
305
// have no effect. Note also that we do not affect
306
// the filter
307
// text on ARROW_LEFT as we would with BS.
308
if (contents.length() > 0) {
309                                 asyncRecomputeProposals(filterText);
310                             }
311                         }
312                         break;
313
314                     // Any unknown keycodes will cause the popup to close.
315
// Modifier keys are explicitly checked and ignored because
316
// they are not complete yet (no character).
317
default:
318                         if (e.keyCode != SWT.CAPS_LOCK && e.keyCode != SWT.MOD1
319                                 && e.keyCode != SWT.MOD2
320                                 && e.keyCode != SWT.MOD3
321                                 && e.keyCode != SWT.MOD4) {
322                             close();
323                         }
324                         return;
325                     }
326
327                     // If any of these navigation events caused a new selection,
328
// then handle that now and return.
329
if (newSelection >= 0) {
330                         selectProposal(newSelection);
331                     }
332                     return;
333                 }
334
335                 // key != 0
336
// Check for special keys involved in cancelling, accepting, or
337
// filtering the proposals.
338
switch (key) {
339                 case SWT.ESC:
340                     e.doit = false;
341                     close();
342                     break;
343
344                 case SWT.LF:
345                 case SWT.CR:
346                     e.doit = false;
347                     Object JavaDoc p = getSelectedProposal();
348                     if (p != null) {
349                         acceptCurrentProposal();
350                     } else {
351                         close();
352                     }
353                     break;
354
355                 case SWT.TAB:
356                     e.doit = false;
357                     getShell().setFocus();
358                     return;
359
360                 case SWT.BS:
361                     // Backspace should back out of any stored filter text
362
if (filterStyle != FILTER_NONE) {
363                         // We have no filter to back out of, so do nothing
364
if (filterText.length() == 0) {
365                             return;
366                         }
367                         // There is filter to back out of
368
filterText = filterText.substring(0, filterText
369                                 .length() - 1);
370                         asyncRecomputeProposals(filterText);
371                         return;
372                     }
373                     // There is no filtering provided by us, but some
374
// clients provide their own filtering based on content.
375
// Recompute the proposals if the cursor position
376
// will change (is not at 0).
377
int pos = getControlContentAdapter().getCursorPosition(
378                             getControl());
379                     // We rely on the fact that the contents and pos do not yet
380
// reflect the result of the BS. If the contents were
381
// already empty, then BS should not cause
382
// a recompute.
383
if (pos > 0) {
384                         asyncRecomputeProposals(filterText);
385                     }
386                     break;
387
388                 default:
389                     // If the key is a defined unicode character, and not one of
390
// the special cases processed above, update the filter text
391
// and filter the proposals.
392
if (Character.isDefined(key)) {
393                         if (filterStyle == FILTER_CUMULATIVE) {
394                             filterText = filterText + String.valueOf(key);
395                         } else if (filterStyle == FILTER_CHARACTER) {
396                             filterText = String.valueOf(key);
397                         }
398                         // Recompute proposals after processing this event.
399
asyncRecomputeProposals(filterText);
400                     }
401                     break;
402                 }
403             }
404         }
405
406         /*
407          * Internal class used to implement the secondary popup.
408          */

409         private class InfoPopupDialog extends PopupDialog {
410
411             /*
412              * The text control that displays the text.
413              */

414             private Text text;
415
416             /*
417              * The String shown in the popup.
418              */

419             private String JavaDoc contents = EMPTY;
420
421             /*
422              * Construct an info-popup with the specified parent.
423              */

424             InfoPopupDialog(Shell parent) {
425                 super(parent, PopupDialog.HOVER_SHELLSTYLE, false, false,
426                         false, false, null, null);
427             }
428
429             /*
430              * Create a text control for showing the info about a proposal.
431              */

432             protected Control createDialogArea(Composite parent) {
433                 text = new Text(parent, SWT.MULTI | SWT.READ_ONLY | SWT.WRAP
434                         | SWT.NO_FOCUS);
435
436                 // Use the compact margins employed by PopupDialog.
437
GridData gd = new GridData(GridData.BEGINNING
438                         | GridData.FILL_BOTH);
439                 gd.horizontalIndent = PopupDialog.POPUP_HORIZONTALSPACING;
440                 gd.verticalIndent = PopupDialog.POPUP_VERTICALSPACING;
441                 text.setLayoutData(gd);
442                 text.setText(contents);
443
444                 // since SWT.NO_FOCUS is only a hint...
445
text.addFocusListener(new FocusAdapter() {
446                     public void focusGained(FocusEvent event) {
447                         ContentProposalPopup.this.close();
448                     }
449                 });
450                 return text;
451             }
452
453             /*
454              * Adjust the bounds so that we appear adjacent to our parent shell
455              */

456             protected void adjustBounds() {
457                 Rectangle parentBounds = getParentShell().getBounds();
458                 Rectangle proposedBounds;
459                 // Try placing the info popup to the right
460
Rectangle rightProposedBounds = new Rectangle(parentBounds.x
461                         + parentBounds.width
462                         + PopupDialog.POPUP_HORIZONTALSPACING, parentBounds.y
463                         + PopupDialog.POPUP_VERTICALSPACING,
464                         parentBounds.width, parentBounds.height);
465                 rightProposedBounds = getConstrainedShellBounds(rightProposedBounds);
466                 // If it won't fit on the right, try the left
467
if (rightProposedBounds.intersects(parentBounds)) {
468                     Rectangle leftProposedBounds = new Rectangle(parentBounds.x
469                             - parentBounds.width - POPUP_HORIZONTALSPACING - 1,
470                             parentBounds.y, parentBounds.width,
471                             parentBounds.height);
472                     leftProposedBounds = getConstrainedShellBounds(leftProposedBounds);
473                     // If it won't fit on the left, choose the proposed bounds
474
// that fits the best
475
if (leftProposedBounds.intersects(parentBounds)) {
476                         if (rightProposedBounds.x - parentBounds.x >= parentBounds.x
477                                 - leftProposedBounds.x) {
478                             rightProposedBounds.x = parentBounds.x
479                                     + parentBounds.width
480                                     + PopupDialog.POPUP_HORIZONTALSPACING;
481                             proposedBounds = rightProposedBounds;
482                         } else {
483                             leftProposedBounds.width = parentBounds.x
484                                     - POPUP_HORIZONTALSPACING
485                                     - leftProposedBounds.x;
486                             proposedBounds = leftProposedBounds;
487                         }
488                     } else {
489                         // use the proposed bounds on the left
490
proposedBounds = leftProposedBounds;
491                     }
492                 } else {
493                     // use the proposed bounds on the right
494
proposedBounds = rightProposedBounds;
495                 }
496                 getShell().setBounds(proposedBounds);
497             }
498
499             /*
500              * Set the text contents of the popup.
501              */

502             void setContents(String JavaDoc newContents) {
503                 if (newContents == null) {
504                     newContents = EMPTY;
505                 }
506                 this.contents = newContents;
507                 if (text != null && !text.isDisposed()) {
508                     text.setText(contents);
509                 }
510             }
511
512             /*
513              * Return whether the popup has focus.
514              */

515             boolean hasFocus() {
516                 if (text == null || text.isDisposed()) {
517                     return false;
518                 }
519                 return text.getShell().isFocusControl()
520                         || text.isFocusControl();
521             }
522         }
523
524         /*
525          * The listener installed on the target control.
526          */

527         private Listener targetControlListener;
528
529         /*
530          * The listener installed in order to close the popup.
531          */

532         private PopupCloserListener popupCloser;
533
534         /*
535          * The table used to show the list of proposals.
536          */

537         private Table proposalTable;
538
539         /*
540          * The proposals to be shown (cached to avoid repeated requests).
541          */

542         private IContentProposal[] proposals;
543
544         /*
545          * Secondary popup used to show detailed information about the selected
546          * proposal..
547          */

548         private InfoPopupDialog infoPopup;
549
550         /*
551          * Flag indicating whether there is a pending secondary popup update.
552          */

553         private boolean pendingDescriptionUpdate = false;
554
555         /*
556          * Filter text - tracked while popup is open, only if we are told to
557          * filter
558          */

559         private String JavaDoc filterText = EMPTY;
560
561         /**
562          * Constructs a new instance of this popup, specifying the control for
563          * which this popup is showing content, and how the proposals should be
564          * obtained and displayed.
565          *
566          * @param infoText
567          * Text to be shown in a lower info area, or
568          * <code>null</code> if there is no info area.
569          */

570         ContentProposalPopup(String JavaDoc infoText, IContentProposal[] proposals) {
571             // IMPORTANT: Use of SWT.ON_TOP is critical here for ensuring
572
// that the target control retains focus on Mac and Linux. Without
573
// it, the focus will disappear, keystrokes will not go to the
574
// popup, and the popup closer will wrongly close the popup.
575
// On platforms where SWT.ON_TOP overrides SWT.RESIZE, we will live
576
// with this.
577
// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=126138
578
super(control.getShell(), SWT.RESIZE | SWT.ON_TOP, false, false,
579                     false, false, null, infoText);
580             this.proposals = proposals;
581         }
582
583         /*
584          * Overridden to force change of colors. See
585          * https://bugs.eclipse.org/bugs/show_bug.cgi?id=136244 (non-Javadoc)
586          *
587          * @see org.eclipse.jface.dialogs.PopupDialog#createContents(org.eclipse.swt.widgets.Composite)
588          */

589         protected Control createContents(Composite parent) {
590             Control contents = super.createContents(parent);
591             changeDefaultColors(parent);
592             return contents;
593         }
594
595         /*
596          * Set the colors of the popup. The contents have already been created.
597          */

598         private void changeDefaultColors(Control control) {
599             applyForegroundColor(getShell().getDisplay().getSystemColor(
600                     SWT.COLOR_LIST_FOREGROUND), control);
601             applyBackgroundColor(getShell().getDisplay().getSystemColor(
602                     SWT.COLOR_LIST_BACKGROUND), control);
603         }
604
605         /*
606          * Creates the content area for the proposal popup. This creates a table
607          * and places it inside the composite. The table will contain a list of
608          * all the proposals.
609          *
610          * @param parent The parent composite to contain the dialog area; must
611          * not be <code>null</code>.
612          */

613         protected final Control createDialogArea(final Composite parent) {
614             // Use virtual where appropriate (see flag definition).
615
if (USE_VIRTUAL) {
616                 proposalTable = new Table(parent, SWT.H_SCROLL | SWT.V_SCROLL
617                         | SWT.VIRTUAL);
618
619                 Listener listener = new Listener() {
620                     public void handleEvent(Event event) {
621                         handleSetData(event);
622                     }
623                 };
624                 proposalTable.addListener(SWT.SetData, listener);
625             } else {
626                 proposalTable = new Table(parent, SWT.H_SCROLL | SWT.V_SCROLL);
627             }
628
629             // set the proposals to force population of the table.
630
setProposals(filterProposals(proposals, filterText));
631
632             proposalTable.setHeaderVisible(false);
633             proposalTable.addSelectionListener(new SelectionListener() {
634
635                 public void widgetSelected(SelectionEvent e) {
636                     // If a proposal has been selected, show it in the secondary
637
// popup. Otherwise close the popup.
638
if (e.item == null) {
639                         if (infoPopup != null) {
640                             infoPopup.close();
641                         }
642                     } else {
643                         showProposalDescription();
644                     }
645                 }
646
647                 // Default selection was made. Accept the current proposal.
648
public void widgetDefaultSelected(SelectionEvent e) {
649                     acceptCurrentProposal();
650                 }
651             });
652             return proposalTable;
653         }
654
655         /*
656          * (non-Javadoc)
657          *
658          * @see org.eclipse.jface.dialogs.PopupDialog.adjustBounds()
659          */

660         protected void adjustBounds() {
661             // Get our control's location in display coordinates.
662
Point location = control.getDisplay().map(control.getParent(),
663                     null, control.getLocation());
664             int initialX = location.x + POPUP_OFFSET;
665             int initialY = location.y + control.getSize().y + POPUP_OFFSET;
666             // If we are inserting content, use the cursor position to
667
// position the control.
668
if (getProposalAcceptanceStyle() == PROPOSAL_INSERT) {
669                 Rectangle insertionBounds = controlContentAdapter
670                         .getInsertionBounds(control);
671                 initialX = initialX + insertionBounds.x;
672                 initialY = location.y + insertionBounds.y
673                         + insertionBounds.height;
674             }
675
676             // If there is no specified size, force it by setting
677
// up a layout on the table.
678
if (popupSize == null) {
679                 GridData data = new GridData(GridData.FILL_BOTH);
680                 data.heightHint = proposalTable.getItemHeight()
681                         * POPUP_CHAR_HEIGHT;
682                 data.widthHint = Math.max(control.getSize().x,
683                         POPUP_MINIMUM_WIDTH);
684                 proposalTable.setLayoutData(data);
685                 getShell().pack();
686                 popupSize = getShell().getSize();
687             }
688             getShell().setBounds(initialX, initialY, popupSize.x, popupSize.y);
689
690             // Now set up a listener to monitor any changes in size.
691
getShell().addListener(SWT.Resize, new Listener() {
692                 public void handleEvent(Event e) {
693                     popupSize = getShell().getSize();
694                     if (infoPopup != null) {
695                         infoPopup.adjustBounds();
696                     }
697                 }
698             });
699         }
700
701         /*
702          * Handle the set data event. Set the item data of the requested item to
703          * the corresponding proposal in the proposal cache.
704          */

705         private void handleSetData(Event event) {
706             TableItem item = (TableItem) event.item;
707             int index = proposalTable.indexOf(item);
708
709             if (0 <= index && index < proposals.length) {
710                 IContentProposal current = proposals[index];
711                 item.setText(getString(current));
712                 item.setImage(getImage(current));
713                 item.setData(current);
714             } else {
715                 // this should not happen, but does on win32
716
}
717         }
718
719         /*
720          * Caches the specified proposals and repopulates the table if it has
721          * been created.
722          */

723         private void setProposals(IContentProposal[] newProposals) {
724             if (newProposals == null || newProposals.length == 0) {
725                 newProposals = getEmptyProposalArray();
726             }
727             this.proposals = newProposals;
728
729             // If there is a table
730
if (isValid()) {
731                 final int newSize = newProposals.length;
732                 if (USE_VIRTUAL) {
733                     // Set and clear the virtual table. Data will be
734
// provided in the SWT.SetData event handler.
735
proposalTable.setItemCount(newSize);
736                     proposalTable.clearAll();
737                 } else {
738                     // Populate the table manually
739
proposalTable.setRedraw(false);
740                     proposalTable.setItemCount(newSize);
741                     TableItem[] items = proposalTable.getItems();
742                     for (int i = 0; i < items.length; i++) {
743                         TableItem item = items[i];
744                         IContentProposal proposal = newProposals[i];
745                         item.setText(getString(proposal));
746                         item.setImage(getImage(proposal));
747                         item.setData(proposal);
748                     }
749                     proposalTable.setRedraw(true);
750                 }
751                 // Default to the first selection if there is content.
752
if (newProposals.length > 0) {
753                     selectProposal(0);
754                 } else {
755                     // No selection, close the secondary popup if it was open
756
if (infoPopup != null) {
757                         infoPopup.close();
758                     }
759
760                 }
761             }
762         }
763
764         /*
765          * Get the string for the specified proposal. Always return a String of
766          * some kind.
767          */

768         private String JavaDoc getString(IContentProposal proposal) {
769             if (proposal == null) {
770                 return EMPTY;
771             }
772             if (labelProvider == null) {
773                 return proposal.getLabel() == null ? proposal.getContent()
774                         : proposal.getLabel();
775             }
776             return labelProvider.getText(proposal);
777         }
778
779         /*
780          * Get the image for the specified proposal. If there is no image
781          * available, return null.
782          */

783         private Image getImage(IContentProposal proposal) {
784             if (proposal == null || labelProvider == null) {
785                 return null;
786             }
787             return labelProvider.getImage(proposal);
788         }
789
790         /*
791          * Return an empty array. Used so that something always shows in the
792          * proposal popup, even if no proposal provider was specified.
793          */

794         private IContentProposal[] getEmptyProposalArray() {
795             return new IContentProposal[0];
796         }
797
798         /*
799          * Answer true if the popup is valid, which means the table has been
800          * created and not disposed.
801          */

802         private boolean isValid() {
803             return proposalTable != null && !proposalTable.isDisposed();
804         }
805
806         /*
807          * Return whether the receiver has focus.
808          */

809         private boolean hasFocus() {
810             if (!isValid()) {
811                 return false;
812             }
813             return getShell().isFocusControl()
814                     || proposalTable.isFocusControl();
815         }
816
817         /*
818          * Return the current selected proposal.
819          */

820         private IContentProposal getSelectedProposal() {
821             if (isValid()) {
822                 int i = proposalTable.getSelectionIndex();
823                 if (proposals == null || i < 0 || i >= proposals.length) {
824                     return null;
825                 }
826                 return proposals[i];
827             }
828             return null;
829         }
830
831         /*
832          * Select the proposal at the given index.
833          */

834         private void selectProposal(int index) {
835             Assert
836                     .isTrue(index >= 0,
837                             "Proposal index should never be negative"); //$NON-NLS-1$
838
if (!isValid() || proposals == null || index >= proposals.length) {
839                 return;
840             }
841             proposalTable.setSelection(index);
842             proposalTable.showSelection();
843
844             showProposalDescription();
845         }
846
847         /**
848          * Opens this ContentProposalPopup. This method is extended in order to
849          * add the control listener when the popup is opened and to invoke the
850          * secondary popup if applicable.
851          *
852          * @return the return code
853          *
854          * @see org.eclipse.jface.window.Window#open()
855          */

856         public int open() {
857             int value = super.open();
858             if (popupCloser == null) {
859                 popupCloser = new PopupCloserListener();
860             }
861             popupCloser.installListeners();
862             IContentProposal p = getSelectedProposal();
863             if (p != null) {
864                 showProposalDescription();
865             }
866             return value;
867         }
868
869         /**
870          * Closes this popup. This method is extended to remove the control
871          * listener.
872          *
873          * @return <code>true</code> if the window is (or was already) closed,
874          * and <code>false</code> if it is still open
875          */

876         public boolean close() {
877             popupCloser.removeListeners();
878             if (infoPopup != null) {
879                 infoPopup.close();
880             }
881             boolean ret = super.close();
882             notifyPopupClosed();
883             return ret;
884         }
885
886         /*
887          * Show the currently selected proposal's description in a secondary
888          * popup.
889          */

890         private void showProposalDescription() {
891             // If we do not already have a pending update, then
892
// create a thread now that will show the proposal description
893
if (!pendingDescriptionUpdate) {
894                 // Create a thread that will sleep for the specified delay
895
// before creating the popup. We do not use Jobs since this
896
// code must be able to run independently of the Eclipse
897
// runtime.
898
Runnable JavaDoc runnable = new Runnable JavaDoc() {
899                     public void run() {
900                         pendingDescriptionUpdate = true;
901                         try {
902                             Thread.sleep(POPUP_DELAY);
903                         } catch (InterruptedException JavaDoc e) {
904                         }
905                         if (!isValid()) {
906                             return;
907                         }
908                         getShell().getDisplay().syncExec(new Runnable JavaDoc() {
909                             public void run() {
910                                 // Query the current selection since we have
911
// been delayed
912
IContentProposal p = getSelectedProposal();
913                                 if (p != null) {
914                                     String JavaDoc description = p.getDescription();
915                                     if (description != null) {
916                                         if (infoPopup == null) {
917                                             infoPopup = new InfoPopupDialog(
918                                                     getShell());
919                                             infoPopup.open();
920                                             infoPopup
921                                                     .getShell()
922                                                     .addDisposeListener(
923                                                             new DisposeListener() {
924                                                                 public void widgetDisposed(
925                                                                         DisposeEvent event) {
926                                                                     infoPopup = null;
927                                                                 }
928                                                             });
929                                         }
930                                         infoPopup.setContents(p
931                                                 .getDescription());
932                                     } else if (infoPopup != null) {
933                                         infoPopup.close();
934                                     }
935                                     pendingDescriptionUpdate = false;
936
937                                 }
938                             }
939                         });
940                     }
941                 };
942                 Thread JavaDoc t = new Thread JavaDoc(runnable);
943                 t.start();
944             }
945         }
946
947         /*
948          * Accept the current proposal.
949          */

950         private void acceptCurrentProposal() {
951             // Close before accepting the proposal.
952
// This is important so that the cursor position can be
953
// properly restored at acceptance, which does not work without
954
// focus on some controls.
955
// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=127108
956
IContentProposal proposal = getSelectedProposal();
957             close();
958             proposalAccepted(proposal);
959         }
960
961         /*
962          * Request the proposals from the proposal provider, and recompute any
963          * caches. Repopulate the popup if it is open.
964          */

965         private void recomputeProposals(String JavaDoc filterText) {
966             IContentProposal[] allProposals = getProposals();
967             // If the non-filtered proposal list is empty, we should
968
// close the popup.
969
// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=147377
970
if (allProposals.length == 0) {
971                 proposals = allProposals;
972                 close();
973             } else {
974                 // Keep the popup open, but filter by any provided filter text
975
setProposals(filterProposals(allProposals, filterText));
976             }
977         }
978
979         /*
980          * In an async block, request the proposals. This is used when clients
981          * are in the middle of processing an event that affects the widget
982          * content. By using an async, we ensure that the widget content is up
983          * to date with the event.
984          */

985         private void asyncRecomputeProposals(final String JavaDoc filterText) {
986             if (isValid()) {
987                 control.getDisplay().asyncExec(new Runnable JavaDoc() {
988                     public void run() {
989                         recordCursorPosition();
990                         recomputeProposals(filterText);
991                     }
992                 });
993             } else {
994                 recomputeProposals(filterText);
995             }
996         }
997
998         /*
999          * Filter the provided list of content proposals according to the filter
1000         * text.
1001         */

1002        private IContentProposal[] filterProposals(
1003                IContentProposal[] proposals, String JavaDoc filterString) {
1004            if (filterString.length() == 0) {
1005                return proposals;
1006            }
1007
1008            // Check each string for a match. Use the string displayed to the
1009
// user, not the proposal content.
1010
ArrayList JavaDoc list = new ArrayList JavaDoc();
1011            for (int i = 0; i < proposals.length; i++) {
1012                String JavaDoc string = getString(proposals[i]);
1013                if (string.length() >= filterString.length()
1014                        && string.substring(0, filterString.length())
1015                                .equalsIgnoreCase(filterString)) {
1016                    list.add(proposals[i]);
1017                }
1018
1019            }
1020            return (IContentProposal[]) list.toArray(new IContentProposal[list
1021                    .size()]);
1022        }
1023
1024        Listener getTargetControlListener() {
1025            if (targetControlListener == null) {
1026                targetControlListener = new TargetControlListener();
1027            }
1028            return targetControlListener;
1029        }
1030    }
1031
1032    /**
1033     * Flag that controls the printing of debug info.
1034     */

1035    public static final boolean DEBUG = false;
1036
1037    /**
1038     * Indicates that a chosen proposal should be inserted into the field.
1039     */

1040    public static final int PROPOSAL_INSERT = 1;
1041
1042    /**
1043     * Indicates that a chosen proposal should replace the entire contents of
1044     * the field.
1045     */

1046    public static final int PROPOSAL_REPLACE = 2;
1047
1048    /**
1049     * Indicates that the contents of the control should not be modified when a
1050     * proposal is chosen. This is typically used when a client needs more
1051     * specialized behavior when a proposal is chosen. In this case, clients
1052     * typically register an IContentProposalListener so that they are notified
1053     * when a proposal is chosen.
1054     */

1055    public static final int PROPOSAL_IGNORE = 3;
1056
1057    /**
1058     * Indicates that there should be no filter applied as keys are typed in the
1059     * popup.
1060     */

1061    public static final int FILTER_NONE = 1;
1062
1063    /**
1064     * Indicates that a single character filter applies as keys are typed in the
1065     * popup.
1066     */

1067    public static final int FILTER_CHARACTER = 2;
1068
1069    /**
1070     * Indicates that a cumulative filter applies as keys are typed in the
1071     * popup. That is, each character typed will be added to the filter.
1072     */

1073    public static final int FILTER_CUMULATIVE = 3;
1074
1075    /*
1076     * Set to <code>true</code> to use a Table with SWT.VIRTUAL. This is a
1077     * workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=98585#c40
1078     * The corresponding SWT bug is
1079     * https://bugs.eclipse.org/bugs/show_bug.cgi?id=90321
1080     */

1081    private static final boolean USE_VIRTUAL = !"motif".equals(SWT.getPlatform()); //$NON-NLS-1$
1082

1083    /*
1084     * The delay before showing a secondary popup.
1085     */

1086    private static final int POPUP_DELAY = 750;
1087
1088    /*
1089     * The character height hint for the popup. May be overridden by using
1090     * setInitialPopupSize.
1091     */

1092    private static final int POPUP_CHAR_HEIGHT = 10;
1093
1094    /*
1095     * The minimum pixel width for the popup. May be overridden by using
1096     * setInitialPopupSize.
1097     */

1098    private static final int POPUP_MINIMUM_WIDTH = 300;
1099
1100    /*
1101     * The pixel offset of the popup from the bottom corner of the control.
1102     */

1103    private static final int POPUP_OFFSET = 3;
1104
1105    /*
1106     * Empty string.
1107     */

1108    private static final String JavaDoc EMPTY = ""; //$NON-NLS-1$
1109

1110    /*
1111     * The object that provides content proposals.
1112     */

1113    private IContentProposalProvider proposalProvider;
1114
1115    /*
1116     * A label provider used to display proposals in the popup, and to extract
1117     * Strings from non-String proposals.
1118     */

1119    private ILabelProvider labelProvider;
1120
1121    /*
1122     * The control for which content proposals are provided.
1123     */

1124    private Control control;
1125
1126    /*
1127     * The adapter used to extract the String contents from an arbitrary
1128     * control.
1129     */

1130    private IControlContentAdapter controlContentAdapter;
1131
1132    /*
1133     * The popup used to show proposals.
1134     */

1135    private ContentProposalPopup popup;
1136
1137    /*
1138     * The keystroke that signifies content proposals should be shown.
1139     */

1140    private KeyStroke triggerKeyStroke;
1141
1142    /*
1143     * The String containing characters that auto-activate the popup.
1144     */

1145    private String JavaDoc autoActivateString;
1146
1147    /*
1148     * Integer that indicates how an accepted proposal should affect the
1149     * control. One of PROPOSAL_IGNORE, PROPOSAL_INSERT, or PROPOSAL_REPLACE.
1150     * Default value is PROPOSAL_INSERT.
1151     */

1152    private int proposalAcceptanceStyle = PROPOSAL_INSERT;
1153
1154    /*
1155     * A boolean that indicates whether key events received while the proposal
1156     * popup is open should also be propagated to the control. Default value is
1157     * true.
1158     */

1159    private boolean propagateKeys = true;
1160
1161    /*
1162     * Integer that indicates the filtering style. One of FILTER_CHARACTER,
1163     * FILTER_CUMULATIVE, FILTER_NONE.
1164     */

1165    private int filterStyle = FILTER_NONE;
1166
1167    /*
1168     * The listener we install on the control.
1169     */

1170    private Listener controlListener;
1171
1172    /*
1173     * The list of IContentProposalListener listeners.
1174     */

1175    private ListenerList proposalListeners = new ListenerList();
1176
1177    /*
1178     * The list of IContentProposalListener2 listeners.
1179     */

1180    private ListenerList proposalListeners2 = new ListenerList();
1181
1182    /*
1183     * Flag that indicates whether the adapter is enabled. In some cases,
1184     * adapters may be installed but depend upon outside state.
1185     */

1186    private boolean isEnabled = true;
1187
1188    /*
1189     * The delay in milliseconds used when autoactivating the popup.
1190     */

1191    private int autoActivationDelay = 0;
1192
1193    /*
1194     * A boolean indicating whether a keystroke has been received. Used to see
1195     * if an autoactivation delay was interrupted by a keystroke.
1196     */

1197    private boolean receivedKeyDown;
1198
1199    /*
1200     * The desired size in pixels of the proposal popup.
1201     */

1202    private Point popupSize;
1203
1204    /*
1205     * The remembered position of the insertion position. Not all controls will
1206     * restore the insertion position if the proposal popup gets focus, so we
1207     * need to remember it.
1208     */

1209    private int insertionPos = -1;
1210    
1211    /*
1212     * A flag that indicates that a pending modify event was caused by
1213     * the adapter rather than the user.
1214     */

1215    private boolean modifyingControlContent = false;
1216
1217    /**
1218     * Construct a content proposal adapter that can assist the user with
1219     * choosing content for the field.
1220     *
1221     * @param control
1222     * the control for which the adapter is providing content assist.
1223     * May not be <code>null</code>.
1224     * @param controlContentAdapter
1225     * the <code>IControlContentAdapter</code> used to obtain and
1226     * update the control's contents as proposals are accepted. May
1227     * not be <code>null</code>.
1228     * @param proposalProvider
1229     * the <code>IContentProposalProvider</code> used to obtain
1230     * content proposals for this control, or <code>null</code> if
1231     * no content proposal is available.
1232     * @param keyStroke
1233     * the keystroke that will invoke the content proposal popup. If
1234     * this value is <code>null</code>, then proposals will be
1235     * activated automatically when any of the auto activation
1236     * characters are typed.
1237     * @param autoActivationCharacters
1238     * An array of characters that trigger auto-activation of content
1239     * proposal. If specified, these characters will trigger
1240     * auto-activation of the proposal popup, regardless of whether
1241     * an explicit invocation keyStroke was specified. If this
1242     * parameter is <code>null</code>, then only a specified
1243     * keyStroke will invoke content proposal. If this parameter is
1244     * <code>null</code> and the keyStroke parameter is
1245     * <code>null</code>, then all alphanumeric characters will
1246     * auto-activate content proposal.
1247     */

1248    public ContentProposalAdapter(Control control,
1249            IControlContentAdapter controlContentAdapter,
1250            IContentProposalProvider proposalProvider, KeyStroke keyStroke,
1251            char[] autoActivationCharacters) {
1252        super();
1253        // We always assume the control and content adapter are valid.
1254
Assert.isNotNull(control);
1255        Assert.isNotNull(controlContentAdapter);
1256        this.control = control;
1257        this.controlContentAdapter = controlContentAdapter;
1258
1259        // The rest of these may be null
1260
this.proposalProvider = proposalProvider;
1261        this.triggerKeyStroke = keyStroke;
1262        if (autoActivationCharacters != null) {
1263            this.autoActivateString = new String JavaDoc(autoActivationCharacters);
1264        }
1265        addControlListener(control);
1266    }
1267
1268    /**
1269     * Get the control on which the content proposal adapter is installed.
1270     *
1271     * @return the control on which the proposal adapter is installed.
1272     */

1273    public Control getControl() {
1274        return control;
1275    }
1276
1277    /**
1278     * Get the label provider that is used to show proposals.
1279     *
1280     * @return the {@link ILabelProvider} used to show proposals, or
1281     * <code>null</code> if one has not been installed.
1282     */

1283    public ILabelProvider getLabelProvider() {
1284        return labelProvider;
1285    }
1286
1287    /**
1288     * Return a boolean indicating whether the receiver is enabled.
1289     *
1290     * @return <code>true</code> if the adapter is enabled, and
1291     * <code>false</code> if it is not.
1292     */

1293    public boolean isEnabled() {
1294        return isEnabled;
1295    }
1296
1297    /**
1298     * Set the label provider that is used to show proposals. The lifecycle of
1299     * the specified label provider is not managed by this adapter. Clients must
1300     * dispose the label provider when it is no longer needed.
1301     *
1302     * @param labelProvider
1303     * the (@link ILabelProvider} used to show proposals.
1304     */

1305    public void setLabelProvider(ILabelProvider labelProvider) {
1306        this.labelProvider = labelProvider;
1307    }
1308
1309    /**
1310     * Return the proposal provider that provides content proposals given the
1311     * current content of the field. A value of <code>null</code> indicates
1312     * that there are no content proposals available for the field.
1313     *
1314     * @return the {@link IContentProposalProvider} used to show proposals. May
1315     * be <code>null</code>.
1316     */

1317    public IContentProposalProvider getContentProposalProvider() {
1318        return proposalProvider;
1319    }
1320
1321    /**
1322     * Set the content proposal provider that is used to show proposals.
1323     *
1324     * @param proposalProvider
1325     * the {@link IContentProposalProvider} used to show proposals
1326     */

1327    public void setContentProposalProvider(
1328            IContentProposalProvider proposalProvider) {
1329        this.proposalProvider = proposalProvider;
1330    }
1331
1332    /**
1333     * Return the array of characters on which the popup is autoactivated.
1334     *
1335     * @return An array of characters that trigger auto-activation of content
1336     * proposal. If specified, these characters will trigger
1337     * auto-activation of the proposal popup, regardless of whether an
1338     * explicit invocation keyStroke was specified. If this parameter is
1339     * <code>null</code>, then only a specified keyStroke will invoke
1340     * content proposal. If this value is <code>null</code> and the
1341     * keyStroke value is <code>null</code>, then all alphanumeric
1342     * characters will auto-activate content proposal.
1343     */

1344    public char[] getAutoActivationCharacters() {
1345        if (autoActivateString == null) {
1346            return null;
1347        }
1348        return autoActivateString.toCharArray();
1349    }
1350
1351    /**
1352     * Set the array of characters that will trigger autoactivation of the
1353     * popup.
1354     *
1355     * @param autoActivationCharacters
1356     * An array of characters that trigger auto-activation of content
1357     * proposal. If specified, these characters will trigger
1358     * auto-activation of the proposal popup, regardless of whether
1359     * an explicit invocation keyStroke was specified. If this
1360     * parameter is <code>null</code>, then only a specified
1361     * keyStroke will invoke content proposal. If this parameter is
1362     * <code>null</code> and the keyStroke value is
1363     * <code>null</code>, then all alphanumeric characters will
1364     * auto-activate content proposal.
1365     *
1366     */

1367    public void setAutoActivationCharacters(char[] autoActivationCharacters) {
1368        if (autoActivationCharacters == null) {
1369            this.autoActivateString = null;
1370        } else {
1371            this.autoActivateString = new String JavaDoc(autoActivationCharacters);
1372        }
1373    }
1374
1375    /**
1376     * Set the delay, in milliseconds, used before any autoactivation is
1377     * triggered.
1378     *
1379     * @return the time in milliseconds that will pass before a popup is
1380     * automatically opened
1381     */

1382    public int getAutoActivationDelay() {
1383        return autoActivationDelay;
1384
1385    }
1386
1387    /**
1388     * Set the delay, in milliseconds, used before autoactivation is triggered.
1389     *
1390     * @param delay
1391     * the time in milliseconds that will pass before a popup is
1392     * automatically opened
1393     */

1394    public void setAutoActivationDelay(int delay) {
1395        autoActivationDelay = delay;
1396
1397    }
1398
1399    /**
1400     * Get the integer style that indicates how an accepted proposal affects the
1401     * control's content.
1402     *
1403     * @return a constant indicating how an accepted proposal should affect the
1404     * control's content. Should be one of <code>PROPOSAL_INSERT</code>,
1405     * <code>PROPOSAL_REPLACE</code>, or <code>PROPOSAL_IGNORE</code>.
1406     * (Default is <code>PROPOSAL_INSERT</code>).
1407     */

1408    public int getProposalAcceptanceStyle() {
1409        return proposalAcceptanceStyle;
1410    }
1411
1412    /**
1413     * Set the integer style that indicates how an accepted proposal affects the
1414     * control's content.
1415     *
1416     * @param acceptance
1417     * a constant indicating how an accepted proposal should affect
1418     * the control's content. Should be one of
1419     * <code>PROPOSAL_INSERT</code>, <code>PROPOSAL_REPLACE</code>,
1420     * or <code>PROPOSAL_IGNORE</code>
1421     */

1422    public void setProposalAcceptanceStyle(int acceptance) {
1423        proposalAcceptanceStyle = acceptance;
1424    }
1425
1426    /**
1427     * Return the integer style that indicates how keystrokes affect the content
1428     * of the proposal popup while it is open.
1429     *
1430     * @return a constant indicating how keystrokes in the proposal popup affect
1431     * filtering of the proposals shown. <code>FILTER_NONE</code>
1432     * specifies that no filtering will occur in the content proposal
1433     * list as keys are typed. <code>FILTER_CUMULATIVE</code>
1434     * specifies that the content of the popup will be filtered by a
1435     * string containing all the characters typed since the popup has
1436     * been open. <code>FILTER_CHARACTER</code> specifies the content
1437     * of the popup will be filtered by the most recently typed
1438     * character. The default is <code>FILTER_NONE</code>.
1439     */

1440    public int getFilterStyle() {
1441        return filterStyle;
1442    }
1443
1444    /**
1445     * Set the integer style that indicates how keystrokes affect the content of
1446     * the proposal popup while it is open. Popup-based filtering is useful for
1447     * narrowing and navigating the list of proposals provided once the popup is
1448     * open. Filtering of the proposals will occur even when the control content
1449     * is not affected by user typing. Note that automatic filtering is not used
1450     * to achieve content-sensitive filtering such as auto-completion. Filtering
1451     * that is sensitive to changes in the control content should be performed
1452     * by the supplied {@link IContentProposalProvider}.
1453     *
1454     * @param filterStyle
1455     * a constant indicating how keystrokes received in the proposal
1456     * popup affect filtering of the proposals shown.
1457     * <code>FILTER_NONE</code> specifies that no automatic
1458     * filtering of the content proposal list will occur as keys are
1459     * typed in the popup. <code>FILTER_CUMULATIVE</code> specifies
1460     * that the content of the popup will be filtered by a string
1461     * containing all the characters typed since the popup has been
1462     * open. <code>FILTER_CHARACTER</code> specifies that the
1463     * content of the popup will be filtered by the most recently
1464     * typed character.
1465     */

1466    public void setFilterStyle(int filterStyle) {
1467        this.filterStyle = filterStyle;
1468    }
1469
1470    /**
1471     * Return the size, in pixels, of the content proposal popup.
1472     *
1473     * @return a Point specifying the last width and height, in pixels, of the
1474     * content proposal popup.
1475     */

1476    public Point getPopupSize() {
1477        return popupSize;
1478    }
1479
1480    /**
1481     * Set the size, in pixels, of the content proposal popup. This size will be
1482     * used the next time the content proposal popup is opened.
1483     *
1484     * @param size
1485     * a Point specifying the desired width and height, in pixels, of
1486     * the content proposal popup.
1487     */

1488    public void setPopupSize(Point size) {
1489        popupSize = size;
1490    }
1491
1492    /**
1493     * Get the boolean that indicates whether key events (including
1494     * auto-activation characters) received by the content proposal popup should
1495     * also be propagated to the adapted control when the proposal popup is
1496     * open.
1497     *
1498     * @return a boolean that indicates whether key events (including
1499     * auto-activation characters) should be propagated to the adapted
1500     * control when the proposal popup is open. Default value is
1501     * <code>true</code>.
1502     */

1503    public boolean getPropagateKeys() {
1504        return propagateKeys;
1505    }
1506
1507    /**
1508     * Set the boolean that indicates whether key events (including
1509     * auto-activation characters) received by the content proposal popup should
1510     * also be propagated to the adapted control when the proposal popup is
1511     * open.
1512     *
1513     * @param propagateKeys
1514     * a boolean that indicates whether key events (including
1515     * auto-activation characters) should be propagated to the
1516     * adapted control when the proposal popup is open.
1517     */

1518    public void setPropagateKeys(boolean propagateKeys) {
1519        this.propagateKeys = propagateKeys;
1520    }
1521
1522    /**
1523     * Return the content adapter that can get or retrieve the text contents
1524     * from the adapter's control. This method is used when a client, such as a
1525     * content proposal listener, needs to update the control's contents
1526     * manually.
1527     *
1528     * @return the {@link IControlContentAdapter} which can update the control
1529     * text.
1530     */

1531    public IControlContentAdapter getControlContentAdapter() {
1532        return controlContentAdapter;
1533    }
1534
1535    /**
1536     * Set the boolean flag that determines whether the adapter is enabled.
1537     *
1538     * @param enabled
1539     * <code>true</code> if the adapter is enabled and responding
1540     * to user input, <code>false</code> if it is ignoring user
1541     * input.
1542     *
1543     */

1544    public void setEnabled(boolean enabled) {
1545        // If we are disabling it while it's proposing content, close the
1546
// content proposal popup.
1547
if (isEnabled && !enabled) {
1548            if (popup != null) {
1549                popup.close();
1550            }
1551        }
1552        isEnabled = enabled;
1553    }
1554
1555    /**
1556     * Add the specified listener to the list of content proposal listeners that
1557     * are notified when content proposals are chosen.
1558     * </p>
1559     *
1560     * @param listener
1561     * the IContentProposalListener to be added as a listener. Must
1562     * not be <code>null</code>. If an attempt is made to register
1563     * an instance which is already registered with this instance,
1564     * this method has no effect.
1565     *
1566     * @see org.eclipse.jface.fieldassist.IContentProposalListener
1567     */

1568    public void addContentProposalListener(IContentProposalListener listener) {
1569        proposalListeners.add(listener);
1570    }
1571
1572    /**
1573     * Removes the specified listener from the list of content proposal
1574     * listeners that are notified when content proposals are chosen.
1575     * </p>
1576     *
1577     * @param listener
1578     * the IContentProposalListener to be removed as a listener. Must
1579     * not be <code>null</code>. If the listener has not already
1580     * been registered, this method has no effect.
1581     *
1582     * @since 3.3
1583     * @see org.eclipse.jface.fieldassist.IContentProposalListener
1584     */

1585    public void removeContentProposalListener(IContentProposalListener listener) {
1586        proposalListeners.remove(listener);
1587    }
1588
1589    /**
1590     * Add the specified listener to the list of content proposal listeners that
1591     * are notified when a content proposal popup is opened or closed.
1592     * </p>
1593     *
1594     * @param listener
1595     * the IContentProposalListener2 to be added as a listener. Must
1596     * not be <code>null</code>. If an attempt is made to register
1597     * an instance which is already registered with this instance,
1598     * this method has no effect.
1599     *
1600     * @since 3.3
1601     * @see org.eclipse.jface.fieldassist.IContentProposalListener2
1602     */

1603    public void addContentProposalListener(IContentProposalListener2 listener) {
1604        proposalListeners2.add(listener);
1605    }
1606
1607    /**
1608     * Remove the specified listener from the list of content proposal listeners
1609     * that are notified when a content proposal popup is opened or closed.
1610     * </p>
1611     *
1612     * @param listener
1613     * the IContentProposalListener2 to be removed as a listener.
1614     * Must not be <code>null</code>. If the listener has not
1615     * already been registered, this method has no effect.
1616     *
1617     * @since 3.3
1618     * @see org.eclipse.jface.fieldassist.IContentProposalListener2
1619     */

1620    public void removeContentProposalListener(IContentProposalListener2 listener) {
1621        proposalListeners2.remove(listener);
1622    }
1623
1624    /*
1625     * Add our listener to the control. Debug information to be left in until
1626     * this support is stable on all platforms.
1627     */

1628    private void addControlListener(Control control) {
1629        if (DEBUG) {
1630            System.out
1631                    .println("ContentProposalListener#installControlListener()"); //$NON-NLS-1$
1632
}
1633
1634        if (controlListener != null) {
1635            return;
1636        }
1637        controlListener = new Listener() {
1638            public void handleEvent(Event e) {
1639                if (!isEnabled) {
1640                    return;
1641                }
1642
1643                switch (e.type) {
1644                case SWT.Traverse:
1645                case SWT.KeyDown:
1646                    if (DEBUG) {
1647                        StringBuffer JavaDoc sb;
1648                        if (e.type == SWT.Traverse) {
1649                            sb = new StringBuffer JavaDoc("Traverse"); //$NON-NLS-1$
1650
} else {
1651                            sb = new StringBuffer JavaDoc("KeyDown"); //$NON-NLS-1$
1652
}
1653                        sb.append(" received by adapter"); //$NON-NLS-1$
1654
dump(sb.toString(), e);
1655                    }
1656                    // If the popup is open, it gets first shot at the
1657
// keystroke and should set the doit flags appropriately.
1658
if (popup != null) {
1659                        popup.getTargetControlListener().handleEvent(e);
1660                        if (DEBUG) {
1661                            StringBuffer JavaDoc sb;
1662                            if (e.type == SWT.Traverse) {
1663                                sb = new StringBuffer JavaDoc("Traverse"); //$NON-NLS-1$
1664
} else {
1665                                sb = new StringBuffer JavaDoc("KeyDown"); //$NON-NLS-1$
1666
}
1667                            sb.append(" after being handled by popup"); //$NON-NLS-1$
1668
dump(sb.toString(), e);
1669                        }
1670
1671                        return;
1672                    }
1673
1674                    // We were only listening to traverse events for the popup
1675
if (e.type == SWT.Traverse) {
1676                        return;
1677                    }
1678
1679                    // The popup is not open. We are looking at keydown events
1680
// for a trigger to open the popup.
1681
if (triggerKeyStroke != null) {
1682                        // Either there are no modifiers for the trigger and we
1683
// check the character field...
1684
if ((triggerKeyStroke.getModifierKeys() == KeyStroke.NO_KEY && triggerKeyStroke
1685                                .getNaturalKey() == e.character)
1686                                ||
1687                                // ...or there are modifiers, in which case the
1688
// keycode and state must match
1689
(triggerKeyStroke.getNaturalKey() == e.keyCode && ((triggerKeyStroke
1690                                        .getModifierKeys() & e.stateMask) == triggerKeyStroke
1691                                        .getModifierKeys()))) {
1692                            // We never propagate the keystroke for an explicit
1693
// keystroke invocation of the popup
1694
e.doit = false;
1695                            openProposalPopup(false);
1696                            return;
1697                        }
1698                    }
1699                    /*
1700                     * The triggering keystroke was not invoked. Check for
1701                     * autoactivation characters.
1702                     */

1703                    if (e.character != 0) {
1704                        // Auto-activation characters were specified. Check
1705
// them.
1706
if (autoActivateString != null) {
1707                            if (autoActivateString.indexOf(e.character) >= 0) {
1708                                e.doit = propagateKeys;
1709                                autoActivate();
1710                            }
1711                        } else {
1712                            // No autoactivation occurred, so record the key
1713
// down
1714
// as a means to interrupt any autoactivation that
1715
// is
1716
// pending.
1717
receivedKeyDown = true;
1718                        }
1719                    }
1720                    break;
1721
1722                // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=147377
1723
// Given that we will close the popup when there are no valid
1724
// proposals, we must reopen it when there are. Normally, the
1725
// keydown event handling will catch all the cases where it
1726
// should reopen. But when autoactivation should occur on all
1727
// content changes, we check it here after keys have been
1728
// processed.
1729
// See also https://bugs.eclipse.org/bugs/show_bug.cgi?id=183650
1730
// We should not autoactivate if the content change was caused
1731
// by the popup itself.
1732
case SWT.Modify:
1733                    if (triggerKeyStroke == null && autoActivateString == null
1734                            && !modifyingControlContent) {
1735                        if (DEBUG) {
1736                            dump("Modify event triggers autoactivation", e); //$NON-NLS-1$
1737
}
1738                        autoActivate();
1739                    }
1740                    break;
1741                default:
1742                    break;
1743                }
1744            }
1745
1746            /**
1747             * Dump the given events to "standard" output.
1748             *
1749             * @param who
1750             * who is dumping the event
1751             * @param e
1752             * the event
1753             */

1754            private void dump(String JavaDoc who, Event e) {
1755                StringBuffer JavaDoc sb = new StringBuffer JavaDoc(
1756                        "--- [ContentProposalAdapter]\n"); //$NON-NLS-1$
1757
sb.append(who);
1758                sb.append(" - e: keyCode=" + e.keyCode + hex(e.keyCode)); //$NON-NLS-1$
1759
sb.append("; character=" + e.character + hex(e.character)); //$NON-NLS-1$
1760
sb.append("; stateMask=" + e.stateMask + hex(e.stateMask)); //$NON-NLS-1$
1761
sb.append("; doit=" + e.doit); //$NON-NLS-1$
1762
sb.append("; detail=" + e.detail + hex(e.detail)); //$NON-NLS-1$
1763
sb.append("; widget=" + e.widget); //$NON-NLS-1$
1764
System.out.println(sb);
1765            }
1766
1767            private String JavaDoc hex(int i) {
1768                return "[0x" + Integer.toHexString(i) + ']'; //$NON-NLS-1$
1769
}
1770        };
1771        control.addListener(SWT.KeyDown, controlListener);
1772        control.addListener(SWT.Traverse, controlListener);
1773        control.addListener(SWT.Modify, controlListener);
1774
1775        if (DEBUG) {
1776            System.out
1777                    .println("ContentProposalAdapter#installControlListener() - installed"); //$NON-NLS-1$
1778
}
1779    }
1780
1781    /**
1782     * Open the proposal popup and display the proposals provided by the
1783     * proposal provider. If there are no proposals to be shown, do not show the
1784     * popup. This method returns immediately. That is, it does not wait for the
1785     * popup to open or a proposal to be selected.
1786     *
1787     * @param autoActivated
1788     * a boolean indicating whether the popup was autoactivated. If
1789     * false, a beep will sound when no proposals can be shown.
1790     */

1791    private void openProposalPopup(boolean autoActivated) {
1792        if (isValid()) {
1793            if (popup == null) {
1794                // Check whether there are any proposals to be shown.
1795
recordCursorPosition(); // must be done before getting proposals
1796
IContentProposal[] proposals = getProposals();
1797                if (proposals.length > 0) {
1798                    if (DEBUG) {
1799                        System.out.println("POPUP OPENED BY PRECEDING EVENT"); //$NON-NLS-1$
1800
}
1801                    recordCursorPosition();
1802                    popup = new ContentProposalPopup(null, proposals);
1803                    popup.open();
1804                    popup.getShell().addDisposeListener(new DisposeListener() {
1805                        public void widgetDisposed(DisposeEvent event) {
1806                            popup = null;
1807                        }
1808                    });
1809                    notifyPopupOpened();
1810                } else if (!autoActivated) {
1811                    getControl().getDisplay().beep();
1812                }
1813            }
1814        }
1815    }
1816
1817    /**
1818     * Open the proposal popup and display the proposals provided by the
1819     * proposal provider. This method returns immediately. That is, it does not
1820     * wait for a proposal to be selected. This method is used by subclasses to
1821     * explicitly invoke the opening of the popup. If there are no proposals to
1822     * show, the popup will not open and a beep will be sounded.
1823     */

1824    protected void openProposalPopup() {
1825        openProposalPopup(false);
1826    }
1827
1828    /**
1829     * Close the proposal popup without accepting a proposal. This method
1830     * returns immediately, and has no effect if the proposal popup was not
1831     * open. This method is used by subclasses to explicitly close the popup
1832     * based on additional logic.
1833     *
1834     * @since 3.3
1835     */

1836    protected void closeProposalPopup() {
1837        if (popup != null) {
1838            popup.close();
1839        }
1840    }
1841
1842    /*
1843     * A content proposal has been accepted. Update the control contents
1844     * accordingly and notify any listeners.
1845     *
1846     * @param proposal the accepted proposal
1847     */

1848    private void proposalAccepted(IContentProposal proposal) {
1849        switch (proposalAcceptanceStyle) {
1850        case (PROPOSAL_REPLACE):
1851            setControlContent(proposal.getContent(), proposal
1852                    .getCursorPosition());
1853            break;
1854        case (PROPOSAL_INSERT):
1855            insertControlContent(proposal.getContent(), proposal
1856                    .getCursorPosition());
1857            break;
1858        default:
1859            // do nothing. Typically a listener is installed to handle this in
1860
// a custom way.
1861
break;
1862        }
1863
1864        // In all cases, notify listeners of an accepted proposal.
1865
notifyProposalAccepted(proposal);
1866    }
1867
1868    /*
1869     * Set the text content of the control to the specified text, setting the
1870     * cursorPosition at the desired location within the new contents.
1871     */

1872    private void setControlContent(String JavaDoc text, int cursorPosition) {
1873        if (isValid()) {
1874            // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=183650
1875
modifyingControlContent = true;
1876
1877            controlContentAdapter.setControlContents(control, text,
1878                    cursorPosition);
1879            
1880            modifyingControlContent = false;
1881        }
1882    }
1883
1884    /*
1885     * Insert the specified text into the control content, setting the
1886     * cursorPosition at the desired location within the new contents.
1887     */

1888    private void insertControlContent(String JavaDoc text, int cursorPosition) {
1889        if (isValid()) {
1890            // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=183650
1891
modifyingControlContent = true;
1892            // Not all controls preserve their selection index when they lose
1893
// focus, so we must set it explicitly here to what it was before
1894
// the popup opened.
1895
// See https://bugs.eclipse.org/bugs/show_bug.cgi?id=127108
1896
if (insertionPos != -1) {
1897                controlContentAdapter.setCursorPosition(control, insertionPos);
1898            }
1899            controlContentAdapter.insertControlContents(control, text,
1900                    cursorPosition);
1901            modifyingControlContent = false;
1902        }
1903    }
1904
1905    /*
1906     * Check that the control and content adapter are valid.
1907     */

1908    private boolean isValid() {
1909        return control != null && !control.isDisposed()
1910                && controlContentAdapter != null;
1911    }
1912
1913    /*
1914     * Record the control's cursor position.
1915     */

1916    private void recordCursorPosition() {
1917        if (isValid()) {
1918            insertionPos = getControlContentAdapter()
1919                    .getCursorPosition(control);
1920
1921        }
1922    }
1923
1924    /*
1925     * Get the proposals from the proposal provider. Gets all of the proposals
1926     * without doing any filtering.
1927     */

1928    private IContentProposal[] getProposals() {
1929        if (proposalProvider == null || !isValid()) {
1930            return null;
1931        }
1932        if (DEBUG) {
1933            System.out.println(">>> obtaining proposals from provider"); //$NON-NLS-1$
1934
}
1935        int position = insertionPos;
1936        if (position == -1) {
1937            position = getControlContentAdapter().getCursorPosition(
1938                    getControl());
1939        }
1940        String JavaDoc contents = getControlContentAdapter().getControlContents(
1941                getControl());
1942        IContentProposal[] proposals = proposalProvider.getProposals(contents,
1943                position);
1944        return proposals;
1945    }
1946
1947    /**
1948     * Autoactivation has been triggered. Open the popup using any specified
1949     * delay.
1950     */

1951    private void autoActivate() {
1952        if (autoActivationDelay > 0) {
1953            Runnable JavaDoc runnable = new Runnable JavaDoc() {
1954                public void run() {
1955                    receivedKeyDown = false;
1956                    try {
1957                        Thread.sleep(autoActivationDelay);
1958                    } catch (InterruptedException JavaDoc e) {
1959                    }
1960                    if (!isValid() || receivedKeyDown) {
1961                        return;
1962                    }
1963                    getControl().getDisplay().syncExec(new Runnable JavaDoc() {
1964                        public void run() {
1965                            openProposalPopup(true);
1966                        }
1967                    });
1968                }
1969            };
1970            Thread JavaDoc t = new Thread JavaDoc(runnable);
1971            t.start();
1972        } else {
1973            // Since we do not sleep, we must open the popup
1974
// in an async exec. This is necessary because
1975
// this method may be called in the middle of handling
1976
// some event that will cause the cursor position or
1977
// other important info to change as a result of this
1978
// event occurring.
1979
getControl().getDisplay().asyncExec(new Runnable JavaDoc() {
1980                public void run() {
1981                    if (isValid()) {
1982                        openProposalPopup(true);
1983                    }
1984                }
1985            });
1986        }
1987    }
1988
1989    /*
1990     * A proposal has been accepted. Notify interested listeners.
1991     */

1992    private void notifyProposalAccepted(IContentProposal proposal) {
1993        if (DEBUG) {
1994            System.out.println("Notify listeners - proposal accepted."); //$NON-NLS-1$
1995
}
1996        final Object JavaDoc[] listenerArray = proposalListeners.getListeners();
1997        for (int i = 0; i < listenerArray.length; i++) {
1998            ((IContentProposalListener) listenerArray[i])
1999                    .proposalAccepted(proposal);
2000        }
2001    }
2002
2003    /*
2004     * The proposal popup has opened. Notify interested listeners.
2005     */

2006    private void notifyPopupOpened() {
2007        if (DEBUG) {
2008            System.out.println("Notify listeners - popup opened."); //$NON-NLS-1$
2009
}
2010        final Object JavaDoc[] listenerArray = proposalListeners2.getListeners();
2011        for (int i = 0; i < listenerArray.length; i++) {
2012            ((IContentProposalListener2) listenerArray[i])
2013                    .proposalPopupOpened(this);
2014        }
2015    }
2016
2017    /*
2018     * The proposal popup has closed. Notify interested listeners.
2019     */

2020    private void notifyPopupClosed() {
2021        if (DEBUG) {
2022            System.out.println("Notify listeners - popup closed."); //$NON-NLS-1$
2023
}
2024        final Object JavaDoc[] listenerArray = proposalListeners2.getListeners();
2025        for (int i = 0; i < listenerArray.length; i++) {
2026            ((IContentProposalListener2) listenerArray[i])
2027                    .proposalPopupClosed(this);
2028        }
2029    }
2030}
2031
Popular Tags