Package org.eclipse.jface.fieldassist

Source Code of org.eclipse.jface.fieldassist.ContentProposalAdapter$ContentProposalPopup$PopupCloserListener

/*******************************************************************************
* Copyright (c) 2005, 2010 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
*     IBM Corporation - initial API and implementation
*     Hannes Erven <hannes@erven.at> - Bug 293841 - [FieldAssist] NumLock keyDown event should not close the proposal popup [with patch]
*******************************************************************************/
package org.eclipse.jface.fieldassist;

import java.util.ArrayList;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ListenerList;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.SelectionListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.widgets.Combo;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.ScrollBar;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;

import org.eclipse.jface.bindings.keys.KeyStroke;
import org.eclipse.jface.dialogs.PopupDialog;
import org.eclipse.jface.preference.JFacePreferences;
import org.eclipse.jface.resource.JFaceResources;
import org.eclipse.jface.util.Util;
import org.eclipse.jface.viewers.ILabelProvider;

/**
* ContentProposalAdapter can be used to attach content proposal behavior to a
* control. This behavior includes obtaining proposals, opening a popup dialog,
* managing the content of the control relative to the selections in the popup,
* and optionally opening up a secondary popup to further describe proposals.
* <p>
* A number of configurable options are provided to determine how the control
* content is altered when a proposal is chosen, how the content proposal popup
* is activated, and whether any filtering should be done on the proposals as
* the user types characters.
* <p>
* This class provides some overridable methods to allow clients to manually
* control the popup. However, most of the implementation remains private.
*
* @since 3.2
*/
public class ContentProposalAdapter {

  /*
   * The lightweight popup used to show content proposals for a text field. If
   * additional information exists for a proposal, then selecting that
   * proposal will result in the information being displayed in a secondary
   * popup.
   */
  class ContentProposalPopup extends PopupDialog {
    /*
     * The listener we install on the popup and related controls to
     * determine when to close the popup. Some events (move, resize, close,
     * deactivate) trigger closure as soon as they are received, simply
     * because one of the registered listeners received them. Other events
     * depend on additional circumstances.
     */
    private final class PopupCloserListener implements Listener {
      private boolean scrollbarClicked = false;

      public void handleEvent(final Event e) {

        // If focus is leaving an important widget or the field's
        // shell is deactivating
        if (e.type == SWT.FocusOut) {
          scrollbarClicked = false;
          /*
           * Ignore this event if it's only happening because focus is
           * moving between the popup shells, their controls, or a
           * scrollbar. Do this in an async since the focus is not
           * actually switched when this event is received.
           */
          e.display.asyncExec(new Runnable() {
            public void run() {
              if (isValid()) {
                if (scrollbarClicked || hasFocus()) {
                  return;
                }
                // Workaround a problem on X and Mac, whereby at
                // this point, the focus control is not known.
                // This can happen, for example, when resizing
                // the popup shell on the Mac.
                // Check the active shell.
                Shell activeShell = e.display.getActiveShell();
                if (activeShell == getShell()
                    || (infoPopup != null && infoPopup
                        .getShell() == activeShell)) {
                  return;
                }
                /*
                 * System.out.println(e);
                 * System.out.println(e.display.getFocusControl());
                 * System.out.println(e.display.getActiveShell());
                 */
                close();
              }
            }
          });
          return;
        }

        // Scroll bar has been clicked. Remember this for focus event
        // processing.
        if (e.type == SWT.Selection) {
          scrollbarClicked = true;
          return;
        }
        // For all other events, merely getting them dictates closure.
        close();
      }

      // Install the listeners for events that need to be monitored for
      // popup closure.
      void installListeners() {
        // Listeners on this popup's table and scroll bar
        proposalTable.addListener(SWT.FocusOut, this);
        ScrollBar scrollbar = proposalTable.getVerticalBar();
        if (scrollbar != null) {
          scrollbar.addListener(SWT.Selection, this);
        }

        // Listeners on this popup's shell
        getShell().addListener(SWT.Deactivate, this);
        getShell().addListener(SWT.Close, this);

        // Listeners on the target control
        control.addListener(SWT.MouseDoubleClick, this);
        control.addListener(SWT.MouseDown, this);
        control.addListener(SWT.Dispose, this);
        control.addListener(SWT.FocusOut, this);
        // Listeners on the target control's shell
        Shell controlShell = control.getShell();
        controlShell.addListener(SWT.Move, this);
        controlShell.addListener(SWT.Resize, this);

      }

      // Remove installed listeners
      void removeListeners() {
        if (isValid()) {
          proposalTable.removeListener(SWT.FocusOut, this);
          ScrollBar scrollbar = proposalTable.getVerticalBar();
          if (scrollbar != null) {
            scrollbar.removeListener(SWT.Selection, this);
          }

          getShell().removeListener(SWT.Deactivate, this);
          getShell().removeListener(SWT.Close, this);
        }

        if (control != null && !control.isDisposed()) {

          control.removeListener(SWT.MouseDoubleClick, this);
          control.removeListener(SWT.MouseDown, this);
          control.removeListener(SWT.Dispose, this);
          control.removeListener(SWT.FocusOut, this);

          Shell controlShell = control.getShell();
          controlShell.removeListener(SWT.Move, this);
          controlShell.removeListener(SWT.Resize, this);
        }
      }
    }

