view org.eclipse.jface/src/org/eclipse/jface/bindings/keys/KeySequenceText.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
line wrap: on
line source

/*******************************************************************************
 * Copyright (c) 2004, 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.bindings.keys.KeySequenceText;

import org.eclipse.jface.bindings.keys.KeyStroke;
import org.eclipse.jface.bindings.keys.SWTKeySupport;
import org.eclipse.jface.bindings.keys.KeySequence;


import org.eclipse.swt.SWT;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.FocusListener;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.Device;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Text;
import org.eclipse.jface.util.IPropertyChangeListener;
import org.eclipse.jface.util.PropertyChangeEvent;

import java.lang.all;
import java.util.Collections;
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
import java.text.ParseException;

/**
 * <p>
 * A wrapper around the SWT text widget that traps literal key presses and
 * converts them into key sequences for display. There are two types of key
 * strokes that are displayed: complete and incomplete. A complete key stroke is
 * one with a natural key, while an incomplete one has no natural key.
 * Incomplete key strokes are only displayed until they are made complete or
 * their component key presses are released.
 * </p>
 *
 * @since 3.1
 */
public final class KeySequenceText {

    /**
     * A key listener that traps incoming events and displays them in the
     * wrapped text field. It has no effect on traversal operations.
     */
    private class KeyTrapListener : Listener {
        /**
         * The index at which insertion should occur. This is used if there is a
         * replacement occurring in the middle of the stroke, and the first key
         * stroke was incomplete.
         */
        private int insertionIndex = -1;

        /**
         * Resets the insertion index to point nowhere. In other words, it is
         * set to <code>-1</code>.
         */
        void clearInsertionIndex() {
            insertionIndex = -1;
        }

        /**
         * Deletes the current selection. If there is no selection, then it
         * deletes the last key stroke.
         *
         * @param keyStrokes
         *            The key strokes from which to delete. This list must not
         *            be <code>null</code>, and must represent a valid key
         *            sequence.
         * @return An array of keystrokes minus the keystrokes that were
         *         deleted.
         */
        private final KeyStroke[] deleteKeyStroke(KeyStroke[] keyStrokes) {
            clearInsertionIndex();

            if (hasSelection()) {
                /*
                 * Delete the current selection -- disallowing incomplete
                 * strokes in the middle of the sequence.
                 */
                KeyStroke[][] deletedKeyStrokes = new KeyStroke[][](1);
                deleteSelection(keyStrokes, false, deletedKeyStrokes);
                return deletedKeyStrokes[0];
            }

            // Remove the last key stroke.
            if (keyStrokes.length > 0) {
                int newKeyStrokesLength = keyStrokes.length - 1;
                KeyStroke[] newKeyStrokes = new KeyStroke[newKeyStrokesLength];
                System.arraycopy(keyStrokes, 0, newKeyStrokes, 0,
                        newKeyStrokesLength);
                return newKeyStrokes;
            }

            return keyStrokes;
        }

        /**
         * Handles the key pressed and released events on the wrapped text
         * widget. This makes sure to either add the pressed key to the
         * temporary key stroke, or complete the current temporary key stroke
         * and prompt for the next. In the case of a key release, this makes
         * sure that the temporary stroke is correctly displayed --
         * corresponding with modifier keys that may have been released.
         *
         * @param event
         *            The triggering event; must not be <code>null</code>.
         */
        public void handleEvent(Event event) {
            KeyStroke[] keyStrokes = getKeySequence().getKeyStrokes();

            // Dispatch the event to the correct handler.
            if (event.type is SWT.KeyDown) {
                keyStrokes = handleKeyDown(event, keyStrokes);
            } else if (event.type is SWT.KeyUp) {
                keyStrokes = handleKeyUp(event, keyStrokes);
            }

            // Update the underlying widget.
            setKeySequence(KeySequence.getInstance(keyStrokes));

            // Prevent the event from reaching the widget.
            event.doit = false;
        }

