How can I bypass the JavaFX's TableView "placeholder"? - java

JavaFX's TableView has a placeholder property that is basically a Node that gets displayed in the TableView whenever it is empty. If this property is set to null (its default value), it appears as a Label or some other text based Node that says "There is no content in the table."
But if there are any rows of data in the table, then the placeholder Node disappears and the entire vertical space in the TableView gets filled with rows, including empty rows if there isn't enough data to fill the whole table.
These empty rows are what I want, even when the table is empty. In other words, I don't want to use the placeholder at all. Does anyone know how I can do this?
I'd rather not do something kludgey like put a empty-looking row in the TableView whenever it's supposed to be actually empty.

Unfortunately, the old issue is still not fixed in fx9 nor later versions up and including fx18.
The original hack of the skin (see below the horizontal line) for fx9 seems to still be working in fx18. Though can be slightly improved since fx12 introduced access to the flow and tableHeaderRow.
While trying to adjust it to newer api, I came up with a - just as dirty and untested - approach. The idea is to override the actual layout method, that is the method that's called when laying out individual children of the table, watch out for the placeholder and replace it with the flow:
public class NoPlaceHolderSkin<T> extends TableViewSkin<T> {
public NoPlaceHolderSkin(TableView<T> control) {
super(control);
}
#Override
protected void layoutInArea(Node child, double areaX, double areaY, double areaWidth,
double areaHeight, double areaBaselineOffset, HPos halignment, VPos valignment) {
if (child.getStyleClass().contains("placeholder")) {
child.setVisible(false);
child = getVirtualFlow();
child.setVisible(true);
}
super.layoutInArea(child, areaX, areaY, areaWidth, areaHeight, areaBaselineOffset, halignment, valignment);
}
}
So revisited the hacks in the context of fx9. There had been changes, good and bad ones:
Skins moved into a public package which now allows to subclass them without accessing hidden classes (good)
the move introduced a bug which doesn't allow to install a custom VirtualFlow (fixed in fx10)
reflective access to hidden members will be strongly disallowed (read: not possible) sometime in future
While digging, I noticed ever so slight glitches with the hacks (note: I did not run them against fx8, so these might be due differences in fx8 vs fx9!)
the forced in-/visibility of placeholder/flow worked fine except when starting up with an empty table (placeholder was shown) and enlarging the table while empty (the "new" region looks empty)
faking the itemCount to not-empty lets the rows dissappear on pressing navigation keys (which probably is not a big problem because users tend to not navigate an empty table) - this is definitely introduced in fx9, working fine in fx8
So I decided to go with the visibility enforcement: the reason for the slight glitches is that layoutChildren doesn't layout the flow if it thinks the placeholder is visible. That's handled by including the flow in the layout if super didn't.
The custom skin:
/**
* TableViewSkin that doesn't show the placeholder.
* The basic trick is keep the placeholder/flow in-/visible at all
* times (similar to https://stackoverflow.com/a/27543830/203657).
* <p>
*
* Updated for fx9 plus ensure to update the layout of the flow as
* needed.
*
* #author Jeanette Winzenburg, Berlin
*/
public class NoPlaceHolderTableViewSkin<T> extends TableViewSkin<T>{
private VirtualFlow<?> flowAlias;
private TableHeaderRow headerAlias;
private Parent placeholderRegionAlias;
private ChangeListener<Boolean> visibleListener = (src, ov, nv) -> visibleChanged(nv);
private ListChangeListener<Node> childrenListener = c -> childrenChanged(c);
/**
* Instantiates the skin.
* #param table the table to skin.
*/
public NoPlaceHolderTableViewSkin(TableView<T> table) {
super(table);
flowAlias = (VirtualFlow<?>) table.lookup(".virtual-flow");
headerAlias = (TableHeaderRow) table.lookup(".column-header-background");
// startet with a not-empty list, placeholder not yet instantiatet
// so add alistener to the children until it will be added
if (!installPlaceholderRegion(getChildren())) {
installChildrenListener();
}
}
/**
* Searches the given list for a Parent with style class "placeholder" and
* wires its visibility handling if found.
* #param addedSubList
* #return true if placeholder found and installed, false otherwise.
*/
protected boolean installPlaceholderRegion(
List<? extends Node> addedSubList) {
if (placeholderRegionAlias != null)
throw new IllegalStateException("placeholder must not be installed more than once");
List<Node> parents = addedSubList.stream()
.filter(e -> e.getStyleClass().contains("placeholder"))
.collect(Collectors.toList());
if (!parents.isEmpty()) {
placeholderRegionAlias = (Parent) parents.get(0);
placeholderRegionAlias.visibleProperty().addListener(visibleListener);
visibleChanged(true);
return true;
}
return false;
}
protected void visibleChanged(Boolean nv) {
if (nv) {
flowAlias.setVisible(true);
placeholderRegionAlias.setVisible(false);
}
}
/**
* Layout of flow unconditionally.
*
*/
protected void layoutFlow(double x, double y, double width,
double height) {
// super didn't layout the flow if empty- do it now
final double baselineOffset = getSkinnable().getLayoutBounds().getHeight() / 2;
double headerHeight = headerAlias.getHeight();
y += headerHeight;
double flowHeight = Math.floor(height - headerHeight);
layoutInArea(flowAlias, x, y,
width, flowHeight,
baselineOffset, HPos.CENTER, VPos.CENTER);
}
/**
* Returns a boolean indicating whether the flow should be layout.
* This implementation returns true if table is empty.
* #return
*/
protected boolean shouldLayoutFlow() {
return getItemCount() == 0;
}
/**
* {#inheritDoc} <p>
*
* Overridden to layout the flow always.
*/
#Override
protected void layoutChildren(double x, double y, double width,
double height) {
super.layoutChildren(x, y, width, height);
if (shouldLayoutFlow()) {
layoutFlow(x, y, width, height);
}
}
/**
* Listener callback from children modifications.
* Meant to find the placeholder when it is added.
* This implementation passes all added sublists to
* hasPlaceHolderRegion for search and install the
* placeholder. Removes itself as listener if installed.
*
* #param c the change
*/
protected void childrenChanged(Change<? extends Node> c) {
while (c.next()) {
if (c.wasAdded()) {
if (installPlaceholderRegion(c.getAddedSubList())) {
uninstallChildrenListener();
return;
}
}
}
}
/**
* Installs a ListChangeListener on the children which calls
* childrenChanged on receiving change notification.
*
*/
protected void installChildrenListener() {
getChildren().addListener(childrenListener);
}
/**
* Uninstalls a ListChangeListener on the children:
*/
protected void uninstallChildrenListener() {
getChildren().removeListener(childrenListener);
}
}
Usage example:
public class EmptyPlaceholdersInSkin extends Application {
private Parent createContent() {
// initially populated
//TableView<Person> table = new TableView<>(Person.persons()) {
// initially empty
TableView<Person> table = new TableView<>() {
#Override
protected Skin<?> createDefaultSkin() {
return new NoPlaceHolderTableViewSkin<>(this);
}
};
TableColumn<Person, String> first = new TableColumn<>("First Name");
first.setCellValueFactory(new PropertyValueFactory<>("firstName"));
table.getColumns().addAll(first);
Button clear = new Button("clear");
clear.setOnAction(e -> table.getItems().clear());
clear.disableProperty().bind(Bindings.isEmpty(table.getItems()));
Button fill = new Button("populate");
fill.setOnAction(e -> table.getItems().setAll(Person.persons()));
fill.disableProperty().bind(Bindings.isNotEmpty(table.getItems()));
BorderPane pane = new BorderPane(table);
pane.setBottom(new HBox(10, clear, fill));
return pane;
}
#Override
public void start(Stage stage) throws Exception {
stage.setScene(new Scene(createContent()));
stage.show();
}
public static void main(String[] args) {
Application.launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(EmptyPlaceholdersInSkin.class.getName());
}

I think I found a solution. It is definitely not nice, since it is accessing the API in a not wanted way, and I'm probably also making undesired use of the visibleProperty, but here you go:
You can try to hack the TableViewSkin. Basically do this to retrieve a hacked Skin:
public class ModifiedTableView<E> extends TableView<E> {
#Override
protected Skin<?> createDefaultSkin() {
final TableViewSkin<E> skin = new TableViewSkin<E>(this) {
// override method here
}
// modifiy skin here
return skin;
}
}
For the TableViewSkin you then need to override following method:
#Override
protected VirtualFlow<TableRow<E>> createVirtualFlow() {
final VirtualFlow<TableRow<E>> flow = new VirtualFlow<TableRow<E>>();
// make the 'scroll-region' always visible:
flow.visibleProperty().addListener((invalidation) -> {
flow.setVisible(true);
});
return flow;
}
And for the skin using reflection stop showing the placeholder:
final Field privateFieldPlaceholderRegion = TableViewSkinBase.class.getDeclaredField("placeholderRegion");
privateFieldPlaceholderRegion.setAccessible(true);
final StackPane placeholderRegion = (StackPane) privateFieldPlaceholderRegion.get(skin);
// make the 'placeholder' never visible:
placeholderRegion.visibleProperty().addListener((invalidation) -> {
placeholderRegion.setVisible(false);
});
Maybe you can change the visibility of the flow in the same method to make the code shorter... But I think you get the concept

I found a solution for javafx8. It makes use of the non-public api, but it uses no reflection (luckly). Basically you need to set (or replace) the skin of the TableView and return a non-zero value in the method getItemCount(). Like so:
(TableView)t.setSkin(new TableViewSkin<Object>(t)
{
#Override
public int getItemCount()
{
int r = super.getItemCount();
return r == 0 ? 1 : r;
}
});
This method can also be used to add an extra row at the bottom of your last item (for if you want to include an add button for example). Basically return always one higher than the actual item-count.
Eventhough this is an old question, hopefully this was helpfull to someone.

If any one is still looking for an alternate solution apart from what others had provided, below is the one which I worked with. As far as to me, this is the most simpliest approach I can go with (no custom skins, no API tweaking & no heavy controls like ListView).
Set a StackPane with a customized CSS that resembles alternate row coloring.
StackPane placeHolder = new StackPane();
placeHolder.setStyle("-fx-background-color:linear-gradient(from 50px 14px to 50px 40px , repeat, #e8e8e8 49% , #f7f7f7 12% );");
tableView.setPlaceholder(placeHolder);
Below is the quick reference for implementation. The left table is with data and the right table is without data showing customized placeholder.
If your are specific with showing the column lines as well, you can follow the #Shreyas Dave approach of building a HBox of the StackPane(s) with border implementation.
HBox placeHolder = new HBox();
tableView.getColumns().forEach(tc->{
StackPane colHolder = new StackPane();
colHolder.getStyleClass().add("table-view-place-holder");
colHolder.prefWidthProperty().bind(tc.widthProperty());
placeHolder.getChildren().add(colHolder);
});
tableView.setPlaceholder(placeHolder);
And the CSS implementation is as below:
.table-view-place-holder{
-fx-background-color:linear-gradient(from 50px 14px to 50px 40px , repeat, #232A2D 49% , #3D4549 12% );
-fx-border-width: 0px 1px 0px 0px;
-fx-border-color: linear-gradient(from 50px 14px to 50px 40px , repeat, #3D4549 49% , #232a2d 12% );
}
I have a requirement of having contrast border color to row background. I can easily acheive that with the above approach for having border color to my placeholder columns.

Here is a tricky way to perform your task,
HBox box = new HBox();
box.setDisable(true);
for (TableColumn column : patientsTable.getColumns()) {
ListView<String> listView = new ListView<>();
listView.getItems().add("");
listView.setPrefWidth(column.getWidth());
box.getChildren().add(listView);
}
tableView.setPlaceholder(box);

Related

How can I bind stage width and height in JavaFX?

I want to bind the stage width and height together, so the user can only resize it by keeping the aspect ration.
This doesn't work:
stage.widthProperty().bind(stage.heightProperty());
There is another way:
stage.minHeightProperty().bind(scene.widthProperty().multiply(1.3));
stage.maxHeightProperty().bind(scene.widthProperty().multiply(1.3));
But in this way I only set the width value.
How could I solve this?
Thanks,
Tibor
Since the width and height properties are read-only you cannot bind them to anything, let alone each other. The reason they're read-only is documented:
Many of the Stage properties are read only because they can be changed externally by the underlying platform and therefore must not be bindable [because bound properties cannot be set].
Both the width and height properties have similar statements in their documentation.
You can still add a listener to each property and, when one property changes, set the other property to the new value. To make sure this doesn't lead to a StackOverflowError you'll have to track if you're currently setting the value in the listener. For example:
// not actually "binding" in the context of Property.bind(...)
public static void bindWidthAndHeightTogether(Window window, double widthToHeightRatio) {
ChangeListener<Number> listener =
new ChangeListener<>() {
private boolean changing;
#Override
public void changed(ObservableValue<? extends Number> obs, Number ov, Number nv) {
if (!changing) {
changing = true;
try {
if (obs == window.widthProperty()) {
window.setHeight(nv.doubleValue() / widthToHeightRatio);
} else {
window.setWidth(nv.doubleValue() * widthToHeightRatio);
}
} finally {
changing = false;
}
}
}
};
window.widthProperty().addListener(listener);
window.heightProperty().addListener(listener);
}
The above worked for me on Windows 10 using JavaFX 14. Note that it prevents the window from being maximized properly but not from going full-screen (at least on Windows 10).

JavaFX: How to move "drop down arrow" in TitledPane to be on right

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)

How to set cell's indentation in TreeView in Javafx

I am working on a TreeView which represents a robot controlling program, each TreeCell represents a statement, and a TreeCell can be nested in an other one. Like in programming, statements can be nested in if or for statements.
Here I have created a simple demo, filled with some random blocks.
Demo Screenshot
To customize the rendering of TreeCell, I have create a class extending TreeCell:
public class TreeDataCell extends TreeCell<TreeData> {
public void updateItem(TreeData item, boolean empty) {
super.updateItem(item, empty);
setText(null);
if (item == null || empty) {
setGraphic(null);
} else {
setGraphic(getCellGraphic(item));
}
}
private Group getCellGraphic(TreeData data) {
Group grp = new Group();
VBox vbox = new VBox();
vbox.setMinWidth(100);
vbox.setMaxWidth(200);
vbox.setBorder(new Border(new BorderStroke(
Color.LIGHTGRAY.darker(),
BorderStrokeStyle.SOLID,
new CornerRadii(10.0),
new BorderWidths(2.0))));
vbox.setBackground(new Background(new BackgroundFill(Color.LIGHTGRAY, new CornerRadii(10.0), null)));
vbox.setEffect(new DropShadow(2.0, 3.0, 3.0, Color.DIMGRAY));
Region header = new Region();
header.setPrefHeight(5.0);
Region footer = new Region();
footer.setPrefHeight(5.0);
Label labTitle = new Label();
labTitle.setFont(new Font("San Serif", 20));
labTitle.setText(data.getTitle());
Label labDesc = null;
if (data.getDescription() != null) {
labDesc = new Label();
labDesc.setWrapText(true);
labDesc.setText(data.getDescription());
}
vbox.getChildren().addAll(header, labTitle);
if (labDesc != null) {
vbox.getChildren().add(labDesc);
}
vbox.getChildren().add(footer);
grp.getChildren().add(vbox);
return grp;
}
}
The TreeData is a simple class containing 2 Strings:
public class TreeData {
private String title;
private String desc;
/* getters + setters */
}
As you can see, the indentation between two levels are too small, and we can barely see statement nesting.
I am hard coding all the styles in Java, since I haven't learnt FXML+CSS yet.
I'd like to know if it is possible to set the size of indentation in Java? I cannot find any API for this purpose. In addition, is it possible to draw lines between parent node and its children like JTree in Swing ?
Thank you.
Regarding having lines like in JTree, there is no built in way to do that as of JavaFX 11. There is a feature request (JDK-8090579) but there doesn't seem to be any plans to implement it. You may be able to implement it yourself but I'm not sure how.
As to modifying the indent of the TreeCells, the easiest way is by using CSS.
As documented in the JavaFX CSS Reference Guide, TreeCell has a CSS property named -fx-indent whose value is a <size>. You can set this property by using a stylesheet or inline it via the style property. An example using inline styles:
public class TreeDataCell extends TreeCell<TreeData> {
public TreeDataCell() {
setStyle("-fx-indent: <size>;");
}
}
However, since you are currently not using CSS or FXML, there is another option that is purely code: Modifying the indent property of TreeCellSkin. This class became public API in JavaFX 9. There may be equivalent internal API in JavaFX 8 but I'm not sure.
By default, the Skin of a TreeCell will be an instance of TreeCellSkin. This means you can get this skin and set the indent value as needed. You have to be careful, though, as the skin is lazily created; it won't necessarily be available until the TreeView is actually part of a showing window.
If you only want to set the property once, one way is to intercept the skin inside createDefaultSkin():
public class TreeDataCell extends TreeCell<TreeData> {
#Override
protected Skin<?> createDefaultSkin() {
TreeCellSkin<?> skin = (TreeCellSkin<?>) super.createDefaultSkin();
skin.setIndent(/* your value */);
return skin;
}
}
You could also extend TreeCellSkin and customize it. Just remember to override createDefaultSkin() and return you custom skin implementation.

Number/Axis: how to force an immediate update of internal state during layout?

Triggered by a question about Slider and a slight bug, I tried to implement a SliderSkin that makes use of axis services, in particular its conversion methods between pixel and value. Works fine except that NumberAxis keeps its conversion offset and scale factor as internal fields that are updated only during a layout pass.
Which poses a problem, if I want to use the conversion during a layout pulse for updating another collaborator: in the case of a Slider that would be the thumb.
Below is a small examples to demonstrate the problem: simply a NumberAxis and a CheckBox at some value. On startup, the box is placed at the value at the middle. For maximal effect, maximize the window and note that the box position is not changed - now sitting near the beginning of the axis. Actually, it's the same when resizing the window but not so visible - see the printout of the difference.
Options to make it work
delay the thumb positioning until the next layout pulse (feels clumsy)
force the axis to update immediately
Looking for a way for the latter (couldn't find anything except reflectively invoke the axis' layoutChildren).
The example:
public class AxisInvalidate extends Application {
public static class AxisInRegion extends Region {
NumberAxis axis;
Control thumb;
IntegerProperty value = new SimpleIntegerProperty(50);
private double thumbWidth;
private double thumbHeight;
public AxisInRegion() {
axis = new NumberAxis(0, 100, 25);
thumb = new CheckBox();
getChildren().addAll(axis, thumb);
}
#Override
protected void layoutChildren() {
thumbWidth = snapSize(thumb.prefWidth(-1));
thumbHeight = snapSize(thumb.prefHeight(-1));
thumb.resize(thumbWidth, thumbHeight);
double axisHeight = axis.prefHeight(-1);
axis.resizeRelocate(0, getHeight() /4, getWidth(), axisHeight);
// this marks the layout as dirty but doesn't immediately update internals
// doesn't make a difference, shouldn't be needed anyway
//axis.requestAxisLayout();
double pixelOnAxis = axis.getDisplayPosition(value.getValue());
Platform.runLater(() -> {
LOG.info("diff " + (pixelOnAxis - axis.getDisplayPosition(value.getValue())));
});
// moving this line into the runlater "solves" the problem
thumb.relocate(pixelOnAxis, getHeight() /4);
}
}
private Parent getContent() {
AxisInRegion region = new AxisInRegion();
BorderPane content = new BorderPane(region);
return content;
}
#Override
public void start(Stage primaryStage) throws Exception {
primaryStage.setScene(new Scene(getContent(), 500, 200));
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger.getLogger(AxisInvalidate.class
.getName());
}
Actually, I think it's a bug: the value/pixel conversion is a public service of the Axis - that should work always.
Not sure if it helps for your initial issue but simply do the calculation manually. Another plus - request layout becomes needless and can be removed.
double range = axis.getUpperBound()-axis.getLowerBound();
double pixelOnAxis = axis.getWidth()*(value.get()-axis.getLowerBound())/range;
Just got a working hack from the bug report: we need to call axis.layout() after changing size/location and before querying the conversion methods, something like:
axis.resizeRelocate(0, getHeight() /4, getWidth(), axisHeight);
// doesn't make a difference, shouldn't be needed anyway
//axis.requestAxisLayout();
// working hack from bug report:
axis.layout();
double pixelOnAxis = axis.getDisplayPosition(value.getValue());
thumb.relocate(pixelOnAxis, getHeight() /4);

How to queue some part of a program

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.

Categories