io.github.palexdev.mfxcore.controls.SkinBase 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.controls;
import io.github.palexdev.mfxcore.behavior.BehaviorBase;
import io.github.palexdev.mfxcore.behavior.DisposableAction;
import io.github.palexdev.mfxcore.behavior.WithBehavior;
import io.github.palexdev.mfxcore.events.WhenEvent;
import io.github.palexdev.mfxcore.observables.When;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
/**
* Extension of {@link javafx.scene.control.SkinBase} used by components that want a seamless integration with the new Behavior API.
* The skin is responsible for initializing the behavior as needed.
*
* This integration defines a specific and recommended to follow strategy to develop UI components. There are three main parts:
* - the Control, which is the component, the class has all its specs
*
- the View, defines the component's look/layout
*
- the Behavior, defines what the component can do and how
*
* So, as you may guess, there must be an 'infrastructure' that makes all these three parts communicate with each other.
* The behavior may need to be connected with the specs of the component, as well as with the subcomponents defined in
* its view.
*
* {@link Control} and {@link Labeled} are a bridge between these three parts. They retain the reference of the current
* built behavior object, which can be retrieved via {@link WithBehavior#getBehavior()}. They are responsible for calling
* {@link #initBehavior(BehaviorBase)} every time the behavior changes, as well as dispose it of course.
*
* The behavior is specifically responsible for managing user input, in other words, event handlers and filters.
* On the other hand, the skin is responsible for handling listeners related to the control's properties.
*
* Essentially, this follows the MVC (Model-View-Controller) pattern applied to UI controls. You have the flexibility to
* change either the skin or the behavior at any time, and the component will remain functional
* without requiring extensive code modifications.
* This high degree of modularity, given by the pattern, allows users to customize such components with ease.
*
* In all of this, the skin plays a central role. Because user input originates from UI elements,
* which are part of the view (the skin), it is responsible for creating the handlers that will invoke behavior methods.
* Additionally, the view (the skin) must respond to any changes in the control (essentially the model),
* which means it also adds the necessary listeners to monitor property changes.
*
* The development flow for controls with the new Behavior and Skin API would be:
* - Have a components that extends either {@link Control}, {@link Labeled} or any of their subclasses
*
- Having an implementation of this base Skin, either one of the already provided or a custom one
*
- Having a behavior class and set the provider on the component
*
- Override the {@link #initBehavior(BehaviorBase)} to initialize the behavior if needed
*
- Initialization and changes to the behavior provider are automatically handled, hassle-free
*/
public abstract class SkinBase, B extends BehaviorBase> extends javafx.scene.control.SkinBase {
//================================================================================
// Properties
//================================================================================
private List listeners = new ArrayList<>();
//================================================================================
// Constructors
//================================================================================
protected SkinBase(C control) {super(control);}
//================================================================================
// Abstract Methods
//================================================================================
/**
* This is responsible for initializing the behavior every time it changes, the given parameter
* is the current uninitialized behavior.
*/
protected abstract void initBehavior(B behavior);
//================================================================================
// Methods
//================================================================================
/**
* Convenience method for {@link #computeMinWidth(double, double, double, double, double)} without the need to pass
* the insets.
*/
protected double computeMinWidth(double height) {
return computeMinWidth(height, snappedTopInset(), snappedRightInset(), snappedBottomInset(), snappedLeftInset());
}
/**
* Convenience method for {@link #computeMinHeight(double, double, double, double, double)} without the need to pass
* the insets.
*/
protected double computeMinHeight(double width) {
return computeMinHeight(width, snappedTopInset(), snappedRightInset(), snappedBottomInset(), snappedLeftInset());
}
/**
* Convenience method for {@link #computePrefWidth(double, double, double, double, double)} without the need to pass
* the insets.
*/
protected double computePrefWidth(double height) {
return computePrefWidth(height, snappedTopInset(), snappedRightInset(), snappedBottomInset(), snappedLeftInset());
}
/**
* Convenience method for {@link #computePrefHeight(double, double, double, double, double)} without the need to pass
* the insets.
*/
protected double computePrefHeight(double width) {
return computePrefHeight(width, snappedTopInset(), snappedRightInset(), snappedBottomInset(), snappedLeftInset());
}
/**
* Convenience method for {@link #computeMaxWidth(double, double, double, double, double)} without the need to pass
* the insets.
*/
protected double computeMaxWidth(double height) {
return computeMaxWidth(height, snappedTopInset(), snappedRightInset(), snappedBottomInset(), snappedLeftInset());
}
/**
* Convenience method for {@link #computeMaxHeight(double, double, double, double, double)} without the need to pass
* the insets.
*/
protected double computeMaxHeight(double width) {
return computeMaxHeight(width, snappedTopInset(), snappedRightInset(), snappedBottomInset(), snappedLeftInset());
}
//================================================================================
// Delegate Methods
//================================================================================
/**
* Delegate for {@link BehaviorBase#register(WhenEvent[])}.
*
* Note this will do nothing if the return value of {@link #getBehavior()} is {@code null}.
*/
@SuppressWarnings({"rawtypes", "unchecked"})
public void events(WhenEvent... wes) {
Optional.ofNullable(getBehavior()).ifPresent(b -> b.register(wes));
}
/**
* While making skins for MaterialFX I always make a great use of {@link When} constructs, simply because they are so
* useful and easy to use there is no point in not doing it. This however comes with a little issue, the more
* constructs a skin uses the longer is the disposal code. A simple solution is to pass the instances to this method
* (just wrap all of them as args), which will store them in a {@code List} so that the disposal can be done
* automatically without having every single construct instance in the class.
*
* Not only that, I'm actually so happy with the work done on {@link When} that I decided to create an equivalent
* for {@code Events} too, see {@link WhenEvent}, and a delegate method {@link #events(WhenEvent[])}
*
* Note: one-shot constructs (see {@link When#oneShot(boolean)} or {@link WhenEvent#oneShot()})
* do not need to be registered as they will be automatically disposed on their first trigger.
* Doing so brings no harm, it's just useless.
*/
public void listeners(When>... listeners) {
for (When> w : listeners) {
if (!w.isActive()) w.listen();
this.listeners.add(w);
}
}
//================================================================================
// Overridden Methods
//================================================================================
@Override
public void dispose() {
listeners.forEach(DisposableAction::dispose);
listeners.clear();
listeners = null;
super.dispose();
}
//================================================================================
// Getters
//================================================================================
/**
* Delegate for {@link WithBehavior#getBehavior()}.
*
* Since this is called on the component, the return value could also be null if the behavior
* provider was not set, or produces null references.
*/
protected B getBehavior() {return getSkinnable().getBehavior();}
}