        /**
         * Handles the case where the key event is an <code>SWT.KeyDown</code>
         * event. This either causes a deletion (if it is an unmodified
         * backspace key stroke), or an insertion (if it is any other key).
         *
         * @param event
         *            The trigger key down event; must not be <code>null</code>.
         * @param keyStrokes
         *            The current list of key strokes. This valud must not be
         *            <code>null</code>, and it must represent a valid key
         *            sequence.
         */
        private KeyStroke[] handleKeyDown(Event event, KeyStroke[] keyStrokes) {
            // Is it an unmodified backspace character?
            if ((event.character is SWT.BS || event.character is SWT.DEL) && (event.stateMask is 0)) {
                return deleteKeyStroke(keyStrokes);
            }

            return insertKeyStroke(event, keyStrokes);
        }

        /**
         * Handles the case where the key event is an <code>SWT.KeyUp</code>
         * event. This resets the insertion index. If there is an incomplete
         * stroke, then that incomplete stroke is modified to match the keys
         * that are still held. If no keys are held, then the incomplete stroke
         * is removed.
         *
         * @param event
         *            The triggering event; must not be <code>null</code>
         * @param keyStrokes
         *            The key strokes that are part of the current key sequence;
         *            these key strokes are guaranteed to represent a valid key
         *            sequence. This value must not be <code>null</code>.
         */
        private final KeyStroke[] handleKeyUp(Event event,
                KeyStroke[] keyStrokes) {
            if (hasIncompleteStroke()) {
                /*
                 * Figure out the SWT integer representation of the remaining
                 * values.
                 */
                Event mockEvent = new Event();
                if ((event.keyCode & SWT.MODIFIER_MASK) !is 0) {
                    // This key up is a modifier key being released.
                    mockEvent.stateMask = event.stateMask - event.keyCode;
                } else {
                    /*
                     * This key up is the other end of a key down that was
                     * trapped by the operating system or window manager.
                     */
                    mockEvent.stateMask = event.stateMask;
                }

                /*
                 * Get a reasonable facsimile of the stroke that is still
                 * pressed.
                 */
                int key = SWTKeySupport
                        .convertEventToUnmodifiedAccelerator(mockEvent);
                KeyStroke remainingStroke = SWTKeySupport
                        .convertAcceleratorToKeyStroke(key);
                int keyStrokesLength = keyStrokes.length;
                KeyStroke[] newKeyStrokes;
                if ((keyStrokesLength > 0)
                        && (remainingStroke.getModifierKeys() !is 0)) {
                    newKeyStrokes = new KeyStroke[keyStrokesLength];
                    System.arraycopy(keyStrokes, 0, newKeyStrokes, 0,
                            keyStrokesLength - 1);
                    newKeyStrokes[keyStrokesLength - 1] = remainingStroke;

                } else if (keyStrokesLength > 0) {
                    newKeyStrokes = new KeyStroke[keyStrokesLength - 1];
                    System.arraycopy(keyStrokes, 0, newKeyStrokes, 0,
                            keyStrokesLength - 1);

                } else if (remainingStroke.getModifierKeys() !is 0) {
                    newKeyStrokes = new KeyStroke[keyStrokesLength + 1];
                    System.arraycopy(keyStrokes, 0, newKeyStrokes, 0,
                            keyStrokesLength);
                    newKeyStrokes[keyStrokesLength] = remainingStroke;

                } else {
                    newKeyStrokes = keyStrokes;

                }

                return newKeyStrokes;
            }

            return keyStrokes;
        }

