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