org.csstudio.autocomplete.ui.content.ContentProposalPopup.java Source code

Java tutorial

Introduction

Here is the source code for org.csstudio.autocomplete.ui.content.ContentProposalPopup.java

Source

/*******************************************************************************
 * 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]
 *     ITER - Adapted to fit CSS auto-completion requirements
 *******************************************************************************/
package org.csstudio.autocomplete.ui.content;

import java.util.ArrayList;
import java.util.List;

import org.csstudio.autocomplete.proposals.Proposal;
import org.csstudio.autocomplete.proposals.ProposalStyle;
import org.csstudio.autocomplete.tooltips.TooltipData;
import org.csstudio.autocomplete.ui.AutoCompleteUIPlugin;
import org.csstudio.autocomplete.ui.util.SSTextLayout;
import org.eclipse.core.runtime.Assert;
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.swt.SWT;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
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.Font;
import org.eclipse.swt.graphics.FontData;
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.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
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.TableColumn;
import org.eclipse.swt.widgets.TableItem;
import org.eclipse.swt.widgets.Text;

/**
 * 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.
 *
 * @author Fred Arnaud (Sopra Group) - ITER
 */
public class ContentProposalPopup extends PopupDialog {

    /*
     * 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();

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

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

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

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

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

            if (e.type == SWT.Resize) {
                // Do not close popup on resize for web version.
                // RAP raise too many resize event
                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 = adapter.getPropagateKeys();
                String delete = new String(new char[] { 8 });
                String currentKey = new String(new char[] { e.character });
                if (!delete.equals(currentKey))
                    handleTopProposals = true;
            }

            // 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;
                    while (nonSelectableItems.contains(newSelection))
                        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;
                    while (nonSelectableItems.contains(newSelection))
                        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;
                    while (nonSelectableItems.contains(newSelection))
                        if (--newSelection < 0)
                            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;
                    while (nonSelectableItems.contains(newSelection))
                        if (++newSelection > proposalTable.getItemCount() - 1)
                            newSelection = 0;
                    if (e.type == SWT.KeyDown) {
                        // don't propagate to control
                        e.doit = false;
                    }
                    break;

                // Need to be propagated to control.
                case SWT.HOME:
                case SWT.ARROW_LEFT:
                    if (e.type == SWT.Traverse) {
                        e.doit = false;
                    } else {
                        e.doit = true;
                    }
                    return; // do nothing more

                case SWT.END:
                    if (e.type == SWT.Traverse) {
                        e.doit = false;
                    } else {
                        e.doit = true;
                        // If the contents was completed (i.e. the end of the
                        // content is behind the selection) the ARROW_RIGHT
                        // shoud trigger the recompute of proposals otherwise we
                        // do nothing.
                        if (adapter.hasSelectedTopProposal()) {
                            handleTopProposals = true;
                            asyncRecomputeProposals();
                        }
                    }
                    return;
                case SWT.ARROW_RIGHT:
                    if (e.type == SWT.Traverse) {
                        e.doit = false;
                        // If the contents was completed (i.e. the end of the
                        // content is behind the selection) the ARROW_RIGHT
                        // shoud trigger the recompute of proposals otherwise we
                        // do nothing.
                        int pos = adapter.getControlContentAdapter().getCursorPosition(control);
                        String contents = adapter.getControlContentAdapter().getControlContents(control);
                        if (adapter.hasSelectedTopProposal() || pos == contents.length()) {
                            handleTopProposals = true;
                            asyncRecomputeProposals();
                        }
                    } else {
                        e.doit = true;
                    }
                    return;

                // 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);
                    Proposal proposal = getSelectedProposal();
                    if (proposal != null)
                        adapter.proposalSelected(proposal);
                }
                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 = true;
                Object p = getSelectedProposal();
                if (p != null) {
                    acceptCurrentProposal(false);
                } else {
                    close();
                }
                break;

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

            case SWT.BS:
                // Recompute the proposals if the cursor position
                // will change (is not at 0).
                int pos = adapter.getControlContentAdapter().getCursorPosition(control);
                // 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();
                }
                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)) {
                    // Recompute proposals after processing this event.
                    asyncRecomputeProposals();
                }
                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;
        private static final int CONTENT_MAX_LENGTH = 50;

        /*
         * 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.READ_ONLY | SWT.NO_FOCUS);

            // Use the compact margins employed by PopupDialog.
            GridData gd = new GridData(GridData.BEGINNING | GridData.FILL_BOTH);
            gd.horizontalIndent = 5;
            gd.verticalAlignment = SWT.CENTER;
            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();
            Point textSize = text.getSize();
            Rectangle itemBounds = proposalTable.getItem(proposalTable.getSelectionIndex()).getBounds();
            int controlY = parentBounds.y + proposalTable.getBounds().y + itemBounds.y + POPUP_VERTICALSPACING + 1;
            int controlWidht = textSize.x + 20;
            int controlHeight = itemBounds.height;
            Rectangle proposedBounds = null;

            // Try placing the info popup to the right
            Rectangle rightProposedBounds = new Rectangle(
                    parentBounds.x + parentBounds.width + PopupDialog.POPUP_HORIZONTALSPACING, controlY,
                    controlWidht, controlHeight);
            rightProposedBounds = getConstrainedShellBounds(rightProposedBounds);

            // If it won't fit on the right, try the left
            if (rightProposedBounds.intersects(parentBounds)) {
                Rectangle leftProposedBounds = new Rectangle(
                        parentBounds.x - controlWidht - POPUP_HORIZONTALSPACING - 1, controlY, controlWidht,
                        controlHeight);
                leftProposedBounds = getConstrainedShellBounds(leftProposedBounds);

                // If it won't fit on the left, display on top of the item
                if (leftProposedBounds.intersects(parentBounds)) {
                    rightProposedBounds.x = parentBounds.x + itemBounds.x;
                    rightProposedBounds.y = controlY - itemBounds.height + 1;
                    proposedBounds = rightProposedBounds;
                } 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;
            }
            if (newContents.length() > CONTENT_MAX_LENGTH)
                newContents = newContents.substring(0, CONTENT_MAX_LENGTH) + "...";
            int crIndex = newContents.indexOf("\n");
            if (crIndex > 0)
                newContents = newContents.substring(0, crIndex);
            this.contents = newContents;
            if (text != null && !text.isDisposed()) {
                text.setText(contents);
                text.pack();
                this.adjustBounds();
            }
        }

        /*
         * 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 text used to display info under the table
     */
    private Text footer;

