blob: 6ca2aee1c04533ff92cc3805fb2f15abfbabe57a [file] [log] [blame]
package org.unicode.cldr.draft;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import org.unicode.cldr.util.SetComparator;
/**
* A class which represents a particular modifier combination (or combinations
* of combinations).
* <p>
* For example {@code alt+cmd?} gets transformed into a native format consisting of sets of ON modifiers. In this case
* it would get transformed into {@code altL+cmd, altR+cmd, altL+altR+cmd, altL, altR, altL+altR} .
* <p>
* This definition can be expanded across multiple combinations. For example {@code optR+caps? cmd+shift} gets
* transformed into {@code optR+caps, optR,
* cmd+shiftL, cmd+shiftR, cmd+shiftL+shiftR} .
*
* <h1>Usage</h1>
* <p>
* There is a 1 to 1 relationship between a {@link KeyboardModifierSet} and a particular key map (a mapping from
* physical keys to their output).
*
* <pre>
* {@code
* // Create the set from the XML modifier=".." attribute
* ModifierSet modifierSet = ModifierSet.parseSet(<modifier=".." value from XML>);
* // Test if this set is active for a particular input combination provided by the keyboard
* modifierSet.contains(<some combination to test>);
* }
* </pre>
*
* @author rwainman@google.com (Raymond Wainman)
*/
public class KeyboardModifierSet {
/**
* Enum of all possible modifier keys.
*/
public enum Modifier {
cmd, ctrlL, ctrlR, caps, altL, altR, optL, optR, shiftL, shiftR;
}
static final SetComparator<Modifier> SINGLETON_COMPARATOR = new SetComparator<Modifier>();
/** Initial input string */
private final String input;
/** Internal representation of all the possible combination variants */
private final Set<Set<Modifier>> variants;
/**
* Private constructor. See factory {@link #parseSet} method.
*
* @param variants
* A set containing all possible variants of the combination
* provided in the input string.
*/
private KeyboardModifierSet(String input, Set<EnumSet<Modifier>> variants) {
this.input = input;
Set<Set<Modifier>> safe = new TreeSet<Set<Modifier>>(SINGLETON_COMPARATOR);
for (EnumSet<Modifier> item : variants) {
safe.add(Collections.unmodifiableSet(item));
}
this.variants = safe;
}
/**
* Return all possible variants for this combination.
*
* @return Set containing all possible variants.
*/
public Set<Set<Modifier>> getVariants() {
return variants;
}
/**
* Determines if the given combination is valid within this set.
*
* @param combination
* A combination of Modifier elements.
* @return True if the combination is valid, false otherwise.
*/
public boolean contains(EnumSet<Modifier> combination) {
return variants.contains(combination);
}
public String getInput() {
return input;
}
@Override
public String toString() {
return input + " => " + variants;
}
@Override
public boolean equals(Object arg0) {
return arg0 == null ? false : variants.equals(((KeyboardModifierSet) arg0).variants);
}
@Override
public int hashCode() {
return variants.hashCode();
}
/**
* Parse a set containing one or more modifier sets. Each modifier set is
* separated by a single space and modifiers within a modifier set are
* separated by a '+'. For example {@code "ctrl+opt?+caps?+shift? alt+caps+cmd?"} has two modifier sets,
* namely:
* <ul>
* <li>{@code "ctrl+opt?+caps?+shift?"}
* <li>{@code "alt+caps+cmd?"}
* </ul>
* <p>
* The '?' symbol appended to some modifiers indicates that this modifier is optional (it can be ON or OFF).
*
* @param input
* String representing the sets of modifier sets. This string
* must match the format defined in the LDML Keyboard Standard.
* @return A {@link KeyboardModifierSet} containing all possible variants of
* the specified combinations.
* @throws IllegalArgumentException
* if the input string is incorrectly formatted.
*/
public static KeyboardModifierSet parseSet(String input) {
if (input == null) {
throw new IllegalArgumentException("Input string cannot be null");
}
String modifierSetInputs[] = input.trim().split(" ");
Set<EnumSet<Modifier>> variants = new HashSet<EnumSet<Modifier>>();
for (String modifierSetInput : modifierSetInputs) {
variants.addAll(parseSingleSet(modifierSetInput));
}
return new KeyboardModifierSet(input, variants);
}
/**
* Parse a modifier set. The set typically looks something like {@code ctrl+opt?+caps?+shift?} or
* {@code alt+caps+cmd?} and return a set
* containing all possible variants for that particular modifier set.
* <p>
* For example {@code alt+caps+cmd?} gets expanded into {@code alt+caps+cmd?, alt+caps} .
*
* @param input
* The input string representing the modifiers. This String must
* match the format defined in the LDML Keyboard Standard.
* @return {@link KeyboardModifierSet}.
* @throws IllegalArgumentException
* if the input string is incorrectly formatted.
*/
private static Set<EnumSet<Modifier>> parseSingleSet(String input) {
if (input == null) {
throw new IllegalArgumentException("Input string cannot be null");
}
if (input.contains(" ")) {
throw new IllegalArgumentException("Input string contains more than one combination");
}
String modifiers[] = input.trim().split("\\+");
List<EnumSet<Modifier>> variants = new ArrayList<EnumSet<Modifier>>();
variants.add(EnumSet.noneOf(Modifier.class)); // Add an initial set
// which is empty
// Trivial case
if (input.isEmpty()) {
return new HashSet<EnumSet<Modifier>>(variants);
}
for (String modifier : modifiers) {
String modifierElementString = modifier.replace("?", "");
// Attempt to parse the modifier as a parent
if (ModifierParent.isParentModifier(modifierElementString)) {
ModifierParent parentModifier = ModifierParent.valueOf(modifierElementString);
// Keep a collection of the new variants that need to be added
// while iterating over the
// existing ones
Set<EnumSet<Modifier>> newVariants = new HashSet<EnumSet<Modifier>>();
for (EnumSet<Modifier> variant : variants) {
// A parent key gets exploded into {Left, Right, Left+Right}
// or {Left, Right, Left+Right,
// (empty)} if it is a don't care
// {Left}
EnumSet<Modifier> leftVariant = EnumSet.copyOf(variant);
leftVariant.add(parentModifier.leftChild);
newVariants.add(leftVariant);
// {Right}
EnumSet<Modifier> rightVariant = EnumSet.copyOf(variant);
rightVariant.add(parentModifier.rightChild);
newVariants.add(rightVariant);
// {Left+Right}
// If it is a don't care, we need to leave the empty case
// {(empty)}
if (modifier.contains("?")) {
EnumSet<Modifier> bothChildrenVariant = EnumSet.copyOf(variant);
bothChildrenVariant.add(parentModifier.rightChild);
bothChildrenVariant.add(parentModifier.leftChild);
newVariants.add(bothChildrenVariant);
}
// No empty case, it is safe to add to the existing variants
else {
variant.add(parentModifier.rightChild);
variant.add(parentModifier.leftChild);
}
}
variants.addAll(newVariants);
}
// Otherwise, parse as a regular modifier
else {
Modifier modifierElement = Modifier.valueOf(modifierElementString);
// On case, add the modifier to all existing variants
if (!modifier.contains("?")) {
for (EnumSet<Modifier> variant : variants) {
variant.add(modifierElement);
}
}
// Don't care case, make a copy of the existing variants and add
// the new key to it.
else {
List<EnumSet<Modifier>> newVariants = new ArrayList<EnumSet<Modifier>>();
for (EnumSet<Modifier> variant : variants) {
EnumSet<Modifier> newVariant = EnumSet.copyOf(variant);
newVariant.add(modifierElement);
newVariants.add(newVariant);
}
variants.addAll(newVariants);
}
}
}
return new HashSet<EnumSet<Modifier>>(variants);
}
/**
* Enum of all parent modifier keys. Defines the relationships with their
* children.
*/
private enum ModifierParent {
ctrl(Modifier.ctrlL, Modifier.ctrlR), alt(Modifier.altL, Modifier.altR), opt(
Modifier.optL, Modifier.optR), shift(Modifier.shiftL, Modifier.shiftR);
private final Modifier leftChild;
private final Modifier rightChild;
private ModifierParent(Modifier leftChild, Modifier rightChild) {
this.leftChild = leftChild;
this.rightChild = rightChild;
}
/**
* Determines if the String passed in is a valid parent key.
*
* @param modifier
* The modifier string to verify.
* @return True if it is a parent key, false otherwise.
*/
private static boolean isParentModifier(String modifier) {
try {
ModifierParent.valueOf(modifier);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
}
public boolean containsSome(KeyboardModifierSet keyMapModifiers) {
for (Set<Modifier> item : keyMapModifiers.variants) {
if (variants.contains(item)) {
return true;
}
}
return false;
}
public String getShortInput() {
int pos = input.indexOf(' ');
if (pos < 0) return input;
return input.substring(0, pos) + "…";
}
}