modules/controls/src/main/java/com/sun/javafx/scene/control/behavior/BehaviorBase.java
Print this page
rev 9240 : 8076423: JEP 253: Prepare JavaFX UI Controls & CSS APIs for Modularization
@@ -1,7 +1,7 @@
/*
- * Copyright (c) 2010, 2014, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
@@ -20,389 +20,127 @@
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
-
package com.sun.javafx.scene.control.behavior;
-import javafx.application.ConditionalFeature;
-import javafx.application.Platform;
-import javafx.beans.InvalidationListener;
-import javafx.beans.Observable;
-import javafx.event.EventHandler;
import javafx.scene.Node;
-import javafx.scene.control.Control;
-import javafx.scene.input.ContextMenuEvent;
-import javafx.scene.input.KeyEvent;
-import javafx.scene.input.MouseEvent;
+import com.sun.javafx.scene.control.inputmap.InputMap;
+import com.sun.javafx.scene.control.inputmap.InputMap.Mapping;
+
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
-import com.sun.javafx.scene.traversal.Direction;
-import static javafx.scene.input.KeyCode.DOWN;
-import static javafx.scene.input.KeyCode.LEFT;
-import static javafx.scene.input.KeyCode.RIGHT;
-import static javafx.scene.input.KeyCode.TAB;
-import static javafx.scene.input.KeyCode.UP;
-
-/**
- * A convenient base class from which all our built-in behaviors extend. The
- * main functionality in BehaviorBase revolves around infrastructure for
- * resolving key events into function calls. The differences between platforms
- * can be subtle, and we attempt to build sufficient infrastructure into
- * BehaviorBase to minimize the amount of code and the complexity of code
- * necessary to support multiple platforms sufficiently well.
- *
- * <p>Although BehaviorBase is typically used as a base class, it is not abstract and
- * several skins instantiate an instance of BehaviorBase directly.</p>
- *
- * <p>BehaviorBase also implements the hooks for focus traversal. This
- * implementation is sufficient for most subclasses of BehaviorBase. The
- * following action names are registered in the keyMap for handling focus
- * traversal. Subclasses which need to invoke focus traversal using non-standard
- * key strokes should map key strokes to these action names:</p>
- * <ul>
- * <li>TraverseUp</li>
- * <li>TraverseDown</li>
- * <li>TraverseLeft</li>
- * <li>TraverseRight</li>
- * <li>TraverseNext</li>
- * <li>TraversePrevious</li>
- * </ul>
- *
- * <p>Note that by convention, action names are camel case with the first letter
- * uppercase, matching class naming conventions.</p>
- */
-public class BehaviorBase<C extends Control> {
- /**
- * A static final reference to whether the platform we are on supports touch.
- */
- protected final static boolean IS_TOUCH_SUPPORTED = Platform.isSupported(ConditionalFeature.INPUT_TOUCH);
+import java.util.function.Consumer;
- /**
- * The default key bindings for focus traversal. For many behavior
- * implementations, you may be able to use this directly. The built in names
- * for these traversal actions are:
- * <ul>
- * <li>TraverseUp</li>
- * <li>TraverseDown</li>
- * <li>TraverseLeft</li>
- * <li>TraverseRight</li>
- * <li>TraverseNext</li>
- * <li>TraversePrevious</li>
- * </ul>
- */
- protected static final List<KeyBinding> TRAVERSAL_BINDINGS = new ArrayList<>();
- static final String TRAVERSE_UP = "TraverseUp";
- static final String TRAVERSE_DOWN = "TraverseDown";
- static final String TRAVERSE_LEFT = "TraverseLeft";
- static final String TRAVERSE_RIGHT = "TraverseRight";
- static final String TRAVERSE_NEXT = "TraverseNext";
- static final String TRAVERSE_PREVIOUS = "TraversePrevious";
-
- static {
- TRAVERSAL_BINDINGS.add(new KeyBinding(UP, TRAVERSE_UP));
- TRAVERSAL_BINDINGS.add(new KeyBinding(DOWN, TRAVERSE_DOWN));
- TRAVERSAL_BINDINGS.add(new KeyBinding(LEFT, TRAVERSE_LEFT));
- TRAVERSAL_BINDINGS.add(new KeyBinding(RIGHT, TRAVERSE_RIGHT));
- TRAVERSAL_BINDINGS.add(new KeyBinding(TAB, TRAVERSE_NEXT));
- TRAVERSAL_BINDINGS.add(new KeyBinding(TAB, TRAVERSE_PREVIOUS).shift());
-
- TRAVERSAL_BINDINGS.add(new KeyBinding(UP, TRAVERSE_UP).shift().alt().ctrl());
- TRAVERSAL_BINDINGS.add(new KeyBinding(DOWN, TRAVERSE_DOWN).shift().alt().ctrl());
- TRAVERSAL_BINDINGS.add(new KeyBinding(LEFT, TRAVERSE_LEFT).shift().alt().ctrl());
- TRAVERSAL_BINDINGS.add(new KeyBinding(RIGHT, TRAVERSE_RIGHT).shift().alt().ctrl());
- TRAVERSAL_BINDINGS.add(new KeyBinding(TAB, TRAVERSE_NEXT).shift().alt().ctrl());
- TRAVERSAL_BINDINGS.add(new KeyBinding(TAB, TRAVERSE_PREVIOUS).alt().ctrl());
- }
-
- /**
- * The Control with which this Behavior is used. This must be specified in
- * the constructor and must not be null.
- */
- private final C control;
+public abstract class BehaviorBase<N extends Node> {
- /**
- * The key bindings for this Behavior.
- */
- private final List<KeyBinding> keyBindings;
+ private final N node;
+ private final List<Mapping<?>> installedDefaultMappings;
+ private final List<Runnable> childInputMapDisposalHandlers;
- /**
- * Listens to any key events on the Control and responds to them
- */
- private final EventHandler<KeyEvent> keyEventListener = e -> {
- if (!e.isConsumed()) {
- callActionForEvent(e);
- }
- };
- /**
- * Listens to any focus events on the Control and calls protected methods as a result
- */
- private final InvalidationListener focusListener = property -> {
- focusChanged();
- };
-
- /**
- * Create a new BehaviorBase for the given control. The Control must not
- * be null.
- *
- * @param control The control. Must not be null.
- * @param keyBindings The key bindings that should be used with this behavior.
- * Null is treated as an empty list.
- */
- public BehaviorBase(final C control, final List<KeyBinding> keyBindings) {
- // Don't need to explicitly check for null because Collections.unmodifiableList
- // will die on null, as will the adding of listeners
- this.control = control;
- this.keyBindings = keyBindings == null ? Collections.emptyList()
- : Collections.unmodifiableList(new ArrayList<>(keyBindings));
- control.addEventHandler(KeyEvent.ANY, keyEventListener);
- control.focusedProperty().addListener(focusListener);
- }
-
- /**
- * Called by a Skin when the Skin is disposed. This method
- * allows a Behavior to implement any logic necessary to clean up itself after
- * the Behavior is no longer needed. Calling dispose twice has no effect. This
- * method is intended to be overridden by subclasses, although all subclasses
- * must call super.dispose() or a potential memory leak will result.
- */
- public void dispose() {
- control.removeEventHandler(KeyEvent.ANY, keyEventListener);
- control.focusedProperty().removeListener(focusListener);
+ public BehaviorBase(N node) {
+ this.node = node;
+ this.installedDefaultMappings = new ArrayList<>();
+ this.childInputMapDisposalHandlers = new ArrayList<>();
}
- /***************************************************************************
- * Implementation of the Behavior "interface" *
- * *
- * One of the specialized duties of the behavior is to react to key *
- * events. The behavior breaks the handling of a key event down into a few *
- * distinct stages. First, the BehaviorBase will analyze the key event and *
- * find the String name of a matching action to invoke, if any. If an *
- * action exists for this event, the name is then fed to *
- * callActionForEvent(name), which will then invoke an actual method on *
- * the behavior that is the implementation of that action. *
- * *
- * The reason for returning the intermediate action name as a String is *
- * twofold. First, the matching is done by analyzing a set of key bindings *
- * which are *statically declared* on each behavior class. The fact that *
- * they are static means that we cannot refer to an actual instance method *
- * to invoke (such as with lambda's). It is also important that these are *
- * static to reduce the memory footprint of a control (since having *
- * per-instance key bindings would add a lot to memory footprint). We *
- * could have used something other than String as the intermediate token, *
- * however String is useful if we ever want to expose to developers a way *
- * to alter the action map from a property file or XML file etc. *
- * *
- **************************************************************************/
-
- /**
- * Gets the control associated with this behavior. Even after the BehaviorBase is
- * disposed, this reference will be non-null.
- *
- * @return The control for this Behavior.
- */
- public final C getControl() { return control; }
+ public abstract InputMap<N> getInputMap();
- /**
- * Invokes the appropriate action for this key event. This is the main entry point where
- * key events are passed when they occur. This method is responsible for invoking
- * matchActionForEvent, callAction, and consuming the event if it was handled by this control.
- *
- * @param e The key event. Must not be null.
- */
- protected void callActionForEvent(KeyEvent e) {
- String action = matchActionForEvent(e);
- if (action != null) {
- callAction(action);
- e.consume();
- }
+ public final N getNode() {
+ return node;
}
- /**
- * Given a key event, this method will find the matching action name, or null if there
- * is not one.
- *
- * @param e The key event. Must not be null.
- * @return The name of the action to invoke, or null if there is not one.
- */
- protected String matchActionForEvent(final KeyEvent e) {
- if (e == null) throw new NullPointerException("KeyEvent must not be null");
- KeyBinding match = null;
- int specificity = 0;
- int maxBindings = keyBindings.size();
- for (int i = 0; i < maxBindings; i++) {
- KeyBinding binding = keyBindings.get(i);
- int s = binding.getSpecificity(control, e);
- if (s > specificity) {
- specificity = s;
- match = binding;
- }
- }
- String action = null;
- if (match != null) {
- action = match.getAction();
- }
- return action;
- }
+ public void dispose() {
+ // when we dispose a behavior, we do NOT want to dispose the InputMap,
+ // as that can remove input mappings that were not installed by the
+ // behavior. Instead, we want to only remove mappings that the behavior
+ // itself installed. This can be done by removing all input mappings that
+ // were installed via the 'addDefaultMapping' method.
- /**
- * Called to invoke the action associated with the given name.
- *
- * <p>When a KeyEvent is handled, it is first passed through
- * callActionForEvent which resolves which "action" should be executed
- * based on the key event. This action is indicated by name. This name is
- * then passed to this function which is responsible for invoking the right
- * function based on the name.</p>
- */
- protected void callAction(String name) {
- switch (name) {
- case TRAVERSE_UP: traverseUp(); break;
- case TRAVERSE_DOWN: traverseDown(); break;
- case TRAVERSE_LEFT: traverseLeft(); break;
- case TRAVERSE_RIGHT: traverseRight(); break;
- case TRAVERSE_NEXT: traverseNext(); break;
- case TRAVERSE_PREVIOUS: traversePrevious(); break;
- }
+ // remove default mappings only
+ for (Mapping<?> mapping : installedDefaultMappings) {
+ getInputMap().getMappings().remove(mapping);
}
- /***************************************************************************
- * Focus Traversal methods *
- **************************************************************************/
-
- /**
- * Called by any of the BehaviorBase traverse methods to actually effect a
- * traversal of the focus. The default behavior of this method is to simply
- * call impl_traverse on the given node, passing the given direction. A
- * subclass may override this method.
- *
- * @param node The node to call impl_traverse on
- * @param dir The direction to traverse
- */
- protected void traverse(final Node node, final Direction dir) {
- node.impl_traverse(dir);
+ // Remove all default child mappings
+ for (Runnable r : childInputMapDisposalHandlers) {
+ r.run();
}
- /**
- * Calls the focus traversal engine and indicates that traversal should
- * go the next focusTraversable Node above the current one.
- */
- public final void traverseUp() {
- traverse(control, com.sun.javafx.scene.traversal.Direction.UP);
+// InputMap<N> inputMap = getInputMap();
+// if (inputMap != null) {
+// inputMap.dispose();
+// }
}
- /**
- * Calls the focus traversal engine and indicates that traversal should
- * go the next focusTraversable Node below the current one.
- */
- public final void traverseDown() {
- traverse(control, com.sun.javafx.scene.traversal.Direction.DOWN);
+ protected void addDefaultMapping(List<Mapping<?>> newMapping) {
+ addDefaultMapping(getInputMap(), newMapping.toArray(new Mapping[newMapping.size()]));
}
- /**
- * Calls the focus traversal engine and indicates that traversal should
- * go the next focusTraversable Node left of the current one.
- */
- public final void traverseLeft() {
- traverse(control, com.sun.javafx.scene.traversal.Direction.LEFT);
+ protected void addDefaultMapping(Mapping<?>... newMapping) {
+ addDefaultMapping(getInputMap(), newMapping);
}
- /**
- * Calls the focus traversal engine and indicates that traversal should
- * go the next focusTraversable Node right of the current one.
- */
- public final void traverseRight() {
- traverse(control, com.sun.javafx.scene.traversal.Direction.RIGHT);
- }
+ protected void addDefaultMapping(InputMap<N> inputMap, Mapping<?>... newMapping) {
+ // make a copy of the existing mappings, so we only check against those
+ List<Mapping<?>> existingMappings = new ArrayList<>(inputMap.getMappings());
- /**
- * Calls the focus traversal engine and indicates that traversal should
- * go the next focusTraversable Node in the focus traversal cycle.
- */
- public final void traverseNext() {
- traverse(control, com.sun.javafx.scene.traversal.Direction.NEXT);
- }
+ for (Mapping<?> mapping : newMapping) {
+ // check if a mapping already exists, and if so, do not add this mapping
+ // TODO this is insufficient as we need to check entire InputMap hierarchy
+// for (Mapping<?> existingMapping : existingMappings) {
+// if (existingMapping != null && existingMapping.equals(mapping)) {
+// return;
+// }
+// }
+ if (existingMappings.contains(mapping)) continue;
- /**
- * Calls the focus traversal engine and indicates that traversal should
- * go the previous focusTraversable Node in the focus traversal cycle.
- */
- public final void traversePrevious() {
- traverse(control, com.sun.javafx.scene.traversal.Direction.PREVIOUS);
+ inputMap.getMappings().add(mapping);
+ installedDefaultMappings.add(mapping);
+ }
}
- /***************************************************************************
- * Event handler methods. *
- * *
- * I'm not sure why only mouse events are here. What about drag and *
- * drop events for instance? What about touch events? What about the *
- * other mouse events? It does seem like these need to be here, because *
- * for example mouse interaction logic might differ from platform to *
- * platform, and the Behavior is supposed to implement all the user *
- * interaction logic (not just key handling). So it seems like *
- * BehaviorBase should have methods for handling all forms of input events,*
- * and not just these four mouse events. *
- **************************************************************************/
-
- /**
- * Called whenever the focus on the control has changed. This method is
- * intended to be overridden by subclasses that are interested in focus
- * change events.
- */
- protected void focusChanged() { }
+ protected <T extends Node> void addDefaultChildMap(InputMap<T> parentInputMap, InputMap<T> newChildInputMap) {
+ parentInputMap.getChildInputMaps().add(newChildInputMap);
- /**
- * Invoked by a Skin when the body of the control has been pressed by
- * the mouse. Subclasses should be sure to call super unless they intend
- * to disable any built-in support.
- *
- * @param e the mouse event
- */
- public void mousePressed(MouseEvent e) { }
+ childInputMapDisposalHandlers.add(() -> parentInputMap.getChildInputMaps().remove(newChildInputMap));
+ }
- /**
- * Invoked by a Skin when the body of the control has been dragged by
- * the mouse. Subclasses should be sure to call super unless they intend
- * to disable any built-in support (for example, for tooltips).
- *
- * @param e the mouse event
- */
- public void mouseDragged(MouseEvent e) { }
+ protected InputMap<N> createInputMap() {
+ // TODO re-enable when InputMap moves back to Node / Control
+// return node.getInputMap() != null ?
+// (InputMap<N>)node.getInputMap() :
+// new InputMap<>(node);
+ return new InputMap<>(node);
+ }
- /**
- * Invoked by a Skin when the body of the control has been released by
- * the mouse. Subclasses should be sure to call super unless they intend
- * to disable any built-in support (for example, for tooltips).
- *
- * @param e the mouse event
- */
- public void mouseReleased(MouseEvent e) { }
+ protected void removeMapping(Object key) {
+ InputMap<?> inputMap = getInputMap();
+ inputMap.lookupMapping(key).ifPresent(mapping -> {
+ inputMap.getMappings().remove(mapping);
+ installedDefaultMappings.remove(mapping);
+ });
+ }
- /**
- * Invoked by a Skin when the body of the control has been entered by
- * the mouse. Subclasses should be sure to call super unless they intend
- * to disable any built-in support.
- *
- * @param e the mouse event
- */
- public void mouseEntered(MouseEvent e) { }
+ void rtl(Node node, Runnable rtlMethod, Runnable nonRtlMethod) {
+ switch(node.getEffectiveNodeOrientation()) {
+ case RIGHT_TO_LEFT: rtlMethod.run(); break;
+ default: nonRtlMethod.run(); break;
+ }
+ }
- /**
- * Invoked by a Skin when the body of the control has been exited by
- * the mouse. Subclasses should be sure to call super unless they intend
- * to disable any built-in support.
- *
- * @param e the mouse event
- */
- public void mouseExited(MouseEvent e) { }
+ <T> void rtl(Node node, T object, Consumer<T> rtlMethod, Consumer<T> nonRtlMethod) {
+ switch(node.getEffectiveNodeOrientation()) {
+ case RIGHT_TO_LEFT: rtlMethod.accept(object); break;
+ default: nonRtlMethod.accept(object); break;
+ }
+ }
- /**
- * Invoked by a Skin when the control has had its context menu requested,
- * most commonly by right-clicking on the control. Subclasses should be sure
- * to call super unless they intend to disable any built-in support.
- *
- * @param e the context menu event
- */
- public void contextMenuRequested(ContextMenuEvent e) { }
+ boolean isRTL(Node n) {
+ switch(n.getEffectiveNodeOrientation()) {
+ case RIGHT_TO_LEFT: return true;
+ default: return false;
+ }
+ }
}