├── .gitignore ├── src ├── main │ └── java │ │ └── org │ │ └── fxmisc │ │ └── wellbehaved │ │ └── event │ │ ├── internal │ │ ├── package-info.java │ │ └── PrefixTree.java │ │ ├── template │ │ ├── package-info.java │ │ ├── InputHandlerTemplate.java │ │ ├── InputHandlerTemplateMap.java │ │ └── InputMapTemplate.java │ │ ├── package-info.java │ │ ├── GenericKeyCombination.java │ │ ├── InputHandler.java │ │ ├── InputHandlerMap.java │ │ ├── Nodes.java │ │ ├── EventPattern.java │ │ └── InputMap.java └── test │ └── java │ └── org │ └── fxmisc │ └── wellbehaved │ └── event │ ├── EventPatternTest.java │ ├── template │ └── InputMapTemplateTest.java │ └── InputMapTest.java ├── gradle.properties.example ├── .travis.yml ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .gradle/ 3 | gradle.properties 4 | .project 5 | .classpath 6 | .settings/ 7 | /bin/ 8 | *.iml 9 | .idea/ -------------------------------------------------------------------------------- /src/main/java/org/fxmisc/wellbehaved/event/internal/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package is for internal usage is will be made inaccessible in Java 10. 3 | */ 4 | package org.fxmisc.wellbehaved.event.internal; -------------------------------------------------------------------------------- /gradle.properties.example: -------------------------------------------------------------------------------- 1 | nexusUsername = 2 | nexusPassword = 3 | 4 | signing.keyId = 5 | signing.password = 6 | signing.secretKeyRingFile = /home//.gnupg/secring.gpg 7 | -------------------------------------------------------------------------------- /src/main/java/org/fxmisc/wellbehaved/event/template/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * This package contains all the {@link org.fxmisc.wellbehaved.event.InputMap} code needed to turn the idea 3 | * into a template that can be instantiated for each instance of a class. 4 | */ 5 | package org.fxmisc.wellbehaved.event.template; -------------------------------------------------------------------------------- /src/main/java/org/fxmisc/wellbehaved/event/template/InputHandlerTemplate.java: -------------------------------------------------------------------------------- 1 | package org.fxmisc.wellbehaved.event.template; 2 | 3 | import javafx.event.Event; 4 | 5 | import org.fxmisc.wellbehaved.event.InputHandler.Result; 6 | 7 | /** 8 | * Template version of {@link org.fxmisc.wellbehaved.event.InputHandler}. 9 | * 10 | * @param the type of the object that will be passed into the {@link InputHandlerTemplate}'s block of code. 11 | * @param the event type for which this InputMap's {@link org.fxmisc.wellbehaved.event.EventPattern} matches 12 | */ 13 | @FunctionalInterface 14 | public interface InputHandlerTemplate { 15 | 16 | Result process(S state, E event); 17 | 18 | } -------------------------------------------------------------------------------- /src/main/java/org/fxmisc/wellbehaved/event/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows one to use pattern matching (think, "a switch statement with more power") to map one or more input event(s) 3 | * (e.g. key event, mouse event, etc.) to a given behavior (a block of code to execute) in a declarative style 4 | * with self-documenting code. 5 | * 6 | * To understand how to use this library well, read through the javadoc of 7 | * {@link org.fxmisc.wellbehaved.event.EventPattern}, {@link org.fxmisc.wellbehaved.event.InputHandler} and then 8 | * {@link org.fxmisc.wellbehaved.event.InputMap} in that order. Once these are understood, one should read the 9 | * javadoc of {@link org.fxmisc.wellbehaved.event.template.InputMapTemplate}. 10 | */ 11 | package org.fxmisc.wellbehaved.event; -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | 5 | os: 6 | - linux 7 | 8 | # enable Java 8u45+, see https://github.com/travis-ci/travis-ci/issues/4042 9 | addons: 10 | apt: 11 | packages: 12 | - oracle-java8-installer 13 | 14 | # run in container 15 | sudo: false 16 | 17 | # use framebuffer for UI 18 | before_install: 19 | - export DISPLAY=:99.0 20 | - sh -e /etc/init.d/xvfb start 21 | 22 | script: 23 | - gradle assemble 24 | - gradle check --info --stacktrace 25 | 26 | # See https://docs.travis-ci.com/user/languages/java/#Caching 27 | before_cache: 28 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 29 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 30 | cache: 31 | directories: 32 | - $HOME/.gradle/caches/ 33 | - $HOME/.gradle/wrapper/ -------------------------------------------------------------------------------- /src/main/java/org/fxmisc/wellbehaved/event/GenericKeyCombination.java: -------------------------------------------------------------------------------- 1 | package org.fxmisc.wellbehaved.event; 2 | 3 | import javafx.scene.input.KeyCombination; 4 | import javafx.scene.input.KeyEvent; 5 | 6 | import java.util.function.Predicate; 7 | 8 | /** 9 | * A generic helper class for pattern-matching a KeyEvent's {@link KeyCombination.Modifier modifiers} along with 10 | * the key event itself. 11 | */ 12 | class GenericKeyCombination extends KeyCombination { 13 | 14 | private final Predicate keyTest; 15 | 16 | GenericKeyCombination(Predicate keyTest, KeyCombination.Modifier... modifiers) { 17 | super(modifiers); 18 | this.keyTest = keyTest; 19 | } 20 | 21 | @Override 22 | public boolean match(KeyEvent event) { 23 | return super.match(event) // matches the modifiers 24 | && keyTest.test(event); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, TomasMikula 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /src/main/java/org/fxmisc/wellbehaved/event/InputHandler.java: -------------------------------------------------------------------------------- 1 | package org.fxmisc.wellbehaved.event; 2 | 3 | import javafx.event.Event; 4 | import javafx.event.EventHandler; 5 | 6 | /** 7 | * Runs a block of code when its corresponding {@link EventPattern} matches a given event type (e.g. the block of code 8 | * to run in a more powerful switch statement) 9 | * @param the type of the event that will be passed into {@link #process(Event)} 10 | */ 11 | @FunctionalInterface 12 | public interface InputHandler extends EventHandler { 13 | 14 | /** 15 | * Signifies what to do after handling some input: 16 | *
    17 | *
  • 18 | * continue trying to match the event type with the next given {@link EventPattern} ({@link #PROCEED}) 19 | *
  • 20 | *
  • 21 | * stop trying to match the event type and consume the event ({@link #CONSUME}) 22 | *
  • 23 | *
  • 24 | * stop trying to match the event type and do not consume it ({@link #IGNORE}) 25 | *
  • 26 | *
27 | */ 28 | public enum Result { 29 | /** 30 | * Try to continue to match the event type with the next given {@link EventPattern}. This can be 31 | * used to run some code before an {@link InputHandler} that consumes the event. 32 | */ 33 | PROCEED, 34 | /** Stop trying to match the event type with the next given {@link EventPattern} and consume the event */ 35 | CONSUME, 36 | /** 37 | * Stop trying to match the event type with the next given {@link EventPattern} and do not consume it 38 | */ 39 | IGNORE 40 | } 41 | 42 | /** 43 | * When the corresponding {@link EventPattern} matches an event type, this method is called. The implementation 44 | * does not need to consume the event. 45 | */ 46 | Result process(T event); 47 | 48 | @Override 49 | default void handle(T event) { 50 | switch(process(event)) { 51 | case CONSUME: event.consume(); break; 52 | case PROCEED: /* do nothing */ break; 53 | case IGNORE: /* do nothing */ break; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/org/fxmisc/wellbehaved/event/InputHandlerMap.java: -------------------------------------------------------------------------------- 1 | package org.fxmisc.wellbehaved.event; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.Iterator; 6 | import java.util.List; 7 | import java.util.Objects; 8 | import java.util.function.BiFunction; 9 | 10 | import javafx.event.Event; 11 | import javafx.event.EventType; 12 | 13 | import org.fxmisc.wellbehaved.event.InputHandler.Result; 14 | import org.fxmisc.wellbehaved.event.InputMap.HandlerConsumer; 15 | import org.fxmisc.wellbehaved.event.internal.PrefixTree; 16 | import org.fxmisc.wellbehaved.event.internal.PrefixTree.Ops; 17 | 18 | class InputHandlerMap { 19 | 20 | private final BiFunction, InputHandler, InputHandler> SEQ = 21 | (h1, h2) -> evt -> { 22 | switch(h1.process(evt)) { 23 | case PROCEED: return h2.process(evt); 24 | case CONSUME: return Result.CONSUME; 25 | case IGNORE: return Result.IGNORE; 26 | default: throw new AssertionError("unreachable code"); 27 | } 28 | }; 29 | 30 | private final Ops, InputHandler> OPS = new Ops, InputHandler>() { 31 | 32 | @Override 33 | public boolean isPrefixOf(EventType t1, EventType t2) { 34 | EventType t = t2; 35 | while(t != null) { 36 | if(t.equals(t1)) { 37 | return true; 38 | } else { 39 | t = t.getSuperType(); 40 | } 41 | } 42 | return false; 43 | } 44 | 45 | @Override 46 | public EventType commonPrefix( 47 | EventType t1, EventType t2) { 48 | Iterator> i1 = toList(t1).iterator(); 49 | Iterator> i2 = toList(t2).iterator(); 50 | EventType common = null; 51 | while(i1.hasNext() && i2.hasNext()) { 52 | EventType c1 = i1.next(); 53 | EventType c2 = i2.next(); 54 | if(Objects.equals(c1, c2)) { 55 | common = c1; 56 | } 57 | } 58 | return (EventType) common; 59 | } 60 | 61 | @Override 62 | public InputHandler promote(InputHandler h, 63 | EventType subTpe, EventType supTpe) { 64 | 65 | if(Objects.equals(subTpe, supTpe)) { 66 | return h; 67 | } 68 | return evt -> { 69 | if(isPrefixOf(subTpe, (EventType) evt.getEventType())) { 70 | return h.process(evt); 71 | } else { 72 | return Result.PROCEED; 73 | } 74 | }; 75 | } 76 | 77 | @Override 78 | public InputHandler squash(InputHandler v1, InputHandler v2) { 79 | return SEQ.apply(v1, v2); 80 | } 81 | 82 | }; 83 | 84 | 85 | private static final List> toList(EventType t) { 86 | List> l = new ArrayList<>(); 87 | while(t != null) { 88 | l.add(t); 89 | t = t.getSuperType(); 90 | } 91 | Collections.reverse(l); 92 | return l; 93 | } 94 | 95 | private PrefixTree, InputHandler> handlerTree = PrefixTree.empty(OPS); 96 | 97 | public void insertAfter(EventType t, InputHandler h) { 98 | InputHandler handler = (InputHandler) h; 99 | handlerTree = handlerTree.insert(t, handler, SEQ); 100 | } 101 | 102 | void forEach(HandlerConsumer f) { 103 | handlerTree.entries().forEach(th -> f.accept(th.getKey(), th.getValue())); 104 | } 105 | } -------------------------------------------------------------------------------- /src/main/java/org/fxmisc/wellbehaved/event/template/InputHandlerTemplateMap.java: -------------------------------------------------------------------------------- 1 | package org.fxmisc.wellbehaved.event.template; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.Iterator; 6 | import java.util.List; 7 | import java.util.Objects; 8 | import java.util.function.Function; 9 | 10 | import javafx.event.Event; 11 | import javafx.event.EventType; 12 | 13 | import org.fxmisc.wellbehaved.event.InputHandler.Result; 14 | import org.fxmisc.wellbehaved.event.internal.PrefixTree; 15 | import org.fxmisc.wellbehaved.event.internal.PrefixTree.Ops; 16 | import org.fxmisc.wellbehaved.event.template.InputMapTemplate.HandlerTemplateConsumer; 17 | 18 | class InputHandlerTemplateMap { 19 | 20 | private static InputHandlerTemplate sequence( 21 | InputHandlerTemplate h1, 22 | InputHandlerTemplate h2) { 23 | return (s, evt) -> { 24 | switch(h1.process(s, evt)) { 25 | case PROCEED: return h2.process(s, evt); 26 | case CONSUME: return Result.CONSUME; 27 | case IGNORE: return Result.IGNORE; 28 | default: throw new AssertionError("unreachable code"); 29 | } 30 | }; 31 | } 32 | 33 | private static Ops, InputHandlerTemplate> ops() { 34 | return new Ops, InputHandlerTemplate>() { 35 | @Override 36 | public boolean isPrefixOf(EventType t1, EventType t2) { 37 | EventType t = t2; 38 | while(t != null) { 39 | if(t.equals(t1)) { 40 | return true; 41 | } else { 42 | t = t.getSuperType(); 43 | } 44 | } 45 | return false; 46 | } 47 | 48 | @Override 49 | public EventType commonPrefix( 50 | EventType t1, EventType t2) { 51 | Iterator> i1 = toList(t1).iterator(); 52 | Iterator> i2 = toList(t2).iterator(); 53 | EventType common = null; 54 | while(i1.hasNext() && i2.hasNext()) { 55 | EventType c1 = i1.next(); 56 | EventType c2 = i2.next(); 57 | if(Objects.equals(c1, c2)) { 58 | common = c1; 59 | } 60 | } 61 | return (EventType) common; 62 | } 63 | 64 | @Override 65 | public InputHandlerTemplate promote(InputHandlerTemplate h, 66 | EventType subTpe, EventType supTpe) { 67 | 68 | if(Objects.equals(subTpe, supTpe)) { 69 | return h; 70 | } 71 | return (s, evt) -> { 72 | if(isPrefixOf(subTpe, (EventType) evt.getEventType())) { 73 | return h.process(s, evt); 74 | } else { 75 | return Result.PROCEED; 76 | } 77 | }; 78 | } 79 | 80 | @Override 81 | public InputHandlerTemplate squash(InputHandlerTemplate v1, InputHandlerTemplate v2) { 82 | return sequence(v1, v2); 83 | } 84 | 85 | }; 86 | } 87 | 88 | 89 | private static final List> toList(EventType t) { 90 | List> l = new ArrayList<>(); 91 | while(t != null) { 92 | l.add(t); 93 | t = t.getSuperType(); 94 | } 95 | Collections.reverse(l); 96 | return l; 97 | } 98 | 99 | private PrefixTree, InputHandlerTemplate> handlerTree; 100 | 101 | public InputHandlerTemplateMap() { 102 | this(PrefixTree.empty(ops())); 103 | } 104 | 105 | private InputHandlerTemplateMap(PrefixTree, InputHandlerTemplate> handlerTree) { 106 | this.handlerTree = handlerTree; 107 | } 108 | 109 | public void insertAfter(EventType t, InputHandlerTemplate h) { 110 | InputHandlerTemplate handler = (InputHandlerTemplate) h; 111 | handlerTree = handlerTree.insert(t, handler, (h1, h2) -> sequence(h1, h2)); 112 | } 113 | 114 | public InputHandlerTemplateMap map( 115 | Function, ? extends InputHandlerTemplate> f) { 116 | return new InputHandlerTemplateMap<>(handlerTree.map(f, ops())); 117 | } 118 | 119 | void forEach(HandlerTemplateConsumer f) { 120 | handlerTree.entries().forEach(th -> f.accept(th.getKey(), th.getValue())); 121 | } 122 | } -------------------------------------------------------------------------------- /src/test/java/org/fxmisc/wellbehaved/event/EventPatternTest.java: -------------------------------------------------------------------------------- 1 | package org.fxmisc.wellbehaved.event; 2 | 3 | import static javafx.scene.input.KeyCode.*; 4 | import static javafx.scene.input.KeyCombination.*; 5 | import static javafx.scene.input.KeyEvent.*; 6 | import static org.fxmisc.wellbehaved.event.EventPattern.*; 7 | import static org.junit.Assert.*; 8 | 9 | import javafx.embed.swing.JFXPanel; 10 | import javafx.event.Event; 11 | import javafx.scene.input.KeyEvent; 12 | 13 | import org.junit.BeforeClass; 14 | import org.junit.Test; 15 | 16 | import com.sun.javafx.util.Utils; 17 | 18 | public class EventPatternTest { 19 | 20 | @BeforeClass 21 | public static void setUpBeforeClass() { 22 | new JFXPanel(); // initialize JavaFX 23 | } 24 | 25 | @Test 26 | public void simpleKeyMatchTest() { 27 | // "p" prefix = EventPattern 28 | // "e" prefix = KeyEvent 29 | 30 | EventPattern pAPressed = keyPressed(A); 31 | EventPattern pShiftAPressed = keyPressed(A, SHIFT_DOWN); 32 | EventPattern pAnyShiftAPressed = keyPressed(A, SHIFT_ANY); 33 | EventPattern pCtrlAReleased = keyReleased(A, CONTROL_DOWN); 34 | EventPattern pMeta_a_Typed = keyTyped("a", META_DOWN); 35 | EventPattern pAltAPressed = keyPressed("a", ALT_DOWN); 36 | EventPattern pNoControlsTyped = keyTyped().onlyIf(e -> !e.isControlDown() && !e.isAltDown() && ! e.isMetaDown()); 37 | EventPattern p_a_Typed = keyTyped("a"); 38 | EventPattern pLeftBracketTyped = keyTypedNoMod("{"); 39 | 40 | KeyEvent eAPressed = new KeyEvent(KEY_PRESSED, "", "", A, false, false, false, false); 41 | KeyEvent eShiftAPressed = new KeyEvent(KEY_PRESSED, "", "", A, true, false, false, false); 42 | KeyEvent eShiftAReleased = new KeyEvent(KEY_RELEASED, "", "", A, true, false, false, false); 43 | KeyEvent eShiftMetaAPressed = new KeyEvent(KEY_PRESSED, "", "", A, true, false, false, true); 44 | KeyEvent eCtrlAReleased = new KeyEvent(KEY_RELEASED, "", "", A, false, true, false, false); 45 | KeyEvent eMeta_a_Typed = new KeyEvent(KEY_TYPED, "a", "", UNDEFINED, false, false, false, true); 46 | KeyEvent eMeta_A_Typed = new KeyEvent(KEY_TYPED, "A", "", UNDEFINED, false, false, false, true); 47 | KeyEvent eShiftQTyped = new KeyEvent(KEY_TYPED, "Q", "", UNDEFINED, true, false, false, false); 48 | KeyEvent eQTyped = new KeyEvent(KEY_TYPED, "q", "", UNDEFINED, false, false, false, false); 49 | KeyEvent eCtrlQTyped = new KeyEvent(KEY_TYPED, "q", "", UNDEFINED, false, true, false, false); 50 | KeyEvent eLeftBracketTyped = new KeyEvent(KEY_TYPED, "{", "", UNDEFINED, true, false, false, true); 51 | KeyEvent eAltAPressed = new KeyEvent(KEY_PRESSED, "", "", A, false, false, true, false); 52 | 53 | KeyEvent e_a_Typed = new KeyEvent(KEY_TYPED, "a", "", UNDEFINED, false, false, false, false); 54 | KeyEvent eShift_a_Typed = new KeyEvent(KEY_TYPED, "a", "", UNDEFINED, true, false, false, false); 55 | 56 | assertMatchSuccess(pAPressed, eAPressed); 57 | assertMatchFailure(pAPressed, eShiftAPressed); // should not match when Shift pressed 58 | assertMatchFailure(pAPressed, eShiftMetaAPressed); // or when any other combo of modifiers pressed 59 | 60 | assertMatchFailure(pShiftAPressed, eAPressed); // should not match when Shift not pressed 61 | assertMatchSuccess(pShiftAPressed, eShiftAPressed); 62 | assertMatchFailure(pShiftAPressed, eShiftMetaAPressed); // should not match when Meta pressed 63 | assertMatchFailure(pShiftAPressed, eShiftAReleased); // released instead of pressed 64 | assertMatchFailure(pCtrlAReleased, eShiftAReleased); // Shift instead of Control 65 | 66 | assertMatchSuccess(pAnyShiftAPressed, eAPressed); 67 | assertMatchSuccess(pAnyShiftAPressed, eShiftAPressed); 68 | 69 | assertMatchSuccess(pCtrlAReleased, eCtrlAReleased); 70 | 71 | assertMatchSuccess(pMeta_a_Typed, eMeta_a_Typed); 72 | assertMatchFailure(pMeta_a_Typed, eMeta_A_Typed); // wrong capitalization 73 | 74 | assertMatchSuccess(pNoControlsTyped, eShiftQTyped); 75 | assertMatchSuccess(pNoControlsTyped, eQTyped); 76 | assertMatchFailure(pNoControlsTyped, eCtrlQTyped); // should not match when Control pressed 77 | 78 | assertMatchSuccess(pLeftBracketTyped, eLeftBracketTyped); 79 | 80 | if(!Utils.isMac()) { // https://bugs.openjdk.java.net/browse/JDK-8134723 81 | assertMatchSuccess(pAltAPressed, eAltAPressed); 82 | } 83 | 84 | assertMatchSuccess(p_a_Typed, e_a_Typed); 85 | assertMatchFailure(p_a_Typed, eShift_a_Typed); // modifier is pressed 86 | } 87 | 88 | private void assertMatchSuccess(EventPattern pattern, KeyEvent event) { 89 | assertTrue(pattern.match(event).isPresent()); 90 | } 91 | 92 | private void assertMatchFailure(EventPattern pattern, KeyEvent event) { 93 | assertFalse(pattern.match(event).isPresent()); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/org/fxmisc/wellbehaved/event/Nodes.java: -------------------------------------------------------------------------------- 1 | package org.fxmisc.wellbehaved.event; 2 | 3 | import java.util.AbstractMap.SimpleEntry; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Map; 7 | import java.util.Map.Entry; 8 | import java.util.Stack; 9 | 10 | import javafx.collections.MapChangeListener; 11 | import javafx.collections.ObservableMap; 12 | import javafx.event.Event; 13 | import javafx.event.EventHandler; 14 | import javafx.event.EventType; 15 | import javafx.scene.Node; 16 | 17 | import org.fxmisc.wellbehaved.event.InputMap.HandlerConsumer; 18 | 19 | /** 20 | * Helper class for "installing/uninstalling" an {@link InputMap} into a {@link Node}. 21 | * 22 | *

Method Summary

23 | *
    24 | *
  • 25 | * To add an {@link InputMap} as a default behavior that can be overridden later, 26 | * use {@link #addFallbackInputMap(Node, InputMap)}. 27 | *
  • 28 | *
  • 29 | * To add an {@code InputMap} that might override default behaviors, use {@link #addInputMap(Node, InputMap)}. 30 | *
  • 31 | *
  • 32 | * To remove an {@code InputMap}, use {@link #removeInputMap(Node, InputMap)}. 33 | *
  • 34 | *
  • 35 | * See also {@link #pushInputMap(Node, InputMap)} and {@link #popInputMap(Node)} for temporary behavior 36 | * modification. 37 | *
  • 38 | *
39 | */ 40 | public class Nodes { 41 | 42 | private static final String P_INPUTMAP = "org.fxmisc.wellbehaved.event.inputmap"; 43 | private static final String P_HANDLERS = "org.fxmisc.wellbehaved.event.handlers"; 44 | private static final String P_STACK = "org.fxmisc.wellbehaved.event.stack"; 45 | 46 | /** 47 | * Adds the given input map to the start of the node's list of input maps, so that an event will be pattern-matched 48 | * against the given input map before being pattern-matched against any other input maps currently 49 | * "installed" in the node. 50 | */ 51 | public static void addInputMap(Node node, InputMap im) { 52 | // getInputMap calls init, so can use unsafe setter 53 | setInputMapUnsafe(node, InputMap.sequence(im, getInputMap(node))); 54 | } 55 | 56 | /** 57 | * Adds the given input map to the end of the node's list of input maps, so that an event will be pattern-matched 58 | * against all other input maps currently "installed" in the node before being pattern-matched against the given 59 | * input map. 60 | */ 61 | public static void addFallbackInputMap(Node node, InputMap im) { 62 | // getInputMap calls init, so can use unsafe setter 63 | setInputMapUnsafe(node, InputMap.sequence(getInputMap(node), im)); 64 | } 65 | 66 | /** 67 | * Removes (or uninstalls) the given input map from the node. 68 | */ 69 | public static void removeInputMap(Node node, InputMap im) { 70 | // getInputMap calls init, so can use unsafe setter 71 | setInputMapUnsafe(node, getInputMap(node).without(im)); 72 | } 73 | 74 | /** 75 | * Gets the {@link InputMap} for the given node or {@link InputMap#empty()} if there is none. 76 | */ 77 | public static InputMap getInputMap(Node node) { 78 | init(node); 79 | return getInputMapUnsafe(node); 80 | } 81 | 82 | /** 83 | * Removes the currently installed {@link InputMap} (InputMap1) on the given node and installs the {@code im} 84 | * (InputMap2) in its place. When finished, InputMap2 can be uninstalled and InputMap1 reinstalled via 85 | * {@link #popInputMap(Node)}. Multiple InputMaps can be installed so that InputMap(n) will be installed over 86 | * InputMap(n-1) 87 | */ 88 | public static void pushInputMap(Node node, InputMap im) { 89 | // store currently installed im; getInputMap calls init 90 | InputMap previousInputMap = getInputMap(node); 91 | getStack(node).push(previousInputMap); 92 | 93 | // completely override the previous one with the given one 94 | setInputMapUnsafe(node, im); 95 | } 96 | 97 | /** 98 | * If the internal stack has an {@link InputMap}, removes the current {@link InputMap} that was installed 99 | * on the give node via {@link #pushInputMap(Node, InputMap)}, reinstalls the previous {@code InputMap}, 100 | * and then returns true. If the stack is empty, returns false. 101 | */ 102 | public static boolean popInputMap(Node node) { 103 | Stack> stackedInputMaps = getStack(node); 104 | if (!stackedInputMaps.isEmpty()) { 105 | // If stack is not empty, node has already been initialized, so can use unsafe methods. 106 | // Now, completely override current input map with previous one on stack 107 | setInputMapUnsafe(node, stackedInputMaps.pop()); 108 | return true; 109 | } else { 110 | return false; 111 | } 112 | } 113 | 114 | /** 115 | * 116 | * @param node 117 | */ 118 | private static void init(Node node) { 119 | ObservableMap nodeProperties = getProperties(node); 120 | if(nodeProperties.get(P_INPUTMAP) == null) { 121 | 122 | nodeProperties.put(P_INPUTMAP, InputMap.empty()); 123 | nodeProperties.put(P_HANDLERS, new ArrayList>()); 124 | 125 | MapChangeListener listener = ch -> { 126 | if(!P_INPUTMAP.equals(ch.getKey())) { 127 | return; 128 | } 129 | 130 | getHandlers(node).forEach(entry -> { 131 | node.removeEventHandler(entry.getKey(), (EventHandler) entry.getValue()); 132 | }); 133 | 134 | getHandlers(node).clear(); 135 | 136 | InputMap inputMap = (InputMap) ch.getValueAdded(); 137 | inputMap.forEachEventType(new HandlerConsumer() { 138 | 139 | @Override 140 | public void accept( 141 | EventType t, InputHandler h) { 142 | node.addEventHandler(t, h); 143 | getHandlers(node).add(new SimpleEntry<>(t, h)); 144 | }}); 145 | }; 146 | nodeProperties.addListener(listener); 147 | } 148 | } 149 | 150 | /** Expects a {@link #init(Node)} call with the given node before this one is called */ 151 | private static void setInputMapUnsafe(Node node, InputMap im) { 152 | getProperties(node).put(P_INPUTMAP, im); 153 | } 154 | 155 | /** Expects a {@link #init(Node)} call with the given node before this one is called */ 156 | private static InputMap getInputMapUnsafe(Node node) { 157 | return (InputMap) getProperties(node).get(P_INPUTMAP); 158 | } 159 | 160 | private static List, EventHandler>> getHandlers(Node node) { 161 | return (List, EventHandler>>) getProperties(node).get(P_HANDLERS); 162 | } 163 | 164 | private static Stack> getStack(Node node) { 165 | ObservableMap nodeProperties = getProperties(node); 166 | if (nodeProperties.get(P_STACK) == null) { 167 | Stack> stackedInputMaps = new Stack<>(); 168 | nodeProperties.put(P_STACK, stackedInputMaps); 169 | return stackedInputMaps; 170 | } 171 | 172 | return (Stack>) nodeProperties.get(P_STACK); 173 | } 174 | 175 | private static ObservableMap getProperties(Node node) { 176 | return node.getProperties(); 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /src/test/java/org/fxmisc/wellbehaved/event/template/InputMapTemplateTest.java: -------------------------------------------------------------------------------- 1 | package org.fxmisc.wellbehaved.event.template; 2 | 3 | import static javafx.scene.input.KeyCode.*; 4 | import static javafx.scene.input.KeyCombination.*; 5 | import static javafx.scene.input.KeyEvent.*; 6 | import static org.fxmisc.wellbehaved.event.EventPattern.*; 7 | import static org.fxmisc.wellbehaved.event.template.InputMapTemplate.*; 8 | import static org.junit.Assert.*; 9 | 10 | import javafx.beans.property.IntegerProperty; 11 | import javafx.beans.property.SimpleIntegerProperty; 12 | import javafx.beans.property.SimpleStringProperty; 13 | import javafx.beans.property.StringProperty; 14 | import javafx.embed.swing.JFXPanel; 15 | import javafx.scene.Node; 16 | import javafx.scene.control.TextArea; 17 | import javafx.scene.input.InputEvent; 18 | import javafx.scene.input.KeyEvent; 19 | import javafx.scene.layout.Region; 20 | 21 | import org.fxmisc.wellbehaved.event.InputHandler; 22 | import org.fxmisc.wellbehaved.event.InputMap; 23 | import org.fxmisc.wellbehaved.event.InputMapTest; 24 | import org.fxmisc.wellbehaved.event.Nodes; 25 | import org.junit.BeforeClass; 26 | import org.junit.Test; 27 | 28 | public class InputMapTemplateTest { 29 | 30 | @BeforeClass 31 | public static void setUpBeforeClass() { 32 | new JFXPanel(); // initialize JavaFX 33 | } 34 | 35 | @Test 36 | public void test() { 37 | StringProperty res = new SimpleStringProperty(); 38 | 39 | InputMapTemplate imt1 = consume(keyPressed(A), (s, e) -> res.set("A")); 40 | InputMapTemplate imt2 = consume(keyPressed(B), (s, e) -> res.set("B")); 41 | InputMapTemplate imt = imt1.orElse(imt2); 42 | InputMap ignA = InputMap.ignore(keyPressed(A)); 43 | 44 | Node node1 = new Region(); 45 | Node node2 = new Region(); 46 | 47 | Nodes.addInputMap(node1, ignA); 48 | InputMapTemplate.installFallback(imt, node1); 49 | InputMapTemplate.installFallback(imt, node2); 50 | 51 | KeyEvent aPressed = new KeyEvent(KEY_PRESSED, "", "", A, false, false, false, false); 52 | KeyEvent bPressed = new KeyEvent(KEY_PRESSED, "", "", B, false, false, false, false); 53 | 54 | InputMapTest.dispatch(aPressed, node1); 55 | assertNull(res.get()); 56 | assertFalse(aPressed.isConsumed()); 57 | 58 | InputMapTest.dispatch(aPressed, node2); 59 | assertEquals("A", res.get()); 60 | assertTrue(aPressed.isConsumed()); 61 | 62 | InputMapTest.dispatch(bPressed, node1); 63 | assertEquals("B", res.get()); 64 | assertTrue(bPressed.isConsumed()); 65 | } 66 | 67 | private static final InputMapTemplate INPUT_MAP_TEMPLATE = 68 | unless(TextArea::isDisabled, sequence( 69 | consume(keyPressed(A, SHORTCUT_DOWN), (area, evt) -> area.selectAll()), 70 | consume(keyPressed(C, SHORTCUT_DOWN), (area, evt) -> area.copy()) 71 | /* ... */ 72 | )); 73 | 74 | @Test 75 | public void textAreaExample() { 76 | TextArea area1 = new TextArea(); 77 | TextArea area2 = new TextArea(); 78 | 79 | InputMapTemplate.installFallback(INPUT_MAP_TEMPLATE, area1); 80 | InputMapTemplate.installFallback(INPUT_MAP_TEMPLATE, area2); 81 | } 82 | 83 | @Test 84 | public void testIfConsumed() { 85 | IntegerProperty counter = new SimpleIntegerProperty(0); 86 | 87 | InputMapTemplate baseIMT = InputMapTemplate.sequence( 88 | consume(keyPressed(UP)), 89 | consume(keyPressed(DOWN)), 90 | consume(keyPressed(LEFT)), 91 | consume(keyPressed(RIGHT)) 92 | ); 93 | InputMapTemplate imtPP = baseIMT.ifConsumed((n, e) -> counter.set(counter.get() + 1)); 94 | 95 | Node node = new Region(); 96 | InputMapTemplate.installFallback(imtPP, node); 97 | 98 | KeyEvent a = new KeyEvent(KEY_PRESSED, "", "", A, false, false, false, false); 99 | KeyEvent up = new KeyEvent(KEY_PRESSED, "", "", UP, false, false, false, false); 100 | KeyEvent down = new KeyEvent(KEY_PRESSED, "", "", DOWN, false, false, false, false); 101 | KeyEvent b = new KeyEvent(KEY_PRESSED, "", "", B, false, false, false, false); 102 | KeyEvent left = new KeyEvent(KEY_PRESSED, "", "", LEFT, false, false, false, false); 103 | KeyEvent right = new KeyEvent(KEY_PRESSED, "", "", RIGHT, false, false, false, false); 104 | 105 | InputMapTest.dispatch(a, node); 106 | assertEquals(0, counter.get()); 107 | assertFalse(a.isConsumed()); 108 | 109 | InputMapTest.dispatch(up, node); 110 | assertEquals(1, counter.get()); 111 | assertTrue(up.isConsumed()); 112 | 113 | InputMapTest.dispatch(down, node); 114 | assertEquals(2, counter.get()); 115 | assertTrue(down.isConsumed()); 116 | 117 | InputMapTest.dispatch(b, node); 118 | assertEquals(2, counter.get()); 119 | assertFalse(b.isConsumed()); 120 | 121 | InputMapTest.dispatch(left, node); 122 | assertEquals(3, counter.get()); 123 | assertTrue(left.isConsumed()); 124 | 125 | InputMapTest.dispatch(right, node); 126 | assertEquals(4, counter.get()); 127 | assertTrue(right.isConsumed()); 128 | } 129 | 130 | @Test 131 | public void testIfIgnored() { 132 | IntegerProperty counter = new SimpleIntegerProperty(0); 133 | 134 | InputMapTemplate baseIMT = InputMapTemplate.sequence( 135 | ignore(keyPressed(UP)), 136 | ignore(keyPressed(DOWN)), 137 | ignore(keyPressed(LEFT)), 138 | ignore(keyPressed(RIGHT)) 139 | ); 140 | InputMapTemplate imtPP = baseIMT.ifIgnored((n, e) -> counter.set(counter.get() + 1)); 141 | 142 | Node node = new Region(); 143 | InputMapTemplate.installFallback(imtPP, node); 144 | 145 | KeyEvent a = new KeyEvent(KEY_PRESSED, "", "", A, false, false, false, false); 146 | KeyEvent up = new KeyEvent(KEY_PRESSED, "", "", UP, false, false, false, false); 147 | KeyEvent down = new KeyEvent(KEY_PRESSED, "", "", DOWN, false, false, false, false); 148 | KeyEvent b = new KeyEvent(KEY_PRESSED, "", "", B, false, false, false, false); 149 | KeyEvent left = new KeyEvent(KEY_PRESSED, "", "", LEFT, false, false, false, false); 150 | KeyEvent right = new KeyEvent(KEY_PRESSED, "", "", RIGHT, false, false, false, false); 151 | 152 | InputMapTest.dispatch(a, node); 153 | assertEquals(0, counter.get()); 154 | assertFalse(a.isConsumed()); 155 | 156 | InputMapTest.dispatch(up, node); 157 | assertEquals(1, counter.get()); 158 | assertFalse(up.isConsumed()); 159 | 160 | InputMapTest.dispatch(down, node); 161 | assertEquals(2, counter.get()); 162 | assertFalse(down.isConsumed()); 163 | 164 | InputMapTest.dispatch(b, node); 165 | assertEquals(2, counter.get()); 166 | assertFalse(b.isConsumed()); 167 | 168 | InputMapTest.dispatch(left, node); 169 | assertEquals(3, counter.get()); 170 | assertFalse(left.isConsumed()); 171 | 172 | InputMapTest.dispatch(right, node); 173 | assertEquals(4, counter.get()); 174 | assertFalse(right.isConsumed()); 175 | } 176 | 177 | @Test 178 | public void testIfProceeded() { 179 | IntegerProperty counter = new SimpleIntegerProperty(0); 180 | 181 | InputHandler.Result returnVal = InputHandler.Result.PROCEED; 182 | 183 | InputMapTemplate baseIMT = InputMapTemplate.sequence( 184 | consume(keyPressed(A)), 185 | consume(keyPressed(B)), 186 | 187 | process(keyPressed(UP), (n, e) -> returnVal), 188 | process(keyPressed(DOWN), (n, e) -> returnVal), 189 | process(keyPressed(LEFT), (n, e) -> returnVal), 190 | process(keyPressed(RIGHT), (n, e) -> returnVal) 191 | ); 192 | InputMapTemplate imtPP = baseIMT.ifProcessed((n, e) -> counter.set(counter.get() + 1)); 193 | 194 | Node node = new Region(); 195 | InputMapTemplate.installFallback(imtPP, node); 196 | 197 | KeyEvent a = new KeyEvent(KEY_PRESSED, "", "", A, false, false, false, false); 198 | KeyEvent up = new KeyEvent(KEY_PRESSED, "", "", UP, false, false, false, false); 199 | KeyEvent down = new KeyEvent(KEY_PRESSED, "", "", DOWN, false, false, false, false); 200 | KeyEvent b = new KeyEvent(KEY_PRESSED, "", "", B, false, false, false, false); 201 | KeyEvent left = new KeyEvent(KEY_PRESSED, "", "", LEFT, false, false, false, false); 202 | KeyEvent right = new KeyEvent(KEY_PRESSED, "", "", RIGHT, false, false, false, false); 203 | 204 | InputMapTest.dispatch(a, node); 205 | assertEquals(0, counter.get()); 206 | assertTrue(a.isConsumed()); 207 | 208 | InputMapTest.dispatch(up, node); 209 | assertEquals(1, counter.get()); 210 | assertFalse(up.isConsumed()); 211 | 212 | InputMapTest.dispatch(down, node); 213 | assertEquals(2, counter.get()); 214 | assertFalse(down.isConsumed()); 215 | 216 | InputMapTest.dispatch(b, node); 217 | assertEquals(2, counter.get()); 218 | assertTrue(b.isConsumed()); 219 | 220 | InputMapTest.dispatch(left, node); 221 | assertEquals(3, counter.get()); 222 | assertFalse(left.isConsumed()); 223 | 224 | InputMapTest.dispatch(right, node); 225 | assertEquals(4, counter.get()); 226 | assertFalse(right.isConsumed()); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/main/java/org/fxmisc/wellbehaved/event/internal/PrefixTree.java: -------------------------------------------------------------------------------- 1 | package org.fxmisc.wellbehaved.event.internal; 2 | 3 | import java.util.AbstractMap.SimpleEntry; 4 | import java.util.ArrayList; 5 | import java.util.Arrays; 6 | import java.util.List; 7 | import java.util.Map; 8 | import java.util.Map.Entry; 9 | import java.util.Objects; 10 | import java.util.function.BiFunction; 11 | import java.util.function.Function; 12 | import java.util.stream.Stream; 13 | 14 | /** 15 | * Prefix tree (Trie) with an additional property that no data is stored in 16 | * internal nodes. 17 | * 18 | * @param type of "strings" used to index values 19 | * @param type of values (data) indexed by this trie 20 | */ 21 | public abstract class PrefixTree { 22 | 23 | public static interface Ops { 24 | boolean isPrefixOf(K k1, K k2); 25 | K commonPrefix(K k1, K k2); 26 | V promote(V v, K oldKey, K newKey); 27 | V squash(V v1, V v2); 28 | } 29 | 30 | private static class Empty extends PrefixTree { 31 | 32 | public Empty(Ops ops) { 33 | super(ops); 34 | } 35 | 36 | @Override 37 | public Stream> entries() { 38 | return Stream.empty(); 39 | } 40 | 41 | @Override 42 | public PrefixTree insert(K key, V value, BiFunction combine) { 43 | return insertInside(key, value, combine); 44 | } 45 | 46 | @Override 47 | PrefixTree insertInside(K key, V value, BiFunction combine) { 48 | return new Data<>(ops, key, value); 49 | } 50 | 51 | @Override 52 | public PrefixTree map( 53 | Function f, 54 | Ops ops) { 55 | return new Empty<>(ops); 56 | } 57 | 58 | } 59 | 60 | private static abstract class NonEmpty extends PrefixTree { 61 | public NonEmpty(Ops ops) { 62 | super(ops); 63 | } 64 | 65 | abstract K getPrefix(); 66 | abstract Data collapse(); 67 | 68 | @Override 69 | public abstract NonEmpty map(Function f, Ops ops); 70 | 71 | @Override 72 | abstract NonEmpty insertInside(K key, V value, BiFunction combine); 73 | 74 | @Override 75 | public PrefixTree insert(K key, V value, BiFunction combine) { 76 | if(ops.isPrefixOf(key, getPrefix())) { // key is a prefix of this tree 77 | return new Data<>(ops, key, value).insertInside(collapse(), flip(combine)); 78 | } else if(ops.isPrefixOf(getPrefix(), key)) { // key is inside this tree 79 | return insertInside(key, value, combine); 80 | } else { 81 | return new Branch<>(ops, this, new Data<>(ops, key, value)); 82 | } 83 | } 84 | } 85 | 86 | private static class Branch extends NonEmpty { 87 | private final K prefix; 88 | private final List> subTrees; 89 | 90 | Branch(Ops ops, K prefix, List> subTrees) { 91 | super(ops); 92 | 93 | assert Objects.equals(prefix, subTrees.stream().map(NonEmpty::getPrefix).reduce(ops::commonPrefix).get()); 94 | assert subTrees.stream().noneMatch(tree -> Objects.equals(tree.getPrefix(), prefix)); 95 | 96 | this.prefix = prefix; 97 | this.subTrees = subTrees; 98 | } 99 | 100 | private Branch(Ops ops, NonEmpty t1, NonEmpty t2) { 101 | this(ops, ops.commonPrefix(t1.getPrefix(), t2.getPrefix()), Arrays.asList(t1, t2)); 102 | } 103 | 104 | @Override 105 | K getPrefix() { 106 | return prefix; 107 | } 108 | 109 | @Override 110 | public Stream> entries() { 111 | return subTrees.stream().flatMap(tree -> tree.entries()); 112 | } 113 | 114 | @Override 115 | Data collapse() { 116 | return subTrees.stream() 117 | .map(tree -> tree.collapse().promote(prefix)) 118 | .reduce(Data::squash).get(); 119 | } 120 | 121 | @Override 122 | NonEmpty insertInside(K key, V value, BiFunction combine) { 123 | assert ops.isPrefixOf(prefix, key); 124 | 125 | if(Objects.equals(key, prefix)) { 126 | return new Data<>(ops, key, value).insertInside(collapse(), flip(combine)); 127 | } 128 | 129 | // try to find a sub-tree that has common prefix with key longer than this branch's prefix 130 | for(int i = 0; i < subTrees.size(); ++i) { 131 | NonEmpty st = subTrees.get(i); 132 | K commonPrefix = ops.commonPrefix(key, st.getPrefix()); 133 | if(!Objects.equals(commonPrefix, prefix)) { 134 | if(Objects.equals(commonPrefix, st.getPrefix())) { 135 | // st contains key, insert inside st 136 | return replaceBranch(i, st.insertInside(key, value, combine)); 137 | } else if(Objects.equals(commonPrefix, key)) { 138 | // st is under key, insert st inside Data(key, value) 139 | return replaceBranch(i, new Data<>(ops, key, value).insertInside(st.collapse(), flip(combine))); 140 | } else { 141 | return replaceBranch(i, new Branch<>(ops, st, new Data<>(ops, key, value))); 142 | } 143 | } 144 | } 145 | 146 | // no branch intersects key, adjoin Data(key, value) to this branch 147 | List> branches = new ArrayList<>(subTrees.size() + 1); 148 | branches.addAll(subTrees); 149 | branches.add(new Data<>(ops, key, value)); 150 | return new Branch<>(ops, prefix, branches); 151 | } 152 | 153 | private Branch replaceBranch(int i, NonEmpty replacement) { 154 | assert ops.isPrefixOf(prefix, replacement.getPrefix()); 155 | assert ops.isPrefixOf(replacement.getPrefix(), subTrees.get(i).getPrefix()); 156 | 157 | ArrayList> branches = new ArrayList<>(subTrees); 158 | branches.set(i, replacement); 159 | return new Branch<>(ops, prefix, branches); 160 | } 161 | 162 | @Override 163 | public NonEmpty map( 164 | Function f, 165 | Ops ops) { 166 | List> mapped = new ArrayList<>(subTrees.size()); 167 | for(NonEmpty tree: subTrees) { 168 | mapped.add(tree.map(f, ops)); 169 | } 170 | return new Branch<>(ops, prefix, mapped); 171 | } 172 | } 173 | 174 | private static class Data extends NonEmpty { 175 | private final K key; 176 | private final V value; 177 | 178 | Data(Ops ops, K key, V value) { 179 | super(ops); 180 | this.key = key; 181 | this.value = value; 182 | } 183 | 184 | @Override 185 | K getPrefix() { 186 | return key; 187 | } 188 | 189 | @Override 190 | public Stream> entries() { 191 | return Stream.of(new SimpleEntry<>(key, value)); 192 | } 193 | 194 | @Override 195 | Data collapse() { 196 | return this; 197 | } 198 | 199 | @Override 200 | NonEmpty insertInside(K key, V value, BiFunction combine) { 201 | assert ops.isPrefixOf(this.key, key); 202 | return new Data<>( 203 | this.ops, 204 | this.key, 205 | combine.apply(this.value, ops.promote(value, key, this.key))); 206 | } 207 | 208 | NonEmpty insertInside(NonEmpty tree, BiFunction combine) { 209 | Data d = tree.collapse(); 210 | return insertInside(d.key, d.value, combine); 211 | } 212 | 213 | Data promote(K key) { 214 | assert ops.isPrefixOf(key, this.key); 215 | return new Data<>(ops, key, ops.promote(value, this.key, key)); 216 | } 217 | 218 | Data squash(Data that) { 219 | assert Objects.equals(this.key, that.key); 220 | return new Data<>(ops, key, ops.squash(this.value, that.value)); 221 | } 222 | 223 | @Override 224 | public Data map( 225 | Function f, 226 | Ops ops) { 227 | return new Data<>(ops, key, f.apply(value)); 228 | } 229 | } 230 | 231 | public static PrefixTree empty(Ops ops) { 232 | return new Empty<>(ops); 233 | } 234 | 235 | private static BiFunction flip(BiFunction f) { 236 | return (a, b) -> f.apply(b, a); 237 | } 238 | 239 | 240 | final Ops ops; 241 | 242 | private PrefixTree(Ops ops) { 243 | this.ops = ops; 244 | } 245 | 246 | public abstract Stream> entries(); 247 | public abstract PrefixTree insert(K key, V value, BiFunction combine); 248 | public abstract PrefixTree map(Function f, Ops ops); 249 | 250 | public final PrefixTree map(Function f) { 251 | return map(f, ops); 252 | } 253 | 254 | abstract PrefixTree insertInside(K key, V value, BiFunction combine); 255 | } 256 | -------------------------------------------------------------------------------- /src/main/java/org/fxmisc/wellbehaved/event/EventPattern.java: -------------------------------------------------------------------------------- 1 | package org.fxmisc.wellbehaved.event; 2 | 3 | import static javafx.scene.input.KeyCombination.ALT_ANY; 4 | import static javafx.scene.input.KeyCombination.META_ANY; 5 | import static javafx.scene.input.KeyCombination.SHIFT_ANY; 6 | import static javafx.scene.input.KeyCombination.SHORTCUT_ANY; 7 | import static javafx.scene.input.KeyEvent.*; 8 | import static javafx.scene.input.MouseEvent.*; 9 | 10 | import java.util.Collections; 11 | import java.util.HashSet; 12 | import java.util.Optional; 13 | import java.util.Set; 14 | import java.util.function.Predicate; 15 | 16 | import javafx.event.Event; 17 | import javafx.event.EventType; 18 | import javafx.scene.input.KeyCharacterCombination; 19 | import javafx.scene.input.KeyCode; 20 | import javafx.scene.input.KeyCodeCombination; 21 | import javafx.scene.input.KeyCombination; 22 | import javafx.scene.input.KeyEvent; 23 | import javafx.scene.input.MouseButton; 24 | import javafx.scene.input.MouseEvent; 25 | 26 | /** 27 | * Helper class for pattern-matching one or more {@link EventType}s (e.g. the "case" line in a powerful switch 28 | * statement). When {@link #match(Event)} returns a non-empty {@link Optional}, the corresponding 29 | * {@link InputHandler} will be called. 30 | * 31 | *

Usages

32 | *

33 | * This class provides a number of static factory methods that provide the base pattern to match. 34 | *

35 | *
    36 | *
  • 37 | * Most will use the ones for the common event types: {@link #keyPressed()}, {@link #keyTyped()}, 38 | * {@link #mousePressed()}, {@link #mouseClicked()}, {@link #mouseDragged()}, etc. 39 | *
  • 40 | *
  • 41 | * For custom events or super event types (e.g. {@link javafx.scene.input.InputEvent#ANY}), one 42 | * will use the base pattern, {@link #eventType(EventType)} 43 | *
  • 44 | *
45 | * 46 | *

47 | * Once a base pattern is created, one can further define the pattern for which to match for by 48 | * adding what are known as "guards" in pattern matching: {@link #andThen(EventPattern)}, 49 | * {@link #onlyIf(Predicate)}, {@link #unless(Predicate)}, and {@link #anyOf(EventPattern[])}. See each 50 | * method's javadoc for more info. 51 | *

52 | * 53 | *

Examples

54 | *

 55 |  * // a pattern that matches any key pressed event
 56 |  * keyPressed()
 57 |  *
 58 |  * // a pattern that matches only key pressed events where the user
 59 |  * // pressed a digit key
 60 |  * keyPressed().onlyIf(pressedKey -> pressedKey.getCode().isDigitKey())
 61 |  * 
62 | */ 63 | public interface EventPattern { 64 | 65 | static KeyCombination.Modifier[] ALL_MODIFIERS_AS_ANY = new KeyCombination.Modifier[] { 66 | SHORTCUT_ANY, SHIFT_ANY, ALT_ANY, META_ANY 67 | }; 68 | 69 | /** 70 | * Returns a non-empty {@link Optional} when a match is found. 71 | */ 72 | Optional match(T event); 73 | Set> getEventTypes(); 74 | 75 | /** 76 | * Returns an EventPattern that matches the given event type only when this event pattern matches it 77 | * and the {@code next} EventPattern matches it. 78 | */ 79 | default EventPattern andThen(EventPattern next) { 80 | return new EventPattern() { 81 | 82 | @Override 83 | public Optional match(T event) { 84 | return EventPattern.this.match(event).flatMap(next::match); 85 | } 86 | 87 | @Override 88 | public Set> getEventTypes() { 89 | return next.getEventTypes(); 90 | } 91 | }; 92 | } 93 | 94 | /** 95 | * Returns an EventPattern that matches the given event type only if this event pattern matches it 96 | * and the event type passed the given {@code condition} 97 | */ 98 | default EventPattern onlyIf(Predicate condition) { 99 | return new EventPattern() { 100 | 101 | @Override 102 | public Optional match(T event) { 103 | return EventPattern.this.match(event).map(u -> condition.test(u) ? u : null); 104 | } 105 | 106 | @Override 107 | public Set> getEventTypes() { 108 | return EventPattern.this.getEventTypes(); 109 | } 110 | 111 | }; 112 | } 113 | 114 | /** 115 | * Returns an EventPattern that matches the given event type only if this event pattern matches it 116 | * and the event type fails the given {@code condition} 117 | */ 118 | default EventPattern unless(Predicate condition) { 119 | return onlyIf(condition.negate()); 120 | } 121 | 122 | /** 123 | * Returns an EventPattern that matches the given event type when any of the given EventPatterns match the 124 | * given event type; useful when one wants to specify the same behavior for a variety of events (i.e. the 125 | * "copy" action when a user press "CTRL+C" on Windows or "COMMAND+C" on Mac). 126 | */ 127 | @SafeVarargs 128 | static EventPattern anyOf(EventPattern... events) { 129 | return new EventPattern() { 130 | 131 | @Override 132 | public Optional match(T event) { 133 | for (EventPattern evt : events) { 134 | Optional match = evt.match(event); 135 | if(match.isPresent()) { 136 | return match; 137 | } 138 | } 139 | return Optional.empty(); 140 | } 141 | 142 | @Override 143 | public Set> getEventTypes() { 144 | HashSet> ret = new HashSet<>(); 145 | for (EventPattern evt : events) { 146 | ret.addAll(evt.getEventTypes()); 147 | } 148 | return ret; 149 | } 150 | 151 | }; 152 | } 153 | 154 | static EventPattern eventType(EventType eventType) { 155 | return new EventPattern() { 156 | 157 | @Override 158 | public Optional match(Event event) { 159 | EventType actualType = event.getEventType(); 160 | do { 161 | if(actualType.equals(eventType)) { 162 | @SuppressWarnings("unchecked") 163 | T res = (T) event; 164 | return Optional.of(res); 165 | } 166 | actualType = actualType.getSuperType(); 167 | } while(actualType != null); 168 | return Optional.empty(); 169 | } 170 | 171 | @Override 172 | public Set> getEventTypes() { 173 | return Collections.singleton(eventType); 174 | } 175 | 176 | }; 177 | } 178 | 179 | static EventPattern keyPressed() { 180 | return eventType(KeyEvent.KEY_PRESSED); 181 | } 182 | 183 | static EventPattern keyPressed(KeyCombination combination) { 184 | return keyPressed().onlyIf(combination::match); 185 | } 186 | 187 | static EventPattern keyPressed(KeyCode code, KeyCombination.Modifier... modifiers) { 188 | return keyPressed(new KeyCodeCombination(code, modifiers)); 189 | } 190 | 191 | static EventPattern keyPressed(Predicate keyTest, KeyCombination.Modifier... modifiers) { 192 | return keyPressed(new GenericKeyCombination(e -> keyTest.test(e.getCode()), modifiers)); 193 | } 194 | 195 | static EventPattern keyPressed(String character, KeyCombination.Modifier... modifiers) { 196 | return keyPressed(new KeyCharacterCombination(character, modifiers)); 197 | } 198 | 199 | /** 200 | * Matches the given key pressed event regardless of modifiers; this should only be used for the rare KeyEvents 201 | * which require a pressed modifier (e.g. Shift) to generate it (e.g. "{"). If passed in a regular character 202 | * (e.g. "a") and this appears before another EventPattern (e.g. keyPressed("a", SHORTCUT_DOWN)) in an 203 | * {@link InputMap#sequence(InputMap[])}, the second EventPattern will never run. 204 | */ 205 | static EventPattern keyPressedNoMod(String character) { 206 | KeyCharacterCombination combination = new KeyCharacterCombination(character, ALL_MODIFIERS_AS_ANY); 207 | return keyPressed().onlyIf(combination::match); 208 | } 209 | 210 | static EventPattern keyReleased() { 211 | return eventType(KEY_RELEASED); 212 | } 213 | 214 | static EventPattern keyReleased(KeyCombination combination) { 215 | return keyReleased().onlyIf(combination::match); 216 | } 217 | 218 | static EventPattern keyReleased(KeyCode code, KeyCombination.Modifier... modifiers) { 219 | return keyReleased(new KeyCodeCombination(code, modifiers)); 220 | } 221 | 222 | static EventPattern keyReleased(Predicate keyTest, KeyCombination.Modifier... modifiers) { 223 | return keyReleased(new GenericKeyCombination(e -> keyTest.test(e.getCode()), modifiers)); 224 | } 225 | 226 | static EventPattern keyReleased(String character, KeyCombination.Modifier... modifiers) { 227 | return keyReleased(new KeyCharacterCombination(character, modifiers)); 228 | } 229 | 230 | /** 231 | * Matches the given key released event regardless of modifiers; this should only be used for the rare KeyEvents 232 | * which require a pressed modifier (e.g. Shift) to generate it (e.g. "{"). If passed in a regular character 233 | * (e.g. "a") and this appears before another EventPattern (e.g. keyReleased("a", SHORTCUT_DOWN)) in an 234 | * {@link InputMap#sequence(InputMap[])}, the second EventPattern will never run. 235 | */ 236 | static EventPattern keyReleasedNoMod(String character) { 237 | KeyCharacterCombination combination = new KeyCharacterCombination(character, ALL_MODIFIERS_AS_ANY); 238 | return keyReleased().onlyIf(combination::match); 239 | } 240 | 241 | static EventPattern keyTyped() { 242 | return eventType(KEY_TYPED); 243 | } 244 | 245 | static EventPattern keyTyped(Predicate charTest, KeyCombination.Modifier... modifiers) { 246 | GenericKeyCombination combination = new GenericKeyCombination(e -> charTest.test(e.getCharacter()), modifiers); 247 | return keyTyped().onlyIf(combination::match); 248 | } 249 | 250 | static EventPattern keyTyped(String character, KeyCombination.Modifier... modifiers) { 251 | return keyTyped(character::equals, modifiers); 252 | } 253 | 254 | /** 255 | * Matches the given key typed event regardless of modifiers; this should only be used for the rare KeyEvents 256 | * which require a pressed modifier (e.g. Shift) to generate it (e.g. "{"). If passed in a regular character 257 | * (e.g. "a") and this appears before another EventPattern (e.g. keyTyped("a", SHORTCUT_DOWN)) in an 258 | * {@link InputMap#sequence(InputMap[])}, the second EventPattern will never run. 259 | */ 260 | static EventPattern keyTypedNoMod(String character) { 261 | return keyTyped().onlyIf(e -> e.getCharacter().equals(character)); 262 | } 263 | 264 | static EventPattern mouseClicked() { 265 | return eventType(MOUSE_CLICKED); 266 | } 267 | 268 | static EventPattern mouseClicked(MouseButton button) { 269 | return mouseClicked().onlyIf(e -> e.getButton() == button); 270 | } 271 | 272 | static EventPattern mousePressed() { 273 | return eventType(MOUSE_PRESSED); 274 | } 275 | 276 | static EventPattern mousePressed(MouseButton button) { 277 | return mousePressed().onlyIf(e -> e.getButton() == button); 278 | } 279 | 280 | static EventPattern mouseReleased() { 281 | return eventType(MOUSE_RELEASED); 282 | } 283 | 284 | static EventPattern mouseReleased(MouseButton button) { 285 | return mouseReleased().onlyIf(e -> e.getButton() == button); 286 | } 287 | 288 | static EventPattern mouseMoved() { 289 | return eventType(MOUSE_MOVED); 290 | } 291 | 292 | static EventPattern mouseDragged() { 293 | return eventType(MOUSE_DRAGGED); 294 | } 295 | 296 | static EventPattern dragDetected() { 297 | return eventType(DRAG_DETECTED); 298 | } 299 | 300 | static EventPattern mouseEntered() { 301 | return eventType(MOUSE_ENTERED); 302 | } 303 | 304 | static EventPattern mouseEnteredTarget() { 305 | return eventType(MOUSE_ENTERED_TARGET); 306 | } 307 | 308 | static EventPattern mouseExited() { 309 | return eventType(MOUSE_EXITED); 310 | } 311 | 312 | static EventPattern mouseExitedTarget() { 313 | return eventType(MOUSE_EXITED_TARGET); 314 | } 315 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **This project is no longer being maintained. See [this issue](https://github.com/FXMisc/WellBehavedFX/issues/22) for more details.** 2 | 3 | WellBehavedFX 4 | ============= 5 | 6 | This project provides a better mechanism for defining and overriding event handlers (e.g. keyboard shortcuts) for JavaFX. Such mechanism, also known as InputMap API, was considered as part of [JEP 253](http://openjdk.java.net/jeps/253) (see also [JDK-8076423](https://bugs.openjdk.java.net/browse/JDK-8076423)), but was dropped. (I guess I was the most vocal opponent of the proposal ([link to discussion thread](http://mail.openjdk.java.net/pipermail/openjfx-dev/2015-August/017667.html)).) 7 | 8 | 9 | Use Cases 10 | --------- 11 | 12 | ### Event Matching ### 13 | 14 | Use cases in this section focus on expressivity of event matching, i.e. expressing what events should be handled. 15 | 16 | #### Key Combinations #### 17 | The task is to add handlers for the following key combinations to `Node node`: 18 | 19 | | Key Combination | Comment | Handler | 20 | | --------------- | ------- | ------- | 21 | | Enter | no modifier keys pressed | `enterPressed()` | 22 | | [Shift+]A | optional Shift, no other modifiers | `aPressed()` | 23 | | Shortcut+Shift+S | | `saveAll()` | 24 | 25 | ```java 26 | Nodes.addInputMap(node, sequence( 27 | consume(keyPressed(ENTER), e -> enterPressed()), 28 | consume(keyPressed(A, SHIFT_ANY), e -> aPressed()), 29 | consume(keyPressed(S, SHORTCUT_DOWN, SHIFT_DOWN), e -> saveAll()) 30 | )); 31 | ``` 32 | 33 | #### Same action for different events #### 34 | In some situations it is desirable to bind multiple different events to the same action. Task: Invoke `action()` when a `Button` is either left-clicked or Space-pressed. If these are the only two events handled by the button, one handler for each of `MOUSE_CLICKED` and `KEY_PRESSED` event types will be installed on the button (as opposed to, for example, installing a common handler for the nearest common supertype, which in this case would be `InputEvent.ANY`). 35 | 36 | ```java 37 | Nodes.addInputMap(button, consume( 38 | anyOf(mouseClicked(PRIMARY), keyPressed(SPACE)), 39 | e -> action())); 40 | ``` 41 | 42 | #### Text Input #### 43 | Handle text input, i.e. `KEY_TYPED` events, _except_ for the new line character, which should be left unconsumed. In this example, echo the input to standard output. 44 | 45 | ```java 46 | Nodes.addInputMap(button, consume( 47 | keyTyped(c -> !c.equals("\n")), 48 | e -> System.out.print(e.getCharacter()))); 49 | ``` 50 | 51 | #### Custom Events #### 52 | Assume the following custom event declaration: 53 | 54 | ```java 55 | class FooEvent extends Event { 56 | public static final EventType FOO; 57 | 58 | public boolean isSecret(); 59 | public String getValue(); 60 | } 61 | ``` 62 | 63 | The task is to print out the value of and consume non-secret `Foo` events of `node`. Secret `Foo` events should be left unconsumed, i.e. let to bubble up. 64 | 65 | ```java 66 | Nodes.addInputMap(node, consume( 67 | eventType(FooEvent.FOO).unless(FooEvent::isSecret), 68 | e -> System.out.print(e.getValue()))); 69 | ``` 70 | 71 | 72 | ### Manipulating Input Mappings ### 73 | 74 | Use cases in this section focus on manipulating input mappings of a control, such as overriding mappings, adding default mappings, intercepting mappings, removing a previously added mapping, etc. 75 | 76 | #### Override a previously defined mapping #### 77 | First install a handler on `node` that invokes `charTyped(String character)` for each typed character. Later override the Tab character with `tabTyped()`. All other characters should still be handled by `charTyped(character)`. 78 | 79 | ```java 80 | Nodes.addInputMap(node, consume(keyTyped(), e -> charTyped(e.getCharacter()))); 81 | 82 | // later override the Tab character 83 | Nodes.addInputMap(node, consume(keyTyped("\t"), e -> tabTyped())); 84 | ``` 85 | 86 | #### Override even a more specific previous mapping #### 87 | The `Control` might have installed a Tab-pressed handler for Tab navigation, but you want to consume all letter, digit and whitespace keys (maybe because you are handling their corresponding key-typed events). The point here is that the previously installed Tab handler is overridden even if it is more specific than the letter/digit/whitespace handler. 88 | 89 | ```java 90 | Nodes.addInputMap(node, consume(keyPressed(TAB), e -> tabNavigation())); 91 | 92 | // later consume all letters, digits and whitespace 93 | Nodes.addInputMap(node, consume(keyPressed(kc -> kc.isLetterKey() || kc.isDigitKey() || kc.isWhitespaceKey()))); 94 | ``` 95 | 96 | #### Add default mappings #### 97 | It has to be possible to add default (or fallback) mappings, i.e. mappings that do not override any previously defined mappings, but take effect if the event is not handled by any previously installed mapping. That is the case for mappings added by skins, since skin is only installed after the user has instantiated the control and customized the mappings. 98 | 99 | The task is to _first_ install the (custom) Tab handler (`tabTyped()`) and _then_ the (default) key typed handler (`charTyped(c)`), but the custom handler should not be overridden by the default handler. 100 | 101 | ```java 102 | // user-specified Tab handler 103 | Nodes.addInputMap(node, consume(keyTyped("\t"), e -> tabTyped())); 104 | 105 | // later in skin 106 | Nodes.addFallbackInputMap(node, consume(keyTyped(), e -> charTyped(e.getCharacter()))); 107 | ``` 108 | 109 | #### Ignore certain events #### 110 | Suppose the skin defines a generic key-pressed handler, but the user needs Tab-pressed to be ignored by the control and bubble up the scene graph. 111 | 112 | ```java 113 | // ignore Tab handler 114 | Nodes.addInputMap(node, ignore(keyPressed(TAB))); 115 | 116 | // later in skin 117 | Nodes.addFallbackInputMap(node, consume(keyPressed(), e -> handleKeyPressed(e))); 118 | ``` 119 | 120 | #### Remove a previously added handler #### 121 | When changing skins, the skin that is being disposed should remove any mappings it has added to the control. Any mappings added before or after the skin was instantiated should stay in effect. In this example, let's add handlers for each of the arrow keys and for mouse move with left button pressed. Later, remove all of them, but leaving any other mappings untouched. 122 | 123 | ```java 124 | // on skin creation 125 | InputMap im = sequence( 126 | consume(keyPressed(UP), e -> moveUp()), 127 | consume(keyPressed(DOWN), e -> moveDown()), 128 | consume(keyPressed(LEFT), e -> moveLeft()), 129 | consume(keyPressed(RIGHT), e -> moveRight()), 130 | consume( 131 | mouseMoved().onlyIf(MouseEvent::isPrimaryButtonDown), 132 | e -> move(e.getX(), e.getY()))); 133 | Nodes.addFallbackInputMap(node, im); 134 | 135 | // on skin disposal 136 | Nodes.removeInputMap(node, im); 137 | ``` 138 | 139 | #### Common post-consumption processing #### 140 | Suppose we have a number of input mappings whose handlers share some common at the end. We would like to factor out this common code to avoid repetition. To give an example, suppose each `move*()` method from the previous example ends with `this.moveCount += 1`. Let's factor out this common code to a single place. (Notice the `ifConsumed`.) 141 | 142 | ```java 143 | InputMap im0 = sequence( 144 | consume(keyPressed(UP), e -> moveUp()), 145 | consume(keyPressed(DOWN), e -> moveDown()), 146 | consume(keyPressed(LEFT), e -> moveLeft()), 147 | consume(keyPressed(RIGHT), e -> moveRight()), 148 | consume( 149 | mouseMoved().onlyIf(MouseEvent::isPrimaryButtonDown), 150 | e -> move(e.getX(), e.getY())) 151 | ).ifConsumed(e - { this.moveCount += 1; }); 152 | 153 | Nodes.addFallbackInputMap(node, im); 154 | ``` 155 | 156 | #### Temporary installation of an InputMap #### 157 | Suppose one wants to use a given `InputMap` for a node's basic behavior, and upon a specific trigger (e.g. the user presses CTRL+Space), we want the node to have a different behavior temporarily. Once another trigger occurs in this "special behavior" context (e.g. the user presses ESC), we want to revert back to the basic behavior. How can this be done? 158 | 159 | ````java 160 | // Basic idea 161 | InputMap anInputMap = // creation code 162 | InputMap aTempInputMap = // creation code 163 | 164 | // install anInputMap 165 | Nodes.addInputMap(node, anInputMap); 166 | // uninstall anInputMap and install aTempInputMap 167 | Nodes.pushInputMap(node, aTempInputMap); 168 | // uninstall aTempInputMap and reinstall anInputMap 169 | Nodes.popInputMap(node); 170 | ```` 171 | 172 | For example: 173 | 174 | ````java 175 | // Special Behavior: refuse to show user a message 176 | InputMap specialBehavior = sequence( 177 | // individual input maps here 178 | consume( 179 | keyPressed("a"), 180 | e -> System.out.println("We aren't showing you what the user pressed :-p"), 181 | 182 | // handler for reverting back to basic behavior 183 | consume( 184 | // trigger that will reinstall basic behavior 185 | keyPressed(ESC), 186 | 187 | // uninstalls this behavior from this node and reinstalls the basic behavior 188 | e -> { 189 | boolean basicBehaviorReinstalled = Nodes.popInputMap(this); 190 | if (!basicBehaviorReinstalled) { 191 | throw new IllegalStateException("Basic behavior was not reinstalled!"); 192 | } 193 | }) 194 | ); 195 | // Basic Behavior: show user a message 196 | InputMap basicBehavior = sequence( 197 | // individual input maps here 198 | consume( 199 | keyPressed("a"), 200 | e -> System.out.println("The user pressed: " + e.getText()), 201 | 202 | // handler for installing special behavior temporarily 203 | consume( 204 | // trigger that will install new behavior 205 | keyPressed(SPACE, CONTROL), 206 | 207 | e -> Nodes.pushInputMap(this, specialBehavior) 208 | ) 209 | ); 210 | Nodes.addInputMap(node, basicBehavior); 211 | 212 | // user presses 'A' 213 | // System outputs: "The user pressed: A" 214 | 215 | // user presses CTRL + Space 216 | // user presses 'A' 217 | // System outputs: "We aren't showing you what the user pressed :-p" 218 | 219 | // user presses 'ESC' 220 | // user presses 'A' 221 | // System outputs: "The user pressed: A" 222 | ```` 223 | 224 | These temporary `InputMap`s can be stacked multiple times, so that one can have multiple contexts: 225 | - basic context 226 | - Up Trigger: when user presses `CTRL+SPACE`, uninstalls this context's behavior and installs `temp context 1` 227 | - temp context 1 228 | - Down Trigger: when user presses `ESC`, uninstalls this context's behavior and reinstalls `basic context` 229 | - Up Trigger: when user presses `CTRL+SPACE`, uninstalls this context's behavior and installs `temp context 2` 230 | - temp context 2 231 | - Down Trigger: when user presses `ESC`, uninstalls this context's behavior and reinstalls `temp context 1` 232 | 233 | ### Structural sharing between input maps ### 234 | Consider a control that defines _m_ input mappings and that there are _n_ instances of this control in the scene. The space complexity of all input mappings of all these controls combined is then _O(n*m)_. The goal is to reduce this complexity to _O(m+n)_ by having a shared structure of complexity _O(m)_ of the _m_ input mappings, and each of the _n_ controls to have an input map that is a constant overhead (_O(1)_) on top of this shared structure. 235 | 236 | This is supported by package `org.fxmisc.wellbehaved.event.template`. 237 | The shared structure is an instance of `InputMapTemplate`. 238 | The API for constructing `InputMapTemplate`s very much copies the API for constructing `InputMap`s that you have seen throughout this document, except the handlers take an additional argument—typically the control or the "behavior". A template can then be _instantiated_ to an `InputMap`, which is a constant overhead wrapper around the template, by providing the control/behavior object. 239 | 240 | **Example:** 241 | 242 | ```java 243 | static final InputMapTemplate INPUT_MAP_TEMPLATE = 244 | unless(TextArea::isDisabled, sequence( 245 | consume(keyPressed(A, SHORTCUT_DOWN), (area, evt) -> area.selectAll()), 246 | consume(keyPressed(C, SHORTCUT_DOWN), (area, evt) -> area.copy()) 247 | /* ... */ 248 | )); 249 | 250 | TextArea area1 = new TextArea(); 251 | TextArea area2 = new TextArea(); 252 | 253 | InputMapTemplate.installFallback(INPUT_MAP_TEMPLATE, area1); 254 | InputMapTemplate.installFallback(INPUT_MAP_TEMPLATE, area2); 255 | ``` 256 | 257 | Notice that `INPUT_MAP_TEMPLATE` is `static` and then added to two `TextArea`s. 258 | 259 | Download 260 | -------- 261 | 262 | Maven artifacts are deployed to Maven Central repository with the following Maven coordinates: 263 | 264 | | Group ID | Artifact ID | Version | 265 | | :--------------------: | :------------: | :-----: | 266 | | org.fxmisc.wellbehaved | wellbehavedfx | 0.3.3 | 267 | 268 | ### Gradle example 269 | 270 | ```groovy 271 | dependencies { 272 | compile group: 'org.fxmisc.wellbehaved', name: 'wellbehavedfx', version: '0.3.3' 273 | } 274 | ``` 275 | 276 | ### Sbt example 277 | 278 | ```scala 279 | libraryDependencies += "org.fxmisc.wellbehaved" % "wellbehavedfx" % "0.3.3" 280 | ``` 281 | 282 | ### Manual download 283 | 284 | [Download](https://oss.sonatype.org/content/groups/public/org/fxmisc/wellbehaved/wellbehavedfx/0.3.3/) the JAR file and place it on your classpath. 285 | 286 | 287 | Links 288 | ------- 289 | 290 | License: [BSD 2-Clause License](http://opensource.org/licenses/BSD-2-Clause) 291 | API documentation: [Javadoc](http://fxmisc.github.io/wellbehaved/javadoc/0.3.3/overview-summary.html) 292 | -------------------------------------------------------------------------------- /src/test/java/org/fxmisc/wellbehaved/event/InputMapTest.java: -------------------------------------------------------------------------------- 1 | package org.fxmisc.wellbehaved.event; 2 | 3 | import static javafx.scene.input.KeyCode.*; 4 | import static javafx.scene.input.KeyEvent.*; 5 | import static org.fxmisc.wellbehaved.event.EventPattern.*; 6 | import static org.fxmisc.wellbehaved.event.InputHandler.Result.*; 7 | import static org.fxmisc.wellbehaved.event.InputMap.*; 8 | import static org.hamcrest.Matchers.*; 9 | import static org.junit.Assert.*; 10 | 11 | import javafx.beans.property.BooleanProperty; 12 | import javafx.beans.property.IntegerProperty; 13 | import javafx.beans.property.SimpleBooleanProperty; 14 | import javafx.beans.property.SimpleIntegerProperty; 15 | import javafx.beans.property.SimpleStringProperty; 16 | import javafx.beans.property.StringProperty; 17 | import javafx.embed.swing.JFXPanel; 18 | import javafx.event.Event; 19 | import javafx.event.EventType; 20 | import javafx.scene.Node; 21 | import javafx.scene.input.InputEvent; 22 | import javafx.scene.input.KeyEvent; 23 | import javafx.scene.input.MouseEvent; 24 | import javafx.scene.layout.Region; 25 | 26 | import org.fxmisc.wellbehaved.event.InputMap.HandlerConsumer; 27 | import org.junit.BeforeClass; 28 | import org.junit.Test; 29 | 30 | import java.util.function.Supplier; 31 | 32 | public class InputMapTest { 33 | 34 | @BeforeClass 35 | public static void setUpBeforeClass() { 36 | new JFXPanel(); // initialize JavaFX 37 | } 38 | 39 | @SuppressWarnings("serial") 40 | private static class FooEvent extends Event { 41 | public static final EventType FOO = new EventType<>("FOO"); 42 | 43 | private final boolean secret; 44 | private final String value; 45 | 46 | public FooEvent(boolean secret, String value) { 47 | super(FOO); 48 | this.secret = secret; 49 | this.value = value; 50 | } 51 | 52 | public boolean isSecret() { 53 | return secret; 54 | } 55 | 56 | public String getValue() { 57 | return value; 58 | } 59 | } 60 | 61 | 62 | public static void dispatch(Event event, InputMap inputMap) { 63 | IntegerProperty matchingHandlers = new SimpleIntegerProperty(0); 64 | 65 | inputMap.forEachEventType(new HandlerConsumer() { 66 | 67 | @Override 68 | public void accept( 69 | EventType t, InputHandler h) { 70 | eventType(t).match(event).ifPresent(evt -> { 71 | h.handle(evt); 72 | matchingHandlers.set(matchingHandlers.get() + 1); 73 | }); 74 | } 75 | 76 | }); 77 | 78 | assertThat(matchingHandlers.get(), lessThanOrEqualTo(1)); 79 | } 80 | 81 | public static void dispatch(Event event, Node node) { 82 | dispatch(event, Nodes.getInputMap(node)); 83 | } 84 | 85 | 86 | @Test 87 | public void overridePreviouslyAddedHandler() { 88 | StringProperty res = new SimpleStringProperty(); 89 | 90 | InputMap im1 = consume(keyPressed(), e -> res.set("handler 1")); 91 | InputMap im2 = consume(keyPressed(A), e -> res.set("handler 2")); 92 | 93 | KeyEvent aPressed = new KeyEvent(KEY_PRESSED, "", "", A, false, false, false, false); 94 | KeyEvent bPressed = new KeyEvent(KEY_PRESSED, "", "", B, false, false, false, false); 95 | 96 | InputMap im = im1.orElse(im2); 97 | dispatch(aPressed, im); 98 | assertEquals("handler 1", res.get()); 99 | dispatch(bPressed, im); 100 | assertEquals("handler 1", res.get()); 101 | 102 | im = im2.orElse(im1); 103 | dispatch(aPressed, im); 104 | assertEquals("handler 2", res.get()); 105 | dispatch(bPressed, im); 106 | assertEquals("handler 1", res.get()); 107 | } 108 | 109 | @Test 110 | public void fallbackHandlerTest() { 111 | StringProperty res = new SimpleStringProperty(); 112 | 113 | InputMap fallback = consume(keyPressed(), e -> res.set("fallback")); 114 | InputMap custom = consume(keyPressed(A), e -> res.set("custom")); 115 | 116 | KeyEvent aPressed = new KeyEvent(KEY_PRESSED, "", "", A, false, false, false, false); 117 | KeyEvent bPressed = new KeyEvent(KEY_PRESSED, "", "", B, false, false, false, false); 118 | 119 | Node node = new Region(); 120 | 121 | // install custom handler first, then fallback 122 | Nodes.addInputMap(node, custom); 123 | Nodes.addFallbackInputMap(node, fallback); 124 | 125 | // check that custom handler is not overridden by fallback handler 126 | dispatch(aPressed, node); 127 | assertEquals("custom", res.get()); 128 | 129 | // check that fallback handler is in effect 130 | dispatch(bPressed, node); 131 | assertEquals("fallback", res.get()); 132 | } 133 | 134 | 135 | @Test 136 | public void ignoreTest() { 137 | StringProperty res = new SimpleStringProperty(); 138 | 139 | InputMap fallback = consume(keyPressed(), e -> res.set("consumed")); 140 | InputMap ignore = ignore (keyPressed(A)); 141 | 142 | KeyEvent aPressed = new KeyEvent(KEY_PRESSED, "", "", A, false, false, false, false); 143 | KeyEvent bPressed = new KeyEvent(KEY_PRESSED, "", "", B, false, false, false, false); 144 | 145 | Node node = new Region(); 146 | 147 | // install ignore handler first, then fallback 148 | Nodes.addInputMap(node, ignore); 149 | Nodes.addFallbackInputMap(node, fallback); 150 | 151 | // check that ignore works 152 | dispatch(aPressed, node); 153 | assertNull(res.get()); 154 | assertFalse(aPressed.isConsumed()); 155 | 156 | // check that other events are not ignored 157 | dispatch(bPressed, node); 158 | assertEquals("consumed", res.get()); 159 | assertTrue(bPressed.isConsumed()); 160 | } 161 | 162 | @Test 163 | public void withoutTest() { 164 | StringProperty res = new SimpleStringProperty(); 165 | 166 | InputMap im1 = consume(keyPressed(B), e -> { res.set("1"); }); 167 | InputMap im2 = consume(keyPressed(A), e -> { res.set("2"); }); 168 | InputMap im3 = consume(keyPressed(A), e -> { res.set("3"); }); 169 | InputMap im4 = process(keyPressed(A), e -> { res.set("4"); return PROCEED; }); 170 | 171 | KeyEvent event = new KeyEvent(KEY_PRESSED, "", "", A, false, false, false, false); 172 | 173 | InputMap im = sequence(im1, im2, im3, im4); 174 | dispatch(event, im); 175 | assertEquals("2", res.get()); 176 | 177 | im = im.without(im2); 178 | event = event.copyFor(null, null); // obtain unconsumed event 179 | dispatch(event, im); 180 | assertEquals("3", res.get()); 181 | 182 | im = im.without(im3); 183 | event = event.copyFor(null, null); // obtain unconsumed event 184 | dispatch(event, im); 185 | assertEquals("4", res.get()); 186 | assertFalse(event.isConsumed()); 187 | } 188 | 189 | @Test 190 | public void whenTest() { 191 | BooleanProperty condition = new SimpleBooleanProperty(false); 192 | 193 | InputMap im = when(condition::get, consume(keyPressed())); 194 | 195 | KeyEvent event = new KeyEvent(KEY_PRESSED, "", "", A, false, false, false, false); 196 | 197 | dispatch(event, im); 198 | assertFalse(event.isConsumed()); 199 | 200 | condition.set(true); 201 | dispatch(event, im); 202 | assertTrue(event.isConsumed()); 203 | } 204 | 205 | @Test 206 | public void removePreviousHandlerTest() { 207 | StringProperty res = new SimpleStringProperty(); 208 | 209 | InputMap fallback = consume(keyPressed(), e -> res.set("fallback")); 210 | InputMap custom = consume(keyPressed(A), e -> res.set("custom")); 211 | 212 | KeyEvent aPressed = new KeyEvent(KEY_PRESSED, "", "", A, false, false, false, false); 213 | KeyEvent bPressed = new KeyEvent(KEY_PRESSED, "", "", B, false, false, false, false); 214 | 215 | Node node = new Region(); 216 | 217 | // install custom handler first, then fallback 218 | Nodes.addInputMap(node, custom); 219 | Nodes.addFallbackInputMap(node, fallback); 220 | 221 | // check that fallback handler works 222 | dispatch(bPressed, node); 223 | assertEquals("fallback", res.get()); 224 | 225 | // remove fallback handler 226 | Nodes.removeInputMap(node, fallback); 227 | res.set(null); 228 | 229 | // check that fallback handler was removed 230 | dispatch(bPressed, node); 231 | assertNull(res.get()); 232 | 233 | // check that custom handler still works 234 | dispatch(aPressed, node); 235 | assertEquals("custom", res.get()); 236 | } 237 | 238 | private void moveUp() {} 239 | private void moveDown() {} 240 | private void moveLeft() {} 241 | private void moveRight() {} 242 | private void move(double x, double y) {} 243 | public void justKidding() { 244 | Node node = new Region(); 245 | 246 | InputMap im = sequence( 247 | consume(keyPressed(UP), e -> moveUp()), 248 | consume(keyPressed(DOWN), e -> moveDown()), 249 | consume(keyPressed(LEFT), e -> moveLeft()), 250 | consume(keyPressed(RIGHT), e -> moveRight()), 251 | consume( 252 | mouseMoved().onlyIf(MouseEvent::isPrimaryButtonDown), 253 | e -> move(e.getX(), e.getY()))); 254 | 255 | Nodes.addFallbackInputMap(node, im); 256 | 257 | Nodes.removeInputMap(node, im); 258 | } 259 | 260 | @Test 261 | public void customEventTest() { 262 | StringProperty res = new SimpleStringProperty(); 263 | 264 | // if event is not secret, assign its value to res. 265 | // Otherwise, don't consume the event. 266 | InputMap im = consume( 267 | eventType(FooEvent.FOO).unless(FooEvent::isSecret), 268 | e -> res.set(e.getValue())); 269 | 270 | FooEvent secret = new FooEvent(true, "Secret"); 271 | FooEvent open = new FooEvent(false, "Open"); 272 | 273 | Node node = new Region(); 274 | Nodes.addInputMap(node, im); 275 | 276 | // check that secret event is not processed or consumed 277 | dispatch(secret, node); 278 | assertNull(res.get()); 279 | assertFalse(secret.isConsumed()); 280 | 281 | // check that open event is processed and consumed 282 | dispatch(open, node); 283 | assertEquals("Open", res.get()); 284 | assertTrue(open.isConsumed()); 285 | } 286 | 287 | @Test 288 | public void ifConsumedTest() { 289 | StringProperty res = new SimpleStringProperty(); 290 | IntegerProperty counter = new SimpleIntegerProperty(0); 291 | 292 | InputMap im = InputMap.sequence( 293 | consume(keyPressed(UP), e -> res.set("Up")), 294 | consume(keyPressed(DOWN), e -> res.set("Down")), 295 | consume(keyPressed(LEFT), e -> res.set("Left")), 296 | consume(keyPressed(RIGHT), e -> res.set("Right"))) 297 | .ifConsumed(e -> counter.set(counter.get() + 1)); 298 | 299 | KeyEvent a = new KeyEvent(KEY_PRESSED, "", "", A, false, false, false, false); 300 | KeyEvent up = new KeyEvent(KEY_PRESSED, "", "", UP, false, false, false, false); 301 | KeyEvent down = new KeyEvent(KEY_PRESSED, "", "", DOWN, false, false, false, false); 302 | KeyEvent left = new KeyEvent(KEY_PRESSED, "", "", LEFT, false, false, false, false); 303 | KeyEvent right = new KeyEvent(KEY_PRESSED, "", "", RIGHT, false, false, false, false); 304 | 305 | dispatch(a, im); 306 | assertNull(res.get()); 307 | assertEquals(0, counter.get()); 308 | assertFalse(a.isConsumed()); 309 | 310 | dispatch(up, im); 311 | assertEquals("Up", res.get()); 312 | assertEquals(1, counter.get()); 313 | assertTrue(up.isConsumed()); 314 | 315 | dispatch(down, im); 316 | assertEquals("Down", res.get()); 317 | assertEquals(2, counter.get()); 318 | assertTrue(down.isConsumed()); 319 | 320 | dispatch(left, im); 321 | assertEquals("Left", res.get()); 322 | assertEquals(3, counter.get()); 323 | assertTrue(left.isConsumed()); 324 | 325 | dispatch(right, im); 326 | assertEquals("Right", res.get()); 327 | assertEquals(4, counter.get()); 328 | assertTrue(right.isConsumed()); 329 | } 330 | 331 | @Test 332 | public void ifIgnoredTest() { 333 | IntegerProperty counter = new SimpleIntegerProperty(0); 334 | 335 | InputMap im = InputMap.sequence( 336 | ignore(keyPressed(UP)), 337 | ignore(keyPressed(DOWN)), 338 | ignore(keyPressed(RIGHT)), 339 | ignore(keyPressed(LEFT)) 340 | ).ifIgnored(e -> counter.set(counter.get() + 1)); 341 | 342 | KeyEvent a = new KeyEvent(KEY_PRESSED, "", "", A, false, false, false, false); 343 | KeyEvent up = new KeyEvent(KEY_PRESSED, "", "", UP, false, false, false, false); 344 | KeyEvent down = new KeyEvent(KEY_PRESSED, "", "", DOWN, false, false, false, false); 345 | KeyEvent left = new KeyEvent(KEY_PRESSED, "", "", LEFT, false, false, false, false); 346 | KeyEvent right = new KeyEvent(KEY_PRESSED, "", "", RIGHT, false, false, false, false); 347 | 348 | dispatch(a, im); 349 | assertEquals(0, counter.get()); 350 | assertFalse(a.isConsumed()); 351 | 352 | dispatch(up, im); 353 | assertEquals(1, counter.get()); 354 | assertFalse(up.isConsumed()); 355 | 356 | dispatch(down, im); 357 | assertEquals(2, counter.get()); 358 | assertFalse(down.isConsumed()); 359 | 360 | dispatch(left, im); 361 | assertEquals(3, counter.get()); 362 | assertFalse(left.isConsumed()); 363 | 364 | dispatch(right, im); 365 | assertEquals(4, counter.get()); 366 | assertFalse(right.isConsumed()); 367 | } 368 | 369 | @Test 370 | public void ifProceededTest() { 371 | StringProperty res = new SimpleStringProperty(); 372 | IntegerProperty counter = new SimpleIntegerProperty(0); 373 | 374 | InputHandler.Result returnVal = InputHandler.Result.PROCEED; 375 | 376 | InputMap im = InputMap.sequence( 377 | consume(keyPressed(A)), 378 | 379 | process(keyPressed(UP), e -> { res.set("Up"); return returnVal; }), 380 | process(keyPressed(DOWN), e -> { res.set("Down"); return returnVal; }), 381 | process(keyPressed(LEFT), e -> { res.set("Left"); return returnVal; }), 382 | process(keyPressed(RIGHT), e -> { res.set("Right"); return returnVal; }) 383 | ).ifProcessed(e -> counter.set(counter.get() + 1)); 384 | 385 | KeyEvent a = new KeyEvent(KEY_PRESSED, "", "", A, false, false, false, false); 386 | KeyEvent up = new KeyEvent(KEY_PRESSED, "", "", UP, false, false, false, false); 387 | KeyEvent down = new KeyEvent(KEY_PRESSED, "", "", DOWN, false, false, false, false); 388 | KeyEvent left = new KeyEvent(KEY_PRESSED, "", "", LEFT, false, false, false, false); 389 | KeyEvent right = new KeyEvent(KEY_PRESSED, "", "", RIGHT, false, false, false, false); 390 | 391 | dispatch(a, im); 392 | assertNull(res.get()); 393 | assertEquals(0, counter.get()); 394 | assertTrue(a.isConsumed()); 395 | 396 | dispatch(up, im); 397 | assertEquals("Up", res.get()); 398 | assertEquals(1, counter.get()); 399 | assertFalse(up.isConsumed()); 400 | 401 | dispatch(down, im); 402 | assertEquals("Down", res.get()); 403 | assertEquals(2, counter.get()); 404 | assertFalse(down.isConsumed()); 405 | 406 | dispatch(left, im); 407 | assertEquals("Left", res.get()); 408 | assertEquals(3, counter.get()); 409 | assertFalse(left.isConsumed()); 410 | 411 | dispatch(right, im); 412 | assertEquals("Right", res.get()); 413 | assertEquals(4, counter.get()); 414 | assertFalse(right.isConsumed()); 415 | } 416 | 417 | @Test 418 | public void pushAndPopInputMap() { 419 | StringProperty res = new SimpleStringProperty(); 420 | 421 | Region node = new Region(); 422 | Nodes.addInputMap(node, InputMap.consume(keyPressed(UP), e -> res.set("Up"))); 423 | Supplier createUpKeyEvent = () -> 424 | new KeyEvent(KEY_PRESSED, "", "", UP, false, false, false, false); 425 | 426 | // regular input map works 427 | KeyEvent up = createUpKeyEvent.get(); 428 | 429 | dispatch(up, node); 430 | assertEquals("Up", res.get()); 431 | assertTrue(up.isConsumed()); 432 | 433 | // temporary input map works 434 | Nodes.pushInputMap(node, InputMap.consume(keyPressed(UP), e -> res.set("Down"))); 435 | up = createUpKeyEvent.get(); 436 | 437 | dispatch(up, node); 438 | assertEquals("Down", res.get()); 439 | assertTrue(up.isConsumed()); 440 | 441 | // popping reinstalls previous input map 442 | Nodes.popInputMap(node); 443 | up = createUpKeyEvent.get(); 444 | 445 | dispatch(up, node); 446 | assertEquals("Up", res.get()); 447 | assertTrue(up.isConsumed()); 448 | 449 | // popping when no temporary input maps exist does nothing 450 | Nodes.popInputMap(node); 451 | up = createUpKeyEvent.get(); 452 | 453 | // set value to something else to insure test works as expected 454 | res.set("Other value"); 455 | 456 | dispatch(up, node); 457 | assertEquals("Up", res.get()); 458 | assertTrue(up.isConsumed()); 459 | } 460 | 461 | } 462 | -------------------------------------------------------------------------------- /src/main/java/org/fxmisc/wellbehaved/event/InputMap.java: -------------------------------------------------------------------------------- 1 | package org.fxmisc.wellbehaved.event; 2 | 3 | import java.util.Arrays; 4 | import java.util.Objects; 5 | import java.util.function.BooleanSupplier; 6 | import java.util.function.Consumer; 7 | import java.util.function.Function; 8 | import java.util.stream.Stream; 9 | 10 | import javafx.event.Event; 11 | import javafx.event.EventType; 12 | 13 | import org.fxmisc.wellbehaved.event.InputHandler.Result; 14 | 15 | /** 16 | * Pattern matching for {@link Event}s. 17 | * 18 | *

General Concept as an Analogy

19 | * 20 | * Think of an {@code InputMap} as a powerful switch statement: 21 | *

 22 |  * switch (eventType) {
 23 |  *     case KEY_PRESSED:    // the basic EventPattern
 24 |  *          doCodeWithEvent(e); // the basic InputHandler
 25 |  *          break;
 26 |  *
 27 |  *     // an EventPattern with a condition
 28 |  *     // it only matches KeyTyped events whose text is a space character
 29 |  *     // other KeyTyped events don't run the space insertion code beow
 30 |  *     case KEY_TYPED e if e.getText().equals(" "):
 31 |  *          addSpaceToText();
 32 |  *          break;
 33 |  *
 34 |  *     // an EventPattern with another condition
 35 |  *     case MOUSE_MOVED && otherNode.isVisible():
 36 |  *          otherNode.hide();
 37 |  *          thirdNode.show();
 38 |  *          break;
 39 |  *
 40 |  *     // runs the same behavior for multiple event types
 41 |  *     case MOUSE_PRESSED || MOUSE_RELEASED || MOUSE_DRAGGED || MOUSE_ENTERED_TARGET:
 42 |  *          showPopupThatSays("You did something with the mouse!")
 43 |  *
 44 |  *     // sometimes there is no "default" behavior and the event bubbles back up
 45 |  *     default:
 46 |  *          break;
 47 |  * }
 48 |  * 
49 | * 50 | *

Types of InputMaps

51 | *

52 | * There are a few types of {@link InputMap}s: 53 | *

54 | *
    55 | *
  • 56 | * The base ones: {@link #ignore(EventType)}, {@link #consume(EventType)}, and 57 | * {@link #process(EventType, Function)} (and all their variations). 58 | *
  • 59 | *
  • 60 | * The composable ones: {@link #sequence(InputMap[])}/{@link #orElse(InputMap)}, 61 | * {@link #upCast(InputMap)}, and {@link #without(InputMap)}. 62 | *
  • 63 | *
  • 64 | * The condition-adding ones: {@link #when(BooleanSupplier, InputMap)} and 65 | * {@link #unless(BooleanSupplier, InputMap)} 66 | *
  • 67 | *
  • 68 | * (convenience) Consume with conditions: 69 | * {@link #consumeWhen(EventType, BooleanSupplier, Consumer)} 70 | * and {@link #consumeUnless(EventType, BooleanSupplier, Consumer)} 71 | *
  • 72 | *
  • 73 | * The post-conditions: {@link #ifIgnored(Consumer)}, {@link #ifProcessed(Consumer)}, 74 | * {@link #ifConsumed(Consumer)}. 75 | *
  • 76 | *
77 | * 78 | *

Examples

79 | *

 80 |  * InputMap<KeyEvent> typeTheLetter = consume(
 81 |  *      // event pattern (e.g. the "case" line in a switch statement)
 82 |  *      keyTyped(),
 83 |  *
 84 |  *      // input handler (e.g. the "block of code" for a case in a switch statement
 85 |  *      e -> textField.addLetter(e.getText)
 86 |  * );
 87 |  *
 88 |  * InputMap<KeyEvent> copy = consume(
 89 |  *      anyOf(
 90 |  *          keyPressed(KeyCode.C, KeyCode.SHORTCUT),
 91 |  *          keyPressed(KeyCode.COPY)
 92 |  *      ),
 93 |  *      e -> textField.copyToClipboard()
 94 |  * );
 95 |  *
 96 |  * InputMap<KeyEvent> pasteThenClearTextField = consume(
 97 |  *      // event pattern
 98 |  *      anyOf(
 99 |  *          keyPressed(KeyCode.V, KeyCode.SHORTCUT),
100 |  *          keyPressed(KeyCode.PASTE)
101 |  *      ),
102 |  *      e -> textField.copyToClipboard()
103 |  * ).ifConsumed(ignoreMe -> textField.clearText();
104 |  *
105 |  * InputMap<CustomEvent> someCustomEventInputMap = // imagine I put something here...
106 |  *
107 |  * // notice the very generic "Event" event type
108 |  * InputMap<? super Event> allBehaviorForCustomTextField = sequence(
109 |  *      // when an event occurs, check whether the below input map's EventPattern matches the given event
110 |  *      typeTheLetter,
111 |  *      // only pattern match against the below input map if the previous one didn't match
112 |  *      copy,
113 |  *      // check this one only if the above two didn't match
114 |  *      pasteThenClearTextField,
115 |  *
116 |  *      someCustomEventInputMap
117 |  *
118 |  *      // If reach this part, none of them matched, so do nothing
119 |  * );
120 |  *
121 |  * // at this point, "allBehaviorForCustomTextField" could be installed into a Node
122 |  * Nodes.addFallbackInputMap(node, allBehaviorForCustomTextField);
123 |  *
124 |  * // when user types a letter, the text field adds that letter
125 |  * // when user moves the mouse over the text field, nothing happens
126 |  * 
127 | * 128 | *

Last Words

129 | * 130 | *

When creating a subclass of a node that will be instantiated multiple times, use 131 | * {@link org.fxmisc.wellbehaved.event.template.InputMapTemplate} to create an {@link InputMap} for each 132 | * created {@link javafx.scene.Node} as this saves some memory. See the template class' javadoc for more details.

133 | * 134 | * @param type of events that this {@linkplain InputMap} may handle. 135 | * That is, {@code InputMap} certainly does not handle any events that 136 | * are not of type {@code E}; it does not mean it handles any 137 | * event of type {@code E}. 138 | */ 139 | @FunctionalInterface 140 | public interface InputMap { 141 | 142 | static final InputMap EMPTY = handlerConsumer -> {}; 143 | static InputMap empty() { return (InputMap) EMPTY; } 144 | 145 | @FunctionalInterface 146 | static interface HandlerConsumer { 147 | void accept(EventType t, InputHandler h); 148 | } 149 | 150 | /** 151 | * For each {@link EventPattern} that matches the given event, run the corresponding {@link InputHandler} 152 | */ 153 | void forEachEventType(HandlerConsumer f); 154 | 155 | /** 156 | * Shorthand for {@link #sequence(InputMap[]) sequence(this, that)} 157 | */ 158 | default InputMap orElse(InputMap that) { 159 | return sequence(this, that); 160 | } 161 | 162 | /** 163 | * Returns an InputMap that does nothing when {@code this.equals(that)}; otherwise, returns this input map. 164 | */ 165 | default InputMap without(InputMap that) { 166 | return this.equals(that) ? empty() : this; 167 | } 168 | 169 | /** 170 | * Executes some additional handler if the event was consumed (e.g. {@link InputHandler#process(Event)} returns 171 | * {@link Result#CONSUME}). 172 | */ 173 | default InputMap ifConsumed(Consumer postConsumption) { 174 | return postResult(this, Result.CONSUME, postConsumption); 175 | } 176 | 177 | /** 178 | * Executes some additional handler if the event was ignored (e.g. {@link InputHandler#process(Event)} returns 179 | * {@link Result#IGNORE}). 180 | */ 181 | default InputMap ifIgnored(Consumer postIgnore) { 182 | return postResult(this, Result.IGNORE, postIgnore); 183 | } 184 | 185 | /** 186 | * Executes some additional handler if the event was matched but not consumed 187 | * (e.g. {@link InputHandler#process(Event)} returns {@link Result#PROCEED}). 188 | */ 189 | default InputMap ifProcessed(Consumer postProceed) { 190 | return postResult(this, Result.PROCEED, postProceed); 191 | } 192 | 193 | static InputMap postResult(InputMap map, Result checkedResult, Consumer postDesiredResult) { 194 | return handlerConsumer -> map.forEachEventType(new HandlerConsumer() { 195 | 196 | @Override 197 | public void accept(EventType t, InputHandler h) { 198 | InputHandler h2 = e -> { 199 | Result res = h.process(e); 200 | if(res == checkedResult) { 201 | postDesiredResult.accept(e); 202 | } 203 | return res; 204 | }; 205 | handlerConsumer.accept(t, h2); 206 | } 207 | 208 | }); 209 | } 210 | 211 | static InputMap upCast(InputMap inputMap) { 212 | // Unsafe cast is justified by this type-safe equivalent expression: 213 | // InputMap res = f -> inputMap.forEachEventType(f); 214 | @SuppressWarnings("unchecked") 215 | InputMap res = (InputMap) inputMap; 216 | return res; 217 | } 218 | 219 | /** 220 | * Creates a single InputMap that pattern matches a given event type against all the given input maps. This 221 | * is often the InputMap installed on a given node since it contains all the other InputMaps. 222 | */ 223 | @SafeVarargs 224 | static InputMap sequence(InputMap... inputMaps) { 225 | return new InputMapChain<>(inputMaps); 226 | } 227 | 228 | /** 229 | * If the given {@link EventPattern} matches the given event type, runs the given action, and then attempts 230 | * to pattern match the event type with the next {@code InputMap} (if one exists). 231 | */ 232 | public static InputMap process( 233 | EventPattern eventPattern, 234 | Function action) { 235 | return new PatternActionMap<>(eventPattern, action); 236 | } 237 | 238 | /** 239 | * When the given event type occurs, runs the given action, and then attempts 240 | * to pattern match the event type with the next {@code InputMap} (if one exists). 241 | */ 242 | public static InputMap process( 243 | EventType eventType, 244 | Function action) { 245 | return process(EventPattern.eventType(eventType), action); 246 | } 247 | 248 | /** 249 | * If the given {@link EventPattern} matches the given event type, runs the given action, consumes the event, 250 | * and does not attempt to match additional {@code InputMap}s (if they exist). 251 | */ 252 | public static InputMap consume( 253 | EventPattern eventPattern, 254 | Consumer action) { 255 | return process(eventPattern, u -> { 256 | action.accept(u); 257 | return Result.CONSUME; 258 | }); 259 | } 260 | 261 | /** 262 | * When the given event type occurs, runs the given action, consumes the event, 263 | * and does not attempt to match additional {@code InputMap}s (if they exist). 264 | */ 265 | public static InputMap consume( 266 | EventType eventType, 267 | Consumer action) { 268 | return consume(EventPattern.eventType(eventType), action); 269 | } 270 | 271 | /** 272 | * If the given {@link EventPattern} matches the given event type, 273 | * consumes the event and does not attempt to match additional 274 | * {@code InputMap}s (if they exist). 275 | */ 276 | public static InputMap consume( 277 | EventPattern eventPattern) { 278 | return process(eventPattern, u -> Result.CONSUME); 279 | } 280 | 281 | /** 282 | * When the given event type occurs, consumes the event and does not attempt 283 | * to match additional {@code InputMap}s (if they exist). 284 | */ 285 | public static InputMap consume( 286 | EventType eventType) { 287 | return consume(EventPattern.eventType(eventType)); 288 | } 289 | 290 | /** 291 | * If the given {@link EventPattern} matches the given event type and {@code condition} is true, 292 | * consumes the event and does not attempt to match additional 293 | * {@code InputMap}s (if they exist). 294 | */ 295 | public static InputMap consumeWhen( 296 | EventPattern eventPattern, 297 | BooleanSupplier condition, 298 | Consumer action) { 299 | return process(eventPattern, u -> { 300 | if(condition.getAsBoolean()) { 301 | action.accept(u); 302 | return Result.CONSUME; 303 | } else { 304 | return Result.PROCEED; 305 | } 306 | }); 307 | } 308 | 309 | /** 310 | * When the given event type occurs and {@code condition} is true, 311 | * consumes the event and does not attempt to match additional 312 | * {@code InputMap}s (if they exist). 313 | */ 314 | public static InputMap consumeWhen( 315 | EventType eventType, 316 | BooleanSupplier condition, 317 | Consumer action) { 318 | return consumeWhen(EventPattern.eventType(eventType), condition, action); 319 | } 320 | 321 | /** 322 | * If the given {@link EventPattern} matches the given event type and {@code condition} is false, 323 | * consumes the event and does not attempt to match additional 324 | * {@code InputMap}s (if they exist). If {@code condition} is true, continues to try to pattern match 325 | * the event type with the next {@code InputMap} (if one exists). 326 | */ 327 | public static InputMap consumeUnless( 328 | EventPattern eventPattern, 329 | BooleanSupplier condition, 330 | Consumer action) { 331 | return consumeWhen(eventPattern, () -> !condition.getAsBoolean(), action); 332 | } 333 | 334 | /** 335 | * When the given event type occurs and {@code condition} is false, 336 | * consumes the event and does not attempt to match additional 337 | * {@code InputMap}s (if they exist). If {@code condition} is true, continues to try to pattern match 338 | * the event type with the next {@code InputMap} (if one exists). 339 | */ 340 | public static InputMap consumeUnless( 341 | EventType eventType, 342 | BooleanSupplier condition, 343 | Consumer action) { 344 | return consumeUnless(EventPattern.eventType(eventType), condition, action); 345 | } 346 | 347 | /** 348 | * If the given {@link EventPattern} matches the given event type, does nothing and does not attempt 349 | * to match additional {@code InputMap}s (if they exist). 350 | */ 351 | public static InputMap ignore( 352 | EventPattern eventPattern) { 353 | return new PatternActionMap<>(eventPattern, PatternActionMap.CONST_IGNORE); 354 | } 355 | 356 | /** 357 | * When the given event type occurs, does nothing and does not attempt to match additional 358 | * {@code InputMap}s (if they exist). 359 | */ 360 | public static InputMap ignore( 361 | EventType eventType) { 362 | return ignore(EventPattern.eventType(eventType)); 363 | } 364 | 365 | /** 366 | * When the given {@code condition} is true, pattern matches the event with the given {@link InputMap} or 367 | * proceeds to the next {@code InputMap} (if it exists). 368 | */ 369 | public static InputMap when( 370 | BooleanSupplier condition, InputMap im) { 371 | 372 | return new InputMap() { 373 | 374 | @Override 375 | public void forEachEventType(HandlerConsumer f) { 376 | HandlerConsumer g = new HandlerConsumer() { 377 | 378 | @Override 379 | public void accept( 380 | EventType t, InputHandler h) { 381 | f.accept(t, evt -> condition.getAsBoolean() ? h.process(evt) : Result.PROCEED); 382 | } 383 | 384 | }; 385 | 386 | im.forEachEventType(g); 387 | } 388 | }; 389 | } 390 | 391 | /** 392 | * When the given {@code condition} is false, pattern matches the event with the given {@link InputMap} or 393 | * proceeds to the next {@code InputMap} (if it exists). 394 | */ 395 | public static InputMap unless( 396 | BooleanSupplier condition, InputMap im) { 397 | return when(() -> !condition.getAsBoolean(), im); 398 | } 399 | } 400 | 401 | class PatternActionMap implements InputMap { 402 | static final Function CONST_IGNORE = x -> Result.IGNORE; 403 | 404 | private final EventPattern pattern; 405 | private final Function action; 406 | 407 | PatternActionMap(EventPattern pattern, Function action) { 408 | this.pattern = pattern; 409 | this.action = action; 410 | } 411 | 412 | @Override 413 | public void forEachEventType(HandlerConsumer f) { 414 | InputHandler h = t -> pattern.match(t).map(action::apply).orElse(Result.PROCEED); 415 | pattern.getEventTypes().forEach(et -> f.accept(et, h)); 416 | } 417 | 418 | @Override 419 | public boolean equals(Object other) { 420 | if(other instanceof PatternActionMap) { 421 | PatternActionMap that = (PatternActionMap) other; 422 | return Objects.equals(this.pattern, that.pattern) 423 | && Objects.equals(this.action, that.action); 424 | } else { 425 | return false; 426 | } 427 | } 428 | 429 | @Override 430 | public int hashCode() { 431 | return Objects.hash(pattern, action); 432 | } 433 | } 434 | 435 | class InputMapChain implements InputMap { 436 | private final InputMap[] inputMaps; 437 | 438 | @SafeVarargs 439 | InputMapChain(InputMap... inputMaps) { 440 | this.inputMaps = inputMaps; 441 | } 442 | 443 | @Override 444 | public void forEachEventType(HandlerConsumer f) { 445 | InputHandlerMap ihm = new InputHandlerMap(); 446 | for(InputMap im: inputMaps) { 447 | im.forEachEventType(ihm::insertAfter); 448 | } 449 | ihm.forEach(f); 450 | } 451 | 452 | @Override 453 | public InputMap without(InputMap that) { 454 | if(this.equals(that)) { 455 | return InputMap.empty(); 456 | } else { 457 | @SuppressWarnings("unchecked") 458 | InputMap[] ims = (InputMap[]) Stream.of(inputMaps) 459 | .map(im -> im.without(that)) 460 | .filter(im -> im != EMPTY) 461 | .toArray(n -> new InputMap[n]); 462 | switch(ims.length) { 463 | case 0: return InputMap.empty(); 464 | case 1: return InputMap.upCast(ims[0]); 465 | default: return new InputMapChain<>(ims); 466 | } 467 | } 468 | } 469 | 470 | @Override 471 | public boolean equals(Object other) { 472 | if(other instanceof InputMapChain) { 473 | InputMapChain that = (InputMapChain) other; 474 | return Arrays.equals(this.inputMaps, that.inputMaps); 475 | } else { 476 | return false; 477 | } 478 | } 479 | 480 | @Override 481 | public int hashCode() { 482 | return Arrays.hashCode(inputMaps); 483 | } 484 | } -------------------------------------------------------------------------------- /src/main/java/org/fxmisc/wellbehaved/event/template/InputMapTemplate.java: -------------------------------------------------------------------------------- 1 | package org.fxmisc.wellbehaved.event.template; 2 | 3 | import java.util.Arrays; 4 | import java.util.Objects; 5 | import java.util.function.BiConsumer; 6 | import java.util.function.BiFunction; 7 | import java.util.function.Function; 8 | import java.util.function.Predicate; 9 | 10 | import javafx.event.Event; 11 | import javafx.event.EventType; 12 | import javafx.scene.Node; 13 | 14 | import org.fxmisc.wellbehaved.event.EventPattern; 15 | import org.fxmisc.wellbehaved.event.InputHandler; 16 | import org.fxmisc.wellbehaved.event.InputHandler.Result; 17 | import org.fxmisc.wellbehaved.event.InputMap; 18 | import org.fxmisc.wellbehaved.event.Nodes; 19 | 20 | /** 21 | * See {@link InputMap} for an explanation. This simply turns that concept into a template that can be used 22 | * to add the same {@link InputMap} to multiple instances of the same class. 23 | * 24 | *

Adding a template to a class

25 | * 26 | *

27 | * Given a class, the InputMapTemplate code should be created in a {@code static} block and then instantiate 28 | * itself in the node's constructor: 29 | *

30 | *

 31 |  * public class CustomTextField extends TextField {
 32 |  *
 33 |  *     private final static InputMapTemplate<? super Event> BEHAVIOR;
 34 |  *
 35 |  *     static {
 36 |  *
 37 |  *          // creating InputMapTemplates by
 38 |  *          InputMapTemplate<CustomTextField, ? extends KeyEvent> keyEventBehavior = sequence(
 39 |  *              consume(keyTyped(), (field, event) -> field.setText(event.getText()),
 40 |  *              consume(
 41 |  *                  anyOf(
 42 |  *                      mousePressed(),
 43 |  *                      mouseMoved(),
 44 |  *                      mouseReleased()
 45 |  *                  ),
 46 |  *                  (field, event) -> field.setText("Mouse event detected! Event: " + event)
 47 |  *              )
 48 |  *          );
 49 |  *
 50 |  *          // Other InputMapTemplates (though these don't need to be broken up by
 51 |  *          // KeyEvent, MouseEvent, etc. They could be interwoven depending on your desired behavior)
 52 |  *          InputMapTemplate<CustomTextField, ? extends MouseEvent> mouseEventBehavior = sequence(
 53 |  *              // other InputMapTemplates go here...
 54 |  *          );
 55 |  *
 56 |  *          InputMapTemplate<CustomTextField, ? extends Event> otherCustomEventBehavior = sequence(
 57 |  *              // other InputMapTemplates go here...
 58 |  *          );
 59 |  *
 60 |  *          // Tying all of them together into one final InputMapTemplate
 61 |  *          BEHAVIOR = sequence(
 62 |  *              keyEventBehavior,
 63 |  *              mouseEventBehavior,
 64 |  *              otherCustomEventBehavior
 65 |  *          );
 66 |  *     }
 67 |  *
 68 |  *     public CustomTextField(Object[] args) {
 69 |  *         super(args);
 70 |  *         // other constructor stuff here
 71 |  *
 72 |  *         // Install InputMapTemplate onto this node here via one of the two "install" methods
 73 |  *         // described after this code block
 74 |  *     }
 75 |  *
 76 |  *     // rest of the class
 77 |  * }
 78 |  * 
79 | *

80 | * The InputMapTemplate can be instantiated as a default behavior ({@link #installFallback(InputMapTemplate, Node)} 81 | * or as something that overrides prior default behavior ({@link #installOverride(InputMapTemplate, Node)} (or 82 | * their variants). Likewise, it can be removed via {@link #uninstall(InputMapTemplate, Node)}. 83 | *

84 | * @param the type of the object that will be passed into the {@link InputHandlerTemplate}'s block of code. 85 | * @param the event type for which this InputMap's {@link EventPattern} matches 86 | */ 87 | public abstract class InputMapTemplate { 88 | 89 | @FunctionalInterface 90 | public static interface HandlerTemplateConsumer { 91 | 92 | void accept( 93 | EventType t, 94 | InputHandlerTemplate h); 95 | 96 | 97 | static HandlerTemplateConsumer from( 98 | InputMap.HandlerConsumer hc, S target) { 99 | return new HandlerTemplateConsumer() { 100 | @Override 101 | public void accept( 102 | EventType t, InputHandlerTemplate h) { 103 | hc.accept(t, evt -> h.process(target, evt)); 104 | 105 | } 106 | }; 107 | } 108 | } 109 | 110 | private InputHandlerTemplateMap inputHandlerTemplates = null; 111 | 112 | public final void forEachEventType(HandlerTemplateConsumer f) { 113 | if(inputHandlerTemplates == null) { 114 | inputHandlerTemplates = getInputHandlerTemplateMap(); 115 | } 116 | inputHandlerTemplates.forEach(f); 117 | } 118 | 119 | /** 120 | * Shorthand for {@link #sequence(InputMapTemplate[])} sequence(this, that)} 121 | */ 122 | public final InputMapTemplate orElse(InputMapTemplate that) { 123 | return sequence(this, that); 124 | } 125 | 126 | /** 127 | * Converts this InputMapTemplate into an {@link InputMap} for the given {@code target} 128 | */ 129 | public final InputMap instantiate(S target) { 130 | return new InputMapTemplateInstance<>(this, target); 131 | } 132 | 133 | protected abstract InputHandlerTemplateMap getInputHandlerTemplateMap(); 134 | 135 | 136 | static InputMapTemplate upCast(InputMapTemplate imt) { 137 | @SuppressWarnings("unchecked") 138 | InputMapTemplate res = (InputMapTemplate) imt; 139 | return res; 140 | } 141 | 142 | /** 143 | * Creates a single InputMapTemplate that pattern matches a given event type against all the given 144 | * InputMapTemplates. This is often the InputMapTemplate installed on a given node since it contains all 145 | * the other InputMapTemplates. 146 | */ 147 | @SafeVarargs 148 | public static InputMapTemplate sequence(InputMapTemplate... templates) { 149 | return new TemplateChain<>(templates); 150 | } 151 | 152 | /** 153 | * If the given {@link EventPattern} matches the given event type, runs the given action, and then attempts 154 | * to pattern match the event type with the next {@code InputMap} (if one exists). 155 | */ 156 | public static InputMapTemplate process( 157 | EventPattern eventPattern, 158 | BiFunction action) { 159 | return new PatternActionTemplate<>(eventPattern, action); 160 | } 161 | 162 | /** 163 | * When the given event type occurs, runs the given action, and then attempts 164 | * to pattern match the event type with the next {@code InputMap} (if one exists). 165 | */ 166 | public static InputMapTemplate process( 167 | EventType eventType, 168 | BiFunction action) { 169 | return process(EventPattern.eventType(eventType), action); 170 | } 171 | 172 | /** 173 | * Executes some additional handler if the event was consumed (e.g. {@link InputHandler#process(Event)} returns 174 | * {@link Result#CONSUME}). 175 | */ 176 | public InputMapTemplate ifConsumed(BiConsumer postConsumption) { 177 | return postResult(Result.CONSUME, postConsumption); 178 | } 179 | 180 | /** 181 | * Executes some additional handler if the event was ignored 182 | * (e.g. {@link InputHandlerTemplate#process(Object, Event)} returns {@link Result#IGNORE}). 183 | */ 184 | public InputMapTemplate ifIgnored(BiConsumer postIgnore) { 185 | return postResult(Result.IGNORE, postIgnore); 186 | } 187 | 188 | /** 189 | * Executes some additional handler if the event was consumed (e.g. {@link InputHandler#process(Event)} returns 190 | * {@link Result#CONSUME}). 191 | */ 192 | public InputMapTemplate ifProcessed(BiConsumer postProceed) { 193 | return postResult(Result.PROCEED, postProceed); 194 | } 195 | 196 | private InputMapTemplate postResult(Result checkedResult, BiConsumer postDesiredResult) { 197 | return new InputMapTemplate() { 198 | @Override 199 | protected InputHandlerTemplateMap getInputHandlerTemplateMap() { 200 | return InputMapTemplate.this.getInputHandlerTemplateMap().map(iht -> { 201 | return (s, evt) -> { 202 | Result res = iht.process(s, evt); 203 | if (res == checkedResult) { 204 | postDesiredResult.accept(s, evt); 205 | } 206 | return res; 207 | }; 208 | }); 209 | } 210 | }; 211 | } 212 | 213 | /** 214 | * If the given {@link EventPattern} matches the given event type, runs the given action, consumes the event, 215 | * and does not attempt to match additional {@code InputMap}s (if they exist). 216 | */ 217 | public static InputMapTemplate consume( 218 | EventPattern eventPattern, 219 | BiConsumer action) { 220 | return process(eventPattern, (s, u) -> { 221 | action.accept(s, u); 222 | return Result.CONSUME; 223 | }); 224 | } 225 | 226 | /** 227 | * When the given event type occurs, runs the given action, consumes the event, 228 | * and does not attempt to match additional {@code InputMap}s (if they exist). 229 | */ 230 | public static InputMapTemplate consume( 231 | EventType eventType, 232 | BiConsumer action) { 233 | return consume(EventPattern.eventType(eventType), action); 234 | } 235 | 236 | /** 237 | * If the given {@link EventPattern} matches the given event type, 238 | * consumes the event and does not attempt to match additional 239 | * {@code InputMap}s (if they exist). 240 | */ 241 | public static InputMapTemplate consume( 242 | EventPattern eventPattern) { 243 | return process(eventPattern, (s, u) -> Result.CONSUME); 244 | } 245 | 246 | /** 247 | * When the given event type occurs, consumes the event and does not attempt 248 | * to match additional {@code InputMap}s (if they exist). 249 | */ 250 | public static InputMapTemplate consume( 251 | EventType eventType) { 252 | return consume(EventPattern.eventType(eventType)); 253 | } 254 | 255 | /** 256 | * If the given {@link EventPattern} matches the given event type and {@code condition} is true, 257 | * consumes the event and does not attempt to match additional 258 | * {@code InputMap}s (if they exist). 259 | */ 260 | public static InputMapTemplate consumeWhen( 261 | EventPattern eventPattern, 262 | Predicate condition, 263 | BiConsumer action) { 264 | return process(eventPattern, (s, u) -> { 265 | if(condition.test(s)) { 266 | action.accept(s, u); 267 | return Result.CONSUME; 268 | } else { 269 | return Result.PROCEED; 270 | } 271 | }); 272 | } 273 | 274 | /** 275 | * When the given event type occurs and {@code condition} is true, 276 | * consumes the event and does not attempt to match additional 277 | * {@code InputMap}s (if they exist). 278 | */ 279 | public static InputMapTemplate consumeWhen( 280 | EventType eventType, 281 | Predicate condition, 282 | BiConsumer action) { 283 | return consumeWhen(EventPattern.eventType(eventType), condition, action); 284 | } 285 | 286 | /** 287 | * If the given {@link EventPattern} matches the given event type and {@code condition} is false, 288 | * consumes the event and does not attempt to match additional 289 | * {@code InputMap}s (if they exist). If {@code condition} is true, continues to try to pattern match 290 | * the event type with the next {@code InputMap} (if one exists). 291 | */ 292 | public static InputMapTemplate consumeUnless( 293 | EventPattern eventPattern, 294 | Predicate condition, 295 | BiConsumer action) { 296 | return consumeWhen(eventPattern, condition.negate(), action); 297 | } 298 | 299 | /** 300 | * When the given event type occurs and {@code condition} is false, 301 | * consumes the event and does not attempt to match additional 302 | * {@code InputMap}s (if they exist). If {@code condition} is true, continues to try to pattern match 303 | * the event type with the next {@code InputMap} (if one exists). 304 | */ 305 | public static InputMapTemplate consumeUnless( 306 | EventType eventType, 307 | Predicate condition, 308 | BiConsumer action) { 309 | return consumeUnless(EventPattern.eventType(eventType), condition, action); 310 | } 311 | 312 | /** 313 | * If the given {@link EventPattern} matches the given event type, does nothing and does not attempt 314 | * to match additional {@code InputMap}s (if they exist). 315 | */ 316 | public static InputMapTemplate ignore( 317 | EventPattern eventPattern) { 318 | return new PatternActionTemplate<>(eventPattern, PatternActionTemplate.CONST_IGNORE); 319 | } 320 | 321 | /** 322 | * When the given event type occurs, does nothing and does not attempt to match additional 323 | * {@code InputMap}s (if they exist). 324 | */ 325 | public static InputMapTemplate ignore( 326 | EventType eventType) { 327 | return ignore(EventPattern.eventType(eventType)); 328 | } 329 | 330 | /** 331 | * When the given {@code condition} is true, pattern matches the event with the given {@link InputMap} or 332 | * proceeds to the next {@code InputMap} (if it exists). 333 | */ 334 | public static InputMapTemplate when( 335 | Predicate condition, InputMapTemplate imt) { 336 | 337 | return new InputMapTemplate() { 338 | @Override 339 | protected InputHandlerTemplateMap getInputHandlerTemplateMap() { 340 | return imt.getInputHandlerTemplateMap().map( 341 | h -> (s, evt) -> condition.test(s) ? h.process(s, evt) : Result.PROCEED); 342 | } 343 | }; 344 | } 345 | 346 | /** 347 | * When the given {@code condition} is false, pattern matches the event with the given {@link InputMap} or 348 | * proceeds to the next {@code InputMap} (if it exists). 349 | */ 350 | public static InputMapTemplate unless( 351 | Predicate condition, InputMapTemplate imt) { 352 | return when(condition.negate(), imt); 353 | } 354 | 355 | public static InputMapTemplate lift( 356 | InputMapTemplate imt, 357 | Function f) { 358 | 359 | return new InputMapTemplate() { 360 | @Override 361 | protected InputHandlerTemplateMap getInputHandlerTemplateMap() { 362 | return imt.getInputHandlerTemplateMap().map( 363 | h -> (s, evt) -> h.process(f.apply(s), evt)); 364 | } 365 | }; 366 | } 367 | 368 | /** 369 | * Instantiates the input map and installs it into the node via {@link Nodes#addInputMap(Node, InputMap)} 370 | */ 371 | public static void installOverride(InputMapTemplate imt, S node) { 372 | Nodes.addInputMap(node, imt.instantiate(node)); 373 | } 374 | 375 | /** 376 | * Instantiates the input map and installs it into the node via {@link Nodes#addInputMap(Node, InputMap)} 377 | */ 378 | public static void installOverride(InputMapTemplate imt, S target, Function getNode) { 379 | Nodes.addInputMap(getNode.apply(target), imt.instantiate(target)); 380 | } 381 | 382 | /** 383 | * Instantiates the input map and installs it into the node via {@link Nodes#addFallbackInputMap(Node, InputMap)} 384 | */ 385 | public static void installFallback(InputMapTemplate imt, S node) { 386 | Nodes.addFallbackInputMap(node, imt.instantiate(node)); 387 | } 388 | 389 | /** 390 | * Instantiates the input map and installs it into the node via {@link Nodes#addFallbackInputMap(Node, InputMap)} 391 | */ 392 | public static void installFallback(InputMapTemplate imt, S target, Function getNode) { 393 | Nodes.addFallbackInputMap(getNode.apply(target), imt.instantiate(target)); 394 | } 395 | 396 | /** 397 | * Removes the input map template's instance from the given node. 398 | */ 399 | public static void uninstall(InputMapTemplate imt, S node) { 400 | Nodes.removeInputMap(node, imt.instantiate(node)); 401 | } 402 | 403 | /** 404 | * Removes the input map template's instance from the given node. 405 | */ 406 | public static void uninstall(InputMapTemplate imt, S target, Function getNode) { 407 | Nodes.removeInputMap(getNode.apply(target), imt.instantiate(target)); 408 | } 409 | } 410 | 411 | class PatternActionTemplate extends InputMapTemplate { 412 | static final BiFunction CONST_IGNORE = (x, y) -> Result.IGNORE; 413 | 414 | private final EventPattern pattern; 415 | private final BiFunction action; 416 | 417 | PatternActionTemplate(EventPattern pattern, BiFunction action) { 418 | this.pattern = pattern; 419 | this.action = action; 420 | } 421 | 422 | @Override 423 | protected InputHandlerTemplateMap getInputHandlerTemplateMap() { 424 | InputHandlerTemplateMap ihtm = new InputHandlerTemplateMap<>(); 425 | InputHandlerTemplate iht = (s, t) -> pattern.match(t).map(u -> action.apply(s, u)).orElse(Result.PROCEED); 426 | pattern.getEventTypes().forEach(et -> ihtm.insertAfter(et, iht)); 427 | return ihtm; 428 | } 429 | 430 | @Override 431 | public boolean equals(Object other) { 432 | if(other instanceof PatternActionTemplate) { 433 | PatternActionTemplate that = (PatternActionTemplate) other; 434 | return Objects.equals(this.pattern, that.pattern) 435 | && Objects.equals(this.action, that.action); 436 | } else { 437 | return false; 438 | } 439 | } 440 | 441 | @Override 442 | public int hashCode() { 443 | return Objects.hash(pattern, action); 444 | } 445 | } 446 | 447 | class TemplateChain extends InputMapTemplate { 448 | private final InputMapTemplate[] templates; 449 | 450 | @SafeVarargs 451 | TemplateChain(InputMapTemplate... templates) { 452 | this.templates = templates; 453 | } 454 | 455 | @Override 456 | protected InputHandlerTemplateMap getInputHandlerTemplateMap() { 457 | InputHandlerTemplateMap ihtm = new InputHandlerTemplateMap<>(); 458 | for(InputMapTemplate imt: templates) { 459 | imt.getInputHandlerTemplateMap().forEach(ihtm::insertAfter); 460 | } 461 | return ihtm; 462 | } 463 | 464 | @Override 465 | public boolean equals(Object other) { 466 | if(other instanceof TemplateChain) { 467 | TemplateChain that = (TemplateChain) other; 468 | return Arrays.equals(this.templates, that.templates); 469 | } else { 470 | return false; 471 | } 472 | } 473 | 474 | @Override 475 | public int hashCode() { 476 | return Arrays.hashCode(templates); 477 | } 478 | } 479 | 480 | class InputMapTemplateInstance implements InputMap { 481 | private final InputMapTemplate template; 482 | private final S target; 483 | 484 | InputMapTemplateInstance(InputMapTemplate template, S target) { 485 | this.template = template; 486 | this.target = target; 487 | } 488 | 489 | @Override 490 | public void forEachEventType(HandlerConsumer hc) { 491 | template.forEachEventType( 492 | InputMapTemplate.HandlerTemplateConsumer.from(hc, target)); 493 | } 494 | 495 | @Override 496 | public boolean equals(Object other) { 497 | if(other instanceof InputMapTemplateInstance) { 498 | InputMapTemplateInstance that = (InputMapTemplateInstance) other; 499 | return Objects.equals(this.template, that.template) 500 | && Objects.equals(this.target, that.target); 501 | } else { 502 | return false; 503 | } 504 | } 505 | 506 | @Override 507 | public int hashCode() { 508 | return Objects.hash(template, target); 509 | } 510 | } --------------------------------------------------------------------------------