1 /* 2 * Copyright (c) 2011, 2016, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.javafx.scene; 27 28 import java.util.AbstractMap; 29 import java.util.AbstractSet; 30 import java.util.ArrayList; 31 import java.util.ConcurrentModificationException; 32 import java.util.HashMap; 33 import java.util.Iterator; 34 import java.util.Map; 35 import java.util.Set; 36 37 import javafx.collections.ObservableList; 38 import javafx.collections.ObservableMap; 39 import javafx.event.Event; 40 import javafx.scene.Node; 41 import javafx.scene.input.KeyCode; 42 import javafx.scene.input.KeyCombination; 43 import javafx.scene.input.KeyEvent; 44 import javafx.scene.input.Mnemonic; 45 46 import com.sun.javafx.PlatformUtil; 47 import com.sun.javafx.collections.ObservableListWrapper; 48 import com.sun.javafx.collections.ObservableMapWrapper; 49 import com.sun.javafx.event.BasicEventDispatcher; 50 import com.sun.javafx.scene.traversal.Direction; 51 52 public final class KeyboardShortcutsHandler extends BasicEventDispatcher { 53 private ObservableMap<KeyCombination, Runnable> accelerators; 54 private CopyOnWriteMap<KeyCombination, Runnable> acceleratorsBackingMap; 55 private ObservableMap<KeyCombination, ObservableList<Mnemonic>> mnemonics; 56 57 public void addMnemonic(Mnemonic m) { 58 ObservableList<Mnemonic> mnemonicsList = getMnemonics().get(m.getKeyCombination()); 59 if (mnemonicsList == null) { 60 mnemonicsList = new ObservableListWrapper<>(new ArrayList<>()); 61 getMnemonics().put(m.getKeyCombination(), mnemonicsList); 62 } 63 boolean foundMnemonic = false; 64 for (Mnemonic mnemonic : mnemonicsList) { 65 if (mnemonic == m) { 66 foundMnemonic = true; 67 break; 68 } 69 } 70 if (!foundMnemonic) { 71 mnemonicsList.add(m); 72 } 73 } 74 75 public void removeMnemonic(Mnemonic m) { 76 ObservableList<Mnemonic> mnemonicsList = getMnemonics().get(m.getKeyCombination()); 77 if (mnemonicsList != null) { 78 for (int i = 0 ; i < mnemonicsList.size() ; i++) { 79 if (mnemonicsList.get(i).getNode() == m.getNode()) { 80 mnemonicsList.remove(i); 81 } 82 } 83 } 84 } 85 86 public ObservableMap<KeyCombination, ObservableList<Mnemonic>> getMnemonics() { 87 if (mnemonics == null) { 88 mnemonics = new ObservableMapWrapper<>(new HashMap<>()); 89 } 90 return mnemonics; 91 } 92 93 public ObservableMap<KeyCombination, Runnable> getAccelerators() { 94 if (accelerators == null) { 95 acceleratorsBackingMap = new CopyOnWriteMap<>(); 96 accelerators = new ObservableMapWrapper<>(acceleratorsBackingMap); 97 } 98 return accelerators; 99 } 100 101 private void traverse(Event event, Node node, Direction dir) { 102 if (node.impl_traverse(dir)) { 103 event.consume(); 104 } 105 } 106 107 public void processTraversal(Event event) { 108 if (event.getEventType() != KeyEvent.KEY_PRESSED) return; 109 if (!(event instanceof KeyEvent)) return; 110 111 KeyEvent keyEvent = (KeyEvent)event; 112 if (!keyEvent.isMetaDown() && !keyEvent.isControlDown() && !keyEvent.isAltDown()) { 113 Object obj = event.getTarget(); 114 if (!(obj instanceof Node)) return; 115 116 Node node = (Node)obj; 117 switch (keyEvent.getCode()) { 118 case TAB : 119 if (keyEvent.isShiftDown()) { 120 traverse(event, node, Direction.PREVIOUS); 121 } 122 else { 123 traverse(event, node, Direction.NEXT); 124 } 125 break; 126 case UP : 127 traverse(event, node, Direction.UP); 128 break; 129 case DOWN : 130 traverse(event, node, Direction.DOWN); 131 break; 132 case LEFT : 133 traverse(event, node, Direction.LEFT); 134 break; 135 case RIGHT : 136 traverse(event, node, Direction.RIGHT); 137 break; 138 default : 139 break; 140 } 141 } 142 } 143 144 @Override 145 public Event dispatchBubblingEvent(Event event) { 146 /* 147 * Historically, we processed all unconsumed events in the following order: 148 * . Mnemonics, 149 * . Accelerators, 150 * . Navigation. 151 * But we have now split the handling between capturing and bubbling phases. 152 * In the capturing phase we handle mnemonics and accelerators, and in the bubbling 153 * phase we handle navigation. See dispatchCapturingEvent for the other half of this 154 * impl. 155 */ 156 if (!(event instanceof KeyEvent)) return event; 157 if (event.getEventType() == KeyEvent.KEY_PRESSED && !event.isConsumed()) { 158 processTraversal(event); 159 } 160 return event; 161 } 162 163 @Override 164 public Event dispatchCapturingEvent(Event event) { 165 /* 166 * Historically, we processed all unconsumed events in the following order: 167 * . Mnemonics, 168 * . Accelerators, 169 * . Navigation. 170 * But we have now split the handling between capturing and bubbling phases. 171 * In the capturing phase we handle mnemonics and accelerators, and in the bubbling 172 * phase we handle navigation. See dispatchBubblingEvent for the other half of this 173 * impl. 174 */ 175 if (!(event instanceof KeyEvent)) return event; 176 final boolean keyPressedEvent = event.getEventType() == KeyEvent.KEY_PRESSED; 177 final boolean keyTypedEvent = event.getEventType() == KeyEvent.KEY_TYPED; 178 final boolean keyReleasedEvent = event.getEventType() == KeyEvent.KEY_RELEASED; 179 final KeyEvent keyEvent = (KeyEvent)event; 180 181 if (keyPressedEvent || keyTypedEvent) { 182 if (PlatformUtil.isMac()) { 183 if (keyEvent.isMetaDown()) { 184 processMnemonics(keyEvent); 185 } 186 } else if (keyEvent.isAltDown() || isMnemonicsDisplayEnabled()) { 187 processMnemonics(keyEvent); 188 } 189 190 if (keyPressedEvent && !event.isConsumed()) { 191 processAccelerators(keyEvent); 192 } 193 } 194 195 /* 196 ** if we're not on mac, and nobody consumed the event, then we should 197 ** check to see if we should highlight the mnemonics on the scene 198 */ 199 if (!PlatformUtil.isMac()) { 200 if (keyPressedEvent && keyEvent.isAltDown() && !event.isConsumed()) { 201 /* 202 * show mnemonics while alt is held 203 */ 204 if (!isMnemonicsDisplayEnabled()) { 205 setMnemonicsDisplayEnabled(true); 206 } else { 207 if (PlatformUtil.isWindows()) { 208 setMnemonicsDisplayEnabled(!isMnemonicsDisplayEnabled()); 209 } 210 } 211 } 212 if (keyReleasedEvent && !keyEvent.isAltDown() && !PlatformUtil.isWindows()) { 213 setMnemonicsDisplayEnabled(false); 214 } 215 } 216 return event; 217 } 218 219 private void processMnemonics(final KeyEvent event) { 220 if (mnemonics == null) return; 221 222 // we are going to create a lookup event that is a copy of this event 223 // except replacing KEY_TYPED with KEY_PRESSED. If we find a mnemonic 224 // with this lookup event, we will consume the event so that 225 // KEY_TYPED events are not fired after a mnemonic consumed the 226 // KEY_PRESSED event. 227 // We pass in isMnemonicDisplayEnabled() for the altDown test, as if 228 // mnemonic display has been enabled, we can act as if the alt key is 229 // being held down. 230 KeyEvent lookupEvent = event; 231 if (event.getEventType() == KeyEvent.KEY_TYPED) { 232 lookupEvent = new KeyEvent(null, event.getTarget(), KeyEvent.KEY_PRESSED, 233 " ", 234 event.getCharacter(), 235 KeyCode.getKeyCode(event.getCharacter()), 236 event.isShiftDown(), 237 event.isControlDown(), 238 isMnemonicsDisplayEnabled(), 239 event.isMetaDown()); 240 } else if (isMnemonicsDisplayEnabled()) { 241 lookupEvent = new KeyEvent(null, event.getTarget(), KeyEvent.KEY_PRESSED, 242 event.getCharacter(), 243 event.getText(), 244 event.getCode(), 245 event.isShiftDown(), 246 event.isControlDown(), 247 isMnemonicsDisplayEnabled(), 248 event.isMetaDown()); 249 } 250 251 252 ObservableList<Mnemonic> mnemonicsList = null; 253 254 for (Map.Entry<KeyCombination, ObservableList<Mnemonic>> mnemonic: mnemonics.entrySet()) { 255 if (mnemonic.getKey().match(lookupEvent)) { 256 mnemonicsList = mnemonic.getValue(); 257 break; 258 } 259 } 260 261 if (mnemonicsList == null) return; 262 263 /* 264 ** for mnemonics we need to check if visible and reachable.... 265 ** if a single Mnemonic on the keyCombo we 266 ** fire the runnable in Mnemoninic, and transfer focus 267 ** if there is more than one then we just 268 ** transfer the focus 269 ** 270 */ 271 boolean multipleNodes = false; 272 Node firstNode = null; 273 Mnemonic firstMnemonics = null; 274 int focusedIndex = -1; 275 int nextFocusable = -1; 276 277 /* 278 ** find first focusable node 279 */ 280 for (int i = 0 ; i < mnemonicsList.size() ; i++) { 281 Mnemonic mnemonic = mnemonicsList.get(i); 282 Node currentNode = mnemonic.getNode(); 283 284 if (firstMnemonics == null && (currentNode.impl_isTreeVisible() && !currentNode.isDisabled())) { 285 firstMnemonics = mnemonic; 286 } 287 288 if (currentNode.impl_isTreeVisible() && (currentNode.isFocusTraversable() && !currentNode.isDisabled())) { 289 if (firstNode == null) { 290 firstNode = currentNode; 291 } else { 292 /* 293 ** there is more than one node on this keyCombo 294 */ 295 multipleNodes = true; 296 if (focusedIndex != -1) { 297 if (nextFocusable == -1) { 298 nextFocusable = i; 299 } 300 } 301 } 302 } 303 304 /* 305 ** one of our targets has the focus already 306 */ 307 if (currentNode.isFocused()) { 308 focusedIndex = i; 309 } 310 } 311 312 if (firstNode != null) { 313 if (!multipleNodes == true) { 314 /* 315 ** just one target 316 */ 317 firstNode.requestFocus(); 318 event.consume(); 319 } 320 else { 321 /* 322 ** we have multiple nodes using the same mnemonic. 323 ** this is allowed for nmemonics, and we simple 324 ** focus traverse between them 325 */ 326 if (focusedIndex == -1) { 327 firstNode.requestFocus(); 328 event.consume(); 329 } 330 else { 331 if (focusedIndex >= mnemonicsList.size()) { 332 firstNode.requestFocus(); 333 event.consume(); 334 } 335 else { 336 if (nextFocusable != -1) { 337 mnemonicsList.get(nextFocusable).getNode().requestFocus(); 338 } 339 else { 340 firstNode.requestFocus(); 341 } 342 event.consume(); 343 } 344 } 345 } 346 } 347 348 if (!multipleNodes && firstMnemonics != null) { 349 if (event.getEventType() == KeyEvent.KEY_TYPED) { 350 event.consume(); 351 } else { 352 firstMnemonics.fire(); 353 event.consume(); 354 } 355 } 356 } 357 358 private void processAccelerators(KeyEvent event) { 359 if (acceleratorsBackingMap != null) { 360 acceleratorsBackingMap.lock(); 361 try { 362 for (Map.Entry<KeyCombination, Runnable> 363 accelerator : acceleratorsBackingMap.backingMap.entrySet()) { 364 365 if (accelerator.getKey().match(event)) { 366 Runnable acceleratorRunnable = accelerator.getValue(); 367 if (acceleratorRunnable != null) { 368 /* 369 ** for accelerators there can only be one target 370 ** and we don't care whether it's visible or reachable.... 371 ** we just run the Runnable....... 372 */ 373 acceleratorRunnable.run(); 374 event.consume(); 375 } 376 } 377 } 378 } finally { 379 acceleratorsBackingMap.unlock(); 380 } 381 } 382 } 383 384 private void processMnemonicsKeyDisplay() { 385 ObservableList<Mnemonic> mnemonicsList = null; 386 if (mnemonics != null) { 387 for (Map.Entry<KeyCombination, ObservableList<Mnemonic>> mnemonic: mnemonics.entrySet()) { 388 mnemonicsList = (ObservableList) mnemonic.getValue(); 389 390 if (mnemonicsList != null) { 391 for (int i = 0 ; i < mnemonicsList.size() ; i++) { 392 Node currentNode = (Node)mnemonicsList.get(i).getNode(); 393 currentNode.impl_setShowMnemonics(mnemonicsDisplayEnabled); 394 } 395 } 396 } 397 } 398 } 399 400 /* 401 ** remember if the alt key is being held 402 */ 403 private boolean mnemonicsDisplayEnabled = false; 404 405 public boolean isMnemonicsDisplayEnabled() { 406 return mnemonicsDisplayEnabled; 407 } 408 public void setMnemonicsDisplayEnabled(boolean b) { 409 if (b != mnemonicsDisplayEnabled) { 410 mnemonicsDisplayEnabled = b; 411 processMnemonicsKeyDisplay(); 412 } 413 } 414 415 public void clearNodeMnemonics(Node node) { 416 if (mnemonics != null) { 417 for (ObservableList<Mnemonic> list : mnemonics.values()) { 418 for (Iterator<Mnemonic> it = list.iterator(); it.hasNext(); ) { 419 Mnemonic m = it.next(); 420 if (m.getNode() == node) { 421 it.remove(); 422 } 423 } 424 } 425 } 426 } 427 428 private static class CopyOnWriteMap<K, V> extends AbstractMap<K, V> { 429 430 private Map<K, V> backingMap = new HashMap<>(); 431 private boolean lock; 432 433 public void lock() { 434 lock = true; 435 } 436 437 public void unlock() { 438 lock = false; 439 } 440 441 @Override 442 public V put(K key, V value) { 443 if (lock) { 444 backingMap = new HashMap<>(backingMap); 445 lock = false; 446 } 447 return backingMap.put(key, value); 448 } 449 450 @Override 451 public Set<Entry<K, V>> entrySet() { 452 return new AbstractSet<Entry<K, V>>() { 453 @Override 454 public Iterator<Entry<K, V>> iterator() { 455 return new Iterator<Entry<K, V>>() { 456 457 private Iterator<Entry<K, V>> backingIt = backingMap.entrySet().iterator(); 458 private Map<K, V> backingMapAtCreation = backingMap; 459 private Entry<K, V> lastNext = null; 460 461 @Override 462 public boolean hasNext() { 463 checkCoMod(); 464 return backingIt.hasNext(); 465 } 466 467 private void checkCoMod() { 468 if (backingMap != backingMapAtCreation) { 469 throw new ConcurrentModificationException(); 470 } 471 } 472 473 @Override 474 public Entry<K, V> next() { 475 checkCoMod(); 476 return lastNext = backingIt.next(); 477 } 478 479 @Override 480 public void remove() { 481 checkCoMod(); 482 if (lastNext == null) { 483 throw new IllegalStateException(); 484 } 485 if (lock) { 486 backingMap = new HashMap<>(backingMap); 487 backingIt = backingMap.entrySet().iterator(); 488 while (!lastNext.equals(backingIt.next())); 489 lock = false; 490 } 491 backingIt.remove(); 492 lastNext = null; 493 } 494 }; 495 } 496 497 @Override 498 public int size() { 499 return backingMap.size(); 500 } 501 }; 502 } 503 } 504 }