    /*
     * The listener we will install on the target control.
     */
    private final class TargetControlListener implements Listener {
      // Key events from the control
      public void handleEvent(Event e) {
        if (!isValid()) {
          return;
        }

        char key = e.character;

        // Traverse events are handled depending on whether the
        // event has a character.
        if (e.type == SWT.Traverse) {
          // If the traverse event contains a legitimate character,
          // then we must set doit false so that the widget will
          // receive the key event. We return immediately so that
          // the character is handled only in the key event.
          // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=132101
          if (key != 0) {
            e.doit = false;
            return;
          }
          // Traversal does not contain a character. Set doit true
          // to indicate TRAVERSE_NONE will occur and that no key
          // event will be triggered. We will check for navigation
          // keys below.
          e.detail = SWT.TRAVERSE_NONE;
          e.doit = true;
        } else {
          // Default is to only propagate when configured that way.
          // Some keys will always set doit to false anyway.
          e.doit = propagateKeys;
        }

        // No character. Check for navigation keys.

        if (key == 0) {
          int newSelection = proposalTable.getSelectionIndex();
          int visibleRows = (proposalTable.getSize().y / proposalTable
              .getItemHeight()) - 1;
          switch (e.keyCode) {
          case SWT.ARROW_UP:
            newSelection -= 1;
            if (newSelection < 0) {
              newSelection = proposalTable.getItemCount() - 1;
            }
            // Not typical - usually we get this as a Traverse and
            // therefore it never propagates. Added for consistency.
            if (e.type == SWT.KeyDown) {
              // don't propagate to control
              e.doit = false;
            }

            break;

          case SWT.ARROW_DOWN:
            newSelection += 1;
            if (newSelection > proposalTable.getItemCount() - 1) {
              newSelection = 0;
            }
            // Not typical - usually we get this as a Traverse and
            // therefore it never propagates. Added for consistency.
            if (e.type == SWT.KeyDown) {
              // don't propagate to control
              e.doit = false;
            }

            break;

          case SWT.PAGE_DOWN:
            newSelection += visibleRows;
            if (newSelection >= proposalTable.getItemCount()) {
              newSelection = proposalTable.getItemCount() - 1;
            }
            if (e.type == SWT.KeyDown) {
              // don't propagate to control
              e.doit = false;
            }
            break;

          case SWT.PAGE_UP:
            newSelection -= visibleRows;
            if (newSelection < 0) {
              newSelection = 0;
            }
            if (e.type == SWT.KeyDown) {
              // don't propagate to control
              e.doit = false;
            }
            break;

          case SWT.HOME:
            newSelection = 0;
            if (e.type == SWT.KeyDown) {
              // don't propagate to control
              e.doit = false;
            }
            break;

          case SWT.END:
            newSelection = proposalTable.getItemCount() - 1;
            if (e.type == SWT.KeyDown) {
              // don't propagate to control
              e.doit = false;
            }
            break;

          // If received as a Traverse, these should propagate
          // to the control as keydown. If received as a keydown,
          // proposals should be recomputed since the cursor
          // position has changed.
          case SWT.ARROW_LEFT:
          case SWT.ARROW_RIGHT:
            if (e.type == SWT.Traverse) {
              e.doit = false;
            } else {
              e.doit = true;
              String contents = getControlContentAdapter()
                  .getControlContents(getControl());
              // If there are no contents, changes in cursor
              // position have no effect. Note also that we do
              // not affect the filter text on ARROW_LEFT as
              // we would with BS.
              if (contents.length() > 0) {
                asyncRecomputeProposals(filterText);
              }
            }
            break;

          // Any unknown keycodes will cause the popup to close.
          // Modifier keys are explicitly checked and ignored because
          // they are not complete yet (no character).
          default:
            if (e.keyCode != SWT.CAPS_LOCK && e.keyCode != SWT.NUM_LOCK
                && e.keyCode != SWT.MOD1
                && e.keyCode != SWT.MOD2
                && e.keyCode != SWT.MOD3
                && e.keyCode != SWT.MOD4) {
              close();
            }
            return;
          }

          // If any of these navigation events caused a new selection,
          // then handle that now and return.
          if (newSelection >= 0) {
            selectProposal(newSelection);
          }
          return;
        }

        // key != 0
        // Check for special keys involved in cancelling, accepting, or
        // filtering the proposals.
        switch (key) {
        case SWT.ESC:
          e.doit = false;
          close();
          break;

        case SWT.LF:
        case SWT.CR:
          e.doit = false;
          Object p = getSelectedProposal();
          if (p != null) {
            acceptCurrentProposal();
          } else {
            close();
          }
          break;

        case SWT.TAB:
          e.doit = false;
          getShell().setFocus();
          return;

        case SWT.BS:
          // Backspace should back out of any stored filter text
          if (filterStyle != FILTER_NONE) {
            // We have no filter to back out of, so do nothing
            if (filterText.length() == 0) {
              return;
            }
            // There is filter to back out of
            filterText = filterText.substring(0, filterText
                .length() - 1);
            asyncRecomputeProposals(filterText);
            return;
          }
          // There is no filtering provided by us, but some
          // clients provide their own filtering based on content.
          // Recompute the proposals if the cursor position
          // will change (is not at 0).
          int pos = getControlContentAdapter().getCursorPosition(
              getControl());
          // We rely on the fact that the contents and pos do not yet
          // reflect the result of the BS. If the contents were
          // already empty, then BS should not cause
          // a recompute.
          if (pos > 0) {
            asyncRecomputeProposals(filterText);
          }
          break;

        default:
          // If the key is a defined unicode character, and not one of
          // the special cases processed above, update the filter text
          // and filter the proposals.
          if (Character.isDefined(key)) {
            if (filterStyle == FILTER_CUMULATIVE) {
              filterText = filterText + String.valueOf(key);
            } else if (filterStyle == FILTER_CHARACTER) {
              filterText = String.valueOf(key);
            }
            // Recompute proposals after processing this event.
            asyncRecomputeProposals(filterText);
          }
          break;
        }
      }
    }

    /*
     * Internal class used to implement the secondary popup.
     */
    private class InfoPopupDialog extends PopupDialog {

      /*
       * The text control that displays the text.
       */
      private Text text;

      /*
       * The String shown in the popup.
       */
      private String contents = EMPTY;

      /*
       * Construct an info-popup with the specified parent.
       */
      InfoPopupDialog(Shell parent) {
        super(parent, PopupDialog.HOVER_SHELLSTYLE, false, false, false,
            false, false, null, null);
      }

      /*
       * Create a text control for showing the info about a proposal.
       */
      protected Control createDialogArea(Composite parent) {
        text = new Text(parent, SWT.MULTI | SWT.READ_ONLY | SWT.WRAP
            | SWT.NO_FOCUS);

        // Use the compact margins employed by PopupDialog.
        GridData gd = new GridData(GridData.BEGINNING
            | GridData.FILL_BOTH);
        gd.horizontalIndent = PopupDialog.POPUP_HORIZONTALSPACING;
        gd.verticalIndent = PopupDialog.POPUP_VERTICALSPACING;
        text.setLayoutData(gd);
        text.setText(contents);

        // since SWT.NO_FOCUS is only a hint...
        text.addFocusListener(new FocusAdapter() {
          public void focusGained(FocusEvent event) {
            ContentProposalPopup.this.close();
          }
        });
        return text;
      }

      /*
       * Adjust the bounds so that we appear adjacent to our parent shell
       */
      protected void adjustBounds() {
        Rectangle parentBounds = getParentShell().getBounds();
        Rectangle proposedBounds;
        // Try placing the info popup to the right
        Rectangle rightProposedBounds = new Rectangle(parentBounds.x
            + parentBounds.width
            + PopupDialog.POPUP_HORIZONTALSPACING, parentBounds.y
            + PopupDialog.POPUP_VERTICALSPACING,
            parentBounds.width, parentBounds.height);
        rightProposedBounds = getConstrainedShellBounds(rightProposedBounds);
        // If it won't fit on the right, try the left
        if (rightProposedBounds.intersects(parentBounds)) {
          Rectangle leftProposedBounds = new Rectangle(parentBounds.x
              - parentBounds.width - POPUP_HORIZONTALSPACING - 1,
              parentBounds.y, parentBounds.width,
              parentBounds.height);
          leftProposedBounds = getConstrainedShellBounds(leftProposedBounds);
          // If it won't fit on the left, choose the proposed bounds
          // that fits the best
          if (leftProposedBounds.intersects(parentBounds)) {
            if (rightProposedBounds.x - parentBounds.x >= parentBounds.x
                - leftProposedBounds.x) {
              rightProposedBounds.x = parentBounds.x
                  + parentBounds.width
                  + PopupDialog.POPUP_HORIZONTALSPACING;
              proposedBounds = rightProposedBounds;
            } else {
              leftProposedBounds.width = parentBounds.x
                  - POPUP_HORIZONTALSPACING
                  - leftProposedBounds.x;
              proposedBounds = leftProposedBounds;
            }
          } else {
            // use the proposed bounds on the left
            proposedBounds = leftProposedBounds;
          }
        } else {
          // use the proposed bounds on the right
          proposedBounds = rightProposedBounds;
        }
        getShell().setBounds(proposedBounds);
      }
     
      /*
       * (non-Javadoc)
       * @see org.eclipse.jface.dialogs.PopupDialog#getForeground()
       */
      protected Color getForeground() {
        return control.getDisplay().
            getSystemColor(SWT.COLOR_INFO_FOREGROUND);
      }
     
      /*
       * (non-Javadoc)
       * @see org.eclipse.jface.dialogs.PopupDialog#getBackground()
       */
      protected Color getBackground() {
        return control.getDisplay().
            getSystemColor(SWT.COLOR_INFO_BACKGROUND);
      }

      /*
       * Set the text contents of the popup.
       */
      void setContents(String newContents) {
        if (newContents == null) {
          newContents = EMPTY;
        }
        this.contents = newContents;
        if (text != null && !text.isDisposed()) {
          text.setText(contents);
        }
      }

      /*
       * Return whether the popup has focus.
       */
      boolean hasFocus() {
        if (text == null || text.isDisposed()) {
          return false;
        }
        return text.getShell().isFocusControl()
            || text.isFocusControl();
      }
    }

    /*
     * The listener installed on the target control.
     */
    private Listener targetControlListener;

    /*
     * The listener installed in order to close the popup.
     */
    private PopupCloserListener popupCloser;

    /*
     * The table used to show the list of proposals.
     */
    private Table proposalTable;

    /*
     * The proposals to be shown (cached to avoid repeated requests).
     */
    private IContentProposal[] proposals;

    /*
     * Secondary popup used to show detailed information about the selected
     * proposal..
     */
    private InfoPopupDialog infoPopup;

