/*
* Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
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}.
* <p>
* 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.
* <p>
* 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}.
* <p>
* 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}.
* <p>
* The string representation consists of sections separated by plus
* characters. Each section specifies either a modifier key or the main key.
* <p>
* 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.
* <p>
* 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}.
* <p>
* Examples of {@code KeyCombination} string representations:
<PRE>
"Ctrl+Alt+Q"
"Ignore Shift+Ctrl+A"
"Alt+'w'"
</PRE>
* @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.
* <p>
* 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<
Modifier>
modifiers = new
ArrayList<
Modifier>(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<
String>
tokens = new
ArrayList<
String>();
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()]);
}
}