JavaFX Tab fit full size of header - css

I want the tabs in my tabpane to fit the complete size of the tabpane which they are in. So basically there shall be no header area visible, everything should be covered by the tabs.
I have 2 problems here:
How can I make the tabs dynamically fit the width of the tabpane?
How can I fit them to the correct height, and remove the little spaces between the tabs? I suppose this is done via css, but I don't quite know how.
greets

You can simply add a listener to the TabPane to detect any changes and adjust the width and/or height of the tabs accordingly.
#FXML
private JFXTabPane tabPane;
Divide the width by number of tabs:
tabPane.widthProperty().addListener((observable, oldValue, newValue) ->
{
tabPane.setTabMinWidth(tabPane.getWidth() / tabPane.getTabs().size());
tabPane.setTabMaxWidth(tabPane.getWidth() / tabPane.getTabs().size());
});

It's true, the property objects of tabs are READ-Only. This means that you cannot manually set the properties. However, the values of the read-only properties can be bound to other properties, also the properties have some manipulation methods e.g. divide, subtract, please check the documentation for extended information.
You can call the .tabminWidthProperty() on the tabpane and bind it to the width-property value of the parent of the layout.
Here's the code to size all available tabs equally filling the whole screen:
tabpane.tabMinWidthProperty().bind(root.widthProperty().divide(tabpane.getTabs().size()).subtract(20));
I subtracted 20 to avoid the horizontal/vertical scrollbar. Surely there is also a way to disable them to pop up at all, but I'm an Android Dev and a greenhorn in JavaFx.

I'm not sure if this is do-able in CSS, and there might be a simpler way in Java to do this, but I wrote a class that extends TabPane in order to stretch the tabs to fill all the space.
public class StretchedTabPane extends TabPane {
public StretchedTabPane() {
super();
setUpChangeListeners();
}
public StretchedTabPane(Tab... tabs) {
super(tabs);
setUpChangeListeners();
}
private void setUpChangeListeners() {
widthProperty().addListener(new ChangeListener<Number>() {
#Override public void changed(ObservableValue<? extends Number> value, Number oldWidth, Number newWidth) {
Side side = getSide();
int numTabs = getTabs().size();
if ((side == Side.BOTTOM || side == Side.TOP) && numTabs != 0) {
setTabMinWidth(newWidth.intValue() / numTabs - (20));
setTabMaxWidth(newWidth.intValue() / numTabs - (20));
}
}
});
heightProperty().addListener(new ChangeListener<Number>() {
#Override public void changed(ObservableValue<? extends Number> value, Number oldHeight, Number newHeight) {
Side side = getSide();
int numTabs = getTabs().size();
if ((side == Side.LEFT || side == Side.RIGHT) && numTabs != 0) {
setTabMinWidth(newHeight.intValue() / numTabs - (20));
setTabMaxWidth(newHeight.intValue() / numTabs - (20));
}
}
});
getTabs().addListener(new ListChangeListener<Tab>() {
public void onChanged(ListChangeListener.Change<? extends Tab> change){
Side side = getSide();
int numTabs = getTabs().size();
if (numTabs != 0) {
if (side == Side.LEFT|| side == Side.RIGHT) {
setTabMinWidth(heightProperty().intValue() / numTabs - (20));
setTabMaxWidth(heightProperty().intValue() / numTabs - (20));
}
if (side == Side.BOTTOM || side == Side.TOP) {
setTabMinWidth(widthProperty().intValue() / numTabs - (20));
setTabMaxWidth(widthProperty().intValue() / numTabs - (20));
}
}
}
});
}
}
This will automatically adjust the width of each tab when the height, width or number of tabs changes.

It can be done directly in fxml file and can be used on any Node no having fitToWidth or fitToHeight property. See small example below, hope it helps somebody.
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Tab?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane xmlns="http://javafx.com/javafx"
xmlns:fx="http://javafx.com/fxml"
fx:controller="somepackage.MainWindowController"
prefHeight="400.0" prefWidth="600.0"
fx:id="mainPane">
<TabPane prefHeight="${mainPane.height}" prefWidth="${mainPane.width}" style="-fx-background-color: blue">
<Tab fx:id="someTab">
<Label text="bla bla">
</Tab>
</TabPane>
I'd like to pinpoint the main part which causes extension of the Tab:
prefHeight="${mainPane.height}" prefWidth="${mainPane.width}"

Related

How to set TableColumn to the width it needs? [duplicate]