    /*
     * Flag indicating whether there is a pending secondary popup update.
     */
    private boolean pendingDescriptionUpdate = false;

    /*
     * Filter text - tracked while popup is open, only if we are told to
     * filter
     */
    private String filterText = EMPTY;

    /**
     * Constructs a new instance of this popup, specifying the control for
     * which this popup is showing content, and how the proposals should be
     * obtained and displayed.
     *
     * @param infoText
     *            Text to be shown in a lower info area, or
     *            <code>null</code> if there is no info area.
     */
    ContentProposalPopup(String infoText, IContentProposal[] proposals) {
      // IMPORTANT: Use of SWT.ON_TOP is critical here for ensuring
      // that the target control retains focus on Mac and Linux. Without
      // it, the focus will disappear, keystrokes will not go to the
      // popup, and the popup closer will wrongly close the popup.
      // On platforms where SWT.ON_TOP overrides SWT.RESIZE, we will live
      // with this.
      // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=126138
      super(control.getShell(), SWT.RESIZE | SWT.ON_TOP, false, false, false,
          false, false, null, infoText);
      this.proposals = proposals;
    }

    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.dialogs.PopupDialog#getForeground()
     */
    protected Color getForeground() {
      return JFaceResources.getColorRegistry().get(
          JFacePreferences.CONTENT_ASSIST_FOREGROUND_COLOR);
    }
   
    /*
     * (non-Javadoc)
     * @see org.eclipse.jface.dialogs.PopupDialog#getBackground()
     */
    protected Color getBackground() {
      return JFaceResources.getColorRegistry().get(
          JFacePreferences.CONTENT_ASSIST_BACKGROUND_COLOR);
    }

    /*
     * Creates the content area for the proposal popup. This creates a table
     * and places it inside the composite. The table will contain a list of
     * all the proposals.
     *
     * @param parent The parent composite to contain the dialog area; must
     * not be <code>null</code>.
     */
    protected final Control createDialogArea(final Composite parent) {
      // Use virtual where appropriate (see flag definition).
      if (USE_VIRTUAL) {
        proposalTable = new Table(parent, SWT.H_SCROLL | SWT.V_SCROLL
            | SWT.VIRTUAL);

        Listener listener = new Listener() {
          public void handleEvent(Event event) {
            handleSetData(event);
          }
        };
        proposalTable.addListener(SWT.SetData, listener);
      } else {
        proposalTable = new Table(parent, SWT.H_SCROLL | SWT.V_SCROLL);
      }

      // set the proposals to force population of the table.
      setProposals(filterProposals(proposals, filterText));

      proposalTable.setHeaderVisible(false);
      proposalTable.addSelectionListener(new SelectionListener() {

        public void widgetSelected(SelectionEvent e) {
          // If a proposal has been selected, show it in the secondary
          // popup. Otherwise close the popup.
          if (e.item == null) {
            if (infoPopup != null) {
              infoPopup.close();
            }
          } else {
            showProposalDescription();
          }
        }

        // Default selection was made. Accept the current proposal.
        public void widgetDefaultSelected(SelectionEvent e) {
          acceptCurrentProposal();
        }
      });
      return proposalTable;
    }

    /*
     * (non-Javadoc)
     *
     * @see org.eclipse.jface.dialogs.PopupDialog.adjustBounds()
     */
    protected void adjustBounds() {
      // Get our control's location in display coordinates.
      Point location = control.getDisplay().map(control.getParent(), null, control.getLocation());     
      int initialX = location.x + POPUP_OFFSET;
      int initialY = location.y + control.getSize().y + POPUP_OFFSET;
      // If we are inserting content, use the cursor position to
      // position the control.
      if (getProposalAcceptanceStyle() == PROPOSAL_INSERT) {
        Rectangle insertionBounds = controlContentAdapter
            .getInsertionBounds(control);
        initialX = initialX + insertionBounds.x;
        initialY = location.y + insertionBounds.y
            + insertionBounds.height;
      }

      // If there is no specified size, force it by setting
      // up a layout on the table.
      if (popupSize == null) {
        GridData data = new GridData(GridData.FILL_BOTH);
        data.heightHint = proposalTable.getItemHeight()
            * POPUP_CHAR_HEIGHT;
        data.widthHint = Math.max(control.getSize().x,
            POPUP_MINIMUM_WIDTH);
        proposalTable.setLayoutData(data);
        getShell().pack();
        popupSize = getShell().getSize();
      }
     
      // Constrain to the display
      Rectangle constrainedBounds = getConstrainedShellBounds(new Rectangle(initialX, initialY, popupSize.x, popupSize.y));
     
      // If there has been an adjustment causing the popup to overlap
      // with the control, then put the popup above the control.
      if (constrainedBounds.y < initialY)
        getShell().setBounds(initialX, location.y - popupSize.y, popupSize.x, popupSize.y);
      else
        getShell().setBounds(initialX, initialY, popupSize.x, popupSize.y);

      // Now set up a listener to monitor any changes in size.
      getShell().addListener(SWT.Resize, new Listener() {
        public void handleEvent(Event e) {
          popupSize = getShell().getSize();
          if (infoPopup != null) {
            infoPopup.adjustBounds();
          }
        }
      });
    }

    /*
     * Handle the set data event. Set the item data of the requested item to
     * the corresponding proposal in the proposal cache.
     */
    private void handleSetData(Event event) {
      TableItem item = (TableItem) event.item;
      int index = proposalTable.indexOf(item);

      if (0 <= index && index < proposals.length) {
        IContentProposal current = proposals[index];
        item.setText(getString(current));
        item.setImage(getImage(current));
        item.setData(current);
      } else {
        // this should not happen, but does on win32
      }
    }

    /*
     * Caches the specified proposals and repopulates the table if it has
     * been created.
     */
    private void setProposals(IContentProposal[] newProposals) {
      if (newProposals == null || newProposals.length == 0) {
        newProposals = getEmptyProposalArray();
      }
      this.proposals = newProposals;

      // If there is a table
      if (isValid()) {
        final int newSize = newProposals.length;
        if (USE_VIRTUAL) {
          // Set and clear the virtual table. Data will be
          // provided in the SWT.SetData event handler.
          proposalTable.setItemCount(newSize);
          proposalTable.clearAll();
        } else {
          // Populate the table manually
          proposalTable.setRedraw(false);
          proposalTable.setItemCount(newSize);
          TableItem[] items = proposalTable.getItems();
          for (int i = 0; i < items.length; i++) {
            TableItem item = items[i];
            IContentProposal proposal = newProposals[i];
            item.setText(getString(proposal));
            item.setImage(getImage(proposal));
            item.setData(proposal);
          }
          proposalTable.setRedraw(true);
        }
        // Default to the first selection if there is content.
        if (newProposals.length > 0) {
          selectProposal(0);
        } else {
          // No selection, close the secondary popup if it was open
          if (infoPopup != null) {
            infoPopup.close();
          }

        }
      }
    }

    /*
     * Get the string for the specified proposal. Always return a String of
     * some kind.
     */
    private String getString(IContentProposal proposal) {
      if (proposal == null) {
        return EMPTY;
      }
      if (labelProvider == null) {
        return proposal.getLabel() == null ? proposal.getContent()
            : proposal.getLabel();
      }
      return labelProvider.getText(proposal);
    }

    /*
     * Get the image for the specified proposal. If there is no image
     * available, return null.
     */
    private Image getImage(IContentProposal proposal) {
      if (proposal == null || labelProvider == null) {
        return null;
      }
      return labelProvider.getImage(proposal);
    }

    /*
     * Return an empty array. Used so that something always shows in the
     * proposal popup, even if no proposal provider was specified.
     */
    private IContentProposal[] getEmptyProposalArray() {
      return new IContentProposal[0];
    }

    /*
     * Answer true if the popup is valid, which means the table has been
     * created and not disposed.
     */
    private boolean isValid() {
      return proposalTable != null && !proposalTable.isDisposed();
    }