        /**
         * <p>
         * Handles the case where a key down event is leading to a key stroke
         * being inserted. The current selection is deleted, and an invalid
         * remanents of the stroke are also removed. The insertion is carried
         * out at the cursor position.
         * </p>
         * <p>
         * If only a natural key is selected (as part of a larger key stroke),
         * then it is possible for the user to press a natural key to replace
         * the old natural key. In this situation, pressing any modifier keys
         * will replace the whole thing.
         * </p>
         * <p>
         * If the insertion point is not at the end of the sequence, then
         * incomplete strokes will not be immediately inserted. Only when the
         * sequence is completed is the stroke inserted. This is a requirement
         * as the widget must always represent a valid key sequence. The
         * insertion point is tracked using <code>insertionIndex</code>,
         * which is an index into the key stroke array.
         * </p>
         *
         * @param event
         *            The triggering key down event; must not be
         *            <code>null</code>.
         * @param keyStrokes
         *            The key strokes into which the current stroke should be
         *            inserted. This value must not be <code>null</code>, and
         *            must represent a valid key sequence.
         */
        private final KeyStroke[] insertKeyStroke(Event event,
                KeyStroke[] keyStrokes) {
            // Compute the key stroke to insert.
            int key = SWTKeySupport.convertEventToUnmodifiedAccelerator(event);
            KeyStroke stroke = SWTKeySupport.convertAcceleratorToKeyStroke(key);

            /*
             * Only insert the stroke if it is *not ScrollLock. Let's not get
             * silly
             */
            if ((SWT.NUM_LOCK is stroke.getNaturalKey())
                    || (SWT.CAPS_LOCK is stroke.getNaturalKey())
                    || (SWT.SCROLL_LOCK is stroke.getNaturalKey())) {
                return keyStrokes;
            }

            if (insertionIndex !is -1) {
                // There is a previous replacement still going on.
                if (stroke.isComplete()) {
                    keyStrokes = insertStrokeAt(keyStrokes, stroke,
                            insertionIndex);
                    clearInsertionIndex();
                }

            } else if (hasSelection()) {
                // There is a selection that needs to be replaced.
                KeyStroke[][] deletedKeyStrokes = new KeyStroke[][](1);
                insertionIndex = deleteSelection(keyStrokes, stroke
                        .isComplete(), deletedKeyStrokes);
                keyStrokes = deletedKeyStrokes[0];
                if ((stroke.isComplete())
                        || (insertionIndex >= keyStrokes.length)) {
                    keyStrokes = insertStrokeAt(keyStrokes, stroke,
                            insertionIndex);
                    clearInsertionIndex();
                }

            } else {
                // No selection, so remove the incomplete stroke, if any
                if ((hasIncompleteStroke()) && (keyStrokes.length > 0)) {
                    KeyStroke[] newKeyStrokes = new KeyStroke[keyStrokes.length - 1];
                    System.arraycopy(keyStrokes, 0, newKeyStrokes, 0,
                            keyStrokes.length - 1);
                    keyStrokes = newKeyStrokes;
                }

                // And then add the new stroke.
                if ((keyStrokes.length is 0)
                        || (insertionIndex >= keyStrokes.length)
                        || (isCursorInLastPosition())) {
                    keyStrokes = insertStrokeAt(keyStrokes, stroke,
                            keyStrokes.length);
                    clearInsertionIndex();
                } else {
                    /*
                     * I'm just getting the insertionIndex here. No actual
                     * deletion should occur.
                     */
                    KeyStroke[][] deletedKeyStrokes = new KeyStroke[][](1);
                    insertionIndex = deleteSelection(keyStrokes, stroke
                            .isComplete(), deletedKeyStrokes);
                    keyStrokes = deletedKeyStrokes[0];
                    if (stroke.isComplete()) {
                        keyStrokes = insertStrokeAt(keyStrokes, stroke,
                                insertionIndex);
                        clearInsertionIndex();
                    }
                }

            }

            return keyStrokes;
        }
    }

