io.github.interacto.jfx.interaction.JfxInteraction Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of interacto-javafx Show documentation
Show all versions of interacto-javafx Show documentation
The JavaFX implementation of the Interacto library
The newest version!
/*
* Interacto
* Copyright (C) 2020 Arnaud Blouin
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
package io.github.interacto.jfx.interaction;
import io.github.interacto.fsm.FSM;
import io.github.interacto.fsm.OutputState;
import io.github.interacto.interaction.InteractionData;
import io.github.interacto.interaction.InteractionImpl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.collections.ObservableSet;
import javafx.collections.SetChangeListener;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.ScrollEvent;
import javafx.scene.input.TouchEvent;
import javafx.stage.Window;
/**
* The base class for JavaFX interactions.
* @param The type of interaction data exposed by the interaction
* @param The type of the interaction FSM
*/
public abstract class JfxInteraction> extends InteractionImpl implements FSMDataHandler {
protected final ObservableSet registeredNodes;
protected final ObservableSet registeredWindows;
protected final List> additionalNodes;
/** The interaction data */
protected final D data;
private EventHandler scrollHandler;
private EventHandler mouseHandler;
private EventHandler keyHandler;
private EventHandler actionHandler;
private EventHandler touchHandler;
private final SetChangeListener nodesHandler = change -> {
if(change.wasAdded()) {
onNewNodeRegistered(change.getElementAdded());
}
if(change.wasRemoved()) {
onNodeUnregistered(change.getElementRemoved());
}
};
private final SetChangeListener windowsHandler = change -> {
if(change.wasAdded()) {
onNewWindowRegistered(change.getElementAdded());
}
if(change.wasRemoved()) {
onWindowUnregistered(change.getElementRemoved());
}
};
private final ListChangeListener addNodesHandler = change -> {
while(change.next()) {
if(change.wasAdded()) {
change.getAddedSubList().forEach(elt -> onNewNodeRegistered(elt));
}
if(change.wasRemoved()) {
change.getRemoved().forEach(elt -> onNodeUnregistered(elt));
}
}
};
protected JfxInteraction(final F fsm) {
super(fsm);
data = createDataObject();
registeredNodes = FXCollections.observableSet();
registeredWindows = FXCollections.observableSet();
additionalNodes = new ArrayList<>();
// Listener to any changes in the list of registered nodes
registeredNodes.addListener(nodesHandler);
// Listener to any changes in the list of registered windows
registeredWindows.addListener(windowsHandler);
}
protected abstract D createDataObject();
@Override
public void reinitData() {
data.flush();
}
@Override
public D getData() {
return data;
}
@Override
protected void updateEventsRegistered(final OutputState newState, final OutputState oldState) {
// Do nothing when the interaction has only two nodes: init node and terminal node (this is a single-event interaction).
if(newState == oldState || fsm.getStates().size() == 2) {
return;
}
final List> currEvents = getCurrentAcceptedEvents(newState);
final List> events = getEventTypesOf(oldState);
final List> eventsToRemove = new ArrayList<>(events);
final List> eventsToAdd = new ArrayList<>(currEvents);
eventsToRemove.removeAll(currEvents);
eventsToAdd.removeAll(events);
registeredNodes.forEach(n -> {
eventsToRemove.forEach(type -> unregisterEventToNode(type, n));
eventsToAdd.forEach(type -> registerEventToNode(type, n));
});
registeredWindows.forEach(w -> {
eventsToRemove.forEach(type -> unregisterEventToWindow(type, w));
eventsToAdd.forEach(type -> registerEventToWindow(type, w));
});
additionalNodes.forEach(nodes -> nodes.forEach(n -> {
eventsToRemove.forEach(type -> unregisterEventToNode(type, n));
eventsToAdd.forEach(type -> registerEventToNode(type, n));
}));
}
protected List> getCurrentAcceptedEvents(final OutputState state) {
return this.getEventTypesOf(state);
}
protected List> getEventTypesOf(final OutputState state) {
return state.getTransitions().stream().map(t -> t.getAcceptedEvents()).flatMap(s -> s.stream()).distinct().map(o -> (EventType) o).
collect(Collectors.toList());
}
protected void onNodeUnregistered(final Node node) {
getEventTypesOf(fsm.getCurrentState()).forEach(type -> unregisterEventToNode(type, node));
}
protected void onWindowUnregistered(final Window window) {
getEventTypesOf(fsm.getCurrentState()).forEach(type -> unregisterEventToWindow(type, window));
}
protected void onNewNodeRegistered(final Node node) {
getEventTypesOf(fsm.getCurrentState()).forEach(type -> registerEventToNode(type, node));
}
protected void onNewWindowRegistered(final Window window) {
getEventTypesOf(fsm.getCurrentState()).forEach(type -> registerEventToWindow(type, window));
}
@Override
protected void consumeEvent(final Event event) {
event.consume();
}
/**
* Permits to listen any change in the content of the given list.
* This is dynamic: on new child nodes, the interaction registers to them.
* On child removals, the interaction unregisters to them.
* @param nodes The list of nodes that will be observed by the interaction.
*/
public void registerToObservableNodeList(final ObservableList nodes) {
if(nodes != null) {
additionalNodes.add(nodes);
if(!nodes.isEmpty()) {
final List> events = getEventTypesOf(fsm.getCurrentState());
nodes.forEach(node -> events.forEach(type -> registerEventToNode(type, node)));
}
// Listener to any changes in the list of registered windows
nodes.addListener(addNodesHandler);
}
}
protected void unregisterEventToNode(final EventType eventType, final Node node) {
if(eventType == ActionEvent.ACTION) {
unregisterActionHandler(node);
return;
}
if(eventType.getSuperType() == MouseEvent.ANY || eventType.getSuperType().getSuperType() == MouseEvent.ANY) {
node.removeEventHandler((EventType) eventType, getMouseHandler());
return;
}
if(eventType == ScrollEvent.SCROLL) {
node.removeEventHandler(ScrollEvent.SCROLL, getScrolledHandler());
return;
}
if(eventType.getSuperType() == KeyEvent.ANY) {
node.removeEventHandler((EventType) eventType, getKeyHandler());
return;
}
if(eventType.getSuperType() == TouchEvent.ANY) {
node.removeEventHandler((EventType) eventType, getTouchHandler());
}
}
protected void registerEventToNode(final EventType eventType, final Node node) {
if(eventType == ActionEvent.ACTION) {
registerActionHandler(node);
return;
}
if(eventType.getSuperType() == MouseEvent.ANY || eventType.getSuperType().getSuperType() == MouseEvent.ANY) {
node.addEventHandler((EventType) eventType, getMouseHandler());
return;
}
if(eventType == ScrollEvent.SCROLL) {
node.addEventHandler(ScrollEvent.SCROLL, getScrolledHandler());
return;
}
if(eventType.getSuperType() == KeyEvent.ANY) {
node.addEventHandler((EventType) eventType, getKeyHandler());
return;
}
if(eventType.getSuperType() == TouchEvent.ANY) {
node.addEventHandler((EventType) eventType, getTouchHandler());
}
}
protected void unregisterEventToWindow(final EventType eventType, final Window window) {
if(eventType == ActionEvent.ACTION) {
window.removeEventHandler(ActionEvent.ACTION, getActionHandler());
return;
}
if(eventType.getSuperType() == MouseEvent.ANY || eventType.getSuperType().getSuperType() == MouseEvent.ANY) {
window.removeEventHandler((EventType) eventType, getMouseHandler());
return;
}
if(eventType == ScrollEvent.SCROLL) {
window.removeEventHandler(ScrollEvent.SCROLL, getScrolledHandler());
return;
}
if(eventType.getSuperType() == KeyEvent.ANY) {
window.removeEventHandler((EventType) eventType, getKeyHandler());
return;
}
if(eventType.getSuperType() == TouchEvent.ANY) {
window.removeEventHandler((EventType) eventType, getTouchHandler());
}
}
protected void registerEventToWindow(final EventType eventType, final Window window) {
if(eventType == ActionEvent.ACTION) {
window.addEventHandler(ActionEvent.ACTION, getActionHandler());
return;
}
if(eventType.getSuperType() == MouseEvent.ANY || eventType.getSuperType().getSuperType() == MouseEvent.ANY) {
window.addEventHandler((EventType) eventType, getMouseHandler());
return;
}
if(eventType == ScrollEvent.SCROLL) {
window.addEventHandler(ScrollEvent.SCROLL, getScrolledHandler());
return;
}
if(eventType.getSuperType() == KeyEvent.ANY) {
window.addEventHandler((EventType) eventType, getKeyHandler());
return;
}
if(eventType.getSuperType() == TouchEvent.ANY) {
window.addEventHandler((EventType) eventType, getTouchHandler());
}
}
protected void registerActionHandler(final Node node) {
node.addEventHandler(ActionEvent.ACTION, getActionHandler());
}
protected void unregisterActionHandler(final Node node) {
node.removeEventHandler(ActionEvent.ACTION, getActionHandler());
}
protected EventHandler getActionHandler() {
if(actionHandler == null) {
actionHandler = evt -> processEvent(evt);
}
return actionHandler;
}
protected EventHandler getMouseHandler() {
if(mouseHandler == null) {
mouseHandler = evt -> processEvent(evt);
}
return mouseHandler;
}
protected EventHandler getTouchHandler() {
if(touchHandler == null) {
touchHandler = evt -> processEvent(evt);
}
return touchHandler;
}
protected EventHandler getKeyHandler() {
if(keyHandler == null) {
keyHandler = evt -> processEvent(evt);
}
return keyHandler;
}
private EventHandler getScrolledHandler() {
if(scrollHandler == null) {
scrollHandler = evt -> processEvent(evt);
}
return scrollHandler;
}
public void registerToNodes(final Collection widgets) {
if(widgets != null) {
registeredNodes.addAll(widgets.stream().filter(Objects::nonNull).collect(Collectors.toList()));
}
}
public void unregisterFromNodes(final Collection widgets) {
if(widgets != null) {
registeredNodes.removeAll(widgets.stream().filter(Objects::nonNull).collect(Collectors.toList()));
}
}
public void registerToWindows(final Collection windows) {
if(windows != null) {
registeredWindows.addAll(windows.stream().filter(Objects::nonNull).collect(Collectors.toList()));
}
}
public void unregisterFromWindows(final Collection windows) {
if(windows != null) {
registeredWindows.removeAll(windows.stream().filter(Objects::nonNull).collect(Collectors.toList()));
}
}
public Set getRegisteredNodes() {
return Collections.unmodifiableSet(registeredNodes);
}
public Set getRegisteredWindows() {
return Collections.unmodifiableSet(registeredWindows);
}
@Override
protected boolean isEventsOfSameType(final Event evt1, final Event evt2) {
return evt1 != null && evt2 != null && evt1.getEventType() == evt2.getEventType();
}
@Override
protected void runInUIThread(final Runnable runnable) {
if(Platform.isFxApplicationThread()) {
runnable.run();
}else {
Platform.runLater(runnable);
}
}
@Override
public void uninstall() {
scrollHandler = null;
mouseHandler = null;
keyHandler = null;
actionHandler = null;
registeredNodes.forEach(n -> onNodeUnregistered(n));
registeredNodes.clear();
registeredWindows.forEach(n -> onWindowUnregistered(n));
registeredWindows.clear();
additionalNodes.forEach(adds -> adds.removeListener(addNodesHandler));
additionalNodes.clear();
registeredNodes.removeListener(nodesHandler);
registeredWindows.removeListener(windowsHandler);
super.uninstall();
}
}