diff --git a/.github/build.sh b/.github/build.sh new file mode 100755 index 0000000..7da4262 --- /dev/null +++ b/.github/build.sh @@ -0,0 +1,3 @@ +#!/bin/sh +curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/master/ci-build.sh +sh ci-build.sh diff --git a/.github/setup.sh b/.github/setup.sh new file mode 100755 index 0000000..f359bbe --- /dev/null +++ b/.github/setup.sh @@ -0,0 +1,3 @@ +#!/bin/sh +curl -fsLO https://raw.githubusercontent.com/scijava/scijava-scripts/master/ci-setup-github-actions.sh +sh ci-setup-github-actions.sh diff --git a/.github/workflows/build-main.yml b/.github/workflows/build-main.yml new file mode 100644 index 0000000..5ef5692 --- /dev/null +++ b/.github/workflows/build-main.yml @@ -0,0 +1,32 @@ +name: build + +on: + push: + branches: + - master + tags: + - "*-[0-9]+.*" + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Java + uses: actions/setup-java@v3 + with: + java-version: '8' + distribution: 'zulu' + cache: 'maven' + - name: Set up CI environment + run: .github/setup.sh + - name: Execute the build + run: .github/build.sh + env: + GPG_KEY_NAME: ${{ secrets.GPG_KEY_NAME }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + MAVEN_USER: ${{ secrets.MAVEN_USER }} + MAVEN_PASS: ${{ secrets.MAVEN_PASS }} + OSSRH_PASS: ${{ secrets.OSSRH_PASS }} + SIGNING_ASC: ${{ secrets.SIGNING_ASC }} diff --git a/.github/workflows/build-pr.yml b/.github/workflows/build-pr.yml new file mode 100644 index 0000000..925b576 --- /dev/null +++ b/.github/workflows/build-pr.yml @@ -0,0 +1,23 @@ +name: build PR + +on: + pull_request: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Java + uses: actions/setup-java@v3 + with: + java-version: '8' + distribution: 'zulu' + cache: 'maven' + - name: Set up CI environment + run: .github/setup.sh + - name: Execute the build + run: .github/build.sh diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..84f23ce --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,25 @@ +Copyright (c) 2015 - 2023, Max Planck Institute of Molecular Cell Biology +and Genetics. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md index 6ddadbe..d7de467 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,5 @@ +[![](https://github.com/scijava/ui-behaviour/actions/workflows/build-main.yml/badge.svg)](https://github.com/scijava/ui-behaviour/actions/workflows/build-main.yml) + # configurable-keys Simplify making AWT mouse-handlers configurable. Works along the lines of the `InputMap` / `ActionMap` mechanism. diff --git a/pom.xml b/pom.xml index 024bc97..5d9c325 100644 --- a/pom.xml +++ b/pom.xml @@ -1,94 +1,139 @@ - + 4.0.0 org.scijava pom-scijava - 9.2.0 + 36.0.0 + ui-behaviour - 1.2.1-SNAPSHOT + 2.0.9-SNAPSHOT + + UI Behaviour + Configurable key and mouse event handling http://github.com/scijava/ui-behaviour 2015 - - Configurable Keys - configurable key and mouse event handling - - - 1.8 - - - - - net.sf.trove4j - trove4j - 3.0.3 - - - com.google.code.gson - gson - - - org.yaml - snakeyaml - 1.13 - - + + SciJava + https://scijava.org/ + + + + Simplified BSD License + https://opensource.org/licenses/BSD-2-Clause + repo + + tpietzsch Tobias Pietzsch - pietzsch@mpi-cbg.de - - MPI-CBG - http://www.mpi-cbg.de/ + https://imagej.net/people/tpietzsch - architect + founder + lead developer + debugger + reviewer + support + maintainer - +1 tinevez Jean-Yves Tinevez - jean-yves.tinevez@pasteur.fr - http://www.pasteur.fr/ip/easysite/pasteur/fr/recherche/plates-formes-technologiques/imagopole/plates-formes-imagopole/la-plate-forme-d-imagerie-dynamique/equipe-pfid/equipe-pfid-jyt - Institut Pasteur - http://www.pasteur.fr/ + https://imagej.net/people/tinevez - architect developer + debugger + reviewer + + + + ctrueden + Curtis Rueden + https://imagej.net/people/ctrueden + + maintainer - +1 + + + Ulrik Günther + https://imagej.net/people/skalarproduktraum + skalarproduktraum + + - - - imagej.public - http://maven.imagej.net/content/groups/public - - + + + SciJava + https://groups.google.com/group/scijava + https://groups.google.com/group/scijava + scijava@googlegroups.com + https://groups.google.com/group/scijava + + - scm:git:git://github.com/scijava/ui-behaviour + scm:git:https://github.com/scijava/ui-behaviour scm:git:git@github.com:scijava/ui-behaviour HEAD https://github.com/scijava/ui-behaviour + + GitHub Issues + https://github.com/scijava/ui-behaviour/issues + - Jenkins - http://jenkins.imagej.net/job/ui-behaviour / + GitHub Actions + https://github.com/scijava/ui-behaviour/actions - - - BSD 2-Clause License - http://opensource.org/licenses/BSD-2-Clause - repo - - + + org.scijava.ui.behaviour + bsd_2 + Max Planck Institute of Molecular Cell Biology +and Genetics. + + + sign,deploy-to-scijava + + 2.0 + + + + + scijava.public + https://maven.scijava.org/content/groups/public + + + + + + org.scijava + scijava-listeners + + + org.scijava + scijava-common + + + net.sf.trove4j + trove4j + + + com.google.code.gson + gson + + + org.yaml + snakeyaml + + diff --git a/src/main/java/org/scijava/ui/behaviour/AbstractMouseAndKeyHandler.java b/src/main/java/org/scijava/ui/behaviour/AbstractMouseAndKeyHandler.java new file mode 100644 index 0000000..527196f --- /dev/null +++ b/src/main/java/org/scijava/ui/behaviour/AbstractMouseAndKeyHandler.java @@ -0,0 +1,180 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.ui.behaviour; + +import java.awt.Toolkit; +import java.util.ArrayList; +import java.util.Map; +import java.util.Set; + +public abstract class AbstractMouseAndKeyHandler +{ + protected static final int DOUBLE_CLICK_INTERVAL = getDoubleClickInterval(); + + private static int getDoubleClickInterval() + { + final Object prop = Toolkit.getDefaultToolkit().getDesktopProperty( "awt.multiClickInterval" ); + return prop == null ? 200 : ( Integer ) prop; + } + + private InputTriggerMap inputMap; + + private BehaviourMap behaviourMap; + + private int inputMapExpectedModCount; + + private int behaviourMapExpectedModCount; + + public void setInputMap( final InputTriggerMap inputMap ) + { + this.inputMap = inputMap; + inputMapExpectedModCount = inputMap.modCount() - 1; + } + + public void setBehaviourMap( final BehaviourMap behaviourMap ) + { + this.behaviourMap = behaviourMap; + behaviourMapExpectedModCount = behaviourMap.modCount() - 1; + } + + /* + * Managing internal behaviour lists. + * + * The internal lists only contain entries for Behaviours that can be + * actually triggered with the current InputMap, grouped by Behaviour type, + * such that hopefully lookup from the event handlers is fast, + */ + + protected static class BehaviourEntry< T extends Behaviour > + { + private final InputTrigger buttons; + + private final T behaviour; + + public BehaviourEntry( + final InputTrigger buttons, + final T behaviour ) + { + this.buttons = buttons; + this.behaviour = behaviour; + } + + public InputTrigger buttons() + { + return buttons; + } + + public T behaviour() + { + return behaviour; + } + } + + protected final ArrayList< BehaviourEntry< DragBehaviour > > buttonDrags = new ArrayList<>(); + + protected final ArrayList< BehaviourEntry< DragBehaviour > > keyDrags = new ArrayList<>(); + + protected final ArrayList< BehaviourEntry< ClickBehaviour > > buttonClicks = new ArrayList<>(); + + protected final ArrayList< BehaviourEntry< ClickBehaviour > > keyClicks = new ArrayList<>(); + + protected final ArrayList< BehaviourEntry< ScrollBehaviour > > scrolls = new ArrayList<>(); + + /** + * Make sure that the internal behaviour lists are up to date. For this, we + * keep track the modification count of {@link #inputMap} and + * {@link #behaviourMap}. If expected mod counts are not matched, call + * {@link #updateInternalMaps()} to rebuild the internal behaviour lists. + */ + protected synchronized void update() + { + final int imc = inputMap.modCount(); + final int bmc = behaviourMap.modCount(); + if ( imc != inputMapExpectedModCount || bmc != behaviourMapExpectedModCount ) + { + inputMapExpectedModCount = imc; + behaviourMapExpectedModCount = bmc; + updateInternalMaps(); + } + } + + /** + * Build internal lists buttonDrag, keyDrags, etc from of {@link #inputMap} + * and {@link #behaviourMap}. The internal lists only contain entries for + * behaviours that can be actually triggered with the current InputMap, + * grouped by behaviour type, such that hopefully lookup from the event + * handlers is fast. + */ + private void updateInternalMaps() + { + buttonDrags.clear(); + keyDrags.clear(); + buttonClicks.clear(); + keyClicks.clear(); + scrolls.clear(); + + for ( final Map.Entry< InputTrigger, Set< String > > entry : inputMap.getAllBindings().entrySet() ) + { + final InputTrigger buttons = entry.getKey(); + final Set< String > behaviourKeys = entry.getValue(); + if ( behaviourKeys == null ) + continue; + + for ( final String behaviourKey : behaviourKeys ) + { + final Behaviour behaviour = behaviourMap.get( behaviourKey ); + if ( behaviour == null ) + continue; + + if ( behaviour instanceof DragBehaviour ) + { + final BehaviourEntry< DragBehaviour > dragEntry = new BehaviourEntry<>( buttons, ( DragBehaviour ) behaviour ); + if ( buttons.isKeyTriggered() ) + keyDrags.add( dragEntry ); + else + buttonDrags.add( dragEntry ); + } + if ( behaviour instanceof ClickBehaviour ) + { + final BehaviourEntry< ClickBehaviour > clickEntry = new BehaviourEntry<>( buttons, ( ClickBehaviour ) behaviour ); + if ( buttons.isKeyTriggered() ) + keyClicks.add( clickEntry ); + else + buttonClicks.add( clickEntry ); + } + if ( behaviour instanceof ScrollBehaviour ) + { + final BehaviourEntry< ScrollBehaviour > scrollEntry = new BehaviourEntry<>( buttons, ( ScrollBehaviour ) behaviour ); + scrolls.add( scrollEntry ); + } + } + } + } +} diff --git a/src/main/java/org/scijava/ui/behaviour/Behaviour.java b/src/main/java/org/scijava/ui/behaviour/Behaviour.java index 639eb4f..64fceb2 100644 --- a/src/main/java/org/scijava/ui/behaviour/Behaviour.java +++ b/src/main/java/org/scijava/ui/behaviour/Behaviour.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; public interface Behaviour diff --git a/src/main/java/org/scijava/ui/behaviour/BehaviourMap.java b/src/main/java/org/scijava/ui/behaviour/BehaviourMap.java index 4f60fcc..73eb005 100644 --- a/src/main/java/org/scijava/ui/behaviour/BehaviourMap.java +++ b/src/main/java/org/scijava/ui/behaviour/BehaviourMap.java @@ -1,9 +1,40 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; +import java.util.Set; import javax.swing.ActionMap; /** @@ -127,6 +158,11 @@ public synchronized Map< String, Behaviour > getAllBindings() return allBindings; } + public synchronized Set< String > keys() + { + return new HashSet<>( behaviours.keySet() ); + } + public int modCount() { if ( parent != null ) diff --git a/src/main/java/org/scijava/ui/behaviour/ClickBehaviour.java b/src/main/java/org/scijava/ui/behaviour/ClickBehaviour.java index d310db4..f9daab8 100644 --- a/src/main/java/org/scijava/ui/behaviour/ClickBehaviour.java +++ b/src/main/java/org/scijava/ui/behaviour/ClickBehaviour.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; public interface ClickBehaviour extends Behaviour diff --git a/src/main/java/org/scijava/ui/behaviour/DragBehaviour.java b/src/main/java/org/scijava/ui/behaviour/DragBehaviour.java index 6beb315..bffcab4 100644 --- a/src/main/java/org/scijava/ui/behaviour/DragBehaviour.java +++ b/src/main/java/org/scijava/ui/behaviour/DragBehaviour.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; import java.awt.event.MouseListener; diff --git a/src/main/java/org/scijava/ui/behaviour/GlobalKeyEventDispatcher.java b/src/main/java/org/scijava/ui/behaviour/GlobalKeyEventDispatcher.java index 84e2d12..2592779 100644 --- a/src/main/java/org/scijava/ui/behaviour/GlobalKeyEventDispatcher.java +++ b/src/main/java/org/scijava/ui/behaviour/GlobalKeyEventDispatcher.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; import java.awt.Component; diff --git a/src/main/java/org/scijava/ui/behaviour/InputTrigger.java b/src/main/java/org/scijava/ui/behaviour/InputTrigger.java index 0019cb6..3413973 100644 --- a/src/main/java/org/scijava/ui/behaviour/InputTrigger.java +++ b/src/main/java/org/scijava/ui/behaviour/InputTrigger.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; import java.awt.AWTKeyStroke; @@ -24,11 +53,18 @@ */ public class InputTrigger { - public static final int DOUBLE_CLICK_MASK = 1 << 20; - - public static final int SCROLL_MASK = 1 << 21; - - public static final int WIN_DOWN_MASK = 1 << 22; + public static final int SHIFT_DOWN_MASK = 1 << 6; // == InputEvent.SHIFT_DOWN_MASK + public static final int CTRL_DOWN_MASK = 1 << 7; // == InputEvent.CTRL_DOWN_MASK + public static final int META_DOWN_MASK = 1 << 8; // == InputEvent.META_DOWN_MASK + public static final int ALT_DOWN_MASK = 1 << 9; // == InputEvent.ALT_DOWN_MASK + public static final int BUTTON1_DOWN_MASK = 1 << 10; // == InputEvent.BUTTON1_DOWN_MASK + public static final int BUTTON2_DOWN_MASK = 1 << 11; // == InputEvent.BUTTON2_DOWN_MASK + public static final int BUTTON3_DOWN_MASK = 1 << 12; // == InputEvent.BUTTON3_DOWN_MASK + public static final int ALT_GRAPH_DOWN_MASK = 1 << 13; // == InputEvent.ALT_GRAPH_DOWN_MASK + + public static final int DOUBLE_CLICK_MASK = 1 << 20; + public static final int SCROLL_MASK = 1 << 21; + public static final int WIN_DOWN_MASK = 1 << 22; public static final int IGNORE_MASK = -1; @@ -52,7 +88,7 @@ public class InputTrigger private static final String WINDOWS_TEXT= "win"; /** - * String used to specify a special {@value #NOT_MAPPED} trigger that blocks + * String used to specify a special {@code NOT_MAPPED} trigger that blocks * all triggers for an action. */ private static final String NOT_MAPPED_TEXT = "not mapped"; @@ -118,7 +154,7 @@ public static InputTrigger getFromString( final String s ) throws IllegalArgumen * TODO: KeyStroke only if no ignore keys are given???? */ KeyStroke keyStroke = null; - if ( ( mask & ( DOUBLE_CLICK_MASK | InputEvent.BUTTON1_DOWN_MASK | InputEvent.BUTTON2_DOWN_MASK | InputEvent.BUTTON3_DOWN_MASK ) ) == 0 ) + if ( ( mask & ( DOUBLE_CLICK_MASK | BUTTON1_DOWN_MASK | BUTTON2_DOWN_MASK | BUTTON3_DOWN_MASK ) ) == 0 ) { // no mouse keys, no double-click -- pure keystroke // This might still fail if for example "win" modifier is present. @@ -304,7 +340,7 @@ public TIntCollection getPressedKeys() public boolean isKeyTriggered() { - return ( mask & ( InputEvent.BUTTON1_DOWN_MASK | InputEvent.BUTTON2_DOWN_MASK | InputEvent.BUTTON3_DOWN_MASK | SCROLL_MASK ) ) == 0; + return ( mask & ( BUTTON1_DOWN_MASK | BUTTON2_DOWN_MASK | BUTTON3_DOWN_MASK | SCROLL_MASK ) ) == 0; } public boolean isKeyStroke() @@ -458,14 +494,14 @@ private void addKeys( final TIntSet keys, final StringBuilder buf ) private void addModifierTexts( final int mask, final StringBuilder buf ) { - addModifierText( mask, InputEvent.SHIFT_DOWN_MASK, "shift", buf ); - addModifierText( mask, InputEvent.CTRL_DOWN_MASK, "ctrl", buf ); - addModifierText( mask, InputEvent.META_DOWN_MASK, "meta", buf ); - addModifierText( mask, InputEvent.ALT_DOWN_MASK, "alt", buf ); - addModifierText( mask, InputEvent.ALT_GRAPH_DOWN_MASK, "altGraph", buf ); - addModifierText( mask, InputEvent.BUTTON1_DOWN_MASK, "button1", buf ); - addModifierText( mask, InputEvent.BUTTON2_DOWN_MASK, "button2", buf ); - addModifierText( mask, InputEvent.BUTTON3_DOWN_MASK, "button3", buf ); + addModifierText( mask, SHIFT_DOWN_MASK, "shift", buf ); + addModifierText( mask, CTRL_DOWN_MASK, "ctrl", buf ); + addModifierText( mask, META_DOWN_MASK, "meta", buf ); + addModifierText( mask, ALT_DOWN_MASK, "alt", buf ); + addModifierText( mask, ALT_GRAPH_DOWN_MASK, "altGraph", buf ); + addModifierText( mask, BUTTON1_DOWN_MASK, "button1", buf ); + addModifierText( mask, BUTTON2_DOWN_MASK, "button2", buf ); + addModifierText( mask, BUTTON3_DOWN_MASK, "button3", buf ); addModifierText( mask, DOUBLE_CLICK_MASK, DOUBLE_CLICK_TEXT, buf ); addModifierText( mask, SCROLL_MASK, SCROLL_TEXT, buf ); addModifierText( mask, WIN_DOWN_MASK, WINDOWS_TEXT, buf ); @@ -493,29 +529,29 @@ private synchronized static Map getModifierKeywords() { final Map< String, Integer > uninitializedMap = new HashMap<>( 8, 1.0f ); uninitializedMap.put( "shift", - Integer.valueOf( InputEvent.SHIFT_DOWN_MASK ) ); + SHIFT_DOWN_MASK ); uninitializedMap.put( "control", - Integer.valueOf( InputEvent.CTRL_DOWN_MASK ) ); + CTRL_DOWN_MASK ); uninitializedMap.put( "ctrl", - Integer.valueOf( InputEvent.CTRL_DOWN_MASK ) ); + CTRL_DOWN_MASK ); uninitializedMap.put( "meta", - Integer.valueOf( InputEvent.META_DOWN_MASK ) ); + META_DOWN_MASK ); uninitializedMap.put( "alt", - Integer.valueOf( InputEvent.ALT_DOWN_MASK ) ); + ALT_DOWN_MASK ); uninitializedMap.put( "altGraph", - Integer.valueOf( InputEvent.ALT_GRAPH_DOWN_MASK ) ); + ALT_GRAPH_DOWN_MASK ); uninitializedMap.put( "button1", - Integer.valueOf( InputEvent.BUTTON1_DOWN_MASK ) ); + BUTTON1_DOWN_MASK ); uninitializedMap.put( "button2", - Integer.valueOf( InputEvent.BUTTON2_DOWN_MASK ) ); + BUTTON2_DOWN_MASK ); uninitializedMap.put( "button3", - Integer.valueOf( InputEvent.BUTTON3_DOWN_MASK ) ); + BUTTON3_DOWN_MASK ); uninitializedMap.put( DOUBLE_CLICK_TEXT, - Integer.valueOf( DOUBLE_CLICK_MASK ) ); + DOUBLE_CLICK_MASK ); uninitializedMap.put( SCROLL_TEXT, - Integer.valueOf( SCROLL_MASK ) ); + SCROLL_MASK ); uninitializedMap.put( WINDOWS_TEXT, - Integer.valueOf( WIN_DOWN_MASK ) ); + WIN_DOWN_MASK ); modifierKeywords = Collections.synchronizedMap( uninitializedMap ); } diff --git a/src/main/java/org/scijava/ui/behaviour/InputTriggerAdder.java b/src/main/java/org/scijava/ui/behaviour/InputTriggerAdder.java index 1ac0b56..433799a 100644 --- a/src/main/java/org/scijava/ui/behaviour/InputTriggerAdder.java +++ b/src/main/java/org/scijava/ui/behaviour/InputTriggerAdder.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; public interface InputTriggerAdder diff --git a/src/main/java/org/scijava/ui/behaviour/InputTriggerMap.java b/src/main/java/org/scijava/ui/behaviour/InputTriggerMap.java index 4d4a88e..f124357 100644 --- a/src/main/java/org/scijava/ui/behaviour/InputTriggerMap.java +++ b/src/main/java/org/scijava/ui/behaviour/InputTriggerMap.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; import java.util.HashMap; @@ -148,11 +177,15 @@ public synchronized void clear() } /** - * Get all bindings defined in this map and its parents. Note the returned - * map not backed by the {@link InputTriggerMap}, i.e., it will not - * reflect changes to the {@link InputTriggerMap}. + * Get all bindings defined in this map and its parents. Note that the + * returned map is not backed by the {@link InputTriggerMap}, i.e., + * it will not reflect changes to the {@link InputTriggerMap}. + *

