/*
* Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved.
* ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*/
package com.sun.javafx.scene;
import java.util.
AbstractMap;
import java.util.
AbstractSet;
import java.util.
ArrayList;
import java.util.
ConcurrentModificationException;
import java.util.
HashMap;
import java.util.
Iterator;
import java.util.
Map;
import java.util.
Set;
import javafx.collections.
ObservableList;
import javafx.collections.
ObservableMap;
import javafx.event.
Event;
import javafx.scene.
Node;
import javafx.scene.input.
KeyCombination;
import javafx.scene.input.
KeyEvent;
import javafx.scene.input.
Mnemonic;
import com.sun.javafx.
PlatformUtil;
import com.sun.javafx.collections.
ObservableListWrapper;
import com.sun.javafx.collections.
ObservableMapWrapper;
import com.sun.javafx.event.
BasicEventDispatcher;
import com.sun.javafx.scene.traversal.
Direction;
public final class
KeyboardShortcutsHandler extends
BasicEventDispatcher {
private
ObservableMap<
KeyCombination,
Runnable>
accelerators;
private
CopyOnWriteMap<
KeyCombination,
Runnable>
acceleratorsBackingMap;
private
ObservableMap<
KeyCombination,
ObservableList<
Mnemonic>>
mnemonics;
public void
addMnemonic(
Mnemonic m) {
ObservableList<
Mnemonic>
mnemonicsList = (
ObservableList)
getMnemonics().
get(
m.
getKeyCombination());
if (
mnemonicsList == null) {
mnemonicsList = new
ObservableListWrapper<
Mnemonic>(new
ArrayList<
Mnemonic>());
getMnemonics().
put(
m.
getKeyCombination(),
mnemonicsList);
}
boolean
foundMnemonic = false;
for (int
i = 0 ;
i <
mnemonicsList.
size() ;
i++) {
if (
mnemonicsList.
get(
i) ==
m) {
foundMnemonic = true;
}
}
if (
foundMnemonic == false) {
mnemonicsList.
add(
m);
}
}
public void
removeMnemonic(
Mnemonic m) {
ObservableList<
Mnemonic>
mnemonicsList = (
ObservableList)
getMnemonics().
get(
m.
getKeyCombination());
if (
mnemonicsList != null) {
for (int
i = 0 ;
i <
mnemonicsList.
size() ;
i++) {
if (
mnemonicsList.
get(
i).
getNode() ==
m.
getNode()) {
mnemonicsList.
remove(
i);
}
}
}
}
public
ObservableMap<
KeyCombination,
ObservableList<
Mnemonic>>
getMnemonics() {
if (
mnemonics == null) {
mnemonics = new
ObservableMapWrapper<
KeyCombination,
ObservableList<
Mnemonic>>(new
HashMap<
KeyCombination,
ObservableList<
Mnemonic>>());
}
return
mnemonics;
}
public
ObservableMap<
KeyCombination,
Runnable>
getAccelerators() {
if (
accelerators == null) {
acceleratorsBackingMap = new
CopyOnWriteMap<>();
accelerators = new
ObservableMapWrapper<>(
acceleratorsBackingMap);
}
return
accelerators;
}
private void
traverse(
Event event,
Node node,
Direction dir) {
if (
node.
impl_traverse(
dir)) {
event.
consume();
}
}
public void
processTraversal(
Event event) {
if (
event instanceof
KeyEvent &&
event.
getEventType() ==
KeyEvent.
KEY_PRESSED) {
if (!((
KeyEvent)
event).
isMetaDown() && !((
KeyEvent)
event).
isControlDown() && !((
KeyEvent)
event).
isAltDown()) {
Object obj =
event.
getTarget();
if (
obj instanceof
Node) {
switch (((
KeyEvent)
event).
getCode()) {
case
TAB :
if (((
KeyEvent)
event).
isShiftDown()) {
traverse(
event, ((
Node)
obj), com.sun.javafx.scene.traversal.
Direction.
PREVIOUS);
}
else {
traverse(
event, ((
Node)
obj), com.sun.javafx.scene.traversal.
Direction.
NEXT);
}
break;
case
UP :
traverse(
event, ((
Node)
obj), com.sun.javafx.scene.traversal.
Direction.
UP);
break;
case
DOWN :
traverse(
event, ((
Node)
obj), com.sun.javafx.scene.traversal.
Direction.
DOWN);
break;
case
LEFT :
traverse(
event, ((
Node)
obj), com.sun.javafx.scene.traversal.
Direction.
LEFT);
break;
case
RIGHT :
traverse(
event, ((
Node)
obj), com.sun.javafx.scene.traversal.
Direction.
RIGHT);
break;
default :
break;
}
}
}
}
}
@
Override
public
Event dispatchBubblingEvent(
Event event) {
/*
** If the key event hasn't been consumed then
** we will process global events in the order :
** . Mnemonics,
** . Accelerators,
** . Navigation.
** This processing is extra to that of listeners and
** the focus Node.
*/
if (
event.
getEventType() ==
KeyEvent.
KEY_PRESSED) {
if (
PlatformUtil.
isMac()) {
if (((
KeyEvent)
event).
isMetaDown()) {
processMnemonics((
KeyEvent)
event);
}
} else {
if (((
KeyEvent)
event).
isAltDown() ||
isMnemonicsDisplayEnabled()) {
processMnemonics((
KeyEvent)
event);
}
}
if (!
event.
isConsumed()) {
processAccelerators((
KeyEvent)
event);
}
if (!
event.
isConsumed()) {
processTraversal(
event);
}
}
/*
** if we're not on mac, and nobody consumed the event, then we should
** check to see if we should highlight the mnemonics on the scene
*/
if (!
PlatformUtil.
isMac()) {
if (
event.
getEventType() ==
KeyEvent.
KEY_PRESSED) {
if (((
KeyEvent)
event).
isAltDown() && !
event.
isConsumed()) {
/*
** show mnemonics while alt is held
*/
if (!
isMnemonicsDisplayEnabled()) {
setMnemonicsDisplayEnabled(true);
}
else {
if (
PlatformUtil.
isWindows()) {
setMnemonicsDisplayEnabled(!
isMnemonicsDisplayEnabled());
}
}
}
}
if (
event.
getEventType() ==
KeyEvent.
KEY_RELEASED) {
if (!((
KeyEvent)
event).
isAltDown()) {
if (!
PlatformUtil.
isWindows()) {
setMnemonicsDisplayEnabled(false);
}
}
}
}
return
event;
}
private void
processMnemonics(
KeyEvent event) {
if (
mnemonics != null) {
ObservableList<
Mnemonic>
mnemonicsList = null;
for (
Map.
Entry<
KeyCombination,
ObservableList<
Mnemonic>>
mnemonic:
mnemonics.
entrySet()) {
if (!
isMnemonicsDisplayEnabled()) {
if (
mnemonic.
getKey().
match(
event)) {
mnemonicsList = (
ObservableList)
mnemonic.
getValue();
break;
}
}
else {
/*
** Mnemonics display has been enabled, which means
** we act as is the alt key is being held down.
*/
KeyEvent fakeEvent = new
KeyEvent(null,
event.
getTarget(),
KeyEvent.
KEY_PRESSED,
event.
getCharacter(),
event.
getText(),
event.
getCode(),
event.
isShiftDown(),
event.
isControlDown(),
true,
event.
isMetaDown());
if (
mnemonic.
getKey().
match(
fakeEvent)) {
mnemonicsList = (
ObservableList)
mnemonic.
getValue();
break;
}
}
}
if (
mnemonicsList != null) {
/*
** for mnemonics we need to check if visible and reachable....
** if a single Mnemonic on the keyCombo we
** fire the runnable in Mnemoninic, and transfer focus
** if there is more than one then we just
** transfer the focus
**
*/
boolean
multipleNodes = false;
Node firstNode = null;
Mnemonic firstMnemonics = null;
int
focusedIndex = -1;
int
nextFocusable = -1;
/*
** find first focusable node
*/
for (int
i = 0 ;
i <
mnemonicsList.
size() ;
i++) {
if (
mnemonicsList.
get(
i) instanceof
Mnemonic) {
Node currentNode = (
Node)
mnemonicsList.
get(
i).
getNode();
if (
firstMnemonics == null && (
currentNode.
impl_isTreeVisible() && !
currentNode.
isDisabled())) {
firstMnemonics =
mnemonicsList.
get(
i);
}
if (
currentNode.
impl_isTreeVisible() && (
currentNode.
isFocusTraversable() && !
currentNode.
isDisabled())) {
if (
firstNode == null) {
firstNode =
currentNode;
}
else {
/*
** there is more than one node on this keyCombo
*/
multipleNodes = true;
if (
focusedIndex != -1) {
if (
nextFocusable == -1) {
nextFocusable =
i;
}
}
}
}
/*
** one of our targets has the focus already
*/
if (
currentNode.
isFocused()) {
focusedIndex =
i;
}
}
}
if (
firstNode != null) {
if (!
multipleNodes == true) {
/*
** just one target
*/
firstNode.
requestFocus();
event.
consume();
}
else {
/*
** we have multiple nodes using the same mnemonic.
** this is allowed for nmemonics, and we simple
** focus traverse between them
*/
if (
focusedIndex == -1) {
firstNode.
requestFocus();
event.
consume();
}
else {
if (
focusedIndex >=
mnemonicsList.
size()) {
firstNode.
requestFocus();
event.
consume();
}
else {
if (
nextFocusable != -1) {
((
Node)
mnemonicsList.
get(
nextFocusable).
getNode()).
requestFocus();
}
else {
firstNode.
requestFocus();
}
event.
consume();
}
}
}
}
if (!
multipleNodes &&
firstMnemonics != null) {
firstMnemonics.
fire();
}
}
}
}
private void
processAccelerators(
KeyEvent event) {
if (
acceleratorsBackingMap != null) {
acceleratorsBackingMap.
lock();
try {
for (
Map.
Entry<
KeyCombination,
Runnable>
accelerator :
acceleratorsBackingMap.
backingMap.
entrySet()) {
if (
accelerator.
getKey().
match(
event)) {
Runnable acceleratorRunnable =
accelerator.
getValue();
if (
acceleratorRunnable != null) {
/*
** for accelerators there can only be one target
** and we don't care whether it's visible or reachable....
** we just run the Runnable.......
*/
acceleratorRunnable.
run();
event.
consume();
}
}
}
} finally {
acceleratorsBackingMap.
unlock();
}
}
}
private void
processMnemonicsKeyDisplay() {
ObservableList<
Mnemonic>
mnemonicsList = null;
if (
mnemonics != null) {
for (
Map.
Entry<
KeyCombination,
ObservableList<
Mnemonic>>
mnemonic:
mnemonics.
entrySet()) {
mnemonicsList = (
ObservableList)
mnemonic.
getValue();
if (
mnemonicsList != null) {
for (int
i = 0 ;
i <
mnemonicsList.
size() ;
i++) {
Node currentNode = (
Node)
mnemonicsList.
get(
i).
getNode();
currentNode.
impl_setShowMnemonics(
mnemonicsDisplayEnabled);
}
}
}
}
}
/*
** remember if the alt key is being held
*/
private boolean
mnemonicsDisplayEnabled = false;
public boolean
isMnemonicsDisplayEnabled() {
return
mnemonicsDisplayEnabled;
}
public void
setMnemonicsDisplayEnabled(boolean
b) {
if (
b !=
mnemonicsDisplayEnabled) {
mnemonicsDisplayEnabled =
b;
processMnemonicsKeyDisplay();
}
}
public void
clearNodeMnemonics(
Node node) {
if (
mnemonics != null) {
for (
ObservableList<
Mnemonic>
list :
mnemonics.
values()) {
for (
Iterator<
Mnemonic>
it =
list.
iterator();
it.
hasNext(); ) {
Mnemonic m =
it.
next();
if (
m.
getNode() ==
node) {
it.
remove();
}
}
}
}
}
private static class
CopyOnWriteMap<K, V> extends
AbstractMap<K, V> {
private
Map<K, V>
backingMap = new
HashMap<>();
private boolean
lock;
public void
lock() {
lock = true;
}
public void
unlock() {
lock = false;
}
@
Override
public V
put(K
key, V
value) {
if (
lock) {
backingMap = new
HashMap<>(
backingMap);
lock = false;
}
return
backingMap.
put(
key,
value);
}
@
Override
public
Set<
Entry<K, V>>
entrySet() {
return new
AbstractSet<
Entry<K, V>>() {
@
Override
public
Iterator<
Entry<K, V>>
iterator() {
return new
Iterator<
Entry<K, V>>() {
private
Iterator<
Entry<K, V>>
backingIt =
backingMap.
entrySet().
iterator();
private
Map<K, V>
backingMapAtCreation =
backingMap;
private
Entry<K, V>
lastNext = null;
@
Override
public boolean
hasNext() {
checkCoMod();
return
backingIt.
hasNext();
}
private void
checkCoMod() {
if (
backingMap !=
backingMapAtCreation) {
throw new
ConcurrentModificationException();
}
}
@
Override
public
Entry<K, V>
next() {
checkCoMod();
return
lastNext =
backingIt.
next();
}
@
Override
public void
remove() {
checkCoMod();
if (
lastNext == null) {
throw new
IllegalStateException();
}
if (
lock) {
backingMap = new
HashMap<>(
backingMap);
backingIt =
backingMap.
entrySet().
iterator();
while (!
lastNext.
equals(
backingIt.
next()));
lock = false;
}
backingIt.
remove();
lastNext = null;
}
};
}
@
Override
public int
size() {
return
backingMap.
size();
}
};
}
}
}