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 (NodeHelper.traverse(node, 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 && (NodeHelper.isTreeVisible(currentNode) && !currentNode.isDisabled())) {
 285                 firstMnemonics = mnemonic;
 286             }
 287 
 288             if (NodeHelper.isTreeVisible(currentNode) && (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                         NodeHelper.setShowMnemonics(currentNode, 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 }