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;
+        }
+    }
 }