    /**
     * A traversal listener that blocks all traversal except for tabs and arrow
     * keys.
     */
    private class TraversalFilter : Listener {
        /**
         * Handles the traverse event on the text field wrapped by this class.
         * It swallows all traverse events example for tab and arrow key
         * navigation. The other forms of navigation can be reached by tabbing
         * off of the control.
         *
         * @param event
         *            The trigger event; must not be <code>null</code>.
         */
        public void handleEvent(Event event) {
            switch (event.detail) {
            case SWT.TRAVERSE_ESCAPE:
            case SWT.TRAVERSE_MNEMONIC:
            case SWT.TRAVERSE_NONE:
            case SWT.TRAVERSE_PAGE_NEXT:
            case SWT.TRAVERSE_PAGE_PREVIOUS:
            case SWT.TRAVERSE_RETURN:
                event.type = SWT.None;
                event.doit = false;
                break;

            case SWT.TRAVERSE_TAB_NEXT:
            case SWT.TRAVERSE_TAB_PREVIOUS:
                // Check if modifiers other than just 'Shift' were
                // down.
                if ((event.stateMask & (SWT.MODIFIER_MASK ^ SWT.SHIFT)) !is 0) {
                    // Modifiers other than shift were down.
                    event.type = SWT.None;
                    event.doit = false;
                    break;
                }

                // fall through -- either no modifiers, or just shift.

            case SWT.TRAVERSE_ARROW_NEXT:
            case SWT.TRAVERSE_ARROW_PREVIOUS:
            default:
                // Let the traversal happen, but clear the incomplete
                // stroke
                if (hasIncompleteStroke()) {
                    KeyStroke[] oldKeyStrokes = getKeySequence()
                            .getKeyStrokes();
                    int newKeyStrokesLength = oldKeyStrokes.length - 1;
                    if (newKeyStrokesLength >= 1) {
                        KeyStroke[] newKeyStrokes = new KeyStroke[newKeyStrokesLength];
                        System.arraycopy(oldKeyStrokes, 0, newKeyStrokes, 0,
                                newKeyStrokesLength);
                        setKeySequence(KeySequence.getInstance(newKeyStrokes));
                    } else {
                        setKeySequence(KeySequence.getInstance());
                    }
                }
            }

        }
    }

    /**
     * The manager resposible for installing and removing the traversal filter
     * when the key sequence entry widget gains and loses focus.
     */
    private class TraversalFilterManager : FocusListener {
        /** The managed filter. We only need one instance. */
        private TraversalFilter filter;

        private bool filtering = false;

        this(){
            filter = new TraversalFilter();
        }

        /**
         * Attaches the global traversal filter.
         *
         * @param event
         *            Ignored.
         */
        public void focusGained(FocusEvent event) {
            Display.getCurrent().addFilter(SWT.Traverse, filter);
            filtering = true;
        }

        /**
         * Detaches the global traversal filter.
         *
         * @param event
         *            Ignored.
         */
        public void focusLost(FocusEvent event) {
            Display.getCurrent().removeFilter(SWT.Traverse, filter);
            filtering = false;
        }

        /**
         * Remove the traverse filter if we close without focusOut.
         */
        public void dispose() {
            if (filtering) {
                Display.getCurrent().removeFilter(SWT.Traverse, filter);
            }
        }
    }

    /**
     * A modification listener that makes sure that external events to this
     * class (i.e., direct modification of the underlying text) do not break
     * this class' view of the world.
     */
    private class UpdateSequenceListener : ModifyListener {
        /**
         * Handles the modify event on the underlying text widget.
         *
         * @param event
         *            The triggering event; ignored.
         */
        public void modifyText(ModifyEvent event) {
            try {
                // The original sequence.
                KeySequence originalSequence = getKeySequence();

                // The new sequence drawn from the text.
                String contents = getText();
                KeySequence newSequence = KeySequence.getInstance(contents);

                // Check to see if they're the same.
                if (!originalSequence.opEquals(newSequence)) {
                    setKeySequence(newSequence);
                }

            } catch (ParseException e) {
                // Abort any cut/paste-driven modifications
                setKeySequence(getKeySequence());
            }
        }
    }

    static this(){
        TreeSet trappedKeys = new TreeSet();
        trappedKeys.add(SWTKeySupport.convertAcceleratorToKeyStroke(SWT.TAB));
        trappedKeys.add(SWTKeySupport.convertAcceleratorToKeyStroke(SWT.TAB
                | SWT.SHIFT));
        trappedKeys.add(SWTKeySupport.convertAcceleratorToKeyStroke(SWT.BS));
        List trappedKeyList = new ArrayList(trappedKeys);
        TRAPPED_KEYS = Collections.unmodifiableList(trappedKeyList);
    }

    /** An empty string instance for use in clearing text values. */
    private static const String EMPTY_STRING = ""; //$NON-NLS-1$

    /**
     * The special integer value for the maximum number of strokes indicating
     * that an infinite number should be allowed.
     */
    public static const int INFINITE = -1;

