Problem
The setTableMenuButtonVisible of a TableView provides a mechanism to change the visibility of a table column. However that functionality leaves a lot to be desired:
The menu should remain open. I have e. g. 15 table columns and it's a pain to click menu open -> click column -> click menu open -> click next column -> ... It's a pain to change the visibility of multiple columns
There should be a select all / deselect all functionality
There should be a way to extend the menu with custom items
After you deselected all columns there's no way to reselect a column because the header is gone and with it the table menu
In other words: The current implementation of the table menu is rather useless.
Question
Does anyone know of a way about how to replace the existing tableview menu with a proper one? I've seen a solution with a ".show-hide-columns-button" style lookup and adding an event filter. However that was 2 years ago, maybe things changed.
Thank you very much!
This is how I'd like to have it, demonstrated via ContextMenu (i. e. right mouse button click on table):
public class TableViewSample extends Application {
private final TableView table = new TableView();
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(300);
stage.setHeight(500);
// create table columns
TableColumn firstNameCol = new TableColumn("First Name");
TableColumn lastNameCol = new TableColumn("Last Name");
TableColumn emailCol = new TableColumn("Email");
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
// add context menu
CustomMenuItem cmi;
ContextMenu cm = new ContextMenu();
// select all item
Label selectAll = new Label( "Select all");
selectAll.addEventHandler( MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for( Object obj: table.getColumns()) {
((TableColumn) obj).setVisible(true);
} }
});
cmi = new CustomMenuItem( selectAll);
cmi.setHideOnClick(false);
cm.getItems().add( cmi);
// deselect all item
Label deselectAll = new Label("Deselect all");
deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for (Object obj : table.getColumns()) {
((TableColumn) obj).setVisible(false);
}
}
});
cmi = new CustomMenuItem( deselectAll);
cmi.setHideOnClick(false);
cm.getItems().add( cmi);
// separator
cm.getItems().add( new SeparatorMenuItem());
// menu item for all columns
for( Object obj: table.getColumns()) {
TableColumn tableColumn = (TableColumn) obj;
CheckBox cb = new CheckBox( tableColumn.getText());
cb.selectedProperty().bindBidirectional( tableColumn.visibleProperty());
cmi = new CustomMenuItem( cb);
cmi.setHideOnClick(false);
cm.getItems().add( cmi);
}
// set context menu
table.setContextMenu(cm);
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(10, 0, 0, 10));
vbox.getChildren().addAll(table);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
}
Inspired by the solution of ControlsFX I solved the problem myself using reflection. If someone has a better idea and cleaner way without reflection, I'm all ears. I created a utils class in order to distinguish from the sample code.
import java.lang.reflect.Field;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.input.MouseEvent;
import com.sun.javafx.scene.control.skin.TableHeaderRow;
import com.sun.javafx.scene.control.skin.TableViewSkin;
public class TableViewUtils {
/**
* Make table menu button visible and replace the context menu with a custom context menu via reflection.
* The preferred height is modified so that an empty header row remains visible. This is needed in case you remove all columns, so that the menu button won't disappear with the row header.
* IMPORTANT: Modification is only possible AFTER the table has been made visible, otherwise you'd get a NullPointerException
* #param tableView
*/
public static void addCustomTableMenu( TableView tableView) {
// enable table menu
tableView.setTableMenuButtonVisible(true);
// get the table header row
TableHeaderRow tableHeaderRow = getTableHeaderRow((TableViewSkin) tableView.getSkin());
// get context menu via reflection
ContextMenu contextMenu = getContextMenu(tableHeaderRow);
// setting the preferred height for the table header row
// if the preferred height isn't set, then the table header would disappear if there are no visible columns
// and with it the table menu button
// by setting the preferred height the header will always be visible
// note: this may need adjustments in case you have different heights in columns (eg when you use grouping)
double defaultHeight = tableHeaderRow.getHeight();
tableHeaderRow.setPrefHeight(defaultHeight);
// modify the table menu
contextMenu.getItems().clear();
addCustomMenuItems( contextMenu, tableView);
}
/**
* Create a menu with custom items. The important thing is that the menu remains open while you click on the menu items.
* #param cm
* #param table
*/
private static void addCustomMenuItems( ContextMenu cm, TableView table) {
// create new context menu
CustomMenuItem cmi;
// select all item
Label selectAll = new Label("Select all");
selectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for (Object obj : table.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(true);
}
}
});
cmi = new CustomMenuItem(selectAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// deselect all item
Label deselectAll = new Label("Deselect all");
deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for (Object obj : table.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(false);
}
}
});
cmi = new CustomMenuItem(deselectAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// separator
cm.getItems().add(new SeparatorMenuItem());
// menu item for each of the available columns
for (Object obj : table.getColumns()) {
TableColumn<?, ?> tableColumn = (TableColumn<?, ?>) obj;
CheckBox cb = new CheckBox(tableColumn.getText());
cb.selectedProperty().bindBidirectional(tableColumn.visibleProperty());
cmi = new CustomMenuItem(cb);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
}
}
/**
* Find the TableHeaderRow of the TableViewSkin
*
* #param tableSkin
* #return
*/
private static TableHeaderRow getTableHeaderRow(TableViewSkin<?> tableSkin) {
// get all children of the skin
ObservableList<Node> children = tableSkin.getChildren();
// find the TableHeaderRow child
for (int i = 0; i < children.size(); i++) {
Node node = children.get(i);
if (node instanceof TableHeaderRow) {
return (TableHeaderRow) node;
}
}
return null;
}
/**
* Get the table menu, i. e. the ContextMenu of the given TableHeaderRow via
* reflection
*
* #param headerRow
* #return
*/
private static ContextMenu getContextMenu(TableHeaderRow headerRow) {
try {
// get columnPopupMenu field
Field privateContextMenuField = TableHeaderRow.class.getDeclaredField("columnPopupMenu");
// make field public
privateContextMenuField.setAccessible(true);
// get field
ContextMenu contextMenu = (ContextMenu) privateContextMenuField.get(headerRow);
return contextMenu;
} catch (Exception ex) {
ex.printStackTrace();
}
return null;
}
}
Example usage:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class CustomTableMenuDemo extends Application {
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com"));
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
stage.setTitle("Table Menu Demo");
stage.setWidth(500);
stage.setHeight(550);
// create table columns
TableColumn<Person, String> firstNameCol = new TableColumn<Person, String>("First Name");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("firstName"));
TableColumn<Person, String> lastNameCol = new TableColumn<Person, String>("Last Name");
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(new PropertyValueFactory<Person, String>("lastName"));
TableColumn<Person, String> emailCol = new TableColumn<Person, String>("Email");
emailCol.setMinWidth(180);
emailCol.setCellValueFactory(new PropertyValueFactory<Person, String>("email"));
TableView<Person> tableView = new TableView<>();
tableView.setPlaceholder(new Text("No content in table"));
tableView.setItems(data);
tableView.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.setPadding(new Insets(10, 10, 10, 10));
BorderPane borderPane = new BorderPane();
borderPane.setCenter( tableView);
vbox.getChildren().addAll( borderPane);
Scene scene = new Scene( vbox);
stage.setScene(scene);
stage.show();
// enable table menu button and add a custom menu to it
TableViewUtils.addCustomTableMenu(tableView);
}
public static class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
}
Screenshots:
Custom table menu in action, the menu remains open while you click the buttons:
Custom table menu still available, even though no columns are visible:
Edit: And here's a version that instead of reflection uses some heuristic and replaces the internal mouse event handler (see the source of JavaFX's TableHeaderRow class if you want to know more):
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.input.MouseEvent;
import com.sun.javafx.scene.control.skin.TableHeaderRow;
import com.sun.javafx.scene.control.skin.TableViewSkin;
public class TableViewUtils {
/**
* Make table menu button visible and replace the context menu with a custom context menu via reflection.
* The preferred height is modified so that an empty header row remains visible. This is needed in case you remove all columns, so that the menu button won't disappear with the row header.
* IMPORTANT: Modification is only possible AFTER the table has been made visible, otherwise you'd get a NullPointerException
* #param tableView
*/
public static void addCustomTableMenu( TableView tableView) {
// enable table menu
tableView.setTableMenuButtonVisible(true);
// replace internal mouse listener with custom listener
setCustomContextMenu( tableView);
}
private static void setCustomContextMenu( TableView table) {
TableViewSkin<?> tableSkin = (TableViewSkin<?>) table.getSkin();
// get all children of the skin
ObservableList<Node> children = tableSkin.getChildren();
// find the TableHeaderRow child
for (int i = 0; i < children.size(); i++) {
Node node = children.get(i);
if (node instanceof TableHeaderRow) {
TableHeaderRow tableHeaderRow = (TableHeaderRow) node;
// setting the preferred height for the table header row
// if the preferred height isn't set, then the table header would disappear if there are no visible columns
// and with it the table menu button
// by setting the preferred height the header will always be visible
// note: this may need adjustments in case you have different heights in columns (eg when you use grouping)
double defaultHeight = tableHeaderRow.getHeight();
tableHeaderRow.setPrefHeight(defaultHeight);
for( Node child: tableHeaderRow.getChildren()) {
// child identified as cornerRegion in TableHeaderRow.java
if( child.getStyleClass().contains( "show-hide-columns-button")) {
// get the context menu
ContextMenu columnPopupMenu = createContextMenu( table);
// replace mouse listener
child.setOnMousePressed(me -> {
// show a popupMenu which lists all columns
columnPopupMenu.show(child, Side.BOTTOM, 0, 0);
me.consume();
});
}
}
}
}
}
/**
* Create a menu with custom items. The important thing is that the menu remains open while you click on the menu items.
* #param cm
* #param table
*/
private static ContextMenu createContextMenu( TableView table) {
ContextMenu cm = new ContextMenu();
// create new context menu
CustomMenuItem cmi;
// select all item
Label selectAll = new Label("Select all");
selectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for (Object obj : table.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(true);
}
}
});
cmi = new CustomMenuItem(selectAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// deselect all item
Label deselectAll = new Label("Deselect all");
deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent event) {
for (Object obj : table.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(false);
}
}
});
cmi = new CustomMenuItem(deselectAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// separator
cm.getItems().add(new SeparatorMenuItem());
// menu item for each of the available columns
for (Object obj : table.getColumns()) {
TableColumn<?, ?> tableColumn = (TableColumn<?, ?>) obj;
CheckBox cb = new CheckBox(tableColumn.getText());
cb.selectedProperty().bindBidirectional(tableColumn.visibleProperty());
cmi = new CustomMenuItem(cb);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
}
return cm;
}
}
Thank you, Roland for your solution.
That was great. I generalized your solution for a little bit to solve some problems:
avoid to have to assign the TableView with the new context menu after
the window shown (it could cause difficulty when showAndWait() should
be used. It solves the problem by registering the onShown event of
the containing Window.
corrects the mispositioning bug when clicking
the + button while the menu is already on. (Clicking the + while
the menu is visible will hide the menu.)
works using keyboard
possibility to add additional menu items
Usage:
contextMenuHelper = new TableViewContextMenuHelper(tableView);
// Adding additional menu options
MenuItem exportMenuItem = new MenuItem("Export...");
contextMenuHelper.getAdditionalMenuItems().add(exportMenuItem);
Maybe someone find it useful, here is my implementation:
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import com.sun.javafx.scene.control.skin.TableHeaderRow;
import com.sun.javafx.scene.control.skin.TableViewSkin;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
/**
* Helper class to replace default column selection popup for TableView.
*
* <p>
* The original idea credeted to Roland and was found on
* {#link http://stackoverflow.com/questions/27739833/adapt-tableview-menu-button}
* </p>
* <p>
* This improved version targets to solve several problems:
* <ul>
* <li>avoid to have to assign the TableView with the new context menu after the
* window shown (it could cause difficulty when showAndWait() should be used. It
* solves the problem by registering the onShown event of the containing Window.
* </li>
* <li>corrects the mispositioning bug when clicking the + button while the menu
* is already on.</li>
* <li>works using keyboard</li>
* <li>possibility to add additional menu items</li>
* </ul>
* </p>
* <p>
* Usage from your code:
*
* <pre>
* contextMenuHelper = new TableViewContextMenuHelper(this);
* // Adding additional menu items
* MenuItem exportMenuItem = new MenuItem("Export...");
* contextMenuHelper.getAdditionalMenuItems().add(exportMenuItem);
* </pre>
* </p>
*
* #author Roland
* #author bvissy
*
*/
public class TableViewContextMenuHelper {
private TableView<?> tableView;
private ContextMenu columnPopupMenu;
private boolean showAllColumnsOperators = true;
private List<MenuItem> additionalMenuItems = new ArrayList<>();
// Default key to show menu: Shortcut + Shift + Space
private Function<KeyEvent, Boolean> showMenuByKeyboardCheck =
ke -> ke.getCode().equals(KeyCode.SPACE) && ke.isShortcutDown() && ke.isShiftDown();
public TableViewContextMenuHelper(TableView<?> tableView) {
super();
this.tableView = tableView;
// Hooking at the event when the whole window is shown
// and then implementing the event handler assignment
tableView.sceneProperty().addListener(i -> {
tableView.getScene().windowProperty().addListener(i2 -> {
tableView.getScene().getWindow().setOnShown(i3 -> {
tableView.tableMenuButtonVisibleProperty().addListener((ob, o, n) -> {
if (n == true) {
registerListeners();
}
});
if (tableView.isTableMenuButtonVisible()) {
registerListeners();
}
});
});
});
}
/**
* Registers the listeners.
*/
private void registerListeners() {
final Node buttonNode = findButtonNode();
// Keyboard listener on the table
tableView.addEventHandler(KeyEvent.KEY_PRESSED, ke -> {
if (showMenuByKeyboardCheck.apply(ke)) {
showContextMenu();
ke.consume();
}
});
// replace mouse listener on "+" node
buttonNode.setOnMousePressed(me -> {
showContextMenu();
me.consume();
});
}
protected void showContextMenu() {
final Node buttonNode = findButtonNode();
setFixedHeader();
// When the menu is already shown clicking the + button hides it.
if (columnPopupMenu != null) {
columnPopupMenu.hide();
} else {
// Show the menu
final ContextMenu newColumnPopupMenu = createContextMenu();
newColumnPopupMenu.setOnHidden(ev -> {
columnPopupMenu = null;
});
columnPopupMenu = newColumnPopupMenu;
columnPopupMenu.show(buttonNode, Side.BOTTOM, 0, 0);
// Repositioning the menu to be aligned by its right side (keeping inside the table view)
columnPopupMenu.setX(
buttonNode.localToScreen(buttonNode.getBoundsInLocal()).getMaxX()
- columnPopupMenu.getWidth());
}
}
private void setFixedHeader() {
// setting the preferred height for the table header row
// if the preferred height isn't set, then the table header would disappear if there are no visible columns
// and with it the table menu button
// by setting the preferred height the header will always be visible
// note: this may need adjustments in case you have different heights in columns (eg when you use grouping)
Region tableHeaderRow = getTableHeaderRow();
double defaultHeight = tableHeaderRow.getHeight();
tableHeaderRow.setPrefHeight(defaultHeight);
}
private Node findButtonNode() {
TableHeaderRow tableHeaderRow = getTableHeaderRow();
if (tableHeaderRow == null) {
return null;
}
for (Node child : tableHeaderRow.getChildren()) {
// child identified as cornerRegion in TableHeaderRow.java
if (child.getStyleClass().contains("show-hide-columns-button")) {
return child;
}
}
return null;
}
private TableHeaderRow getTableHeaderRow() {
TableViewSkin<?> tableSkin = (TableViewSkin<?>) tableView.getSkin();
if (tableSkin == null) {
return null;
}
// get all children of the skin
ObservableList<Node> children = tableSkin.getChildren();
// find the TableHeaderRow child
for (int i = 0; i < children.size(); i++) {
Node node = children.get(i);
if (node instanceof TableHeaderRow) {
return (TableHeaderRow) node;
}
}
return null;
}
/**
* Create a menu with custom items. The important thing is that the menu
* remains open while you click on the menu items.
*
* #param cm
* #param table
*/
private ContextMenu createContextMenu() {
ContextMenu cm = new ContextMenu();
// create new context menu
CustomMenuItem cmi;
if (showAllColumnsOperators) {
// select all item
Label selectAll = new Label("Select all");
selectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> doSelectAll(event));
cmi = new CustomMenuItem(selectAll);
cmi.setOnAction(e -> doSelectAll(e));
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// deselect all item
Label deselectAll = new Label("Deselect all");
deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> doDeselectAll(event));
cmi = new CustomMenuItem(deselectAll);
cmi.setOnAction(e -> doDeselectAll(e));
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// separator
cm.getItems().add(new SeparatorMenuItem());
}
// menu item for each of the available columns
for (Object obj : tableView.getColumns()) {
TableColumn<?, ?> tableColumn = (TableColumn<?, ?>) obj;
CheckBox cb = new CheckBox(tableColumn.getText());
cb.selectedProperty().bindBidirectional(tableColumn.visibleProperty());
cmi = new CustomMenuItem(cb);
cmi.setOnAction(e -> {
cb.setSelected(!cb.isSelected());
e.consume();
});
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
}
if (!additionalMenuItems.isEmpty()) {
cm.getItems().add(new SeparatorMenuItem());
cm.getItems().addAll(additionalMenuItems);
}
return cm;
}
protected void doDeselectAll(Event e) {
for (Object obj : tableView.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(false);
}
e.consume();
}
protected void doSelectAll(Event e) {
for (Object obj : tableView.getColumns()) {
((TableColumn<?, ?>) obj).setVisible(true);
}
e.consume();
}
public boolean isShowAllColumnsOperators() {
return showAllColumnsOperators;
}
/**
* Sets whether the Select all/Deselect all buttons are visible
*
* #param showAllColumnsOperators
*/
public void setShowAllColumnsOperators(boolean showAllColumnsOperators) {
this.showAllColumnsOperators = showAllColumnsOperators;
}
public List<MenuItem> getAdditionalMenuItems() {
return additionalMenuItems;
}
public Function<KeyEvent, Boolean> getShowMenuByKeyboardCheck() {
return showMenuByKeyboardCheck;
}
/**
* Overrides the keypress check to show the menu. Default is Shortcut +
* Shift + Space.
*
* <p>
* To disable keyboard shortcut use the <code>e -> false</code> function.
* </p>
*
* #param showMenuByKeyboardCheck
*/
public void setShowMenuByKeyboardCheck(Function<KeyEvent, Boolean> showMenuByKeyboardCheck) {
this.showMenuByKeyboardCheck = showMenuByKeyboardCheck;
}
}
Update
Concerning the fact that when you deselect all the columns, the header is still visible and so do the menu button. JDK 8u72
I tried to implement Balage1551's solution.
For my Application I had to change the listeners in TableViewContextMenuHelper(...).
Without this changes I received a NullPointerException every time i changed the actual Scene and returned afterwards to my Screen containing the tableview.
I hope someone else might find this helpful!
// Hooking at the event when the whole window is shown
// and then implementing the event handler assignment
/*tableView.sceneProperty().addListener(i -> {
tableView.getScene().windowProperty().addListener(i2 -> {
tableView.getScene().getWindow().setOnShown(i3 -> {
tableView.tableMenuButtonVisibleProperty().addListener((ob, o, n) -> {
if (n == true) {
registerListeners();
}
});
if (tableView.isTableMenuButtonVisible()) {
registerListeners();
}
});
});
});*/
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^OLD!^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvNEW!vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
tableView.skinProperty().addListener((a, b, newSkin) -> {
tableView.tableMenuButtonVisibleProperty().addListener((ob, o, n) -> {
if (n == true) {
registerListeners();
}
});
if (tableView.isTableMenuButtonVisible()) {
registerListeners();
}
});
This adaptation allows to initialize the TableViewContextMenuHelper again when you open an other scene with:
javafx.stage.Stage.setScreen(...);
I have a table (actually a bunch of tables) where the columns aren't fixed. Every time the columns are changed the above solution was re-setting the list of columns. So if a column called "Collar Size" was hidden it would appear again when the table was refreshed with a new set of data.
This may be crude but I've added a Set to store the names of columns that were hidden last time and then re-hide them this time.
The gist is a Set:
private Set<String> turnedOff = new HashSet<>();
and then the management of adding and removing items from the set. I needed to add a listener on the table columns to hide new columns that matched a name previously hidden.
Other ideas on how to accomplish this will be appreciated.
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Function;
import com.sun.javafx.scene.control.skin.TableHeaderRow;
import com.sun.javafx.scene.control.skin.TableViewSkin;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
public class TableViewContextMenuHelper {
private Set<String> turnedOff = new HashSet<>();
private TableView<?> tableView;
private ContextMenu columnPopupMenu;
private boolean showAllColumnsOperators = true;
private List<MenuItem> additionalMenuItems = new ArrayList<>();
// Default key to show menu: Shortcut + Shift + Space
private Function<KeyEvent, Boolean> showMenuByKeyboardCheck =
ke -> ke.getCode().equals(KeyCode.SPACE) && ke.isShortcutDown() && ke.isShiftDown();
public TableViewContextMenuHelper(TableView<?> tableView) {
super();
this.tableView = tableView;
tableView.skinProperty().addListener((a, b, newSkin) -> {
tableView.tableMenuButtonVisibleProperty().addListener((ob, o, n) -> {
if (n == true) {
registerListeners();
}
});
if (tableView.isTableMenuButtonVisible()) {
registerListeners();
}
});
}
/**
* Registers the listeners.
*/
private void registerListeners() {
final Node buttonNode = findButtonNode();
// Keyboard listener on the table
tableView.addEventHandler(KeyEvent.KEY_PRESSED, ke -> {
if (showMenuByKeyboardCheck.apply(ke)) {
showContextMenu();
ke.consume();
}
});
// replace mouse listener on "+" node
buttonNode.setOnMousePressed(me -> {
showContextMenu();
me.consume();
});
tableView.getColumns().addListener(new ListChangeListener<TableColumn<?,?>>(){
#Override
public void onChanged(javafx.collections.ListChangeListener.Change<? extends TableColumn<?, ?>> c) {
while(c.next()){
if(c.getAddedSize()>0){
// hide "turned off" columns
for(TableColumn<?, ?> tc:c.getAddedSubList()){
if(turnedOff.contains(tc.getText())){
tc.setVisible(false);
}
}
}
}
}
});
}
protected void showContextMenu() {
final Node buttonNode = findButtonNode();
setFixedHeader();
// When the menu is already shown clicking the + button hides it.
if (columnPopupMenu != null) {
columnPopupMenu.hide();
} else {
// Show the menu
final ContextMenu newColumnPopupMenu = createContextMenu();
newColumnPopupMenu.setOnHidden(ev -> {
columnPopupMenu = null;
});
columnPopupMenu = newColumnPopupMenu;
columnPopupMenu.show(buttonNode, Side.BOTTOM, 0, 0);
// Repositioning the menu to be aligned by its right side (keeping inside the table view)
columnPopupMenu.setX(
buttonNode.localToScreen(buttonNode.getBoundsInLocal()).getMaxX()
- columnPopupMenu.getWidth());
}
}
private void setFixedHeader() {
// setting the preferred height for the table header row
// if the preferred height isn't set, then the table header would disappear if there are no visible columns
// and with it the table menu button
// by setting the preferred height the header will always be visible
// note: this may need adjustments in case you have different heights in columns (eg when you use grouping)
Region tableHeaderRow = getTableHeaderRow();
double defaultHeight = tableHeaderRow.getHeight();
tableHeaderRow.setPrefHeight(defaultHeight);
}
private Node findButtonNode() {
TableHeaderRow tableHeaderRow = getTableHeaderRow();
if (tableHeaderRow == null) {
return null;
}
for (Node child : tableHeaderRow.getChildren()) {
// child identified as cornerRegion in TableHeaderRow.java
if (child.getStyleClass().contains("show-hide-columns-button")) {
return child;
}
}
return null;
}
private TableHeaderRow getTableHeaderRow() {
TableViewSkin<?> tableSkin = (TableViewSkin<?>) tableView.getSkin();
if (tableSkin == null) {
return null;
}
// get all children of the skin
ObservableList<Node> children = tableSkin.getChildren();
// find the TableHeaderRow child
for (int i = 0; i < children.size(); i++) {
Node node = children.get(i);
if (node instanceof TableHeaderRow) {
return (TableHeaderRow) node;
}
}
return null;
}
/**
* Create a menu with custom items. The important thing is that the menu
* remains open while you click on the menu items.
*
* #param cm
* #param table
*/
private ContextMenu createContextMenu() {
ContextMenu cm = new ContextMenu();
// create new context menu
CustomMenuItem cmi;
if (showAllColumnsOperators) {
// select all item
Label selectAll = new Label("Select all");
selectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> doSelectAll(event));
cmi = new CustomMenuItem(selectAll);
cmi.setOnAction(e -> doSelectAll(e));
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// deselect all item
Label deselectAll = new Label("Deselect all");
deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> doDeselectAll(event));
cmi = new CustomMenuItem(deselectAll);
cmi.setOnAction(e -> doDeselectAll(e));
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// separator
cm.getItems().add(new SeparatorMenuItem());
}
// menu item for each of the available columns
for (Object obj : tableView.getColumns()) {
TableColumn<?, ?> tableColumn = (TableColumn<?, ?>) obj;
CheckBox cb = new CheckBox(tableColumn.getText());
cb.selectedProperty().bindBidirectional(tableColumn.visibleProperty());
cmi = new CustomMenuItem(cb);
if(turnedOff.contains(cb.getText())){
cb.setSelected(false);
}
cmi.setOnAction(e -> {
cb.setSelected(!cb.isSelected());
if(cb.isSelected()){
turnedOff.remove(cb.getText());
} else {
turnedOff.add(cb.getText());
}
e.consume();
});
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
}
if (!additionalMenuItems.isEmpty()) {
cm.getItems().add(new SeparatorMenuItem());
cm.getItems().addAll(additionalMenuItems);
}
return cm;
}
protected void doDeselectAll(Event e) {
for (TableColumn<?, ?> obj : tableView.getColumns()) {
turnedOff.add(obj.getText());
obj.setVisible(false);
}
e.consume();
}
protected void doSelectAll(Event e) {
for (TableColumn<?, ?> obj : tableView.getColumns()) {
turnedOff.remove(obj.getText());
obj.setVisible(true);
}
e.consume();
}
public boolean isShowAllColumnsOperators() {
return showAllColumnsOperators;
}
/**
* Sets whether the Select all/Deselect all buttons are visible
*
* #param showAllColumnsOperators
*/
public void setShowAllColumnsOperators(boolean showAllColumnsOperators) {
this.showAllColumnsOperators = showAllColumnsOperators;
}
public List<MenuItem> getAdditionalMenuItems() {
return additionalMenuItems;
}
public Function<KeyEvent, Boolean> getShowMenuByKeyboardCheck() {
return showMenuByKeyboardCheck;
}
/**
* Overrides the keypress check to show the menu. Default is Shortcut +
* Shift + Space.
*
* <p>
* To disable keyboard shortcut use the <code>e -> false</code> function.
* </p>
*
* #param showMenuByKeyboardCheck
*/
public void setShowMenuByKeyboardCheck(Function<KeyEvent, Boolean> showMenuByKeyboardCheck) {
this.showMenuByKeyboardCheck = showMenuByKeyboardCheck;
}
}
If you just want to listen to events from the Table Menu Button (and save/restore the state say to java.util.Preferences), then add a listener to the table's VisibleLeafColumns [the ObservableList from getColumns will not change with the selection].
I adjusted the code above to be more generic to work with both the TreeTableView and TableView.
import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import javafx.beans.property.BooleanProperty;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.geometry.Side;
import javafx.scene.Node;
import javafx.scene.control.CheckBox;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.Control;
import javafx.scene.control.CustomMenuItem;
import javafx.scene.control.Label;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumnBase;
import javafx.scene.control.TableView;
import javafx.scene.control.TreeTableView;
import javafx.scene.control.skin.TableHeaderRow;
import javafx.scene.control.skin.TableViewSkinBase;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Region;
import org.jetbrains.annotations.NotNull;
/**
* Helper class to replace default column selection popup for TableView.
*
* <p>
* The original idea credeted to Roland and was found on https://stackoverflow.com/questions/27739833/adapt-tableview-menu-button
* </p>
* <p>
* This improved version targets to solve several problems:
* <ul>
* <li>avoid to have to assign the TableView with the new context menu after the
* window shown (it could cause difficulty when showAndWait() should be used. It
* solves the problem by registering the onShown event of the containing Window.
* </li>
* <li>corrects the mispositioning bug when clicking the + button while the menu
* is already on.</li>
* <li>works using keyboard</li>
* <li>possibility to add additional menu items</li>
* </ul>
* </p>
* <p>
* Usage from your code:
*
* <pre>
* contextMenuHelper = new TableViewContextMenuHelper(this);
* // Adding additional menu items
* MenuItem exportMenuItem = new MenuItem("Export...");
* contextMenuHelper.getAdditionalMenuItems().add(exportMenuItem);
* </pre>
* </p>
* <p>
* https://stackoverflow.com/questions/27739833/adapt-tableview-menu-button
*
* #author Roland
* #author bvissy
*/
public class TreeColumnMenuHelper {
private final Control tableView;
private final List<MenuItem> additionalMenuItems = new ArrayList<>();
private ContextMenu columnPopupMenu;
private boolean showAllColumnsOperators = true;
// Default key to show menu: Shortcut (CTRL on windows) + Shift + Space
private Function<KeyEvent, Boolean> showMenuByKeyboardCheck = ke ->
ke.getCode().equals(KeyCode.SPACE) && ke.isShortcutDown() && ke.isShiftDown();
public TreeColumnMenuHelper(TableView tableView) {
this((Control) tableView);
}
public TreeColumnMenuHelper(TreeTableView tableView) {
this((Control) tableView);
}
private TreeColumnMenuHelper(Control tableView) {
super();
this.tableView = tableView;
if (tableView.getSkin() != null) {
registerListeners();
return;
}
// listen to skin change - this should happen once the table is shown
tableView.skinProperty().addListener((a, b, newSkin) -> {
final BooleanProperty tableMenuButtonVisibleProperty = getTableMenuButtonVisibleProperty(tableView);
tableMenuButtonVisibleProperty.addListener((ob, o, n) -> {
if (n) {
registerListeners();
}
});
if (tableMenuButtonVisibleProperty.get()) {
registerListeners();
}
});
}
/**
*
* #return property that controls the menu button in the corner of the table
*/
private BooleanProperty getTableMenuButtonVisibleProperty(#NotNull Control tableView) {
if(tableView instanceof TableView tab) {
return tab.tableMenuButtonVisibleProperty();
}
if(tableView instanceof TreeTableView tree) {
return tree.tableMenuButtonVisibleProperty();
}
throw new IllegalArgumentException("Argument is no TableView or TreeTableView. Actual class: "+tableView.getClass().getName());
}
/**
* Get columns of the table or treetable
* #return list of columns
*/
private static List<? extends TableColumnBase> getColumns(Control table) {
if (table instanceof TableView tab) {
return tab.getColumns();
} else if (table instanceof TreeTableView tree) {
return tree.getColumns();
} else {
throw new IllegalArgumentException(
"Table argument is no TreeTableView or TableView. Actual class: " + table.getClass()
.getName());
}
}
/**
* Registers the listeners.
*/
private void registerListeners() {
final Node buttonNode = findButtonNode();
// Keyboard listener on the table
tableView.addEventHandler(KeyEvent.KEY_PRESSED, ke -> {
if (showMenuByKeyboardCheck.apply(ke)) {
showContextMenu();
ke.consume();
}
});
// replace mouse listener on "+" node
assert buttonNode != null;
buttonNode.setOnMousePressed(me -> {
showContextMenu();
me.consume();
});
}
protected void showContextMenu() {
final Node buttonNode = findButtonNode();
setFixedHeader();
// When the menu is already shown clicking the + button hides it.
if (columnPopupMenu != null) {
columnPopupMenu.hide();
} else {
// Show the menu
final ContextMenu newColumnPopupMenu = createContextMenu();
newColumnPopupMenu.setOnHidden(ev -> columnPopupMenu = null);
columnPopupMenu = newColumnPopupMenu;
columnPopupMenu.show(buttonNode, Side.BOTTOM, 0, 0);
// Repositioning the menu to be aligned by its right side (keeping inside the table view)
columnPopupMenu.setX(
buttonNode.localToScreen(buttonNode.getBoundsInLocal()).getMaxX() - columnPopupMenu
.getWidth());
}
}
private void setFixedHeader() {
// setting the preferred height for the table header row
// if the preferred height isn't set, then the table header would disappear if there are no visible columns
// and with it the table menu button
// by setting the preferred height the header will always be visible
// note: this may need adjustments in case you have different heights in columns (eg when you use grouping)
Region tableHeaderRow = getTableHeaderRow();
double defaultHeight = tableHeaderRow.getHeight();
tableHeaderRow.setPrefHeight(defaultHeight);
}
private Node findButtonNode() {
TableHeaderRow tableHeaderRow = getTableHeaderRow();
if (tableHeaderRow == null) {
return null;
}
for (Node child : tableHeaderRow.getChildren()) {
// child identified as cornerRegion in TableHeaderRow.java
if (child.getStyleClass().contains("show-hide-columns-button")) {
return child;
}
}
return null;
}
private TableHeaderRow getTableHeaderRow() {
TableViewSkinBase tableSkin = (TableViewSkinBase) tableView.getSkin();
if (tableSkin == null) {
return null;
}
// get all children of the skin
ObservableList<Node> children = tableSkin.getChildren();
// find the TableHeaderRow child
for (Node node : children) {
if (node instanceof TableHeaderRow header) {
return header;
}
}
return null;
}
/**
* Create a menu with custom items. The important thing is that the menu remains open while you
* click on the menu items.
*/
private ContextMenu createContextMenu() {
ContextMenu cm = new ContextMenu();
// create new context menu
CustomMenuItem cmi;
if (showAllColumnsOperators) {
// select all item
Label selectAll = new Label("Select all");
selectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, this::doSelectAll);
cmi = new CustomMenuItem(selectAll);
cmi.setOnAction(this::doSelectAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// deselect all item
Label deselectAll = new Label("Deselect all");
deselectAll.addEventHandler(MouseEvent.MOUSE_CLICKED, this::doDeselectAll);
cmi = new CustomMenuItem(deselectAll);
cmi.setOnAction(this::doDeselectAll);
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
// separator
cm.getItems().add(new SeparatorMenuItem());
}
if (!additionalMenuItems.isEmpty()) {
cm.getItems().addAll(additionalMenuItems);
cm.getItems().add(new SeparatorMenuItem());
}
// menu item for each of the available columns
for (TableColumnBase col : getColumns(tableView)) {
CheckBox cb = new CheckBox(col.getText());
cb.selectedProperty().bindBidirectional(col.visibleProperty());
cmi = new CustomMenuItem(cb);
cmi.setOnAction(e -> {
cb.setSelected(!cb.isSelected());
e.consume();
});
cmi.setHideOnClick(false);
cm.getItems().add(cmi);
}
return cm;
}
protected void setAllVisible(boolean visible) {
for (TableColumnBase col : getColumns(tableView)) {
col.setVisible(visible);
}
}
protected void doDeselectAll(Event e) {
setAllVisible(false);
e.consume();
}
protected void doSelectAll(Event e) {
setAllVisible(true);
e.consume();
}
public boolean isShowAllColumnsOperators() {
return showAllColumnsOperators;
}
/**
* Sets whether the Select all/Deselect all buttons are visible
*
* #param showAllColumnsOperators
*/
public void setShowAllColumnsOperators(boolean showAllColumnsOperators) {
this.showAllColumnsOperators = showAllColumnsOperators;
}
public List<MenuItem> getAdditionalMenuItems() {
return additionalMenuItems;
}
public Function<KeyEvent, Boolean> getShowMenuByKeyboardCheck() {
return showMenuByKeyboardCheck;
}
/**
* Overrides the keypress check to show the menu. Default is Shortcut + Shift + Space.
*
* <p>
* To disable keyboard shortcut use the <code>e -> false</code> function.
* </p>
*
* #param showMenuByKeyboardCheck
*/
public void setShowMenuByKeyboardCheck(Function<KeyEvent, Boolean> showMenuByKeyboardCheck) {
this.showMenuByKeyboardCheck = showMenuByKeyboardCheck;
}
}
Related
EDIT: Forgot the code...
I have an app that let's the user select CSV files for viewing. I'm using JavaFX TableViews to display the data.
For one page, the user can type into a special text box. It's a custom class I made called AutoCompleteTextArea, which extends RichTextFX's StyleClassedTextArea. On other pages, this text box should be hidden. When I have just one TableView, things work fine.
vbox.getChildren().addAll(menuBar, title, subtitle, reqBox, reqTable);
But I need other pages with different TableViews. If I add another TableView to the VBox, my AutoCompleteTextArea goes away!
vbox.getChildren().addAll(menuBar, title, subtitle, reqBox, reqTable, tempTable);
The TableViews do not appear to be overlapping... Any idea why the AutoCompleteTextArea is disappearing? The other weird thing is that if I substitute a regular TextField for the AutoCompleteTextArea, things work fine!
Here's my code. You will need RichTextFX on your build path in order to run it. Use the View Menu to see the problem. The first menu item shows the AutoCompleteTextArea (in the working case). The second menu item shows a different TableView, but this is the broken case - the AutoCompleteTextArea is gone from the first page.
Line 132 is the line in question.
I hope someone is up for the challenge!
More background:
I originally wanted to have just one TableView, and update it's contents based on the user's selection in the View Menu. But I couldn't find a good way to do that, and now here I am again... (see this post: How do I clone a JavaFX TableView?)
package FLOOR;
// --- Imports
import java.util.LinkedList;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import org.fxmisc.richtext.StyleClassedTextArea;
import javafx.application.Application;
import javafx.beans.property.StringProperty;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.TextAlignment;
import javafx.stage.Stage;
// --- Main Class
public class Example extends Application {
// --- All Pages
final Page[] pages = new Page[] {
new Page("Welcome!",
"Welcome Page"),
new Page("Page 1", "Shows Table_1"),
new Page("Page 2", "Shows Table_2"),
};
// --- All Tables
TableView<ObservableList<StringProperty>> reqTable = new TableView<>();
TableView<ObservableList<StringProperty>> tempTable = new TableView<>();
//TextField reqBox = new TextField();
AutoCompleteTextArea reqBox = new AutoCompleteTextArea();
// --- Current Page
final Label title = new Label();
final Label subtitle = new Label();
// --- Main
public static void main(String[] args) { launch(args); }
// --- Start
#Override
public void start(Stage stage) {
// --- Menus
// --- File Menu
// --- Import Submenu
Menu menuFile = new Menu("File");
Menu importMenu = new Menu("Import");
MenuItem reqOption = new MenuItem("Requirements");
MenuItem tempOption = new MenuItem("Templates");
importMenu.getItems().addAll(reqOption, tempOption);
//Import Requirements
reqOption.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent e) {
//TODO
}
});
//Import Templates
tempOption.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent e) {
//TODO
}
});
//Export
MenuItem export = new MenuItem("Export");
export.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent t) {
//TODO
}
});
//Exit
MenuItem exit = new MenuItem("Exit");
exit.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent t) {
System.exit(0);
}
});
menuFile.getItems().addAll(importMenu, export, new SeparatorMenuItem(), exit);
// --- View Menu
Menu menuView = new Menu("View");
//Page1
MenuItem viewRequirements = new MenuItem("Requirements");
viewRequirements.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent t) {
getPage1();
}
});
//Page2
MenuItem viewTemplates = new MenuItem("Templates");
viewTemplates.setOnAction(new EventHandler<ActionEvent>() {
public void handle(ActionEvent t) {
getPage2();
}
});
menuView.getItems().addAll(viewRequirements, viewTemplates);
// --- Menu Bar
MenuBar menuBar = new MenuBar();
menuBar.getMenus().addAll(menuFile, menuView);
// --- VBox
final VBox vbox = new VBox();
vbox.setAlignment(Pos.TOP_CENTER);
vbox.setSpacing(10);
reqTable.setMinHeight(300);
tempTable.setMinHeight(300);
reqTable.translateYProperty().set(30);
tempTable.translateYProperty().set(-275);
reqTable.setVisible(false);
tempTable.setVisible(false);
reqBox.setVisible(false);
// --- Welcome Page
title.setFont(new Font("Arial", 24));
title.translateYProperty().set(10);
title.setText(pages[0].title);
subtitle.setText(pages[0].subtitle);
subtitle.setMinHeight(30);
subtitle.setTextAlignment(TextAlignment.CENTER);
// --- Show FLOOR
vbox.getChildren().addAll(menuBar, title, subtitle, reqBox, reqTable);
//vbox.getChildren().addAll(menuBar, title, subtitle, reqBox, reqTable, tempTable);
Scene scene = new Scene(vbox, 900, 500);
stage.setScene(scene);
stage.setTitle("FLOOR");
stage.show();
}
// --- Methods
// Page Getters
private void getPage1() {
title.setFont(new Font("Arial", 24));
title.translateYProperty().set(10);
title.setText(pages[1].title);
subtitle.setText(pages[1].subtitle);
subtitle.setMinHeight(20);
reqBox.setVisible(true);
reqTable.setVisible(true);
tempTable.setVisible(false);
}
private void getPage2() {
title.setFont(new Font("Arial", 24));
title.translateYProperty().set(10);
title.setText(pages[2].title);
subtitle.setText(pages[2].subtitle);
subtitle.setMinHeight(20);
reqBox.setVisible(false);
reqTable.setVisible(false);
tempTable.setVisible(true);
}
// --- Classes
// Page
private class Page {
public String title;
public String subtitle;
public Page(String title, String subtitle) {
this.title = title;
this.subtitle = subtitle;
}
}
// AutoCompleteTextArea
public class AutoCompleteTextArea extends StyleClassedTextArea {
public final SortedSet<String> entries;
public ContextMenu entriesPopup;
public AutoCompleteTextArea() {
super();
entries = new TreeSet<>();
entriesPopup = new ContextMenu();
}
public SortedSet<String> getEntries() { return entries; }
public void populatePopup(List<String> searchResult) {
List<CustomMenuItem> menuItems = new LinkedList<>();
int maxEntries = 20;
int count = Math.min(searchResult.size(), maxEntries);
for (int i = 0; i < count; i++) {
final String result = searchResult.get(i);
Label entryLabel = new Label(result);
CustomMenuItem item = new CustomMenuItem(entryLabel, true);
menuItems.add(item);
}
entriesPopup.getItems().clear();
entriesPopup.getItems().addAll(menuItems);
}
}
}
I have a own component (extended TextField). When I display the component in an AnchorPane the layout from the component is correctly displayed. But when I display the component in a TabPane then the first time when the component is shown the layout isn't correct rendered.
Screenshot:
Own component in an AnchorPane
Screenshot:
Own component in a TabPane
Here a MCVE:
import static javafx.scene.layout.Region.USE_PREF_SIZE;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.event.ActionEvent;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.Separator;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.control.TextField;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
/**
*
* #author Naoghuman
*/
public class ExtendedComponentsMCVE extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
ExtendedTextField extendedTextField = new ExtendedTextField();
// OnlyAnchorPane oap = new OnlyAnchorPane(extendedTextField);
// Scene scene = new Scene(oap, 800, 600);
TabAnchorPane tap = new TabAnchorPane(extendedTextField);
Scene scene = new Scene(tap, 800, 600);
primaryStage.setTitle("Demo Extended Components");
primaryStage.setScene(scene);
primaryStage.show();
}
// ExtendedTextField #######################################################
class ExtendedTextField extends HBox {
private BooleanProperty configureCheckBoxProperty;
private BooleanProperty configureLeftLabelProperty;
private BooleanProperty configureTopLabelProperty;
private CheckBox cbReadOnly;
private Label lLeft;
private Label lTop;
private TextField tfText;
private VBox vBox;
private String lastUserInput = ""; // NOI18N
ExtendedTextField() {
super();
this.init();
}
private void init() {
// vbox
super.setAlignment(Pos.BOTTOM_LEFT);
super.setStyle("-fx-background-color: lightgreen;");
// left label
configureLeftLabelProperty = new SimpleBooleanProperty(Boolean.TRUE);
lLeft = new Label("<left>"); // NOI18N
lLeft.setMaxHeight(USE_PREF_SIZE);
lLeft.setMinHeight(USE_PREF_SIZE);
lLeft.visibleProperty().bind(configureLeftLabelProperty);
lLeft.managedProperty().bind(configureLeftLabelProperty);
super.getChildren().add(lLeft);
// checkbox
configureCheckBoxProperty = new SimpleBooleanProperty(Boolean.TRUE);
cbReadOnly = new CheckBox();
cbReadOnly.setMaxHeight(USE_PREF_SIZE);
cbReadOnly.setMinHeight(USE_PREF_SIZE);
cbReadOnly.visibleProperty().bind(configureCheckBoxProperty);
cbReadOnly.managedProperty().bind(configureCheckBoxProperty);
super.getChildren().add(cbReadOnly);
// vbox
vBox = new VBox();
HBox.setHgrow(vBox, Priority.ALWAYS);
// top label
configureTopLabelProperty = new SimpleBooleanProperty(Boolean.TRUE);
lTop = new Label("<top>"); // NOI18N
lTop.visibleProperty().bind(configureTopLabelProperty);
lTop.managedProperty().bind(configureTopLabelProperty);
vBox.getChildren().add(lTop);
// textfield
tfText = new TextField(lastUserInput);
tfText.disableProperty().bind(cbReadOnly.selectedProperty().not());
lLeft.prefHeightProperty().bind(tfText.heightProperty());
cbReadOnly.prefHeightProperty().bind(tfText.heightProperty());
vBox.getChildren().add(tfText);
super.getChildren().add(vBox);
}
public void setCheckBoxSelected(Boolean selected) {
cbReadOnly.setSelected(selected);
if (selected) {
tfText.setText(lastUserInput);
}
else {
lastUserInput = tfText.getText();
tfText.setText(null);
}
}
public void setCheckBoxVisibleManaged(Boolean visible) {
configureCheckBoxProperty.setValue(visible);
}
public void setLeftLabelVisibleManaged(boolean selected) {
configureLeftLabelProperty.setValue(selected);
}
public void setTopLabelVisibleManaged(boolean selected) {
configureTopLabelProperty.setValue(selected);
}
}
// ExtendedTextField #######################################################
// OnlyAnchorPane ##########################################################
class OnlyAnchorPane extends AnchorPane {
OnlyAnchorPane(ExtendedTextField extendedTextField) {
super();
super.setStyle("-fx-background-color: BLANCHEDALMOND;");
// hbox
HBox hbox = new HBox();
hbox.setStyle("-fx-background-color: KHAKI;");
hbox.setSpacing(7.0d);
hbox.setPrefWidth(Double.MAX_VALUE);
// extendedTextField
VBox vbox = new VBox();
vbox.getChildren().add(extendedTextField);
HBox.setHgrow(vbox, Priority.ALWAYS);
hbox.getChildren().add(vbox);
// menu
MenuVBox menu = new MenuVBox(extendedTextField);
hbox.getChildren().add(menu);
AnchorPane.setBottomAnchor(hbox, 14d);
AnchorPane.setLeftAnchor(hbox, 14d);
AnchorPane.setTopAnchor(hbox, 14d);
AnchorPane.setRightAnchor(hbox, 14d);
super.getChildren().add(hbox);
}
}
// OnlyAnchorPane ##########################################################
// TabAnchorPane ###########################################################
class TabAnchorPane extends AnchorPane {
TabAnchorPane(ExtendedTextField extendedTextField) {
super();
super.setStyle("-fx-background-color: BLANCHEDALMOND;");
// tabpane
TabPane tp = new TabPane();
// tab
Tab t = new Tab("TextField");
t.setClosable(false);
tp.getTabs().add(t);
// hbox
HBox hbox = new HBox();
hbox.setStyle("-fx-background-color: KHAKI;");
hbox.setSpacing(7.0d);
hbox.setPrefWidth(Double.MAX_VALUE);
// extendedTextField
VBox vbox = new VBox();
vbox.getChildren().add(extendedTextField);
HBox.setHgrow(vbox, Priority.ALWAYS);
hbox.getChildren().add(vbox);
// menu
MenuVBox menu = new MenuVBox(extendedTextField);
hbox.getChildren().add(menu);
t.setContent(hbox);
AnchorPane.setBottomAnchor(tp, 14d);
AnchorPane.setLeftAnchor(tp, 14d);
AnchorPane.setTopAnchor(tp, 14d);
AnchorPane.setRightAnchor(tp, 14d);
super.getChildren().add(tp);
}
}
// TabAnchorPane ###########################################################
// MenuVBox ################################################################
class MenuVBox extends VBox {
MenuVBox(ExtendedTextField extendedTextField) {
super();
super.setStyle("-fx-background-color: HONEYDEW;");
super.setSpacing(7.0d);
super.setMaxWidth(200.0d);
super.setMinWidth(200.0d);
super.setPrefWidth(200.0d);
// show top label
CheckBox cb1 = new CheckBox("Show top label");
cb1.setSelected(true);
cb1.setOnAction((ActionEvent event) -> {
extendedTextField.setTopLabelVisibleManaged(cb1.isSelected());
});
super.getChildren().add(cb1);
// show left label
CheckBox cb2 = new CheckBox("Show left label");
cb2.setSelected(true);
cb2.setOnAction((ActionEvent event) -> {
extendedTextField.setLeftLabelVisibleManaged(cb2.isSelected());
});
super.getChildren().add(cb2);
// seperator
super.getChildren().add(new Separator());
// select checkbox
CheckBox cb3 = new CheckBox("Select checkbox");
cb3.setSelected(false);
cb3.setOnAction((ActionEvent event) -> {
extendedTextField.setCheckBoxSelected(cb3.isSelected());
});
super.getChildren().add(cb3);
// show checkbox
CheckBox cb4 = new CheckBox("Show checkbox");
cb4.setSelected(true);
cb4.setOnAction((ActionEvent event) -> {
extendedTextField.setCheckBoxVisibleManaged(cb4.isSelected());
});
super.getChildren().add(cb4);
}
}
// MenuVBox ################################################################
}
That is the problem of laying out maybe due to the fixed values you tried to give for related nodes:
lLeft.setMaxHeight(USE_PREF_SIZE);
lLeft.setMinHeight(USE_PREF_SIZE);
and
cbReadOnly.setMaxHeight(USE_PREF_SIZE);
cbReadOnly.setMinHeight(USE_PREF_SIZE);
This just a guess, but anyway you can manually request the renderer to layout the components just at the end of constructing of them via runLater():
class ExtendedTextField extends HBox {
...
...
private void init() {
...
Platform.runLater( ()->{
requestLayout();
});
}
...
}
BTW, I could not see any control that extends TextField ;)
I found this example of Internal Frames
http://docs.oracle.com/javase/tutorial/uiswing/components/internalframe.html
Is it possible to make the same internal Frames in JavaFX?
With JFXtras there is a Window control, where you can add content and handle the internal window behavior.
First you will need to put in your classpath the jfxtras library. They have some instructions where you can get the library. If you are using maven, just need to add:
<dependency>
<groupId>org.jfxtras</groupId>
<artifactId>jfxtras-labs</artifactId>
<version>2.2-r5</version>
</dependency>
Or download the library and put it into your project classpath, whatever.
Now I put a sample of the demo of the Window with a little difference, allowing generation of several windows.
import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.stage.Stage;
import jfxtras.labs.scene.control.window.CloseIcon;
import jfxtras.labs.scene.control.window.MinimizeIcon;
import jfxtras.labs.scene.control.window.Window;
public class WindowTests extends Application {
private static int counter = 1;
private void init(Stage primaryStage) {
final Group root = new Group();
Button button = new Button("Add more windows");
root.getChildren().addAll(button);
primaryStage.setResizable(false);
primaryStage.setScene(new Scene(root, 600, 500));
button.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent arg0) {
// create a window with title "My Window"
Window w = new Window("My Window#"+counter);
// set the window position to 10,10 (coordinates inside canvas)
w.setLayoutX(10);
w.setLayoutY(10);
// define the initial window size
w.setPrefSize(300, 200);
// either to the left
w.getLeftIcons().add(new CloseIcon(w));
// .. or to the right
w.getRightIcons().add(new MinimizeIcon(w));
// add some content
w.getContentPane().getChildren().add(new Label("Content... \nof the window#"+counter++));
// add the window to the canvas
root.getChildren().add(w);
}
});
}
public double getSampleWidth() {return 600;}
public double getSampleHeight() {return 500;}
#Override
public void start(Stage primaryStage) throws Exception {
init(primaryStage);
primaryStage.show();
}
public static void main(String[] args) {launch(args);}
}
In the original demo, the event code was in the init method, and no button was included. I add the button to create dynamically windows and adding them to the screen.
Here is a snapshot of the result of the application:
I totally recommend you try the demo of jfxtras. They have really great stuff. Hope it helps.
You can implement simple internal window themselves. Main idea, that InternalWindow class just skeleton, that has internal frame like functionality. You can apply any content to it.
1) Declare class
public class InternalWindow extends Region
2) You should be able to set content in window
public void setRoot(Node node) {
getChildren().add(node);
}
3) You should be able to bring window to front if many window exist
public void makeFocusable() {
this.setOnMouseClicked(mouseEvent -> {
toFront();
});
}
4) Now we need dragging functionality
//just for encapsulation
private static class Delta {
double x, y;
}
//we can select nodes that react drag event
public void makeDragable(Node what) {
final Delta dragDelta = new Delta();
what.setOnMousePressed(mouseEvent -> {
dragDelta.x = getLayoutX() - mouseEvent.getScreenX();
dragDelta.y = getLayoutY() - mouseEvent.getScreenY();
//also bring to front when moving
toFront();
});
what.setOnMouseDragged(mouseEvent -> {
setLayoutX(mouseEvent.getScreenX() + dragDelta.x);
setLayoutY(mouseEvent.getScreenY() + dragDelta.y);
});
}
5) Also we want able to resize window (I show only simple right-bottom resizing)
//current state
private boolean RESIZE_BOTTOM;
private boolean RESIZE_RIGHT;
public void makeResizable(double mouseBorderWidth) {
this.setOnMouseMoved(mouseEvent -> {
//local window's coordiantes
double mouseX = mouseEvent.getX();
double mouseY = mouseEvent.getY();
//window size
double width = this.boundsInLocalProperty().get().getWidth();
double height = this.boundsInLocalProperty().get().getHeight();
//if we on the edge, change state and cursor
if (Math.abs(mouseX - width) < mouseBorderWidth
&& Math.abs(mouseY - height) < mouseBorderWidth) {
RESIZE_RIGHT = true;
RESIZE_BOTTOM = true;
this.setCursor(Cursor.NW_RESIZE);
} else {
RESIZE_BOTTOM = false;
RESIZE_RIGHT = false;
this.setCursor(Cursor.DEFAULT);
}
});
this.setOnMouseDragged(mouseEvent -> {
//resize root
Region region = (Region) getChildren().get(0);
//resize logic depends on state
if (RESIZE_BOTTOM && RESIZE_RIGHT) {
region.setPrefSize(mouseEvent.getX(), mouseEvent.getY());
} else if (RESIZE_RIGHT) {
region.setPrefWidth(mouseEvent.getX());
} else if (RESIZE_BOTTOM) {
region.setPrefHeight(mouseEvent.getY());
}
});
}
6) Usage. First we construct all layout. Then apply it to InternalWindow.
private InternalWindow constructWindow() {
// content
ImageView imageView = new ImageView("https://upload.wikimedia.org/wikipedia/commons/thumb/a/a9/Cheetah4.jpg/250px-Cheetah4.jpg");
// title bar
BorderPane titleBar = new BorderPane();
titleBar.setStyle("-fx-background-color: green; -fx-padding: 3");
Label label = new Label("header");
titleBar.setLeft(label);
Button closeButton = new Button("x");
titleBar.setRight(closeButton);
// title bat + content
BorderPane windowPane = new BorderPane();
windowPane.setStyle("-fx-border-width: 1; -fx-border-color: black");
windowPane.setTop(titleBar);
windowPane.setCenter(imageView);
//apply layout to InternalWindow
InternalWindow interalWindow = new InternalWindow();
interalWindow.setRoot(windowPane);
//drag only by title
interalWindow.makeDragable(titleBar);
interalWindow.makeDragable(label);
interalWindow.makeResizable(20);
interalWindow.makeFocusable();
return interalWindow;
}
7) And how add window to layout
#Override
public void start(Stage primaryStage) throws Exception {
Pane root = new Pane();
root.getChildren().add(constructWindow());
root.getChildren().add(constructWindow());
primaryStage.setScene(new Scene(root, 300, 275));
primaryStage.show();
}
Result
Full code: gist
Upd about close button:
You can add method to InternalWindow
public void setCloseButton(Button btn) {
btn.setOnAction(event -> ((Pane) getParent()).getChildren().remove(this));
}
And when construct:
interalWindow.setCloseButton(closeButton);
I need to change the style of arbitrary cells in a TableView which has an variable number of columns. The code below shows the basic problem.
The ExampleRow class is proxy for the real data which comes from a spreadsheet, it's other function is to hold the highlighting information. Since I can't know how many columns there will be I just hold a list of columns that should be highlighted (column re-arrangement won't be supported). The ExampleTableCell class just sets the text for the cell and applies the highlight if needed.
If I set a highlight before the table gets drawn [cell (2,2)] then the cell correctly gets displayed with red text when the application starts. The problem is clicking the button sets cell (1,1) to be highlighted but the table doesn't change. If I resize the application window to nothing then open it back up again the highlighting of cell (1,1) is correctly drawn - presumably because this process forces a full redraw.
What I would like to know is how can I trigger the table to redraw newly highlighted cells (or all visible cells) so the styling is correct?
TIA
package example;
import java.util.HashSet;
import java.util.Set;
import javafx.application.Application;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.util.Callback;
public class CellHighlightExample extends Application {
private final int columnCount = 4;
private final int rowCount = 5;
private TableView<ExampleRow> table = new TableView<>();
#Override
public void start(Stage stage) {
BorderPane root = new BorderPane();
Scene scene = new Scene(root);
Callback<TableColumn.CellDataFeatures<ExampleRow, String>, ObservableValue<String>> cellValueFactory = new Callback<TableColumn.CellDataFeatures<ExampleRow, String>, ObservableValue<String>>() {
#Override
public ObservableValue<String> call(TableColumn.CellDataFeatures<ExampleRow, String> p) {
int row = p.getValue().getRow();
int col = p.getTableView().getColumns().indexOf(p.getTableColumn());
return new SimpleObjectProperty<>("(" + row + ", " + col + ")");
}
};
Callback<TableColumn<ExampleRow, String>, TableCell<ExampleRow, String>> cellFactory = new Callback<TableColumn<ExampleRow, String>, TableCell<ExampleRow, String>>() {
#Override
public TableCell<ExampleRow, String> call(TableColumn<ExampleRow, String> p) {
return new ExampleTableCell<>();
}
};
for (int i = 0, n = columnCount; i < n; i++) {
TableColumn<ExampleRow, String> column = new TableColumn<>();
column.setCellValueFactory(cellValueFactory);
column.setCellFactory(cellFactory);
table.getColumns().add(column);
}
ObservableList<ExampleRow> rows = FXCollections.observableArrayList();
for (int i = 0, n = rowCount; i < n; i++) {
ExampleRow row = new ExampleRow(i);
//Force a cell to be highlighted to show that highlighting works.
if (i == 2) { row.addHighlightedColumn(2); }
rows.add(row);
}
table.setItems(rows);
Button b = new Button("Click to Highlight");
b.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent t) {
ExampleRow row = table.getItems().get(1);
row.addHighlightedColumn(1);
//How to trigger a redraw of the table or cell to reflect the new highlighting?
}
});
root.setTop(b);
root.setCenter(table);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
private class ExampleTableCell<S extends ExampleRow, T extends String> extends TableCell<S, T> {
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (item == null) {
setText(null);
setGraphic(null);
} else {
setText(item);
int colIndex = getTableView().getColumns().indexOf(getTableColumn());
ExampleRow row = getTableView().getItems().get(getIndex());
if (row.isHighlighted(colIndex)) {
setTextFill(Color.RED);
}
}
}
}
private class ExampleRow {
private SimpleIntegerProperty row;
private Set<Integer> highlightedColumns = new HashSet<>();
public ExampleRow(int row) {
this.row = new SimpleIntegerProperty(row);
}
public int getRow() { return row.get(); }
public void setRow(int row) { this.row.set(row); }
public SimpleIntegerProperty rowProperty() { return row; }
public boolean isHighlighted(int col) {
if (highlightedColumns.contains(col)) {
return true;
}
return false;
}
public void addHighlightedColumn(int col) {
highlightedColumns.add(col);
}
}
}
There are lots of discussions about this problem, namely refreshing tableview after altering the item(s).
See
JavaFX 2.1 TableView refresh items
Issues
http://javafx-jira.kenai.com/browse/RT-21822
http://javafx-jira.kenai.com/browse/RT-22463
http://javafx-jira.kenai.com/browse/RT-22599
The solution is to trigger internal tableview update method. Some suggests to remove tableview items and add them again vs.. but the simplest workaround for your case seems to:
b.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent t) {
ExampleRow row = table.getItems().get(1);
row.addHighlightedColumn(1);
//How to trigger a redraw of the table or cell to reflect the new highlighting?
// Workaround
table.getColumns().get(0).setVisible(false);
table.getColumns().get(0).setVisible(true);
}
});
which found in issue comments linked above. Is this a really workaround or illusion of it? You need to dig deeper yourself.
My application uses JPA read data into TableView then modify and display them. The table refreshed modified record under JavaFx 2.0.3. Under JavaFx 2.1, 2.2, the table wouldn't refresh the update anymore. I found other people have similar issue. My plan was to continue using 2.0.3 until someone fixes the issue under 2.1 and 2.2. Now I know it is not a bug and wouldn't be fixed. Well, I don't know how to deal with this. Following are codes are modified from sample demo to show the issue. If I add a new record or delete a old record from table, table refreshes fine. If I modify a record, the table wouldn't refreshes the change until a add, delete or sort action is taken. If I remove the modified record and add it again, table refreshes. But the modified record is put at button of table. Well, if I remove the modified record, add the same record then move the record to the original spot, the table wouldn't refresh anymore. Below is a completely code, please shine some light on this.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class Main extends Application {
private TextField firtNameField = new TextField();
private TextField lastNameField = new TextField();
private TextField emailField = new TextField();
private Stage editView;
private Person fPerson;
public static class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
private TableView<Person> table = new TableView<Person>();
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com"));
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setTitle("Table View Sample");
stage.setWidth(535);
stage.setHeight(535);
editView = new Stage();
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
TableColumn firstNameCol = new TableColumn("First Name");
firstNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("firstName"));
firstNameCol.setMinWidth(150);
TableColumn lastNameCol = new TableColumn("Last Name");
lastNameCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("lastName"));
lastNameCol.setMinWidth(150);
TableColumn emailCol = new TableColumn("Email");
emailCol.setMinWidth(200);
emailCol.setCellValueFactory(
new PropertyValueFactory<Person, String>("email"));
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
//--- create a edit button and a editPane to edit person
Button addButton = new Button("Add");
addButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
fPerson = null;
firtNameField.setText("");
lastNameField.setText("");
emailField.setText("");
editView.show();
}
});
Button editButton = new Button("Edit");
editButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
if (table.getSelectionModel().getSelectedItem() != null) {
fPerson = table.getSelectionModel().getSelectedItem();
firtNameField.setText(fPerson.getFirstName());
lastNameField.setText(fPerson.getLastName());
emailField.setText(fPerson.getEmail());
editView.show();
}
}
});
Button deleteButton = new Button("Delete");
deleteButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
if (table.getSelectionModel().getSelectedItem() != null) {
data.remove(table.getSelectionModel().getSelectedItem());
}
}
});
HBox addEditDeleteButtonBox = new HBox();
addEditDeleteButtonBox.getChildren().addAll(addButton, editButton, deleteButton);
addEditDeleteButtonBox.setAlignment(Pos.CENTER_RIGHT);
addEditDeleteButtonBox.setSpacing(3);
GridPane editPane = new GridPane();
editPane.getStyleClass().add("editView");
editPane.setPadding(new Insets(3));
editPane.setHgap(5);
editPane.setVgap(5);
Label personLbl = new Label("Person:");
editPane.add(personLbl, 0, 1);
GridPane.setHalignment(personLbl, HPos.LEFT);
firtNameField.setPrefWidth(250);
lastNameField.setPrefWidth(250);
emailField.setPrefWidth(250);
Label firstNameLabel = new Label("First Name:");
Label lastNameLabel = new Label("Last Name:");
Label emailLabel = new Label("Email:");
editPane.add(firstNameLabel, 0, 3);
editPane.add(firtNameField, 1, 3);
editPane.add(lastNameLabel, 0, 4);
editPane.add(lastNameField, 1, 4);
editPane.add(emailLabel, 0, 5);
editPane.add(emailField, 1, 5);
GridPane.setHalignment(firstNameLabel, HPos.RIGHT);
GridPane.setHalignment(lastNameLabel, HPos.RIGHT);
GridPane.setHalignment(emailLabel, HPos.RIGHT);
Button saveButton = new Button("Save");
saveButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
if (fPerson == null) {
fPerson = new Person(
firtNameField.getText(),
lastNameField.getText(),
emailField.getText());
data.add(fPerson);
} else {
int k = -1;
if (data.size() > 0) {
for (int i = 0; i < data.size(); i++) {
if (data.get(i) == fPerson) {
k = i;
}
}
}
fPerson.setFirstName(firtNameField.getText());
fPerson.setLastName(lastNameField.getText());
fPerson.setEmail(emailField.getText());
data.set(k, fPerson);
table.setItems(data);
// The following will work, but edited person has to be added to the button
//
// data.remove(fPerson);
// data.add(fPerson);
// add and remove refresh the table, but now move edited person to original spot,
// it failed again with the following code
// while (data.indexOf(fPerson) != k) {
// int i = data.indexOf(fPerson);
// Collections.swap(data, i, i - 1);
// }
}
editView.close();
}
});
Button cancelButton = new Button("Cancel");
cancelButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
editView.close();
}
});
HBox saveCancelButtonBox = new HBox();
saveCancelButtonBox.getChildren().addAll(saveButton, cancelButton);
saveCancelButtonBox.setAlignment(Pos.CENTER_RIGHT);
saveCancelButtonBox.setSpacing(3);
VBox editBox = new VBox();
editBox.getChildren().addAll(editPane, saveCancelButtonBox);
Scene editScene = new Scene(editBox);
editView.setTitle("Person");
editView.initStyle(StageStyle.UTILITY);
editView.initModality(Modality.APPLICATION_MODAL);
editView.setScene(editScene);
editView.close();
final VBox vbox = new VBox();
vbox.setSpacing(5);
vbox.getChildren().addAll(label, table, addEditDeleteButtonBox);
vbox.setPadding(new Insets(10, 0, 0, 10));
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
}
See the answer to Updating rows in Tableview. Add these getters and it will just work.
Additionally since the data is an ObservableList which is set as items to tableView, any changes to this data list will be reflected to the table.getItems() too. Namely no need to table.setItems(data) again.
I have found a simple workaround for triggering the refresh of the TableView in JavaFX 2.1 TableView refresh items. It solved the issue for me.
Add this to your code:
tableView.getColumns().get(0).setVisible(false);
tableView.getColumns().get(0).setVisible(true);
looking into the TableView.java code, there's private refresh() which just executes
getProperties().put(TableViewSkinBase.REFRESH, Boolean.TRUE);
At last, the code below worked for me(Java8). (be careful, the constant's name is not REFRESH but RECREATE)
tableView.getProperties().put(TableViewSkinBase.RECREATE, Boolean.TRUE);
(reading javafx's code, this will force cell re-creation)
Notification-based updates of JavaFX controls typically require that the properties of the data model object backing your GUI meet the minimum definition for a JavaFX Bean.
The following exemplifies the minimum code needed in order for a JavaFX property to satisfy these requirements:
public class Client extends DB {
private IntegerProperty id = new SimpleIntegerProperty();
private StringProperty lastName = new SimpleStringProperty();
private StringProperty firstName = new SimpleStringProperty();
public final int getID() {return this.id.get(); }
void setID(int id) { this.id.set(id); }
public final IntegerProperty idProperty() { return this.id; }
public final String getLastName() { return this.lastName.get(); }
public final void setLastName(String ln) { this.lastName.set(ln); }
public final StringProperty lastNameProperty() { return this.lastName; }
public final String getFirstName() { return this.firstName.get(); }
public final void setFirstName(String fn) { this.firstName.set(fn); }
public final StringProperty firstNameProperty() { return this.firstName; }
:
:
}
Glancing over your code, it does not appear that your properties satisfy the requirements for a JavFX Bean. As such, automatic notification-based updates will not occur.
I have the same problem, and not being able to add SimpleStringProperty to the POJO's used by JPA makes this a bit problematic. But it seems to me that this should be fixable issue because I have noticed the following behavior:
In my application, clicking on a row in the table populates some text fields on the screen, that the user can then edit.
At that point the user can save the data, or create a new item with the same or changed data. If the user creates a new item, which is then inserted into the observable list that the tableview represents, the change is immediately reflected in the contents of the tableview. However if the user just saves the change the new data is not reflected in the table. To put the new data in the list I'm simply doing
trialList.set(currentIndex, tempTrial);
And here's what I think points to this as a fixable issue: if I scroll the affected row out of view on the table and then scroll it back, the 'new' value(s) are now presented.
Hopefully, this can be fixed. Sorry this isn't an answer, so to speak, but might provide some insight for a fix.
this worked for me
#FXML
private void refreshTableView()
{
firstNameCol.setVisible(false);
lastNameCol.setVisible(false);
emailCol.setVisible(false);
firstNameCol.setVisible(true);
lastNameCol.setVisible(true);
emailCol.setVisible(true);
}
I had the same problem and after some search this is my workaround. I found that if the columns are removed and then re-added the table is updated.
public static <T,U> void refreshTableView(final TableView<T> tableView, final List<TableColumn<T,U>> columns, final List<T> rows) {
if (tableView == null) {
throw new NullPointerException();
}
if (columns == null) {
throw new NullPointerException();
}
if (rows == null) {
throw new NullPointerException();
}
tableView.getColumns().clear();
tableView.getColumns().addAll(columns);
ObservableList<T> list = FXCollections.observableArrayList(rows);
tableView.setItems(list);
}
Example of usage:
refreshTableView(myTableView, Arrays.asList(col1, col2, col3), rows);