    /*
     * Return whether the receiver has focus. Since 3.4, this includes a
     * check for whether the info popup has focus.
     */
    private boolean hasFocus() {
      if (!isValid()) {
        return false;
      }
      if (getShell().isFocusControl() || proposalTable.isFocusControl()) {
        return true;
      }
      if (infoPopup != null && infoPopup.hasFocus()) {
        return true;
      }
      return false;
    }

    /*
     * Return the current selected proposal.
     */
    private IContentProposal getSelectedProposal() {
      if (isValid()) {
        int i = proposalTable.getSelectionIndex();
        if (proposals == null || i < 0 || i >= proposals.length) {
          return null;
        }
        return proposals[i];
      }
      return null;
    }

    /*
     * Select the proposal at the given index.
     */
    private void selectProposal(int index) {
      Assert
          .isTrue(index >= 0,
              "Proposal index should never be negative"); //$NON-NLS-1$
      if (!isValid() || proposals == null || index >= proposals.length) {
        return;
      }
      proposalTable.setSelection(index);
      proposalTable.showSelection();

      showProposalDescription();
    }

    /**
     * Opens this ContentProposalPopup. This method is extended in order to
     * add the control listener when the popup is opened and to invoke the
     * secondary popup if applicable.
     *
     * @return the return code
     *
     * @see org.eclipse.jface.window.Window#open()
     */
    public int open() {
      int value = super.open();
      if (popupCloser == null) {
        popupCloser = new PopupCloserListener();
      }
      popupCloser.installListeners();
      IContentProposal p = getSelectedProposal();
      if (p != null) {
        showProposalDescription();
      }
      return value;
    }

    /**
     * Closes this popup. This method is extended to remove the control
     * listener.
     *
     * @return <code>true</code> if the window is (or was already) closed,
     *         and <code>false</code> if it is still open
     */
    public boolean close() {
      popupCloser.removeListeners();
      if (infoPopup != null) {
        infoPopup.close();
      }
      boolean ret = super.close();
      notifyPopupClosed();
      return ret;
    }

    /*
     * Show the currently selected proposal's description in a secondary
     * popup.
     */
    private void showProposalDescription() {
      // If we do not already have a pending update, then
      // create a thread now that will show the proposal description
      if (!pendingDescriptionUpdate) {
        // Create a thread that will sleep for the specified delay
        // before creating the popup. We do not use Jobs since this
        // code must be able to run independently of the Eclipse
        // runtime.
        Runnable runnable = new Runnable() {
          public void run() {
            pendingDescriptionUpdate = true;
            try {
              Thread.sleep(POPUP_DELAY);
            } catch (InterruptedException e) {
            }
            if (!isValid()) {
              return;
            }
            getShell().getDisplay().syncExec(new Runnable() {
              public void run() {
                // Query the current selection since we have
                // been delayed
                IContentProposal p = getSelectedProposal();
                if (p != null) {
                  String description = p.getDescription();
                  if (description != null) {
                    if (infoPopup == null) {
                      infoPopup = new InfoPopupDialog(
                          getShell());
                      infoPopup.open();
                      infoPopup
                          .getShell()
                          .addDisposeListener(
                              new DisposeListener() {
                                public void widgetDisposed(
                                    DisposeEvent event) {
                                  infoPopup = null;
                                }
                              });
                    }
                    infoPopup.setContents(p
                        .getDescription());
                  } else if (infoPopup != null) {
                    infoPopup.close();
                  }
                  pendingDescriptionUpdate = false;

                }
              }
            });
          }
        };
        Thread t = new Thread(runnable);
        t.start();
      }
    }

    /*
     * Accept the current proposal.
     */
    private void acceptCurrentProposal() {
      // Close before accepting the proposal. This is important
      // so that the cursor position can be properly restored at
      // acceptance, which does not work without focus on some controls.
      // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=127108
      IContentProposal proposal = getSelectedProposal();
      close();
      proposalAccepted(proposal);
    }

    /*
     * Request the proposals from the proposal provider, and recompute any
     * caches. Repopulate the popup if it is open.
     */
    private void recomputeProposals(String filterText) {
      IContentProposal[] allProposals = getProposals();
      if (allProposals == null)
         allProposals = getEmptyProposalArray();
      // If the non-filtered proposal list is empty, we should
      // close the popup.
      // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=147377
      if (allProposals.length == 0) {
        proposals = allProposals;
        close();
      } else {
        // Keep the popup open, but filter by any provided filter text
        setProposals(filterProposals(allProposals, filterText));
      }
    }

    /*
     * In an async block, request the proposals. This is used when clients
     * are in the middle of processing an event that affects the widget
     * content. By using an async, we ensure that the widget content is up
     * to date with the event.
     */
    private void asyncRecomputeProposals(final String filterText) {
      if (isValid()) {
        control.getDisplay().asyncExec(new Runnable() {
          public void run() {
            recordCursorPosition();
            recomputeProposals(filterText);
          }
        });
      } else {
        recomputeProposals(filterText);
      }
    }

    /*
     * Filter the provided list of content proposals according to the filter
     * text.
     */
    private IContentProposal[] filterProposals(
        IContentProposal[] proposals, String filterString) {
      if (filterString.length() == 0) {
        return proposals;
      }

      // Check each string for a match. Use the string displayed to the
      // user, not the proposal content.
      ArrayList list = new ArrayList();
      for (int i = 0; i < proposals.length; i++) {
        String string = getString(proposals[i]);
        if (string.length() >= filterString.length()
            && string.substring(0, filterString.length())
                .equalsIgnoreCase(filterString)) {
          list.add(proposals[i]);
        }

      }
      return (IContentProposal[]) list.toArray(new IContentProposal[list
          .size()]);
    }

    Listener getTargetControlListener() {
      if (targetControlListener == null) {
        targetControlListener = new TargetControlListener();
      }
      return targetControlListener;
    }
  }

  /**
   * Flag that controls the printing of debug info.
   */
  public static final boolean DEBUG = false;

  /**
   * Indicates that a chosen proposal should be inserted into the field.
   */
  public static final int PROPOSAL_INSERT = 1;

  /**
   * Indicates that a chosen proposal should replace the entire contents of
   * the field.
   */
  public static final int PROPOSAL_REPLACE = 2;

  /**
   * Indicates that the contents of the control should not be modified when a
   * proposal is chosen. This is typically used when a client needs more
   * specialized behavior when a proposal is chosen. In this case, clients
   * typically register an IContentProposalListener so that they are notified
   * when a proposal is chosen.
   */
  public static final int PROPOSAL_IGNORE = 3;

  /**
   * Indicates that there should be no filter applied as keys are typed in the
   * popup.
   */
  public static final int FILTER_NONE = 1;

  /**
   * Indicates that a single character filter applies as keys are typed in the
   * popup.
   */
  public static final int FILTER_CHARACTER = 2;

  /**
   * Indicates that a cumulative filter applies as keys are typed in the
   * popup. That is, each character typed will be added to the filter.
   *
   * @deprecated As of 3.4, filtering that is sensitive to changes in the
   *             control content should be performed by the supplied
   *             {@link IContentProposalProvider}, such as that performed by
   *             {@link SimpleContentProposalProvider}
   */
  public static final int FILTER_CUMULATIVE = 3;

  /*
   * Set to <code>true</code> to use a Table with SWT.VIRTUAL. This is a
   * workaround for https://bugs.eclipse.org/bugs/show_bug.cgi?id=98585#c40
   * The corresponding SWT bug is
   * https://bugs.eclipse.org/bugs/show_bug.cgi?id=90321
   */
  private static final boolean USE_VIRTUAL = !Util.isMotif();

  /*
   * The delay before showing a secondary popup.
   */
  private static final int POPUP_DELAY = 750;

  /*
   * The character height hint for the popup. May be overridden by using
   * setInitialPopupSize.
   */
  private static final int POPUP_CHAR_HEIGHT = 10;

  /*
   * The minimum pixel width for the popup. May be overridden by using
   * setInitialPopupSize.
   */
  private static final int POPUP_MINIMUM_WIDTH = 300;

  /*
   * The pixel offset of the popup from the bottom corner of the control.
   */
  private static final int POPUP_OFFSET = 3;

  /*
   * Empty string.
   */
  private static final String EMPTY = ""; //$NON-NLS-1$

