• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 package org.unicode.cldr.draft;
2 
3 import java.util.ArrayList;
4 import java.util.Collections;
5 import java.util.EnumSet;
6 import java.util.HashSet;
7 import java.util.List;
8 import java.util.Set;
9 import java.util.TreeSet;
10 
11 import org.unicode.cldr.util.SetComparator;
12 
13 /**
14  * A class which represents a particular modifier combination (or combinations
15  * of combinations).
16  * <p>
17  * For example {@code alt+cmd?} gets transformed into a native format consisting of sets of ON modifiers. In this case
18  * it would get transformed into {@code altL+cmd, altR+cmd, altL+altR+cmd, altL, altR, altL+altR} .
19  * <p>
20  * This definition can be expanded across multiple combinations. For example {@code optR+caps? cmd+shift} gets
21  * transformed into {@code optR+caps, optR,
22  * cmd+shiftL, cmd+shiftR, cmd+shiftL+shiftR} .
23  *
24  * <h1>Usage</h1>
25  * <p>
26  * There is a 1 to 1 relationship between a {@link KeyboardModifierSet} and a particular key map (a mapping from
27  * physical keys to their output).
28  *
29  * <pre>
30  * {@code
31  * // Create the set from the XML modifier=".." attribute
32  * ModifierSet modifierSet = ModifierSet.parseSet(<modifier=".." value from XML>);
33  * // Test if this set is active for a particular input combination provided by the keyboard
34  * modifierSet.contains(<some combination to test>);
35  * }
36  * </pre>
37  *
38  * @author rwainman@google.com (Raymond Wainman)
39  */
40 public class KeyboardModifierSet {
41     /**
42      * Enum of all possible modifier keys.
43      */
44     public enum Modifier {
45         cmd, ctrlL, ctrlR, caps, altL, altR, optL, optR, shiftL, shiftR;
46     }
47 
48     static final SetComparator<Modifier> SINGLETON_COMPARATOR = new SetComparator<>();
49 
50     /** Initial input string */
51     private final String input;
52     /** Internal representation of all the possible combination variants */
53     private final Set<Set<Modifier>> variants;
54 
55     /**
56      * Private constructor. See factory {@link #parseSet} method.
57      *
58      * @param variants
59      *            A set containing all possible variants of the combination
60      *            provided in the input string.
61      */
KeyboardModifierSet(String input, Set<EnumSet<Modifier>> variants)62     private KeyboardModifierSet(String input, Set<EnumSet<Modifier>> variants) {
63         this.input = input;
64         Set<Set<Modifier>> safe = new TreeSet<>(SINGLETON_COMPARATOR);
65         for (EnumSet<Modifier> item : variants) {
66             safe.add(Collections.unmodifiableSet(item));
67         }
68         this.variants = safe;
69     }
70 
71     /**
72      * Return all possible variants for this combination.
73      *
74      * @return Set containing all possible variants.
75      */
getVariants()76     public Set<Set<Modifier>> getVariants() {
77         return variants;
78     }
79 
80     /**
81      * Determines if the given combination is valid within this set.
82      *
83      * @param combination
84      *            A combination of Modifier elements.
85      * @return True if the combination is valid, false otherwise.
86      */
contains(EnumSet<Modifier> combination)87     public boolean contains(EnumSet<Modifier> combination) {
88         return variants.contains(combination);
89     }
90 
getInput()91     public String getInput() {
92         return input;
93     }
94 
95     @Override
toString()96     public String toString() {
97         return input + " => " + variants;
98     }
99 
100     @Override
equals(Object arg0)101     public boolean equals(Object arg0) {
102         return arg0 == null ? false : variants.equals(((KeyboardModifierSet) arg0).variants);
103     }
104 
105     @Override
hashCode()106     public int hashCode() {
107         return variants.hashCode();
108     }
109 
110     /**
111      * Parse a set containing one or more modifier sets. Each modifier set is
112      * separated by a single space and modifiers within a modifier set are
113      * separated by a '+'. For example {@code "ctrl+opt?+caps?+shift? alt+caps+cmd?"} has two modifier sets,
114      * namely:
115      * <ul>
116      * <li>{@code "ctrl+opt?+caps?+shift?"}
117      * <li>{@code "alt+caps+cmd?"}
118      * </ul>
119      * <p>
120      * The '?' symbol appended to some modifiers indicates that this modifier is optional (it can be ON or OFF).
121      *
122      * @param input
123      *            String representing the sets of modifier sets. This string
124      *            must match the format defined in the LDML Keyboard Standard.
125      * @return A {@link KeyboardModifierSet} containing all possible variants of
126      *         the specified combinations.
127      * @throws IllegalArgumentException
128      *             if the input string is incorrectly formatted.
129      */
parseSet(String input)130     public static KeyboardModifierSet parseSet(String input) {
131         if (input == null) {
132             throw new IllegalArgumentException("Input string cannot be null");
133         }
134 
135         String modifierSetInputs[] = input.trim().split(" ");
136         Set<EnumSet<Modifier>> variants = new HashSet<>();
137         for (String modifierSetInput : modifierSetInputs) {
138             variants.addAll(parseSingleSet(modifierSetInput));
139         }
140         return new KeyboardModifierSet(input, variants);
141     }
142 
143     /**
144      * Parse a modifier set. The set typically looks something like {@code ctrl+opt?+caps?+shift?} or
145      * {@code alt+caps+cmd?} and return a set
146      * containing all possible variants for that particular modifier set.
147      * <p>
148      * For example {@code alt+caps+cmd?} gets expanded into {@code alt+caps+cmd?, alt+caps} .
149      *
150      * @param input
151      *            The input string representing the modifiers. This String must
152      *            match the format defined in the LDML Keyboard Standard.
153      * @return {@link KeyboardModifierSet}.
154      * @throws IllegalArgumentException
155      *             if the input string is incorrectly formatted.
156      */
parseSingleSet(String input)157     private static Set<EnumSet<Modifier>> parseSingleSet(String input) {
158         if (input == null) {
159             throw new IllegalArgumentException("Input string cannot be null");
160         }
161         if (input.contains(" ")) {
162             throw new IllegalArgumentException("Input string contains more than one combination");
163         }
164 
165         String modifiers[] = input.trim().split("\\+");
166 
167         List<EnumSet<Modifier>> variants = new ArrayList<>();
168         variants.add(EnumSet.noneOf(Modifier.class)); // Add an initial set
169                                                       // which is empty
170 
171         // Trivial case
172         if (input.isEmpty()) {
173             return new HashSet<>(variants);
174         }
175 
176         for (String modifier : modifiers) {
177             String modifierElementString = modifier.replace("?", "");
178 
179             // Attempt to parse the modifier as a parent
180             if (ModifierParent.isParentModifier(modifierElementString)) {
181                 ModifierParent parentModifier = ModifierParent.valueOf(modifierElementString);
182 
183                 // Keep a collection of the new variants that need to be added
184                 // while iterating over the
185                 // existing ones
186                 Set<EnumSet<Modifier>> newVariants = new HashSet<>();
187                 for (EnumSet<Modifier> variant : variants) {
188                     // A parent key gets exploded into {Left, Right, Left+Right}
189                     // or {Left, Right, Left+Right,
190                     // (empty)} if it is a don't care
191 
192                     // {Left}
193                     EnumSet<Modifier> leftVariant = EnumSet.copyOf(variant);
194                     leftVariant.add(parentModifier.leftChild);
195                     newVariants.add(leftVariant);
196 
197                     // {Right}
198                     EnumSet<Modifier> rightVariant = EnumSet.copyOf(variant);
199                     rightVariant.add(parentModifier.rightChild);
200                     newVariants.add(rightVariant);
201 
202                     // {Left+Right}
203                     // If it is a don't care, we need to leave the empty case
204                     // {(empty)}
205                     if (modifier.contains("?")) {
206                         EnumSet<Modifier> bothChildrenVariant = EnumSet.copyOf(variant);
207                         bothChildrenVariant.add(parentModifier.rightChild);
208                         bothChildrenVariant.add(parentModifier.leftChild);
209                         newVariants.add(bothChildrenVariant);
210                     }
211                     // No empty case, it is safe to add to the existing variants
212                     else {
213                         variant.add(parentModifier.rightChild);
214                         variant.add(parentModifier.leftChild);
215                     }
216                 }
217                 variants.addAll(newVariants);
218             }
219             // Otherwise, parse as a regular modifier
220             else {
221                 Modifier modifierElement = Modifier.valueOf(modifierElementString);
222                 // On case, add the modifier to all existing variants
223                 if (!modifier.contains("?")) {
224                     for (EnumSet<Modifier> variant : variants) {
225                         variant.add(modifierElement);
226                     }
227                 }
228                 // Don't care case, make a copy of the existing variants and add
229                 // the new key to it.
230                 else {
231                     List<EnumSet<Modifier>> newVariants = new ArrayList<>();
232                     for (EnumSet<Modifier> variant : variants) {
233                         EnumSet<Modifier> newVariant = EnumSet.copyOf(variant);
234                         newVariant.add(modifierElement);
235                         newVariants.add(newVariant);
236                     }
237                     variants.addAll(newVariants);
238                 }
239             }
240         }
241 
242         return new HashSet<>(variants);
243     }
244 
245     /**
246      * Enum of all parent modifier keys. Defines the relationships with their
247      * children.
248      */
249     private enum ModifierParent {
250         ctrl(Modifier.ctrlL, Modifier.ctrlR), alt(Modifier.altL, Modifier.altR), opt(
251             Modifier.optL, Modifier.optR), shift(Modifier.shiftL, Modifier.shiftR);
252 
253         private final Modifier leftChild;
254         private final Modifier rightChild;
255 
ModifierParent(Modifier leftChild, Modifier rightChild)256         private ModifierParent(Modifier leftChild, Modifier rightChild) {
257             this.leftChild = leftChild;
258             this.rightChild = rightChild;
259         }
260 
261         /**
262          * Determines if the String passed in is a valid parent key.
263          *
264          * @param modifier
265          *            The modifier string to verify.
266          * @return True if it is a parent key, false otherwise.
267          */
isParentModifier(String modifier)268         private static boolean isParentModifier(String modifier) {
269             try {
270                 ModifierParent.valueOf(modifier);
271                 return true;
272             } catch (IllegalArgumentException e) {
273                 return false;
274             }
275         }
276     }
277 
containsSome(KeyboardModifierSet keyMapModifiers)278     public boolean containsSome(KeyboardModifierSet keyMapModifiers) {
279         for (Set<Modifier> item : keyMapModifiers.variants) {
280             if (variants.contains(item)) {
281                 return true;
282             }
283         }
284         return false;
285     }
286 
getShortInput()287     public String getShortInput() {
288         int pos = input.indexOf(' ');
289         if (pos < 0) return input;
290         return input.substring(0, pos) + "…";
291     }
292 }
293