    /**
     * The name of the property representing the current key sequence in this
     * key sequence widget.
     *
     * @since 3.2
     */
    public static const String P_KEY_SEQUENCE = "org.eclipse.jface.bindings.keys.KeySequenceText.KeySequence"; //$NON-NLS-1$

    /**
     * The keys trapped by this widget. This list is guaranteed to be roughly
     * accurate. Perfection is not possible, as SWT does not export traversal
     * keys as constants.
     */
    public static const List TRAPPED_KEYS;

    /**
     * The key filter attached to the underlying widget that traps key events.
     */
    private const KeyTrapListener keyFilter;

    /**
     * The text of the key sequence -- containing only the complete key strokes.
     */
    private KeySequence keySequence;

    /**
     * Those listening to changes to the key sequence in this widget. This value
     * may be <code>null</code> if there are no listeners.
     */
    private Collection listeners = null;

    /** The maximum number of key strokes permitted in the sequence. */
    private int maxStrokes = INFINITE;

    /** The text widget that is wrapped for this class. */
    private const Text text;

    /**
     * The listener that makes sure that the text widget remains up-to-date with
     * regards to external modification of the text (e.g., cut & pasting).
     */
    private const UpdateSequenceListener updateSequenceListener;

    /**
     * Constructs an instance of <code>KeySequenceTextField</code> with the
     * text field to use. If the platform is carbon (MacOS X), then the font is
     * set to be the same font used to display accelerators in the menus.
     *
     * @param wrappedText
     *            The text widget to wrap; must not be <code>null</code>.
     */
    public this(Text wrappedText) {
        keyFilter = new KeyTrapListener();
        keySequence = KeySequence.getInstance();
        updateSequenceListener = new UpdateSequenceListener();

        text = wrappedText;

        // Set the font if the platform is carbon.
        if ("carbon".equals(SWT.getPlatform())) { //$NON-NLS-1$
            // Don't worry about this font name here; it is the official menu
            // font and point size on the Mac.
            Font font = new Font(cast(Device)text.getDisplay(),
                    "Lucida Grande", 13, SWT.NORMAL); //$NON-NLS-1$
            text.setFont(font);
            text.addDisposeListener(new class(font) DisposeListener {
                Font font_;
                this(Font a){
                    font_=a;
                }
                public void widgetDisposed(DisposeEvent e) {
                    font_.dispose();
                }
            });
        }

        // Add the key listener.
        text.addListener(SWT.KeyUp, keyFilter);
        text.addListener(SWT.KeyDown, keyFilter);

        TraversalFilterManager traversalFilterManager = new TraversalFilterManager();
        text.addFocusListener(traversalFilterManager);
        text.addDisposeListener(new class(traversalFilterManager) DisposeListener {
            TraversalFilterManager traversalFilterManager_;
            this(TraversalFilterManager a){
                traversalFilterManager_=a;
            }
            public void widgetDisposed(DisposeEvent e) {
                traversalFilterManager_.dispose();
            }
        });

        // Add an internal modify listener.
        text.addModifyListener(updateSequenceListener);
    }

    /**
     * Adds a property change listener to this key sequence widget. It will be
     * notified when the key sequence changes.
     *
     * @param listener
     *            The listener to be notified when changes occur; must not be
     *            <code>null</code>.
     * @since 3.2
     */
    public final void addPropertyChangeListener(
            IPropertyChangeListener listener) {
        if (listener is null) {
            return;
        }

        if (listeners is null) {
            listeners = new ArrayList(1);
        }

        listeners.add(cast(Object)listener);
    }

    /**
     * Clears the text field and resets all the internal values.
     */
    public void clear() {
        KeySequence oldKeySequence = keySequence;
        keySequence = KeySequence.getInstance();
        text.setText(EMPTY_STRING);
        firePropertyChangeEvent(oldKeySequence);
    }

