| /******************************************************************************* |
| * Copyright (c) 2004, 2005 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 |
| *******************************************************************************/ |
| |
| package org.eclipse.jface.bindings.keys; |
| |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.TreeSet; |
| |
| import org.eclipse.jface.util.IPropertyChangeListener; |
| import org.eclipse.jface.util.PropertyChangeEvent; |
| 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.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; |
| |
| /** |
| * <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 implements 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. |
| */ |
| private final KeyStroke[] deleteKeyStroke(final KeyStroke[] keyStrokes) { |
| clearInsertionIndex(); |
| |
| if (hasSelection()) { |
| /* |
| * Delete the current selection -- disallowing incomplete |
| * strokes in the middle of the sequence. |
| */ |
| final KeyStroke[][] deletedKeyStrokes = new KeyStroke[1][]; |
| deleteSelection(keyStrokes, false, deletedKeyStrokes); |
| return deletedKeyStrokes[0]; |
| } |
| |
| // Remove the last key stroke. |
| if (keyStrokes.length > 0) { |
| final int newKeyStrokesLength = keyStrokes.length - 1; |
| final 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 == SWT.KeyDown) { |
| keyStrokes = handleKeyDown(event, keyStrokes); |
| } else if (event.type == 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 == SWT.BS) && (event.stateMask == 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(final Event event, |
| final KeyStroke[] keyStrokes) { |
| if (hasIncompleteStroke()) { |
| /* |
| * Figure out the SWT integer representation of the remaining |
| * values. |
| */ |
| Event mockEvent = new Event(); |
| if ((event.keyCode & SWT.MODIFIER_MASK) != 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); |
| final int keyStrokesLength = keyStrokes.length; |
| final KeyStroke[] newKeyStrokes; |
| if ((keyStrokesLength > 0) |
| && (remainingStroke.getModifierKeys() != 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() != 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(final 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 == stroke.getNaturalKey()) |
| || (SWT.CAPS_LOCK == stroke.getNaturalKey()) |
| || (SWT.SCROLL_LOCK == stroke.getNaturalKey())) { |
| return keyStrokes; |
| } |
| |
| if (insertionIndex != -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. |
| final 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)) { |
| final 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 == 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. |
| */ |
| final 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 implements 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)) != 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()) { |
| final KeyStroke[] oldKeyStrokes = getKeySequence() |
| .getKeyStrokes(); |
| final int newKeyStrokesLength = oldKeyStrokes.length - 1; |
| if (newKeyStrokesLength >= 1) { |
| final 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 implements FocusListener { |
| /** The managed filter. We only need one instance. */ |
| private TraversalFilter filter = new TraversalFilter(); |
| |
| /** |
| * Attaches the global traversal filter. |
| * |
| * @param event |
| * Ignored. |
| */ |
| public void focusGained(FocusEvent event) { |
| Display.getCurrent().addFilter(SWT.Traverse, filter); |
| } |
| |
| /** |
| * Detaches the global traversal filter. |
| * |
| * @param event |
| * Ignored. |
| */ |
| public void focusLost(FocusEvent event) { |
| 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 implements 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.equals(newSequence)) { |
| setKeySequence(newSequence); |
| } |
| |
| } catch (ParseException e) { |
| // Abort any cut/paste-driven modifications |
| setKeySequence(getKeySequence()); |
| } |
| } |
| } |
| |
| static { |
| 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 final 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 final int INFINITE = -1; |
| |
| /** |
| * The name of the property representing the current key sequence in this |
| * key sequence widget. |
| */ |
| public static final 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 final List TRAPPED_KEYS; |
| |
| /** |
| * The key filter attached to the underlying widget that traps key events. |
| */ |
| private final KeyTrapListener keyFilter = new KeyTrapListener(); |
| |
| /** |
| * The text of the key sequence -- containing only the complete key strokes. |
| */ |
| private KeySequence keySequence = KeySequence.getInstance(); |
| |
| /** |
| * 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 final 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 final UpdateSequenceListener updateSequenceListener = new 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 KeySequenceText(Text wrappedText) { |
| 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. |
| final Font font = new Font(text.getDisplay(), |
| "Lucida Grande", 13, SWT.NORMAL); //$NON-NLS-1$ |
| text.setFont(font); |
| text.addDisposeListener(new DisposeListener() { |
| public void widgetDisposed(DisposeEvent e) { |
| font.dispose(); |
| } |
| }); |
| } |
| |
| // Add the key listener. |
| text.addListener(SWT.KeyUp, keyFilter); |
| text.addListener(SWT.KeyDown, keyFilter); |
| |
| // Add the focus listener that attaches the global traversal filter. |
| text.addFocusListener(new TraversalFilterManager()); |
| |
| // 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>. |
| */ |
| public final void addPropertyChangeListener( |
| final IPropertyChangeListener listener) { |
| if (listener == null) { |
| return; |
| } |
| |
| if (listeners == null) { |
| listeners = new ArrayList(1); |
| } |
| |
| listeners.add(listener); |
| } |
| |
| /** |
| * Clears the text field and resets all the internal values. |
| */ |
| public void clear() { |
| final 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. |
| * @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(final KeyStroke[] keyStrokes, |
| final boolean allowIncomplete, final 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 = new String(); |
| List currentStrokes = new ArrayList(); |
| int startTextIndex = 0; // keeps track of the start of the stroke |
| final 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() == start, then the cursor is positioned between |
| * strokes (i.e., selection is outside of a stroke). |
| */ |
| int startStrokeIndex; |
| if (string.length() == 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 == end) { |
| 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. |
| */ |
| final int newLength = endStrokeIndex - startStrokeIndex + 1; |
| deletedKeyStrokes[0] = new KeyStroke[newLength]; |
| final KeyStroke startStroke = keyStrokes[startStrokeIndex]; |
| System.arraycopy(keyStrokes, 0, keyStrokes, 0, newLength); |
| |
| /* |
| * Allow the first stroke removed to be replaced by an incomplete |
| * stroke. |
| */ |
| if (allowIncomplete) { |
| final int modifierKeys = startStroke.getModifierKeys(); |
| KeyStroke incompleteStroke = KeyStroke.getInstance(modifierKeys, |
| KeyStroke.NO_KEY); |
| int incompleteStrokeLength = incompleteStroke.format().length(); |
| if ((startTextIndex + incompleteStrokeLength) <= start) { |
| final 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); |
| 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>. |
| */ |
| protected final void firePropertyChangeEvent( |
| final KeySequence oldKeySequence) { |
| if (listeners != null) { |
| final Iterator listenerItr = listeners.iterator(); |
| final PropertyChangeEvent event = new PropertyChangeEvent(this, |
| P_KEY_SEQUENCE, oldKeySequence, getKeySequence()); |
| while (listenerItr.hasNext()) { |
| final IPropertyChangeListener listener = (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 boolean 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 boolean 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. |
| final KeySequence keySequence = getKeySequence(); |
| final KeyStroke[] oldKeyStrokes = keySequence.getKeyStrokes(); |
| final KeyStroke[] newKeyStrokes; |
| if ((hasIncompleteStroke()) && (!keySequence.isEmpty())) { |
| final 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 == -1) { |
| index = 0; |
| } |
| final KeyStroke[] strokes = (deletedKeyStrokes[0] == null) ? new KeyStroke[0] |
| : deletedKeyStrokes[0]; |
| final KeyStroke[] keyStrokes = insertStrokeAt(strokes, 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(final KeyStroke[] keyStrokes, |
| KeyStroke stroke, int index) { |
| final int keyStrokesLength = keyStrokes.length; |
| final KeyStroke currentStroke = (index >= keyStrokesLength) ? null |
| : keyStrokes[index]; |
| if ((currentStroke != null) && (!currentStroke.isComplete())) { |
| int modifierKeys = currentStroke.getModifierKeys(); |
| final int naturalKey = stroke.getNaturalKey(); |
| modifierKeys |= stroke.getModifierKeys(); |
| keyStrokes[index] = KeyStroke.getInstance(modifierKeys, naturalKey); |
| return keyStrokes; |
| } |
| |
| final 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); |
| } |
| 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 boolean 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>. |
| */ |
| public final void removePropertyChangeListener( |
| final IPropertyChangeListener listener) { |
| if ((listener == null) || (listeners == null)) { |
| return; |
| } |
| |
| listeners.remove(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) { |
| final KeySequence oldKeySequence = keySequence; |
| keySequence = newKeySequence; |
| |
| // Trim any extra strokes. |
| if (maxStrokes != INFINITE) { |
| final KeyStroke[] oldKeyStrokes = keySequence.getKeyStrokes(); |
| if (maxStrokes < oldKeyStrokes.length) { |
| final 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 == INFINITE) |
| this.maxStrokes = keyStrokeLimit; |
| else |
| throw new IllegalArgumentException(); |
| |
| // Make sure we are obeying the new limit. |
| setKeySequence(getKeySequence()); |
| } |
| } |