    /*
     * The proposals to be shown (cached to avoid repeated requests).
     */
    private ContentProposalList proposalList;

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

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

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

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

    /*
     * A flag indicating whether or not we should handle top proposals.
     */
    private boolean handleTopProposals = false;

    private ContentProposalAdapter adapter;

    private Image partialContentImage;
    private Image partialContentImageSelected;
    private Image functionContentImage;
    private Image functionContentImageSelected;
    private SSTextLayout[] textLayouts;

    private final int SWTMeasureItem = 41;
    private final int SWTPaintItem = 42;
    private int maxItemWidth = 0;

    private List<Integer> nonSelectableItems;
    private Long uniqueId = Long.MIN_VALUE;

    /**
     * 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(ContentProposalAdapter adapter, String infoText, ContentProposalList proposalList) {
        // 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(adapter.getControl().getShell(), SWT.RESIZE | SWT.ON_TOP | SWT.NO_FOCUS, false, false, false, false,
                false, null, infoText);
        this.adapter = adapter;
        this.control = adapter.getControl();

        // this.labelProvider = adapter.getLabelProvider();
        this.partialContentImage = AutoCompleteUIPlugin.getDefault()
                .getImageFromPlugin(AutoCompleteUIPlugin.PLUGIN_ID, "icons/mglass-16.png");
        this.partialContentImageSelected = AutoCompleteUIPlugin.getDefault()
                .getImageFromPlugin(AutoCompleteUIPlugin.PLUGIN_ID, "icons/mglass-16-white.png");
        this.functionContentImage = AutoCompleteUIPlugin.getDefault()
                .getImageFromPlugin(AutoCompleteUIPlugin.PLUGIN_ID, "icons/function-16.png");
        this.functionContentImageSelected = AutoCompleteUIPlugin.getDefault()
                .getImageFromPlugin(AutoCompleteUIPlugin.PLUGIN_ID, "icons/function-16-white.png");
        this.nonSelectableItems = new ArrayList<Integer>();

        this.proposalList = proposalList;
        // When the popup is opened & the content is not already completed, we
        // want to handle this behaviour
        if (!adapter.isPreventTopProposalSelection())
            handleTopProposals = true;
    }

    /*
     * (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) {
        Composite wrapper = (Composite) super.createDialogArea(parent);
        wrapper.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
        wrapper.setLayout(new GridLayout());

        // Use virtual where appropriate (see flag definition).
        if (USE_VIRTUAL) {
            proposalTable = new Table(wrapper, SWT.H_SCROLL | SWT.V_SCROLL | SWT.VIRTUAL | SWT.NO_FOCUS);
            Listener listener = new Listener() {
                public void handleEvent(Event event) {
                    handleSetData(event);
                }
            };
            proposalTable.addListener(SWT.SetData, listener);

            /*
             * NOTE: MeasureItem, PaintItem and EraseItem are called repeatedly.
             * Therefore, it is critical for performance that these methods be
             * as efficient as possible. NOT implemented for RAP.
             */
            if (!AutoCompleteUIPlugin.isRAP()) {
                proposalTable.addListener(SWTPaintItem, new Listener() {
                    public void handleEvent(Event event) {
                        TableItem item = (TableItem) event.item;
                        int index = proposalTable.indexOf(item);
                        if (textLayouts != null && index < textLayouts.length && textLayouts[index] != null) {
                            textLayouts[index].handlePaintItemEvent(event, 20, 2);
                        }
                        Proposal p = (Proposal) item.getData();
                        Image image = getImage(p, index == proposalTable.getSelectionIndex());
                        if (image != null)
                            event.gc.drawImage(image, event.x, event.y + 2);
                    }
                });
                proposalTable.addListener(SWTMeasureItem, new Listener() {
                    public void handleEvent(Event event) {
                        TableItem item = (TableItem) event.item;
                        int index = proposalTable.indexOf(item);
                        if (textLayouts != null && index < textLayouts.length && textLayouts[index] != null) {
                            textLayouts[index].handleMeasureItemEvent(event);
                        }
                    }
                });
            }
        } else {
            proposalTable = new Table(parent, SWT.H_SCROLL | SWT.V_SCROLL | SWT.NO_FOCUS);
        }

        footer = new Text(wrapper, SWT.READ_ONLY | SWT.WRAP | SWT.NO_FOCUS);
        GridData textGridData = new GridData(GridData.HORIZONTAL_ALIGN_END);
        textGridData.heightHint = FOOTER_MINIMUM_HEIGHT;
        textGridData.widthHint = 100;
        footer.setLayoutData(textGridData);

        // set the proposals to force population of the table.
        setProposals(proposalList);

        proposalTable.setHeaderVisible(false);
        proposalTable.addListener(SWT.KeyDown, new Listener() {
            @Override
            public void handleEvent(Event e) {
                getTargetControlListener().handleEvent(e);
            }
        });
        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 {
                    Proposal proposal = (Proposal) e.item.getData();
                    if (proposal != null) {
                        showProposalDescription();
                        adapter.proposalSelected(proposal);
                    } else {
                        if (infoPopup != null)
                            infoPopup.close();
                        proposalTable.deselectAll();
                    }
                }
            }

            // Default selection was made. Accept the current proposal.
            public void widgetDefaultSelected(SelectionEvent e) {
                Proposal proposal = (Proposal) e.item.getData();
                if (proposal != null) {
                    acceptCurrentProposal(true);
                } else {
                    proposalTable.deselectAll();
                }
            }
        });

        // Added to solve a item resize bug on windows:
        new TableColumn(proposalTable, SWT.NONE | SWT.NO_FOCUS);
        proposalTable.addControlListener(new ControlAdapter() {
            public void controlResized(ControlEvent event) {
                if (proposalTable.getColumnCount() > 0) {
                    if (proposalTable.getClientArea().width > maxItemWidth) {
                        proposalTable.getColumn(0).setWidth(proposalTable.getClientArea().width);
                    } else {
                        proposalTable.getColumn(0).setWidth(maxItemWidth);
                    }
                }
            }
        });

        return proposalTable;
    }

    /*
     * (non-Javadoc)
     *
     * @see org.eclipse.jface.dialogs.PopupDialog.adjustBounds()
     */
    protected void adjustBounds() {
        adjustTableBounds();
        // Now set up a listener to monitor any changes in size.
        if (AutoCompleteUIPlugin.isRAP())
            return;
        getShell().addListener(SWT.Resize, new Listener() {
            public void handleEvent(Event e) {
                popupSize = getShell().getSize();
                if (infoPopup != null) {
                    infoPopup.close();
                    Proposal p = getSelectedProposal();
                    if (p != null) {
                        showProposalDescription();
                    }
                }
            }
        });
    }

    private void adjustTableBounds() {
        // 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;

        GridData data = new GridData(GridData.FILL_BOTH);
        data.heightHint = proposalTable.getItemHeight() * getTableLength() + 30;
        data.widthHint = Math.max(maxItemWidth, Math.max(control.getSize().x, POPUP_MINIMUM_WIDTH));
        proposalTable.setLayoutData(data);

        getShell().pack();
        popupSize = getShell().getSize();
        int scrollBarWitdh = proposalTable.getVerticalBar().getSize().x;
        Rectangle shellBounds = new Rectangle(initialX, initialY, popupSize.x + scrollBarWitdh + 30, popupSize.y);

        // Constrain to the display
        Rectangle constrainedBounds = getConstrainedShellBounds(shellBounds);
        shellBounds.x = constrainedBounds.x;
        // If there has been an adjustment causing the popup to overlap
        // with the control, then reduce the popup size.
        if (constrainedBounds.y < initialY) {
            shellBounds.height -= initialY - constrainedBounds.y;
        }

        getShell().setBounds(shellBounds);
    }

    private void initializeTextLayouts() {
        if (AutoCompleteUIPlugin.isRAP())
            return;
        Display display = Display.getCurrent();

        FontData defaultFontData = display.getSystemFont().getFontData()[0];
        String fontName = defaultFontData.getName();
        int fontHeight = defaultFontData.getHeight();

        Font headerFont = new Font(display, new FontData(fontName, fontHeight, SWT.ITALIC | SWT.BOLD));
        Font noFont = new Font(display, new FontData(fontName, fontHeight, SWT.NORMAL));
        Color black = display.getSystemColor(SWT.COLOR_BLACK);

        int index = 0;
        textLayouts = new SSTextLayout[getTableLength()];
        for (Proposal proposal : proposalList.getTopProposalList()) {
            textLayouts[index] = AutoCompleteUIPlugin.getUIHelper().newTextLayout();

            String text = getString(proposal);
            textLayouts[index].init(display, text);
            textLayouts[index].addStyle(noFont, black, 0, text.length());
            if (proposal.getStyles() != null && !proposal.getStyles().isEmpty()) {
                for (ProposalStyle style : proposal.getStyles()) {
                    FontData newFontData = new FontData(fontName, fontHeight, style.fontStyle);
                    Font font = new Font(display, newFontData);
                    Color color = display.getSystemColor(style.fontColor);
                    textLayouts[index].addStyle(font, color, style.from, style.to);
                }
            }
            index++;
        }
        for (String provider : proposalList.getProviderList()) {
            textLayouts[index] = AutoCompleteUIPlugin.getUIHelper().newTextLayout();

            int count = proposalList.getCount(provider);
            String headerText = provider + " (" + count + " matching items)";
            textLayouts[index].init(display, headerText);
            textLayouts[index].addStyle(headerFont, black, 0, headerText.length());
            index++;

            for (Proposal proposal : proposalList.getProposals(provider)) {
                textLayouts[index] = AutoCompleteUIPlugin.getUIHelper().newTextLayout();

                String text = getString(proposal);
                textLayouts[index].init(display, text);
                textLayouts[index].addStyle(noFont, black, 0, text.length());
                if (proposal.getStyles() != null && !proposal.getStyles().isEmpty()) {
                    for (ProposalStyle style : proposal.getStyles()) {
                        FontData newFontData = new FontData(fontName, fontHeight, style.fontStyle);
                        Font font = new Font(display, newFontData);
                        Color color = display.getSystemColor(style.fontColor);
                        textLayouts[index].addStyle(font, color, style.from, style.to);
                    }
                }
                index++;
            }
        }
        for (SSTextLayout sstl : textLayouts)
            if (sstl != null && sstl.getBounds() != null && sstl.getBounds().width > maxItemWidth)
                maxItemWidth = sstl.getBounds().width;
        adjustTableBounds();
    }

    /*
     * 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);
        Display display = Display.getCurrent();

        int proposalIndex = 0;
        for (Proposal proposal : proposalList.getTopProposalList()) {
            if (index == proposalIndex) {
                if (AutoCompleteUIPlugin.isRAP()) {
                    item.setText("  " + getString(proposal));
                    item.setImage(getImage(proposal, false));
                    if (maxItemWidth < item.getBounds().width)
                        maxItemWidth = item.getBounds().width;
                }
                item.setData(proposal);
                return;
            }
            proposalIndex++;
        }
        for (String provider : proposalList.getProviderList()) {
            if (index == proposalIndex) {
                // Data == null => not selectable
                item.setData(null);
                item.setBackground(display.getSystemColor(SWT.COLOR_GRAY));
                if (AutoCompleteUIPlugin.isRAP()) {
                    int count = proposalList.getCount(provider);
                    String text = provider + " (" + count + " matching items)";
                    item.setText(text);

                    FontData fontData = display.getSystemFont().getFontData()[0];
                    FontData newFontData = new FontData(fontData.getName(), fontData.getHeight(),
                            SWT.ITALIC | SWT.BOLD);
                    Font font = new Font(display, newFontData);
                    item.setFont(font);

                    if (maxItemWidth < item.getBounds().width)
                        maxItemWidth = item.getBounds().width;
                }
                return;
            }
            proposalIndex++;
            for (Proposal proposal : proposalList.getProposals(provider)) {
                if (index == proposalIndex) {
                    if (AutoCompleteUIPlugin.isRAP()) {
                        item.setText("  " + getString(proposal));
                        item.setImage(getImage(proposal, false));
                        if (maxItemWidth < item.getBounds().width)
                            maxItemWidth = item.getBounds().width;
                    }
                    item.setData(proposal);
                    return;
                }
                proposalIndex++;
            }
        }
    }

    /*
     * Caches the specified proposals and repopulates the table if it has been
     * created.
     */
    private void setProposals(ContentProposalList newProposalList) {
        if (newProposalList == null) {
            newProposalList = getEmptyProposalArray();
        }
        this.proposalList = newProposalList;
        if (!isValid())
            return;

        // Reset item width
        maxItemWidth = 0;
        nonSelectableItems.clear();
        int proposalIndex = proposalList.getTopProposalList().size();
        for (String provider : proposalList.getProviderList()) {
            nonSelectableItems.add(proposalIndex);
            proposalIndex += proposalList.getProposals(provider).length + 1;
        }

        // If there is a table
        if (isValid()) {
            if (USE_VIRTUAL) {
                // Set and clear the virtual table. Data will be
                // provided in the SWT.SetData event handler.
                proposalTable.setItemCount(getTableLength());
                proposalTable.clearAll();
                initializeTextLayouts();
            } else {
                // Populate the table manually
                proposalTable.setRedraw(false);
                proposalTable.setItemCount(getTableLength());
                TableItem[] items = proposalTable.getItems();

                int index = 0;
                for (Proposal proposal : newProposalList.getTopProposalList()) {
                    TableItem item = items[index];
                    item.setText("  " + getString(proposal));
                    item.setImage(getImage(proposal, false));
                    item.setData(proposal);
                    index++;
                }
                for (String provider : newProposalList.getProviderList()) {
                    TableItem item = items[index];
                    int count = newProposalList.getCount(provider);
                    String text = provider + " (" + count + " matching items)";
                    item.setText(text);
                    // Data == null => not selectable
                    item.setData(null);

                    Display display = Display.getCurrent();
                    Color color = display.getSystemColor(SWT.COLOR_GRAY);
                    FontData fontData = item.getFont().getFontData()[0];
                    Font font = new Font(display,
                            new FontData(fontData.getName(), fontData.getHeight(), SWT.ITALIC | SWT.BOLD));
                    item.setBackground(color);
                    item.setFont(font);

                    index++;
                    for (Proposal proposal : newProposalList.getProposals(provider)) {
                        item.setText("  " + getString(proposal));
                        item.setImage(getImage(proposal, false));
                        item.setData(proposal);
                        index++;
                    }
                }
                proposalTable.setRedraw(true);
            }
            if (infoPopup != null) {
                infoPopup.close();
            }
        }
        footer.setText("");

        if (handleTopProposals) {
            adapter.handleTopProposals(newProposalList);
            // First to respond win otherwise if all reponded,
            // we stop handle
            if (adapter.hasSelectedTopProposal() || newProposalList.allResponded()) {
                handleTopProposals = false;
            }
        }
        // Select the top proposal that was displayed, if any
        proposalTable.deselectAll();
        if (adapter.hasSelectedTopProposal()) {
            int index = proposalList.getTopProposalList().indexOf(adapter.getSelectedTopProposal());
            if (index >= 0)
                selectProposal(index);
        }
        if (AutoCompleteUIPlugin.isRAP())
            adjustTableBounds();
    }

    /*
     * Return the proposal table length including header & top proposals.
     */
    private int getTableLength() {
        if (proposalList == null)
            return 0;
        return proposalList.length() + proposalList.getProviderList().size()
                + proposalList.getTopProposalList().size();
    }

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

    /*
     * Get the image for the specified proposal. If there is no image available,
     * return null.
     */
    private Image getImage(Proposal proposal, boolean selected) {
        if (proposal == null)
            return null;
        // return labelProvider.getImage(proposal);
        if (proposal.isPartial() && partialContentImage != null)
            return selected ? partialContentImageSelected : partialContentImage;
        if (proposal.isFunction() && functionContentImage != null)
            return selected ? functionContentImageSelected : functionContentImage;
        return null;
    }

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

    /*
     * 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. This includes a check for whether
     * the info popup has focus.
     */
    public 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 Proposal getSelectedProposal() {
        if (isValid()) {
            int index = proposalTable.getSelectionIndex();
            if (proposalList == null || index < 0 || index >= getTableLength()) {
                return null;
            }
            int proposalIndex = 0;
            for (Proposal proposal : proposalList.getTopProposalList()) {
                if (index == proposalIndex) {
                    return proposal;
                }
                proposalIndex++;
            }
            for (String provider : proposalList.getProviderList()) {
                if (index == proposalIndex) {
                    return null;
                }
                proposalIndex++;
                for (Proposal proposal : proposalList.getProposals(provider)) {
                    if (index == proposalIndex) {
                        return proposal;
                    }
                    proposalIndex++;
                }
            }
        }
        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() || proposalList == null || index >= getTableLength()) {
            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();
        Proposal 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();
        adapter.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
                            Proposal 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(boolean addToHistory) {
        // 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
        Proposal proposal = getSelectedProposal();
        if (proposal != null) {
            adapter.proposalAccepted(proposal, addToHistory);
            close();
        }
    }

    /*
     * Get the proposals from the proposal provider, and recompute any caches.
     * Repopulate the popup if it is open.
     */
    private void recomputeProposals(ContentProposalList newProposalList) {
        if (newProposalList == null)
            newProposalList = getEmptyProposalArray();
        if (newProposalList.fullLength() == 0 && newProposalList.allResponded()) {
            this.proposalList = newProposalList;
            close();
        } else {
            setProposals(newProposalList);
        }
    }

    /*
     * 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() {
        footer.setText("Searching...");
        if (isValid()) {
            synchronized (uniqueId) {
                if (uniqueId == Long.MAX_VALUE)
                    uniqueId = Long.MIN_VALUE;
                uniqueId++;
            }
            final Long currentId = new Long(uniqueId);
            control.getDisplay().asyncExec(new Runnable() {
                public void run() {
                    adapter.getProposals(new IContentProposalSearchHandler() {

                        @Override
                        public void handleResult(final ContentProposalList proposalList) {
                            if (control != null && !control.isDisposed()) {
                                control.getDisplay().asyncExec(new Runnable() {
                                    public void run() {
                                        if (currentId.equals(uniqueId))
                                            recomputeProposals(proposalList);
                                    }
                                });
                            }
                        }

                        @Override
                        public void handleTooltips(List<TooltipData> tooltips) {
                            adapter.handleTooltipData(tooltips);
                        }
                    });
                }
            });
        }
    }

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

    public Point getPopupSize() {
        return popupSize;
    }

    public void setPopupSize(Point size) {
        popupSize = size;
    }

    public void refreshProposals(ContentProposalList newProposalList) {
        if (!adapter.isPreventTopProposalSelection())
            handleTopProposals = true;
        recomputeProposals(newProposalList);
    }
}