afaik The TableView in javafx have 2 column resize policies: CONSTRAINED_RESIZE_POLICY and UNCONSTRAINED_RESIZE_POLICY, but I want columns is resized to fit the content of theirs cells
I think it's a simple problem in other platform (like datagridview in C#) but can not resolve
After 3 years I come back to this problem again, some suggestions are calculating the size of text of data in each cell (it's complicated depending on font size, font family, padding...)
But I realize that when I click on the divider on table header, it's resized fit to content as I want. So I dig into JavaFX source code I finally found resizeColumnToFitContent method in TableViewSkin, but it is protected method, we can resolve by reflection:
import com.sun.javafx.scene.control.skin.TableViewSkin;
import javafx.scene.control.Skin;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class GUIUtils {
private static Method columnToFitMethod;
static {
try {
columnToFitMethod = TableViewSkin.class.getDeclaredMethod("resizeColumnToFitContent", TableColumn.class, int.class);
columnToFitMethod.setAccessible(true);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
public static void autoFitTable(TableView tableView) {
tableView.getItems().addListener(new ListChangeListener<Object>() {
#Override
public void onChanged(Change<?> c) {
for (Object column : tableView.getColumns()) {
try {
columnToFitMethod.invoke(tableView.getSkin(), column, -1);
} catch (IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
}
}
});
}
}
Note that we call "tableView.getItems()" so we have to call this function after setItems()
After testing the previous solutions I finally found one that worked for me.
So here is mine (call the method after inserting the data into table):
public static void autoResizeColumns( TableView<?> table )
{
//Set the right policy
table.setColumnResizePolicy( TableView.UNCONSTRAINED_RESIZE_POLICY);
table.getColumns().stream().forEach( (column) ->
{
//Minimal width = columnheader
Text t = new Text( column.getText() );
double max = t.getLayoutBounds().getWidth();
for ( int i = 0; i < table.getItems().size(); i++ )
{
//cell must not be empty
if ( column.getCellData( i ) != null )
{
t = new Text( column.getCellData( i ).toString() );
double calcwidth = t.getLayoutBounds().getWidth();
//remember new max-width
if ( calcwidth > max )
{
max = calcwidth;
}
}
}
//set the new max-widht with some extra space
column.setPrefWidth( max + 10.0d );
} );
}
I think just by overriding a call back function that returns true will solve your problem it will disable the re-sizing of columns and all columns will be re-sized to fit the content of their cells.
Example:
TableView<String[]> table = new TableView<>();
table.setColumnResizePolicy(new Callback<TableView.ResizeFeatures, Boolean>() {
#Override
public Boolean call(ResizeFeatures p) {
return true;
}
});
If you want that only one column fills the remaining width of a table, I have found a pretty straight forward solution, which is short and does not require the hacky reflection solution described above:
DoubleBinding usedWidth = columnA.widthProperty().add(columnB.widthProperty()).add(columnC.widthProperty());
fillingColumn.prefWidthProperty().bind(tableView.widthProperty().subtract(usedWidth));
Or to make it short:
// automatically adjust width of columns depending on their content
configAttributeTreeTable.setColumnResizePolicy((param) -> true );
I have used the other solutions on this question, and it works pretty good. However, the downside of this is when the width of the TableView is greater than the required width of the TableColumns together. I have created a hack to solve this problem, and it works OK:
orderOverview.setColumnResizePolicy((param) -> true );
Platform.runLater(() -> FXUtils.customResize(orderOverview));
where FXUtils.customResize() is created as follows:
public static void customResize(TableView<?> view) {
AtomicDouble width = new AtomicDouble();
view.getColumns().forEach(col -> {
width.addAndGet(col.getWidth());
});
double tableWidth = view.getWidth();
if (tableWidth > width.get()) {
TableColumn<?, ?> col = view.getColumns().get(view.getColumns().size()-1);
col.setPrefWidth(col.getWidth()+(tableWidth-width.get()));
}
}
I hope this could be helpful for other people as well!
This is the way I found :
tableview.setColumnResizePolicy( TableView.CONSTRAINED_RESIZE_POLICY );
idCol.setMaxWidth( 1f * Integer.MAX_VALUE * 50 ); // 50% width
nameCol.setMaxWidth( 1f * Integer.MAX_VALUE * 30 ); // 30% width
ageCol.setMaxWidth( 1f * Integer.MAX_VALUE * 20 ); // 20% width
This code autoresizes all column widths in relational proportions to the table width,
while it can fix the first column width to a given value when table width is lower than x
// To generalize the columns width proportions in relation to the table width,
// you do not need to put pixel related values, you can use small float numbers if you wish,
// because it's the relative proportion of each columns width what matters here:
final float[] widths = { 1.2f, 2f, 0.8f };// define the relational width of each column
// whether the first column should be fixed
final boolean fixFirstColumm = true;
// fix the first column width when table width is lower than:
final float fixOnTableWidth = 360; //pixels
// calulates sum of widths
float sum = 0;
for (double i : widths) {
sum += i;
}
// calculates the fraction of the first column proportion in relation to the sum of all column proportions
float firstColumnProportion = widths[0] / sum;
// calculate the fitting fix width for the first column, you can change it by your needs, but it jumps to this width
final float firstColumnFixSize = fixOnTableWidth * firstColumnProportion;
// set the width to the columns
for (int i = 0; i < widths.length; i++) {
table.getColumns().get(i).prefWidthProperty().bind(table.widthProperty().multiply((widths[i] / sum)));
// ---------The exact width-------------^-------------^
if (fixFirstColumm)
if (i == 0) {
table.widthProperty().addListener(new ChangeListener<Number>() {
#Override
public void changed(ObservableValue<? extends Number> arg0, Number oldTableWidth, Number newTableWidth) {
if (newTableWidth.intValue() <= fixOnTableWidth) {
// before you can set new value to column width property, need to unbind the autoresize binding
table.getColumns().get(0).prefWidthProperty().unbind();
table.getColumns().get(0).prefWidthProperty().setValue(firstColumnFixSize);
} else if (!table.getColumns().get(0).prefWidthProperty().isBound()) {
// than readd the autoresize binding if condition table.width > x
table.getColumns().get(0).prefWidthProperty()
.bind(table.widthProperty().multiply(firstColumnProportion));
}
}
});
}
}
advice to put the code in an separated TableAutoresizeModel class, there you can handle further calculations, for example on hiding columns add listener...
#HarleyDavidson 's answer in kotlin
val String.fxWidth: Double
get() = Text(this).layoutBounds.width
// call the method after inserting the data into table
fun <T> TableView<T>.autoResizeColumns() {
columnResizePolicy = TableView.UNCONSTRAINED_RESIZE_POLICY
columns.forEach { column ->
column.setPrefWidth(
(((0 until items.size).mapNotNull {
column.getCellData(it)
}.map {
it.toString().fxWidth
}.toMutableList() + listOf(
column.text.fxWidth
)).maxOrNull() ?: 0.0) + 10.0
)
}
}
This will set the minimum width of columns based on the font and the text, so that the column names wont be cropped.
public static void setDataTableMinColumnWidth(TableView<?> dataTable)
{
for (Node columnHeader : dataTable.lookupAll(".column-header"))
{
var columnString = columnHeader.getId();
if (columnString != null)
{
for (Node columnHeaderLabel : columnHeader.lookupAll(".label"))
{
var tableColumn = dataTable.getColumns()
.stream()
.filter(x -> x.getId()
.equals(columnString))
.findFirst();
if (columnHeaderLabel instanceof Label && tableColumn.isPresent())
{
var label = (Label) columnHeaderLabel;
/* calc text width based on font */
var theText = new Text(label.getText());
theText.setFont(label.getFont());
var width = theText.getBoundsInLocal()
.getWidth();
/*
* add 10px because of paddings/margins for the button
*/
tableColumn.get()
.setMinWidth(width + 10);
}
}
}
}
}
How to use:
dataTable.needsLayoutProperty()
.addListener((obs, o, n) -> setDataTableMinColumnWidth(dataTable));
For the Columns, the id property needs to be set first:
TableColumn<BundImportTask, String> columnTask = new TableColumn<>("My Column");
columnTask.setId("MyColumnId");
columnTask.setCellValueFactory(data -> new SimpleStringProperty(data.getValue()
.fileName()));
I implemented a solution that it's fairly more complicated than the ones that I found here, but that allows a specific column to be resized by double clicking on the header, while still letting the user resize columns manually.
This is achieved by listening to click events on the header of the table (TableHeaderRow). When a double click occurs, the specific column header is found by matching the mouse event X and Y.
Note: to make this work it's necessary that each column has an ID set.
// when skin is loaded (hence css), setup click listener on header to make column fit to content max width on double click
tableView.skinProperty().addListener((a, b, newSkin) -> {
TableHeaderRow headerRow = (TableHeaderRow) tableView.lookup("TableHeaderRow");
NestedTableColumnHeader headers = (NestedTableColumnHeader) (headerRow.getChildren().get(1));
headerRow.setOnMouseClicked(evt -> {
if (evt.getClickCount() != 2 || evt.getButton() != MouseButton.PRIMARY) return;
// find the header column that contains the click
for (TableColumnHeader header : headers.getColumnHeaders()) {
if (header.contains(header.parentToLocal(evt.getX(), evt.getY()))) {
fitColumnWidthToContent(header.getId());
}
}
evt.consume();
});
});
The method that takes care of the resizing is the following:
private void fitColumnWidthToContent (String colId) {
// find column matching id
TableColumn column = null;
for (TableColumn tempCol : tableView.getColumns()) {
if (tempCol.getId().equals(colId)) {
column = tempCol;
break;
}
}
if (column == null) {
throw new IllegalStateException("Column ID doesn't match any actual column");
}
// set default width to column header width
Text text = new Text(column.getText());
double max = text.getLayoutBounds().getWidth();
for (int i = 0; i < tableView.getItems().size(); i++ ) {
if (column.getCellData(i) == null) continue;
text = new Text(column.getCellData(i).toString());
double textWidth = text.getLayoutBounds().getWidth();
if (textWidth > max) {
max = textWidth;
}
}
column.setPrefWidth(max + 12);
}
I hope this can be useful to anyone.
In order to allow also manual resizing, it's necessary to add a bit more code on table initalization:
// listen to width changes in columns and set to pref width (otherwise if for example width changes because of
// user resizing the column, applying the old pref width won't work because it stayed the same)
for (TableColumn col : tableView.getColumns()) {
col.widthProperty().addListener((obs, oldVal, newVal) -> {
col.setPrefWidth(newVal.doubleValue());
});
}
I have implemented a solution for TreeTableView. It is still in evolution but it manifests now promising results. Hereafter a description of the solution.
In the control skin class, I added to the control children the TreeTableView and an invisible VBox. A cell factory provide derived cells to the target TreeTableColumn. The derived cells wrap a Label node which is added or removed to the invisible VBox according to the empty property, and which its prefWidth is set according to the cell width. The cells make use of:
getProperties().put(Properties.DEFER_TO_PARENT_PREF_WIDTH, Boolean.TRUE)
I override the cell's computePrefWidth() method as follow:
#Override
protected double computePrefWidth(double height) {
return Double.max(_box.prefWidth(-1.0), super.computePrefWidth(height) + 24.0);
}
The Vbox width property is bind to the TreeTableColumn's prefWidth. This is required to resize as well the header of the column.
Is worth to note, that at the time being, to simplify the development of a solution, this approach works well with built in sort, order, and resize feature disabled. Ie.
_nameColumn = new TreeTableColumn<>("Name");
_nameColumn.setResizable(false);
_nameColumn.setReorderable(false);
_nameColumn.setSortable(false);
Happy coding
After long research. Best Solution is..
tblPlan.setColumnResizePolicy((param) -> true );
Platform.runLater(() -> customResize(tblPlan));
"Custom Resize"
public void customResize(TableView<?> view) {
AtomicLong width = new AtomicLong();
view.getColumns().forEach(col -> {
width.addAndGet((long) col.getWidth());
});
double tableWidth = view.getWidth();
if (tableWidth > width.get()) {
view.getColumns().forEach(col -> {
col.setPrefWidth(col.getWidth()+((tableWidth-width.get())/view.getColumns().size()));
});
}
}
<TableView fx:id="datalist" layoutX="30.0" layoutY="65.0" prefHeight="400.0" AnchorPane.bottomAnchor="100.0" AnchorPane.leftAnchor="30.0" AnchorPane.rightAnchor="30.0" AnchorPane.topAnchor="100.0">
<columns>
<TableColumn fx:id="number" minWidth="-1.0" prefWidth="-1.0" style="width: auto;" text="number" />
<TableColumn fx:id="id" minWidth="-1.0" prefWidth="-1.0" text="id" />
<TableColumn fx:id="name" minWidth="-1.0" prefWidth="-1.0" text="name" />
<TableColumn fx:id="action" minWidth="-1.0" prefWidth="-1.0" text="todo" />
</columns>
**<columnResizePolicy>
<TableView fx:constant="CONSTRAINED_RESIZE_POLICY" />
</columnResizePolicy>**
</TableView>

Moving an image using mouse drag events

I am currently making a game in javafx, and what i need is to be able to make an image move left and right using mouse drag events, while the image is moving down. I have figured the latter part(the moving down part), and require assistance in adding the drag event to my image view object, so that on dragging it to the right is one event and dragging it to left is another event.
Thank you in advance.
This is my fxml file
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.Button?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0" prefWidth="600.0" xmlns="http://javafx.com/javafx/9.0.1" xmlns:fx="http://javafx.com/fxml/1" fx:controller="application.Controller">
<children>
<ImageView fx:id="player" fitHeight="42.0" fitWidth="98.0" layoutX="47.0" layoutY="27.0" pickOnBounds="true" preserveRatio="true">
<image>
<Image url="#freefall.png" />
</image>
</ImageView>
<Button fx:id="button" layoutX="44.0" layoutY="21.0" mnemonicParsing="false" prefHeight="54.0" prefWidth="81.0" style="-fx-background-color: transparent; -fx-text-fill: transparent; -fx-border-fill: transparent;" text="Button" />
</children>
</AnchorPane>
This is my Controller Class
package application;
import java.net.URL;
import java.util.ResourceBundle;
import javafx.animation.TranslateTransition;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.image.ImageView;
import javafx.util.Duration;
public class Controller implements Initializable {
#FXML
ImageView player;
TranslateTransition transition = new TranslateTransition();
private double startDragX;
private double startDragY;
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
player.setOnMousePressed(e -> {
startDragX = e.getSceneX();
startDragY = e.getSceneY();
});
player.setOnMouseDragged(e -> {
double X = e.getSceneX();
if(X - startDragX > 0)
player.setTranslateX(5);
else if (X - startDragX < 0)
player.setTranslateX(-5);
});
//This code makes the player move downwards continuously
transition.setDuration(Duration.seconds(15));
transition.setNode(player);
transition.setToY(800);
transition.play();
}
}
The problem i am facing after changing the code is that the player moves, when you drag it once to the right. But when u again drag it to the right nothing happens, all I can do is drag it back to its original position (by dragging it back to the left).
I think you already have the basics in your example.
There is something that I don't understand though: why is there a transition? If you want the ImageView to follow your mouse cursor as you drag, then it would be weird to mix transition in (i.e. it would appear like a lag to users).
This is the basic way to move your ImageView with drag event.
public class Controller implements Initializable {
#FXML
ImageView player;
private double startDragX;
private double startDragY;
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
button.setMousePressed(e -> {
startDragX = e.getSceneX();
startDragY = e.getSceneY();
});
button.setMouseDragged(e -> {
button.setTranslateX(e.getSceneX() - startDragX);
button.setTranslateY(e.getSceneY() - startDragY);
});
}
}
Of course, you can choose to add the events in FXML and inject the event methods with #FXML annotation.
Edit
After having understood the question, I think this is what you want (or at least almost). I assumed that you want to achieve an effect similar to "flick" on a touch mobile device, except that this is done with mouse.
public class Controller implements Initializable {
#FXML
ImageView player;
private static final double MIN_FLICK_PIXELS = 10;
private static final double FLICK_MOVEMENT = 5;
private enum Direction {
LEFT, RIGHT
}
private double lastXPosition;
private Direction lastFlickDirection = null;
TranslateTransition transition = new TranslateTransition();
#Override
public void initialize(URL arg0, ResourceBundle arg1) {
player.setOnMousePressed(e -> {
lastXPosition = e.getSceneX();
});
player.setOnMouseDragged(e -> {
double currentX = e.getSceneX();
// Detect as "right flick" if the previous flick direction is not right, and the dragged pixels is more than 10
if (lastFlickDirection != Direction.RIGHT && currentX - lastXPosition > MIN_FLICK_PIXELS) {
direction = Direction.RIGHT;
player.setTranslateX(player.getTranslateX() + FLICK_MOVEMENT);
lastXPosition = currentX;
}
// Detect as "left flick" if the previous flick direction is not left, and the dragged pixels is more than -10
else if (lastFlickDirection != Direction.LEFT && currentX - lastXPosition < -MIN_FLICK_PIXELS) {
direction = Direction.LEFT;
player.setTranslateX(player.getTranslateX() -FLICK_MOVEMENT);
lastXPosition = currentX;
}
});
player.setOnMouseReleased(e -> {
lastFlickDirection = null;
});
//This code makes the player move downwards continuously
transition.setDuration(Duration.seconds(15));
transition.setNode(player);
transition.setToY(800);
transition.play();
}
}
Doing this will:
Shift the image 5 pixels to the right or left when user drag at least 10 pixels to the corresponding direction.
If the user drags 20 pixels continuously to the same direction, it only shift once.
If the user, within a single action, dragged 10 pixels to the right, then drag 10 pixels back to the left, the image will shift 5 pixels right, then 5 pixels left (which is original position).
One possible obvious flaw here is that the events are set on the ImageView, so the events may be lost if the cursor goes outside the bounds of the ImageView (it might not be lost, I didn't test this). If a problem occurs, shift the events out to perhaps its parent.

JavaFX TabPane selection wont work properly

I have two *.fxml forms with controllers. First is Window, second - ProductPane.
Simplified Window.fxml is:
<BorderPane prefWidth="650.0" prefHeight="450.0" fx:controller="package.WindowController">
<center>
<TabPane fx:id="tabsPane">
<tabs>
<Tab fx:id="productsTab" text="Products"/>
<Tab fx:id="warehouseTab" text="Warehouse"/>
<Tab fx:id="saleTab" text="Sale"/>
</tabs>
</TabPane>
</center>
</BorderPane>
Controller for Window.fxml:
public class WindowController {
#FXML
private TabPane tabsPane;
#FXML
private Tab productsTab;
#FXML
void initialize() {
sout("Main Window initialization...");
tabsPane.getSelectionModel().selectedIndexProperty().addListener((e, o, n) -> {
sout("Changed to " + n);
});
tabsPane.getSelectionModel().selectedItemProperty().addListener((e, o, n) -> {
sout("New item: " + n);
// Load ProductPane content:
if(n == productsTab) {
try {
Parent p = FXMLLoader.load(getClass().getResource("productPane.fxml"));
n.setContent(p);
} catch(IOException ex) {
ex.printStackTrace();
}
}
});
sout("Select first item...");
tabsPane.getSelectionModel().selectFirst();
// This methods also don't work
// tabsPane.getSelectionModel().clearAndSelect();
// tabsPane.getSelectionModel().select(productTab);
// tabsPane.getSelectionModel().select(0);
}
}
The problem is: when I load Window.fxml in main() and launch it appearse window with empty first tab.
Debug output:
Main Window initialization...
Select first item...
But ProductPane not loaded and listener do not call. If I switch between tabs in Window, listeners are triggered and Products tab loaded properly.
What's the problem?
You added a ChangeListener to the tab pane's selection model, which of course gets notified when the selection changes. By default, the first tab is selected, so at the time the change listener is added, the first tab is already selected. This means when you call selectFirst(), the selection doesn't change (because you're asking to select the tab that is already selected), so the listener isn't notified.
The solution is a little ugly: you just need to directly load your products tab content if the products tab is selected at the time you add the listener. I would factor that code into a separate method to avoid too much repetition:
#FXML
void initialize() {
System.out.println("Main Window initialization...");
tabsPane.getSelectionModel().selectedIndexProperty().addListener((e, o, n) -> {
System.out.println("Changed to " + n);
});
tabsPane.getSelectionModel().selectedItemProperty().addListener((e, o, n) -> {
System.out.println("New item: " + n);
// Load ProductPane content:
if(n == productsTab) {
loadProductsTab();
}
});
if (tabPane.getSelectionModel().getSelectedItem() == productsTab) {
loadProductsTab();
}
}
private void loadProductsTab() {
try {
Parent p = FXMLLoader.load(getClass().getResource("productPane.fxml"));
productsTab.setContent(p);
} catch(IOException ex) {
ex.printStackTrace();
}
}
If you find you need this kind of functionality a lot, you might be interested in the ReactFX framework, which (I think) has built-in functionality that handles these kinds of cases.

Javafx, TextArea caret doesn't move back to first line on clear

I'm having a hard time by setting back the cursor to position 0 in the first line, after clearing the text from the textarea.
Problem Background
I have a textarea, which listens to keyevents. The textarea only listens to Enter key, if found, either submits the text or clears the text if there is a "\n" in the text area.
I tried all of the following, but none really worked out.
textArea.setText("")
textArea.clear()
textArea.getText().replace("\n", "")
Remove focus from it and put it back again.
Here is a test project that is runnable and demonstrates the problem.
The Main class:
public class Main extends Application {
Stage primaryStage;
AnchorPane pane;
public void start(Stage primaryStage){
this.primaryStage = primaryStage;
initMain();
}
public void initMain(){
try {
FXMLLoader loader = new FXMLLoader();
loader.setLocation(Main.class.getResource("main.fxml"));
pane = loader.load();
Controller controller = loader.getController();
Scene scene = new Scene(pane);
primaryStage.setScene(scene);
primaryStage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String args[]){
launch();
}
}
The Controller class:
public class Controller {
#FXML
TextArea textArea;
public void initialize() {
textArea.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent keyEvent) {
if (keyEvent.getCode() == KeyCode.ENTER) {
if (!textArea.getText().equals("")
&& !textArea.getText().contains("\n")) {
handleSubmit();
}
if (textArea.getText().contains("\n")) {
handleAsk();
}
}
}
});
}
/**
* After the user gives in a short input, that has no \n, the user submits by hitting enter.
* This method will be called, and the cursor jumps over to the beginning of the next line.
*/
public void handleSubmit(){
System.out.println("Submitted");
}
/**
* When this method is calls, the cursor is not in the first line.
* This method should move the cursor to the first position in the first line in the completely
* cleared text area.
*/
public void handleAsk(){
System.out.println("Asking and clearing text area.");
textArea.setText("");
}
}
The fxml:
<?xml version="1.0" encoding="UTF-8"?>
<?import javafx.scene.control.*?>
<?import java.lang.*?>
<?import javafx.scene.layout.*?>
<?import javafx.scene.layout.AnchorPane?>
<AnchorPane prefHeight="317.0" prefWidth="371.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1" fx:controller="Controller">
<children>
<TextArea fx:id="textArea" layoutX="78.0" layoutY="59.0" prefHeight="114.0" prefWidth="200.0" />
</children>
</AnchorPane>
My problem is, that the cursor won't jump back...
I found a short solution: after I called the desired method (handleAsk()), that should clear the text area when its finished, I call: keyEvent.consume();
This consumes the default effect of ENTER.
So: First, the event handler that you explicitly define does it's job, and then you can decide if you want to have as a "side effect" the default effect of the given key event, if not, you can consume it.
So, either your key handler will be invoked before the text is changed (due to something being typed), or it will be invoked after the text is changed.
You are presumably hoping it will be invoked before, since otherwise
textArea.getText().contains("\n")
would always evaluate to true (since the user just pressed the Enter key).
But in this case, on the second press of Enter, you will clear the text before the text is then modified. So you clear the text, then the new line is added (from the user pressing Enter). Hence the blank line in the text area.
You probably don't want to rely on the order of the events being processed anyway. It turns out that the text is modified on keyTyped events (I think), but that's not documented, and so there's really no guarantee of it. The safer way is to listen to changes in the text, and then to count the number of newlines:
textArea.textProperty().addListener((obs, oldText, newText) -> {
int oldNewlines = countNewlines(oldText); // note, can be more efficient by caching this
int numNewlines = countNewlines(newText);
if (numNewlines == 1 && oldNewlines != 1) {
// submit data
} else if (numNewlines == 2) {
textArea.clear();
}
});
With
private int countNewlines(String text) {
int newlines = 0 ;
while (text.indexOf("\n") >= 0) {
newlines++ ;
text = text.substring(text.indexOf("\n") + 1);
}
return newlines ;
}
(or some other implementation, eg using regex).

How to traverse the entire scene graph hierarchy?

I would like to write a method that will print the entire scene graph (including the names of the nodes and the css properties associated with the nodes) to the console. This would enable viewing the hierarchy of the scene graph and all the css attributes associated with each element. I tried to do this (see the scala code below) but my attempt failed.
The scene graph I'm testing this on contains a custom controller that I made following this tutorial. It is not printing any of the nested controls contained in the custom controller. The custom controller appears fine on the stage (and it functions properly) so I know all the required controls are part of the scene graph, but for some reason the example code does not recurse into the custom controller. It does print the name of the custom controller, but not the nested elements inside the controller.
object JavaFXUtils {
def printNodeHierarchy(node: Node): Unit = {
val builder = new StringBuilder()
traverse(0, node, builder)
println(builder)
}
private def traverse(depth: Int, node: Node, builder: StringBuilder) {
val tab = getTab(depth)
builder.append(tab)
startTag(node, builder)
middleTag(depth, node, builder)
node match {
case parent: Parent => builder.append(tab)
case _ =>
}
endTag(node, builder)
}
private def getTab(depth: Int): String = {
val builder = new StringBuilder("\n")
for(i <- 0 until depth) {
builder.append(" ")
}
builder.toString()
}
private def startTag(node: Node, builder: StringBuilder): Unit = {
def styles: String = {
val styleClasses = node.getStyleClass
if(styleClasses.isEmpty) {
""
} else {
val b = new StringBuilder(" styleClass=\"")
for(i <- 0 until styleClasses.size()) {
if(i > 0) {
b.append(" ")
}
b.append(".").append(styleClasses.get(i))
}
b.append("\"")
b.toString()
}
}
def id: String = {
val nodeId = node.getId
if(nodeId == null || nodeId.isEmpty) {
""
} else {
val b = new StringBuilder(" id=\"").append(nodeId).append("\"")
b.toString()
}
}
builder.append(s"<${node.getClass.getSimpleName}$id$styles>")
}
private def middleTag(depth: Int, node: Node, builder: StringBuilder): Unit = {
node match {
case parent: Parent =>
val children: ObservableList[Node] = parent.getChildrenUnmodifiable
for (i <- 0 until children.size()) {
traverse(depth + 1, children.get(i), builder)
}
case _ =>
}
}
private def endTag(node: Node, builder: StringBuilder) {
builder.append(s"</${node.getClass.getSimpleName}>")
}
}
What is the proper way to print the contents of the entire scene graph? The accepted answer can be written in either Java or Scala.
Update
Upon further review, I'm noticing that it does work properly for some of the custom controls, but not all of them. I have 3 custom controls. I'll demonstrate 2 of them. The custom controls are ClientLogo and MenuViewController. The previously listed traversal code properly shows the children of the ClientLogo, but does not show the children of the MenuViewController. (Maybe this is because the MenuViewController is a subclass of TitledPane?)
client_logo.fxml:
<?import javafx.scene.image.ImageView?>
<fx:root type="javafx.scene.layout.StackPane" xmlns:fx="http://javafx.com/fxml" id="clientLogo">
<ImageView fx:id="logo"/>
</fx:root>
ClientLogo.scala
class ClientLogo extends StackPane {
#FXML #BeanProperty var logo: ImageView = _
val logoFxml: URL = classOf[ClientLogo].getResource("/fxml/client_logo.fxml")
val loader: FXMLLoader = new FXMLLoader(logoFxml)
loader.setRoot(this)
loader.setController(this)
loader.load()
logo.setImage(Config.clientLogo)
logo.setPreserveRatio(false)
var logoWidth: Double = .0
def getLogoWidth = logoWidth
def setLogoWidth(logoWidth: Double) {
this.logoWidth = logoWidth
logo.setFitWidth(logoWidth)
}
var logoHeight: Double = .0
def getLogoHeight = logoHeight
def setLogoHeight(logoHeight: Double) {
this.logoHeight = logoHeight
logo.setFitHeight(logoHeight)
}
}
Usage of ClientLogo:
<ClientLogo MigPane.cc="id clientLogo, pos (50px) (-25px)"/>
menu.fxml:
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.shape.Line?>
<?import javafx.scene.control.Label?>
<fx:root type="javafx.scene.control.TitledPane" xmlns:fx="http://javafx.com/fxml" id="menu" collapsible="true" expanded="false">
<GridPane vgap="0" hgap="0">
<children>
<Line fx:id="line" styleClass="menu-line" startX="0" startY="1" endX="150" endY="1" GridPane.rowIndex="0" GridPane.columnIndex="0"/>
<Label fx:id="btnSettings" id="btn-settings" styleClass="btn-menu" text="%text.change.password" GridPane.rowIndex="1" GridPane.columnIndex="0"/>
<Label fx:id="btnAdmin" id="btn-admin" styleClass="btn-menu" text="%text.admin" GridPane.rowIndex="2" GridPane.columnIndex="0"/>
<Label fx:id="btnQuickTips" id="btn-quick-tips" styleClass="btn-menu" text="%text.quick.tips" GridPane.rowIndex="3" GridPane.columnIndex="0"/>
<Label fx:id="btnLogout" id="btn-logout" styleClass="btn-menu" text="%text.logout" GridPane.rowIndex="4" GridPane.columnIndex="0"/>
</children>
</GridPane>
</fx:root>
MenuViewController.scala:
class MenuViewController extends TitledPane with ViewController[UserInfo] with LogHelper with Resources {
private var menuWidth: Double = .0
#FXML #BeanProperty var line: Line = _
#FXML #BeanProperty var btnSettings: Label = _
#FXML #BeanProperty var btnAdmin: Label = _
#FXML #BeanProperty var btnQuickTips: Label = _
#FXML #BeanProperty var btnLogout: Label = _
val menuFxml: URL = classOf[MenuViewController].getResource("/fxml/menu.fxml")
val loader: FXMLLoader = new FXMLLoader(menuFxml)
loader.setRoot(this)
loader.setController(this)
loader.setResources(resources)
loader.load()
var userInfo: UserInfo = _
#FXML
private def initialize() {
def handle(message: Any): Unit = {
setExpanded(false)
uiController(message)
}
btnSettings.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(SettingsClicked)))
btnAdmin.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(AdminClicked)))
btnQuickTips.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(QuickTipsClicked)))
btnLogout.setOnMouseClicked(EventHandlerFactory.mouseEvent(e => handle(LogoutClicked)))
}
override def update(model: UserInfo) {
userInfo = model
setText(if (userInfo == null) "Menu" else userInfo.displayName)
}
def getMenuWidth: Double = {
return menuWidth
}
def setMenuWidth(menuWidth: Double) {
this.menuWidth = menuWidth
val spaceToRemove: Double = (menuWidth / 3)
line.setEndX(menuWidth - spaceToRemove)
line.setTranslateX(spaceToRemove / 2)
Array(btnSettings, btnAdmin, btnQuickTips, btnLogout).foreach(btn => {
btn.setMinWidth(menuWidth)
btn.setPrefWidth(menuWidth)
btn.setMaxWidth(menuWidth)
})
}
}
Usage of MenuViewController:
<MenuViewController MigPane.cc="id menu, pos (100% - 250px) (29)" menuWidth="200" fx:id="menu"/>
This is a sample of the output from printing the scene graph to the console:
<BorderPane styleClass=".root">
<StackPane>
<MigPane id="home" styleClass=".top-blue-bar-bottom-blue-curve">
<ClientLogo id="clientLogo">
<ImageView id="logo"></ImageView>
</ClientLogo>
...
<MenuViewController id="menu" styleClass=".titled-pane">
</MenuViewController>
</MigPane>
</StackPane>
</BorderPane>
As you can see, the child elements in the ClientLogo custom control are properly traversed, but the elements inside menu.fxml are missing. I would expect to see the substructure that is added to a TitledPane and the GridPane with its nested substructure listed in menu.fxml.
Suggested Tool
ScenicView is a 3rd party tool for introspecting on the SceneGraph. It is highly recommended that you use ScenicView rather than developing your own debugging tools to dump the Scene Graph.
Dump Utility
Here is a very simple little routine to dump the scene graph to System.out, invoke it via DebugUtil.dump(stage.getScene().getRoot()):
import javafx.scene.Node;
import javafx.scene.Parent;
public class DebugUtil {
/** Debugging routine to dump the scene graph. */
public static void dump(Node n) {
dump(n, 0);
}
private static void dump(Node n, int depth) {
for (int i = 0; i < depth; i++) System.out.print(" ");
System.out.println(n);
if (n instanceof Parent) {
for (Node c : ((Parent) n).getChildrenUnmodifiable()) {
dump(c, depth + 1);
}
}
}
}
The simple routine above will usually dump all of the style class info because the default toString() on node outputs styleclass data.
An additional enhancement would be to check if the node being printed is also an instance of Labeled or Text, then also print the text string associated with the node in those cases.
Caveat
For some control types, some of the nodes which are created for the control are instantiated by the control's skin which can be defined in CSS and the skin sometimes creates its child nodes lazily.
What you need to do to see those nodes is ensure a css layout pass has been run on the scene graph by running your dump after one of the following events:
stage.show() has been called for an initial scene.
After a pulse occurs for a modified scene.
use AnimationTimer for this to count a pulse.
After you have manually forced a layout pass.
taking a synchronous snapshot of the scene will force a layout pass, though I think there may be some other APIs that I can't remember the name of which allow you to force the layout pass without a snapshot.
Check that you have done one of these things before you run your dump.
Example of using an AnimationTimer
// Modify scene.
// . . .
// Start a timer which delays the scene dump call.
AnimationTimer timer = new AnimationTimer() {
#Override
public void handle(long now) {
// Take action on receiving a pulse.
dump(stage.getScene().getRoot());
// Stop monitoring pulses.
// You can stop immediately like this sample.
stop();
// OR you could count a specified number pulses before stopping.
}
};
timer.start();
Based on #jewelsea's excellent answer , heres a version in Jython. It traverses a bit deeper by using a special case handler for controls that dont respond well to the findchildren method. It should be straight forward enough to translate into generic java, just use the python code as pseudocode!
from javafx.scene import Parent
from javafx.scene import control
class SceneGraph(object):
def parse(self,n,depth=0):
print " "*depth,n
if isinstance(n,control.Tab):
for i in n.getContent().getChildrenUnmodifiable():
self.parse(i,depth+1)
elif isinstance(n,control.TabPane):
for i in n.getTabs():
self.parse(i,depth+1)
elif isinstance(n,Parent):
for i in n.getChildrenUnmodifiable():
self.parse(i,depth+1)
elif isinstance(n,control.Accordion):
for i in n.getPanes():
self.parse(i,depth+1)
Special handlers might be needed for other controls, and the code could perhaps be improved by a closer examination of the class heirachy.
I'm not entirely sure how best to handle third party control packs, at this stage.

Resources