io.github.palexdev.mfxcore.events.WhenEvent Maven / Gradle / Ivy
Show all versions of materialfx-all Show documentation
/*
* Copyright (C) 2023 Parisi Alessandro - [email protected]
* This file is part of MaterialFX (https://github.com/palexdev/MaterialFX)
*
* MaterialFX is free software: you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 3 of the License,
* or (at your option) any later version.
*
* MaterialFX 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 Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with MaterialFX. If not, see .
*/
package io.github.palexdev.mfxcore.events;
import io.github.palexdev.mfxcore.base.TriConsumer;
import io.github.palexdev.mfxcore.behavior.DisposableAction;
import io.github.palexdev.mfxcore.collections.WeakHashSet;
import io.github.palexdev.mfxcore.observables.When;
import javafx.beans.value.ObservableValue;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Node;
import java.lang.ref.WeakReference;
import java.util.Optional;
import java.util.WeakHashMap;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* In the veins of the great and so useful {@link When} construct, this class, strongly inspired by it and its implementations,
* allows to do pretty much the same things but on {@link Event}s.
*
* This construct can be read as "When an event of a given type, occurs on a given node, then do this" or "Intercept events
* of a given type on a given node, then do this".
*
* Just like the {@link When} construct, you can specify an action on the intercept event, a condition under which process
* or not the event, an action to perform if the condition was not met, and of course you can create a handler that is
* one shot, in other words automatically disposed after the first time its triggered.
*
* The one thing that is missing as of now is the possibility of run the set action immediately (the executeNow() functionality)
* as the action needs an event, and the only way would be to generate synthetic events, which is not easy and may not work
* as intended.
*
* Another difference is that, handlers can be registered as filters too, you can specify such behavior using {@link #asFilter()}.
*
* To activate this construct after you've set everything make sure to call {@link #register()}.
*
*
* {@code
* // A full example could be...
* MFXButton bnt = new MFXButton("Click me");
* WhenEvent.intercept(btn, MouseEvent.MOUSE_CLICKED)
* .condition(e -> e.getButton() == MouseButton.PRIMARY)
* .process(e -> System.out.println("Button was clicked"))
* .otherwise((w, e) -> {
* // What happens here is that, if the pressed mouse button was not the primary
* // then we print it to the console, and then we dispose the construct,
* // meaning that further events won't be processed
* // Note that the 'w' parameter in the lambda is a WeakReference to the construct,
* // so first we make sure it was not garbage collected (not null)
* System.out.println("Not the primary button!");
* WhenEvent we = w.get();
* if (we != null) we.dispose();
* })
* .asFilter()
* .oneShot()
* .register();
*
* // More details
* // 1) Note that the asFilter functionality can be quite useful. In fact, you can even create
* // a filter that consumes the events, thus avoiding other constructs or handlers to process the same type of events
* // 2) This specific example I would call it as a "full one shot" haha. Check this, if you press the
* // PRIMARY button, the oneShot() will be taken into account, so the construct will only run once and then disposed
* // If you press any other button, you enter the "otherwise" action, and there it is also disposed.
* // So, this specific example will run once and only once
* }
*
*/
public class WhenEvent implements DisposableAction {
//================================================================================
// Properties
//================================================================================
protected static final WhenEventsMap whens = new WhenEventsMap();
private Node node;
private EventType eventType;
private EventHandler handler;
private Consumer action;
private Function condition = e -> true;
private BiConsumer>, T> otherwise = (w, e) -> {};
private boolean oneShot = false;
private boolean asFilter = false;
private RegUnRegWrapper rurWrapper;
private boolean active = false;
//================================================================================
// Constructors
//================================================================================
public WhenEvent(Node node, EventType eventType) {
this.node = node;
this.eventType = eventType;
}
public static WhenEvent intercept(Node node, EventType eventType) {
return new WhenEvent<>(node, eventType);
}
//================================================================================
// Methods
//================================================================================
/**
* Sets the {@link Consumer} used to "process" any given event.
*/
public WhenEvent process(Consumer action) {
this.action = action;
return this;
}
/**
* Sets the condition under which an event will be passed to the action specified by {@link #process(Consumer)}.
*
* @see #otherwise(BiConsumer)
*/
public WhenEvent condition(Function condition) {
this.condition = condition;
return this;
}
/**
* Allows you to specify an action to run for events that fails the check set by {@link #condition(Function)}.
*/
public WhenEvent otherwise(BiConsumer>, T> otherwise) {
this.otherwise = otherwise;
return this;
}
/**
* Responsible for building the {@link EventHandler} with all the given parameters and then add it on the specified
* Node. This method won't run if the construct was disposed before, or if the handler is not null (meaning that it
* was already registered before).
*/
public WhenEvent register() {
if (isDisposed() || handler != null) return this;
rurWrapper = new RegUnRegWrapper();
if (oneShot) {
handler = e -> {
if (condition.apply(e)) {
action.accept(e);
dispose();
} else {
otherwise.accept(asWeak(), e);
}
};
} else {
handler = e -> {
if (condition.apply(e)) {
action.accept(e);
} else {
otherwise.accept(asWeak(), e);
}
};
}
doRegister();
return this;
}
/**
* Invoked by {@link #register()} if everything went well. Here, the construct is added to a static Map that retains
* all the built constructs, the mapping is as follows: {@code Node -> Set>}.
*
* Finally, the built {@link EventHandler} is added on the specified Node.
*/
protected void doRegister() {
WeakHashSet> set = whens.computeIfAbsent(node, n -> new WeakHashSet<>());
set.add(this);
rurWrapper.reg();
active = true;
}
/**
* @return whether the construct is "one-shot"
* @see #oneShot()
*/
public boolean isOneShot() {
return oneShot;
}
/**
* Sets the construct as 'one-shot', meaning that once an event occurs the first time and the action is executed,
* the construct will automatically dispose itself.
*/
public WhenEvent oneShot() {
this.oneShot = true;
return this;
}
/**
* @return whether the built {@link EventHandler} will be registered as a simple handler or filter
* @see #asFilter()
*/
public boolean isFilter() {
return asFilter;
}
/**
* Sets a flag that will make the built {@link EventHandler} be registered as a filter.
*/
public WhenEvent asFilter() {
this.asFilter = true;
return this;
}
/**
* Unregisters the {@link EventHandler} from the node, sets everything to null, and removes the construct from
* the "global" map.
*/
@Override
public void dispose() {
if (node != null) {
if (handler != null) {
rurWrapper.unReg();
handler = null;
rurWrapper = null;
}
handleMapDisposal();
eventType = null;
node = null;
active = false;
}
}
/**
* Calls {@link #dispose()} on the given {@code WhenEvent} construct.
*/
public static void dispose(WhenEvent> w) {
if (w != null) w.dispose();
}
/**
* Calls {@link #dispose(WhenEvent)} on each of the given {@code WhenEvent} constructs.
*/
public static void dispose(WhenEvent>... whens) {
for (WhenEvent> w : whens) w.dispose();
}
/**
* @return whether the construct is active and not disposed, the flag is set if {@link #doRegister()} run successfully
*/
public boolean isActive() {
return active;
}
/**
* @return whether this construct has been disposed before. By default, checks if the given {@link EventType} and node
* are null
*/
public boolean isDisposed() {
return node == null &&
eventType == null;
}
/**
* @return the total number of existing {@code WhenEvent} constructs for a given node
*/
public static int size(Node node) {
return Optional.ofNullable(whens.get(node))
.map(WeakHashSet::size)
.orElse(0);
}
/**
* @return the total number of existing {@code WhenEvent} constructs for any registered {@link ObservableValue}
*/
public static int totalSize() {
return whens.keySet().stream()
.mapToInt(WhenEvent::size)
.sum();
}
/**
* @return this construct wrapped in a {@link WeakReference}
*/
protected final WeakReference> asWeak() {
return new WeakReference<>(this);
}
/**
* This is called when handling the construct's disposal.
* The aforementioned Map used to store the built {@code WhenEvent} constructs, uses this mapping:
*
* {@code
* [key -> value] = [Node -> WeakHashSet>]
* }
*
* This is because {@code WhenEvent} allows to register multiple constructs on a single node,
* for this reason, there are several things to consider on disposal:
* 1) There is a non-null Set mapped to the current node
*
2) The construct can be removed from the Set without any null check, but after the removal
* it's good to check whether the Set is now empty
*
3) In such case, we can also remove the mapping from the Map.
*/
protected final void handleMapDisposal() {
WeakHashSet> set = whens.get(node);
if (set == null) return;
set.remove(this);
if (set.isEmpty()) whens.remove(node);
}
//================================================================================
// Internal Classes
//================================================================================
public static class WhenEventsMap extends WeakHashMap>> {}
/**
* Utility internal class that allows to remove some ifs when registering/unregistering the {@link EventHandler}
* on the node.
*/
protected class RegUnRegWrapper {
private final TriConsumer, EventHandler> reg;
private final TriConsumer, EventHandler> unReg;
protected RegUnRegWrapper() {
if (asFilter) {
reg = Node::addEventFilter;
unReg = Node::removeEventFilter;
} else {
reg = Node::addEventHandler;
unReg = Node::removeEventHandler;
}
}
public void reg() {
reg.accept(node, eventType, handler);
}
public void unReg() {
unReg.accept(node, eventType, handler);
}
}
}