    /**
     * Removes the key strokes from the list corresponding the selection. If
     * <code>allowIncomplete</code>, then invalid key sequences will be
     * allowed (i.e., those with incomplete strokes in the non-terminal
     * position). Otherwise, incomplete strokes will be removed. This modifies
     * <code>keyStrokes</code> in place, and has no effect on the text widget
     * this class wraps.
     *
     * @param keyStrokes
     *            The list of key strokes from which the selection should be
     *            removed; must not be <code>null</code>.
     * @param allowIncomplete
     *            Whether incomplete strokes should be allowed to exist in the
     *            list after the deletion.
     * @param deletedKeyStrokes
     *            The list of keystrokes that were deleted by this operation.
     *            Declared as final since it will hold a reference to the new
     *            keyStroke array that has deleted the selected keystrokes.
     * @return The index at which a subsequent insert should occur. This index
     *         only has meaning to the <code>insertStrokeAt</code> method.
     */
    private final int deleteSelection(KeyStroke[] keyStrokes,
            bool allowIncomplete, KeyStroke[][] deletedKeyStrokes) {
        // Get the current selection.
        Point selection = text.getSelection();
        int start = selection.x;
        int end = selection.y;

        /*
         * Using the key sequence format method, discover the point at which
         * adding key strokes passes or equals the start of the selection. In
         * other words, find the first stroke that is part of the selection.
         * Keep track of the text range under which the stroke appears (i.e.,
         * startTextIndex->string.length() is the first selected stroke).
         */
        String string;
        auto currentStrokes = new ArrayList();
        int startTextIndex = 0; // keeps track of the start of the stroke
        int keyStrokesLength = keyStrokes.length;
        int i;
        for (i = 0; (i < keyStrokesLength) && (string.length < start); i++) {
            startTextIndex = string.length;
            currentStrokes.add(keyStrokes[i]);
            string = KeySequence.getInstance(currentStrokes).format();
        }

        /*
         * If string.length() is start, then the cursor is positioned between
         * strokes (i.e., selection is outside of a stroke).
         */
        int startStrokeIndex;
        if (string.length is start) {
            startStrokeIndex = currentStrokes.size();
        } else {
            startStrokeIndex = currentStrokes.size() - 1;
        }

        /*
         * Check to see if the cursor is only positioned, rather than actually
         * selecting something. We only need to compute the end if there is a
         * selection.
         */
        int endStrokeIndex;
        if (start is end) {
            // return the current keystrokes, nothing has to be deleted
            deletedKeyStrokes[0] = keyStrokes;
            return startStrokeIndex;
        }

        for (; (i < keyStrokesLength) && (string.length < end); i++) {
            currentStrokes.add(keyStrokes[i]);
            string = KeySequence.getInstance(currentStrokes).format();
        }
        endStrokeIndex = currentStrokes.size() - 1;
        if (endStrokeIndex < 0) {
            endStrokeIndex = 0;
        }

        /*
         * Remove the strokes that are touched by the selection. Keep track of
         * the first stroke removed.
         */
        int newLength = keyStrokesLength
                - (endStrokeIndex - startStrokeIndex + 1);
        deletedKeyStrokes[0] = new KeyStroke[newLength];
        KeyStroke startStroke = keyStrokes[startStrokeIndex];
        KeyStroke keyStrokeResult[] = new KeyStroke[newLength];
        System.arraycopy(keyStrokes, 0, keyStrokeResult, 0, startStrokeIndex);
        System.arraycopy(keyStrokes, endStrokeIndex + 1, keyStrokeResult,
                startStrokeIndex, keyStrokesLength - endStrokeIndex - 1);
        System.arraycopy(keyStrokeResult, 0, deletedKeyStrokes[0], 0, newLength);

        /*
         * Allow the first stroke removed to be replaced by an incomplete
         * stroke.
         */
        if (allowIncomplete) {
            int modifierKeys = startStroke.getModifierKeys();
            KeyStroke incompleteStroke = KeyStroke.getInstance(modifierKeys,
                    KeyStroke.NO_KEY);
            int incompleteStrokeLength = incompleteStroke.format().length;
            if ((startTextIndex + incompleteStrokeLength) <= start) {
                KeyStroke[] added = new KeyStroke[newLength + 1];
                System.arraycopy(deletedKeyStrokes[0], 0, added, 0,
                        startStrokeIndex);
                added[startStrokeIndex] = incompleteStroke;
                System.arraycopy(deletedKeyStrokes[0], startStrokeIndex, added,
                        startStrokeIndex + 1, newLength - startStrokeIndex);
                deletedKeyStrokes[0] = added;
            }
        }

        return startStrokeIndex;
    }