  /*
   * The object that provides content proposals.
   */
  private IContentProposalProvider proposalProvider;

  /*
   * A label provider used to display proposals in the popup, and to extract
   * Strings from non-String proposals.
   */
  private ILabelProvider labelProvider;

  /*
   * The control for which content proposals are provided.
   */
  private Control control;

  /*
   * The adapter used to extract the String contents from an arbitrary
   * control.
   */
  private IControlContentAdapter controlContentAdapter;

  /*
   * The popup used to show proposals.
   */
  private ContentProposalPopup popup;

  /*
   * The keystroke that signifies content proposals should be shown.
   */
  private KeyStroke triggerKeyStroke;

  /*
   * The String containing characters that auto-activate the popup.
   */
  private String autoActivateString;

  /*
   * Integer that indicates how an accepted proposal should affect the
   * control. One of PROPOSAL_IGNORE, PROPOSAL_INSERT, or PROPOSAL_REPLACE.
   * Default value is PROPOSAL_INSERT.
   */
  private int proposalAcceptanceStyle = PROPOSAL_INSERT;

  /*
   * A boolean that indicates whether key events received while the proposal
   * popup is open should also be propagated to the control. Default value is
   * true.
   */
  private boolean propagateKeys = true;

  /*
   * Integer that indicates the filtering style. One of FILTER_CHARACTER,
   * FILTER_CUMULATIVE, FILTER_NONE.
   */
  private int filterStyle = FILTER_NONE;

  /*
   * The listener we install on the control.
   */
  private Listener controlListener;

  /*
   * The list of IContentProposalListener listeners.
   */
  private ListenerList proposalListeners = new ListenerList();

  /*
   * The list of IContentProposalListener2 listeners.
   */
  private ListenerList proposalListeners2 = new ListenerList();

  /*
   * Flag that indicates whether the adapter is enabled. In some cases,
   * adapters may be installed but depend upon outside state.
   */
  private boolean isEnabled = true;

  /*
   * The delay in milliseconds used when autoactivating the popup.
   */
  private int autoActivationDelay = 0;

  /*
   * A boolean indicating whether a keystroke has been received. Used to see
   * if an autoactivation delay was interrupted by a keystroke.
   */
  private boolean receivedKeyDown;

  /*
   * The desired size in pixels of the proposal popup.
   */
  private Point popupSize;

  /*
   * The remembered position of the insertion position. Not all controls will
   * restore the insertion position if the proposal popup gets focus, so we
   * need to remember it.
   */
  private int insertionPos = -1;

  /*
   * The remembered selection range. Not all controls will restore the
   * selection position if the proposal popup gets focus, so we need to
   * remember it.
   */
  private Point selectionRange = new Point(-1, -1);

  /*
   * A flag that indicates that we are watching modify events
   */
  private boolean watchModify = false;

  /**
   * Construct a content proposal adapter that can assist the user with
   * choosing content for the field.
   *
   * @param control
   *            the control for which the adapter is providing content assist.
   *            May not be <code>null</code>.
   * @param controlContentAdapter
   *            the <code>IControlContentAdapter</code> used to obtain and
   *            update the control's contents as proposals are accepted. May
   *            not be <code>null</code>.
   * @param proposalProvider
   *            the <code>IContentProposalProvider</code> used to obtain
   *            content proposals for this control, or <code>null</code> if
   *            no content proposal is available.
   * @param keyStroke
   *            the keystroke that will invoke the content proposal popup. If
   *            this value is <code>null</code>, then proposals will be
   *            activated automatically when any of the auto activation
   *            characters are typed.
   * @param autoActivationCharacters
   *            An array of characters that trigger auto-activation of content
   *            proposal. If specified, these characters will trigger
   *            auto-activation of the proposal popup, regardless of whether
   *            an explicit invocation keyStroke was specified. If this
   *            parameter is <code>null</code>, then only a specified
   *            keyStroke will invoke content proposal. If this parameter is
   *            <code>null</code> and the keyStroke parameter is
   *            <code>null</code>, then all alphanumeric characters will
   *            auto-activate content proposal.
   */
  public ContentProposalAdapter(Control control,
      IControlContentAdapter controlContentAdapter,
      IContentProposalProvider proposalProvider, KeyStroke keyStroke,
      char[] autoActivationCharacters) {
    super();
    // We always assume the control and content adapter are valid.
    Assert.isNotNull(control);
    Assert.isNotNull(controlContentAdapter);
    this.control = control;
    this.controlContentAdapter = controlContentAdapter;

    // The rest of these may be null
    this.proposalProvider = proposalProvider;
    this.triggerKeyStroke = keyStroke;
    if (autoActivationCharacters != null) {
      this.autoActivateString = new String(autoActivationCharacters);
    }
    addControlListener(control);
  }

  /**
   * Get the control on which the content proposal adapter is installed.
   *
   * @return the control on which the proposal adapter is installed.
   */
  public Control getControl() {
    return control;
  }

  /**
   * Get the label provider that is used to show proposals.
   *
   * @return the {@link ILabelProvider} used to show proposals, or
   *         <code>null</code> if one has not been installed.
   */
  public ILabelProvider getLabelProvider() {
    return labelProvider;
  }

  /**
   * Return a boolean indicating whether the receiver is enabled.
   *
   * @return <code>true</code> if the adapter is enabled, and
   *         <code>false</code> if it is not.
   */
  public boolean isEnabled() {
    return isEnabled;
  }

  /**
   * Set the label provider that is used to show proposals. The lifecycle of
   * the specified label provider is not managed by this adapter. Clients must
   * dispose the label provider when it is no longer needed.
   *
   * @param labelProvider
   *            the (@link ILabelProvider} used to show proposals.
   */
  public void setLabelProvider(ILabelProvider labelProvider) {
    this.labelProvider = labelProvider;
  }

  /**
   * Return the proposal provider that provides content proposals given the
   * current content of the field. A value of <code>null</code> indicates
   * that there are no content proposals available for the field.
   *
   * @return the {@link IContentProposalProvider} used to show proposals. May
   *         be <code>null</code>.
   */
  public IContentProposalProvider getContentProposalProvider() {
    return proposalProvider;
  }

  /**
   * Set the content proposal provider that is used to show proposals.
   *
   * @param proposalProvider
   *            the {@link IContentProposalProvider} used to show proposals
   */
  public void setContentProposalProvider(
      IContentProposalProvider proposalProvider) {
    this.proposalProvider = proposalProvider;
  }

  /**
   * Return the array of characters on which the popup is autoactivated.
   *
   * @return An array of characters that trigger auto-activation of content
   *         proposal. If specified, these characters will trigger
   *         auto-activation of the proposal popup, regardless of whether an
   *         explicit invocation keyStroke was specified. If this parameter is
   *         <code>null</code>, then only a specified keyStroke will invoke
   *         content proposal. If this value is <code>null</code> and the
   *         keyStroke value is <code>null</code>, then all alphanumeric
   *         characters will auto-activate content proposal.
   */
  public char[] getAutoActivationCharacters() {
    if (autoActivateString == null) {
      return null;
    }
    return autoActivateString.toCharArray();
  }

  /**
   * Set the array of characters that will trigger autoactivation of the
   * popup.
   *
   * @param autoActivationCharacters
   *            An array of characters that trigger auto-activation of content
   *            proposal. If specified, these characters will trigger
   *            auto-activation of the proposal popup, regardless of whether
   *            an explicit invocation keyStroke was specified. If this
   *            parameter is <code>null</code>, then only a specified
   *            keyStroke will invoke content proposal. If this parameter is
   *            <code>null</code> and the keyStroke value is
   *            <code>null</code>, then all alphanumeric characters will
   *            auto-activate content proposal.
   *
   */
  public void setAutoActivationCharacters(char[] autoActivationCharacters) {
    if (autoActivationCharacters == null) {
      this.autoActivateString = null;
    } else {
      this.autoActivateString = new String(autoActivationCharacters);
    }
  }

