diff org.eclipse.jface/src/org/eclipse/jface/fieldassist/ContentProposalAdapter.d @ 12:bc29606a740c

Added dwt-addons in original directory structure of eclipse.org
author Frank Benoit <benoit@tionex.de>
date Sat, 14 Mar 2009 18:23:29 +0100
parents
children 6f068362a363
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/org.eclipse.jface/src/org/eclipse/jface/fieldassist/ContentProposalAdapter.d	Sat Mar 14 18:23:29 2009 +0100
@@ -0,0 +1,2117 @@
+/*******************************************************************************
+ * Copyright (c) 2005, 2008 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
+ * Port to the D programming language:
+ *     Frank Benoit <benoit@tionex.de>
+ *******************************************************************************/
+module org.eclipse.jface.fieldassist.ContentProposalAdapter;
+
+import org.eclipse.jface.fieldassist.IContentProposal;
+import org.eclipse.jface.fieldassist.IContentProposalProvider;
+import org.eclipse.jface.fieldassist.IControlContentAdapter;
+import org.eclipse.jface.fieldassist.IControlContentAdapter2;
+import org.eclipse.jface.fieldassist.IContentProposalListener;
+import org.eclipse.jface.fieldassist.IContentProposalListener2;
+
+
+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.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.core.runtime.Assert;
+import org.eclipse.core.runtime.ListenerList;
+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.viewers.ILabelProvider;
+
+import java.lang.all;
+import java.util.Set;
+import java.lang.JThread;
+static import tango.text.Text;
+import tango.io.Stdout;
+import tango.text.convert.Format;
+alias tango.text.Text.Text!(char) StringBuffer;
+/**
+ * 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 is not intended to be subclassed.
+ *
+ * @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 : 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 : Listener {
+            private bool scrollbarClicked = false;
+
+            public void handleEvent(Event e) {
+
+                // If focus is leaving an important widget or the field's
+                // shell is deactivating
+                if (e.type is 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 class(e) Runnable {
+                        Event e_;
+                        this(Event e__){ e_=e__; }
+                        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 is getShell()
+                                        || (infoPopup !is null && infoPopup
+                                                .getShell() is 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 is 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 !is 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 !is null) {
+                        scrollbar.removeListener(SWT.Selection, this);
+                    }
+
+                    getShell().removeListener(SWT.Deactivate, this);
+                    getShell().removeListener(SWT.Close, this);
+                }
+
+                if (control !is 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 : 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 is 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 !is 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 is 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 is 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 is 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 is 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 is SWT.KeyDown) {
+                            // don't propagate to control
+                            e.doit = false;
+                        }
+                        break;
+
+                    case SWT.HOME:
+                        newSelection = 0;
+                        if (e.type is SWT.KeyDown) {
+                            // don't propagate to control
+                            e.doit = false;
+                        }
+                        break;
+
+                    case SWT.END:
+                        newSelection = proposalTable.getItemCount() - 1;
+                        if (e.type is 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 is 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 !is SWT.CAPS_LOCK && e.keyCode !is SWT.MOD1
+                                && e.keyCode !is SWT.MOD2
+                                && e.keyCode !is SWT.MOD3
+                                && e.keyCode !is 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 !is 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 = cast(Object)getSelectedProposal();
+                    if (p !is 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 !is FILTER_NONE) {
+                        // We have no filter to back out of, so do nothing
+                        if (filterText.length is 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 (CharacterIsDefined(key)) {
+                        if (filterStyle is FILTER_CUMULATIVE) {
+                            filterText = filterText ~ dcharToString(key);
+                        } else if (filterStyle is FILTER_CHARACTER) {
+                            filterText = dcharToString(key);
+                        }
+                        // Recompute proposals after processing this event.
+                        asyncRecomputeProposals(filterText);
+                    }
+                    break;
+                }
+            }
+        }
+
+        /*
+         * Internal class used to implement the secondary popup.
+         */
+        private class InfoPopupDialog : 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.
+             */
+            this(Shell parent) {
+                super(parent, PopupDialog.HOVER_SHELLSTYLE, false, false,
+                        false, false, null, null);
+            }
+
+            /*
+             * Create a text control for showing the info about a proposal.
+             */
+            protected override 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 class FocusAdapter {
+                    public void focusGained(FocusEvent event) {
+                        this.outer.close();
+                    }
+                });
+                return text;
+            }
+
+            /*
+             * Adjust the bounds so that we appear adjacent to our parent shell
+             */
+            protected override 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 is null) {
+                    newContents = EMPTY;
+                }
+                this.contents = newContents;
+                if (text !is null && !text.isDisposed()) {
+                    text.setText(contents);
+                }
+            }
+
+            /*
+             * Return whether the popup has focus.
+             */
+            bool hasFocus() {
+                if (text is 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 bool 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.
+         */
+        this(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, 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 override final Control createDialogArea(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 class 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 class 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 is null) {
+                        if (infoPopup !is 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 override 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() is 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 is 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();
+            }
+            getShell().setBounds(initialX, initialY, popupSize.x, popupSize.y);
+
+            // Now set up a listener to monitor any changes in size.
+            getShell().addListener(SWT.Resize, new class Listener {
+                public void handleEvent(Event e) {
+                    popupSize = getShell().getSize();
+                    if (infoPopup !is 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 = cast(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(cast(Object)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 is null || newProposals.length is 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(cast(Object)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 !is null) {
+                        infoPopup.close();
+                    }
+
+                }
+            }
+        }
+
+        /*
+         * Get the string for the specified proposal. Always return a String of
+         * some kind.
+         */
+        private String getString(IContentProposal proposal) {
+            if (proposal is null) {
+                return EMPTY;
+            }
+            if (labelProvider is null) {
+                return proposal.getLabel() is null ? proposal.getContent()
+                        : proposal.getLabel();
+            }
+            return labelProvider.getText(cast(Object)proposal);
+        }
+
+        /*
+         * Get the image for the specified proposal. If there is no image
+         * available, return null.
+         */
+        private Image getImage(IContentProposal proposal) {
+            if (proposal is null || labelProvider is null) {
+                return null;
+            }
+            return labelProvider.getImage(cast(Object)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 bool isValid() {
+            return proposalTable !is null && !proposalTable.isDisposed();
+        }
+
+        /*
+         * Return whether the receiver has focus. Since 3.4, this includes a
+         * check for whether the info popup has focus.
+         */
+        private bool hasFocus() {
+            if (!isValid()) {
+                return false;
+            }
+            if (getShell().isFocusControl() || proposalTable.isFocusControl()) {
+                return true;
+            }
+            if (infoPopup !is null && infoPopup.hasFocus()) {
+                return true;
+            }
+            return false;
+        }
+
+        /*
+         * Return the current selected proposal.
+         */
+        private IContentProposal getSelectedProposal() {
+            if (isValid()) {
+                int i = proposalTable.getSelectionIndex();
+                if (proposals is 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 is 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 override int open() {
+            int value = super.open();
+            if (popupCloser is null) {
+                popupCloser = new PopupCloserListener();
+            }
+            popupCloser.installListeners();
+            IContentProposal p = getSelectedProposal();
+            if (p !is 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 override bool close() {
+            popupCloser.removeListeners();
+            if (infoPopup !is null) {
+                infoPopup.close();
+            }
+            bool 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.
+                auto r = new class() Runnable {
+                    public void run() {
+                        pendingDescriptionUpdate = true;
+
+                        try {
+                            JThread.sleep( POPUP_DELAY );
+                        }
+                        catch (InterruptedException e) {
+                        }
+
+                        if (!isValid()) {
+                            return;
+                        }
+                        getShell().getDisplay().syncExec(new class() Runnable {
+                            public void run() {
+                                // Query the current selection since we have
+                                // been delayed
+                                IContentProposal p = getSelectedProposal();
+                                if (p !is null) {
+                                    String description = p.getDescription();
+                                    if (description !is null) {
+                                        if (infoPopup is null) {
+                                            infoPopup = new InfoPopupDialog(
+                                                    getShell());
+                                            infoPopup.open();
+                                            infoPopup
+                                                    .getShell()
+                                                    .addDisposeListener(
+                                                            new class DisposeListener {
+                                                                public void widgetDisposed(
+                                                                        DisposeEvent event) {
+                                                                    infoPopup = null;
+                                                                }
+                                                            });
+                                        }
+                                        infoPopup.setContents(p
+                                                .getDescription());
+                                    } else if (infoPopup !is null) {
+                                        infoPopup.close();
+                                    }
+                                    pendingDescriptionUpdate = false;
+
+                                }
+                            }
+                        });
+                    }
+                };
+                JThread t = new JThread(r);
+                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 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 is 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(String filterText) {
+            if (isValid()) {
+                control.getDisplay().asyncExec(new class(filterText) Runnable {
+                    String filterText_;
+                    this(String a){filterText_=a;}
+                    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 is 0) {
+                return proposals;
+            }
+
+            // Check each string for a match. Use the string displayed to the
+            // user, not the proposal content.
+            scope IContentProposal[] list = new IContentProposal[proposals.length];
+            int idx = 0;
+            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[idx++] = proposals[i];
+                }
+
+            }
+            return list[ 0 .. idx ].dup;
+        }
+
+        Listener getTargetControlListener() {
+            if (targetControlListener is null) {
+                targetControlListener = new TargetControlListener();
+            }
+            return targetControlListener;
+        }
+    }
+
+    /**
+     * Flag that controls the printing of debug info.
+     */
+    public static const bool DEBUG = false;
+
+    /**
+     * Indicates that a chosen proposal should be inserted into the field.
+     */
+    public static const int PROPOSAL_INSERT = 1;
+
+    /**
+     * Indicates that a chosen proposal should replace the entire contents of
+     * the field.
+     */
+    public static const 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 const int PROPOSAL_IGNORE = 3;
+
+    /**
+     * Indicates that there should be no filter applied as keys are typed in the
+     * popup.
+     */
+    public static const int FILTER_NONE = 1;
+
+    /**
+     * Indicates that a single character filter applies as keys are typed in the
+     * popup.
+     */
+    public static const 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 const 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 const bool USE_VIRTUAL = !"motif".equals(SWT.getPlatform()); //$NON-NLS-1$
+
+    /*
+     * The delay before showing a secondary popup.
+     */
+    private static const int POPUP_DELAY = 750;
+
+    /*
+     * The character height hint for the popup. May be overridden by using
+     * setInitialPopupSize.
+     */
+    private static const int POPUP_CHAR_HEIGHT = 10;
+
+    /*
+     * The minimum pixel width for the popup. May be overridden by using
+     * setInitialPopupSize.
+     */
+    private static const int POPUP_MINIMUM_WIDTH = 300;
+
+    /*
+     * The pixel offset of the popup from the bottom corner of the control.
+     */
+    private static const int POPUP_OFFSET = 3;
+
+    /*
+     * Empty string.
+     */
+    private static const 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 bool that indicates whether key events received while the proposal
+     * popup is open should also be propagated to the control. Default value is
+     * true.
+     */
+    private bool 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;
+
+    /*
+     * The list of IContentProposalListener2 listeners.
+     */
+    private ListenerList proposalListeners2;
+
+    /*
+     * Flag that indicates whether the adapter is enabled. In some cases,
+     * adapters may be installed but depend upon outside state.
+     */
+    private bool isEnabled_ = true;
+
+    /*
+     * The delay in milliseconds used when autoactivating the popup.
+     */
+    private int autoActivationDelay = 0;
+
+    /*
+     * A bool indicating whether a keystroke has been received. Used to see
+     * if an autoactivation delay was interrupted by a keystroke.
+     */
+    private bool 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;
+
+    /*
+     * A flag that indicates that we are watching modify events
+     */
+    private bool 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 this(Control control,
+            IControlContentAdapter controlContentAdapter,
+            IContentProposalProvider proposalProvider, KeyStroke keyStroke,
+            char[] autoActivationCharacters) {
+        //DWT_Init
+        proposalListeners = new ListenerList();
+        proposalListeners2 = new ListenerList();
+        selectionRange = new Point(-1, -1);
+        //super();
+        // We always assume the control and content adapter are valid.
+        Assert.isNotNull(cast(Object)control);
+        Assert.isNotNull(cast(Object)controlContentAdapter);
+        this.control = control;
+        this.controlContentAdapter = controlContentAdapter;
+
+        // The rest of these may be null
+        this.proposalProvider = proposalProvider;
+        this.triggerKeyStroke = keyStroke;
+        if (autoActivationCharacters.length !is 0 ) {
+            this.autoActivateString = 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 bool indicating whether the receiver is enabled.
+     *
+     * @return <code>true</code> if the adapter is enabled, and
+     *         <code>false</code> if it is not.
+     */
+    public bool 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 is 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.length is 0) {
+            this.autoActivateString = null;
+        } else {
+            this.autoActivateString = 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 bool 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 bool 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 bool getPropagateKeys() {
+        return propagateKeys;
+    }
+
+    /**
+     * Set the bool 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 bool 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(bool 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 bool 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(bool enabled) {
+        // If we are disabling it while it's proposing content, close the
+        // content proposal popup.
+        if (isEnabled_ && !enabled) {
+            if (popup !is 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(cast(Object)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(cast(Object)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(cast(Object)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(cast(Object)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) {
+            Stdout.formatln("ContentProposalListener#installControlListener()"); //$NON-NLS-1$
+        }
+
+        if (controlListener !is null) {
+            return;
+        }
+        controlListener = new class 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 is 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 !is null) {
+                        popup.getTargetControlListener().handleEvent(e);
+                        if (DEBUG) {
+                            StringBuffer sb;
+                            if (e.type is 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);
+                        }
+
+                        return;
+                    }
+
+                    // We were only listening to traverse events for the popup
+                    if (e.type is SWT.Traverse) {
+                        return;
+                    }
+
+                    // The popup is not open. We are looking at keydown events
+                    // for a trigger to open the popup.
+                    if (triggerKeyStroke !is null) {
+                        // Either there are no modifiers for the trigger and we
+                        // check the character field...
+                        if ((triggerKeyStroke.getModifierKeys() is KeyStroke.NO_KEY && triggerKeyStroke
+                                .getNaturalKey() is e.character)
+                                ||
+                                // ...or there are modifiers, in which case the
+                                // keycode and state must match
+                                (triggerKeyStroke.getNaturalKey() is e.keyCode && ((triggerKeyStroke
+                                        .getModifierKeys() & e.stateMask) is 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 !is 0) {
+                        if (autoActivateString !is 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;
+                            }
+                        } 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 is null) {
+                                watchModify = true;
+                            }
+                        }
+                    }
+                    break;
+
+                // 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 reopen it when there are. This means
+                // we should check modifications in those cases.
+                // See also https://bugs.eclipse.org/bugs/show_bug.cgi?id=183650
+                // The watchModify flag ensures that we don't autoactivate if
+                // the content change was caused by something other than typing.
+                case SWT.Modify:
+                    if (triggerKeyStroke is null && autoActivateString is null
+                            && watchModify) {
+                        if (DEBUG) {
+                            dump("Modify event triggers autoactivation", e); //$NON-NLS-1$
+                        }
+                        watchModify = false;
+                        // We don't autoactivate if the net change is no
+                        // content.  In other words, backspacing to empty
+                        // should never cause a popup to open.
+                        if (!isControlContentEmpty()) {
+                            autoActivate();
+                        }
+                    }
+                    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(Format(" - e: keyCode={}{}", e.keyCode, hex(e.keyCode))); //$NON-NLS-1$
+                sb.append(Format("; character={}{}", e.character, hex(e.character))); //$NON-NLS-1$
+                sb.append(Format("; stateMask={}{}", e.stateMask, hex(e.stateMask))); //$NON-NLS-1$
+                sb.append(Format("; doit={}", e.doit)); //$NON-NLS-1$
+                sb.append(Format("; detail={}", e.detail, hex(e.detail))); //$NON-NLS-1$
+                sb.append(Format("; widget={}", e.widget)); //$NON-NLS-1$
+                Stdout.formatln("{}",sb.toString);
+            }
+
+            private String hex(int i) {
+                return Format("[0x{:X}]", i); //$NON-NLS-1$
+            }
+        };
+        control.addListener(SWT.KeyDown, controlListener);
+        control.addListener(SWT.Traverse, controlListener);
+        control.addListener(SWT.Modify, controlListener);
+
+        if (DEBUG) {
+            Stdout.formatln("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 bool indicating whether the popup was autoactivated. If
+     *            false, a beep will sound when no proposals can be shown.
+     */
+    private void openProposalPopup(bool autoActivated) {
+        if (isValid()) {
+            if (popup is 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) {
+                        Stdout.formatln("POPUP OPENED BY PRECEDING EVENT"); //$NON-NLS-1$
+                    }
+                    recordCursorPosition();
+                    popup = new ContentProposalPopup(null, proposals);
+                    popup.open();
+                    popup.getShell().addDisposeListener(new class DisposeListener {
+                        public void widgetDisposed(DisposeEvent event) {
+                            popup = null;
+                        }
+                    });
+                    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 !is 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 ((null !is cast(IControlContentAdapter2)controlContentAdapter)
+                    && selectionRange.x !is -1) {
+                (cast(IControlContentAdapter2) controlContentAdapter).setSelection(
+                        control, selectionRange);
+            } else if (insertionPos !is -1) {
+                controlContentAdapter.setCursorPosition(control, insertionPos);
+            }
+            controlContentAdapter.insertControlContents(control, text,
+                    cursorPosition);
+        }
+    }
+
+    /*
+     * Check that the control and content adapter are valid.
+     */
+    private bool isValid() {
+        return control !is null && !control.isDisposed()
+                && controlContentAdapter !is 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 (null !is cast(IControlContentAdapter2)adapter ) {
+                selectionRange = (cast(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 is null || !isValid()) {
+            return null;
+        }
+        if (DEBUG) {
+            Stdout.formatln(">>> obtaining proposals from provider"); //$NON-NLS-1$
+        }
+        int position = insertionPos;
+        if (position is -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) {
+            auto r = new class Runnable{
+                public void run(){
+                    receivedKeyDown = false;
+                    try {
+                        JThread.sleep(autoActivationDelay);
+                    } catch (InterruptedException e) {
+                    }
+                    if (!isValid() || receivedKeyDown) {
+                        return;
+                    }
+                    getControl().getDisplay().syncExec(new class Runnable {
+                        public void run() {
+                            openProposalPopup(true);
+                        }
+                    });
+                }
+            };
+            JThread t = new JThread(r);
+            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 class Runnable {
+                public void run() {
+                    if (isValid()) {
+                        openProposalPopup(true);
+                    }
+                }
+            });
+        }
+    }
+
+    /*
+     * A proposal has been accepted. Notify interested listeners.
+     */
+    private void notifyProposalAccepted(IContentProposal proposal) {
+        if (DEBUG) {
+            Stdout.formatln("Notify listeners - proposal accepted."); //$NON-NLS-1$
+        }
+        Object[] listenerArray = proposalListeners.getListeners();
+        for (int i = 0; i < listenerArray.length; i++) {
+            (cast(IContentProposalListener) listenerArray[i])
+                    .proposalAccepted(proposal);
+        }
+    }
+
+    /*
+     * The proposal popup has opened. Notify interested listeners.
+     */
+    private void notifyPopupOpened() {
+        if (DEBUG) {
+            Stdout.formatln("Notify listeners - popup opened."); //$NON-NLS-1$
+        }
+        Object[] listenerArray = proposalListeners2.getListeners();
+        for (int i = 0; i < listenerArray.length; i++) {
+            (cast(IContentProposalListener2) listenerArray[i])
+                    .proposalPopupOpened(this);
+        }
+    }
+
+    /*
+     * The proposal popup has closed. Notify interested listeners.
+     */
+    private void notifyPopupClosed() {
+        if (DEBUG) {
+            Stdout.formatln("Notify listeners - popup closed."); //$NON-NLS-1$
+        }
+        Object[] listenerArray = proposalListeners2.getListeners();
+        for (int i = 0; i < listenerArray.length; i++) {
+            (cast(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 bool hasProposalPopupFocus() {
+        return popup !is null && popup.hasFocus();
+    }
+
+    /*
+     * Return whether the control content is empty
+     */
+    private bool isControlContentEmpty() {
+        return getControlContentAdapter().getControlContents(getControl())
+                .length is 0;
+    }
+}