    /**
     * Fires a property change event to all of the listeners.
     *
     * @param oldKeySequence
     *            The old key sequence; must not be <code>null</code>.
     * @since 3.2
     */
    protected final void firePropertyChangeEvent(
            KeySequence oldKeySequence) {
        if (listeners !is null) {
            Iterator listenerItr = listeners.iterator();
            PropertyChangeEvent event = new PropertyChangeEvent(this,
                    P_KEY_SEQUENCE, oldKeySequence, getKeySequence());
             while (listenerItr.hasNext()) {
                IPropertyChangeListener listener = cast(IPropertyChangeListener) listenerItr
                        .next();
                listener.propertyChange(event);
            }
        }
    }

    /**
     * An accessor for the <code>KeySequence</code> that corresponds to the
     * current state of the text field. This includes incomplete strokes.
     *
     * @return The key sequence representation; never <code>null</code>.
     */
    public KeySequence getKeySequence() {
        return keySequence;
    }

    /**
     * An accessor for the underlying text widget's contents.
     *
     * @return The text contents of this entry; never <code>null</code>.
     */
    private String getText() {
        return text.getText();
    }

    /**
     * Tests whether the current key sequence has a stroke with no natural key.
     *
     * @return <code>true</code> is there is an incomplete stroke;
     *         <code>false</code> otherwise.
     */
    private bool hasIncompleteStroke() {
        return !keySequence.isComplete();
    }

    /**
     * Tests whether the current text widget has some text selection.
     *
     * @return <code>true</code> if the number of selected characters it
     *         greater than zero; <code>false</code> otherwise.
     */
    private bool hasSelection() {
        return (text.getSelectionCount() > 0);
    }

    /**
     * Inserts the key stroke at the current insertion point. This does a
     * regular delete and insert, as if the key had been pressed.
     *
     * @param stroke
     *            The key stroke to insert; must not be <code>null</code>.
     */
    public void insert(KeyStroke stroke) {
        if (!stroke.isComplete()) {
            return;
        }

        // Copy the key strokes in the current key sequence.
        KeySequence keySequence = getKeySequence();
        KeyStroke[] oldKeyStrokes = keySequence.getKeyStrokes();
        KeyStroke[] newKeyStrokes;
        if ((hasIncompleteStroke()) && (!keySequence.isEmpty())) {
            int newKeyStrokesLength = oldKeyStrokes.length - 1;
            newKeyStrokes = new KeyStroke[newKeyStrokesLength];
            System.arraycopy(oldKeyStrokes, 0, newKeyStrokes, 0,
                    newKeyStrokesLength);
        } else {
            newKeyStrokes = oldKeyStrokes;
        }

        KeyStroke[][] deletedKeyStrokes = new KeyStroke[][](1);
        int index = deleteSelection(newKeyStrokes, false, deletedKeyStrokes);
        if (index is -1) {
            index = 0;
        }

        KeyStroke[] keyStrokes = insertStrokeAt(newKeyStrokes, stroke, index);
        keyFilter.clearInsertionIndex();
        setKeySequence(KeySequence.getInstance(keyStrokes));
    }

