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