1 /* 2 * Copyright (c) 2012, 2014, 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.control.behavior; 27 28 import javafx.css.PseudoClass; 29 import javafx.scene.Node; 30 import javafx.scene.control.Control; 31 import javafx.scene.control.PopupControl; 32 33 import javafx.scene.Scene; 34 import javafx.scene.input.KeyEvent; 35 36 import javafx.beans.value.ChangeListener; 37 import javafx.event.Event; 38 import javafx.event.EventDispatcher; 39 import javafx.event.EventHandler; 40 import javafx.scene.input.MouseEvent; 41 42 /** 43 * A two level focus handler allows a Control to behave as if it 44 * has three focus states : 45 * - not focused 46 * - focused with internal focus 47 * - focused with external focus 48 * 49 * In external focus mode it intercepts focus and traversal events and 50 * prevents the Controls acting upon them, or trapping focus. 51 * In internal focus mode most events go to the Control, except 52 * for events that are defined to exit the mode. 53 */ 54 public class TwoLevelFocusBehavior { 55 56 Node tlNode = null; 57 PopupControl tlPopup = null; 58 EventDispatcher origEventDispatcher = null; 59 60 public TwoLevelFocusBehavior() { 61 } 62 63 public TwoLevelFocusBehavior(Node node) { 64 tlNode = node; 65 tlPopup = null; 66 67 tlNode.addEventHandler(KeyEvent.ANY, keyEventListener); 68 tlNode.addEventHandler(MouseEvent.MOUSE_PRESSED, mouseEventListener); 69 tlNode.focusedProperty().addListener(focusListener); 70 71 // block ScrollEvent from being passed down to scrollbar's skin 72 origEventDispatcher = tlNode.getEventDispatcher(); 73 tlNode.setEventDispatcher(tlfEventDispatcher); 74 } 75 76 /** 77 * Invoked by the behavior when it is disposed, so that any listeners installed by 78 * the TwoLevelFocusBehavior can also be uninstalled 79 */ 80 public void dispose() { 81 tlNode.removeEventHandler(KeyEvent.ANY, keyEventListener); 82 tlNode.removeEventHandler(MouseEvent.MOUSE_PRESSED, mouseEventListener); 83 tlNode.focusedProperty().removeListener(focusListener); 84 tlNode.setEventDispatcher(origEventDispatcher); 85 } 86 87 /** 88 * Don't allow the Node to handle a key event if it is in externalFocus mode. 89 * the only keyboard actions allowed are the navigation keys...... 90 */ 91 final EventDispatcher preemptiveEventDispatcher = (event, tail) -> { 92 93 // block the event from being passed down to children 94 if (event instanceof KeyEvent && event.getEventType() == KeyEvent.KEY_PRESSED) { 95 if (!((KeyEvent)event).isMetaDown() && !((KeyEvent)event).isControlDown() && !((KeyEvent)event).isAltDown()) { 96 if (isExternalFocus()) { 97 // 98 // don't let the behaviour leak any navigation keys when 99 // we're not in blocking mode.... 100 // 101 Object obj = event.getTarget(); 102 103 switch (((KeyEvent)event).getCode()) { 104 case TAB : 105 if (((KeyEvent)event).isShiftDown()) { 106 ((Node)obj).impl_traverse(com.sun.javafx.scene.traversal.Direction.PREVIOUS); 107 } 108 else { 109 ((Node)obj).impl_traverse(com.sun.javafx.scene.traversal.Direction.NEXT); 110 } 111 event.consume(); 112 break; 113 case UP : 114 ((Node)obj).impl_traverse(com.sun.javafx.scene.traversal.Direction.UP); 115 event.consume(); 116 break; 117 case DOWN : 118 ((Node)obj).impl_traverse(com.sun.javafx.scene.traversal.Direction.DOWN); 119 event.consume(); 120 break; 121 case LEFT : 122 ((Node)obj).impl_traverse(com.sun.javafx.scene.traversal.Direction.LEFT); 123 event.consume(); 124 break; 125 case RIGHT : 126 ((Node)obj).impl_traverse(com.sun.javafx.scene.traversal.Direction.RIGHT); 127 event.consume(); 128 break; 129 case ENTER : 130 setExternalFocus(false); 131 event.consume(); 132 break; 133 default : 134 // this'll kill mnemonics.... unless! 135 Scene s = tlNode.getScene(); 136 Event.fireEvent(s, event); 137 event.consume(); 138 break; 139 } 140 } 141 } 142 } 143 144 return event; 145 }; 146 147 final EventDispatcher tlfEventDispatcher = (event, tail) -> { 148 149 if ((event instanceof KeyEvent)) { 150 if (isExternalFocus()) { 151 tail = tail.prepend(preemptiveEventDispatcher); 152 return tail.dispatchEvent(event); 153 } 154 } 155 return origEventDispatcher.dispatchEvent(event, tail); 156 }; 157 158 private Event postDispatchTidyup(Event event) { 159 // block the event from being passed down to children 160 if (event instanceof KeyEvent && event.getEventType() == KeyEvent.KEY_PRESSED) { 161 if (!isExternalFocus()) { 162 // 163 // don't let the behaviour leak any navigation keys when 164 // we're not in blocking mode.... 165 // 166 if (!((KeyEvent)event).isMetaDown() && !((KeyEvent)event).isControlDown() && !((KeyEvent)event).isAltDown()) { 167 168 switch (((KeyEvent)event).getCode()) { 169 case TAB : 170 case UP : 171 case DOWN : 172 case LEFT : 173 case RIGHT : 174 event.consume(); 175 break; 176 case ENTER : 177 setExternalFocus(true); 178 event.consume(); 179 break; 180 default : 181 break; 182 } 183 } 184 } 185 } 186 return event; 187 } 188 189 190 private final EventHandler<KeyEvent> keyEventListener = e -> { 191 postDispatchTidyup(e); 192 }; 193 194 195 /** 196 * When a node gets focus, put it in external-focus mode. 197 */ 198 final ChangeListener<Boolean> focusListener = (observable, oldVal, newVal) -> { 199 if (newVal && tlPopup != null) { 200 setExternalFocus(false); 201 } 202 else { 203 setExternalFocus(true); 204 } 205 }; 206 207 private final EventHandler<MouseEvent> mouseEventListener = e -> { 208 setExternalFocus(false); 209 }; 210 211 private boolean externalFocus = true; 212 213 public boolean isExternalFocus() { 214 return externalFocus; 215 } 216 217 private static final PseudoClass INTERNAL_PSEUDOCLASS_STATE = 218 PseudoClass.getPseudoClass("internal-focus"); 219 private static final PseudoClass EXTERNAL_PSEUDOCLASS_STATE = 220 PseudoClass.getPseudoClass("external-focus"); 221 222 public void setExternalFocus(boolean value) { 223 externalFocus = value; 224 225 if (tlNode != null && tlNode instanceof Control) { 226 tlNode.pseudoClassStateChanged(INTERNAL_PSEUDOCLASS_STATE, !value); 227 tlNode.pseudoClassStateChanged(EXTERNAL_PSEUDOCLASS_STATE, value); 228 } 229 else if (tlPopup != null) { 230 tlPopup.pseudoClassStateChanged(INTERNAL_PSEUDOCLASS_STATE, !value); 231 tlPopup.pseudoClassStateChanged(EXTERNAL_PSEUDOCLASS_STATE, value); 232 } 233 } 234 }