    /**
     * Inserts the stroke at the given index in the list of strokes. If the
     * stroke currently at that index is incomplete, then it tries to merge the
     * two strokes. If merging is a complete failure (unlikely), then it will
     * simply overwrite the incomplete stroke. If the stroke at the index is
     * complete, then it simply inserts the stroke independently.
     *
     * @param keyStrokes
     *            The list of key strokes in which the key stroke should be
     *            appended; must not be <code>null</code>.
     * @param stroke
     *            The stroke to insert; should not be <code>null</code>.
     * @param index
     *            The index at which to insert; must be a valid index into the
     *            list of key strokes.
     */
    private final KeyStroke[] insertStrokeAt(KeyStroke[] keyStrokes,
            KeyStroke stroke, int index) {
        int keyStrokesLength = keyStrokes.length;
        KeyStroke currentStroke = (index >= keyStrokesLength) ? null
                : keyStrokes[index];
        if ((currentStroke !is null) && (!currentStroke.isComplete())) {
            int modifierKeys = currentStroke.getModifierKeys();
            int naturalKey = stroke.getNaturalKey();
            modifierKeys |= stroke.getModifierKeys();
            keyStrokes[index] = KeyStroke.getInstance(modifierKeys, naturalKey);
            return keyStrokes;
        }

        KeyStroke[] newKeyStrokes = new KeyStroke[keyStrokesLength + 1];
        System.arraycopy(keyStrokes, 0, newKeyStrokes, 0, index);
        newKeyStrokes[index] = stroke;
        if (index < keyStrokesLength) {
            System.arraycopy(keyStrokes, index, newKeyStrokes, index + 1,
                    keyStrokesLength-index);
        }
        return newKeyStrokes;
    }

    /**
     * Tests whether the cursor is in the last position. This means that the
     * selection extends to the last position.
     *
     * @return <code>true</code> if the selection extends to the last
     *         position; <code>false</code> otherwise.
     */
    private bool isCursorInLastPosition() {
        return (text.getSelection().y >= getText().length);
    }

    /**
     * Removes the given listener from this key sequence widget.
     *
     * @param listener
     *            The listener to be removed; must not be <code>null</code>.
     * @since 3.2
     */
    public final void removePropertyChangeListener(
            IPropertyChangeListener listener) {
        if ((listener is null) || (listeners is null)) {
            return;
        }

        listeners.remove(cast(Object)listener);
    }

    /**
     * <p>
     * A mutator for the key sequence stored within this widget. The text and
     * caret position are updated.
     * </p>
     * <p>
     * All sequences are limited to maxStrokes number of strokes in length. If
     * there are already that number of strokes, then it does not show
     * incomplete strokes, and does not keep track of them.
     * </p>
     *
     * @param newKeySequence
     *            The new key sequence for this widget; may be <code>null</code>
     *            if none.
     */
    public void setKeySequence(KeySequence newKeySequence) {
        KeySequence oldKeySequence = keySequence;

        if (newKeySequence is null) {
            text.setText(""); //$NON-NLS-1$
        } else {
            keySequence = newKeySequence;
        }

        // Trim any extra strokes.
        if (maxStrokes !is INFINITE) {
            KeyStroke[] oldKeyStrokes = keySequence.getKeyStrokes();
            if (maxStrokes < oldKeyStrokes.length) {
                KeyStroke[] newKeyStrokes = new KeyStroke[maxStrokes];
                System
                        .arraycopy(oldKeyStrokes, 0, newKeyStrokes, 0,
                                maxStrokes);
                keySequence = KeySequence.getInstance(newKeyStrokes);
            }
        }

        // Check to see if the text has changed.
        String currentString = getText();
        String newString = keySequence.format();
        if (!currentString.equals(newString)) {
            // We need to update the text
            text.removeModifyListener(updateSequenceListener);
            text.setText(keySequence.format());
            text.addModifyListener(updateSequenceListener);
            text.setSelection(getText().length);
        }

        firePropertyChangeEvent(oldKeySequence);
    }

    /**
     * Returns the maximum number of strokes that are permitted in this widget
     * at one time.
     *
     * @return The maximum number of strokes; will be a positive integer or
     *         <code>INFINITE</code>.
     */
    public int getKeyStrokeLimit() {
        return maxStrokes;
    }

    /**
     * A mutator for the maximum number of strokes that are permitted in this
     * widget at one time.
     *
     * @param keyStrokeLimit
     *            The maximum number of strokes; must be a positive integer or
     *            <code>INFINITE</code>.
     */
    public void setKeyStrokeLimit(int keyStrokeLimit) {
        if (keyStrokeLimit > 0 || keyStrokeLimit is INFINITE) {
            this.maxStrokes = keyStrokeLimit;
        } else {
            throw new IllegalArgumentException(null);
        }

        // Make sure we are obeying the new limit.
        setKeySequence(getKeySequence());
    }
}