+ * This differs from {@code getBindings()} in that this method includes the + * bindings defined in the parent. * - * @return all bindings defined in this map and its parents. + * @return all bindings (trigger to set of behaviour keys) defined in this + * map and its parents. */ public synchronized Map< InputTrigger, Set< String > > getAllBindings() { @@ -162,22 +195,42 @@ public synchronized Map< InputTrigger, Set< String > > getAllBindings() else allBindings = new HashMap<>(); + addBindings( allBindings ); + + return allBindings; + } + + /** + * Get all bindings defined in this map. Note that the returned map is + * not backed by the {@link InputTriggerMap}, i.e., it will not + * reflect changes to the {@link InputTriggerMap}. + * + * @return all bindings (trigger to set of behaviour keys) defined in this + * map. + */ + public synchronized Map< InputTrigger, Set< String > > getBindings() + { + final Map< InputTrigger, Set< String > > bindings = new HashMap<>(); + addBindings( bindings ); + return bindings; + } + + private void addBindings( final Map< InputTrigger, Set< String > > bindings ) + { for ( final Entry< InputTrigger, Set< String > > entry : triggerToKeys.entrySet() ) { final InputTrigger inputTrigger = entry.getKey(); if ( entry.getValue() == null || entry.getValue().isEmpty() ) continue; - Set< String > behaviourKeys = allBindings.get( inputTrigger ); + Set< String > behaviourKeys = bindings.get( inputTrigger ); if ( behaviourKeys == null ) { behaviourKeys = new HashSet<>(); - allBindings.put( inputTrigger, behaviourKeys ); + bindings.put( inputTrigger, behaviourKeys ); } behaviourKeys.addAll( entry.getValue() ); } - - return allBindings; } public int modCount() diff --git a/src/main/java/org/scijava/ui/behaviour/KeyPressedManager.java b/src/main/java/org/scijava/ui/behaviour/KeyPressedManager.java index 1a37d39..0694064 100644 --- a/src/main/java/org/scijava/ui/behaviour/KeyPressedManager.java +++ b/src/main/java/org/scijava/ui/behaviour/KeyPressedManager.java @@ -1,23 +1,49 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; -import java.awt.event.KeyListener; - import gnu.trove.set.TIntSet; /** - * Distributes {@link KeyListener#keyPressed(java.awt.event.KeyEvent)} events - * between windows that share the same {@link KeyPressedManager}. The goal is to - * make keyboard click/drag behaviours work like mouse click/drag: When a - * behaviour is initiated with a key press, the window under the mouse receives - * focus and the behaviour is handled there. + * Distributes KEY_PRESSED events between windows that share the same + * {@link KeyPressedManager}. The goal is to make keyboard click/drag behaviours + * work like mouse click/drag: When a behaviour is initiated with a key press, + * the window under the mouse receives focus and the behaviour is handled there. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public class KeyPressedManager { public interface KeyPressedReceiver { - public void handleKeyPressed( final int mask, final boolean doubleClick, final TIntSet pressedKeys ); + void handleKeyPressed( final KeyPressedReceiver origin, final int mask, final boolean doubleClick, final TIntSet pressedKeys ); } private KeyPressedReceiver active = null; @@ -29,9 +55,9 @@ public void handleKeyPressed( final TIntSet pressedKeys ) { if ( active != null ) - active.handleKeyPressed( mask, doubleClick, pressedKeys ); + active.handleKeyPressed( origin, mask, doubleClick, pressedKeys ); else - origin.handleKeyPressed( mask, doubleClick, pressedKeys ); + origin.handleKeyPressed( origin, mask, doubleClick, pressedKeys ); } public void activate( final KeyPressedReceiver handler) diff --git a/src/main/java/org/scijava/ui/behaviour/KeyStrokeAdder.java b/src/main/java/org/scijava/ui/behaviour/KeyStrokeAdder.java index 634e5b5..d56f187 100644 --- a/src/main/java/org/scijava/ui/behaviour/KeyStrokeAdder.java +++ b/src/main/java/org/scijava/ui/behaviour/KeyStrokeAdder.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; import javax.swing.InputMap; @@ -10,8 +39,11 @@ public interface Factory public KeyStrokeAdder keyStrokeAdder( InputMap map, final String ... contexts ); } + @Deprecated public void put( final String actionName, final KeyStroke... defaultKeyStrokes ); + public void put( final String actionName, final InputTrigger... defaultKeyStrokes ); + public void put( final String actionName, final String... defaultKeyStrokes ); public void put( final String actionName ); diff --git a/src/main/java/org/scijava/ui/behaviour/MouseAndKeyHandler.java b/src/main/java/org/scijava/ui/behaviour/MouseAndKeyHandler.java index 430706a..09c083a 100644 --- a/src/main/java/org/scijava/ui/behaviour/MouseAndKeyHandler.java +++ b/src/main/java/org/scijava/ui/behaviour/MouseAndKeyHandler.java @@ -1,7 +1,35 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; import java.awt.Component; -import java.awt.Toolkit; import java.awt.event.FocusEvent; import java.awt.event.FocusListener; import java.awt.event.InputEvent; @@ -13,8 +41,6 @@ import java.awt.event.MouseWheelEvent; import java.awt.event.MouseWheelListener; import java.util.ArrayList; -import java.util.Map.Entry; -import java.util.Set; import org.scijava.ui.behaviour.KeyPressedManager.KeyPressedReceiver; @@ -22,149 +48,15 @@ import gnu.trove.set.TIntSet; import gnu.trove.set.hash.TIntHashSet; -public class MouseAndKeyHandler +public class MouseAndKeyHandler extends AbstractMouseAndKeyHandler implements KeyListener, MouseListener, MouseWheelListener, MouseMotionListener, FocusListener { - private static final int DOUBLE_CLICK_INTERVAL = getDoubleClickInterval(); - private static final int OSX_META_LEFT_CLICK = InputEvent.BUTTON1_MASK | InputEvent.BUTTON3_MASK | InputEvent.META_MASK; private static final int OSX_ALT_LEFT_CLICK = InputEvent.BUTTON1_MASK | InputEvent.BUTTON2_MASK | InputEvent.ALT_MASK; private static final int OSX_ALT_RIGHT_CLICK = InputEvent.BUTTON3_MASK | InputEvent.BUTTON2_MASK | InputEvent.ALT_MASK | InputEvent.META_MASK; - private static int getDoubleClickInterval() - { - final Object prop = Toolkit.getDefaultToolkit().getDesktopProperty( "awt.multiClickInterval" ); - return prop == null ? 200 : ( Integer ) prop; - } - - private InputTriggerMap inputMap; - - private BehaviourMap behaviourMap; - - private int inputMapExpectedModCount; - - private int behaviourMapExpectedModCount; - - public void setInputMap( final InputTriggerMap inputMap ) - { - this.inputMap = inputMap; - inputMapExpectedModCount = inputMap.modCount() - 1; - } - - public void setBehaviourMap( final BehaviourMap behaviourMap ) - { - this.behaviourMap = behaviourMap; - behaviourMapExpectedModCount = behaviourMap.modCount() - 1; - } - - /* - * Managing internal behaviour lists. - * - * The internal lists only contain entries for Behaviours that can be - * actually triggered with the current InputMap, grouped by Behaviour type, - * such that hopefully lookup from the event handlers is fast, - */ - - static class BehaviourEntry< T extends Behaviour > - { - final InputTrigger buttons; - - final T behaviour; - - BehaviourEntry( - final InputTrigger buttons, - final T behaviour ) - { - this.buttons = buttons; - this.behaviour = behaviour; - } - } - - private final ArrayList< BehaviourEntry< DragBehaviour > > buttonDrags = new ArrayList<>(); - - private final ArrayList< BehaviourEntry< DragBehaviour > > keyDrags = new ArrayList<>(); - - private final ArrayList< BehaviourEntry< ClickBehaviour > > buttonClicks = new ArrayList<>(); - - private final ArrayList< BehaviourEntry< ClickBehaviour > > keyClicks = new ArrayList<>(); - - private final ArrayList< BehaviourEntry< ScrollBehaviour > > scrolls = new ArrayList<>(); - - /** - * Make sure that the internal behaviour lists are up to date. For this, we - * keep track the modification count of {@link #inputMap} and - * {@link #behaviourMap}. If expected mod counts are not matched, call - * {@link #updateInternalMaps()} to rebuild the internal behaviour lists. - */ - private synchronized void update() - { - final int imc = inputMap.modCount(); - final int bmc = behaviourMap.modCount(); - if ( imc != inputMapExpectedModCount || bmc != behaviourMapExpectedModCount ) - { - inputMapExpectedModCount = imc; - behaviourMapExpectedModCount = bmc; - updateInternalMaps(); - } - } - - /** - * Build internal lists buttonDrag, keyDrags, etc from of {@link #inputMap} - * and {@link #behaviourMap}. The internal lists only contain entries for - * behaviours that can be actually triggered with the current InputMap, - * grouped by behaviour type, such that hopefully lookup from the event - * handlers is fast. - */ - private void updateInternalMaps() - { - buttonDrags.clear(); - keyDrags.clear(); - buttonClicks.clear(); - keyClicks.clear(); - scrolls.clear(); - - for ( final Entry< InputTrigger, Set< String > > entry : inputMap.getAllBindings().entrySet() ) - { - final InputTrigger buttons = entry.getKey(); - final Set< String > behaviourKeys = entry.getValue(); - if ( behaviourKeys == null ) - continue; - - for ( final String behaviourKey : behaviourKeys ) - { - final Behaviour behaviour = behaviourMap.get( behaviourKey ); - if ( behaviour == null ) - continue; - - if ( behaviour instanceof DragBehaviour ) - { - final BehaviourEntry< DragBehaviour > dragEntry = new BehaviourEntry<>( buttons, ( DragBehaviour ) behaviour ); - if ( buttons.isKeyTriggered() ) - keyDrags.add( dragEntry ); - else - buttonDrags.add( dragEntry ); - } - if ( behaviour instanceof ClickBehaviour ) - { - final BehaviourEntry< ClickBehaviour > clickEntry = new BehaviourEntry<>( buttons, ( ClickBehaviour ) behaviour ); - if ( buttons.isKeyTriggered() ) - keyClicks.add( clickEntry ); - else - buttonClicks.add( clickEntry ); - } - if ( behaviour instanceof ScrollBehaviour ) - { - final BehaviourEntry< ScrollBehaviour > scrollEntry = new BehaviourEntry<>( buttons, ( ScrollBehaviour ) behaviour ); - scrolls.add( scrollEntry ); - } - } - } - } - - - /* * Event handling. Forwards to registered behaviours. */ @@ -213,9 +105,9 @@ private int getMask( final InputEvent e ) * We keep track of whether the SHIFT key was actually pressed for disambiguation. */ if ( globalKeys.shiftPressed() ) - mask |= InputEvent.SHIFT_DOWN_MASK; + mask |= InputTrigger.SHIFT_DOWN_MASK; else - mask &= ~InputEvent.SHIFT_DOWN_MASK; + mask &= ~InputTrigger.SHIFT_DOWN_MASK; /* * On OS X AWT sets the META_DOWN_MASK to for right clicks. We keep @@ -223,9 +115,9 @@ private int getMask( final InputEvent e ) * disambiguation. */ if ( globalKeys.metaPressed() ) - mask |= InputEvent.META_DOWN_MASK; + mask |= InputTrigger.META_DOWN_MASK; else - mask &= ~InputEvent.META_DOWN_MASK; + mask &= ~InputTrigger.META_DOWN_MASK; if ( globalKeys.winPressed() ) mask |= InputTrigger.WIN_DOWN_MASK; @@ -247,11 +139,11 @@ private int getMask( final InputEvent e ) if ( e.getID() != MouseEvent.MOUSE_WHEEL && e.getID() != MouseEvent.MOUSE_RELEASED ) { if ( ( modifiers & InputEvent.BUTTON1_MASK ) != 0 ) - mask |= InputEvent.BUTTON1_DOWN_MASK; + mask |= InputTrigger.BUTTON1_DOWN_MASK; if ( ( modifiers & InputEvent.BUTTON2_MASK ) != 0 ) - mask |= InputEvent.BUTTON2_DOWN_MASK; + mask |= InputTrigger.BUTTON2_DOWN_MASK; if ( ( modifiers & InputEvent.BUTTON3_MASK ) != 0 ) - mask |= InputEvent.BUTTON3_DOWN_MASK; + mask |= InputTrigger.BUTTON3_DOWN_MASK; } /* @@ -259,21 +151,21 @@ private int getMask( final InputEvent e ) * that. */ if ( modifiers == OSX_META_LEFT_CLICK ) - mask &= ~InputEvent.BUTTON3_DOWN_MASK; + mask &= ~InputTrigger.BUTTON3_DOWN_MASK; /* * On OS X AWT sets the BUTTON2_DOWN_MASK for alt+left clicks. Fix * that. */ if ( modifiers == OSX_ALT_LEFT_CLICK ) - mask &= ~InputEvent.BUTTON2_DOWN_MASK; + mask &= ~InputTrigger.BUTTON2_DOWN_MASK; /* * On OS X AWT sets the BUTTON2_DOWN_MASK for alt+right clicks. Fix * that. */ if ( modifiers == OSX_ALT_RIGHT_CLICK ) - mask &= ~InputEvent.BUTTON2_DOWN_MASK; + mask &= ~InputTrigger.BUTTON2_DOWN_MASK; /* * Deal with mouse double-clicks. @@ -302,11 +194,11 @@ public void mouseDragged( final MouseEvent e ) // System.out.println( e ); update(); - final int x = e.getX(); - final int y = e.getY(); + mouseX = e.getX(); + mouseY = e.getY(); for ( final BehaviourEntry< DragBehaviour > drag : activeButtonDrags ) - drag.behaviour.drag( x, y ); + drag.behaviour().drag( mouseX, mouseY ); } @Override @@ -319,7 +211,7 @@ public void mouseMoved( final MouseEvent e ) mouseY = e.getY(); for ( final BehaviourEntry< DragBehaviour > drag : activeKeyDrags ) - drag.behaviour.drag( mouseX, mouseY ); + drag.behaviour().drag( mouseX, mouseY ); } @Override @@ -341,14 +233,14 @@ public void mouseWheelMoved( final MouseWheelEvent e ) * the SHIFT key is not pressed. With SHIFT pressed, everything is * treated as vertical scrolling. */ - final boolean exShiftMask = ( e.getModifiersEx() & InputEvent.SHIFT_DOWN_MASK ) != 0; + final boolean exShiftMask = ( e.getModifiersEx() & InputTrigger.SHIFT_DOWN_MASK ) != 0; final boolean isHorizontal = !globalKeys.shiftPressed() && exShiftMask; for ( final BehaviourEntry< ScrollBehaviour > scroll : scrolls ) { - if ( scroll.buttons.matches( mask, globalKeys.pressedKeys() ) ) + if ( scroll.buttons().matches( mask, globalKeys.pressedKeys() ) ) { - scroll.behaviour.scroll( wheelRotation, isHorizontal, x, y ); + scroll.behaviour().scroll( wheelRotation, isHorizontal, x, y ); } } } @@ -367,10 +259,10 @@ public void mouseClicked( final MouseEvent e ) final int clickMask = mask & ~InputTrigger.DOUBLE_CLICK_MASK; for ( final BehaviourEntry< ClickBehaviour > click : buttonClicks ) { - if ( click.buttons.matches( mask, pressedKeys ) || - ( clickMask != mask && click.buttons.matches( clickMask, pressedKeys ) ) ) + if ( click.buttons().matches( mask, globalKeys.pressedKeys() ) || + ( clickMask != mask && click.buttons().matches( clickMask, globalKeys.pressedKeys() ) ) ) { - click.behaviour.click( x, y ); + click.behaviour().click( x, y ); } } } @@ -388,9 +280,9 @@ public void mousePressed( final MouseEvent e ) for ( final BehaviourEntry< DragBehaviour > drag : buttonDrags ) { - if ( drag.buttons.matches( mask, globalKeys.pressedKeys() ) ) + if ( drag.buttons().matches( mask, globalKeys.pressedKeys() ) ) { - drag.behaviour.init( x, y ); + drag.behaviour().init( x, y ); activeButtonDrags.add( drag ); } } @@ -409,9 +301,9 @@ public void mouseReleased( final MouseEvent e ) final ArrayList< BehaviourEntry< ? > > ended = new ArrayList<>(); for ( final BehaviourEntry< DragBehaviour > drag : activeButtonDrags ) - if ( !drag.buttons.matchesSubset( mask, globalKeys.pressedKeys() ) ) + if ( !drag.buttons().matchesSubset( mask, globalKeys.pressedKeys() ) ) { - drag.behaviour.end( x, y ); + drag.behaviour().end( x, y ); ended.add( drag ); } activeButtonDrags.removeAll( ended ); @@ -470,9 +362,9 @@ public void keyPressed( final KeyEvent e ) } if ( keypressManager != null ) - keypressManager.handleKeyPressed( receiver, mask, doubleClick, pressedKeys ); + keypressManager.handleKeyPressed( receiver, mask, doubleClick, globalKeys.pressedKeys() ); else - handleKeyPressed( mask, doubleClick, pressedKeys, false ); + handleKeyPressed( mask, doubleClick, globalKeys.pressedKeys(), false ); } } @@ -504,7 +396,7 @@ public void setKeypressManager( this.receiver = new KeyPressedReceiver() { @Override - public void handleKeyPressed( final int mask, final boolean doubleClick, final TIntSet pressedKeys ) + public void handleKeyPressed( final KeyPressedReceiver origin, final int mask, final boolean doubleClick, final TIntSet pressedKeys ) { if ( MouseAndKeyHandler.this.handleKeyPressed( mask, doubleClick, pressedKeys, true ) ) focus.run(); @@ -545,26 +437,26 @@ private boolean handleKeyPressed( final int mask, final boolean doubleClick, fin for ( final BehaviourEntry< DragBehaviour > drag : keyDrags ) { if ( !activeKeyDrags.contains( drag ) && - ( drag.buttons.matches( mask, pressedKeys ) || - ( doubleClick && drag.buttons.matches( doubleClickMask, pressedKeys ) ) ) ) + ( drag.buttons().matches( mask, pressedKeys ) || + ( doubleClick && drag.buttons().matches( doubleClickMask, pressedKeys ) ) ) ) { if ( dryRun ) return true; triggered = true; - drag.behaviour.init( mouseX, mouseY ); + drag.behaviour().init( mouseX, mouseY ); activeKeyDrags.add( drag ); } } for ( final BehaviourEntry< ClickBehaviour > click : keyClicks ) { - if ( click.buttons.matches( mask, pressedKeys ) || - ( doubleClick && click.buttons.matches( doubleClickMask, pressedKeys ) ) ) + if ( click.buttons().matches( mask, pressedKeys ) || + ( doubleClick && click.buttons().matches( doubleClickMask, pressedKeys ) ) ) { if ( dryRun ) return true; triggered = true; - click.behaviour.click( mouseX, mouseY ); + click.behaviour().click( mouseX, mouseY ); } } @@ -591,9 +483,9 @@ public void keyReleased( final KeyEvent e ) final ArrayList< BehaviourEntry< ? > > ended = new ArrayList<>(); for ( final BehaviourEntry< DragBehaviour > drag : activeKeyDrags ) - if ( !drag.buttons.matchesSubset( mask, pressedKeys ) ) + if ( !drag.buttons().matchesSubset( mask, globalKeys.pressedKeys() ) ) { - drag.behaviour.end( mouseX, mouseY ); + drag.behaviour().end( mouseX, mouseY ); ended.add( drag ); } activeKeyDrags.removeAll( ended ); diff --git a/src/main/java/org/scijava/ui/behaviour/ScrollBehaviour.java b/src/main/java/org/scijava/ui/behaviour/ScrollBehaviour.java index 7f7f314..a499e12 100644 --- a/src/main/java/org/scijava/ui/behaviour/ScrollBehaviour.java +++ b/src/main/java/org/scijava/ui/behaviour/ScrollBehaviour.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; public interface ScrollBehaviour extends Behaviour diff --git a/src/main/java/org/scijava/ui/behaviour/io/InputTriggerConfig.java b/src/main/java/org/scijava/ui/behaviour/io/InputTriggerConfig.java index c551e57..ac2af75 100644 --- a/src/main/java/org/scijava/ui/behaviour/io/InputTriggerConfig.java +++ b/src/main/java/org/scijava/ui/behaviour/io/InputTriggerConfig.java @@ -1,10 +1,41 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour.io; import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.Map.Entry; import java.util.Set; @@ -18,16 +49,16 @@ public class InputTriggerConfig implements InputTriggerAdder.Factory, KeyStrokeAdder.Factory { - final HashMap< String, Set< Input > > actionToInputsMap; + final LinkedHashMap< String, Set< Input > > actionToInputsMap; public InputTriggerConfig() { - actionToInputsMap = new HashMap<>(); + actionToInputsMap = new LinkedHashMap<>(); } public InputTriggerConfig( final Collection< InputTriggerDescription > keyMappings ) throws IllegalArgumentException { - actionToInputsMap = new HashMap<>(); + actionToInputsMap = new LinkedHashMap<>(); if ( keyMappings == null ) return; @@ -43,12 +74,7 @@ public InputTriggerConfig( final Collection< InputTriggerDescription > keyMappin final InputTrigger trigger = InputTrigger.getFromString( triggerStr ); final Input input = new Input( trigger, behaviour, contexts ); - Set< Input > inputs = actionToInputsMap.get( input.behaviour ); - if ( inputs == null ) - { - inputs = new HashSet<>(); - actionToInputsMap.put( input.behaviour, inputs ); - } + final Set< Input > inputs = actionToInputsMap.computeIfAbsent( input.behaviour, k -> new LinkedHashSet<>() ); inputs.add( input ); } } @@ -60,13 +86,155 @@ public InputTriggerAdder inputTriggerAdder( final InputTriggerMap map, final Str return new InputTriggerAdderImp( map, this, contexts ); } - @Override public KeyStrokeAdder keyStrokeAdder( final InputMap map, final String ... contexts ) { return new KeyStrokeAdderImp( map, this, contexts ); } + public Set< InputTrigger > getInputs( final String behaviourName, final String context ) + { + return getInputs( behaviourName, Collections.singleton( context ) ); + } + + public Set< InputTrigger > getInputs( final String behaviourName, final Set< String > contexts ) + { + final Set< Input > inputs = actionToInputsMap.get( behaviourName ); + final Set< InputTrigger > triggers = new LinkedHashSet<>(); + if ( inputs != null ) + { + for ( final Input input : inputs ) + if ( ! Collections.disjoint( contexts, input.contexts ) ) + triggers.add( input.trigger ); + } + return triggers; + } + + /** + * Creates a pretty printed list of keys, useful in conjunction with + * {@link InputTriggerConfig#getInputs}. + * + * @param triggers + * Set of triggers to be printed. + * + * @return String with formatted content. + */ + public static String prettyPrintInputs( final Set< InputTrigger > triggers ) + { + final StringBuilder sb = new StringBuilder(); + final Iterator< InputTrigger > it = triggers.iterator(); + while ( it.hasNext() ) + { + sb.append( it.next() ); + if ( it.hasNext() ) + sb.append( " or " ); + } + return sb.toString(); + } + + public void clear() + { + actionToInputsMap.clear(); + } + + public void set( final InputTriggerConfig config ) + { + actionToInputsMap.clear(); + for ( final Entry< String, Set< Input > > entry : config.actionToInputsMap.entrySet() ) + { + final String behaviourName = entry.getKey(); + final Set< Input > inputs = new LinkedHashSet<>(); + entry.getValue().forEach( i -> inputs.add( i.copy() ) ); + actionToInputsMap.put( behaviourName, inputs ); + } + } + + public void add( final String trigger, final String behaviourName, final String context ) + { + add( InputTrigger.getFromString( trigger ), behaviourName, context ); + } + + public void add( final InputTrigger trigger, final String behaviourName, final String context ) + { + add( trigger, behaviourName, Collections.singleton( context ) ); + } + + public synchronized void add( final InputTrigger trigger, final String behaviourName, final Collection< String > contexts ) + { + final Set< Input > inputs = actionToInputsMap.computeIfAbsent( behaviourName, k -> new LinkedHashSet<>() ); + for ( final Input input : inputs ) + { + if ( input.trigger.equals( trigger ) ) + { + /* + * the trigger -> behaviour binding already exists. + * just add the new context + */ + input.contexts.addAll( contexts ); + return; + /* + * NB: this assumes that there exists not more than one Input + * record for each (trigger, behaviour) pair. This property is + * maintained by the add/remove implementations. + */ + } + } + + /* + * the trigger -> behaviour binding does not exist. + * add it + */ + inputs.add( new Input( trigger, behaviourName, contexts ) ); + } + + public void remove( final String trigger, final String behaviourName, final String context ) + { + remove( InputTrigger.getFromString( trigger ), behaviourName, context ); + } + + public void remove( final InputTrigger trigger, final String behaviourName, final String context ) + { + remove( trigger, behaviourName, Collections.singleton( context ) ); + } + + public synchronized void remove( final InputTrigger trigger, final String behaviourName, final Collection< String > contexts ) + { + final Set< Input > inputs = actionToInputsMap.get( behaviourName ); + if ( inputs == null ) + return; + + for ( final Input input : inputs ) + { + if ( input.trigger.equals( trigger ) ) + { + // found Input that covers this trigger -> behaviour binding, + // make sure it does not exist for the given context(s) + input.contexts.removeAll( contexts ); + if ( input.contexts.isEmpty() ) + { + // empty context set -> invalid record -> remove it + inputs.remove( input ); + + if ( inputs.isEmpty() ) + actionToInputsMap.remove( behaviourName ); + } + + return; + /* + * NB: this assumes that there exists not more than one Input + * record for each (trigger, behaviour) pair. This property is + * maintained by the add/remove implementations. + */ + } + } + } + + @Override + public String toString() + { + return "InputTriggerConfig{" + new InputTriggerDescriptionsBuilder( this ).getDescriptions() + '}'; + } + public static class InputTriggerAdderImp implements InputTriggerAdder { private final InputTriggerMap map; @@ -92,8 +260,7 @@ public InputTriggerAdderImp( { this.map = map; this.config = config; - this.contexts = new HashSet<>(); - this.contexts.addAll( Arrays.asList( contexts ) ); + this.contexts = new HashSet<>( Arrays.asList( contexts ) ); } @Override @@ -110,11 +277,17 @@ public void put( final String behaviourName, final InputTrigger ... defaultTrigg } else if ( defaultTriggers.length > 0 ) { - if ( defaultTriggers[ 0 ].equals( InputTrigger.NOT_MAPPED )) + if ( defaultTriggers[ 0 ].equals( InputTrigger.NOT_MAPPED ) ) + { + config.add( InputTrigger.NOT_MAPPED, behaviourName, contexts ); return; + } for ( final InputTrigger trigger : defaultTriggers ) + { + config.add( trigger, behaviourName, contexts ); map.put( trigger, behaviourName ); + } } else { @@ -164,10 +337,10 @@ public KeyStrokeAdderImp( { this.map = map; this.config = config; - this.contexts = new HashSet<>(); - this.contexts.addAll( Arrays.asList( contexts ) ); + this.contexts = new HashSet<>( Arrays.asList( contexts ) ); } + @Deprecated @Override public void put( final String actionName, final KeyStroke... defaultKeyStrokes ) { @@ -190,7 +363,10 @@ public void put( final String actionName, final KeyStroke... defaultKeyStrokes ) if ( defaultKeyStrokes.length > 0 ) { for ( final KeyStroke keyStroke : defaultKeyStrokes ) + { + config.add( InputTrigger.getFromString( keyStroke.toString() ), actionName, contexts ); map.put( keyStroke, actionName ); + } } else { @@ -198,13 +374,55 @@ public void put( final String actionName, final KeyStroke... defaultKeyStrokes ) } } + @Override + public void put( final String actionName, final InputTrigger... defaultKeyStrokes ) + { + final Set< InputTrigger > triggers = config.getInputs( actionName, contexts ); + if ( triggers.contains( InputTrigger.NOT_MAPPED ) ) + return; + + boolean configKeyAdded = false; + for ( final InputTrigger trigger : triggers ) + { + if ( trigger.isKeyStroke() ) + { + map.put( trigger.getKeyStroke(), actionName ); + configKeyAdded = true; + } + } + if ( configKeyAdded ) + return; + + if ( defaultKeyStrokes.length > 0 ) + { + if ( defaultKeyStrokes[ 0 ].equals( InputTrigger.NOT_MAPPED ) ) + { + config.add( InputTrigger.NOT_MAPPED, actionName, contexts ); + return; + } + + for ( final InputTrigger trigger : defaultKeyStrokes ) + { + if ( trigger.isKeyStroke() ) + { + config.add( trigger, actionName, contexts ); + map.put( trigger.getKeyStroke(), actionName ); + configKeyAdded = true; + } + } + } + + if ( !configKeyAdded ) + System.err.println( "Could not assign KeyStroke for \"" + actionName + "\". Nothing defined in InputTriggerConfig, and no default given." ); + } + @Override public void put( final String actionName, final String... defaultKeyStrokes ) { - final KeyStroke[] keyStrokes = new KeyStroke[ defaultKeyStrokes.length ]; + final InputTrigger[] keyStrokes = new InputTrigger[ defaultKeyStrokes.length ]; int i = 0; for ( final String s : defaultKeyStrokes ) - keyStrokes[ i++ ] = KeyStroke.getKeyStroke( s ); + keyStrokes[ i++ ] = InputTrigger.getFromString( s ); put( actionName, keyStrokes ); } @@ -230,13 +448,25 @@ static class Input Input( final InputTrigger trigger, final String behaviour, - final Set< String > contexts ) + final Collection< String > contexts ) { this.trigger = trigger; this.behaviour = behaviour; this.contexts = new HashSet<>( contexts ); } + Input( final Input input ) + { + this.trigger = input.trigger; + this.behaviour = input.behaviour; + this.contexts = new HashSet<>( input.contexts ); + } + + Input copy() + { + return new Input( this ); + } + @Override public int hashCode() { @@ -250,7 +480,7 @@ public int hashCode() @Override public boolean equals( final Object obj ) { - if ( obj == null && !( obj instanceof Input ) ) + if ( obj == null || !( obj instanceof Input ) ) return false; final Input i = ( Input ) obj; return i.trigger.equals( trigger ) && i.behaviour.equals( behaviour ) && i.contexts.equals( contexts ); @@ -262,19 +492,6 @@ InputTriggerDescription getDescription() } } - Set< InputTrigger > getInputs( final String behaviourName, final Set< String > contexts ) - { - final Set< Input > inputs = actionToInputsMap.get( behaviourName ); - final Set< InputTrigger > triggers = new HashSet<>(); - if ( inputs != null ) - { - for ( final Input input : inputs ) - if ( ! Collections.disjoint( contexts, input.contexts ) ) - triggers.add( input.trigger ); - } - return triggers; - } - /* * creating InputTriggerConfig from InputTriggerMaps and InputMaps */ @@ -288,12 +505,7 @@ void addMap( final InputTriggerMap map, final String context ) for ( final String behaviourName : behaviours ) { - Set< Input > inputs = actionToInputsMap.get( behaviourName ); - if ( inputs == null ) - { - inputs = new HashSet<>(); - actionToInputsMap.put( behaviourName, inputs ); - } + final Set< Input > inputs = actionToInputsMap.computeIfAbsent( behaviourName, k -> new LinkedHashSet<>() ); boolean added = false; for ( final Input input : inputs ) @@ -329,12 +541,7 @@ void addMap( final InputMap map, final String context ) final InputTrigger trigger = InputTrigger.getFromString( key.toString() ); final String behaviourName = map.get( key ).toString(); - Set< Input > inputs = actionToInputsMap.get( behaviourName ); - if ( inputs == null ) - { - inputs = new HashSet<>(); - actionToInputsMap.put( behaviourName, inputs ); - } + final Set< Input > inputs = actionToInputsMap.computeIfAbsent( behaviourName, k -> new LinkedHashSet<>() ); boolean added = false; for ( final Input input : inputs ) @@ -342,7 +549,7 @@ void addMap( final InputMap map, final String context ) if ( input.trigger.equals( trigger ) ) { /* - * the trigger -> behavioiur binding already exists. + * the trigger -> behaviour binding already exists. * just add the new context */ input.contexts.add( context ); diff --git a/src/main/java/org/scijava/ui/behaviour/io/InputTriggerDescription.java b/src/main/java/org/scijava/ui/behaviour/io/InputTriggerDescription.java index eeb4d38..7003775 100644 --- a/src/main/java/org/scijava/ui/behaviour/io/InputTriggerDescription.java +++ b/src/main/java/org/scijava/ui/behaviour/io/InputTriggerDescription.java @@ -1,5 +1,35 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour.io; +import java.util.Arrays; import javax.swing.Action; import org.scijava.ui.behaviour.Behaviour; @@ -92,4 +122,13 @@ public void setContexts( final String[] contexts ) else this.contexts = contexts; } -} \ No newline at end of file + + public void addTrigger( final String trigger ) + { + if ( !Arrays.asList( triggers ).contains( trigger ) ) + { + triggers = Arrays.copyOf( triggers, triggers.length + 1 ); + triggers[ triggers.length - 1 ] = trigger; + } + } +} diff --git a/src/main/java/org/scijava/ui/behaviour/io/InputTriggerDescriptionsBuilder.java b/src/main/java/org/scijava/ui/behaviour/io/InputTriggerDescriptionsBuilder.java index 004d0f0..5f46d96 100644 --- a/src/main/java/org/scijava/ui/behaviour/io/InputTriggerDescriptionsBuilder.java +++ b/src/main/java/org/scijava/ui/behaviour/io/InputTriggerDescriptionsBuilder.java @@ -1,8 +1,38 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour.io; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.List; import java.util.Map.Entry; import java.util.Set; @@ -24,17 +54,20 @@ public class InputTriggerDescriptionsBuilder public InputTriggerDescriptionsBuilder() { - config = new InputTriggerConfig(); + this( new InputTriggerConfig() ); + } + + public InputTriggerDescriptionsBuilder( final InputTriggerConfig config ) + { + this.config = config; } public List< InputTriggerDescription > getDescriptions() { final ArrayList< InputTriggerDescription > descs = new ArrayList<>(); - final String[] emptyStringArray = new String[ 0 ]; - for ( final Entry< String, Set< Input > > entry : config.actionToInputsMap.entrySet() ) + for ( final Set< Input > inputs : config.actionToInputsMap.values() ) { - final Set< Input > inputs = entry.getValue(); for ( final Input input : inputs ) { boolean found = false; @@ -43,20 +76,14 @@ public List< InputTriggerDescription > getDescriptions() if ( input.behaviour.equals( desc.getAction() ) && input.contexts.equals( new HashSet<>( Arrays.asList( desc.getContexts() ) ) ) ) { - final HashSet< String > triggers = new HashSet<>( Arrays.asList( desc.getTriggers() ) ); - triggers.add( input.trigger.toString() ); - desc.setTriggers( triggers.toArray( emptyStringArray ) ); + desc.addTrigger( input.trigger.toString() ); found = true; break; } } if ( !found ) { - final InputTriggerDescription desc = new InputTriggerDescription( - new String[] { input.trigger.toString() }, - input.behaviour, - input.contexts.toArray( emptyStringArray ) ); - descs.add( desc ); + descs.add( input.getDescription() ); } } } @@ -64,6 +91,20 @@ public List< InputTriggerDescription > getDescriptions() return descs; } + public Set< String > getContexts() + { + final Set< String > contexts = new LinkedHashSet<>(); + for ( final Entry< String, Set< Input > > entry : config.actionToInputsMap.entrySet() ) + for ( final Input input : entry.getValue() ) + contexts.addAll( input.contexts ); + return contexts; + } + + public Set< String > getBehaviourNames() + { + return new LinkedHashSet<>( config.actionToInputsMap.keySet() ); + } + public void addMap( final InputTriggerMap map, final String context ) { config.addMap( map, context ); diff --git a/src/main/java/org/scijava/ui/behaviour/io/gui/Command.java b/src/main/java/org/scijava/ui/behaviour/io/gui/Command.java new file mode 100644 index 0000000..f4a4a13 --- /dev/null +++ b/src/main/java/org/scijava/ui/behaviour/io/gui/Command.java @@ -0,0 +1,81 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.ui.behaviour.io.gui; + +/** + * Behaviour or action name in a context. + */ +public class Command +{ + private final String name; + + private final String context; + + public Command( final String name, final String context ) + { + if ( name == null || context == null ) + throw new IllegalArgumentException(); + this.name = name; + this.context = context; + } + + public String getName() + { + return name; + } + + public String getContext() + { + return context; + } + + @Override + public boolean equals( final Object o ) + { + if ( this == o ) + return true; + if ( o == null || getClass() != o.getClass() ) + return false; + + final Command command = ( Command ) o; + + if ( !name.equals( command.name ) ) + return false; + return context.equals( command.context ); + } + + @Override + public int hashCode() + { + int result = name.hashCode(); + result = 31 * result + context.hashCode(); + return result; + } +} diff --git a/src/main/java/org/scijava/ui/behaviour/io/gui/CommandDescriptionProvider.java b/src/main/java/org/scijava/ui/behaviour/io/gui/CommandDescriptionProvider.java new file mode 100644 index 0000000..7fd072c --- /dev/null +++ b/src/main/java/org/scijava/ui/behaviour/io/gui/CommandDescriptionProvider.java @@ -0,0 +1,121 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.ui.behaviour.io.gui; + +import org.scijava.plugin.SciJavaPlugin; + +/** + * Implementations of this interface, annotated with {@code @Plugin}, are + * discovered for automatically adding actions/behaviours to a + * {@link CommandDescriptions} map. + *

+ * (This allows to discover Plugin shortcuts which cannot be hardwired into the + * default keymap.) + */ +public abstract class CommandDescriptionProvider implements SciJavaPlugin +{ + private final String[] expectedContexts; + + private final Scope scope; + + protected CommandDescriptionProvider( final Scope scope, final String... expectedContexts ) + { + this.scope = scope; + this.expectedContexts = expectedContexts; + } + + /** + * The scope describes the application / Fiji plugin that the actions are + * defined in. This is to make it possible to only harvest descriptions + * for the desired scopes. + *

+ * Note that {@code CommandDescriptionProvider} is only used for harvesting + * actions/behaviours for the config dialog. So basically it has nothing to + * do with reality, necessarily. Whether these actions are ever actualized + * depends on other code! The {@code scope} is for which scope + * the config dialog will make the actions configurable. For example, if you + * put {"mastodon"} there, then the {@code InputTriggerConfig} will be picked + * up by a config dialog requesting these scopes. + */ + public Scope getScope() + { + return scope; + } + + /** + * The contexts in which the described actions/behaviours are expected to be + * used. + *

+ * Note that {@code CommandDescriptionProvider} is only used for harvesting + * actions/behaviours for the config dialog. So basically it has nothing to + * do with reality, necessarily. Whether these actions are ever actualized + * depends on other code! The {@code expectedContexts} is for which context + * the config dialog will make the actions configurable. For example, if you + * put {"bdv", "ts"} there, then the {@code InputTriggerConfig} made by the + * config dialog will put InputTriggers with these contexts. + */ + public String[] getExpectedContexts() + { + return expectedContexts; + } + + public abstract void getCommandDescriptions( final CommandDescriptions descriptions ); + + public static class Scope + { + private final String name; + + public Scope( final String name ) + { + this.name = name; + } + + public String getName() + { + return name; + } + + @Override + public boolean equals( final Object o ) + { + if ( this == o ) + return true; + if ( !( o instanceof Scope ) ) + return false; + return name.equals( ( ( Scope ) o ).name ); + } + + @Override + public int hashCode() + { + return name.hashCode(); + } + } +} diff --git a/src/main/java/org/scijava/ui/behaviour/io/gui/CommandDescriptions.java b/src/main/java/org/scijava/ui/behaviour/io/gui/CommandDescriptions.java new file mode 100644 index 0000000..b994597 --- /dev/null +++ b/src/main/java/org/scijava/ui/behaviour/io/gui/CommandDescriptions.java @@ -0,0 +1,161 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.ui.behaviour.io.gui; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.scijava.ui.behaviour.InputTrigger; +import org.scijava.ui.behaviour.io.InputTriggerConfig; + +/** + * A map linking {@link Command} (action name and context name) to its + * {@link DescriptionAndTriggers} (textual description and default triggers of + * the command). + *

+ * New entries are added using {@link #add(String, String[], String)}. This + * specifies only the action name of the {@code Command}. The context of the + * {@code Command} is the current context of this + * {@code CommandDescriptions}. The current context is changed by + * {@link #setKeyconfigContext(String)}, and then used for subsequently added + * entries. + */ +public final class CommandDescriptions +{ + public static final class DescriptionAndTriggers + { + private final String description; + + private final String[] defaultTriggers; + + public DescriptionAndTriggers( final String description, final String[] defaultTriggers ) + { + this.description = description; + this.defaultTriggers = defaultTriggers; + } + } + + private final Map< Command, DescriptionAndTriggers > descriptions = new LinkedHashMap<>(); + + private String context; + + /** + * Adds a new entry, linking a {@code Command} to textual description and + * default triggers. + * + * @param name + * name of the {@code Action} or {@code Behaviour}. Together with + * the current context name (see + * {@link #setKeyconfigContext(String)}) this specifies a + * {@link Command}. + * @param defaultTriggers + * default {@link InputTrigger}s for the command. + * @param description + * textual description of the command (for displaying in UI). + */ + public void add( final String name, final String[] defaultTriggers, final String description ) + { + final Command c = new Command( name, context ); + final DescriptionAndTriggers cd = new DescriptionAndTriggers( description, defaultTriggers ); + descriptions.put( c, cd ); + } + + /** + * Sets the current context. This context name is then used for subsequently + * {@link #add(String, String[], String) added} commands. + * + * @param context + * the context name. + */ + public void setKeyconfigContext( final String context ) + { + this.context = context; + } + + /** + * Builds a map from {@link Command} to textual description. This is for + * making a keyconfig {@link VisualEditorPanel}. + * + * @return a new map. + */ + public Map< Command, String > createCommandDescriptionsMap() + { + final Map< Command, String > map = new LinkedHashMap<>(); + descriptions.forEach( ( c, d ) -> map.put( c, d.description ) ); + return map; + } + + /** + * Builds a {@link InputTriggerConfig} with all commands and their default + * triggers. Commands that have no specified default triggers will have + * trigger {@code "not mapped"}. + * + * @return a new {@link InputTriggerConfig}. + */ + public InputTriggerConfig createDefaultKeyconfig() + { + final InputTriggerConfig config = new InputTriggerConfig(); + descriptions.forEach( ( c, d ) -> { + final String name = c.getName(); + final String context = c.getContext(); + final String[] triggers = d.defaultTriggers; + if ( triggers == null || triggers.length == 0 ) + config.add( "not mapped", name, context ); + else + Arrays.stream( triggers ).forEachOrdered( t -> config.add( t, name, context ) ); + } ); + return config; + } + + /** + * For commands that are not yet defined in {@code config}, add them with + * their default triggers. Commands that have no specified default triggers + * will have trigger {@code "not mapped"}. + * + * @param config + * the input trigger config to add commands to. + */ + public void augmentInputTriggerConfig( final InputTriggerConfig config ) + { + descriptions.forEach( ( c, d ) -> { + final String name = c.getName(); + final String context = c.getContext(); + if ( config.getInputs( name, context ).isEmpty() ) + { + final String[] triggers = d.defaultTriggers; + if ( triggers == null || triggers.length == 0 ) + config.add( "not mapped", name, context ); + else + Arrays.stream( triggers ).forEachOrdered( t -> config.add( t, name, context ) ); + } + } ); + } +} diff --git a/src/main/java/org/scijava/ui/behaviour/io/gui/CommandDescriptionsBuilder.java b/src/main/java/org/scijava/ui/behaviour/io/gui/CommandDescriptionsBuilder.java new file mode 100644 index 0000000..e40dfe7 --- /dev/null +++ b/src/main/java/org/scijava/ui/behaviour/io/gui/CommandDescriptionsBuilder.java @@ -0,0 +1,180 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.ui.behaviour.io.gui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import java.util.function.Predicate; +import org.scijava.AbstractContextual; +import org.scijava.plugin.Parameter; +import org.scijava.plugin.PluginService; + +/** + * Helper class to populate {@link CommandDescriptions} from + * {@link CommandDescriptionProvider}s. + */ +public class CommandDescriptionsBuilder extends AbstractContextual +{ + @Parameter + private PluginService pluginService; + + private static class ProviderAndContext + { + final CommandDescriptionProvider provider; + + final String context; + + public ProviderAndContext( final CommandDescriptionProvider provider, final String context ) + { + this.provider = provider; + this.context = context; + } + } + + private final List< ProviderAndContext > registered = new ArrayList<>(); + + /** + * Manually adds a {@code provider} in a specified {@code context}. For + * example, this is useful for adding stuff in a specific order, for + * building nice {@code keyconfig.yaml} files. + * + * @param provider + * the provider to add. + * @param context + * the context to add to. + */ + public void addManually( final CommandDescriptionProvider provider, final String context ) + { + for ( final ProviderAndContext pac : registered ) + { + if ( pac.context.equals( context ) && pac.provider.getClass().equals( provider.getClass() ) ) + System.err.println( "Potential problem: a provider of class " + provider.getClass() + " is already registered for context \"" + context + "\"." ); + } + registered.add( new ProviderAndContext( provider, context ) ); + } + + /** + * Manually add a {@code provider} in the specified {@code contexts}. For + * example, this is useful for adding stuff in a specific order, for + * building nice {@code keyconfig.yaml} files. + * + * @param provider + * the provider to add. + * @param contexts + * the list of contexts to add to. + */ + public void addManually( final CommandDescriptionProvider provider, final String ... contexts ) + { + Arrays.stream( contexts ).forEachOrdered( context -> addManually( provider, context ) ); + } + + /** + * Adds all {@link CommandDescriptionProvider}s on the plugin index, with + * their respective {@link CommandDescriptionProvider#getExpectedContexts() + * expected contexts}. + */ + public void discoverProviders() + { + discoverProviders( p -> true ); + } + + /** + * Adds all {@link CommandDescriptionProvider}s on the plugin index having + * any of the given {@code scopes}, with their respective {@link + * CommandDescriptionProvider#getExpectedContexts() expected contexts}. + */ + public void discoverProviders( final CommandDescriptionProvider.Scope ... scopes ) + { + discoverProviders( provider -> Arrays.asList( scopes ).contains( provider.getScope() ) ); + } + + /** + * Adds all {@link CommandDescriptionProvider}s on the plugin index that + * match the given {@code predicate}, with their respective {@link + * CommandDescriptionProvider#getExpectedContexts() expected contexts}. + */ + public void discoverProviders( final Predicate< CommandDescriptionProvider > predicate ) + { + final List< CommandDescriptionProvider > providers = pluginService.createInstancesOfType( CommandDescriptionProvider.class ); + for ( final CommandDescriptionProvider provider : providers ) + if ( predicate.test( provider ) ) + for ( final String context : provider.getExpectedContexts() ) + registered.add( new ProviderAndContext( provider, context ) ); + } + + /** + * Debugging helper. Checks whether all manually added providers are + * automatically discovered, and vice versa. Prints warnings to stderr + * otherwise. + */ + public void verifyManuallyAdded() + { + final List< ProviderAndContext > discovered = new ArrayList<>(); + final List< CommandDescriptionProvider > providers = pluginService.createInstancesOfType( CommandDescriptionProvider.class ); + for ( final CommandDescriptionProvider provider : providers ) + for ( final String context : provider.getExpectedContexts() ) + discovered.add( new ProviderAndContext( provider, context ) ); + + // Can all registered providers be discovered? + boolean anyFailed = false; + A: for ( final ProviderAndContext r : registered ) + { + for ( final ProviderAndContext d : discovered ) + if ( r.context.equals( d.context ) && r.provider.getClass().equals( d.provider.getClass() ) ) + continue A; + System.err.println( r.provider.getClass() + " (\"" + r.context + "\") is manually registered, but could not be discovered." ); + anyFailed = true; + } + if ( anyFailed ) + System.err.println(); + + // Were all discovered providers manually added? + A: for ( final ProviderAndContext d : discovered ) + { + for ( final ProviderAndContext r : registered ) + if ( r.context.equals( d.context ) && r.provider.getClass().equals( d.provider.getClass() ) ) + continue A; + System.err.println( d.provider.getClass() + " (\"" + d.context + "\") was discovered, but was not manually registered." ); + } + } + + public CommandDescriptions build() + { + final CommandDescriptions descriptions = new CommandDescriptions(); + for ( final ProviderAndContext pac : registered ) + { + descriptions.setKeyconfigContext( pac.context ); + pac.provider.getCommandDescriptions( descriptions ); + } + return descriptions; + } +} diff --git a/src/main/java/org/scijava/ui/behaviour/io/gui/InputTriggerPanelEditor.java b/src/main/java/org/scijava/ui/behaviour/io/gui/InputTriggerPanelEditor.java new file mode 100644 index 0000000..801842f --- /dev/null +++ b/src/main/java/org/scijava/ui/behaviour/io/gui/InputTriggerPanelEditor.java @@ -0,0 +1,669 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.ui.behaviour.io.gui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import javax.swing.AbstractAction; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; + +import org.scijava.ui.behaviour.InputTrigger; + +import static org.scijava.ui.behaviour.io.gui.TagPanelEditor.mix; + +public class InputTriggerPanelEditor extends JPanel +{ + + @FunctionalInterface + public static interface InputTriggerChangeListener + { + public void inputTriggerChanged(); + } + + private static final long serialVersionUID = 1L; + + private static final String COMMIT_ACTION = "commit"; + + private final List< KeyItem > keyItems; + + private final JTextField textField; + + private final HashSet< InputTriggerChangeListener > listeners; + + private InputTrigger trigger = InputTrigger.NOT_MAPPED; + + private InputTrigger lastValidInputTrigger = InputTrigger.NOT_MAPPED; + + private String invalidTriggerStr = null; + + public InputTriggerPanelEditor( final boolean editable ) + { + this.keyItems = new ArrayList<>(); + this.listeners = new HashSet<>(); + + setPreferredSize( new Dimension( 400, 26 ) ); + setMinimumSize( new Dimension( 26, 26 ) ); + setLayout( new BoxLayout( this, BoxLayout.LINE_AXIS ) ); + + this.textField = new JTextField(); + textField.setColumns( 10 ); + textField.setBorder( new EmptyBorder( 0, 0, 0, 0 ) ); + textField.setOpaque( false ); + textField.setEditable( editable ); + + if ( editable ) + { + final Autocomplete autoComplete = new Autocomplete(); + textField.getDocument().addDocumentListener( autoComplete ); + textField.getInputMap().put( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 ), COMMIT_ACTION ); + textField.getInputMap().put( KeyStroke.getKeyStroke( ' ' ), COMMIT_ACTION ); + textField.setFocusTraversalKeys( KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, Collections.emptySet() ); + textField.getInputMap().put( KeyStroke.getKeyStroke( KeyEvent.VK_TAB, 0 ), COMMIT_ACTION ); + textField.getActionMap().put( COMMIT_ACTION, autoComplete.new CommitAction() ); + textField.addKeyListener( new KeyAdapter() + { + @Override + public void keyPressed( final KeyEvent e ) + { + /* + * We have to use a key listener to deal separately with + * character removal and tag removal. + */ + if ( e.getKeyCode() == KeyEvent.VK_BACK_SPACE && textField.getText().isEmpty() && !keyItems.isEmpty() ) + { + removeLastKeyItem(); + e.consume(); + } + } + } ); + } + + add( textField ); + add( Box.createHorizontalGlue() ); + } + + @Override + public void updateUI() + { + super.updateUI(); + setBorder( UIManager.getBorder( "TextField.border" ) ); + setBackground( UIManager.getColor( "TextField.background" ) ); + } + + public void setInputTrigger( final InputTrigger trigger ) + { + this.trigger = trigger; + this.lastValidInputTrigger = trigger; + this.invalidTriggerStr = null; + regenKeyPanels(); + } + + public InputTrigger getInputTrigger() + { + return trigger; + } + + public InputTrigger getLastValidInputTrigger() + { + return lastValidInputTrigger; + } + + private void checkAndAppendKey( final String key ) + { + final String trimmed = key.trim(); + if ( trimmed.equals( "|" ) ) + { + /* + * Special case: the user wants to enter keys to ignore. + */ + + final String str = ( null == trigger || trigger == InputTrigger.NOT_MAPPED ) + ? trimmed + : trigger.toString() + " " + trimmed; + + this.trigger = null; + this.invalidTriggerStr = str.trim(); + regenKeyPanels(); + return; + } + + // Find its proper case equivalent in the syntax list. + String properCapKey = null; + for ( int i = 0; i < INPUT_TRIGGER_SYNTAX_TAGS.size(); i++ ) + { + if ( INPUT_TRIGGER_SYNTAX_TAGS.get( i ).equalsIgnoreCase( trimmed ) ) + { + properCapKey = INPUT_TRIGGER_SYNTAX_TAGS.get( i ); + break; + } + } + + if ( null != properCapKey ) + { + // Some tags are replaced for constructing the trigger, e.g., "cmd" is replaces by "meta" + properCapKey = INPUT_TRIGGER_SYNTAX_TAG_REMAP.getOrDefault( properCapKey, properCapKey ); + + // Try to append the key to the trigger. + final String str = ( null == trigger ) + ? invalidTriggerStr + " " + properCapKey + : ( trigger == InputTrigger.NOT_MAPPED ) + ? properCapKey + : trigger.toString() + " " + properCapKey; + + try + { + this.trigger = InputTrigger.getFromString( str ); + this.lastValidInputTrigger = trigger; + this.invalidTriggerStr = null; + } + catch ( final IllegalArgumentException iae ) + { + this.trigger = null; + this.invalidTriggerStr = str.trim(); + } + regenKeyPanels(); + } + } + + private void removeLastKeyItem() + { + // Try to remove the last key item. + final String[] tokens; + if ( null == trigger ) + { + /* + * The trigger was invalid. In that case we show what would be the + * key sequence in red. + */ + tokens = invalidTriggerStr.split( " " ); + } + else + { + /* + * The trigger field is valid. + */ + tokens = trigger.toString().split( " " ); + } + if ( tokens.length == 0 ) + return; + + // We have to sort tokens as they are displayed. + sortTokens( tokens ); + + final StringBuilder strBlder = new StringBuilder(); + for ( int i = 0; i < tokens.length - 1; i++ ) + strBlder.append( tokens[ i ] + " " ); + + final String str = strBlder.toString(); + try + { + this.trigger = str.isEmpty() + ? InputTrigger.NOT_MAPPED + : InputTrigger.getFromString( str ); + this.lastValidInputTrigger = trigger; + this.invalidTriggerStr = null; + } + catch ( final IllegalArgumentException iae ) + { + this.trigger = null; + this.invalidTriggerStr = str.trim(); + } + regenKeyPanels(); + notifyListeners(); + } + + private void regenKeyPanels() + { + // Clear + for ( final KeyItem keyItem : keyItems ) + remove( keyItem ); + keyItems.clear(); + + final String[] tokens; + boolean valid; + if ( null == trigger ) + { + /* + * The trigger was invalid. In that case we show what would be the + * key sequence in red. + */ + tokens = invalidTriggerStr.split( " " ); + valid = false; + } + else + { + /* + * The trigger field is valid. + */ + + valid = true; + if ( trigger == InputTrigger.NOT_MAPPED ) + tokens = new String[] {}; + else + tokens = trigger.toString().split( " " ); + + } + + if ( tokens.length == 0 ) + { + trigger = InputTrigger.NOT_MAPPED; + lastValidInputTrigger = trigger; + invalidTriggerStr = null; + } + else + { + sortTokens( tokens ); + for ( final String key : tokens ) + { + final KeyItem tagp = new KeyItem( key, valid ); + keyItems.add( tagp ); + add( tagp, getComponentCount() - 2 ); + } + } + + revalidate(); + repaint(); + } + + @Override + public boolean requestFocusInWindow() + { + return textField.requestFocusInWindow(); + } + + /* + * INNER CLASSES + */ + + /** + * Adapted from + * http://stackabuse.com/example-adding-autocomplete-to-jtextfield/ + */ + private class Autocomplete implements DocumentListener + { + + @Override + public void changedUpdate( final DocumentEvent ev ) + {} + + @Override + public void removeUpdate( final DocumentEvent ev ) + {} + + @Override + public void insertUpdate( final DocumentEvent ev ) + { + if ( ev.getLength() != 1 ) + return; + + final int pos = ev.getOffset(); + String content = null; + try + { + content = textField.getText( 0, pos + 1 ); + } + catch ( final BadLocationException e ) + { + e.printStackTrace(); + } + + // Find where the word starts + int w; + for ( w = pos; w >= 0; w-- ) + { + if ( !Character.isLetter( content.charAt( w ) ) ) + { + break; + } + } + + // Too few chars + if ( pos - w < 2 ) + return; + + final String prefix = content.substring( w + 1 ); + // We search on the lower case list. + final int n = Collections.binarySearch( INPUT_TRIGGER_SYNTAX_TAGS_SMALL_CAPS, prefix.toLowerCase() ); + if ( n < 0 && -n <= INPUT_TRIGGER_SYNTAX_TAGS_SORTED.size() ) + { + final String match = INPUT_TRIGGER_SYNTAX_TAGS_SORTED.get( -n - 1 ); + if ( match.toLowerCase().startsWith( prefix.toLowerCase() ) ) + { + // A completion is found + final String completion = match.substring( pos - w ); + // We cannot modify Document from within notification, + // so we submit a task that does the change later + SwingUtilities.invokeLater( new CompletionTask( completion, pos + 1 ) ); + } + } + } + + public class CommitAction extends AbstractAction + { + private static final long serialVersionUID = 5794543109646743416L; + + @Override + public void actionPerformed( final ActionEvent ev ) + { + final String key = textField.getText().trim(); + if ( key.isEmpty() ) + { + KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent(); + return; + } + checkAndAppendKey( key ); + textField.setText( "" ); + notifyListeners(); + } + } + + private class CompletionTask implements Runnable + { + private final String completion; + + private final int position; + + CompletionTask( final String completion, final int position ) + { + this.completion = completion; + this.position = position; + } + + @Override + public void run() + { + final StringBuffer sb = new StringBuffer( textField.getText() ); + sb.insert( position, completion ); + textField.setText( sb.toString() ); + textField.setCaretPosition( position + completion.length() ); + textField.moveCaretPosition( position ); + } + } + + } + + private final class KeyItem extends JPanel + { + private static final long serialVersionUID = 1L; + + private final boolean valid; + + private final JLabel txt; + + public KeyItem( final String tag, final boolean valid ) + { + this.valid = valid; + final String str = TRIGGER_SYMBOLS.containsKey( tag ) ? ( " " + TRIGGER_SYMBOLS.get( tag ) + " " ) : ( " " + tag + " " ); + txt = new JLabel( str ); + txt.setOpaque( true ); + updateTxtLook(); + + setLayout( new BoxLayout( this, BoxLayout.LINE_AXIS ) ); + add( Box.createHorizontalStrut( 1 ) ); + add( txt ); + add( Box.createHorizontalStrut( 1 ) ); + setOpaque( false ); + } + + @Override + public void updateUI() + { + super.updateUI(); + updateTxtLook(); + } + + private void updateTxtLook() + { + if ( txt != null ) + { + final Color tfg = UIManager.getColor( "TextField.foreground" ); + final Color tbg = UIManager.getColor( "TextField.background" ); + final Color bg = valid ? mix( tbg, tfg, 0.95 ) : mix( tbg, Color.red, 0.5 ); + final Color borderColor = mix( bg, tfg, 0.8 ); + txt.setBackground( bg ); + txt.setBorder( new RoundBorder( borderColor, InputTriggerPanelEditor.this, 1 ) ); + + Font font = UIManager.getFont( "Label.font" ); + font = font.deriveFont( font.getSize2D() - 2f ); + txt.setFont( font ); + } + } + } + + private void notifyListeners() + { + for ( final InputTriggerChangeListener listener : listeners ) + listener.inputTriggerChanged(); + } + + public void addInputTriggerChangeListener( final InputTriggerChangeListener listener ) + { + listeners.add( listener ); + } + + public void removeInputTriggerChangeListener( final InputTriggerChangeListener listener ) + { + listeners.remove( listener ); + } + + /** Contains the tags in the order we want them to appear in the panel. */ + private static final List< String > INPUT_TRIGGER_SYNTAX_TAGS = new ArrayList<>(); + /** Contains the tags sorted so that they can be searched by the autocomplete process. */ + private static final List< String > INPUT_TRIGGER_SYNTAX_TAGS_SORTED = new ArrayList<>(); + /** Small-caps version of INPUT_TRIGGER_SYNTAX_TAGS_SORTED. */ + private static final List< String > INPUT_TRIGGER_SYNTAX_TAGS_SMALL_CAPS; + /** Visual replacement for some tags. */ + private static final Map< String, String > TRIGGER_SYMBOLS = new HashMap<>(); + private static final Map INPUT_TRIGGER_SYNTAX_TAG_REMAP = new HashMap<>(); + + static + { + INPUT_TRIGGER_SYNTAX_TAGS.addAll( + Arrays.asList( + "all", + "ctrl", + "alt", + "altGraph", + "shift", + "meta", + "command", + "cmd", + "win", + "ENTER", + "BACK_SPACE", + "TAB", + "CANCEL", + "CLEAR", + "COMPOSE", + "PAUSE", + "CAPS_LOCK", + "ESCAPE", + "SPACE", + "PAGE_UP", + "PAGE_DOWN", + "END", + "HOME", + "BEGIN", + "COMMA", + "PERIOD", + "SLASH", + "SEMICOLON", + "EQUALS", + "OPEN_BRACKET", + "BACK_SLASH", + "CLOSE_BRACKET", + "LEFT", + "UP", + "RIGHT", + "DOWN", + "NUMPAD0", + "NUMPAD1", + "NUMPAD2", + "NUMPAD3", + "NUMPAD4", + "NUMPAD5", + "NUMPAD6", + "NUMPAD7", + "NUMPAD8", + "NUMPAD9", + "MULTIPLY", + "ADD", + "SEPARATOR", + "SUBTRACT", + "DECIMAL", + "DIVIDE", + "DELETE", + "NUM_LOCK", + "SCROLL_LOCK", + "double-click", + "button1", + "button2", + "button3", + "scroll", + "|" ) ); + for ( int i = 0; i < 26; i++ ) + INPUT_TRIGGER_SYNTAX_TAGS.add( String.valueOf( ( char ) ( 'A' + i ) ) ); + for ( int i = 0; i < 10; i++ ) + INPUT_TRIGGER_SYNTAX_TAGS.add( "" + i ); + for ( int i = 1; i <= 24; i++ ) + INPUT_TRIGGER_SYNTAX_TAGS.add( "F" + i ); + + INPUT_TRIGGER_SYNTAX_TAGS_SORTED.addAll( INPUT_TRIGGER_SYNTAX_TAGS ); + INPUT_TRIGGER_SYNTAX_TAGS_SORTED.sort( String.CASE_INSENSITIVE_ORDER ); + INPUT_TRIGGER_SYNTAX_TAGS_SMALL_CAPS = new ArrayList<>( INPUT_TRIGGER_SYNTAX_TAGS_SORTED.size() ); + for ( final String tag : INPUT_TRIGGER_SYNTAX_TAGS_SORTED ) + INPUT_TRIGGER_SYNTAX_TAGS_SMALL_CAPS.add( tag.toLowerCase() ); + + INPUT_TRIGGER_SYNTAX_TAG_REMAP.put( "cmd", "meta" ); + INPUT_TRIGGER_SYNTAX_TAG_REMAP.put( "command", "meta" ); + INPUT_TRIGGER_SYNTAX_TAG_REMAP.put( "windows", "win" ); + + TRIGGER_SYMBOLS.put( "ENTER", "\u23CE" ); + TRIGGER_SYMBOLS.put( "BACK_SPACE", "\u232B" ); + TRIGGER_SYMBOLS.put( "DELETE", "\u2326" ); + TRIGGER_SYMBOLS.put( "TAB", "\u21E5" ); + TRIGGER_SYMBOLS.put( "PAUSE", "||" ); + TRIGGER_SYMBOLS.put( "CAPS_LOCK", "\u21EA" ); + TRIGGER_SYMBOLS.put( "PAGE_UP", "\u21DE" ); + TRIGGER_SYMBOLS.put( "PAGE_DOWN", "\u21DF" ); + TRIGGER_SYMBOLS.put( "END", "\u2198" ); + TRIGGER_SYMBOLS.put( "HOME", "\u2196" ); + TRIGGER_SYMBOLS.put( "ESCAPE", "\u238b" ); + TRIGGER_SYMBOLS.put( "LEFT", "\u2190" ); + TRIGGER_SYMBOLS.put( "UP", "\u2191" ); + TRIGGER_SYMBOLS.put( "RIGHT", "\u2192" ); + TRIGGER_SYMBOLS.put( "DOWN", "\u2193" ); + TRIGGER_SYMBOLS.put( "NUMPAD0", "\u24ea" ); + TRIGGER_SYMBOLS.put( "NUMPAD1", "\u2460" ); + TRIGGER_SYMBOLS.put( "NUMPAD2", "\u2461" ); + TRIGGER_SYMBOLS.put( "NUMPAD3", "\u2462" ); + TRIGGER_SYMBOLS.put( "NUMPAD4", "\u2463" ); + TRIGGER_SYMBOLS.put( "NUMPAD5", "\u2464" ); + TRIGGER_SYMBOLS.put( "NUMPAD6", "\u2465" ); + TRIGGER_SYMBOLS.put( "NUMPAD7", "\u2466" ); + TRIGGER_SYMBOLS.put( "NUMPAD8", "\u2467" ); + TRIGGER_SYMBOLS.put( "NUMPAD9", "\u2468" ); + TRIGGER_SYMBOLS.put( "MULTIPLY", "\u00d7" ); + TRIGGER_SYMBOLS.put( "DIVIDE", "\u00f7" ); + TRIGGER_SYMBOLS.put( "ADD", "+" ); + TRIGGER_SYMBOLS.put( "SUBTRACT", "-" ); + TRIGGER_SYMBOLS.put( "COMMA", ","); + TRIGGER_SYMBOLS.put( "PERIOD", "."); + TRIGGER_SYMBOLS.put( "SLASH", "/" ); + TRIGGER_SYMBOLS.put( "SEMICOLON", ";" ); + TRIGGER_SYMBOLS.put( "EQUALS", "="); + TRIGGER_SYMBOLS.put( "OPEN_BRACKET", "["); + TRIGGER_SYMBOLS.put( "BACK_SLASH", "\\"); + TRIGGER_SYMBOLS.put( "CLOSE_BRACKET", "]"); + TRIGGER_SYMBOLS.put( "ctrl", "\u2303" ); + TRIGGER_SYMBOLS.put( "alt", "\u2387" ); + TRIGGER_SYMBOLS.put( "shift", "\u21e7" ); + TRIGGER_SYMBOLS.put( "meta", isMac() ? "\u2318" : "\u25c6" ); + TRIGGER_SYMBOLS.put( "win", "\u2756" ); + // Vertical bar is special + TRIGGER_SYMBOLS.put( "|", " | " ); + } + + /** + * Sort tokens in a visually pleasing way. Makes sure we do not mess with + * the '|' syntax. + */ + private static final void sortTokens( final String[] tokens ) + { + int vbarIndex = -1; + for ( int i = 0; i < tokens.length; i++ ) + { + if ( tokens[ i ].equals( "|" ) ) + { + vbarIndex = i; + break; + } + } + if ( vbarIndex >= 0 ) + { + Arrays.sort( tokens, 0, vbarIndex, Comparator.comparingInt( INPUT_TRIGGER_SYNTAX_TAGS::indexOf ) ); + Arrays.sort( tokens, vbarIndex + 1, tokens.length, Comparator.comparingInt( INPUT_TRIGGER_SYNTAX_TAGS::indexOf ) ); + } + else + Arrays.sort( tokens, Comparator.comparingInt( INPUT_TRIGGER_SYNTAX_TAGS::indexOf ) ); + } + + private static boolean isMac() + { + final String OS = System.getProperty( "os.name", "generic" ).toLowerCase( Locale.ENGLISH ); + return ( OS.indexOf( "mac" ) >= 0 ) || ( OS.indexOf( "darwin" ) >= 0 ); + } +} diff --git a/src/main/java/org/scijava/ui/behaviour/io/gui/RoundBorder.java b/src/main/java/org/scijava/ui/behaviour/io/gui/RoundBorder.java new file mode 100644 index 0000000..f613871 --- /dev/null +++ b/src/main/java/org/scijava/ui/behaviour/io/gui/RoundBorder.java @@ -0,0 +1,83 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.ui.behaviour.io.gui; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Graphics; +import java.awt.Graphics2D; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.geom.Area; +import java.awt.geom.RoundRectangle2D; + +import javax.swing.border.LineBorder; + +public class RoundBorder extends LineBorder +{ + + private static final long serialVersionUID = 1L; + private final Component parent; + + public RoundBorder( final Color color, final Component parent, final int arc ) + { + super( color, arc ); + this.parent = parent; + } + + @Override + public void paintBorder( final Component c, final Graphics g, final int x, final int y, final int width, final int height ) + { + if ( ( this.thickness > 0 ) && ( g instanceof Graphics2D ) ) + { + final Graphics2D g2d = ( Graphics2D ) g; + final Object oldRenderingHint = g2d.getRenderingHint( RenderingHints.KEY_ANTIALIASING ); + final Color oldColor = g2d.getColor(); + g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON ); + + final float arc = 4f * thickness; + final Shape inner = new RoundRectangle2D.Float( x, y, width - 1, height - 1, (int) arc, (int) arc ); + final Shape outer = new Rectangle( x, y, width, height ); + + g2d.setColor( parent.getBackground() ); + final Area area = new Area(outer); + area.exclusiveOr( new Area(inner) ); + g2d.fill( area ); + + g2d.setColor( this.lineColor ); + g2d.draw( inner ); + + g2d.setColor( oldColor ); + g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, oldRenderingHint ); + } + } + +} diff --git a/src/main/java/org/scijava/ui/behaviour/io/gui/TagPanelEditor.java b/src/main/java/org/scijava/ui/behaviour/io/gui/TagPanelEditor.java new file mode 100644 index 0000000..616698e --- /dev/null +++ b/src/main/java/org/scijava/ui/behaviour/io/gui/TagPanelEditor.java @@ -0,0 +1,434 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.ui.behaviour.io.gui; + +import java.awt.Color; +import java.awt.Dimension; +import java.awt.Font; +import java.awt.KeyboardFocusManager; +import java.awt.event.ActionEvent; +import java.awt.event.KeyAdapter; +import java.awt.event.KeyEvent; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +import javax.swing.AbstractAction; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.KeyStroke; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.border.EmptyBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.text.BadLocationException; + +public class TagPanelEditor extends JPanel +{ + + @FunctionalInterface + public static interface TagSelectionChangeListener + { + public void tagSelectionChanged(); + } + + private static final long serialVersionUID = 1L; + + private static final String COMMIT_ACTION = "commit"; + + protected final List< String > selectedTags; + + protected final List< TagPanel > tagPanels; + + protected final List< String > tags; + + private final JTextField textField; + + private final boolean editable; + + private final HashSet< TagSelectionChangeListener > listeners; + + private final Map< String, String > printables; + + public TagPanelEditor( final Collection< String > tags ) + { + this( tags, true ); + } + + public TagPanelEditor( final Collection< String > tags, final boolean editable ) + { + this( tags, editable, Collections.emptyMap() ); + } + + public TagPanelEditor( final Collection< String > tags, final boolean editable, final Map< String, String > printables ) + { + this.editable = editable; + this.printables = printables; + this.tags = new ArrayList<>( tags ); + this.tags.sort( null ); + this.selectedTags = new ArrayList<>(); + this.tagPanels = new ArrayList<>(); + this.listeners = new HashSet<>(); + + setPreferredSize( new Dimension( 400, 26 ) ); + setMinimumSize( new Dimension( 26, 26 ) ); + setLayout( new BoxLayout( this, BoxLayout.LINE_AXIS ) ); + + this.textField = new JTextField(); + textField.setColumns( 10 ); + textField.setBorder( new EmptyBorder( 0, 0, 0, 0 ) ); + textField.setOpaque( false ); + textField.setEditable( editable ); + + if ( editable ) + { + final Autocomplete autoComplete = new Autocomplete(); + textField.getDocument().addDocumentListener( autoComplete ); + textField.getInputMap().put( KeyStroke.getKeyStroke( KeyEvent.VK_ENTER, 0 ), COMMIT_ACTION ); + textField.setFocusTraversalKeys( KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, Collections.emptySet() ); + textField.getInputMap().put( KeyStroke.getKeyStroke( KeyEvent.VK_TAB, 0 ), COMMIT_ACTION ); + textField.getActionMap().put( COMMIT_ACTION, autoComplete.new CommitAction() ); + textField.addKeyListener( new KeyAdapter() + { + @Override + public void keyPressed( final KeyEvent e ) + { + /* + * We have to use a key listener to deal separately with + * character removal and tag removal. + */ + if ( e.getKeyCode() == KeyEvent.VK_BACK_SPACE && textField.getText().isEmpty() && !selectedTags.isEmpty() ) + { + removeTag( selectedTags.get( selectedTags.size() - 1 ) ); + e.consume(); + } + } + } ); + } + + add( textField ); + add( Box.createHorizontalGlue() ); + } + + @Override + public void updateUI() + { + super.updateUI(); + setBorder( UIManager.getBorder( "TextField.border" ) ); + setBackground( UIManager.getColor( "TextField.background" ) ); + } + + public List< String > getSelectedTags() + { + return Collections.unmodifiableList( selectedTags ); + } + + public void setTags( final Collection< String > tags ) + { + for ( final TagPanel tagPanel : tagPanels ) + remove( tagPanel ); + selectedTags.clear(); + tagPanels.clear(); + + for ( final String tag : tags ) + addTag( tag ); + revalidate(); + repaint(); + } + + public void setAcceptableTags( final Collection< String > acceptableTags ) + { + tags.clear(); + tags.addAll( acceptableTags ); + tags.sort( null ); + repaint(); + } + + protected void addTag( final String tag ) + { + final TagPanel tagp = new TagPanel( tag, this.tags.contains( tag ) ); + selectedTags.add( tag ); + tagPanels.add( tagp ); + add( tagp, getComponentCount() - 2 ); + } + + private void removeTag( final String tag ) + { + final int index = selectedTags.indexOf( tag ); + if ( index < 0 ) + return; + + selectedTags.remove( index ); + final TagPanel tagPanel = tagPanels.remove( index ); + notifyListeners(); + remove( tagPanel ); + revalidate(); + repaint(); + } + + /* + * INNER CLASSES + */ + + /** + * Adapted from + * http://stackabuse.com/example-adding-autocomplete-to-jtextfield/ + */ + private class Autocomplete implements DocumentListener + { + + @Override + public void changedUpdate( final DocumentEvent ev ) + {} + + @Override + public void removeUpdate( final DocumentEvent ev ) + {} + + @Override + public void insertUpdate( final DocumentEvent ev ) + { + if ( ev.getLength() != 1 ) + return; + + final int pos = ev.getOffset(); + String content = null; + try + { + content = textField.getText( 0, pos + 1 ); + } + catch ( final BadLocationException e ) + { + e.printStackTrace(); + } + + // Find where the word starts + int w; + for ( w = pos; w >= 0; w-- ) + { + if ( !Character.isLetter( content.charAt( w ) ) ) + { + break; + } + } + + // Too few chars + if ( pos - w < 2 ) + return; + + final String prefix = content.substring( w + 1 ).toLowerCase(); + final int n = Collections.binarySearch( tags, prefix ); + if ( n < 0 && -n <= tags.size() ) + { + final String match = tags.get( -n - 1 ); + if ( match.startsWith( prefix ) ) + { + // A completion is found + final String completion = match.substring( pos - w ); + // We cannot modify Document from within notification, + // so we submit a task that does the change later + SwingUtilities.invokeLater( new CompletionTask( completion, pos + 1 ) ); + } + } + } + + public class CommitAction extends AbstractAction + { + private static final long serialVersionUID = 5794543109646743416L; + + @Override + public void actionPerformed( final ActionEvent ev ) + { + final String tag = textField.getText(); + + if ( tag.isEmpty() ) + { + KeyboardFocusManager.getCurrentKeyboardFocusManager().focusNextComponent(); + return; + } + + if ( selectedTags.contains( tag ) ) + { + // Do not allow more than 1 tag instance. + textField.setText( "" ); + return; + } + + addTag( tag ); + textField.setText( "" ); + notifyListeners(); + revalidate(); + repaint(); + } + } + + private class CompletionTask implements Runnable + { + private final String completion; + + private final int position; + + CompletionTask( final String completion, final int position ) + { + this.completion = completion; + this.position = position; + } + + @Override + public void run() + { + final StringBuffer sb = new StringBuffer( textField.getText() ); + sb.insert( position, completion ); + textField.setText( sb.toString() ); + textField.setCaretPosition( position + completion.length() ); + textField.moveCaretPosition( position ); + } + } + + } + + final class TagPanel extends JPanel + { + private static final long serialVersionUID = 1L; + + private final JPanel content; + + private final boolean valid; + + private final JLabel txt; + + private JLabel close; + + public TagPanel( final String tag, final boolean valid ) + { + this.valid = valid; + + content = new JPanel(); + content.setLayout( new BoxLayout( content, BoxLayout.LINE_AXIS ) ); + content.setOpaque( true ); + + if ( editable ) + { + close = new JLabel( "\u00D7" ); + close.setOpaque( false ); + close.addMouseListener( new java.awt.event.MouseAdapter() + { + @Override + public void mousePressed( final java.awt.event.MouseEvent evt ) + { + removeTag( tag ); + } + } ); + content.add( close ); + content.add( createHorizontalStrutWithMaxHeight1( 2 ) ); + } + + final String str = printables.containsKey( tag ) ? printables.get( tag ) : tag; + txt = new JLabel( str ); + txt.setOpaque( false ); + content.add( txt ); + updateTxtLook(); + + setLayout( new BoxLayout( this, BoxLayout.LINE_AXIS ) ); + add( Box.createHorizontalStrut( 1 ) ); + add( content ); + add( Box.createHorizontalStrut( 4 ) ); + setOpaque( false ); + } + + @Override + public void updateUI() + { + super.updateUI(); + updateTxtLook(); + } + + private void updateTxtLook() + { + if ( content != null ) + { + final Color tfg = UIManager.getColor( "TextField.foreground" ); + final Color tbg = UIManager.getColor( "TextField.background" ); + final Color bg = valid ? mix( tbg, tfg, 0.95 ) : mix( tbg, Color.red, 0.5 ); + final Color borderColor = mix( bg, tfg, 0.8 ); + content.setBackground( bg ); + content.setBorder( new RoundBorder( borderColor, TagPanelEditor.this, 1 ) ); + + Font font = UIManager.getFont( "Label.font" ); + font = font.deriveFont( font.getSize2D() - 2f ); + txt.setFont( font ); + if ( close != null ) + close.setFont( font ); + } + } + } + + private static Box.Filler createHorizontalStrutWithMaxHeight1( final int width ) + { + return new Box.Filler( new Dimension( width, 0 ), new Dimension( width, 0 ), + new Dimension( width, 1 ) ); + } + + + private void notifyListeners() + { + for ( final TagSelectionChangeListener listener : listeners ) + listener.tagSelectionChanged(); + } + + public void addTagSelectionChangeListener( final TagSelectionChangeListener listener ) + { + listeners.add( listener ); + } + + public void removeTagSelectionChangeListener( final TagSelectionChangeListener listener ) + { + listeners.remove( listener ); + } + + /** + * Mix colors {@code c1} and {@code c2} by ratios {@code c1Weight} and {@code (1-c1Weight)}, respectively. + */ + static Color mix( final Color c1, final Color c2, final double c1Weight ) + { + final double c2Weight = 1.0 - c1Weight; + return new Color( + ( int ) ( c1.getRed() * c1Weight + c2.getRed() * c2Weight ), + ( int ) ( c1.getGreen() * c1Weight + c2.getGreen() * c2Weight ), + ( int ) ( c1.getBlue() * c1Weight + c2.getBlue() * c2Weight ) ); + } +} diff --git a/src/main/java/org/scijava/ui/behaviour/io/gui/VisualEditorPanel.java b/src/main/java/org/scijava/ui/behaviour/io/gui/VisualEditorPanel.java new file mode 100644 index 0000000..7f56adb --- /dev/null +++ b/src/main/java/org/scijava/ui/behaviour/io/gui/VisualEditorPanel.java @@ -0,0 +1,1277 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.ui.behaviour.io.gui; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.FlowLayout; +import java.awt.Font; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.KeyboardFocusManager; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.swing.Box; +import javax.swing.BoxLayout; +import javax.swing.JButton; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JTextArea; +import javax.swing.JTextField; +import javax.swing.ListSelectionModel; +import javax.swing.RowFilter; +import javax.swing.ScrollPaneConstants; +import javax.swing.border.EmptyBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.filechooser.FileFilter; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableRowSorter; +import org.scijava.listeners.Listeners; +import org.scijava.ui.behaviour.InputTrigger; +import org.scijava.ui.behaviour.io.InputTriggerConfig; +import org.scijava.ui.behaviour.io.InputTriggerDescription; +import org.scijava.ui.behaviour.io.InputTriggerDescriptionsBuilder; + +public class VisualEditorPanel extends JPanel +{ + + private static final long serialVersionUID = 1L; + + static JFileChooser fileChooser = new JFileChooser(); + + static + { + fileChooser.setFileFilter( new FileFilter() + { + + @Override + public String getDescription() + { + return "CSV files"; + } + + @Override + public boolean accept( final File f ) + { + return f.isFile() && f.getName().toLowerCase().endsWith( ".csv" ); + } + } ); + } + + /** + * Interface for listeners notified when settings are changed in the visual + * editor. + */ + @FunctionalInterface + public interface ConfigChangeListener + { + /** + * Called when settings are changed in the visual editor. + */ + void configChanged(); + } + + private JTextField textFieldFilter; + + private MyTableModel tableModel; + + private TableRowSorter< MyTableModel > tableRowSorter; + + private boolean blockRemoveNotMapped = false; + + private final InputTriggerPanelEditor keybindingEditor; + + private final TagPanelEditor contextsEditor; + + private final JLabel labelCommandName; + + private final JTable tableBindings; + + private final InputTriggerConfig config; + + private final Set< Command > commands; + + private final Map< String, Set< String > > commandNameToAcceptableContexts; + + private final Map< Command, String > actionDescriptions; + + private final JLabel lblConflict; + + private final JTextArea textAreaDescription; + + private final JPanel panelEditor; + + private final JPanel panelButtons; + + /** + * Set of listeners that are triggered whenever any single item change in the GUI. + * This, however, does not mean that the underlying {@link InputTriggerConfig} was + * changed at all as the GUI is buffering changes until the "Apply" button is pressed. + */ + private final Listeners.List< ConfigChangeListener > modelChangedListeners; + + /** + * Set of listeners that are triggered only when the "Apply" button is pressed, + * which is precisely the moment when the current state/content of GUI is committed + * to the underlying {@link InputTriggerConfig} via the ModelToConfig(). + */ + private final Listeners.List< ConfigChangeListener > configCommittedListeners; + + private final JButton btnApply; + + private final JButton btnRestore; + + /** + * Creates a visual editor for an {@link InputTriggerConfig}. The config + * object is directly modified when the user clicks the 'Apply' button. + * + * @param config + * the {@link InputTriggerConfig} object to modify. + */ + public VisualEditorPanel( final InputTriggerConfig config ) + { + this( config, extractEmptyCommandDescriptions( config ) ); + } + + /** + * Creates a visual editor for an {@link InputTriggerConfig}. The config + * object is directly modified when the user clicks the 'Apply' button. + * + * @param config + * the {@link InputTriggerConfig} object to modify. + * @param commandDescriptions + * The commands available. They are specified as a map from + * command to description. Use null as value to not + * specify a description. + * @see CommandDescriptionsBuilder + */ + public VisualEditorPanel( final InputTriggerConfig config, final Map< Command, String > commandDescriptions ) + { + + this.config = config; + this.actionDescriptions = commandDescriptions; + this.commands = commandDescriptions.keySet(); + commandNameToAcceptableContexts = new HashMap<>(); + for ( final Command command : commands ) + commandNameToAcceptableContexts.computeIfAbsent( command.getName(), k -> new HashSet<>() ).add( command.getContext() ); + this.modelChangedListeners = new Listeners.SynchronizedList<>(); + this.configCommittedListeners = new Listeners.SynchronizedList<>(); + + /* + * GUI + */ + + setLayout( new BorderLayout( 0, 0 ) ); + + final JPanel panelFilter = new JPanel(); + add( panelFilter, BorderLayout.NORTH ); + panelFilter.setLayout( new BoxLayout( panelFilter, BoxLayout.X_AXIS ) ); + + final Component horizontalStrut = Box.createHorizontalStrut( 5 ); + panelFilter.add( horizontalStrut ); + + final JLabel lblFilter = new JLabel( "Filter:" ); + lblFilter.setToolTipText( "Filter on command names. Accept regular expressions." ); + lblFilter.setAlignmentX( Component.CENTER_ALIGNMENT ); + panelFilter.add( lblFilter ); + + final Component horizontalStrut_1 = Box.createHorizontalStrut( 5 ); + panelFilter.add( horizontalStrut_1 ); + + textFieldFilter = new JTextField(); + panelFilter.add( textFieldFilter ); + textFieldFilter.setColumns( 10 ); + textFieldFilter.getDocument().addDocumentListener( new DocumentListener() + { + + @Override + public void removeUpdate( final DocumentEvent e ) + { + filterRows(); + } + + @Override + public void insertUpdate( final DocumentEvent e ) + { + filterRows(); + } + + @Override + public void changedUpdate( final DocumentEvent e ) + { + filterRows(); + } + } ); + + panelEditor = new JPanel(); + add( panelEditor, BorderLayout.SOUTH ); + panelEditor.setLayout( new BorderLayout( 0, 0 ) ); + + final JPanel panelCommandButtons = new JPanel(); + panelEditor.add( panelCommandButtons, BorderLayout.NORTH ); + panelCommandButtons.setLayout( new BoxLayout( panelCommandButtons, BoxLayout.X_AXIS ) ); + + final JButton btnCopyCommand = new JButton( "Copy" ); + btnCopyCommand.setToolTipText( "Duplicate command binding to a new, blank binding." ); + panelCommandButtons.add( btnCopyCommand ); + + final JButton btnUnbindAction = new JButton( "Unbind" ); + btnUnbindAction.setToolTipText( "Remove current binding for selected command." ); + panelCommandButtons.add( btnUnbindAction ); + + final JButton btnDeleteAction = new JButton( "Unbind all" ); + btnDeleteAction.setToolTipText( "Remove all bindings to selected command." ); + panelCommandButtons.add( btnDeleteAction ); + + final Component horizontalGlue = Box.createHorizontalGlue(); + panelCommandButtons.add( horizontalGlue ); + + final JButton btnExportCsv = new JButton( "Export CSV" ); + btnExportCsv.setToolTipText( "Export all command bindings to a CSV file." ); + panelCommandButtons.add( btnExportCsv ); + + final JPanel panelCommandEditor = new JPanel(); + panelEditor.add( panelCommandEditor, BorderLayout.CENTER ); + final GridBagLayout gbl_panelCommandEditor = new GridBagLayout(); + gbl_panelCommandEditor.rowHeights = new int[] { 0, 0, 0, 0, 60 }; + gbl_panelCommandEditor.columnWidths = new int[] { 30, 100 }; + gbl_panelCommandEditor.columnWeights = new double[] { 0.0, 1.0 }; + gbl_panelCommandEditor.rowWeights = new double[] { 0.0, 0.0, 0.0, 0.0, 0.0 }; + panelCommandEditor.setLayout( gbl_panelCommandEditor ); + + final JLabel lblName = new JLabel( "Name:" ); + final GridBagConstraints gbc_lblName = new GridBagConstraints(); + gbc_lblName.insets = new Insets( 5, 5, 5, 5 ); + gbc_lblName.anchor = GridBagConstraints.WEST; + gbc_lblName.gridx = 0; + gbc_lblName.gridy = 0; + panelCommandEditor.add( lblName, gbc_lblName ); + + this.labelCommandName = new JLabel(); + final GridBagConstraints gbc_labelActionName = new GridBagConstraints(); + gbc_labelActionName.anchor = GridBagConstraints.WEST; + gbc_labelActionName.insets = new Insets( 5, 5, 5, 0 ); + gbc_labelActionName.gridx = 1; + gbc_labelActionName.gridy = 0; + panelCommandEditor.add( labelCommandName, gbc_labelActionName ); + + final JLabel lblBinding = new JLabel( "Binding:" ); + final GridBagConstraints gbc_lblBinding = new GridBagConstraints(); + gbc_lblBinding.anchor = GridBagConstraints.WEST; + gbc_lblBinding.insets = new Insets( 5, 5, 5, 5 ); + gbc_lblBinding.gridx = 0; + gbc_lblBinding.gridy = 1; + panelCommandEditor.add( lblBinding, gbc_lblBinding ); + + this.keybindingEditor = new InputTriggerPanelEditor( true ); + final GridBagConstraints gbc_textFieldBinding = new GridBagConstraints(); + gbc_textFieldBinding.insets = new Insets( 5, 5, 5, 5 ); + gbc_textFieldBinding.fill = GridBagConstraints.HORIZONTAL; + gbc_textFieldBinding.gridx = 1; + gbc_textFieldBinding.gridy = 1; + panelCommandEditor.add( keybindingEditor, gbc_textFieldBinding ); + + final JLabel lblContext = new JLabel( "Contexts:" ); + final GridBagConstraints gbc_lblContext = new GridBagConstraints(); + gbc_lblContext.anchor = GridBagConstraints.WEST; + gbc_lblContext.insets = new Insets( 5, 5, 5, 5 ); + gbc_lblContext.gridx = 0; + gbc_lblContext.gridy = 2; + panelCommandEditor.add( lblContext, gbc_lblContext ); + + this.contextsEditor = new TagPanelEditor( Collections.emptyList() ); + final GridBagConstraints gbc_comboBoxContext = new GridBagConstraints(); + gbc_comboBoxContext.insets = new Insets( 5, 5, 5, 5 ); + gbc_comboBoxContext.fill = GridBagConstraints.BOTH; + gbc_comboBoxContext.gridx = 1; + gbc_comboBoxContext.gridy = 2; + panelCommandEditor.add( contextsEditor, gbc_comboBoxContext ); + + final JLabel lblConflicts = new JLabel( "Conflicts:" ); + final GridBagConstraints gbc_lblConflicts = new GridBagConstraints(); + gbc_lblConflicts.insets = new Insets( 5, 5, 5, 5 ); + gbc_lblConflicts.anchor = GridBagConstraints.WEST; + gbc_lblConflicts.gridx = 0; + gbc_lblConflicts.gridy = 3; + panelCommandEditor.add( lblConflicts, gbc_lblConflicts ); + + lblConflict = new JLabel( "" ); + lblConflict.setToolTipText( "Conflicts with other commands." ); + lblConflict.setForeground( Color.PINK.darker() ); + lblConflict.setFont( getFont().deriveFont( Font.BOLD ) ); + final GridBagConstraints gbc_lblConflict = new GridBagConstraints(); + gbc_lblConflict.insets = new Insets( 5, 5, 5, 0 ); + gbc_lblConflict.anchor = GridBagConstraints.WEST; + gbc_lblConflict.gridx = 1; + gbc_lblConflict.gridy = 3; + panelCommandEditor.add( lblConflict, gbc_lblConflict ); + + final JLabel lblDescription = new JLabel( "Description:" ); + final GridBagConstraints gbc_lblDescription = new GridBagConstraints(); + gbc_lblDescription.insets = new Insets( 5, 5, 5, 5 ); + gbc_lblDescription.anchor = GridBagConstraints.NORTHWEST; + gbc_lblDescription.gridx = 0; + gbc_lblDescription.gridy = 4; + panelCommandEditor.add( lblDescription, gbc_lblDescription ); + + final JScrollPane scrollPaneDescription = new JScrollPane(); + scrollPaneDescription.setOpaque( false ); + scrollPaneDescription.setHorizontalScrollBarPolicy( ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER ); + final GridBagConstraints gbc_scrollPaneDescription = new GridBagConstraints(); + gbc_scrollPaneDescription.insets = new Insets( 5, 5, 5, 5 ); + gbc_scrollPaneDescription.fill = GridBagConstraints.BOTH; + gbc_scrollPaneDescription.gridx = 1; + gbc_scrollPaneDescription.gridy = 4; + panelCommandEditor.add( scrollPaneDescription, gbc_scrollPaneDescription ); + + textAreaDescription = new JTextArea(); + textAreaDescription.setRows( 3 ); + textAreaDescription.setFont( getFont().deriveFont( getFont().getSize2D() - 1f ) ); + textAreaDescription.setOpaque( false ); + textAreaDescription.setWrapStyleWord( true ); + textAreaDescription.setEditable( false ); + textAreaDescription.setLineWrap( true ); + textAreaDescription.setFocusable( false ); + scrollPaneDescription.setViewportView( textAreaDescription ); + + panelButtons = new JPanel(); + panelEditor.add( panelButtons, BorderLayout.SOUTH ); + final FlowLayout flowLayout = ( FlowLayout ) panelButtons.getLayout(); + flowLayout.setAlignment( FlowLayout.TRAILING ); + + this.btnRestore = new JButton( "Restore" ); + btnRestore.setToolTipText( "Re-read the key bindings from the config." ); + panelButtons.add( btnRestore ); + + this.btnApply = new JButton( "Apply" ); + btnApply.setToolTipText( "Write these key bindings in the config." ); + panelButtons.add( btnApply ); + + final JScrollPane scrollPane = new JScrollPane(); + scrollPane.setVerticalScrollBarPolicy( ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS ); + scrollPane.setHorizontalScrollBarPolicy( ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER ); + add( scrollPane, BorderLayout.CENTER ); + + tableBindings = new JTable() { + @Override + public void updateUI() + { + super.updateUI(); + setRowHeight( ( int ) ( getFontMetrics( getFont() ).getHeight() * 1.5 ) ); + } + }; + tableBindings.setSelectionMode( ListSelectionModel.SINGLE_SELECTION ); + tableBindings.setFillsViewportHeight( true ); + tableBindings.setAutoResizeMode( JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS ); + tableBindings.getSelectionModel().addListSelectionListener( new ListSelectionListener() + { + @Override + public void valueChanged( final ListSelectionEvent e ) + { + if ( e.getValueIsAdjusting() ) + return; + updateEditors(); + } + } ); + tableBindings.getSelectionModel().addListSelectionListener( new ListSelectionListener() + { + @Override + public void valueChanged( final ListSelectionEvent e ) + { + if ( e.getValueIsAdjusting() ) + return; + + if ( blockRemoveNotMapped ) + { + blockRemoveNotMapped = false; + return; + } + + final int selIndex = tableBindings.getSelectionModel().getMinSelectionIndex(); + if ( selIndex < 0 ) + return; + final MyTableRow selectedRowToRestore = tableModel.rows.get( selIndex ); + if ( !tableModel.removeSuperfluousNotMapped() ) + return; + + final int bs = Collections.binarySearch( tableModel.rows, selectedRowToRestore, MyTableRowComparator ); + if ( bs < 0 ) + return; + final int vbs = tableBindings.convertRowIndexToView( bs ); + tableBindings.getSelectionModel().setSelectionInterval( vbs, vbs ); + } + } ); + tableBindings.setFocusTraversalKeys( KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, null ); + tableBindings.setFocusTraversalKeys( KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, null ); + + // Listen to changes in the keybinding editor and forward to table + // model. + keybindingEditor.addInputTriggerChangeListener( () -> keybindingsChanged( + keybindingEditor.getInputTrigger() == null + ? keybindingEditor.getLastValidInputTrigger() + : keybindingEditor.getInputTrigger() ) ); + + // Listen to changes in context editor and forward to table model. + contextsEditor.addTagSelectionChangeListener( () -> contextsChanged( contextsEditor.getSelectedTags() ) ); + + // Button presses. + btnCopyCommand.addActionListener( ( e ) -> copyCommand() ); + btnUnbindAction.addActionListener( ( e ) -> unbindCommand() ); + btnDeleteAction.addActionListener( ( e ) -> unbindAllCommand() ); + btnExportCsv.addActionListener( ( e ) -> exportToCsv() ); + btnRestore.addActionListener( ( e ) -> configToModel() ); + btnApply.addActionListener( ( e ) -> modelToConfig() ); + + // Buttons re-enabling when model and config are out of sync. + modelChangedListeners.add( () -> { + btnApply.setEnabled( true ); + btnRestore.setEnabled( true ); + } ); + + configToModel(); + tableBindings.getRowSorter().toggleSortOrder( 0 ); + if ( tableBindings.getRowCount() > 0 ) + tableBindings.getSelectionModel().setSelectionInterval( 0, 0); + + scrollPane.setViewportView( tableBindings ); + } + + private void lookForConflicts() + { + lblConflict.setText( "" ); + + final int viewRow = tableBindings.getSelectedRow(); + if ( viewRow < 0 ) + return; + final int modelRow = tableBindings.convertRowIndexToModel( viewRow ); + + lblConflict.setText( "" ); + final InputTrigger inputTrigger = tableModel.rows.get( modelRow ).getTrigger(); + if ( inputTrigger == InputTrigger.NOT_MAPPED ) + return; + final List< String > contexts = tableModel.rows.get( modelRow ).getContexts(); + + final ArrayList< String > conflicts = new ArrayList<>(); + for ( int i = 0; i < tableModel.getRowCount(); i++ ) + { + if ( i == modelRow ) + continue; + + if ( tableModel.rows.get( i ).getTrigger().equals( inputTrigger ) ) + { + // Same trigger. Check if contexts overlap. + final List< String > overlappingContexts = new ArrayList<>( tableModel.rows.get( i ).getContexts() ); + overlappingContexts.retainAll( contexts ); + if ( !overlappingContexts.isEmpty() ) + { + final StringBuilder str = new StringBuilder(); + str.append( tableModel.rows.get( i ).getName() ); + str.append( " in " ).append( overlappingContexts.get( 0 ) ); + for ( int j = 1; j < overlappingContexts.size(); j++ ) + str.append( ", " ).append( overlappingContexts.get( j ) ); + + conflicts.add( str.toString() ); + } + } + } + + if ( !conflicts.isEmpty() ) + { + final StringBuilder str = new StringBuilder( conflicts.get( 0 ) ); + for ( int i = 1; i < conflicts.size(); i++ ) + str.append( "; " ).append( conflicts.get( i ) ); + lblConflict.setText( str.toString() ); + } + } + + public void setButtonPanelVisible( final boolean visible ) + { + panelEditor.remove( panelButtons ); + if ( visible ) + panelEditor.add( panelButtons, BorderLayout.SOUTH ); + } + + // TODO Change method name to 'apply()'. API breaking change. + /** + * Copies the settings in this editor to the {@link InputTriggerConfig} + * specified at construction. The {@link InputTriggerConfig} is cleared + * before copying. + */ + public void modelToConfig() + { + config.clear(); + for ( final MyTableRow row : tableModel.rows ) + { + final InputTrigger inputTrigger = row.getTrigger(); + if ( inputTrigger == InputTrigger.NOT_MAPPED ) + continue; + + final String action = row.getName(); + config.add( inputTrigger, action, row.getContexts() ); + } + + // fill in InputTrigger.NOT_MAPPED for any action that doesn't have any input + for ( final Command command : commands ) + { + final String action = command.getName(); + if ( config.getInputs( action, command.getContext() ).isEmpty() ) + config.add( InputTrigger.NOT_MAPPED, action, command.getContext() ); + } + + btnApply.setEnabled( false ); + btnRestore.setEnabled( false ); + + configCommittedListeners.list.forEach( ConfigChangeListener::configChanged ); + } + + public void configToModel() + { + tableModel = new MyTableModel( commands, config ); + tableBindings.setModel( tableModel ); + + tableRowSorter = new TableRowSorter<>( tableModel ); + tableRowSorter.setComparator( 1, InputTriggerComparator ); + tableBindings.setRowSorter( tableRowSorter ); + filterRows(); + + // Renderers. + tableBindings.getColumnModel().getColumn( 1 ).setCellRenderer( new MyBindingsRenderer() ); + tableBindings.getColumnModel().getColumn( 2 ).setCellRenderer( new MyContextsRenderer( Collections.emptyList() ) ); + + // Notify listeners. + notifyListeners(); + + btnApply.setEnabled( false ); + btnRestore.setEnabled( false ); + } + + private void filterRows() + { + final String regex = textFieldFilter.getText(); + final Pattern pattern = Pattern.compile(regex, Pattern.CASE_INSENSITIVE ); + final Matcher matcher = pattern.matcher( "" ); + final RowFilter< MyTableModel, Integer > rf = new RowFilter< MyTableModel, Integer >() + { + + @Override + public boolean include( final Entry< ? extends MyTableModel, ? extends Integer > entry ) + { + int count = entry.getValueCount(); + while ( --count >= 0 ) + { + matcher.reset( entry.getStringValue( count ) ); + if ( matcher.find() ) { return true; } + } + return false; + } + }; + tableRowSorter.setRowFilter( rf ); + } + + private final static String CSV_SEPARATOR = ","; + + private void exportToCsv() + { + final int userSignal = fileChooser.showSaveDialog( this ); + if ( userSignal != JFileChooser.APPROVE_OPTION ) + return; + + final File file = fileChooser.getSelectedFile(); + if ( file.exists() ) + { + if ( !file.canWrite() ) + { + JOptionPane.showMessageDialog( fileChooser, "Cannot write on existing file " + file.getAbsolutePath(), "File error", JOptionPane.ERROR_MESSAGE ); + return; + } + final int doOverwrite = JOptionPane.showConfirmDialog( fileChooser, "The file already exists. Do you want to overwrite it?", "Overwrite?", JOptionPane.YES_NO_OPTION ); + if ( doOverwrite != JOptionPane.YES_OPTION ) + return; + } + + final StringBuilder sb = new StringBuilder(); + sb.append( MyTableModel.TABLE_HEADERS[ 0 ] ); + sb.append( CSV_SEPARATOR + '\t' ); + sb.append( MyTableModel.TABLE_HEADERS[ 1 ] ); + sb.append( CSV_SEPARATOR + '\t' ); + sb.append( MyTableModel.TABLE_HEADERS[ 2 ] ); + sb.append( '\n' ); + + for ( int i = 0; i < tableModel.getRowCount(); i++ ) + { + sb.append( tableModel.rows.get( i ).getName() ); + sb.append( CSV_SEPARATOR + '\t' ); + sb.append( tableModel.rows.get( i ).getTrigger().toString() ); + sb.append( CSV_SEPARATOR + '\t' ); + final List< String > contexts = tableModel.rows.get( i ).getContexts(); + if ( !contexts.isEmpty() ) + { + sb.append( contexts.get( 0 ) ); + for ( int j = 1; j < contexts.size(); j++ ) + sb.append( " - " ).append( contexts.get( j ) ); + } + sb.append( '\n' ); + } + + try ( final PrintWriter pw = new PrintWriter( file ) ) + { + pw.write( sb.toString() ); + } + catch ( final FileNotFoundException e ) + { + JOptionPane.showMessageDialog( fileChooser, "Error writing file:\n" + e.getMessage(), "Error writing file.", JOptionPane.ERROR_MESSAGE ); + e.printStackTrace(); + } + } + + private void updateEditors() + { + final int viewRow = tableBindings.getSelectedRow(); + if ( viewRow < 0 ) + { + labelCommandName.setText( "" ); + keybindingEditor.setInputTrigger( InputTrigger.NOT_MAPPED ); + contextsEditor.setTags( Collections.emptyList() ); + textAreaDescription.setText( "" ); + return; + } + + final int modelRow = tableBindings.convertRowIndexToModel( viewRow ); + final MyTableRow row = tableModel.rows.get( modelRow ); + final String action = row.getName(); + final InputTrigger trigger = row.getTrigger(); + final List< String > contexts = row.getContexts(); + + final String description; + final Set< String > acceptableContexts = commandNameToAcceptableContexts.get( action ); + if ( acceptableContexts.isEmpty() ) + { + description = ""; + } + else + { + final StringBuilder str = new StringBuilder(); + for ( final String context : acceptableContexts ) + { + final String d = actionDescriptions.get( new Command( action, context ) ); + if ( d != null ) + str.append( "\n\nIn " ).append( context ).append( ":\n" ).append( d ); + else + str.append( "\n\nIn " ).append( context ).append( " - no description." ); + } + str.delete( 0, 2 ); + description = str.toString(); + } + + labelCommandName.setText( action ); + keybindingEditor.setInputTrigger( trigger ); + contextsEditor.setAcceptableTags( acceptableContexts ); + contextsEditor.setTags( contexts ); + textAreaDescription.setText( description ); + textAreaDescription.setCaretPosition( 0 ); + + lookForConflicts(); + } + + private void unbindCommand() + { + final int viewRow = tableBindings.getSelectedRow(); + if ( viewRow < 0 ) + return; + final int modelRow = tableBindings.convertRowIndexToModel( viewRow ); + tableModel.rows.remove( modelRow ); + if ( !tableModel.addMissingRows() ) + tableModel.fireTableRowsDeleted( modelRow, modelRow ); + + // Notify listeners. + notifyListeners(); + } + + private void unbindAllCommand() + { + final int viewRow = tableBindings.getSelectedRow(); + if ( viewRow < 0 ) + return; + final int modelRow = tableBindings.convertRowIndexToModel( viewRow ); + final String removeName = tableModel.rows.get( modelRow ).getName(); + tableModel.rows.removeIf( row -> row.getName().equals( removeName ) ); + if ( !tableModel.addMissingRows() ) + tableModel.fireTableDataChanged(); + + // Notify listeners. + notifyListeners(); + } + + private void copyCommand() + { + final int viewRow = tableBindings.getSelectedRow(); + if ( viewRow < 0 ) + return; + final int modelRow = tableBindings.convertRowIndexToModel( viewRow ); + final MyTableRow row = tableModel.rows.get( modelRow ); + + final MyTableRow copiedRow = new MyTableRow( row.getName(), InputTrigger.NOT_MAPPED, row.getContexts() ); + tableModel.rows.add( modelRow + 1, copiedRow ); + blockRemoveNotMapped = true; + if ( !tableModel.mergeRows() ) + tableModel.fireTableRowsInserted( modelRow + 1, modelRow + 1 ); + + blockRemoveNotMapped = true; + // Find the row we just added if any. + final int modelRowToSelect = Collections.binarySearch( tableModel.rows, copiedRow, MyTableRowComparator ); + final int rowToSelect; + if ( modelRowToSelect < 0 ) + rowToSelect = tableBindings.convertRowIndexToView( modelRow ); + else + rowToSelect = tableBindings.convertRowIndexToView( modelRowToSelect ); + tableBindings.getSelectionModel().setSelectionInterval( rowToSelect, rowToSelect ); + + keybindingEditor.requestFocusInWindow(); + } + + private void keybindingsChanged( final InputTrigger inputTrigger ) + { + final int viewRow = tableBindings.getSelectedRow(); + if ( viewRow < 0 ) + return; + final int modelRow = tableBindings.convertRowIndexToModel( viewRow ); + final MyTableRow row = tableModel.rows.get( modelRow ); + + final MyTableRow updatedRow = new MyTableRow( row.getName(), inputTrigger, row.getContexts() ); + tableModel.rows.set( modelRow, updatedRow ); + if ( !tableModel.mergeRows() ) + tableModel.fireTableRowsUpdated( modelRow, modelRow ); + lookForConflicts(); + + final int modelRowToSelect = Collections.binarySearch( tableModel.rows, updatedRow, MyTableRowComparator ); + final int rowToSelect; + if ( modelRowToSelect < 0 ) + rowToSelect = tableBindings.convertRowIndexToView( modelRow ); + else + rowToSelect = tableBindings.convertRowIndexToView( modelRowToSelect ); + blockRemoveNotMapped = true; + tableBindings.getSelectionModel().setSelectionInterval( rowToSelect, rowToSelect ); + + // Notify listeners. + notifyListeners(); + } + + private void contextsChanged( final List< String > selectedContexts ) + { + final int viewRow = tableBindings.getSelectedRow(); + if ( viewRow < 0 ) + return; + final int modelRow = tableBindings.convertRowIndexToModel( viewRow ); + final MyTableRow row = tableModel.rows.get( modelRow ); + + final List< String > newContexts = new ArrayList<>( selectedContexts ); + newContexts.sort( null ); + tableModel.rows.set( modelRow, new MyTableRow( row.getName(), row.getTrigger(), newContexts ) ); + if ( !tableModel.addMissingRows() ) + tableModel.fireTableRowsUpdated( modelRow, modelRow ); + + // Notify listeners. + notifyListeners(); + + // Select proper row again (might have sorted differently now). + final int viewRowToSelect = tableBindings.convertRowIndexToView( modelRow ); + if ( viewRowToSelect < 0 ) + return; + tableBindings.getSelectionModel().setSelectionInterval( viewRowToSelect, viewRowToSelect ); + } + + private void notifyListeners() + { + modelChangedListeners.list.forEach( ConfigChangeListener::configChanged ); + } + + private static Map< Command, String > extractEmptyCommandDescriptions( final InputTriggerConfig keyconf ) + { + final List< InputTriggerDescription > descriptions = new InputTriggerDescriptionsBuilder( keyconf ).getDescriptions(); + final Set< Command > commands = new LinkedHashSet<>(); + for ( final InputTriggerDescription desc : descriptions ) + for( final String context : desc.getContexts() ) + commands.add( new Command( desc.getAction(), context ) ); + final Map< Command, String > commandDescriptions = new HashMap<>(); + commands.forEach( command -> commandDescriptions.put( command, null ) ); + return commandDescriptions; + } + + /** + * @deprecated Use {@code modelChangedListeners()} instead. + */ + @Deprecated + public Listeners< ConfigChangeListener > configChangeListeners() + { + return modelChangedListeners(); + } + + /** + * @deprecated Use {@code modelChangedListeners().add(listener)} instead. + */ + @Deprecated + public void addConfigChangeListener( final ConfigChangeListener listener ) + { + modelChangedListeners().add( listener ); + } + + /** + * @deprecated Use {@code modelChangedListeners().remove(listener)} instead. + */ + @Deprecated + public void removeConfigChangeListener( final ConfigChangeListener listener ) + { + modelChangedListeners().remove( listener ); + } + + /** + * Please, see the documentation of {@link VisualEditorPanel#modelChangedListeners} + * and {@link VisualEditorPanel#configCommittedListeners} to understand when these + * listeners are triggered. In short, listeners here are triggered anytime a GUI item is changed. + */ + public Listeners< ConfigChangeListener > modelChangedListeners() + { + return modelChangedListeners; + } + + /** + * Please, see the documentation of {@link VisualEditorPanel#modelChangedListeners} + * and {@link VisualEditorPanel#configCommittedListeners} to understand when these + * listeners are triggered. In short, listeners here are triggered only when "Apply" button is pressed. + */ + public Listeners< ConfigChangeListener > configCommittedListeners() + { + return configCommittedListeners; + } + + /* + * INNER CLASSES + */ + + private final class MyContextsRenderer extends TagPanelEditor implements TableCellRenderer + { + + private static final long serialVersionUID = 1L; + + public MyContextsRenderer( final Collection< String > tags ) + { + super( tags, false ); + } + + @Override + public void updateUI() + { + super.updateUI(); + setBorder( new EmptyBorder( 0, 0, 0, 0 ) ); + } + + @Override + public Component getTableCellRendererComponent( final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column ) + { + setForeground( isSelected ? table.getSelectionForeground() : table.getForeground() ); + setBackground( isSelected ? table.getSelectionBackground() : table.getBackground() ); + final int modelRow = tableBindings.convertRowIndexToModel( row ); + final String name = tableModel.rows.get( modelRow ).getName(); + setAcceptableTags( commandNameToAcceptableContexts.get( name ) ); + + @SuppressWarnings( "unchecked" ) + final List< String > contexts = value != null + ? ( List< String > ) value + : Collections.emptyList(); + if ( contexts.isEmpty() ) + setBackground( Color.PINK ); + setTags( contexts ); + setToolTipText( contexts.toString() ); + return this; + } + } + + private static final class MyBindingsRenderer extends InputTriggerPanelEditor implements TableCellRenderer + { + + private static final long serialVersionUID = 1L; + + public MyBindingsRenderer() + { + super( false ); + } + + @Override + public void updateUI() + { + super.updateUI(); + setBorder( new EmptyBorder( 0, 0, 0, 0 ) ); + } + + @Override + public Component getTableCellRendererComponent( final JTable table, final Object value, final boolean isSelected, final boolean hasFocus, final int row, final int column ) + { + setForeground( isSelected ? table.getSelectionForeground() : table.getForeground() ); + setBackground( isSelected ? table.getSelectionBackground() : table.getBackground() ); + + final InputTrigger input = ( InputTrigger ) value; + if ( null != input ) + { + setInputTrigger( input ); + final String val = input.toString(); + setToolTipText( val ); + } + else + { + setInputTrigger( InputTrigger.NOT_MAPPED ); + setToolTipText( "No binding" ); + } + return this; + } + } + + private static class MyTableRow + { + private final String name; + + private final InputTrigger trigger; + + private final List< String > contexts; + + public MyTableRow( final String name, final InputTrigger trigger, final String context ) + { + this( name, trigger, Collections.singletonList( context ) ); + } + + public MyTableRow( final String name, final InputTrigger trigger, final Collection< String > contexts ) + { + this.name = name; + this.trigger = trigger; + this.contexts = new ArrayList<>( contexts ); + } + + public String getName() + { + return name; + } + + public InputTrigger getTrigger() + { + return trigger; + } + + public List< String > getContexts() + { + return contexts; + } + + @Override + public boolean equals( final Object o ) + { + if ( this == o ) + return true; + if ( o == null || getClass() != o.getClass() ) + return false; + + final MyTableRow that = ( MyTableRow ) o; + + if ( !name.equals( that.name ) ) + return false; + if ( !trigger.equals( that.trigger ) ) + return false; + return contexts.equals( that.contexts ); + } + + @Override + public int hashCode() + { + int result = name.hashCode(); + result = 31 * result + trigger.hashCode(); + result = 31 * result + contexts.hashCode(); + return result; + } + + @Override + public String toString() + { + return "MyTableRow{" + + "name='" + name + '\'' + + ", trigger=" + trigger + + ", contexts=" + contexts + + '}'; + } + } + + private static class MyTableModel extends AbstractTableModel + { + + private static final long serialVersionUID = 1L; + + private static final String[] TABLE_HEADERS = new String[] { "Command", "Binding", "Contexts" }; + + private final List< MyTableRow > rows; + + private final Set< Command > allCommands; + + public MyTableModel( final Set< Command > commands, final InputTriggerConfig config ) + { + rows = new ArrayList<>(); + allCommands = commands; + for ( final Command command : commands ) + { + final Set< InputTrigger > inputs = config.getInputs( command.getName(), command.getContext() ); + for ( final InputTrigger input : inputs ) + rows.add( new MyTableRow( command.getName(), input, command.getContext() ) ); + } + addMissingRows(); + } + + /** + * Find and remove rows with trigger NOT_MAPPED. + */ + public void removeAllNotMapped( final List< MyTableRow > rows ) + { + rows.removeIf( row -> row.getTrigger().equals( InputTrigger.NOT_MAPPED ) ); + } + + /** + * Find and remove table rows with trigger {@code NOT_MAPPED}, whose + * name and contexts are covered by other rows (that map to other + * triggers). + * + * If any changes are made, {@code fireTableDataChanged} is fired. + * + * @return true, if changes were made. + */ + public boolean removeSuperfluousNotMapped() + { + final ArrayList< MyTableRow > copy = new ArrayList<>( rows ); + removeAllNotMapped( rows ); + addMissingRows( rows ); + if ( !copy.equals( rows ) ) + { + this.fireTableDataChanged(); + return true; + } + return false; + } + + /** + * Find and merge table rows with the same action name and trigger, but + * different contexts. + * + * If any changes are made, {@code fireTableDataChanged} is fired. + * + * @return true, if changes were made. + */ + private boolean mergeRows() + { + final ArrayList< MyTableRow > copy = new ArrayList<>( rows ); + mergeRows( rows ); + if ( !copy.equals( rows ) ) + { + this.fireTableDataChanged(); + return true; + } + return false; + } + + /** + * In the given list of {@code rows}, find and merge rows with the same + * action name and trigger, but different contexts. + * + * @param rows list of rows to modify. + */ + private void mergeRows( final List< MyTableRow > rows ) + { + final List< MyTableRow > rowsUnmerged = new ArrayList<>( rows ); + rows.clear(); + + rowsUnmerged.sort( MyTableRowComparator ); + + for ( int i = 0; i < rowsUnmerged.size(); ) + { + final MyTableRow rowA = rowsUnmerged.get( i ); + int j = i + 1; + while ( j < rowsUnmerged.size() && MyTableRowComparator.compare( rowsUnmerged.get( j ), rowA ) == 0 ) + ++j; + + final Set< String > contexts = new HashSet<>(); + for ( int k = i; k < j; ++k ) + contexts.addAll( rowsUnmerged.get( k ).getContexts() ); + + rows.add( new MyTableRow( rowA.getName(), rowA.getTrigger(), contexts ) ); + + i = j; + } + } + + /** + * Add {@code NOT_MAPPED} rows for (name, context) pairs in + * {@link #allCommands} that are not otherwise covered. Then + * {@link #mergeRows()}. + * + * If any changes are made, {@code fireTableDataChanged} is fired. + * + * @return true, if changes were made. + */ + private boolean addMissingRows() + { + final ArrayList< MyTableRow > copy = new ArrayList<>( rows ); + addMissingRows( rows ); + if ( !copy.equals( rows ) ) + { + this.fireTableDataChanged(); + return true; + } + return false; + } + + /** + * In the given list of {@code rows}, add {@code NOT_MAPPED} rows for + * (name, context) pairs in {@link #allCommands} that are not otherwise + * covered. Then {@link #mergeRows(List)}. + * + * @param rows + * list of rows to modify. + */ + private void addMissingRows( final List< MyTableRow > rows ) + { + final ArrayList< Command > missingCommands = new ArrayList<>(); + for ( final Command command : allCommands ) + { + boolean found = false; + for ( final MyTableRow row : rows ) + { + if ( row.getName().equals( command.getName() ) && row.getContexts().contains( command.getContext() ) ) + { + found = true; + break; + } + } + if ( !found ) + missingCommands.add( command ); + } + + for ( final Command command : missingCommands ) + rows.add( new MyTableRow( command.getName(), InputTrigger.NOT_MAPPED, command.getContext() ) ); + + mergeRows( rows ); + } + + @Override + public int getRowCount() + { + return rows.size(); + } + + @Override + public int getColumnCount() + { + return 3; + } + + @Override + public Object getValueAt( final int rowIndex, final int columnIndex ) + { + switch ( columnIndex ) + { + case 0: + return rows.get( rowIndex ).getName(); + case 1: + return rows.get( rowIndex ).getTrigger(); + case 2: + return rows.get( rowIndex ).getContexts(); + default: + throw new NoSuchElementException( "Cannot access column " + columnIndex + " in this model." ); + } + } + + @Override + public String getColumnName( final int column ) + { + return TABLE_HEADERS[ column ]; + } + } + + private static final Comparator< MyTableRow > MyTableRowComparator = new Comparator< MyTableRow >() + { + @Override + public int compare( final MyTableRow o1, final MyTableRow o2 ) + { + final int cn = o1.name.compareTo( o2.name ); + if ( cn != 0 ) + return cn; + + return compare( o1.trigger, o2.trigger ); + } + + private int compare( final InputTrigger o1, final InputTrigger o2 ) + { + if ( o1 == InputTrigger.NOT_MAPPED ) + return o2 == InputTrigger.NOT_MAPPED ? 0 : 1; + if ( o2 == InputTrigger.NOT_MAPPED ) + return -1; + return o1.toString().compareTo( o2.toString() ); + } + }; + + private static final Comparator< InputTrigger > InputTriggerComparator = new Comparator< InputTrigger >() + { + @Override + public int compare( final InputTrigger o1, final InputTrigger o2 ) + { + if ( o1 == InputTrigger.NOT_MAPPED ) + return 1; + if ( o2 == InputTrigger.NOT_MAPPED ) + return -1; + return o1.toString().compareTo( o2.toString() ); + } + }; +} diff --git a/src/main/java/org/scijava/ui/behaviour/io/json/JsonConfigIO.java b/src/main/java/org/scijava/ui/behaviour/io/json/JsonConfigIO.java index a82cff4..a8c3b7d 100644 --- a/src/main/java/org/scijava/ui/behaviour/io/json/JsonConfigIO.java +++ b/src/main/java/org/scijava/ui/behaviour/io/json/JsonConfigIO.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour.io.json; import java.io.FileReader; diff --git a/src/main/java/org/scijava/ui/behaviour/io/yaml/YamlConfigIO.java b/src/main/java/org/scijava/ui/behaviour/io/yaml/YamlConfigIO.java index a27400a..76fb0fb 100644 --- a/src/main/java/org/scijava/ui/behaviour/io/yaml/YamlConfigIO.java +++ b/src/main/java/org/scijava/ui/behaviour/io/yaml/YamlConfigIO.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour.io.yaml; import java.io.FileReader; @@ -10,6 +39,7 @@ import org.scijava.ui.behaviour.io.InputTriggerDescription; import org.yaml.snakeyaml.DumperOptions; +import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.TypeDescription; import org.yaml.snakeyaml.Yaml; import org.yaml.snakeyaml.constructor.Constructor; @@ -57,14 +87,17 @@ public class YamlConfigIO */ private static final Yaml getYaml() { - final Representer representer = new Representer(); + final DumperOptions options = new DumperOptions(); + options.setExplicitStart( true ); + + final Representer representer = new Representer( options ); representer.addClassTag( InputTriggerDescription.class, tag ); - final Constructor constructor = new Constructor(); + final LoaderOptions loaderOptions = new LoaderOptions(); + + final Constructor constructor = new Constructor( loaderOptions ); constructor.addTypeDescription( new TypeDescription( InputTriggerDescription.class, tag ) ); - final DumperOptions options = new DumperOptions(); - options.setExplicitStart( true ); final Yaml yaml = new Yaml( constructor, representer, options ); return yaml; diff --git a/src/main/java/org/scijava/ui/behaviour/package-info.java b/src/main/java/org/scijava/ui/behaviour/package-info.java new file mode 100644 index 0000000..139284a --- /dev/null +++ b/src/main/java/org/scijava/ui/behaviour/package-info.java @@ -0,0 +1,282 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +/** + *

Configurable-keys

Simplify making AWT mouse-handlers configurable. + * + * Works along the lines of the InputMap / ActionMap mechanism. The syntax for + * defining key and mouse "triggers" is described in the InputTrigger-syntax + * wiki page InputTrigger-syntax + * wiki page. They are also repeated below. + * + *

Overview

+ * + * ui-behaviour is a library for binding behaviours by keys or mouse-actions. + * The idea is similar to Swing's {@code InputMap/ActionMap} framework. The + * difference is that actions are atomic while behaviours (possibly) stretch + * over a period of time. For example, a {@code DragBehaviour} is initialized + * (e.g., with a mouse click) at certain coordinates, goes through a series of + * coordinate updates, and then ends (when the mouse is released). + *

+ * The combination of modifiers, keys, mouse buttons etc. that initiates a + * behaviour is called a "trigger" and is constructed from a string description. + *

+ * The basic syntax for a trigger description is a sequence of modifier and key + * names separated by whitespace. Examples are "{@code SPACE}", + * "{@code button1}", "{@code shift alt scroll}", and "{@code ctrl F G}". + *

+ * Additionally, one can specify a combination of modifiers, keys, mouse buttons + * etc. that should be ignored when triggering the behaviour. This is a another + * sequence of modifier and key names separated from the trigger description by + * "{@code |}". For example, "{@code ctrl button1 | shift alt}" is triggered by + * pressing the left mouse-button while holding the ctrl key. If the shift + * and/or alt key are pressed simultaneously, the trigger still matches. To + * ignore all other modifiers and keys, the special name "{@code all}" is used. + * So, "{@code A | all}" is a trigger that matches when the A key is pressed, + * regardless which other modifiers, keys, or buttons are active at the same + * time. + * + * + *

Chaining principle

+ * + * In Swing, a {@code JComponent} has an {@code InputMap}. Each InputMap can + * have a parent, and if a mapping for a given key is not found in the InputMap, + * it asks the parent. + *

+ * For BigDataViewer, we thought that this concept is nice for adding + * InputMap/ActionMap pairs with related actions to the viewer. For example one + * map with navigation shortcuts, then one map for bookmarking, and then each + * user extension could also create its own InputMap/ActionMap pair and just + * chain it to the existing maps. + *

+ * The slight complication with this is, that if you have a situation like this: + * + *

+ * component → map2 → map1
+ * 
+ * + * and you want to add another one, "map3", it should look like this: + * + *
+ * component → map3 → map2 → map1
+ * 
+ * + * So, "map3" should be inserted between "component" and "map2". + *

+ * This is resolved in ui-behaviours {@code InputActionBindings} by having a + * {@code theInputMap/theActionMap} pair that acts as an empty leaf map with + * exchangeable parents. From the component side it always looks like + * + *

+ * component → theMap
+ * 
+ * + * and then internally the {@code InputActionBindings} maintains a list + * {@code [map1, map2, ...]} which is then assembled into a new chain whenever + * something changes. So + * + *
+ * theMap → map2 → map1
+ * 
+ * + * becomes + * + *
+ * theMap → map3 → map2 → map1
+ * 
+ * + * when "map3" is added. + *

+ * And the behaviours framework doesn't have to care about the Swing particulars + * (e.g., actually each {@code JComponent} has 3 different inputmaps for + * different situations). + * + *

Blocking maps

+ * + * On top of this, InputActionBindings adds a few convenience features. + *

+ * For every map pair that you append to {@code InputActionBindings} you specify + * a unique name that can later be used to remove the map pair again. + *

+ * E.g., if map2 would have the name "bookmarking", then + * + *

+ * InputActionBindings.removeInputMap( "bookmarking" )
+ * 
+ * + * would reassemble + * + *
+ * theMap → map3 → map2 → map1
+ * 
+ * + * to + * + *
+ * theMap → map3 → map1
+ * 
+ * + * Finally, given that all maps have unique names, when you add a map you can + * specify a set of other maps to block. + *

+ * For example, let's say that you want to temporarily add your own specific + * keys that maybe conflict with the bookmarking keys. + *

+ * Starting from this situation: + * + *

+ * theMap → map3 → map2 → map1
+ * 
+ * + * the following + * + *
+ * InputActionBindings.addInputMap( map4, "myMap", "navigation", "bookmarking" )
+ * // "myMap" blocks "navigation", "bookmarking"
+ * 
+ * + * will result in + * + *
+ * theMap → map4 → map3
+ * 
+ * + * that is, map1 and map2 were blocked. + *

+ * + * However, InputActionsBindings still knows about map1 and map2. Once the + * blocking "myMap" is removed, they will be reinstated. + *

+ * As a concrete example: this whole machinery is used for bookmarking in BDV. + * You set a bookmark by pressing "shift B" and then some other key, that is the + * name for the bookmark. For example, you can press "shift B" "1" to set a + * bookmark named "1". + *

+ * After you press "shift B", a temporary inputmap is installed that blocks all + * other inputmaps. So "1" will not trigger switching to bdv source 1. After the + * bookmark name "1" is received, the temporary inputmap is removed again. + *

+ * For Behaviours, the chaining and blocking concepts are exactly the same + * + * + * + *

Modifier names.

+ * + * The following modifiers can be used: + * + * + * + *

Mouse buttons and scrolling.

+ * + * + * + * + *

Key names.

+ * + * Key names are the usual alphanumeric and function keys: + * + * + * + * Moreover the following special key names are supported (as in the + * AWTKeyStroke.getAWTKeyStroke(String) method): + * + * + * + * These are names for cursor keys: + * + * + * + * These are names for the numpad keys: + * + * + * + */ +package org.scijava.ui.behaviour; diff --git a/src/main/java/org/scijava/ui/behaviour/util/AbstractNamedAction.java b/src/main/java/org/scijava/ui/behaviour/util/AbstractNamedAction.java index c70afed..79e10a3 100644 --- a/src/main/java/org/scijava/ui/behaviour/util/AbstractNamedAction.java +++ b/src/main/java/org/scijava/ui/behaviour/util/AbstractNamedAction.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour.util; import javax.swing.AbstractAction; diff --git a/src/main/java/org/scijava/ui/behaviour/util/AbstractNamedBehaviour.java b/src/main/java/org/scijava/ui/behaviour/util/AbstractNamedBehaviour.java index cc904ea..2034217 100644 --- a/src/main/java/org/scijava/ui/behaviour/util/AbstractNamedBehaviour.java +++ b/src/main/java/org/scijava/ui/behaviour/util/AbstractNamedBehaviour.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour.util; import org.scijava.ui.behaviour.Behaviour; diff --git a/src/main/java/org/scijava/ui/behaviour/util/Actions.java b/src/main/java/org/scijava/ui/behaviour/util/Actions.java index 5322444..4cb4a68 100644 --- a/src/main/java/org/scijava/ui/behaviour/util/Actions.java +++ b/src/main/java/org/scijava/ui/behaviour/util/Actions.java @@ -1,7 +1,44 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour.util; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + import javax.swing.ActionMap; import javax.swing.InputMap; +import javax.swing.KeyStroke; import org.scijava.ui.behaviour.KeyStrokeAdder; import org.scijava.ui.behaviour.io.InputTriggerConfig; @@ -22,9 +59,11 @@ public class Actions private final ActionMap actionMap; - protected final KeyStrokeAdder.Factory keyConfig; + private final String[] keyConfigContexts; - protected final KeyStrokeAdder keyStrokeAdder; + protected KeyStrokeAdder.Factory keyConfig; + + protected KeyStrokeAdder keyStrokeAdder; /** * Construct with new, empty {@link InputMap} and {@link ActionMap}. Actions @@ -73,8 +112,9 @@ public Actions( { this.actionMap = actionMap; this.inputMap = inputMap; - this.keyConfig = keyConfig; - keyStrokeAdder = keyConfig.keyStrokeAdder( inputMap, keyConfigContexts ); + this.keyConfig = keyConfig != null ? keyConfig : new InputTriggerConfig(); + this.keyConfigContexts = keyConfigContexts; + keyStrokeAdder = this.keyConfig.keyStrokeAdder( inputMap, keyConfigContexts ); } public InputMap getInputMap() @@ -113,8 +153,8 @@ public void install( } /** - * Create and install a new {@link Action} with the specified {@code name} that - * calls the specified {@link Runnable} when triggered. + * Create and install a new {@link javax.swing.Action} with the specified + * {@code name} that calls the specified {@link Runnable} when triggered. * * @param runnable * action to install. @@ -145,4 +185,61 @@ public void namedAction( final AbstractNamedAction action, final String... defau keyStrokeAdder.put( action.name(), defaultKeyStrokes ); action.put( actionMap ); } + + /** + * Clears the {@link InputMap} and re-adds all ({@code String}) action keys + * from {@link ActionMap} using the provided {@code keyConfig}. + * + * @param keyConfig + * the new keyConfig + */ + public void updateKeyConfig( final InputTriggerConfig keyConfig ) + { + updateKeyConfig( keyConfig, true ); + } + + /** + * Clears the {@link InputMap} and re-adds all ({@code String}) action keys + * from {@link ActionMap} using the provided {@code keyConfig}. + *

+ * Actions that are currently in the {@code InputMap} but are not defined in + * the {@code keyConfig} retain their current keystrokes (note that + * {@code keyConfig} can map actions to "not mapped"). + * + * @param keyConfig + * the new keyConfig + * @param clearAll + * whether to clear all bindings (also of actions that are + * undefined in {@code keyConfig}) + */ + public void updateKeyConfig( final InputTriggerConfig keyConfig, final boolean clearAll ) + { + this.keyConfig = keyConfig; + + final Map< Object, List< KeyStroke > > unassigned = new HashMap<>(); + if ( !clearAll ) + { + final KeyStroke[] inputs = inputMap.keys(); + if ( inputs != null ) + { + final HashSet< String > contexts = new HashSet<>( Arrays.asList( keyConfigContexts ) ); + for ( final KeyStroke input : inputs ) + { + final Object actionKey = inputMap.get( input ); + if ( ( !( actionKey instanceof String ) ) || keyConfig.getInputs( ( String ) actionKey, contexts ).isEmpty() ) + unassigned.computeIfAbsent( actionKey, k -> new ArrayList<>() ).add( input ); + } + } + } + + keyStrokeAdder = keyConfig.keyStrokeAdder( inputMap, keyConfigContexts ); + inputMap.clear(); + final Object[] keys = actionMap.keys(); + if ( keys != null ) + for ( final Object o : keys ) + if ( !unassigned.containsKey( o ) ) + keyStrokeAdder.put( ( String ) o ); + + unassigned.forEach( ( actionMapKey, keyStrokes ) -> keyStrokes.forEach( keyStroke -> inputMap.put( keyStroke, actionMapKey ) ) ); + } } diff --git a/src/main/java/org/scijava/ui/behaviour/util/Behaviours.java b/src/main/java/org/scijava/ui/behaviour/util/Behaviours.java index 1c191ac..0774627 100644 --- a/src/main/java/org/scijava/ui/behaviour/util/Behaviours.java +++ b/src/main/java/org/scijava/ui/behaviour/util/Behaviours.java @@ -1,7 +1,46 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour.util; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; + import org.scijava.ui.behaviour.Behaviour; import org.scijava.ui.behaviour.BehaviourMap; +import org.scijava.ui.behaviour.InputTrigger; import org.scijava.ui.behaviour.InputTriggerAdder; import org.scijava.ui.behaviour.InputTriggerMap; import org.scijava.ui.behaviour.io.InputTriggerConfig; @@ -22,9 +61,11 @@ public class Behaviours private final BehaviourMap behaviourMap; - protected final InputTriggerAdder.Factory keyConfig; + private final String[] keyConfigContexts; - protected final InputTriggerAdder inputTriggerAdder; + protected InputTriggerAdder.Factory keyConfig; + + protected InputTriggerAdder inputTriggerAdder; /** * Construct with new, empty {@link InputTriggerMap} and @@ -73,8 +114,9 @@ public Behaviours( { this.inputTriggerMap = inputTriggerMap; this.behaviourMap = behaviourMap; - this.keyConfig = keyConfig; - inputTriggerAdder = keyConfig.inputTriggerAdder( inputTriggerMap, keyConfigContexts ); + this.keyConfig = keyConfig != null ? keyConfig : new InputTriggerConfig(); + this.keyConfigContexts = keyConfigContexts; + inputTriggerAdder = this.keyConfig.inputTriggerAdder( inputTriggerMap, keyConfigContexts ); } public InputTriggerMap getInputTriggerMap() @@ -124,4 +166,59 @@ public void namedBehaviour( final AbstractNamedBehaviour behaviour, final String inputTriggerAdder.put( behaviour.name(), defaultTriggers ); behaviour.put( behaviourMap ); } + + /** + * Clears the {@link InputTriggerMap} and re-adds all behaviour keys from + * {@link BehaviourMap} using the provided {@code keyConfig}. + * + * @param keyConfig + * the new keyConfig + */ + public void updateKeyConfig( final InputTriggerConfig keyConfig ) + { + updateKeyConfig( keyConfig, true ); + } + + /** + * Clears the {@link InputTriggerMap} and re-adds all behaviour keys from + * {@link BehaviourMap} using the provided {@code keyConfig}. + *

+ * If {@code clearAll==false}, then behaviours that are currently in the + * {@code InputTriggerMap} but are not defined in the {@code keyConfig} + * retain their current keystrokes (note that {@code keyConfig} can map + * behaviours to "not mapped"). + * + * @param keyConfig + * the new keyConfig + * @param clearAll + * whether to clear all bindings (also of behaviours that are + * undefined in {@code keyConfig}) + */ + public void updateKeyConfig( final InputTriggerConfig keyConfig, final boolean clearAll ) + { + this.keyConfig = keyConfig; + + final Map< String, List< InputTrigger > > unassigned = new HashMap<>(); + if ( !clearAll ) + { + final Map< InputTrigger, Set< String > > bindings = inputTriggerMap.getBindings(); + final HashSet< String > contexts = new HashSet<>( Arrays.asList( keyConfigContexts ) ); + for ( final Entry< InputTrigger, Set< String > > entry : bindings.entrySet() ) + { + final InputTrigger trigger = entry.getKey(); + for ( final String behaviourKey : entry.getValue() ) + { + if ( keyConfig.getInputs( behaviourKey, contexts ).isEmpty() ) + unassigned.computeIfAbsent( behaviourKey, k -> new ArrayList<>() ).add( trigger ); + } + } + } + + inputTriggerAdder = keyConfig.inputTriggerAdder( inputTriggerMap, keyConfigContexts ); + inputTriggerMap.clear(); + for ( final String behaviourName : behaviourMap.keys() ) + inputTriggerAdder.put( behaviourName ); + + unassigned.forEach( ( behaviourKey, triggers ) -> triggers.forEach( trigger -> inputTriggerMap.put( trigger, behaviourKey ) ) ); + } } diff --git a/src/main/java/org/scijava/ui/behaviour/util/InputActionBindings.java b/src/main/java/org/scijava/ui/behaviour/util/InputActionBindings.java index 8fa4727..3a133ec 100644 --- a/src/main/java/org/scijava/ui/behaviour/util/InputActionBindings.java +++ b/src/main/java/org/scijava/ui/behaviour/util/InputActionBindings.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour.util; import java.util.ArrayList; @@ -12,6 +41,7 @@ import javax.swing.ComponentInputMap; import javax.swing.InputMap; import javax.swing.JComponent; +import javax.swing.SwingUtilities; /** * Maintains lists of {@link ActionMap}s and {@link InputMap}s, which are @@ -21,7 +51,7 @@ * overrides all previous ones. For added {@link InputMap}s it is possible to * block maps that were added earlier. * - * @author Tobias Pietzsch <tobias.pietzsch@gmail.com> + * @author Tobias Pietzsch */ public final class InputActionBindings { @@ -35,6 +65,16 @@ public final class InputActionBindings */ private final ActionMap theActionMap; + /** + * parent for the {@link #getConcatenatedInputMap() concatenated InputMap}. + */ + private InputMap parentInputMap; + + /** + * parent for the {@link #getConcatenatedActionMap()} () concatenated ActionMap}. + */ + private ActionMap parentActionMap; + private final List< Actions > actions; private final List< Keys > inputs; @@ -79,6 +119,23 @@ public void addActionMap( final String id, final ActionMap actionMap ) updateTheActionMap(); } + /** + * Add as {@link ActionMap} with the specified id at the specified position + * in the list (overrides maps at lower positions). If the specified id + * already exists in the list, remove the corresponding earlier + * {@link ActionMap}. + */ + public void addActionMap( final int index, final String id, final ActionMap actionMap ) + { + removeId( actions, id ); + if ( actionMap != null ) + { + final int i = Math.max( 0, Math.min( actions.size(), index ) ); + actions.add( i, new Actions( id, actionMap ) ); + } + updateTheActionMap(); + } + /** * Remove the {@link ActionMap} with the given id from the list. */ @@ -89,7 +146,17 @@ public void removeActionMap( final String id ) } /** - * Add as {@link InputMap} with the specified id to the end of the list + * Set existing parent ActionMap, which is managed outside of this + * InputActionBindings and serves as a parent for the whole chain. + */ + public void setParentActionMap( final ActionMap parent ) + { + parentActionMap = parent; + updateTheActionMap(); + } + + /** + * Adds a {@link InputMap} with the specified id to the end of the list * (overrides maps that were added earlier). If the specified id already * exists in the list, remove the corresponding earlier {@link InputMap}. *

@@ -109,7 +176,7 @@ public void addInputMap( final String id, final InputMap inputMap, final String. } /** - * Add as {@link InputMap} with the specified id to the end of the list + * Adds a {@link InputMap} with the specified id to the end of the list * (overrides maps that were added earlier). If the specified id already * exists in the list, remove the corresponding earlier {@link InputMap}. *

@@ -131,6 +198,56 @@ public void addInputMap( final String id, final InputMap inputMap, final Collect updateTheInputMap(); } + /** + * Inserts a {@link InputMap} with the specified id at the specified + * position in the list (overrides maps at lower positions). If the + * specified id already exists in the list, remove the corresponding earlier + * {@link InputMap}. + *

+ * If {@code idsToBlock} are given, {@link InputMap}s with these ids earlier + * in the chain that should be disabled. The special id "all" blocks all + * earlier {@link InputMap}s. + * + * @param index + * @param id + * @param inputMap + * @param idsToBlock + * ids of {@link InputMap}s earlier in the chain that should be + * disabled. + */ + public void addInputMap( final int index, final String id, final InputMap inputMap, final String... idsToBlock ) + { + addInputMap( index, id, inputMap, Arrays.asList( idsToBlock ) ); + } + + /** + * Inserts a {@link InputMap} with the specified id at the specified + * position in the list (overrides maps at lower positions). If the + * specified id already exists in the list, remove the corresponding earlier + * {@link InputMap}. + *

+ * If {@code idsToBlock} are given, {@link InputMap}s with these ids earlier + * in the chain that should be disabled. The special id "all" blocks all + * earlier {@link InputMap}s. + * + * @param index + * @param id + * @param inputMap + * @param idsToBlock + * ids of {@link InputMap}s earlier in the chain that should be + * disabled. + */ + public void addInputMap( final int index, final String id, final InputMap inputMap, final Collection< String > idsToBlock ) + { + removeId( inputs, id ); + if ( inputMap != null ) + { + final int i = Math.max( 0, Math.min( inputs.size(), index ) ); + inputs.add( i, new Keys( id, inputMap, idsToBlock ) ); + } + updateTheInputMap(); + } + /** * Remove the {@link InputMap} with the given id from the list. */ @@ -140,6 +257,16 @@ public void removeInputMap( final String id ) updateTheInputMap(); } + /** + * Set existing parent InputMap, which is managed outside of this + * InputActionBindings and serves as a parent for the whole chain. + */ + public void setParentInputMap( final InputMap parent ) + { + parentInputMap = parent; + updateTheInputMap(); + } + /** * Get the chained {@link InputMap}. This is the leaf map, that has all * {@link #addInputMap(String, InputMap, String...) added} {@link InputMap}s @@ -162,6 +289,38 @@ public ActionMap getConcatenatedActionMap() return theActionMap; } + /** + * Creates a new {@code InputActionBindings} and installs it into + * {@code component}, either augmenting or replacing {@code component}s + * existing {@code InputMap} and {@code ActionMap}. + * + * @param component + * the component whose {@code InputMap} and {@code ActionMap} to set. + * @param condition + * one of {@code JComponent.WHEN_IN_FOCUSED_WINDOW}, + * {@code WHEN_IN_FOCUSED_WINDOW}, + * {@code WHEN_ANCESTOR_OF_FOCUSED_COMPONENT}. + * @param replaceExistingMaps + * if {@code true}, the existing {@code InputMap} and {@code ActionMap} + * will be replaced. If {@code false}, the existing maps will become the + * parents of the new ones. + * @return the new {@code InputActionBindings} + */ + public static InputActionBindings installNewBindings( final JComponent component, final int condition, final boolean replaceExistingMaps ) + { + final InputActionBindings keybindings = new InputActionBindings(); + if ( !replaceExistingMaps ) + { + final ActionMap existingActionMap = component.getActionMap(); + final InputMap existingInputMap = component.getInputMap( condition ); + keybindings.setParentActionMap( existingActionMap.getParent() ); + keybindings.setParentInputMap( existingInputMap.getParent() ); + } + SwingUtilities.replaceUIActionMap( component, keybindings.getConcatenatedActionMap() ); + SwingUtilities.replaceUIInputMap( component, condition, keybindings.getConcatenatedInputMap() ); + return keybindings; + } + private interface WithId { public String getId(); @@ -247,7 +406,7 @@ private void updateTheActionMap() root = map; } } - root.setParent( null ); + root.setParent( parentActionMap ); } private void updateTheInputMap() @@ -271,8 +430,11 @@ private void updateTheInputMap() blocked.addAll( keys.getKeysIdsToBlock() ); if ( blocked.contains( "all" ) ) - break; + { + root.setParent( null ); + return; + } } - root.setParent( null ); + root.setParent( parentInputMap ); } } diff --git a/src/main/java/org/scijava/ui/behaviour/util/RunnableAction.java b/src/main/java/org/scijava/ui/behaviour/util/RunnableAction.java index eaa3b41..1c74d23 100644 --- a/src/main/java/org/scijava/ui/behaviour/util/RunnableAction.java +++ b/src/main/java/org/scijava/ui/behaviour/util/RunnableAction.java @@ -1,8 +1,37 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour.util; import java.awt.event.ActionEvent; -public class RunnableAction extends AbstractNamedAction +public class RunnableAction extends AbstractNamedAction implements Runnable { private final Runnable action; @@ -18,5 +47,11 @@ public void actionPerformed( final ActionEvent e ) action.run(); } + @Override + public void run() + { + action.run(); + } + private static final long serialVersionUID = 1L; } diff --git a/src/main/java/org/scijava/ui/behaviour/util/TriggerBehaviourBindings.java b/src/main/java/org/scijava/ui/behaviour/util/TriggerBehaviourBindings.java index ce1ae78..a9cce8e 100644 --- a/src/main/java/org/scijava/ui/behaviour/util/TriggerBehaviourBindings.java +++ b/src/main/java/org/scijava/ui/behaviour/util/TriggerBehaviourBindings.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour.util; import java.util.ArrayList; @@ -8,9 +37,6 @@ import java.util.ListIterator; import java.util.Set; -import javax.swing.ActionMap; -import javax.swing.InputMap; - import org.scijava.ui.behaviour.BehaviourMap; import org.scijava.ui.behaviour.InputTriggerMap; @@ -28,12 +54,12 @@ public final class TriggerBehaviourBindings { /** - * the root of the {@link InputMap} chain. + * the root of the {@link InputTriggerMap} chain. */ private final InputTriggerMap theInputTriggerMap; /** - * the root of the {@link ActionMap} chain. + * the root of the {@link BehaviourMap} chain. */ private final BehaviourMap theBehaviourMap; @@ -62,7 +88,7 @@ public void addBehaviourMap( final String id, final BehaviourMap behaviourMap ) } /** - * Remove the {@link ActionMap} with the given id from the list. + * Remove the {@link BehaviourMap} with the given id from the list. */ public void removeBehaviourMap( final String id ) { @@ -75,6 +101,10 @@ public void removeBehaviourMap( final String id ) * list (overrides maps that were added earlier). If the specified id * already exists in the list, remove the corresponding earlier * {@link InputTriggerMap}. + *

+ * If {@code idsToBlock} are given, {@link InputTriggerMap}s with these ids + * earlier in the chain that should be disabled. The special id "all" blocks + * all earlier {@link InputTriggerMap}s. * * @param id * @param inputTriggerMap @@ -92,6 +122,10 @@ public void addInputTriggerMap( final String id, final InputTriggerMap inputTrig * list (overrides maps that were added earlier). If the specified id * already exists in the list, remove the corresponding earlier * {@link InputTriggerMap}. + *

+ * If {@code idsToBlock} are given, {@link InputTriggerMap}s with these ids + * earlier in the chain that should be disabled. The special id "all" blocks + * all earlier {@link InputTriggerMap}s. * * @param id * @param inputTriggerMap @@ -219,6 +253,7 @@ private void updateTheBehaviourMap() final BehaviourMap map = iter.previous().getBehaviourMap(); if ( map != null ) { + map.setParent( null ); root.setParent( map ); root = map; } @@ -241,6 +276,7 @@ private void updateTheInputTriggerMap() final InputTriggerMap map = keys.getInputTriggerMap(); if ( map != null ) { + map.setParent( null ); root.setParent( map ); root = map; } diff --git a/src/main/java/org/scijava/ui/behaviour/util/WrappedActionMap.java b/src/main/java/org/scijava/ui/behaviour/util/WrappedActionMap.java new file mode 100644 index 0000000..9929f3a --- /dev/null +++ b/src/main/java/org/scijava/ui/behaviour/util/WrappedActionMap.java @@ -0,0 +1,106 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.ui.behaviour.util; + +import javax.swing.Action; +import javax.swing.ActionMap; + +/** + * A view of the specified {@link ActionMap} that can have its own parent. The + * wrapped {@code ActionMap} should not have a parent!? + */ +public class WrappedActionMap extends ActionMap +{ + private static final long serialVersionUID = 1L; + + private final ActionMap actionMap; + + private ActionMap parent; + + public WrappedActionMap( final ActionMap inputMap ) + { + this.actionMap = inputMap; + } + + @Override + public void setParent( final ActionMap map ) + { + parent = map; + } + + @Override + public ActionMap getParent() + { + return parent; + } + + @Override + public void put( final Object key, final Action action ) + { + throw new UnsupportedOperationException( getClass().getSimpleName() + " cannot be modified." ); + } + + @Override + public Action get( final Object key ) + { + final Action value = actionMap.get( key ); + if ( value == null ) + { + final ActionMap parent = getParent(); + if ( parent != null ) + return parent.get( key ); + } + return value; + } + + @Override + public void remove( final Object key ) + { + throw new UnsupportedOperationException( getClass().getSimpleName() + " cannot be modified." ); + } + + @Override + public void clear() + { + throw new UnsupportedOperationException( getClass().getSimpleName() + " cannot be modified." ); + } + + @Override + public Object[] keys() + { + return actionMap.keys(); + } + + @Override + public int size() + { + return actionMap.size(); + } +} diff --git a/src/main/java/org/scijava/ui/behaviour/util/WrappedBehaviourMap.java b/src/main/java/org/scijava/ui/behaviour/util/WrappedBehaviourMap.java new file mode 100644 index 0000000..6e34af0 --- /dev/null +++ b/src/main/java/org/scijava/ui/behaviour/util/WrappedBehaviourMap.java @@ -0,0 +1,125 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.ui.behaviour.util; + +import java.util.HashMap; +import java.util.Map; + +import org.scijava.ui.behaviour.Behaviour; +import org.scijava.ui.behaviour.BehaviourMap; + +/** + * A view of the specified {@link BehaviourMap} that can have its own parent. + * The wrapped {@code BehaviourMap} should not have a parent!? + */ +public class WrappedBehaviourMap extends BehaviourMap +{ + private final BehaviourMap behaviourMap; + + private BehaviourMap parent; + + private int expectedParentModCount; + + public WrappedBehaviourMap( final BehaviourMap behaviourMap ) + { + this.behaviourMap = behaviourMap; + parent = null; + expectedParentModCount = 0; + } + + @Override + public void setParent( final BehaviourMap map ) + { + parent = map; + if ( map != null ) + expectedParentModCount = parent.modCount(); + } + + @Override + public BehaviourMap getParent() + { + return parent; + } + + @Override + public synchronized void put( final String key, final Behaviour behaviour ) + { + throw new UnsupportedOperationException( getClass().getSimpleName() + " cannot be modified." ); + } + + @Override + public synchronized Behaviour get( final String key ) + { + final Behaviour behaviour = behaviourMap.get( key ); + if ( behaviour == null && parent != null ) + return parent.get( key ); + else + return behaviour; + } + + @Override + public synchronized void remove( final String key ) + { + throw new UnsupportedOperationException( getClass().getSimpleName() + " cannot be modified." ); + } + + @Override + public synchronized void clear() + { + throw new UnsupportedOperationException( getClass().getSimpleName() + " cannot be modified." ); + } + + @Override + public synchronized Map< String, Behaviour > getAllBindings() + { + final Map< String, Behaviour > allBindings = ( parent == null ) ? new HashMap<>() : parent.getAllBindings(); + + for ( final Map.Entry< String, Behaviour > entry : behaviourMap.getAllBindings().entrySet() ) + allBindings.put( entry.getKey(), entry.getValue() ); + + return allBindings; + } + + @Override + public int modCount() + { + if ( parent != null ) + { + final int m = parent.modCount(); + if ( m != expectedParentModCount ) + { + expectedParentModCount = m; + behaviourMap.remove( null ); // hack to bump + // behaviourMap.modCount + } + } + return behaviourMap.modCount(); + } +} diff --git a/src/main/java/org/scijava/ui/behaviour/util/WrappedInputMap.java b/src/main/java/org/scijava/ui/behaviour/util/WrappedInputMap.java new file mode 100644 index 0000000..ad530ec --- /dev/null +++ b/src/main/java/org/scijava/ui/behaviour/util/WrappedInputMap.java @@ -0,0 +1,106 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.ui.behaviour.util; + +import javax.swing.InputMap; +import javax.swing.KeyStroke; + +/** + * A view of the specified {@link InputMap} that can have its own parent. The + * wrapped {@code InputMap} should not have a parent!? + */ +public class WrappedInputMap extends InputMap +{ + private static final long serialVersionUID = 1L; + + private final InputMap inputMap; + + private InputMap parent; + + public WrappedInputMap( final InputMap inputMap ) + { + this.inputMap = inputMap; + } + + @Override + public void setParent( final InputMap map ) + { + parent = map; + } + + @Override + public InputMap getParent() + { + return parent; + } + + @Override + public void put( final KeyStroke keyStroke, final Object actionMapKey ) + { + throw new UnsupportedOperationException( getClass().getSimpleName() + " cannot be modified." ); + } + + @Override + public Object get( final KeyStroke keyStroke ) + { + final Object value = inputMap.get( keyStroke ); + if ( value == null ) + { + final InputMap parent = getParent(); + if ( parent != null ) + return parent.get( keyStroke ); + } + return value; + } + + @Override + public void remove( final KeyStroke key ) + { + throw new UnsupportedOperationException( getClass().getSimpleName() + " cannot be modified." ); + } + + @Override + public void clear() + { + throw new UnsupportedOperationException( getClass().getSimpleName() + " cannot be modified." ); + } + + @Override + public KeyStroke[] keys() + { + return inputMap.keys(); + } + + @Override + public int size() + { + return inputMap.size(); + } +} diff --git a/src/main/java/org/scijava/ui/behaviour/util/WrappedInputTriggerMap.java b/src/main/java/org/scijava/ui/behaviour/util/WrappedInputTriggerMap.java new file mode 100644 index 0000000..e97fadb --- /dev/null +++ b/src/main/java/org/scijava/ui/behaviour/util/WrappedInputTriggerMap.java @@ -0,0 +1,146 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.ui.behaviour.util; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import org.scijava.ui.behaviour.InputTrigger; +import org.scijava.ui.behaviour.InputTriggerMap; + +/** + * A view of the specified {@link InputTriggerMap} that can have its own parent. + * The wrapped {@code InputTriggerMap} should not have a parent!? + */ +public class WrappedInputTriggerMap extends InputTriggerMap +{ + private final InputTriggerMap inputTriggerMap; + + private InputTriggerMap parent; + + private int expectedParentModCount; + + public WrappedInputTriggerMap( final InputTriggerMap inputTriggerMap ) + { + this.inputTriggerMap = inputTriggerMap; + parent = null; + expectedParentModCount = 0; + } + + @Override + public void setParent( final InputTriggerMap map ) + { + parent = map; + if ( map != null ) + expectedParentModCount = parent.modCount(); + } + + @Override + public InputTriggerMap getParent() + { + return parent; + } + + @Override + public synchronized void put( final InputTrigger inputTrigger, final String behaviourKey ) + { + throw new UnsupportedOperationException( getClass().getSimpleName() + " cannot be modified." ); + } + + @Override + public synchronized Set< String > get( final InputTrigger inputTrigger ) + { + Set< String > keys = null; + if ( parent != null ) + keys = parent.get( inputTrigger ); + else + keys = new HashSet<>(); + keys.addAll( inputTriggerMap.get( inputTrigger ) ); + return keys; + } + + @Override + public synchronized void remove( final InputTrigger inputTrigger, final String behaviourKey ) + { + throw new UnsupportedOperationException( getClass().getSimpleName() + " cannot be modified." ); + } + + @Override + public synchronized void removeAll( final InputTrigger inputTrigger ) + { + throw new UnsupportedOperationException( getClass().getSimpleName() + " cannot be modified." ); + } + + @Override + public synchronized void clear() + { + throw new UnsupportedOperationException( getClass().getSimpleName() + " cannot be modified." ); + } + + @Override + public synchronized Map< InputTrigger, Set< String > > getAllBindings() + { + final Map< InputTrigger, Set< String > > allBindings; + if ( parent != null ) + allBindings = parent.getAllBindings(); + else + allBindings = new HashMap<>(); + + for ( final Map.Entry< InputTrigger, Set< String > > entry : inputTriggerMap.getAllBindings().entrySet() ) + { + final InputTrigger inputTrigger = entry.getKey(); + if ( entry.getValue() == null || entry.getValue().isEmpty() ) + continue; + + final Set< String > behaviourKeys = allBindings.computeIfAbsent( inputTrigger, k -> new HashSet<>() ); + behaviourKeys.addAll( entry.getValue() ); + } + + return allBindings; + } + + @Override + public int modCount() + { + if ( parent != null ) + { + final int m = parent.modCount(); + if ( m != expectedParentModCount ) + { + expectedParentModCount = m; + inputTriggerMap.remove( null, null ); // hack to bump + // inputTriggerMap.modCount + } + } + return inputTriggerMap.modCount(); + } +} diff --git a/src/test/java/org/scijava/ui/behaviour/EventsInteractiveTest.java b/src/test/java/org/scijava/ui/behaviour/EventsInteractiveTest.java index ea0c2d5..3f79aad 100644 --- a/src/test/java/org/scijava/ui/behaviour/EventsInteractiveTest.java +++ b/src/test/java/org/scijava/ui/behaviour/EventsInteractiveTest.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; import java.awt.Dimension; diff --git a/src/test/java/org/scijava/ui/behaviour/MultiWindowExample.java b/src/test/java/org/scijava/ui/behaviour/MultiWindowExample.java index 8e0cb68..5134999 100644 --- a/src/test/java/org/scijava/ui/behaviour/MultiWindowExample.java +++ b/src/test/java/org/scijava/ui/behaviour/MultiWindowExample.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; import java.awt.Dimension; diff --git a/src/test/java/org/scijava/ui/behaviour/UsageExample.java b/src/test/java/org/scijava/ui/behaviour/UsageExample.java index a68db3c..95e4830 100644 --- a/src/test/java/org/scijava/ui/behaviour/UsageExample.java +++ b/src/test/java/org/scijava/ui/behaviour/UsageExample.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour; import java.awt.Dimension; @@ -10,66 +39,62 @@ import org.scijava.ui.behaviour.io.InputTriggerConfig; import org.scijava.ui.behaviour.io.InputTriggerDescription; import org.scijava.ui.behaviour.io.yaml.YamlConfigIO; +import org.scijava.ui.behaviour.util.AbstractNamedBehaviour; +import org.scijava.ui.behaviour.util.Behaviours; public class UsageExample { - static class MyDragBehaviour implements DragBehaviour + static class MyDragBehaviour extends AbstractNamedBehaviour implements DragBehaviour { - private final String name; - public MyDragBehaviour( final String name ) { - this.name = name; + super( name ); } @Override public void init( final int x, final int y ) { - System.out.println( name + ": init(" + x + ", " + y + ")" ); + System.out.println( name() + ": init(" + x + ", " + y + ")" ); } @Override public void drag( final int x, final int y ) { - System.out.println( name + ": drag(" + x + ", " + y + ")" ); + System.out.println( name() + ": drag(" + x + ", " + y + ")" ); } @Override public void end( final int x, final int y ) { - System.out.println( name + ": end(" + x + ", " + y + ")" ); + System.out.println( name() + ": end(" + x + ", " + y + ")" ); } } - static class MyClickBehaviour implements ClickBehaviour + static class MyClickBehaviour extends AbstractNamedBehaviour implements ClickBehaviour { - private final String name; - public MyClickBehaviour( final String name ) { - this.name = name; + super( name ); } @Override public void click( final int x, final int y ) { - System.out.println( name + ": click(" + x + ", " + y + ")" ); + System.out.println( name() + ": click(" + x + ", " + y + ")" ); } } - static class MyScrollBehaviour implements ScrollBehaviour + static class MyScrollBehaviour extends AbstractNamedBehaviour implements ScrollBehaviour { - private final String name; - public MyScrollBehaviour( final String name ) { - this.name = name; + super( name ); } @Override public void scroll( final double wheelRotation, final boolean isHorizontal, final int x, final int y ) { - System.out.println( name + ": scroll(" + wheelRotation + ", " + isHorizontal + ", " + x + ", " + y + ")" ); + System.out.println( name() + ": scroll(" + wheelRotation + ", " + isHorizontal + ", " + x + ", " + y + ")" ); } } @@ -124,20 +149,19 @@ public static void main( final String[] args ) /* * Create behaviours and input mappings. */ - behaviourMap.put( "drag1", new MyDragBehaviour( "drag1" ) ); - behaviourMap.put( "drag2", new MyDragBehaviour( "drag2" ) ); - behaviourMap.put( "scroll1", new MyScrollBehaviour( "scroll1" ) ); - behaviourMap.put( "click1", new MyClickBehaviour( "click1" ) ); - - final InputTriggerAdder adder = config.inputTriggerAdder( inputMap, "all" ); - adder.put( "drag1" ); // put input trigger as defined in config - adder.put( "drag2", "button1", "shift A | G" ); // default triggers if not defined in config - adder.put( "scroll1", "alt scroll" ); - adder.put( "click1", "button3", "B | all" ); + Behaviours behaviours = new Behaviours( inputMap, behaviourMap, config, "all" ); + behaviours.namedBehaviour( new MyDragBehaviour( "drag1" ) ); // put input trigger as defined in config + behaviours.namedBehaviour( new MyDragBehaviour( "drag2" ), + "button1", "shift A | G" ); // default triggers if not defined in config + behaviours.namedBehaviour( new MyScrollBehaviour( "scroll1" ), + "alt scroll" ); + behaviours.namedBehaviour( new MyClickBehaviour( "click1" ), + "button3", "B | all" ); /* - * See bdv.viewer.TriggerBehaviourBindings for chaining InputMaps and BehaviourMaps. - * (might move here in the future) + * See org.scijava.ui.behaviour.util.InputActionBindings and + * org.scijava.ui.behaviour.util.TriggerBehaviourBindings for chaining + * InputMaps and BehaviourMaps. */ } } diff --git a/src/test/java/org/scijava/ui/behaviour/io/gui/VisualEditorPanelDemo.java b/src/test/java/org/scijava/ui/behaviour/io/gui/VisualEditorPanelDemo.java new file mode 100644 index 0000000..3c0e5ea --- /dev/null +++ b/src/test/java/org/scijava/ui/behaviour/io/gui/VisualEditorPanelDemo.java @@ -0,0 +1,177 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ +package org.scijava.ui.behaviour.io.gui; + +import java.awt.EventQueue; +import java.io.StringReader; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.swing.JFrame; +import javax.swing.SwingUtilities; +import javax.swing.UIManager; +import javax.swing.UnsupportedLookAndFeelException; + +import org.scijava.ui.behaviour.io.InputTriggerConfig; +import org.scijava.ui.behaviour.io.InputTriggerDescription; +import org.scijava.ui.behaviour.io.yaml.YamlConfigIO; + +public class VisualEditorPanelDemo +{ + /* + * DEMO METHODS. + */ + + private static InputTriggerConfig getDemoConfig() + { + final StringReader reader = new StringReader( "---\n" + + "- !mapping" + "\n" + + " action: fluke" + "\n" + + " contexts: [all]" + "\n" + + " triggers: [F]" + "\n" + + "- !mapping" + "\n" + + " action: drag1" + "\n" + + " contexts: [all]" + "\n" + + " triggers: [button1, win G]" + "\n" + + "- !mapping" + "\n" + + " action: scroll1" + "\n" + + " contexts: [all]" + "\n" + + " triggers: [scroll]" + "\n" + + "- !mapping" + "\n" + + " action: scroll1" + "\n" + + " contexts: [trackscheme, mamut]" + "\n" + + " triggers: [shift D]" + "\n" + + "- !mapping" + "\n" + + " action: destroy the world" + "\n" + + " contexts: [mamut]" + "\n" + + " triggers: [control A]" + "\n" + + "" ); + final List< InputTriggerDescription > triggers = YamlConfigIO.read( reader ); + return new InputTriggerConfig( triggers ); + } + + private static Map< Command, String > getDemoCommands() + { + return new CommandDescriptionBuilder() + .addCommand( "drag1", "mamut", "Move an item around the editor." ) + .addCommand( "drag1", "trackscheme", "Move an item around the editor." ) + .addCommand( "drag1", "other", "Move an item around the editor." ) + .addCommand( "Elude", "other", "Refuse to answer the question." ) + .addCommand( "scroll1", "mamut", null ) + .addCommand( "destroy the world", "all", "Make a disgusting coffee for breakfast. \n" + + "For this one, you are by yourself. Good luck and know that we are with you. This is a long line. Hopefully long engouh.\n" + + "Hey, what about we add:\n" + + "tabulation1\ttabulation2\n" + + "lalallala\ttrollololo." ) + .addCommand( "ride the dragon", "all", "Go to work by bike." ) + .addCommand( "Punish", "all", "Go to work by parisian metro." ) + .addCommand( "make some coffee", "mamut", null ) + .addCommand( "make some coffee", "trackscheme", "Make a decent coffee." ) + .get(); + } + + /** + * Utility class that is used to build a map of {@link Command}s to their + * description. + * + * @author Jean-Yves Tinevez + */ + static class CommandDescriptionBuilder + { + + /** + * The map of {@link Command} to description of what the command does. + */ + private final HashMap< Command, String > map; + + public CommandDescriptionBuilder() + { + this.map = new HashMap<>(); + } + + public CommandDescriptionBuilder addCommand( final String name, final String context, final String description ) + { + final Command command = new Command( name, context ); + map.put( command, description ); + return this; + } + + /** + * Returns the map of {@link Command} to description of what the command + * does. + * + * @return a new immutable map + */ + public Map< Command, String > get() + { + return Collections.unmodifiableMap( map ); + } + + } + + /** + * Launch the application. + * + * @param args + * + * @throws UnsupportedLookAndFeelException + * @throws IllegalAccessException + * @throws InstantiationException + * @throws ClassNotFoundException + */ + public static void main( final String[] args ) throws ClassNotFoundException, InstantiationException, IllegalAccessException, UnsupportedLookAndFeelException + { + UIManager.setLookAndFeel( UIManager.getSystemLookAndFeelClassName() ); + EventQueue.invokeLater( new Runnable() + { + @Override + public void run() + { + try + { + final JFrame frame = new JFrame( "Behaviour Key bindings editor" ); + final VisualEditorPanel editorPanel = new VisualEditorPanel( getDemoConfig(), getDemoCommands() ); + editorPanel.modelChangedListeners().add( () -> System.out.println( "Model (GUI only) changed @ " + new java.util.Date().toString() ) ); + editorPanel.configCommittedListeners().add( () -> System.out.println( "Config changed, Apply button pressed @ " + new java.util.Date().toString() ) ); + SwingUtilities.updateComponentTreeUI( VisualEditorPanel.fileChooser ); + frame.getContentPane().add( editorPanel ); + frame.pack(); + frame.setVisible( true ); + } + catch ( final Exception e ) + { + e.printStackTrace(); + } + } + } ); + } +} diff --git a/src/test/java/org/scijava/ui/behaviour/io/json/JsonInteractiveTest.java b/src/test/java/org/scijava/ui/behaviour/io/json/JsonInteractiveTest.java index 27115f9..7953859 100644 --- a/src/test/java/org/scijava/ui/behaviour/io/json/JsonInteractiveTest.java +++ b/src/test/java/org/scijava/ui/behaviour/io/json/JsonInteractiveTest.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour.io.json; import java.io.BufferedReader; diff --git a/src/test/java/org/scijava/ui/behaviour/io/yaml/YamlInteractiveTest.java b/src/test/java/org/scijava/ui/behaviour/io/yaml/YamlInteractiveTest.java index 8c5ffe6..4dcb4ac 100644 --- a/src/test/java/org/scijava/ui/behaviour/io/yaml/YamlInteractiveTest.java +++ b/src/test/java/org/scijava/ui/behaviour/io/yaml/YamlInteractiveTest.java @@ -1,3 +1,32 @@ +/*- + * #%L + * Configurable key and mouse event handling + * %% + * Copyright (C) 2015 - 2023 Max Planck Institute of Molecular Cell Biology + * and Genetics. + * %% + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * 1. Redistributions of source code must retain the above copyright notice, + * this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR CONTRIBUTORS BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * #L% + */ package org.scijava.ui.behaviour.io.yaml; import java.io.BufferedReader;