Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 8 years ago.
Improve this question
RESUME
There is a time I have been studying the new JavaFX technology, and I've been faced with some barriers in creating custom controls. I learned how to use CSS to customize my controls, and then I came across the case of customizing controls using Skin and SkinBase.
Seen such resources, it was easy to initiate and complete the creation of new controls with visual and specific functionalities. However, personalization, that is, the visual and functional editing of existing controls on JavaFX library becomes somewhat more complicated. In many cases the programmer is forced to use resources that are only available in private packages from Oracle (com.sun ...), which would become a bad practice, resulting in the production of software not maintainable.
Imagine the example where we want to customize the ScrollBar control. It is possible to change its appearance completely using CSS. However, the desire of adding new behaviors to such control involves creating a new Theme from ZERO, without any reuse of ScrollBarSkin, because it is in the private Oracle package. This forces the programmer having to reimplement the logics that have already been implemented, such as the positioning of thumb, the update of values, what happens when you click the track, among many other things. In stubbornness to create a subtype of ScrollBarSkin it is seen that there are many important methods that have been encapsulated as not being overwritten, leaving you to have to compulsorily reimplement the existing logic.
What appears, at least, is that many important components for customizing a control are caged, causing you to have to use a single path to reach them (still limited).
EXAMPLE (from theory to practice)
To illustrate what I mean by this and emphasize in the conclusion of this question in this community, we will briefly try to customize the ScrollBar existing in JavaFX package. My intention is to create a scrollbar to look like this:
LINK1 & LINK2
With regard to the behavior of the scrollbar, while clicking on it’s arrows, they should move a little, returning to their positions when the mouse button is released. When you pass the mouse over the thumb and arrows, they should light up. By clicking in the track or pushing any of our scrollbar arrows, the thumb must moves smoothly, in animated form and not abruptly.
So let's start with our experiment. First, let's create a CSS file that will serve as a definition of some appearances:
.scroll-bar {
-fx-skin: "packageA.packageB.ScrollBarSkin2";
-fx-background-color: rgb(66,64,64);
-fx-border-color: rgb(96,96,98);
-fx-border-width: 1px;
}
.scroll-bar > .thumb {
-fx-background-insets: 5 0 5 0;
-fx-shape: "M0 0c3,0 7,0 10,0l0 6c-3,0 -7,0 -10,0l0 -6z";
}
.scroll-bar > .increment-button > .increment-arrow ,
.scroll-bar > .decrement-button > .decrement-arrow ,
.scroll-bar > .thumb {
-fx-background-color: rgb(254,254,254);
}
/* ------------------------------------------------------------ BUTTONS */
.scroll-bar:horizontal > .increment-button ,
.scroll-bar:horizontal > .decrement-button ,
.scroll-bar:vertical > .increment-button ,
.scroll-bar:vertical > .decrement-button {
-fx-padding: 4px;
}
.scroll-bar:horizontal > .increment-button:hover ,
.scroll-bar:horizontal > .decrement-button:hover ,
.scroll-bar:vertical > .increment-button:hover ,
.scroll-bar:vertical > .decrement-button:hover {
-fx-background-color: null;
}
/* ------------------------------------------------------------ BUTTONS SHAPES */
.scroll-bar:horizontal > .increment-button > .increment-arrow {
-fx-shape: "m -745.01097,-1519.0664 -156.95606,90.6186 -156.95607,90.6186 0,-181.2372 0,-181.2372 156.95608,90.6186 z";
}
.scroll-bar:horizontal > .decrement-button > .decrement-arrow {
-fx-shape: "m -1455.5694,-1550.495 153.1056,-88.3956 153.1056,-88.3955 0,176.7911 0,176.7911 -153.1056,-88.3955 z";
}
.scroll-bar:vertical > .increment-button > .increment-arrow {
-fx-shape: "m -1334.2856,-2204.9669 85.446,147.9968 85.446,147.9968 -170.892,0 -170.8921,0 85.446,-147.9968 z";
}
.scroll-bar:vertical > .decrement-button > .decrement-arrow {
-fx-shape: "m -1234.2856,-1096.134 -94.0582,-162.9135 -94.0582,-162.9136 188.1164,0 188.1163,0 -94.0582,162.9136 z";
}
Observing our CSS file, we can see that we chose one of the 3 existing ways to connect our control to our skin, and we use the definition of -fx-skin property in our CSS file. Now we need to link our CSS file created with our control. This is done in Java code where we just have to set the CSS style sheet our control:
scrollBar.getStylesheets().setAll(this.getClass().getResource("scroll-bar-style.css").toExternalForm());
Note: To run the example, you need to have a test class with a main
method to create and place a ScrollBar in a scene graph.
We already have our control linked to our Skin, but we have not really created the Skin. Creating or editing controls takes into account that the controls themselves, ie, the objects that extend Control, are considered part of the model of the MVC pattern, existing in JavaFX. The part of the control and visualization is originally divided (do not know for what reason) into two parts. One, called the Skin, and the other, called Behavior. Both are existing interfaces in JavaFX with Skin representing the Visualization part, and Behavior being the part of Control. Unfortunately (do not know why!) Behavior is considered a private part of the JavaFX package, so the users developers of JavaFX are pushed to treat the part of Visualization and Control inside the Skin, which would be the part initially set for Visualization only. Having said all these strange things, let's create our Skin class:
// Public packages.
import javafx.animation.Animation;
import javafx.animation.TranslateTransition;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.EventHandler;
import javafx.geometry.Orientation;
import javafx.scene.control.ScrollBar;
import javafx.scene.effect.DropShadow;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.util.Duration;
// ... And here we have a private package.
import com.sun.javafx.scene.control.skin.ScrollBarSkin;
public class ScrollBarSkin2 extends ScrollBarSkin
{
// #########################################################################################################
// INSTANCES
// #########################################################################################################
// SUBSTRUCTURES
private StackPane thumb;
private StackPane track;
private Region incButton;
private Region decButton;
private Region incArrow;
private Region decArrow;
// EFFECTS
private DropShadow drop_thumb;
private DropShadow drop_inc;
private DropShadow drop_dec;
// ANIMATIONS
private BindableTransition aniTran_thumbDrop;
private BindableTransition aniTran_incDrop;
private BindableTransition aniTran_decDrop;
private TranslateTransition aniTran_setaInc;
private TranslateTransition aniTran_setaDec;
// #########################################################################################################
// CONSTRUCTORS
// #########################################################################################################
public ScrollBarSkin2(ScrollBar scrollbar)
{
super(scrollbar);
this.thumb = (StackPane) this.getSkinnable().lookup(".thumb");
this.track = (StackPane) this.getSkinnable().lookup(".track");
this.incButton = (Region) this.getSkinnable().lookup(".increment-button");
this.decButton = (Region) this.getSkinnable().lookup(".decrement-button");
this.incArrow = (Region) this.getSkinnable().lookup(".increment-arrow");
this.decArrow = (Region) this.getSkinnable().lookup(".decrement-arrow");
this.configureSubstructures();
this.addEvents();
}
/** Sets the substructures obtained.*/
protected void configureSubstructures()
{
// ####################
// THUMB
// ####################
this.drop_thumb = new DropShadow();
this.drop_thumb.setRadius(0);
this.drop_thumb.setColor(Color.WHITE);
this.thumb.setEffect(this.drop_thumb);
this.aniTran_thumbDrop = new BindableTransition(Duration.millis(250));
// ####################
// INCREMENT BUTTON
// ####################
this.drop_inc = new DropShadow();
this.drop_inc.setRadius(0);
this.drop_inc.setColor(Color.WHITE);
this.incArrow.setEffect(this.drop_inc);
this.aniTran_incDrop = new BindableTransition(Duration.millis(250));
// ####################
// DECREMENT BUTTON
// ####################
this.drop_dec = new DropShadow();
this.drop_dec.setRadius(0);
this.drop_dec.setColor(Color.WHITE);
this.decArrow.setEffect(this.drop_dec);
this.aniTran_decDrop = new BindableTransition(Duration.millis(250));
// ####################
// ARROWS
// ####################
this.aniTran_setaInc = new TranslateTransition(Duration.millis(100) , this.incArrow);
this.aniTran_setaDec = new TranslateTransition(Duration.millis(100) , this.decArrow);
if(this.getSkinnable().getOrientation() == Orientation.HORIZONTAL)
{
this.aniTran_setaInc.setFromX(this.incArrow.getLayoutX());
this.aniTran_setaInc.setToX(this.incArrow.getLayoutX() + 2);
this.aniTran_setaDec.setFromX(this.incArrow.getLayoutX());
this.aniTran_setaDec.setToX(this.incArrow.getLayoutX() - 2);
}
else if(this.getSkinnable().getOrientation() == Orientation.VERTICAL)
{
this.aniTran_setaInc.setFromY(this.incArrow.getLayoutY());
this.aniTran_setaInc.setToY(this.incArrow.getLayoutY() - 2);
this.aniTran_setaDec.setFromY(this.incArrow.getLayoutY());
this.aniTran_setaDec.setToY(this.incArrow.getLayoutY() + 2);
}
}
/** Adds events animations. Here we also have the logic part.*/
protected void addEvents()
{
// ####################
// THUMB
// ####################
thumb.addEventHandler(MouseEvent.MOUSE_ENTERED , new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent e)
{
if(aniTran_thumbDrop.getStatus() != Animation.Status.RUNNING)
{
aniTran_thumbDrop.setRate(1);
aniTran_thumbDrop.play();
}
else
{
aniTran_thumbDrop.setRate(1);
}
}
});
thumb.addEventHandler(MouseEvent.MOUSE_EXITED , new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent e)
{
if(aniTran_thumbDrop.getStatus() != Animation.Status.RUNNING)
{
aniTran_thumbDrop.setRate(-1);
aniTran_thumbDrop.play();
}
else
{
aniTran_thumbDrop.setRate(-1);
}
}
});
this.aniTran_thumbDrop.fractionProperty().addListener(new ChangeListener<Number>()
{
#Override public void changed(ObservableValue<? extends Number> observable , Number oldValue , Number newValue)
{
drop_thumb.setRadius(4 * newValue.doubleValue());
}
});
// ####################
// INCREMENT BUTTON
// ####################
incButton.addEventHandler(MouseEvent.MOUSE_ENTERED , new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent e)
{
if(aniTran_incDrop.getStatus() != Animation.Status.RUNNING)
{
aniTran_incDrop.setRate(1);
aniTran_incDrop.play();
}
else
{
aniTran_incDrop.setRate(1);
}
}
});
incButton.addEventHandler(MouseEvent.MOUSE_EXITED , new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent e)
{
if(aniTran_incDrop.getStatus() != Animation.Status.RUNNING)
{
aniTran_incDrop.setRate(-1);
aniTran_incDrop.play();
}
else
{
aniTran_incDrop.setRate(-1);
}
}
});
this.aniTran_incDrop.fractionProperty().addListener(new ChangeListener<Number>()
{
#Override public void changed(ObservableValue<? extends Number> observable , Number oldValue , Number newValue)
{
drop_inc.setRadius(4 * newValue.doubleValue());
}
});
// ####################
// DECREMENT BUTTON
// ####################
decButton.addEventHandler(MouseEvent.MOUSE_ENTERED , new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent e)
{
if(aniTran_decDrop.getStatus() != Animation.Status.RUNNING)
{
aniTran_decDrop.setRate(1);
aniTran_decDrop.play();
}
else
{
aniTran_decDrop.setRate(1);
}
}
});
decButton.addEventHandler(MouseEvent.MOUSE_EXITED , new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent e)
{
if(aniTran_decDrop.getStatus() != Animation.Status.RUNNING)
{
aniTran_decDrop.setRate(-1);
aniTran_decDrop.play();
}
else
{
aniTran_decDrop.setRate(-1);
}
}
});
this.aniTran_decDrop.fractionProperty().addListener(new ChangeListener<Number>()
{
#Override public void changed(ObservableValue<? extends Number> observable , Number oldValue , Number newValue)
{
drop_dec.setRadius(4 * newValue.doubleValue());
}
});
// ####################
// INCREMENT ARROW
// ####################
this.incButton.addEventHandler(MouseEvent.MOUSE_PRESSED , new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent event)
{
if(aniTran_setaInc.getStatus() != Animation.Status.RUNNING)
{
aniTran_setaInc.setRate(1);
aniTran_setaInc.play();
}
else
{
aniTran_setaInc.setRate(1);
}
}
});
this.incButton.addEventHandler(MouseEvent.MOUSE_RELEASED , new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent event)
{
if(aniTran_setaInc.getStatus() != Animation.Status.RUNNING)
{
aniTran_setaInc.setRate(-1);
aniTran_setaInc.play();
}
else
{
aniTran_setaInc.setRate(-1);
}
}
});
// ####################
// DECREMENT ARROW
// ####################
this.decButton.addEventHandler(MouseEvent.MOUSE_PRESSED , new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent event)
{
if(aniTran_setaDec.getStatus() != Animation.Status.RUNNING)
{
aniTran_setaDec.setRate(1);
aniTran_setaDec.play();
}
else
{
aniTran_setaDec.setRate(1);
}
}
});
this.decButton.addEventHandler(MouseEvent.MOUSE_RELEASED , new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent event)
{
if(aniTran_setaDec.getStatus() != Animation.Status.RUNNING)
{
aniTran_setaDec.setRate(-1);
aniTran_setaDec.play();
}
else
{
aniTran_setaDec.setRate(-1);
}
}
});
// ####################
// TRACK
// ####################
this.getSkinnable().valueProperty().addListener(new InvalidationListener()
{
#Override public void invalidated(Observable observable)
{
System.out.println("ScrollBar value is invalid: " + getSkinnable().getValue());
}
});
this.getSkinnable().valueProperty().addListener(new ChangeListener<Number>()
{
#Override public void changed(ObservableValue<? extends Number> observable , Number oldValue , Number newValue)
{
System.out.printf("ScrollBar value changed! - [OLD: %f , NEW: %f] %n" ,
oldValue.doubleValue() , newValue.doubleValue() );
}
});
this.track.addEventHandler(MouseEvent.MOUSE_PRESSED , new EventHandler<MouseEvent>()
{
#Override public void handle(MouseEvent event)
{
System.out.println("Track pressed!");
}
});
ChangeListener<Number> listenerThumb = new ChangeListener<Number>()
{
#Override public void changed(ObservableValue<? extends Number> observable , Number oldValue, Number newValue)
{
System.out.println("Thumb moved!");
}
};
thumb.layoutXProperty().addListener(listenerThumb);
thumb.layoutYProperty().addListener(listenerThumb);
thumb.translateXProperty().addListener(listenerThumb);
thumb.translateYProperty().addListener(listenerThumb);
}
#Override protected void handleControlPropertyChanged(String p)
{
System.out.println("Beginning.: " + p);
super.handleControlPropertyChanged(p);
System.out.println("End: " + p);
}
}
As you can see, we insist on creating a subtype of ScrollBarSkin, a private implementation of JavaFX (com.sun ...) package. I also want to make clear that I borrowed the BindableTransition class:
import javafx.animation.Transition;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.ReadOnlyDoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.util.Duration;
/**
* A simple Transition thats fraction property can be bound to any other
* properties.
*
* #author hendrikebbers
*
*/
public class BindableTransition extends Transition {
private DoubleProperty fraction;
public BindableTransition(Duration duration) {
fraction = new SimpleDoubleProperty();
setCycleDuration(duration);
}
#Override
protected final void interpolate(double frac) {
fraction.set(frac);
}
public ReadOnlyDoubleProperty fractionProperty() {
return fraction;
}
}
This is a class that I took from AquaFX, so I have no credit for its creation. The original author, as written in the class itself, is hendrikebbers. I want to thank him/her and the AquaFX team for making available the source code, without which I'd be lost.
As you can see, ScrollBarSkin2 has the addEvents method, which is tasked to add certain events to certain components from skin. Largely, the animations are treated there. The heaviest problem of this code appears when I try to make the thumb to move smoothly once the user click on the track, or the arrow buttons. I just could not implement such behavior, because I have no idea how to do it. I've tried to override the handleControlPropertyChanged method (from BehaviorSkinBase), trying to create a proper positionThumb method. Unfortunately this was not possible because I need some ScrollBarSkin properties in order to properly position the thumb. Such properties would be, for example, trackPos and trackLenght, reserved to be calculated and used only in ScrollBarSkin.
CONCLUSION
I can then conclude that I do not know what else to do. It is very annoying that Oracle provide JavaFX technology limiting its use (at least this is what appears). Appears to be no documentation on the study site of JavaFX about more advanced customization. The documents on editing and creating custom controls in JavaFX existing on internet talk about the simple customization via CSS, not mentioning how we can implement more advanced features (such as animation of their substructures) within the skin. It makes me so frustrated, after all I see great potential in this technology (incomparable to Swing) and still not have the ability to use this feature.
At the most, I felt myself powerless not knowing what to do. I wish someone responsible for developing JavaFX and documentation take notion of certain limitations that are appearing to us. The existing books on the subject are completely outdated, and if not, they do not say anything more than the existing content here (also).
QUESTION
I do not know where to turn to talk about these kinds of things, but I would like someone to tell me that I'm customizing my controls in the wrong way, and that I must correct some lines of code for any reason whatsoever. After all, JavaFX 8 will be fully launched on March 18. Someone please tell me what I'm doing wrong. If I am not doing anything wrong, what needs to be done for this to be notified to the developers of JavaFX?
Thank you for your attention.
EDIT
Some people do not really understand what I wanted to show here. I wanted to demonstrate that there is apparently some need for that part of the JavaFX API becomes public. I said that because I cannot create a simple custom scroll bar. So I'm guessing for the other side. Is there really a need to leave part of the API public? It was then that I finally could ask:
Am I doing the customization of controls in a wrong way? If I am, can anyone correct me?
Simple as that. The huge text post here was proof that I wanted to show, A plus B, that I had to come here to ask this, possibly something that for some people this may be a silly thing. In other words ... I demonstrated how I'm customizing my scroll bar (the source code of my problem). And then I asked: "Hey, can anyone help me on this? Thank you". I have come not to teach how to customize components, came to show how I'm doing (which apparently seems to be the wrong way or not).
Despite having already accepted the answer of a user, my doubts still exist. Anyway, I put the same question in the Oracle JavaFX community, as sugested. And unfortunately things take time to work there (I know that the vast majority is busy working).
You asked:
If I am not doing anything wrong, what needs to be done for this to be notified to the developers of JavaFX?
From the community section on the JavaFX home page (emphasis mine):
The OTN JavaFX Forum is a great place to post, answer, and review
issues related to JavaFX.
The Jira bug tracking system is the place where you want to report
issues with JavaFX, or file a feature request. Instructions on how to
submit bug reports or feature requests are available in the FAQ section.
You asked:
What Oracle expects of us?
I'm presuming they expect you to go through their traditional contact channels, which the developers do read and consider.
Your post is well-written (er... probably... at least it's well-formatted), but you may wish to divide it into smaller, more bite-sized issues or summarize and provide details as-requested in order to make it easier to parse and respond to your concerns.
Related
I used the custom switch button in the custom SwitchButton answer. Now I would like to animate the circle part when the user toggles between the 2 values (state). I used KeyValue and KeyFrame in order to do so.
The snippet of the animation that I added to the SwitchButton() method :
KeyValue start = new KeyValue(button.alignmentProperty(), Pos.CENTER_RIGHT);
KeyValue end = new KeyValue(button.alignmentProperty(), Pos.CENTER_LEFT);
KeyFrame frame = new KeyFrame(Duration.seconds(4), start, end);
Timeline timeline = new Timeline(frame);
timeline.play();
How can I make the animation?
You can use a TranslateTransition, to animate moving the knob for the switch on the track.
Although unrelated to the animation, this solution also modifies the original example to use style classes in a stylesheet, an on pseudo-class, and an exposed on state property for the custom control.
This answer is also based on a combination of ideas from previous questions:
Creating sliding on/off Switch button in javaFX
Change JavaFX style class based on model state
Related question:
One control many click listeners
Example Code
import javafx.animation.TranslateTransition;
import javafx.application.Application;
import javafx.beans.property.*;
import javafx.css.PseudoClass;
import javafx.event.*;
import javafx.geometry.*;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.scene.shape.*;
import javafx.stage.Stage;
import javafx.util.Duration;
public class SwitchApp extends Application {
#Override
public void start(Stage stage) {
Switch lightSwitch = new Switch();
lightSwitch.onProperty().addListener((observable, wasOn, nowOn) -> {
System.out.println(nowOn ? "on" : "off");
});
StackPane layout = new StackPane(lightSwitch);
layout.setPadding(new Insets(30));
stage.setScene(new Scene(layout));
stage.show();
}
public static void main(String[] args) {
launch(args);
}
}
class Switch extends StackPane {
private static final double TRACK_WIDTH = 30;
private static final double TRACK_HEIGHT = 10;
private static final double KNOB_DIAMETER = 15;
private static final Duration ANIMATION_DURATION = Duration.seconds(0.25);
public static final String CSS = "data:text/css," + // language=CSS
"""
.switch > .track {
-fx-fill: #ced5da;
}
.switch > .knob {
-fx-effect: dropshadow(
three-pass-box,
rgba(0,0,0,0.2),
0.2, 0.0, 0.0, 2
);
-fx-background-color: WHITE;
}
.switch:on > .track {
-fx-fill: #80C49E;
}
.switch:on > .knob {
-fx-background-color: #00893d;
}
""";
private final TranslateTransition onTransition;
private final TranslateTransition offTransition;
public Switch() {
// construct switch UI
getStylesheets().add(CSS);
getStyleClass().add("switch");
Rectangle track = new Rectangle(TRACK_WIDTH, TRACK_HEIGHT);
track.getStyleClass().add("track");
track.setArcHeight(track.getHeight());
track.setArcWidth(track.getHeight());
Button knob = new Button();
knob.getStyleClass().add("knob");
knob.setShape(new Circle(KNOB_DIAMETER / 2));
knob.setMaxSize(KNOB_DIAMETER, KNOB_DIAMETER);
knob.setMinSize(KNOB_DIAMETER, KNOB_DIAMETER);
knob.setFocusTraversable(false);
setAlignment(knob, Pos.CENTER_LEFT);
getChildren().addAll(track, knob);
setMinSize(TRACK_WIDTH, KNOB_DIAMETER);
// define animations
onTransition = new TranslateTransition(ANIMATION_DURATION, knob);
onTransition.setFromX(0);
onTransition.setToX(TRACK_WIDTH - KNOB_DIAMETER);
offTransition = new TranslateTransition(ANIMATION_DURATION, knob);
offTransition.setFromX(TRACK_WIDTH - KNOB_DIAMETER);
offTransition.setToX(0);
// add event handling
EventHandler<Event> click = e -> setOn(!isOn());
setOnMouseClicked(click);
knob.setOnMouseClicked(click);
onProperty().addListener((observable, wasOn, nowOn) -> updateState(nowOn));
updateState(isOn());
}
private void updateState(Boolean nowOn) {
onTransition.stop();
offTransition.stop();
if (nowOn != null && nowOn) {
onTransition.play();
} else {
offTransition.play();
}
}
public void setOn(boolean on) {
this.on.set(on);
}
public boolean isOn() {
return on.get();
}
public BooleanProperty onProperty() {
return on;
}
public BooleanProperty on =
new BooleanPropertyBase(false) {
#Override protected void invalidated() {
pseudoClassStateChanged(ON_PSEUDO_CLASS, get());
}
#Override public Object getBean() {
return Switch.this;
}
#Override public String getName() {
return "on";
}
};
private static final PseudoClass
ON_PSEUDO_CLASS = PseudoClass.getPseudoClass("on");
}
Suggested Further Improvements
There are some other enhancements that could be made to the control which are unrelated to the original question but would improve the quality and reusability of the control. Many applications won't need these enhancements, plus the enhancements can add additional complexity to the implementation which is unnecessary for these applications.
Control sizes are still hardcoded in this solution, but if preferred, you could modify the control to use a system based on em sizes (similar to the standard JavaFX controls).
Also, not provided in this solution, you could make use of JavaFX CSS looked up and derived colors to have the control match the color scheme defined for the default Java modena.css stylesheet, so that setting, for instance, the -fx-base color for the application to a different value will change the color scheme for this control as it does with the standard controls.
The in-built controls use a Skin abstraction to separate the public API from the internal implementation of the UI for the control so that users can assign custom skins to completely change the control's UI. Conceptually in operation, this switch is actually a kind of toggle button, so instead of having a custom control, it could be implemented as a custom skin that can be applied to the existing ToggleButton control. Or, less preferable, because it is more redundant, you could take the Switch implementation here and split it into a Switch class extending Control for the public API and a SwitchSkin class extending Skin for the UI and behavior implementation.
Alternative implementation
Before adopting either the other solutions linked in this answer or the solution in this answer, for a fairly common control like a switch, consider whether you would be better off using an off-the-shelf control either from the JavaFX framework core controls or a third-party library. Often those library-based controls will be of higher quality than something you create yourself or find in forum and Q&A posts.
For this particular animated switch control, the MaterialFX library has a particularly nice implementation, powered by additional bling :-)
I hope everyone is doing well.
I'm trying to move the drop down arrow in a TitledPane to be laid out on the right, instead of the left like it is by default. I'm using JavaFX 8, and many of the resources I've found don't seem to work.
I have found that I am able to move the arrow a specific amount, like 20 pixels shown below
.accordion .title > .arrow-button .arrow
{
-fx-translate-x: 20;
}
But I want something responsive. Is there some way that I can get the width of the titled pane, and then subtract some pixels so that so that the arrow appears to be laid out on the right when resizing? Is there a better way to it? I added the element using SceneBuilder2 if that matters.
Thanks so much for your time.
Edit: The following was added for clarification
Primarily, I want the arrow to be right justified, like below
Instead of just "to the right" of the arrow. I really appreciate all the assistance.
Unfortunately, there's no public API for moving the arrow to the right side of the TitledPane. This doesn't mean this can't be accomplished, however, we just have to translate the arrow dynamically, using bindings. In order for the rest of the title area to look correct we'll also have to translate the text, and graphic if present, to the left. The easiest way to do all this is by subclassing TitledPaneSkin and accessing the internals of the "title region".
Here's an example implementation. It lets you position the arrow on the left or right side via CSS. It's also responsive to resizing as well as alignment and graphic changes.
package com.example;
import static javafx.css.StyleConverter.getEnumConverter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.DoubleBinding;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.css.CssMetaData;
import javafx.css.SimpleStyleableObjectProperty;
import javafx.css.StyleableObjectProperty;
import javafx.css.StyleableProperty;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.control.TitledPane;
import javafx.scene.control.skin.TitledPaneSkin;
import javafx.scene.layout.Region;
import javafx.scene.text.Text;
public class CustomTitledPaneSkin extends TitledPaneSkin {
public enum ArrowSide {
LEFT, RIGHT
}
/* ********************************************************
* *
* Properties *
* *
**********************************************************/
private final StyleableObjectProperty<ArrowSide> arrowSide
= new SimpleStyleableObjectProperty<>(StyleableProperties.ARROW_SIDE, this, "arrowSide", ArrowSide.LEFT) {
#Override protected void invalidated() {
adjustTitleLayout();
}
};
public final void setArrowSide(ArrowSide arrowSide) { this.arrowSide.set(arrowSide); }
public final ArrowSide getArrowSide() { return arrowSide.get(); }
public final ObjectProperty<ArrowSide> arrowSideProperty() { return arrowSide; }
/* ********************************************************
* *
* Instance Fields *
* *
**********************************************************/
private final Region title;
private final Region arrow;
private final Text text;
private DoubleBinding arrowTranslateBinding;
private DoubleBinding textGraphicTranslateBinding;
private Node graphic;
/* ********************************************************
* *
* Constructors *
* *
**********************************************************/
public CustomTitledPaneSkin(TitledPane control) {
super(control);
title = (Region) Objects.requireNonNull(control.lookup(".title"));
arrow = (Region) Objects.requireNonNull(title.lookup(".arrow-button"));
text = (Text) Objects.requireNonNull(title.lookup(".text"));
registerChangeListener(control.graphicProperty(), ov -> adjustTitleLayout());
}
/* ********************************************************
* *
* Skin Stuff *
* *
**********************************************************/
private void adjustTitleLayout() {
clearBindings();
if (getArrowSide() != ArrowSide.RIGHT) {
// if arrow is on the left we don't need to translate anything
return;
}
arrowTranslateBinding = Bindings.createDoubleBinding(() -> {
double rightInset = title.getPadding().getRight();
return title.getWidth() - arrow.getLayoutX() - arrow.getWidth() - rightInset;
}, title.paddingProperty(), title.widthProperty(), arrow.widthProperty(), arrow.layoutXProperty());
arrow.translateXProperty().bind(arrowTranslateBinding);
textGraphicTranslateBinding = Bindings.createDoubleBinding(() -> {
switch (getSkinnable().getAlignment()) {
case TOP_CENTER:
case CENTER:
case BOTTOM_CENTER:
case BASELINE_CENTER:
return 0.0;
default:
return -(arrow.getWidth());
}
}, getSkinnable().alignmentProperty(), arrow.widthProperty());
text.translateXProperty().bind(textGraphicTranslateBinding);
graphic = getSkinnable().getGraphic();
if (graphic != null) {
graphic.translateXProperty().bind(textGraphicTranslateBinding);
}
}
private void clearBindings() {
if (arrowTranslateBinding != null) {
arrow.translateXProperty().unbind();
arrow.setTranslateX(0);
arrowTranslateBinding.dispose();
arrowTranslateBinding = null;
}
if (textGraphicTranslateBinding != null) {
text.translateXProperty().unbind();
text.setTranslateX(0);
if (graphic != null) {
graphic.translateXProperty().unbind();
graphic.setTranslateX(0);
graphic = null;
}
textGraphicTranslateBinding.dispose();
textGraphicTranslateBinding = null;
}
}
#Override
public void dispose() {
clearBindings();
unregisterChangeListeners(getSkinnable().graphicProperty());
super.dispose();
}
/* ********************************************************
* *
* Stylesheet Handling *
* *
**********************************************************/
public static List<CssMetaData<?, ?>> getClassCssMetaData() {
return StyleableProperties.CSS_META_DATA;
}
#Override
public List<CssMetaData<?, ?>> getCssMetaData() {
return getClassCssMetaData();
}
private static class StyleableProperties {
private static final CssMetaData<TitledPane, ArrowSide> ARROW_SIDE
= new CssMetaData<>("-fx-arrow-side", getEnumConverter(ArrowSide.class), ArrowSide.LEFT) {
#Override
public boolean isSettable(TitledPane styleable) {
Property<?> prop = (Property<?>) getStyleableProperty(styleable);
return prop != null && !prop.isBound();
}
#Override
public StyleableProperty<ArrowSide> getStyleableProperty(TitledPane styleable) {
Skin<?> skin = styleable.getSkin();
if (skin instanceof CustomTitledPaneSkin) {
return ((CustomTitledPaneSkin) skin).arrowSide;
}
return null;
}
};
private static final List<CssMetaData<?, ?>> CSS_META_DATA;
static {
List<CssMetaData<?,?>> list = new ArrayList<>(TitledPane.getClassCssMetaData().size() + 1);
list.addAll(TitledPaneSkin.getClassCssMetaData());
list.add(ARROW_SIDE);
CSS_META_DATA = Collections.unmodifiableList(list);
}
}
}
You can then apply this skin to all TitledPanes in your application from CSS, like so:
.titled-pane {
-fx-skin: "com.example.CustomTitledPaneSkin";
-fx-arrow-side: right;
}
/*
* The arrow button has some right padding that's added
* by "modena.css". This simply puts the padding on the
* left since the arrow is positioned on the right.
*/
.titled-pane > .title > .arrow-button {
-fx-padding: 0.0em 0.0em 0.0em 0.583em;
}
Or you could target only certain TitledPanes by adding a style class and using said class instead of .titled-pane.
The above works with JavaFX 11 and likely JavaFX 10 and 9 as well. To get it to compile on JavaFX 8 you need to change some things:
Import com.sun.javafx.scene.control.skin.TitledPaneSkin instead.
The skin classes were made public in JavaFX 9.
Remove the calls to registerChangeListener(...) and unregisterChangeListeners(...). I believe replacing them with the following is correct:
#Override
protected void handleControlPropertyChange(String p) {
super.handleControlPropertyChange(p);
if ("GRAPHIC".equals(p)) {
adjustTitleLayout();
}
}
Use new SimpleStyleableObjectProperty<ArrowSide>(...) {...} and new CssMetaData<TitledPane, ArrowSide>(...) {...}.
Type inference was improved in later versions of Java.
Use (StyleConverter<?, ArrowSide>) getEnumConverter(ArrowSide.class).
There was a bug in the generic signature of getEnumConverter that was fixed in a later version. Using the cast works around the problem. You may wish to #SuppressWarnings("unchecked") the cast.
Issue: Even with the above changes there's a problem in JavaFX 8—the arrow is only translated once the TitledPane is focused. This doesn't appear to be a problem with the above code as even changing the alignment property does not cause the TitledPane to update until it has focus (even when not using the above skin, but rather just the default skin). I've been unable to find a workaround to this problem (while using the custom skin) but maybe you or someone else can. I was using Java 1.8.0_202 when testing for JavaFX 8.
If you don't want to use a custom skin, or you're on JavaFX 8 (this will cause the arrow to be translated without needing to focus the TitledPane first), you can extract the necessary code, with some modifications, into a utility method:
public static void putArrowOnRight(TitledPane pane) {
Region title = (Region) pane.lookup(".title");
Region arrow = (Region) title.lookup(".arrow-button");
Text text = (Text) title.lookup(".text");
arrow.translateXProperty().bind(Bindings.createDoubleBinding(() -> {
double rightInset = title.getPadding().getRight();
return title.getWidth() - arrow.getLayoutX() - arrow.getWidth() - rightInset;
}, title.paddingProperty(), title.widthProperty(), arrow.widthProperty(), arrow.layoutXProperty()));
arrow.setStyle("-fx-padding: 0.0em 0.0em 0.0em 0.583em;");
DoubleBinding textGraphicBinding = Bindings.createDoubleBinding(() -> {
switch (pane.getAlignment()) {
case TOP_CENTER:
case CENTER:
case BOTTOM_CENTER:
case BASELINE_CENTER:
return 0.0;
default:
return -(arrow.getWidth());
}
}, arrow.widthProperty(), pane.alignmentProperty());
text.translateXProperty().bind(textGraphicBinding);
pane.graphicProperty().addListener((observable, oldGraphic, newGraphic) -> {
if (oldGraphic != null) {
oldGraphic.translateXProperty().unbind();
oldGraphic.setTranslateX(0);
}
if (newGraphic != null) {
newGraphic.translateXProperty().bind(textGraphicBinding);
}
});
if (pane.getGraphic() != null) {
pane.getGraphic().translateXProperty().bind(textGraphicBinding);
}
}
Note: While this puts the arrow on the right without having to focus the TitledPane first, the TitledPane still suffers from the issue noted above. For instance, changing the alignment property doesn't update the TitledPane until it's focused. I'm guessing this is simply a bug in JavaFX 8.
This way of doing things is not as "easy" as the skin approach and requires two things:
The TitledPane must be using the default TitledPaneSkin.
The TitledPane must have been displayed in a Window (window was showing) before calling the utility method.
Due to the lazy nature of JavaFX controls, the skin and the associated nodes will not have been created until the control has been displayed in a window. Calling the utility method before the control was displayed will result in a NullPointerException being thrown since the lookup calls will return null.
If using FXML, note that the initialize method is called during a call to FXMLLoader.load (any of the overloads). This means, under normal circumstances, it's not possible for the created nodes to be part of a Scene yet, let alone a showing Window. You must wait for the TitledPane to be displayed first, then call the utility method.
Waiting for the TitledPane to be displayed can be achieved by listening to the Node.scene property, the Scene.window property, and the Window.showing property (or you could listen for WindowEvent.WINDOW_SHOWN events). However, if you immediately put the loaded nodes into a showing Window, then you can forgo observing the properties; call the utility method inside a Platform.runLater call from inside initialize.
When using the skin approach, the whole wait-for-showing-window hassle is avoided.
Usual Warning: This answer relies on the internal structure of TitledPane which may change in a future release. Be cautious when changing JavaFX versions. I only (somewhat) tested this on JavaFX 8u202 and JavaFX 11.0.2.
This isn’t exactly the same, visually, but you can hide the arrow button and create a graphic that acts like an arrow button. TitledPane extends Labeled, so you have control over the placement of the graphic relative to the text, via the contentDisplay property.
First, hide the arrow button in the stylesheet:
.accordion .title > .arrow-button
{
visibility: hidden;
}
In the code, you can create a Label to act as a fake button and set it as the TitledPane’s graphic. The entire title line is sensitive to the mouse, so an interactive control (like a Button) is not needed.
Label collapseButton = new Label();
collapseButton.textProperty().bind(
Bindings.when(titledPane.expandedProperty())
.then("\u25bc").otherwise("\u25b6"));
titledPane.setGraphic(collapseButton);
titledPane.setContentDisplay(ContentDisplay.RIGHT);
In FXML you can just add nodeOrientation="RIGHT_TO_LEFT"
or use yourNode.setNodeOrientation((NodeOrientation orientation)
https://openjfx.io/javadoc/11/javafx.graphics/javafx/scene/Node.html#setNodeOrientation(javafx.geometry.NodeOrientation)
I am new in Java and I am trying Java events but I am absolutely lost. I know events from C# where is no problem with it and they works perfectly, but Java is different universe . I tried to find something on internet but I can't figured it out long time so I am here.
I have one object and I need trigger some action. When this action is triggered I need to call not only one event handler, but more of them from different objects.
For example I just use Button class.
There are two ways how to do that:
One way is to use button.setOnAction method. But this is not working because when I call this method second time (from another object) I just replace one event handler by another. You can see what I mean in code in method initEventsUselessWay().
Second way is to use button.onActionProperty().addListener. But this is not working at all. You can see in method initEventsNeededWay().
So, why button.onActionProperty().addListeneris not working?
And is there any way how to do this in Javafx?
Finally I will not use Button class, but something like MyClass and I need to implement this here. But if this is not working on Button class it will not work on MyClas neither.
Thank you very much for advice.
package sample;
import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
public class JavaEventsTest1589 extends Application {
private Button btnDemo1;
private Button btnDemo2;
#Override
public void start(Stage primaryStage) {
// panel
Pane rootPane = new Pane();
// scene
Scene scene = new Scene(rootPane, 300, 250);
primaryStage.setTitle("events demo");
primaryStage.setScene(scene);
// button 1
btnDemo1 = new Button();
rootPane.getChildren().add(btnDemo1);
btnDemo1.setText("Execute Demo 1");
btnDemo1.setLayoutX(50);
btnDemo1.setLayoutY(10);
// button 2
btnDemo2 = new Button();
rootPane.getChildren().add(btnDemo2);
btnDemo2.setText("Execute Demo 2");
btnDemo2.setLayoutX(50);
btnDemo2.setLayoutY(50);
initEventsUselessWay();
initEventsNeededWay();
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
private void initEventsUselessWay() {
btnDemo1.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
runDemoPrimaryReaction();
}
});
btnDemo1.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
runDemoSecondaryReaction();
}
});
}
private void initEventsNeededWay() {
btnDemo2.onActionProperty().addListener(new ChangeListener<EventHandler<ActionEvent>>() {
#Override
public void changed(ObservableValue<? extends EventHandler<ActionEvent>> observableValue, EventHandler<ActionEvent> actionEventEventHandler, EventHandler<ActionEvent> actionEventEventHandler2) {
runDemoThisINeed_No1();
}
});
btnDemo2.onActionProperty().addListener(new ChangeListener<EventHandler<ActionEvent>>() {
#Override
public void changed(ObservableValue<? extends EventHandler<ActionEvent>> observableValue, EventHandler<ActionEvent> actionEventEventHandler, EventHandler<ActionEvent> actionEventEventHandler2) {
runDemoThisINeed_No2();
}
});
}
private void runDemoPrimaryReaction() {
System.out.println("useless way - primary reaction");
}
private void runDemoSecondaryReaction() {
System.out.println("useless way - secondary reaction");
}
private void runDemoThisINeed_No1() {
System.out.println("not working way - No1");
}
private void runDemoThisINeed_No2() {
System.out.println("not working way - No2");
}
}
Use addEventHandler:
btnDemo1.addEventHandler(ActionEvent.ACTION, new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
runDemoPrimaryReaction();
}
});
btnDemo1.addEventHandler(ActionEvent.ACTION, new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
runDemoSecondaryReaction();
}
});
I recommend using Java 8, in which case you can write
btnDemo1.addEventHandler(ActionEvent.ACTION, event -> runDemoPrimaryReaction());
btnDemo1.addEventHandler(ActionEvent.ACTION, event -> runDemoSecondaryReaction());
The setOnAction(...) method is a "convenience" method. The way it works is that the Button maintains an ObjectProperty<EventHandler<ActionEvent>>. If you set the value of this property (to an EventHandler<ActionEvent>), that event handler will automatically be added to the event handlers for the button. If you set it a second time, since it's just a property, it will replace the existing event handler. So you can use this for a slightly quicker approach in the case you only have one handler. (It also plays nicely with FXML.)
onActionProperty().addListener(...) is a different thing entirely: it listens for changes in the property itself. So if you register a listener in this way, then call setOnAction(...), the listener you registered with the property will be invoked when you call setOnAction (not when the button is pressed).
Have a look at the tutorial, particularly the first section "Processing events" and the second "Working with convenience methods". The second section makes it clear when setOnAction is actually doing.
You need to think of an 'action' as a property of the Button rather than an event.
A Button can only have one action at a time, hence you are setting it to one value and then changing it to another which means only the code in the second handler will execute.
The action property is implemented using one of the JavaFX property classes which provides built in property change notification. When you call button.onActionProperty().addListener(... you are adding a listener that will be invoked when the action is changed.
Try calling the addListener(... code before calling setOnAction(... and it should be clear what is happening.
If you want to think of this in terms of C# then
button.setOnAction(.. is like a property e.g. button.Action = ...
button.onActionProperty().addListener(... is like an event e.g. button.ActionChanged += ...
If you are new to JavaFX and need some help with their particular implementation of properties and events I can suggest the following resources:
Creating JavaFX Properties
Creating read-only properties in JavaFX
JavaFX: Handling events
JavaFX: Using properties and binding
Update:
After reading your comments I realise this isn't really JavaFX specific but about events in general. Events are just an implementation of the observer pattern (even in C#).
A basic implementation might look something like this although there is little point in writing this yourself as JavaFX does include a built in way registering listeners for events as demonstrated by James_D.
public interface MyListener {
invoke();
}
class MyButton extends Button {
List<MyListener> listeners = new ArrayList<MyListener>();
public void addListener(MyListener toAdd) {
listeners.add(toAdd);
}
private void invoke() {
for (HelloListener l : listeners)
l.invoke();
}
this.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
this.invoke();
}
});
}
Based on the OP's comment
So I exprect that when I click on button "Execute Demo 1" in console I will see in console two rows: 'useless way - primary reaction useless way - secondary reaction'
you need to call both the methods in the same event handler as following:
btnDemo1.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
runDemoPrimaryReaction();
runDemoSecondaryReaction();
}
});
I am confused all about the subject of event handling.
I am trying to implement a game. I already wrote the game logic separately, and the GUI (on JavaFX).
Below is some sample code; What can I do so that the updateScoresLabel() method will run whenever the setScore(...) method is executed?
public class MyGameLogic
{
private int scores=0;
public void setScore(int scores)
{
this.scores=scores;
}
public int getScore()
{
return scores;
}
}
public class JustAGUIExample
{
Label scoresLabel;
MyGameLogic gameLogic;
public void updateScoresLabel()
{
this.scoresLabel=gameLogic.getScore();
}
}
Use Binding Instead of Event Handlers
You don't need an event handler to accomplish a label update when a model change occurs.
You can bind your label property to your model property, then when you change the model, the label will update automatically.
Adapting the code from your question to use binding.
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.control.Label;
public class MyGameLogic {
private IntegerProperty scores = new SimpleIntegerProperty(0);
public void setScore(int scores) {
this.scores.set(scores);
}
public int getScore() {
return scores.get();
}
public IntegerProperty scoreProperty() {
return scores;
}
}
class JustAGUIExample {
private Label scoresLabel;
private MyGameLogic gameLogic;
public JustAGUIExample() {
scoresLabel.textProperty().bind(
gameLogic.scoreProperty().asString()
);
}
}
There are extensive examples of this kind of binding strategy in this JavaFX tic-tac-toe game example.
For more complex logic use a ChangeListener
Let's say you also wanted to play a victory sound when the score changes, you could use something like this:
class JustAGUIExample {
private Label scoresLabel;
private MyGameLogic gameLogic;
private AudioClip levelUpAudio = new AudioClip("levelup.mp3");
public JustAGUIExample() {
scoresLabel.textProperty().bind(
gameLogic.scoreProperty().asString()
);
gameLogic.scoreProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> observable, Number oldValue, Number newValue) {
levelUpAudio.play();
}
});
}
}
So a ChangeListener is kind of like an event listener for property changes. But I only say kind of because in JavaFX events are there own separate things and are usually reserved for GUI system events like mouse clicks, window resizing notification, touchpad swipes, etc.
Using Java 8 the syntax is nicer:
gameLogic.scoreProperty().addListener((observable, oldValue, newValue) ->
levelUpAudio.play()
);
Tutorial on Event Handling in Java
Even though you don't really need event handling for the sample from your question, you can read up on the Oracle JavaFX event handling tutorial to find out what events really are and how they work.
My Thoughts On Swing Based Suggestions
As you are writing a JavaFX program please disregard any suggestions which relate to event handling in Swing. Instead, learn to do this stuff in a JavaFX way or you will just confuse yourself.
For a GUI to run an event, the class must implement ActionListener. From this the method actionPerformed must be added to that class.
Here is a sample implementation of that
//Run, help, and about are all buttons on this frame
public void actionPerformed(ActionEvent e){
if(e.getSource() == run){ //Check if the event was the run button being pressed
//Run the "run" program
}else if(e.getSource() == about){ //Check if the event was the about button being pressed
//Open welcome
}else if(e.getSource() == help){ //Check if the event was the help button being pressed
//Have the help screen appear
}
}
Is there a way to only "render" an component in Javafx if a condition is met?
I am interested in doing a user interface with different roles and just add
a component if the role allows it, also I would like to keep working with FXML.
I haven't read about anything like that.
Bind visibility
Bind the visible property of your component to a BooleanExpression representing the condition under which it should be visible.
FXML can do binding, but note that as of JavaFX 2.2 "Only simple expressions that resolve to property values or page variables are currently supported. Support for more complex expressions involving boolean or other operators may be added in the future."
If you don't bind in FXML, you can bind in Java code using the JavaFX API.
node.visibleProperty().bind(conditionProperty);
If you don't want the invisible property to take up layout space, also first bind the managed property to the visible property.
node.managedProperty().bind(node.visibleProperty());
Alternately change visibility in change listeners
Note that setting up a binding for a full role based system is pretty complex, so for such a task you are better off using change listeners and coding part of your logic in Java rather than trying to do everything in FXML. You will still be able to design your UI in FXML, but you can add some code in your controller to manage what items are visible for what role.
Sample role based display
Here is a role based solution using the Java API.
The solution shows different labeled pictures depending on the selected roles.
import javafx.application.Application;
import javafx.beans.value.*;
import javafx.collections.*;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.stage.Stage;
import java.util.*;
enum Role { father, son, mother, daughter, brother, sister }
class RoleManager {
private final Map<Node, List<Role>> nodeRoles = new HashMap<>();
private ObservableList<Role> activeRoles;
public final ListChangeListener<Role> ACTIVE_ROLE_LISTENER = new ListChangeListener<Role>() {
#Override
public void onChanged(Change<? extends Role> c) {
showActiveNodes();
}
};
public void setActiveRoles(ObservableList<Role> activeRoles) {
if (this.activeRoles != null) {
this.activeRoles.removeListener(ACTIVE_ROLE_LISTENER);
}
this.activeRoles = activeRoles;
this.activeRoles.addListener(ACTIVE_ROLE_LISTENER);
}
public void showActiveNodes() {
for (Node node : nodeRoles.keySet()) {
node.setVisible(isActive(node));
}
}
public void assignRole(Node node, Role... roles) {
nodeRoles.put(node, Arrays.asList(roles));
}
private boolean isActive(Node node) {
if (activeRoles == null) {
return false;
}
for (Role role: nodeRoles.get(node)) {
if (activeRoles.contains(role)) {
return true;
}
}
return false;
}
}
public class RoleVisibility extends Application {
private RoleManager roleManager = new RoleManager();
#Override
public void start(Stage stage) {
VBox layout = new VBox(10);
layout.getChildren().setAll(
getRoleChooser(),
createContent()
);
layout.setStyle("-fx-padding: 10px; -fx-background-color: cornsilk;");
roleManager.showActiveNodes();
stage.setTitle("Role Selector");
stage.setScene(new Scene(layout));
stage.show();
}
private Node getRoleChooser() {
ObservableList<Role> activeRoles = FXCollections.observableArrayList();
VBox roleChooser = new VBox(10);
for (final Role role: Role.values()) {
CheckBox checkBox = new CheckBox(role.toString());
checkBox.selectedProperty().addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean wasSelected, Boolean isSelected) {
if (isSelected) {
activeRoles.add(role);
} else {
activeRoles.remove(role);
}
}
});
roleChooser.getChildren().add(checkBox);
}
roleManager.setActiveRoles(
activeRoles
);
return roleChooser;
}
private Pane createContent() {
HBox content = new HBox(10);
// icon license:
//License: Free for non-commercial use.
//Commercial usage: Not allowed
//The products or characters depicted in these icons are © by Lucasfilm Ltd.
content.getChildren().addAll(
createLabel("Darth Vader", "Vader-03-icon.png", Role.father),
createLabel("Queen Amidala", "Padme-Amidala-icon.png", Role.mother),
createLabel("Luke Skywalker", "Luke-Skywalker-01-icon.png", Role.brother, Role.son),
createLabel("Princess Leia", "Leia-icon.png", Role.daughter, Role.sister)
);
return content;
}
private Label createLabel(String text, String graphic, Role... roles) {
Label label = new Label(
text,
new ImageView(
new Image(
"http://icons.iconarchive.com/icons/jonathan-rey/star-wars-characters/128/" + graphic
)
)
);
label.setContentDisplay(ContentDisplay.TOP);
roleManager.assignRole(label, roles);
return label;
}
public static void main(String[] args) {
launch(args);
}
}
FXML Based Solution
I was interested in what it would take to get something like this to work with FXML, so I created a small framework to handle role based FXML UIs. It could perhaps have some performance enhancements and add some convenience definitions for defining role based controllers as well as shorthands for role definitions in the FXML, but it does seem to work in principle and demonstrate a basic approach.