javafx.scene.input.KeyCombination Maven / Gradle / Ivy
/*
* Copyright (c) 2011, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package javafx.scene.input;
import com.sun.javafx.tk.Toolkit;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
// PENDING_DOC_REVIEW
/**
* Represents a combination of keys which are used in keyboard shortcuts.
* A key combination consists of a main key and a set of modifier keys. The main
* key can be specified by its key code - {@code KeyCodeCombination} or key
* character - {@code KeyCharacterCombination}. A modifier key is {@code shift},
* {@code control}, {@code alt}, {@code meta} or {@code shortcut} and can be
* defined as {@code DOWN}, {@code UP} or {@code ANY}.
*
* The {@code shortcut} modifier is used to represent the modifier key which is
* used commonly in keyboard shortcuts on the host platform. This is for
* example {@code control} on Windows and {@code meta} (command key) on Mac.
* By using {@code shortcut} key modifier developers can create platform
* independent shortcuts. So the "Shortcut+C" key combination is handled
* internally as "Ctrl+C" on Windows and "Meta+C" on Mac.
* @since JavaFX 2.0
*/
public abstract class KeyCombination {
/** Modifier which specifies that the {@code shift} key must be down. */
public static final Modifier SHIFT_DOWN =
new Modifier(KeyCode.SHIFT, ModifierValue.DOWN);
/**
* Modifier which specifies that the {@code shift} key can be either up or
* down.
*/
public static final Modifier SHIFT_ANY =
new Modifier(KeyCode.SHIFT, ModifierValue.ANY);
/** Modifier which specifies that the {@code control} key must be down. */
public static final Modifier CONTROL_DOWN =
new Modifier(KeyCode.CONTROL, ModifierValue.DOWN);
/**
* Modifier which specifies that the {@code control} key can be either up or
* down.
*/
public static final Modifier CONTROL_ANY =
new Modifier(KeyCode.CONTROL, ModifierValue.ANY);
/** Modifier which specifies that the {@code alt} key must be down. */
public static final Modifier ALT_DOWN =
new Modifier(KeyCode.ALT, ModifierValue.DOWN);
/**
* Modifier which specifies that the {@code alt} key can be either up or
* down.
*/
public static final Modifier ALT_ANY =
new Modifier(KeyCode.ALT, ModifierValue.ANY);
/** Modifier which specifies that the {@code meta} key must be down. */
public static final Modifier META_DOWN =
new Modifier(KeyCode.META, ModifierValue.DOWN);
/**
* Modifier which specifies that the {@code meta} key can be either up or
* down.
*/
public static final Modifier META_ANY =
new Modifier(KeyCode.META, ModifierValue.ANY);
/** Modifier which specifies that the {@code shortcut} key must be down. */
public static final Modifier SHORTCUT_DOWN =
new Modifier(KeyCode.SHORTCUT, ModifierValue.DOWN);
/**
* Modifier which specifies that the {@code shortcut} key can be either up
* or down.
*/
public static final Modifier SHORTCUT_ANY =
new Modifier(KeyCode.SHORTCUT, ModifierValue.ANY);
private static final Modifier[] POSSIBLE_MODIFIERS = {
SHIFT_DOWN, SHIFT_ANY,
CONTROL_DOWN, CONTROL_ANY,
ALT_DOWN, ALT_ANY,
META_DOWN, META_ANY,
SHORTCUT_DOWN, SHORTCUT_ANY
};
/**
* A KeyCombination that will match with no events.
*/
public static final KeyCombination NO_MATCH = new KeyCombination() {
@Override
public boolean match(KeyEvent e) {
return false;
}
};
/** The state of the {@code shift} key in this key combination. */
private final ModifierValue shift;
/**
* The state of the {@code shift} key in this key combination.
* @return The state of the {@code shift} key in this key combination
*/
public final ModifierValue getShift() {
return shift;
}
/** The state of the {@code control} key in this key combination. */
private final ModifierValue control;
/**
* The state of the {@code control} key in this key combination.
* @return The state of the {@code control} key in this key combination
*/
public final ModifierValue getControl() {
return control;
}
/** The state of the {@code alt} key in this key combination. */
private final ModifierValue alt;
/**
* The state of the {@code alt} key in this key combination.
* @return The state of the {@code alt} key in this key combination.
*/
public final ModifierValue getAlt() {
return alt;
}
/** The state of the {@code meta} key in this key combination. */
private final ModifierValue meta;
/**
* The state of the {@code meta} key in this key combination.
* @return The state of the {@code meta} key in this key combination
*/
public final ModifierValue getMeta() {
return meta;
}
/** The state of the {@code shortcut} key in this key combination. */
private final ModifierValue shortcut;
/**
* The state of the {@code shortcut} key in this key combination.
* @return The state of the {@code shortcut} key in this key combination
*/
public final ModifierValue getShortcut() {
return shortcut;
}
/**
* Constructs a {@code KeyCombination} with an explicit specification
* of all modifier keys. Each modifier key can be set to {@code DOWN},
* {@code UP} or {@code ANY}.
*
* @param shift the value of the {@code shift} modifier key
* @param control the value of the {@code control} modifier key
* @param alt the value of the {@code alt} modifier key
* @param meta the value of the {@code meta} modifier key
* @param shortcut the value of the {@code shortcut} modifier key
*/
protected KeyCombination(final ModifierValue shift,
final ModifierValue control,
final ModifierValue alt,
final ModifierValue meta,
final ModifierValue shortcut) {
if ((shift == null)
|| (control == null)
|| (alt == null)
|| (meta == null)
|| (shortcut == null)) {
throw new NullPointerException("Modifier value must not be null!");
}
this.shift = shift;
this.control = control;
this.alt = alt;
this.meta = meta;
this.shortcut = shortcut;
}
/**
* Constructs a {@code KeyCombination} with the specified list of modifiers.
* All modifier keys which are not explicitly listed are set to the
* default {@code UP} value.
*
* All possible modifiers which change the default modifier value are
* defined as constants in the {@code KeyCombination} class.
*
* @param modifiers the list of modifier keys and their corresponding values
*/
protected KeyCombination(final Modifier... modifiers) {
this(getModifierValue(modifiers, KeyCode.SHIFT),
getModifierValue(modifiers, KeyCode.CONTROL),
getModifierValue(modifiers, KeyCode.ALT),
getModifierValue(modifiers, KeyCode.META),
getModifierValue(modifiers, KeyCode.SHORTCUT));
}
/**
* Tests whether this key combination matches the combination in the given
* {@code KeyEvent}.
*
* The implementation of this method in the {@code KeyCombination} class
* does only a partial test with the modifier keys. This method is
* overridden in subclasses to include the main key in the test.
*
* @param event the key event
* @return {@code true} if the key combinations match, {@code false}
* otherwise
*/
public boolean match(final KeyEvent event) {
final KeyCode shortcutKey =
Toolkit.getToolkit().getPlatformShortcutKey();
return test(KeyCode.SHIFT, shift, shortcutKey, shortcut,
event.isShiftDown())
&& test(KeyCode.CONTROL, control, shortcutKey, shortcut,
event.isControlDown())
&& test(KeyCode.ALT, alt, shortcutKey, shortcut,
event.isAltDown())
&& test(KeyCode.META, meta, shortcutKey, shortcut,
event.isMetaDown());
}
/**
* Returns a string representation of this {@code KeyCombination}.
*
* The string representation consists of sections separated by plus
* characters. Each section specifies either a modifier key or the main key.
*
* A modifier key section contains the {@code KeyCode} name of a modifier
* key. It can be prefixed with the {@code Ignored} keyword. A non-prefixed
* modifier key implies its {@code DOWN} value while the prefixed version
* implies the {@code ANY} (ignored) value. If some modifier key is not
* specified in the string at all, it means it has the default {@code UP}
* value.
*
* The format of the main key section of the key combination string depends
* on the {@code KeyCombination} subclass. It is either the key code name
* for {@code KeyCodeCombination} or the single quoted key character for
* {@code KeyCharacterCombination}.
*
* Examples of {@code KeyCombination} string representations:
"Ctrl+Alt+Q"
"Ignore Shift+Ctrl+A"
"Alt+'w'"
* @return the string representation of this {@code KeyCombination}
*/
public String getName() {
StringBuilder sb = new StringBuilder();
addModifiersIntoString(sb);
return sb.toString();
}
/**
* Returns a string representation of this {@code KeyCombination} that is
* suitable for display in a user interface (for example, beside a menu item).
*
* @return A string representation of this {@code KeyCombination}, suitable
* for display in a user interface.
* @since JavaFX 8u20
*/
public String getDisplayText() {
StringBuilder stringBuilder = new StringBuilder();
if (com.sun.javafx.PlatformUtil.isMac()) {
// Macs have a different convention for keyboard accelerators -
// no pluses to separate modifiers, and special symbols for
// each modifier (in a particular order), etc
if (getControl() == KeyCombination.ModifierValue.DOWN) {
stringBuilder.append("\u2303");
}
if (getAlt() == KeyCombination.ModifierValue.DOWN) {
stringBuilder.append("\u2325");
}
if (getShift() == KeyCombination.ModifierValue.DOWN) {
stringBuilder.append("\u21e7");
}
if (getMeta() == KeyCombination.ModifierValue.DOWN || getShortcut() == KeyCombination.ModifierValue.DOWN) {
stringBuilder.append("\u2318");
}
// TODO refer to RT-14486 for remaining glyphs
}
else {
if (getControl() == KeyCombination.ModifierValue.DOWN || getShortcut() == KeyCombination.ModifierValue.DOWN ) {
stringBuilder.append("Ctrl+");
}
if (getAlt() == KeyCombination.ModifierValue.DOWN) {
stringBuilder.append("Alt+");
}
if (getShift() == KeyCombination.ModifierValue.DOWN) {
stringBuilder.append("Shift+");
}
if (getMeta() == KeyCombination.ModifierValue.DOWN) {
stringBuilder.append("Meta+");
}
}
return stringBuilder.toString();
}
/**
* Tests whether this {@code KeyCombination} equals to the specified object.
*
* @param obj the object to compare to
* @return {@code true} if the objects are equal, {@code false} otherwise
*/
@Override
public boolean equals(final Object obj) {
if (!(obj instanceof KeyCombination)) {
return false;
}
final KeyCombination other = (KeyCombination) obj;
return (shift == other.shift)
&& (control == other.control)
&& (alt == other.alt)
&& (meta == other.meta)
&& (shortcut == other.shortcut);
}
/**
* Returns a hash code value for this {@code KeyCombination}.
*
* @return the hash code value
*/
@Override
public int hashCode() {
int hash = 7;
hash = 23 * hash + shift.hashCode();
hash = 23 * hash + control.hashCode();
hash = 23 * hash + alt.hashCode();
hash = 23 * hash + meta.hashCode();
hash = 23 * hash + shortcut.hashCode();
return hash;
}
/**
* Returns a string representation of this object. Implementation returns
* the result of the {@code getName()} call.
*
* @return the string representation of this {@code KeyCombination}
*/
@Override
public String toString() {
return getName();
}
/**
* Constructs a new {@code KeyCombination} from the specified string. The
* string should be in the same format as produced by the {@code getName}
* method.
*
* If the main key section string is quoted in single quotes the method
* creates a new {@code KeyCharacterCombination} for the unquoted substring.
* Otherwise it finds the key code which name corresponds to the main key
* section string and creates a {@code KeyCodeCombination} for it. If this
* can't be done, it falls back to the {@code KeyCharacterCombination}.
*
* @param value the string which represents the requested key combination
* @return the constructed {@code KeyCombination}
* @since JavaFX 2.1
*/
public static KeyCombination valueOf(String value) {
final List modifiers = new ArrayList<>(4);
final String[] tokens = splitName(value);
KeyCode keyCode = null;
String keyCharacter = null;
for (String token : tokens) {
if ((token.length() > 2)
&& (token.charAt(0) == '\'')
&& (token.charAt(token.length() - 1) == '\'')) {
if ((keyCode != null) || (keyCharacter != null)) {
throw new IllegalArgumentException(
"Cannot parse key binding " + value);
}
keyCharacter = token.substring(1, token.length() - 1)
.replace("\\'", "'");
continue;
}
final String normalizedToken = normalizeToken(token);
final Modifier modifier = getModifier(normalizedToken);
if (modifier != null) {
modifiers.add(modifier);
continue;
}
if ((keyCode != null) || (keyCharacter != null)) {
throw new IllegalArgumentException(
"Cannot parse key binding " + value);
}
keyCode = KeyCode.getKeyCode(normalizedToken);
if (keyCode == null) {
keyCharacter = token;
}
}
if ((keyCode == null) && (keyCharacter == null)) {
throw new IllegalArgumentException(
"Cannot parse key binding " + value);
}
final Modifier[] modifierArray =
modifiers.toArray(new Modifier[modifiers.size()]);
return (keyCode != null)
? new KeyCodeCombination(keyCode, modifierArray)
: new KeyCharacterCombination(keyCharacter, modifierArray);
}
/**
* Constructs a new {@code KeyCombination} from the specified string. This
* method simply delegates to {@link #valueOf(String)}.
*
* @param name the string which represents the requested key combination
* @return the constructed {@code KeyCombination}
*
* @see #valueOf(String)
*/
public static KeyCombination keyCombination(String name) {
return valueOf(name);
}
/**
* This class represents a pair of modifier key and its value.
* @since JavaFX 2.0
*/
public static final class Modifier {
private final KeyCode key;
private final ModifierValue value;
private Modifier(final KeyCode key,
final ModifierValue value) {
this.key = key;
this.value = value;
}
/**
* Gets the modifier key of this {@code Modifier}.
*
* @return the modifier key
*/
public KeyCode getKey() {
return key;
}
/**
* Gets the modifier value of this {@code Modifier}.
*
* @return the modifier value
*/
public ModifierValue getValue() {
return value;
}
/**
* Returns a string representation of the modifier.
* @return a string representation of the modifier
*/
@Override
public String toString() {
return ((value == ModifierValue.ANY) ? "Ignore " : "")
+ key.getName();
}
}
/**
* {@code ModifierValue} specifies state of modifier keys.
* @since JavaFX 2.0
*/
public static enum ModifierValue {
/** Constant which indicates that the modifier key must be down. */
DOWN,
/** Constant which indicates that the modifier key must be up. */
UP,
/**
* Constant which indicates that the modifier key can be either up or
* down.
*/
ANY
}
private void addModifiersIntoString(final StringBuilder sb) {
addModifierIntoString(sb, KeyCode.SHIFT, shift);
addModifierIntoString(sb, KeyCode.CONTROL, control);
addModifierIntoString(sb, KeyCode.ALT, alt);
addModifierIntoString(sb, KeyCode.META, meta);
addModifierIntoString(sb, KeyCode.SHORTCUT, shortcut);
}
private static void addModifierIntoString(
final StringBuilder sb,
final KeyCode modifierKey,
final ModifierValue modifierValue) {
if (modifierValue == ModifierValue.UP) {
return;
}
if (sb.length() > 0) {
sb.append("+");
}
if (modifierValue == ModifierValue.ANY) {
sb.append("Ignore ");
}
sb.append(modifierKey.getName());
}
private static boolean test(final KeyCode testedModifierKey,
final ModifierValue testedModifierValue,
final KeyCode shortcutModifierKey,
final ModifierValue shortcutModifierValue,
final boolean isKeyDown) {
final ModifierValue finalModifierValue =
(testedModifierKey == shortcutModifierKey)
? resolveModifierValue(testedModifierValue,
shortcutModifierValue)
: testedModifierValue;
return test(finalModifierValue, isKeyDown);
}
private static boolean test(final ModifierValue modifierValue,
final boolean isDown) {
switch (modifierValue) {
case DOWN:
return isDown;
case UP:
return !isDown;
case ANY:
default:
return true;
}
}
private static ModifierValue resolveModifierValue(
final ModifierValue firstValue,
final ModifierValue secondValue) {
if ((firstValue == ModifierValue.DOWN)
|| (secondValue == ModifierValue.DOWN)) {
return ModifierValue.DOWN;
}
if ((firstValue == ModifierValue.ANY)
|| (secondValue == ModifierValue.ANY)) {
return ModifierValue.ANY;
}
return ModifierValue.UP;
}
static Modifier getModifier(final String name) {
for (final Modifier modifier: POSSIBLE_MODIFIERS) {
if (modifier.toString().equals(name)) {
return modifier;
}
}
return null;
}
private static ModifierValue getModifierValue(
final Modifier[] modifiers,
final KeyCode modifierKey) {
ModifierValue modifierValue = ModifierValue.UP;
for (final Modifier modifier: modifiers) {
if (modifier == null) {
throw new NullPointerException("Modifier must not be null!");
}
if (modifier.getKey() == modifierKey) {
if (modifierValue != ModifierValue.UP) {
throw new IllegalArgumentException(
(modifier.getValue() != modifierValue)
? "Conflicting modifiers specified!"
: "Duplicate modifiers specified!");
}
modifierValue = modifier.getValue();
}
}
return modifierValue;
}
private static String normalizeToken(final String token) {
final String[] words = token.split("\\s+");
final StringBuilder sb = new StringBuilder();
for (final String word: words) {
if (sb.length() > 0) {
sb.append(' ');
}
sb.append(word.substring(0, 1).toUpperCase(Locale.ROOT));
sb.append(word.substring(1).toLowerCase(Locale.ROOT));
}
return sb.toString();
}
private static String[] splitName(String name) {
List tokens = new ArrayList<>();
char[] chars = name.trim().toCharArray();
final int STATE_BASIC = 0; // general text
final int STATE_WHITESPACE = 1; // spaces found
final int STATE_SEPARATOR = 2; // plus found
final int STATE_QUOTED = 3; // quoted text
int state = STATE_BASIC;
int tokenStart = 0;
int tokenEnd = -1;
for (int i = 0; i < chars.length; i++) {
char c = chars[i];
switch(state) {
case STATE_BASIC:
switch(c) {
case ' ':
case '\t':
case '\n':
case '\f':
case '\r':
case '\u000B':
tokenEnd = i;
state = STATE_WHITESPACE;
break;
case '+':
tokenEnd = i;
state = STATE_SEPARATOR;
break;
case '\'':
if (i == 0 || chars[i - 1] != '\\') {
state = STATE_QUOTED;
}
break;
default:
break;
}
break;
case STATE_WHITESPACE:
switch(c) {
case ' ':
case '\t':
case '\n':
case '\f':
case '\r':
case '\u000B':
break;
case '+':
state = STATE_SEPARATOR;
break;
case '\'':
state = STATE_QUOTED;
tokenEnd = -1;
break;
default:
state = STATE_BASIC;
tokenEnd = -1;
break;
}
break;
case STATE_SEPARATOR:
switch(c) {
case ' ':
case '\t':
case '\n':
case '\f':
case '\r':
case '\u000B':
break;
case '+':
throw new IllegalArgumentException(
"Cannot parse key binding " + name);
default:
if (tokenEnd <= tokenStart) {
throw new IllegalArgumentException(
"Cannot parse key binding " + name);
}
tokens.add(new String(chars,
tokenStart, tokenEnd - tokenStart));
tokenStart = i;
tokenEnd = -1;
state = (c == '\'' ? STATE_QUOTED : STATE_BASIC);
break;
}
break;
case STATE_QUOTED:
if (c == '\'' && chars[i - 1] != '\\') {
state = STATE_BASIC;
}
break;
}
}
switch(state) {
case STATE_BASIC:
case STATE_WHITESPACE:
tokens.add(new String(chars,
tokenStart, chars.length - tokenStart));
break;
case STATE_SEPARATOR:
case STATE_QUOTED:
throw new IllegalArgumentException(
"Cannot parse key binding " + name);
}
return tokens.toArray(new String[tokens.size()]);
}
}