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 :-)
Related
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 apologize for the topic headline, it might not exactly express my thought but i'll give it a try. If someone knows what's the better headline, please suggest an edit.
So i'd like to create rectangles and give values for them after the button has been pressed. Everything's plain and simple if i know how many rectangles i want to create. Here's where thing gets complicated - i get the rectangle count after i've pressed the button.
I'll explain with an example, so it's a bit more clear:
final ArrayList rectList = new ArrayList();
btn.setOnAction(new EventHandler<ActionEvent>() {
public void handle(final ActionEvent event) {
ArrayList getFromMethodAnArrayList = methodWhichReturnsAnArrayList();
for (int i = 0; i<getFromMethodAnArrayList.size();i++){
rectList.add(new Rectangle(0,0,0,30));
}
}
});
HBox box1 = new HBox(1);
for (int i = 0; i<rectList.size();i++){
box1.getChildren().add(rectList.get(i));
}
This code gives an error because when first loaded the rectList is empty. How could i queue adding elements into HBox, so it would be performed after the rectList has been valued.
Recommendation
You don't need a queue here and you don't need to multi-thread either, at least as you have currently described your question - additional requirements on the implementation could imply that the use of both of those things are necessary.
Sample code
What the sample code does is define a source of items which are model data for something you want to display. When you click on the create button, it will generate a random number of new items with random data values for each item. These items will be placed in a queue and a subsequent routine will take the items from the queue, read their data values and create appropriate visual representations (rectangles) for the item data. It uses a queue data structure, but a simple array or list would have worked just fine.
import javafx.application.Application;
import javafx.scene.*;
import javafx.scene.control.*;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;
import java.util.ArrayDeque;
import java.util.Queue;
import java.util.Random;
// java 8 code
public class RectangleAddition extends Application {
private final Random random = new Random(42);
public static void main(String[] args) {
launch(args);
}
public void start(Stage stage) {
FlowPane flow = createItemContainer();
ScrollPane scroll = makeContainerScrollable(flow);
ItemSource itemSource = new ItemSource();
Button create = createItemControl(flow, scroll, itemSource);
VBox layout = createLayout(create, scroll);
Scene scene = new Scene(layout);
stage.setScene(scene);
stage.show();
}
private FlowPane createItemContainer() {
FlowPane flow = new FlowPane();
flow.setHgap(5);
flow.setVgap(5);
return flow;
}
/**
* The control will
* retrieve items from the source,
* add them to the scrollable pane,
* scroll the pane to the bottom on each addition.
*/
private Button createItemControl(Pane flow, ScrollPane scroll, ItemSource itemSource) {
Button create = new Button("Create Rectangles (keep pressing to create more)");
create.setOnAction(event -> {
addRectangles(flow, itemSource);
scroll.setVvalue(scroll.getVmax());
});
return create;
}
private VBox createLayout(Button create, ScrollPane scroll) {
VBox layout = new VBox(10, create, scroll);
layout.setStyle("-fx-padding: 10px;");
layout.setPrefSize(300, 300);
VBox.setVgrow(scroll, Priority.ALWAYS);
create.setMinHeight(Button.USE_PREF_SIZE);
return layout;
}
/**
* fetches some items from the source,
* creates rectangle nodes for them
* adds them to the container.
*/
private void addRectangles(Pane container, ItemSource itemSource) {
Queue<Item> items = itemSource.fetchNextItems();
while (!items.isEmpty()) {
Item item = items.remove();
Node rectangle = createRectangle(item);
container.getChildren().add(rectangle);
}
}
private Rectangle createRectangle(Item item) {
Rectangle rectangle = new Rectangle(item.size, item.size, item.color);
rectangle.setRotate(item.rotation);
return rectangle;
}
private ScrollPane makeContainerScrollable(FlowPane flow) {
ScrollPane scroll = new ScrollPane(flow);
scroll.setFitToWidth(true);
scroll.setMaxSize(Double.MAX_VALUE, Double.MAX_VALUE);
return scroll;
}
/** some model data for application items */
class Item {
// item will be colored according to rgb values from the (inclusive) range
// MIN_COLOR_VALUE to MIN_COLOR_VALUE + COLOR_RANGE - 1
private static final int MIN_COLOR_VALUE = 50;
private static final int COLOR_RANGE = 201;
// item will be sized from the (inclusive) range
// MIN_SIZE to MIN_SIZE + SIZE_RANGE - 1
private static final int MIN_SIZE = 5;
private static final int SIZE_RANGE = 21;
// item will be (z-axis) rotated from the (inclusive) range
// - ROTATE_SCOPE to + ROTATE_SCOPE
private static final int ROTATE_SCOPE = 10;
private Color color;
private int size;
private int rotation;
public Item() {
color = Color.rgb(
createColorComponent(),
createColorComponent(),
createColorComponent()
);
size = random.nextInt(SIZE_RANGE) + MIN_SIZE;
rotation = random.nextInt(ROTATE_SCOPE * 2 + 1) - ROTATE_SCOPE;
}
private int createColorComponent() {
return random.nextInt(COLOR_RANGE) + MIN_COLOR_VALUE;
}
}
/** a never-ending source of new items fetched in batches */
class ItemSource {
// will fetch between 1 and MAX_NUM_ITEMS_PER_FETCH (inclusive) items on each fetch call.
private static final int MAX_NUM_ITEMS_PER_FETCH = 5;
public Queue<Item> fetchNextItems() {
int numItems = random.nextInt(MAX_NUM_ITEMS_PER_FETCH) + 1;
Queue<Item> queue = new ArrayDeque<>(numItems);
for (int i = 0; i < numItems; i++) {
queue.add(new Item());
}
return queue;
}
}
}
Thoughts On Multithreading
Where you might want a different implementation which does actually use multi-threading is if the item creation or fetching from the item source takes a long time. For example you need to read the item data from a network, database or very large file. If you don't multi-thread such things, then you will end up freezing the UI while it waits for the I/O to complete. A general rule is if the operation you are performing will finish in less than a sixtieth of a millisecond, then you can do it on the JavaFX UI thread without any issue as there will be no visible lag and stuttering in the UI, but if it takes longer than that then you should use concurrency utilities (which are more tricky to use than single-threaded code).
Java has numerous threading mechanisms, which can be used, but you in many cases, using the JavaFX specific concurrency extensions is the best way to integrate multi-threaded code into your JavaFX application.
The appropriate concurrency utility to use usually would be the JavaFX Task or Service interfaces if you are doing this on demand from the UI. You can read the documentation for these facilities which demonstrates sample code for doing things like "a task which returns partial results" (which is a bit similar to your question).
If the thing which provides the items to be consumed is some background long running network task to which items are pushed, rather than pulled on demand, then running it in it's own thread and calling back into the JavaFX to signal a UI update via platform.runLater() is the way to go. Another data structure which can aid in such cases is a BlockingQueue as demonstrated in this multi-chart creation code - but that is quite a sophisticated solution.
I guess part of my point is that you may not need to use these concurrency utilities for your situation, you need to evaluate it on a case by case basis and use the most appropriate solution.
I think you can simplify your code quite a bit here by getting rid of the ArrayList and populating box1 when the button event happens:
final HBox box1 = new HBox(1);
btn.setOnAction(new EventHandler<ActionEvent>() {
public void handle(final ActionEvent event) {
ArrayList getFromMethodAnArrayList = methodWhichReturnsAnArrayList();
for (int i = 0; i<getFromMethodAnArrayList.size();i++){
box1.getChildren().add(new Rectangle(0,0,0,30));
}
}
});
If it is concurrency that you are interested in, it would be good to read Concurrency in JavaFX, although I don't think that is the right solution for the question you posted.
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.
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.
Overview
As a Swing developer of ten years, I've been thrilled with the features introduced with JavaFX 2.0, especially the rich, fluent, high-level data-binding facilities. This facility alone is worth the cost of learning a new API (which is much less since abandoning FX script). It's going to have a direct impact on the readability and maintainably of my model/view synchronization code.
So far I'm having great success at first level and basic derived bindings, but am struggling to figure out the "JavaFX way" of binding one value to a value two or more levels of indirection in the data graph.
Problem
As shown in the code example below, I'm attempting to use javafx.beans.binding.Bindings.select() to synchronize the text value of a Label with one of the contained properties of the currently selected item in a ComboBox. This code is a simple example of something more complex I'm trying to do, so I understand that it's not hard to do this with the lower level bindings API. I'd like to know if it's possible with the higher-level fluent API, and if the select(...) method actually tracks changes in the indirect properties (i.e. update property if either the direct property or the selected subproperty change).
The documentation and examples on select(...) are sparse, so I'm hoping someone with advanced experience with this can tell me if I'm trying to use the API as designed, or if there's another way to use the high-level binding API to do what I want.
Sample Code
Here's the demo code. When run, there's a ComboBox with two items in it, and then two labels. The first label shows the toString() version of the selected item. The second label attempts to display one of the properties of the selected item, but only displays null.
import static javafx.beans.binding.Bindings.*;
import javafx.application.Application;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/** Testing cascading binding change triggers. */
public class SandboxTest extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
VBox root = new VBox(8);
root.setStyle("-fx-padding: 8;");
Scene s = new Scene(root);
stage.setWidth(200);
stage.setScene(s);
ComboBox<MoPoJo> list = new ComboBox<SandboxTest.MoPoJo>();
list.itemsProperty().set(FXCollections.observableArrayList(new MoPoJo("foo", "bar"), new MoPoJo("baz", "bat")));
Label direct = new Label();
direct.setTooltip(new Tooltip("Selected item to string"));
Label withSelect = new Label();
withSelect.setTooltip(new Tooltip("Second property of selected item"));
direct.textProperty().bind(convert(list.getSelectionModel().selectedItemProperty()));
withSelect.textProperty().bind(convert(select(list.getSelectionModel().selectedItemProperty(), "two")));
root.getChildren().addAll(list, direct, withSelect);
stage.show();
}
private static class MoPoJo {
private StringProperty _one = new SimpleStringProperty();
private StringProperty _two = new SimpleStringProperty();
private StringProperty _name = new SimpleStringProperty();
public MoPoJo(String o, String t) {
_one.set(o);
_two.set(t);
_name.bind(format("{ %s, %s }", oneProperty(), twoProperty()));
}
public StringProperty oneProperty() {
return _one;
}
public StringProperty twoProperty() {
return _two;
}
public ReadOnlyStringProperty nameProperty() {
return _name;
}
#Override
public String toString() {
return nameProperty().get();
}
}
}
Bindings.select can't access private class. Make MoPoJo a public class and your code will work.
public static class MoPoJo {
P.S: I believe that fact worth to be mentioned in docs, so I filed http://javafx-jira.kenai.com/browse/RT-20640 on JavaFX javadoc.