  /**
   * Set the delay, in milliseconds, used before any autoactivation is
   * triggered.
   *
   * @return the time in milliseconds that will pass before a popup is
   *         automatically opened
   */
  public int getAutoActivationDelay() {
    return autoActivationDelay;

  }

  /**
   * Set the delay, in milliseconds, used before autoactivation is triggered.
   *
   * @param delay
   *            the time in milliseconds that will pass before a popup is
   *            automatically opened
   */
  public void setAutoActivationDelay(int delay) {
    autoActivationDelay = delay;

  }

  /**
   * Get the integer style that indicates how an accepted proposal affects the
   * control's content.
   *
   * @return a constant indicating how an accepted proposal should affect the
   *         control's content. Should be one of <code>PROPOSAL_INSERT</code>,
   *         <code>PROPOSAL_REPLACE</code>, or <code>PROPOSAL_IGNORE</code>.
   *         (Default is <code>PROPOSAL_INSERT</code>).
   */
  public int getProposalAcceptanceStyle() {
    return proposalAcceptanceStyle;
  }

  /**
   * Set the integer style that indicates how an accepted proposal affects the
   * control's content.
   *
   * @param acceptance
   *            a constant indicating how an accepted proposal should affect
   *            the control's content. Should be one of
   *            <code>PROPOSAL_INSERT</code>, <code>PROPOSAL_REPLACE</code>,
   *            or <code>PROPOSAL_IGNORE</code>
   */
  public void setProposalAcceptanceStyle(int acceptance) {
    proposalAcceptanceStyle = acceptance;
  }

  /**
   * Return the integer style that indicates how keystrokes affect the content
   * of the proposal popup while it is open.
   *
   * @return a constant indicating how keystrokes in the proposal popup affect
   *         filtering of the proposals shown. <code>FILTER_NONE</code>
   *         specifies that no filtering will occur in the content proposal
   *         list as keys are typed. <code>FILTER_CHARACTER</code> specifies
   *         the content of the popup will be filtered by the most recently
   *         typed character. <code>FILTER_CUMULATIVE</code> is deprecated
   *         and no longer recommended. It specifies that the content of the
   *         popup will be filtered by a string containing all the characters
   *         typed since the popup has been open. The default is
   *         <code>FILTER_NONE</code>.
   */
  public int getFilterStyle() {
    return filterStyle;
  }

  /**
   * Set the integer style that indicates how keystrokes affect the content of
   * the proposal popup while it is open. Popup-based filtering is useful for
   * narrowing and navigating the list of proposals provided once the popup is
   * open. Filtering of the proposals will occur even when the control content
   * is not affected by user typing. Note that automatic filtering is not used
   * to achieve content-sensitive filtering such as auto-completion. Filtering
   * that is sensitive to changes in the control content should be performed
   * by the supplied {@link IContentProposalProvider}.
   *
   * @param filterStyle
   *            a constant indicating how keystrokes received in the proposal
   *            popup affect filtering of the proposals shown.
   *            <code>FILTER_NONE</code> specifies that no automatic
   *            filtering of the content proposal list will occur as keys are
   *            typed in the popup. <code>FILTER_CHARACTER</code> specifies
   *            that the content of the popup will be filtered by the most
   *            recently typed character. <code>FILTER_CUMULATIVE</code> is
   *            deprecated and no longer recommended. It specifies that the
   *            content of the popup will be filtered by a string containing
   *            all the characters typed since the popup has been open.
   */
  public void setFilterStyle(int filterStyle) {
    this.filterStyle = filterStyle;
  }

  /**
   * Return the size, in pixels, of the content proposal popup.
   *
   * @return a Point specifying the last width and height, in pixels, of the
   *         content proposal popup.
   */
  public Point getPopupSize() {
    return popupSize;
  }

  /**
   * Set the size, in pixels, of the content proposal popup. This size will be
   * used the next time the content proposal popup is opened.
   *
   * @param size
   *            a Point specifying the desired width and height, in pixels, of
   *            the content proposal popup.
   */
  public void setPopupSize(Point size) {
    popupSize = size;
  }

  /**
   * Get the boolean that indicates whether key events (including
   * auto-activation characters) received by the content proposal popup should
   * also be propagated to the adapted control when the proposal popup is
   * open.
   *
   * @return a boolean that indicates whether key events (including
   *         auto-activation characters) should be propagated to the adapted
   *         control when the proposal popup is open. Default value is
   *         <code>true</code>.
   */
  public boolean getPropagateKeys() {
    return propagateKeys;
  }

  /**
   * Set the boolean that indicates whether key events (including
   * auto-activation characters) received by the content proposal popup should
   * also be propagated to the adapted control when the proposal popup is
   * open.
   *
   * @param propagateKeys
   *            a boolean that indicates whether key events (including
   *            auto-activation characters) should be propagated to the
   *            adapted control when the proposal popup is open.
   */
  public void setPropagateKeys(boolean propagateKeys) {
    this.propagateKeys = propagateKeys;
  }

  /**
   * Return the content adapter that can get or retrieve the text contents
   * from the adapter's control. This method is used when a client, such as a
   * content proposal listener, needs to update the control's contents
   * manually.
   *
   * @return the {@link IControlContentAdapter} which can update the control
   *         text.
   */
  public IControlContentAdapter getControlContentAdapter() {
    return controlContentAdapter;
  }

  /**
   * Set the boolean flag that determines whether the adapter is enabled.
   *
   * @param enabled
   *            <code>true</code> if the adapter is enabled and responding
   *            to user input, <code>false</code> if it is ignoring user
   *            input.
   *
   */
  public void setEnabled(boolean enabled) {
    // If we are disabling it while it's proposing content, close the
    // content proposal popup.
    if (isEnabled && !enabled) {
      if (popup != null) {
        popup.close();
      }
    }
    isEnabled = enabled;
  }

  /**
   * Add the specified listener to the list of content proposal listeners that
   * are notified when content proposals are chosen.
   * </p>
   *
   * @param listener
   *            the IContentProposalListener to be added as a listener. Must
   *            not be <code>null</code>. If an attempt is made to register
   *            an instance which is already registered with this instance,
   *            this method has no effect.
   *
   * @see org.eclipse.jface.fieldassist.IContentProposalListener
   */
  public void addContentProposalListener(IContentProposalListener listener) {
    proposalListeners.add(listener);
  }

  /**
   * Removes the specified listener from the list of content proposal
   * listeners that are notified when content proposals are chosen.
   * </p>
   *
   * @param listener
   *            the IContentProposalListener to be removed as a listener. Must
   *            not be <code>null</code>. If the listener has not already
   *            been registered, this method has no effect.
   *
   * @since 3.3
   * @see org.eclipse.jface.fieldassist.IContentProposalListener
   */
  public void removeContentProposalListener(IContentProposalListener listener) {
    proposalListeners.remove(listener);
  }

  /**
   * Add the specified listener to the list of content proposal listeners that
   * are notified when a content proposal popup is opened or closed.
   * </p>
   *
   * @param listener
   *            the IContentProposalListener2 to be added as a listener. Must
   *            not be <code>null</code>. If an attempt is made to register
   *            an instance which is already registered with this instance,
   *            this method has no effect.
   *
   * @since 3.3
   * @see org.eclipse.jface.fieldassist.IContentProposalListener2
   */
  public void addContentProposalListener(IContentProposalListener2 listener) {
    proposalListeners2.add(listener);
  }

  /**
   * Remove the specified listener from the list of content proposal listeners
   * that are notified when a content proposal popup is opened or closed.
   * </p>
   *
   * @param listener
   *            the IContentProposalListener2 to be removed as a listener.
   *            Must not be <code>null</code>. If the listener has not
   *            already been registered, this method has no effect.
   *
   * @since 3.3
   * @see org.eclipse.jface.fieldassist.IContentProposalListener2
   */
  public void removeContentProposalListener(IContentProposalListener2 listener) {
    proposalListeners2.remove(listener);
  }

