Spec-Zone .ru
спецификации, руководства, описания, API
001/*
002 * Copyright (c) 2011, 2013, Oracle and/or its affiliates. All rights reserved.
003 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004 *
005 * This code is free software; you can redistribute it and/or modify it
006 * under the terms of the GNU General Public License version 2 only, as
007 * published by the Free Software Foundation.  Oracle designates this
008 * particular file as subject to the "Classpath" exception as provided
009 * by Oracle in the LICENSE file that accompanied this code.
010 *
011 * This code is distributed in the hope that it will be useful, but WITHOUT
012 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
014 * version 2 for more details (a copy is included in the LICENSE file that
015 * accompanied this code).
016 *
017 * You should have received a copy of the GNU General Public License version
018 * 2 along with this work; if not, write to the Free Software Foundation,
019 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020 *
021 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
022 * or visit www.oracle.com if you need additional information or have any
023 * questions.
024 */
025
026package javafx.scene.input;
027
028import com.sun.javafx.tk.Toolkit;
029import java.util.ArrayList;
030import java.util.List;
031
032// PENDING_DOC_REVIEW
033/**
034 * Represents a combination of keys which are used in keyboard shortcuts.
035 * A key combination consists of a main key and a set of modifier keys. The main
036 * key can be specified by its key code - {@code KeyCodeCombination} or key
037 * character - {@code KeyCharacterCombination}. A modifier key is {@code shift},
038 * {@code control}, {@code alt}, {@code meta} or {@code shortcut} and can be
039 * defined as {@code DOWN}, {@code UP} or {@code ANY}.
040 * <p>
041 * The {@code shortcut} modifier is used to represent the modifier key which is
042 * used commonly in keyboard shortcuts on the host platform. This is for
043 * example {@code control} on Windows and {@code meta} (command key) on Mac.
044 * By using {@code shortcut} key modifier developers can create platform
045 * independent shortcuts. So the "Shortcut+C" key combination is handled
046 * internally as "Ctrl+C" on Windows and "Meta+C" on Mac.
047 */
048public abstract class KeyCombination {
049
050    /** Modifier which specifies that the {@code shift} key must be down. */
051    public static final Modifier SHIFT_DOWN =
052            new Modifier(KeyCode.SHIFT, ModifierValue.DOWN);
053    /**
054     * Modifier which specifies that the {@code shift} key can be either up or
055     * down.
056     */
057    public static final Modifier SHIFT_ANY =
058            new Modifier(KeyCode.SHIFT, ModifierValue.ANY);
059    /** Modifier which specifies that the {@code control} key must be down. */
060    public static final Modifier CONTROL_DOWN =
061            new Modifier(KeyCode.CONTROL, ModifierValue.DOWN);
062    /**
063     * Modifier which specifies that the {@code control} key can be either up or
064     * down.
065     */
066    public static final Modifier CONTROL_ANY =
067            new Modifier(KeyCode.CONTROL, ModifierValue.ANY);
068    /** Modifier which specifies that the {@code alt} key must be down. */
069    public static final Modifier ALT_DOWN =
070            new Modifier(KeyCode.ALT, ModifierValue.DOWN);
071    /**
072     * Modifier which specifies that the {@code alt} key can be either up or
073     * down.
074     */
075    public static final Modifier ALT_ANY =
076            new Modifier(KeyCode.ALT, ModifierValue.ANY);
077    /** Modifier which specifies that the {@code meta} key must be down. */
078    public static final Modifier META_DOWN =
079            new Modifier(KeyCode.META, ModifierValue.DOWN);
080    /**
081     * Modifier which specifies that the {@code meta} key can be either up or
082     * down.
083     */
084    public static final Modifier META_ANY =
085            new Modifier(KeyCode.META, ModifierValue.ANY);
086    /** Modifier which specifies that the {@code shortcut} key must be down. */
087    public static final Modifier SHORTCUT_DOWN =
088            new Modifier(KeyCode.SHORTCUT, ModifierValue.DOWN);
089    /**
090     * Modifier which specifies that the {@code shortcut} key can be either up
091     * or down.
092     */
093    public static final Modifier SHORTCUT_ANY =
094            new Modifier(KeyCode.SHORTCUT, ModifierValue.ANY);
095
096    private static final Modifier[] POSSIBLE_MODIFIERS = {
097        SHIFT_DOWN, SHIFT_ANY,
098        CONTROL_DOWN, CONTROL_ANY,
099        ALT_DOWN, ALT_ANY,
100        META_DOWN, META_ANY,
101        SHORTCUT_DOWN, SHORTCUT_ANY
102    };
103    /** The state of the {@code shift} key in this key combination. */
104    private final ModifierValue shift;
105
106    /**
107     * The state of the {@code shift} key in this key combination.
108     * @return The state of the {@code shift} key in this key combination
109     */
110    public final ModifierValue getShift() {
111        return shift;
112    }
113    /** The state of the {@code control} key in this key combination. */
114    private final ModifierValue control;
115
116    /**
117     * The state of the {@code control} key in this key combination.
118     * @return The state of the {@code control} key in this key combination
119     */
120    public final ModifierValue getControl() {
121        return control;
122    }
123    /** The state of the {@code alt} key in this key combination. */
124    private final ModifierValue alt;
125
126    /**
127     * The state of the {@code alt} key in this key combination.
128     * @return The state of the {@code alt} key in this key combination.
129     */
130    public final ModifierValue getAlt() {
131        return alt;
132    }
133    /** The state of the {@code meta} key in this key combination. */
134    private final ModifierValue meta;
135
136    /**
137     * The state of the {@code meta} key in this key combination.
138     * @return The state of the {@code meta} key in this key combination
139     */
140    public final ModifierValue getMeta() {
141        return meta;
142    }
143
144    /** The state of the {@code shortcut} key in this key combination. */
145    private final ModifierValue shortcut;
146
147    /**
148     * The state of the {@code shortcut} key in this key combination.
149     * @return The state of the {@code shortcut} key in this key combination
150     */
151    public final ModifierValue getShortcut() {
152        return shortcut;
153    }
154
155    /**
156     * Constructs a {@code KeyCombination} with an explicit specification
157     * of all modifier keys. Each modifier key can be set to {@code DOWN},
158     * {@code UP} or {@code ANY}.
159     *
160     * @param shift the value of the {@code shift} modifier key
161     * @param control the value of the {@code control} modifier key
162     * @param alt the value of the {@code alt} modifier key
163     * @param meta the value of the {@code meta} modifier key
164     * @param shortcut the value of the {@code shortcut} modifier key
165     */
166    protected KeyCombination(final ModifierValue shift,
167                             final ModifierValue control,
168                             final ModifierValue alt,
169                             final ModifierValue meta,
170                             final ModifierValue shortcut) {
171        if ((shift == null)
172                || (control == null)
173                || (alt == null)
174                || (meta == null)
175                || (shortcut == null)) {
176            throw new NullPointerException("Modifier value must not be null!");
177        }
178
179        this.shift = shift;
180        this.control = control;
181        this.alt = alt;
182        this.meta = meta;
183        this.shortcut = shortcut;
184    }
185
186    /**
187     * Constructs a {@code KeyCombination} with the specified list of modifiers.
188     * All modifier keys which are not explicitly listed are set to the
189     * default {@code UP} value.
190     * <p>
191     * All possible modifiers which change the default modifier value are
192     * defined as constants in the {@code KeyCombination} class.
193     *
194     * @param modifiers the list of modifier keys and their corresponding values
195     */
196    protected KeyCombination(final Modifier... modifiers) {
197        this(getModifierValue(modifiers, KeyCode.SHIFT),
198             getModifierValue(modifiers, KeyCode.CONTROL),
199             getModifierValue(modifiers, KeyCode.ALT),
200             getModifierValue(modifiers, KeyCode.META),
201             getModifierValue(modifiers, KeyCode.SHORTCUT));
202    }
203
204    /**
205     * Tests whether this key combination matches the combination in the given
206     * {@code KeyEvent}.
207     * <p>
208     * The implementation of this method in the {@code KeyCombination} class
209     * does only a partial test with the modifier keys. This method is
210     * overridden in subclasses to include the main key in the test.
211     *
212     * @param event the key event
213     * @return {@code true} if the key combinations match, {@code false}
214     *      otherwise
215     */
216    public boolean match(final KeyEvent event) {
217        final KeyCode shortcutKey =
218                Toolkit.getToolkit().getPlatformShortcutKey();
219        return test(KeyCode.SHIFT, shift, shortcutKey, shortcut,
220                    event.isShiftDown())
221                && test(KeyCode.CONTROL, control, shortcutKey, shortcut,
222                        event.isControlDown())
223                && test(KeyCode.ALT, alt, shortcutKey, shortcut,
224                        event.isAltDown())
225                && test(KeyCode.META, meta, shortcutKey, shortcut,
226                        event.isMetaDown());
227    }
228
229    /**
230     * Returns a string representation of this {@code KeyCombination}.
231     * <p>
232     * The string representation consists of sections separated by plus
233     * characters. Each section specifies either a modifier key or the main key.
234     * <p>
235     * A modifier key section contains the {@code KeyCode} name of a modifier
236     * key. It can be prefixed with the {@code Ignored} keyword. A non-prefixed
237     * modifier key implies its {@code DOWN} value while the prefixed version
238     * implies the {@code ANY} (ignored) value. If some modifier key is not
239     * specified in the string at all, it means it has the default {@code UP}
240     * value.
241     * <p>
242     * The format of the main key section of the key combination string depends
243     * on the {@code KeyCombination} subclass. It is either the key code name
244     * for {@code KeyCodeCombination} or the single quoted key character for
245     * {@code KeyCharacterCombination}.
246     * <p>
247     * Examples of {@code KeyCombination} string representations:
248
249<PRE>
250"Ctrl+Alt+Q"
251"Ignore Shift+Ctrl+A"
252"Alt+'w'"
253</PRE>
254
255     * @return the string representation of this {@code KeyCombination}
256     */
257    public String getName() {
258        StringBuilder sb = new StringBuilder();
259        addModifiersIntoString(sb);
260
261        return sb.toString();
262    }
263
264    /**
265     * Tests whether this {@code KeyCombination} equals to the specified object.
266     *
267     * @param obj the object to compare to
268     * @return {@code true} if the objects are equal, {@code false} otherwise
269     */
270    @Override
271    public boolean equals(final Object obj) {
272        if (!(obj instanceof KeyCombination)) {
273            return false;
274        }
275
276        final KeyCombination other = (KeyCombination) obj;
277        return (shift == other.shift)
278                && (control == other.control)
279                && (alt == other.alt)
280                && (meta == other.meta)
281                && (shortcut == other.shortcut);
282    }
283
284    /**
285     * Returns a hash code value for this {@code KeyCombination}.
286     *
287     * @return the hash code value
288     */
289    @Override
290    public int hashCode() {
291        int hash = 7;
292
293        hash = 23 * hash + shift.hashCode();
294        hash = 23 * hash + control.hashCode();
295        hash = 23 * hash + alt.hashCode();
296        hash = 23 * hash + meta.hashCode();
297        hash = 23 * hash + shortcut.hashCode();
298
299        return hash;
300    }
301
302    /**
303     * Returns a string representation of this object. Implementation returns
304     * the result of the {@code getName()} call.
305     *
306     * @return the string representation of this {@code KeyCombination}
307     */
308    @Override
309    public String toString() {
310        return getName();
311    }
312
313    /**
314     * Constructs a new {@code KeyCombination} from the specified string. The
315     * string should be in the same format as produced by the {@code getName}
316     * method.
317     * <p>
318     * If the main key section string is quoted in single quotes the method
319     * creates a new {@code KeyCharacterCombination} for the unquoted substring.
320     * Otherwise it finds the key code which name corresponds to the main key
321     * section string and creates a {@code KeyCodeCombination} for it. If this
322     * can't be done, it falls back to the {@code KeyCharacterCombination}.
323     *
324     * @param value the string which represents the requested key combination
325     * @return the constructed {@code KeyCombination}
326     */
327    public static KeyCombination valueOf(String value) {
328        final List<Modifier> modifiers = new ArrayList<Modifier>(4);
329
330        final String[] tokens = splitName(value);
331
332        KeyCode keyCode = null;
333        String keyCharacter = null;
334        for (String token : tokens) {
335
336            if ((token.length() > 2)
337                    && (token.charAt(0) == '\'')
338                    && (token.charAt(token.length() - 1) == '\'')) {
339                if ((keyCode != null) || (keyCharacter != null)) {
340                    throw new IllegalArgumentException(
341                            "Cannot parse key binding " + value);
342                }
343
344                keyCharacter = token.substring(1, token.length() - 1)
345                        .replace("\\'", "'");
346                continue;
347            }
348
349            final String normalizedToken = normalizeToken(token);
350
351            final Modifier modifier = getModifier(normalizedToken);
352            if (modifier != null) {
353                modifiers.add(modifier);
354                continue;
355            }
356
357            if ((keyCode != null) || (keyCharacter != null)) {
358                throw new IllegalArgumentException(
359                        "Cannot parse key binding " + value);
360            }
361
362            keyCode = KeyCode.getKeyCode(normalizedToken);
363            if (keyCode == null) {
364                keyCharacter = token;
365            }
366        }
367
368        if ((keyCode == null) && (keyCharacter == null)) {
369            throw new IllegalArgumentException(
370                    "Cannot parse key binding " + value);
371        }
372
373        final Modifier[] modifierArray =
374                modifiers.toArray(new Modifier[modifiers.size()]);
375        return (keyCode != null)
376                    ? new KeyCodeCombination(keyCode, modifierArray)
377                    : new KeyCharacterCombination(keyCharacter, modifierArray);
378    }
379    
380    /**
381     * Constructs a new {@code KeyCombination} from the specified string. This
382     * method simply delegates to {@link #valueOf(String)}.
383     *
384     * @param name the string which represents the requested key combination
385     * @return the constructed {@code KeyCombination}
386     *
387     * @see #valueOf(String)
388     */
389    public static KeyCombination keyCombination(String name) {
390        return valueOf(name);
391    }
392
393    /**
394     * This class represents a pair of modifier key and its value.
395     */
396    public static final class Modifier {
397        private final KeyCode key;
398        private final ModifierValue value;
399
400        private Modifier(final KeyCode key,
401                         final ModifierValue value) {
402            this.key = key;
403            this.value = value;
404        }
405
406        /**
407         * Gets the modifier key of this {@code Modifier}.
408         *
409         * @return the modifier key
410         */
411        public KeyCode getKey() {
412            return key;
413        }
414
415        /**
416         * Gets the modifier value of this {@code Modifier}.
417         *
418         * @return the modifier value
419         */
420        public ModifierValue getValue() {
421            return value;
422        }
423
424        /**
425         * Returns a string representation of the modifier.
426         * @return a string representation of the modifier
427         */
428        @Override
429        public String toString() {
430            return ((value == ModifierValue.ANY) ? "Ignore " : "")
431                       + key.getName();
432
433        }
434    }
435
436    /**
437     * {@code ModifierValue} specifies state of modifier keys.
438     */
439    public static enum ModifierValue {
440        /** Constant which indicates that the modifier key must be down. */
441        DOWN,
442        /** Constant which indicates that the modifier key must be up. */
443        UP,
444        /**
445         * Constant which indicates that the modifier key can be either up or
446         * down.
447         */
448        ANY
449    }
450
451    private void addModifiersIntoString(final StringBuilder sb) {
452        addModifierIntoString(sb, KeyCode.SHIFT, shift);
453        addModifierIntoString(sb, KeyCode.CONTROL, control);
454        addModifierIntoString(sb, KeyCode.ALT, alt);
455        addModifierIntoString(sb, KeyCode.META, meta);
456        addModifierIntoString(sb, KeyCode.SHORTCUT, shortcut);
457    }
458
459    private static void addModifierIntoString(
460            final StringBuilder sb,
461            final KeyCode modifierKey,
462            final ModifierValue modifierValue) {
463
464        if (modifierValue == ModifierValue.UP) {
465            return;
466        }
467
468        if (sb.length() > 0) {
469            sb.append("+");
470        }
471
472        if (modifierValue == ModifierValue.ANY) {
473            sb.append("Ignore ");
474        }
475
476        sb.append(modifierKey.getName());
477    }
478
479    private static boolean test(final KeyCode testedModifierKey,
480                                final ModifierValue testedModifierValue,
481                                final KeyCode shortcutModifierKey,
482                                final ModifierValue shortcutModifierValue,
483                                final boolean isKeyDown) {
484        final ModifierValue finalModifierValue =
485                (testedModifierKey == shortcutModifierKey)
486                        ? resolveModifierValue(testedModifierValue,
487                                               shortcutModifierValue)
488                        : testedModifierValue;
489
490        return test(finalModifierValue, isKeyDown);
491
492    }
493
494    private static boolean test(final ModifierValue modifierValue,
495                                final boolean isDown) {
496        switch (modifierValue) {
497            case DOWN:
498                return isDown;
499
500            case UP:
501                return !isDown;
502
503            case ANY:
504            default:
505                return true;
506        }
507    }
508
509    private static ModifierValue resolveModifierValue(
510            final ModifierValue firstValue,
511            final ModifierValue secondValue) {
512        if ((firstValue == ModifierValue.DOWN)
513                || (secondValue == ModifierValue.DOWN)) {
514            return ModifierValue.DOWN;
515        }
516
517        if ((firstValue == ModifierValue.ANY)
518                || (secondValue == ModifierValue.ANY)) {
519            return ModifierValue.ANY;
520        }
521
522        return ModifierValue.UP;
523    }
524
525    static Modifier getModifier(final String name) {
526        for (final Modifier modifier: POSSIBLE_MODIFIERS) {
527            if (modifier.toString().equals(name)) {
528                return modifier;
529            }
530        }
531
532        return null;
533    }
534
535    private static ModifierValue getModifierValue(
536            final Modifier[] modifiers,
537            final KeyCode modifierKey) {
538        ModifierValue modifierValue = ModifierValue.UP;
539        for (final Modifier modifier: modifiers) {
540            if (modifier == null) {
541                throw new NullPointerException("Modifier must not be null!");
542            }
543
544            if (modifier.getKey() == modifierKey) {
545                if (modifierValue != ModifierValue.UP) {
546                    throw new IllegalArgumentException(
547                            (modifier.getValue() != modifierValue)
548                                    ? "Conflicting modifiers specified!"
549                                    : "Duplicate modifiers specified!");
550                }
551
552                modifierValue = modifier.getValue();
553            }
554        }
555
556        return modifierValue;
557    }
558
559    private static String normalizeToken(final String token) {
560        final String[] words = token.split("\\s+");
561        final StringBuilder sb = new StringBuilder();
562
563        for (final String word: words) {
564            if (sb.length() > 0) {
565                sb.append(' ');
566            }
567
568            sb.append(word.substring(0, 1).toUpperCase());
569            sb.append(word.substring(1).toLowerCase());
570        }
571
572        return sb.toString();
573    }
574
575    private static String[] splitName(String name) {
576        List<String> tokens = new ArrayList<String>();
577        char[] chars = name.trim().toCharArray();
578
579        final int STATE_BASIC = 0;      // general text
580        final int STATE_WHITESPACE = 1; // spaces found
581        final int STATE_SEPARATOR = 2;  // plus found
582        final int STATE_QUOTED = 3;     // quoted text
583
584        int state = STATE_BASIC;
585        int tokenStart = 0;
586        int tokenEnd = -1;
587
588        for (int i = 0; i < chars.length; i++) {
589            char c = chars[i];
590            switch(state) {
591                case STATE_BASIC:
592                    switch(c) {
593                        case ' ':
594                        case '\t':
595                        case '\n':
596                        case '\f':
597                        case '\r':
598                        case '\u000B':
599                            tokenEnd = i;
600                            state = STATE_WHITESPACE;
601                            break;
602                        case '+':
603                            tokenEnd = i;
604                            state = STATE_SEPARATOR;
605                            break;
606                        case '\'':
607                            if (i == 0 || chars[i - 1] != '\\') {
608                                state = STATE_QUOTED;
609                            }
610                            break;
611                        default:
612                            break;
613                    }
614                    break;
615                case STATE_WHITESPACE:
616                    switch(c) {
617                        case ' ':
618                        case '\t':
619                        case '\n':
620                        case '\f':
621                        case '\r':
622                        case '\u000B':
623                            break;
624                        case '+':
625                            state = STATE_SEPARATOR;
626                            break;
627                        case '\'':
628                            state = STATE_QUOTED;
629                            tokenEnd = -1;
630                            break;
631                        default:
632                            state = STATE_BASIC;
633                            tokenEnd = -1;
634                            break;
635                    }
636                    break;
637                case STATE_SEPARATOR:
638                    switch(c) {
639                        case ' ':
640                        case '\t':
641                        case '\n':
642                        case '\f':
643                        case '\r':
644                        case '\u000B':
645                            break;
646                        case '+':
647                            throw new IllegalArgumentException(
648                                    "Cannot parse key binding " + name);
649                        default:
650                            if (tokenEnd <= tokenStart) {
651                                throw new IllegalArgumentException(
652                                        "Cannot parse key binding " + name);
653                            }
654                            tokens.add(new String(chars,
655                                    tokenStart, tokenEnd - tokenStart));
656                            tokenStart = i;
657                            tokenEnd = -1;
658                            state = (c == '\'' ? STATE_QUOTED : STATE_BASIC);
659                            break;
660                    }
661                    break;
662                case STATE_QUOTED:
663                    if (c == '\'' && chars[i - 1] != '\\') {
664                        state = STATE_BASIC;
665                    }
666                    break;
667            }
668        }
669
670        switch(state) {
671            case STATE_BASIC:
672            case STATE_WHITESPACE:
673                tokens.add(new String(chars,
674                        tokenStart, chars.length - tokenStart));
675                break;
676            case STATE_SEPARATOR:
677            case STATE_QUOTED:
678                throw new IllegalArgumentException(
679                        "Cannot parse key binding " + name);
680        }
681
682        return tokens.toArray(new String[tokens.size()]);
683    }
684}