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