  /*
   * Add our listener to the control. Debug information to be left in until
   * this support is stable on all platforms.
   */
  private void addControlListener(Control control) {
    if (DEBUG) {
      System.out
          .println("ContentProposalListener#installControlListener()"); //$NON-NLS-1$
    }

    if (controlListener != null) {
      return;
    }
    controlListener = new Listener() {
      public void handleEvent(Event e) {
        if (!isEnabled) {
          return;
        }

        switch (e.type) {
        case SWT.Traverse:
        case SWT.KeyDown:
          if (DEBUG) {
            StringBuffer sb;
            if (e.type == SWT.Traverse) {
              sb = new StringBuffer("Traverse"); //$NON-NLS-1$
            } else {
              sb = new StringBuffer("KeyDown"); //$NON-NLS-1$
            }
            sb.append(" received by adapter"); //$NON-NLS-1$
            dump(sb.toString(), e);
          }
          // If the popup is open, it gets first shot at the
          // keystroke and should set the doit flags appropriately.
          if (popup != null) {
            popup.getTargetControlListener().handleEvent(e);
            if (DEBUG) {
              StringBuffer sb;
              if (e.type == SWT.Traverse) {
                sb = new StringBuffer("Traverse"); //$NON-NLS-1$
              } else {
                sb = new StringBuffer("KeyDown"); //$NON-NLS-1$
              }
              sb.append(" after being handled by popup"); //$NON-NLS-1$
              dump(sb.toString(), e);
            }
            // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=192633
            // If the popup is open and this is a valid character, we
            // want to watch for the modified text.
            if (propagateKeys && e.character != 0)
              watchModify = true;

            return;
          }

          // We were only listening to traverse events for the popup
          if (e.type == SWT.Traverse) {
            return;
          }

          // The popup is not open. We are looking at keydown events
          // for a trigger to open the popup.
          if (triggerKeyStroke != null) {
            // Either there are no modifiers for the trigger and we
            // check the character field...
            if ((triggerKeyStroke.getModifierKeys() == KeyStroke.NO_KEY && triggerKeyStroke
                .getNaturalKey() == e.character)
                ||
                // ...or there are modifiers, in which case the
                // keycode and state must match
                (triggerKeyStroke.getNaturalKey() == e.keyCode && ((triggerKeyStroke
                    .getModifierKeys() & e.stateMask) == triggerKeyStroke
                    .getModifierKeys()))) {
              // We never propagate the keystroke for an explicit
              // keystroke invocation of the popup
              e.doit = false;
              openProposalPopup(false);
              return;
            }
          }
          /*
           * The triggering keystroke was not invoked. If a character
           * was typed, compare it to the autoactivation characters.
           */
          if (e.character != 0) {
            if (autoActivateString != null) {
              if (autoActivateString.indexOf(e.character) >= 0) {
                autoActivate();
              } else {
                // No autoactivation occurred, so record the key
                // down as a means to interrupt any
                // autoactivation that is pending due to
                // autoactivation delay.
                receivedKeyDown = true;
                // watch the modify so we can close the popup in
                // cases where there is no longer a trigger
                // character in the content
                watchModify = true;
              }
            } else {
              // The autoactivate string is null. If the trigger
              // is also null, we want to act on any modification
              // to the content. Set a flag so we'll catch this
              // in the modify event.
              if (triggerKeyStroke == null) {
                watchModify = true;
              }
            }
          } else {
            // A non-character key has been pressed. Interrupt any
            // autoactivation that is pending due to autoactivation delay.
            receivedKeyDown = true;
          }
          break;


          // There are times when we want to monitor content changes
          // rather than individual keystrokes to determine whether
          // the popup should be closed or opened based on the entire
          // content of the control.
          // The watchModify flag ensures that we don't autoactivate if
          // the content change was caused by something other than typing.
          // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=183650
          case SWT.Modify:
            if (allowsAutoActivate() && watchModify) {
              if (DEBUG) {
                dump("Modify event triggers popup open or close", e); //$NON-NLS-1$
              }
              watchModify = false;
              // We are in autoactivation mode, either for specific
              // characters or for all characters. In either case,
              // we should close the proposal popup when there is no
              // content in the control.
              if (isControlContentEmpty()) {
                // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=192633
                closeProposalPopup();
              } else {
                // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=147377
                // Given that we will close the popup when there are
                // no valid proposals, we must consider reopening it on any
                // content change when there are no particular autoActivation
                // characters
                if (autoActivateString == null) {
                  autoActivate();
                } else {
                  // Autoactivation characters are defined, but this
                  // modify event does not involve one of them.  See
                  // if any of the autoactivation characters are left
                  // in the content and close the popup if none remain.
                  if (!shouldPopupRemainOpen())
                    closeProposalPopup();
                }
              }
            }
            break;
        default:
          break;
        }
      }

      /**
       * Dump the given events to "standard" output.
       *
       * @param who
       *            who is dumping the event
       * @param e
       *            the event
       */
      private void dump(String who, Event e) {
        StringBuffer sb = new StringBuffer(
            "--- [ContentProposalAdapter]\n"); //$NON-NLS-1$
        sb.append(who);
        sb.append(" - e: keyCode=" + e.keyCode + hex(e.keyCode)); //$NON-NLS-1$
        sb.append("; character=" + e.character + hex(e.character)); //$NON-NLS-1$
        sb.append("; stateMask=" + e.stateMask + hex(e.stateMask)); //$NON-NLS-1$
        sb.append("; doit=" + e.doit); //$NON-NLS-1$
        sb.append("; detail=" + e.detail + hex(e.detail)); //$NON-NLS-1$
        sb.append("; widget=" + e.widget); //$NON-NLS-1$
        System.out.println(sb);
      }

      private String hex(int i) {
        return "[0x" + Integer.toHexString(i) + ']'; //$NON-NLS-1$
      }
    };
    control.addListener(SWT.KeyDown, controlListener);
    control.addListener(SWT.Traverse, controlListener);
    control.addListener(SWT.Modify, controlListener);

    if (DEBUG) {
      System.out
          .println("ContentProposalAdapter#installControlListener() - installed"); //$NON-NLS-1$
    }
  }

  /**
   * Open the proposal popup and display the proposals provided by the
   * proposal provider. If there are no proposals to be shown, do not show the
   * popup. This method returns immediately. That is, it does not wait for the
   * popup to open or a proposal to be selected.
   *
   * @param autoActivated
   *            a boolean indicating whether the popup was autoactivated. If
   *            false, a beep will sound when no proposals can be shown.
   */
  private void openProposalPopup(boolean autoActivated) {
    if (isValid()) {
      if (popup == null) {
        // Check whether there are any proposals to be shown.
        recordCursorPosition(); // must be done before getting proposals
        IContentProposal[] proposals = getProposals();
        if (proposals.length > 0) {
          if (DEBUG) {
            System.out.println("POPUP OPENED BY PRECEDING EVENT"); //$NON-NLS-1$
          }
          recordCursorPosition();
          popup = new ContentProposalPopup(null, proposals);
          popup.open();
          popup.getShell().addDisposeListener(new DisposeListener() {
            public void widgetDisposed(DisposeEvent event) {
              popup = null;
            }
          });
          internalPopupOpened();
          notifyPopupOpened();
        } else if (!autoActivated) {
          getControl().getDisplay().beep();
        }
      }
    }
  }

  /**
   * Open the proposal popup and display the proposals provided by the
   * proposal provider. This method returns immediately. That is, it does not
   * wait for a proposal to be selected. This method is used by subclasses to
   * explicitly invoke the opening of the popup. If there are no proposals to
   * show, the popup will not open and a beep will be sounded.
   */
  protected void openProposalPopup() {
    openProposalPopup(false);
  }

  /**
   * Close the proposal popup without accepting a proposal. This method
   * returns immediately, and has no effect if the proposal popup was not
   * open. This method is used by subclasses to explicitly close the popup
   * based on additional logic.
   *
   * @since 3.3
   */
  protected void closeProposalPopup() {
    if (popup != null) {
      popup.close();
    }
  }

  /*
   * A content proposal has been accepted. Update the control contents
   * accordingly and notify any listeners.
   *
   * @param proposal the accepted proposal
   */
  private void proposalAccepted(IContentProposal proposal) {
    switch (proposalAcceptanceStyle) {
    case (PROPOSAL_REPLACE):
      setControlContent(proposal.getContent(), proposal
          .getCursorPosition());
      break;
    case (PROPOSAL_INSERT):
      insertControlContent(proposal.getContent(), proposal
          .getCursorPosition());
      break;
    default:
      // do nothing. Typically a listener is installed to handle this in
      // a custom way.
      break;
    }

    // In all cases, notify listeners of an accepted proposal.
    notifyProposalAccepted(proposal);
  }

  /*
   * Set the text content of the control to the specified text, setting the
   * cursorPosition at the desired location within the new contents.
   */
  private void setControlContent(String text, int cursorPosition) {
    if (isValid()) {
      // should already be false, but just in case.
      watchModify = false;
      controlContentAdapter.setControlContents(control, text,
          cursorPosition);
    }
  }

  /*
   * Insert the specified text into the control content, setting the
   * cursorPosition at the desired location within the new contents.
   */
  private void insertControlContent(String text, int cursorPosition) {
    if (isValid()) {
      // should already be false, but just in case.
      watchModify = false;
      // Not all controls preserve their selection index when they lose
      // focus, so we must set it explicitly here to what it was before
      // the popup opened.
      // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=127108
      // See https://bugs.eclipse.org/bugs/show_bug.cgi?id=139063
      if (controlContentAdapter instanceof IControlContentAdapter2
          && selectionRange.x != -1) {
        ((IControlContentAdapter2) controlContentAdapter).setSelection(
            control, selectionRange);
      } else if (insertionPos != -1) {
        controlContentAdapter.setCursorPosition(control, insertionPos);
      }
      controlContentAdapter.insertControlContents(control, text,
          cursorPosition);
    }
  }

  /*
   * Check that the control and content adapter are valid.
   */
  private boolean isValid() {
    return control != null && !control.isDisposed()
        && controlContentAdapter != null;
  }

  /*
   * Record the control's cursor position.
   */
  private void recordCursorPosition() {
    if (isValid()) {
      IControlContentAdapter adapter = getControlContentAdapter();
      insertionPos = adapter.getCursorPosition(control);
      // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=139063
      if (adapter instanceof IControlContentAdapter2) {
        selectionRange = ((IControlContentAdapter2) adapter)
            .getSelection(control);
      }

    }
  }

  /*
   * Get the proposals from the proposal provider. Gets all of the proposals
   * without doing any filtering.
   */
  private IContentProposal[] getProposals() {
    if (proposalProvider == null || !isValid()) {
      return null;
    }
    if (DEBUG) {
      System.out.println(">>> obtaining proposals from provider"); //$NON-NLS-1$
    }
    int position = insertionPos;
    if (position == -1) {
      position = getControlContentAdapter().getCursorPosition(
          getControl());
    }
    String contents = getControlContentAdapter().getControlContents(
        getControl());
    IContentProposal[] proposals = proposalProvider.getProposals(contents,
        position);
    return proposals;
  }

  /**
   * Autoactivation has been triggered. Open the popup using any specified
   * delay.
   */
  private void autoActivate() {
    if (autoActivationDelay > 0) {
      Runnable runnable = new Runnable() {
        public void run() {
          receivedKeyDown = false;
          try {
            Thread.sleep(autoActivationDelay);
          } catch (InterruptedException e) {
          }
          if (!isValid() || receivedKeyDown) {
            return;
          }
          getControl().getDisplay().syncExec(new Runnable() {
            public void run() {
              openProposalPopup(true);
            }
          });
        }
      };
      Thread t = new Thread(runnable);
      t.start();
    } else {
      // Since we do not sleep, we must open the popup
      // in an async exec. This is necessary because
      // this method may be called in the middle of handling
      // some event that will cause the cursor position or
      // other important info to change as a result of this
      // event occurring.
      getControl().getDisplay().asyncExec(new Runnable() {
        public void run() {
          if (isValid()) {
            openProposalPopup(true);
          }
        }
      });
    }
  }

  /*
   * A proposal has been accepted. Notify interested listeners.
   */
  private void notifyProposalAccepted(IContentProposal proposal) {
    if (DEBUG) {
      System.out.println("Notify listeners - proposal accepted."); //$NON-NLS-1$
    }
    final Object[] listenerArray = proposalListeners.getListeners();
    for (int i = 0; i < listenerArray.length; i++) {
      ((IContentProposalListener) listenerArray[i])
          .proposalAccepted(proposal);
    }
  }

  /*
   * The proposal popup has opened. Notify interested listeners.
   */
  private void notifyPopupOpened() {
    if (DEBUG) {
      System.out.println("Notify listeners - popup opened."); //$NON-NLS-1$
    }
    final Object[] listenerArray = proposalListeners2.getListeners();
    for (int i = 0; i < listenerArray.length; i++) {
      ((IContentProposalListener2) listenerArray[i])
          .proposalPopupOpened(this);
    }
  }

  /*
   * The proposal popup has closed. Notify interested listeners.
   */
  private void notifyPopupClosed() {
    if (DEBUG) {
      System.out.println("Notify listeners - popup closed."); //$NON-NLS-1$
    }
    final Object[] listenerArray = proposalListeners2.getListeners();
    for (int i = 0; i < listenerArray.length; i++) {
      ((IContentProposalListener2) listenerArray[i])
          .proposalPopupClosed(this);
    }
  }

  /**
   * Returns whether the content proposal popup has the focus. This includes
   * both the primary popup and any secondary info popup that may have focus.
   *
   * @return <code>true</code> if the proposal popup or its secondary info
   *         popup has the focus
   * @since 3.4
   */
  public boolean hasProposalPopupFocus() {
    return popup != null && popup.hasFocus();
  }

  /*
   * Return whether the control content is empty
   */
  private boolean isControlContentEmpty() {
    return getControlContentAdapter().getControlContents(getControl())
        .length() == 0;
  }
 
  /*
   * The popup has just opened, but listeners have not yet
   * been notified.  Perform any cleanup that is needed.
   */
  private void internalPopupOpened() {
    // see https://bugs.eclipse.org/bugs/show_bug.cgi?id=243612
    if (control instanceof Combo) {
      ((Combo)control).setListVisible(false);
    }
  }
 
  /*
   * Return whether a proposal popup should remain open.
   * If it was autoactivated by specific characters, and
   * none of those characters remain, then it should not remain
   * open.  This method should not be used to determine
   * whether autoactivation has occurred or should occur, only whether
   * the circumstances would dictate that a popup remain open.
   */
  private boolean shouldPopupRemainOpen() {
    // If we always autoactivate or never autoactivate, it should remain open
    if (autoActivateString == null || autoActivateString.length() == 0)
      return true;
    String content = getControlContentAdapter().getControlContents(getControl());
    for (int i=0; i<autoActivateString.length(); i++) {
      if (content.indexOf(autoActivateString.charAt(i)) >= 0)
        return true;
    }
    return false;
  }
 
  /*
   * Return whether this adapter is configured for autoactivation, by
   * specific characters or by any characters.
   */
  private boolean allowsAutoActivate() {
    return (autoActivateString != null && autoActivateString.length() > 0) // there are specific autoactivation chars supplied
      || (autoActivateString == null && triggerKeyStroke == null);    // we autoactivate on everything
  }
 
  /**
   * Sets focus to the proposal popup. If the proposal popup is not opened,
   * this method is ignored. If the secondary popup has focus, focus is
   * returned to the main proposal popup.
   *
   * @since 3.6
   */
  public void setProposalPopupFocus() {
    if (isValid() && popup != null)
      popup.getShell().setFocus();
  }
 
  /**
   * Answers a boolean indicating whether the main proposal popup is open.
   *
   * @return <code>true</code> if the proposal popup is open, and
   *         <code>false</code> if it is not.
   *
   * @since 3.6
   */
  public boolean isProposalPopupOpen() {
    if (isValid() && popup != null)
      return true;
    return false;
  }

}
TOP

Related Classes of org.eclipse.jface.fieldassist.ContentProposalAdapter$ContentProposalPopup$PopupCloserListener

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.