ContextMenu in JavaFX keeps getting duplicates of the same MenuItem - javafx

I use this class to give a TreeItem a textfield for editing (not relevant to the problem) and to set a ContextMenu on the TreeItem:
package domain;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TextField;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public final class TextFieldTreeCellImpl extends TreeCell<MyNode> {
private TextField textField;
private ContextMenu cm = new ContextMenu();
String oldItem = "";
private Connection connection;
String url = "jdbc:sqlserver://localhost:1433;databaseName=HOGENT1415_11";
String user = "sa";
String password = "root";
Statement statement;
public TextFieldTreeCellImpl() throws SQLException {
connection = DriverManager.getConnection(url, user, password);
statement = connection.createStatement();
}
#Override
public void startEdit() {
super.startEdit();
if (textField == null) {
createTextField();
}
setText(null);
setGraphic(textField);
textField.selectAll();
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText((String) getItem().value);
setGraphic(getTreeItem().getGraphic());
}
#Override
public void updateItem(MyNode item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(getTreeItem().getGraphic());
MenuItem cmItem1 = new MenuItem("Add continent");
cmItem1.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent e) {
System.out.println("Geklikt!");
}
});
cm.getItems().add(cmItem1);
setContextMenu(cm);
}
}
}
private void createTextField() {
textField = new TextField(getString());
textField.setOnKeyReleased(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent t) {
if (t.getCode() == KeyCode.ENTER) {
String sql = "UPDATE Continents SET Name='" + textField.getText() + "' WHERE ContinentID=" + getItemId();
if (getItem().isCountry()) {
sql = "UPDATE Countries SET Name='" + textField.getText() + "' WHERE CountryID=" + getItemId();
}
if (getItem().isClimateChart()) {
sql = "UPDATE ClimateCharts SET Location='" + textField.getText() + "' WHERE ClimateChartID=" + getItemId();
}
try {
ResultSet result = statement.executeQuery(sql);
} catch (SQLException ex) {
Logger.getLogger(TextFieldTreeCellImpl.class.getName()).log(Level.SEVERE, null, ex);
}
commitEdit(new MyNode(textField.getText(), getType(), getItemId()));
} else if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
private String getType() {
return getItem() == null ? "" : getItem().type;
}
private int getItemId() {
return getItem() == null ? null : getItem().id;
}
}
I instance this class in my Controller class using the following code:
selectionTreeView.setEditable(true);
selectionTreeView.setCellFactory(new Callback<TreeView<MyNode>, TreeCell<MyNode>>() {
#Override
public TreeCell<MyNode> call(TreeView<MyNode> p) {
try {
return new TextFieldTreeCellImpl();
} catch (SQLException ex) {
Logger.getLogger(MainPanel.class.getName()).log(Level.SEVERE, null, ex);
}
return null;
}
});
However when I run the program and I right click on the items everything works fine, but if I keep clicking some more on other items there keep on getting items in the contextmenu.
To narrow it down, this happens everytime I double click on an item.
Check the screenshots:
I know that this is because ipdateItem keeps on getting called, but how can I prevent this?

Every time updateItem(...) is called, you add the menu item again (and you never remove it). So each time the cell is reused, it gets another copy of the menu item.
The most efficient approach is to create the menu item in the constructor and pass it to the context menu there. Note that the event handler can access the current item easily:
public TextFieldTreeCellImpl() throws SQLException {
connection = DriverManager.getConnection(url, user, password);
statement = connection.createStatement();
MenuItem cmItem1 = new MenuItem("Add continent");
cmItem1.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent e) {
MyNode item = getItem();
// ...
System.out.println("Geklikt!");
}
});
cm.getItems().add(cmItem1);
}
You probably also want to remove the context menu for empty cells:
#Override
public void updateItem(MyNode item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
setContextMenu(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(getTreeItem().getGraphic());
setContextMenu(cm);
}
}
}
You can also configure the menu items further in the updateItem(...) method if you need, by declaring them as fields, e.g.
public final class TextFieldTreeCellImpl extends TreeCell<MyNode> {
private TextField textField;
private ContextMenu cm = new ContextMenu();
private MenuItem cmItem1 ;
// ...
#Override
public void updateItem(MyNode item, boolean empty) {
super.updateItem(item, empty);
// ...
cmItem1.setText(...);
}
}
Finally, if you really need to completely restructure the context menu when the item changes, then you can do
#Override
public void updateItem(MyNode item, boolean empty) {
super.updateItem(item, empty);
// ...
cm.getItems().clear();
// Now create all menu items from scratch and add to the context menu
}

Related

How to set text and color of multiple ListView items when clicked

I know there are many related questions about this but maybe I'm missing something because I can't get the behavior I'm expecting, to work.
#FXML
private ListView<String> guiList;
void performAction(Actions action) {
try {
Task<String> task = new Task<>() {
#Override
public String call() {
String mySelection = Context.getInstance().getSelected();
ArrayList<String> selectedList = Context.getInstance().getItemsClicked();
if (selectedList == null) {
selectedList = new ArrayList<>();
}
selectedList.add(mySelection);
Context.getInstance().setItemsClicked(selectedList);
guiList.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> param) {
ListCell<String> cell = new ListCell<String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if(item != null && item.matches(mySelection)) {
setText(mySelection + " [" + action + "]");
setFont(Font.font(Font.getDefault().getFamily(), FontWeight.BOLD, Font.getDefault().getSize()));
setStyle("-fx-text-fill: green;");
} else {
setText(item);
}
}
};
return cell;
}
});
return "";
}
};
} catch (Exception e) {
}
}
When I click in an item of guiList, the text is changed, gets bold and shows in green color but I don't understand why I need the else statement. If I don't use it, all the other items of the list disappear.
I ask this because I want to change ALL of the items I click and in the current behavior, the changes are only made in the last one clicked.
Here is on approach. Use an object that has a Boolean variable to keeps up with if the item has been selected.
KeyCode 1
lvMain.getSelectionModel().selectedItemProperty().addListener(((ov, t, t1) - > {
if (t1 != null) {
t1.setSelected(true);
}
}));
Key Code 2
lvMain.setCellFactory(lv - > new ListCell < MyItem > () {
#Override
public void updateItem(MyItem item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setText(item.getText());
if (item.isSelected()) {
setTextFill(Color.RED);
}
}
}
});
Main
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
*
* #author blj0011
*/
public class App extends Application {
#Override
public void start(Stage primaryStage) {
ListView<MyItem> lvMain = new ListView();//Create ListView
lvMain.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);//Change ListView selection mode to multiple
ObservableList<MyItem> items = FXCollections.observableArrayList(new MyItem("Julia"), new MyItem("Ian"), new MyItem("Sue"), new MyItem("Matthew"), new MyItem("Hannah"));//ObseravableList that will be used to set the ListView
lvMain.setItems(items);//Set the ListView's items
lvMain.setCellFactory(lv -> new ListCell<MyItem>()
{
#Override
public void updateItem(MyItem item, boolean empty)
{
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
}
else {
setText(item.getText());
if(item.isSelected())
{
setTextFill(Color.RED);
}
}
}
});
lvMain.getSelectionModel().selectedItemProperty().addListener(((ov, t, t1) -> {
if(t1 != null)
{
t1.setSelected(true);
}
}));
VBox vbox = new VBox();
vbox.getChildren().addAll(lvMain);
StackPane root = new StackPane();
root.getChildren().add(vbox);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
MyItem
/**
*
* #author Sed
*/
public class MyItem {
private String text;
private boolean selected;
public MyItem(String text) {
this.text = text;
this.selected = false;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean isSelected) {
this.selected = isSelected;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
Output
I think a better solution would be to use the ListView's built in multiple selection or have your cells have a ToggleButton. When the ToggleButton is on, change the color of the text. When it is off, change the color back to it's original state.

JavaFx TableCellEditor

I have JavaFx TableView and I permit user to enter some data in the table and retrieve what a user entered in the table for this reason I create also ArrayList of TextFields and use the following code but the size of the ArrayList should be 3 in my case but I found the size 7, what's wrong?
Edit the full code here
import java.util.ArrayList;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Stage;
public class TestTableView extends Application
{
private final TableView <ProductOpeningBalance> tableProductOpeningBalance = new TableView();
private final Scene scene = new Scene(tableProductOpeningBalance, 400, 200);
private final TableColumn productQuantity = new TableColumn("Product Quantity");
private final ObservableList <ProductOpeningBalance> data = FXCollections.observableArrayList(
new ProductOpeningBalance("0"),
new ProductOpeningBalance("0"));
private final ArrayList <TextField> txtProductQuantity = new ArrayList <> ();
#Override
public void start(Stage stage)
{
productQuantity.impl_setReorderable(false);
productQuantity.setEditable(true);
productQuantity.setCellValueFactory(new PropertyValueFactory("ProductQuantity"));
productQuantity.setCellFactory(column -> new TableCell()
{
#Override
public void startEdit()
{
if(!isEmpty())
{
super.startEdit();
createTextField();
setText(null);
setGraphic(txtProductQuantity.get(txtProductQuantity.size() - 1));
txtProductQuantity.get(txtProductQuantity.size() - 1).selectAll();
}
}
#Override
public void cancelEdit()
{
super.cancelEdit();
setText((String) getItem());
setGraphic(null);
}
public void updateItem(String item, boolean empty)
{
super.updateItem(item, empty);
if(empty)
{
setText(null);
setGraphic(null);
}
else
{
if(isEditing())
{
if(txtProductQuantity.get(txtProductQuantity.size() - 1) != null)
{
txtProductQuantity.get(txtProductQuantity.size() - 1).setText(getString());
}
setText(null);
setGraphic(txtProductQuantity.get(txtProductQuantity.size() - 1));
}
else
{
setText(getString());
setGraphic(null);
}
}
}
private void createTextField()
{
txtProductQuantity.add(new TextField(getString()));
txtProductQuantity.get(txtProductQuantity.size() - 1).
setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
txtProductQuantity.get(txtProductQuantity.size() - 1).setAlignment(Pos.BASELINE_RIGHT);
txtProductQuantity.get(txtProductQuantity.size() - 1).focusedProperty().
addListener((ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) ->
{
if(!arg2)
{
commitEdit(txtProductQuantity.get(txtProductQuantity.size() - 1).getText());
}
});
}
private String getString()
{
return getItem() == null ? "" : getItem().toString();
}
});
tableProductOpeningBalance.setEditable(true);
tableProductOpeningBalance.getColumns().addAll(productQuantity);
tableProductOpeningBalance.setItems(data);
stage.setScene(scene);
stage.show();
}
public class ProductOpeningBalance
{
private final SimpleStringProperty ProductQuantity;
public ProductOpeningBalance(String productQuantity)
{
this.ProductQuantity = new SimpleStringProperty(productQuantity);
}
public void setProductQuantity(String productQuantity)
{
ProductQuantity.set(productQuantity);
}
public String getProductQuantity()
{
return ProductQuantity.get();
}
}
}
Solution finally:
this code help me to find what I need after spent a lot of time in searching and trying a lot of methods
purchaseItemPrice.setCellFactory(column -> new TableCell()
{
#Override
public void startEdit()
{
if(!isEmpty())
{
if(txtPurchaseItemPrice.size() < data.size() && getGraphic() == null)
{
super.startEdit();
txtPurchaseItemPrice.add(new TextField());
txtPurchaseItemPrice.get(txtPurchaseItemPrice.size() - 1).setAlignment(Pos.BASELINE_RIGHT);
setGraphic(txtPurchaseItemPrice.get(txtPurchaseItemPrice.size() - 1));
}
}
}
public void updateItem(String item, boolean empty)
{
super.updateItem(item, empty);
if(!empty)
{
if(isEditing())
{
setGraphic(txtPurchaseItemPrice.get(txtPurchaseItemPrice.size() - 1));
}
}
}
});

JavaFX TableView get row from button cell

Referring to question JavaFX TableView custom cell rendering split menu button, i'm able to render split menu button in every row. I've updated my code as suggested by James_D and in the answer by Keyur Bhanderi.
The question is about get value of the row where split menu is located without selecting row before click.
Update: Added images to view output
The images below show the output, every button i click.
Updated SplitMenuCellFactory.java
package com.example.splimenubtn;
import java.util.List;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
public class SplitMenuCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
private List<MenuItemFactory<T>> menuItems;
public SplitMenuCellFactory() {}
public SplitMenuCellFactory(List<MenuItemFactory<T>> items) {
menuItems = items;
}
#Override
public TableCell<S, T> call(TableColumn<S, T> param) {
return new TableCell<S, T>() {
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
setText(null);
} else {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
if (getTableRow() != null) {
setGraphic(new SplitMenuButtonFactory<>(menuItems, getTableRow().getIndex()).buildButton());
}
}
}
};
}
}
Update, adding missing class
SplitMenuButtonFactory.java
package com.example.splimenubtn;
import java.util.List;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SplitMenuButton;
public class SplitMenuButtonFactory<T> {
private List<MenuItemFactory<T>> menuItems;
private int rIndex = 0;
public SplitMenuButtonFactory(List<MenuItemFactory<T>> items) {
menuItems = items;
}
public SplitMenuButtonFactory(List<MenuItemFactory<T>> items, int rI) {
menuItems = items;
rIndex = rI;
}
public SplitMenuButton buildButton() {
SplitMenuButton menuBtn = new SplitMenuButton();
// menuBtn.getItems().addAll(menuItems);
for (MenuItemFactory<?> mIF : menuItems) {
MenuItem btn = mIF.setRowIndex(rIndex).buildMenuItem();
if (mIF.isDefault()) {
menuBtn.setText(btn.getText());
menuBtn.setOnAction(btn.getOnAction());
}
menuBtn.getItems().add(btn);
}
return menuBtn;
}
}
MenuItemsFactory.java
package com.example.splimenubtn;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableView;
public class MenuItemFactory<S> {
private MenuItemActions itemType;
private String itemLbl;
private TableView<S> table;
private boolean defaultAction;
private int rIndex = 0;
public MenuItemFactory(MenuItemActions itemType, String itemLabel, boolean dA) {
this.itemType = itemType;
itemLbl = itemLabel;
defaultAction = dA;
}
public MenuItemFactory(MenuItemActions itemType, String itemLabel, TableView<S> t, boolean dA) {
this.itemType = itemType;
itemLbl = itemLabel;
defaultAction = dA;
table = t;
}
public MenuItemFactory<S> setDataList(TableView<S> t) {
table = t;
return this;
}
public boolean isDefault() {
return defaultAction;
}
public MenuItemFactory<S> setRowIndex(int rI) {
rIndex = rI;
return this;
}
public MenuItem buildMenuItem() {
MenuItem mI = new MenuItem();
switch (itemType) {
case DETAILS:
mI.setText(itemLbl);
mI.setOnAction(handleDetails());
break;
case EDIT:
mI.setText(itemLbl);
mI.setOnAction(handleEdit());
break;
case DELETE:
mI.setText(itemLbl);
mI.setOnAction(handleDelete());
break;
default:
break;
}
return mI;
}
private EventHandler<ActionEvent> handleDetails() {
return new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent aE) {
System.out.println("*** DETAIL REQUEST ***");
}
};
}
private EventHandler<ActionEvent> handleEdit() {
return new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent aE) {
System.out.println("*** EDIT REQUESTED ***");
table.getSelectionModel().select(rIndex);
System.out.println("*** " + table.getSelectionModel().getSelectedItem().toString() + " ***");
}
};
}
private EventHandler<ActionEvent> handleDelete() {
return new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent aE) {
System.out.println("*** DELETE REQUESTED ***");
System.out.println("*** " + table.getSelectionModel().getSelectedItem().toString() + " ***");
}
};
}
}
But when i click on the button, i'm getting always last value.
How can i get the object in the row where button is?
Any help or suggestion that point me to right direction is appreciated.
Simply use the TableCell to retrieve the value in the onAction event handler (or whatever you use in the product of the SplitMenuButtonFactory you're not showing to us).
Simplified example
public static SplitMenuButton createSplitMenuButton(final TableCell cell) {
SplitMenuButton result = new SplitMenuButton();
result.setOnAction(evt -> {
TableRow row = cell.getTableRow();
System.out.println("row item: " +row.getItem());
});
return result;
}
Furthermore it's better to reuse the SplitMenuButton in the cell and updating it instead of recerating it every time the cell item changes.
#Override
public TableCell<S, T> call(TableColumn<S, T> param) {
return new TableCell<S, T>() {
private final SplitMenuButton button = createSplitMenuButton(this);
{
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
updateMenuButton(button, item); // placeholder for updating the button according to the new item
setGraphic(button);
}
}
};
}

Automatically updating/styling TreeView's TreeCells when BooleanProperty is true in seperate TableView

What's the easiest way to have a TreeView's cells auto-refresh with a new style when a condition is met in a separate TableView?
I'm currently setting the TreeCells' styles in the updateItem() method in the TreeView cell factory, but this only fires off if the user adds or removes something in the TreeView. I want to be able to change the style of a given TreeCell if I check off all 3 checkboxes in a separate dialog box.
I'm currently able to monitor the number of checked checkboxes with a BooleanProperty and an IntegerProperty, but I have no idea how I'm supposed to "auto-update" or call a TreeView refresh when a TreeItem's Object's BooleanProperty changes.
Any help is greatly appreciated.
You can set the style in the TreeCell whenever a boolean property on the value underlying the TreeCell is updated (via a binding).
return new TreeCell<Message>() {
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
styleProperty().unbind();
if (empty || item == null || item.getText() == null) {
setText(null);
styleProperty.set(null);
} else {
setText(item.getText());
styleProperty().bind(
Bindings.when(
item.readProperty()
).then("-fx-background-color: red;")
.otherwise("-fx-background-color: null;")
);
}
}
};
Full Sample
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class TreeViewSample extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
ObservableList<Message> messages = FXCollections.observableArrayList();
TreeItem<Message> rootItem = new TreeItem<> (new Message("Inbox"));
rootItem.setExpanded(true);
for (int i = 1; i < 6; i++) {
Message message = new Message("Message" + i);
messages.add(message);
TreeItem<Message> item = new TreeItem<> (message);
rootItem.getChildren().add(item);
}
TreeView<Message> tree = new TreeView<> (rootItem);
tree.setCellFactory(new Callback<TreeView<Message>, TreeCell<Message>>() {
#Override
public TreeCell<Message> call(TreeView<Message> param) {
return new TreeCell<Message>() {
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
styleProperty().unbind();
if (empty || item == null || item.getText() == null) {
setText(null);
styleProperty.set(null);
} else {
setText(item.getText());
styleProperty().bind(
Bindings.when(
item.readProperty()
).then("-fx-background-color: red;")
.otherwise("-fx-background-color: null;")
);
}
}
};
}
});
TableView<Message> tableView = new TableView<>();
tableView.setEditable(true);
TableColumn<Message, String> textCol = new TableColumn<>("Text");
textCol.setCellValueFactory(new PropertyValueFactory<>("text"));
tableView.getColumns().add(textCol);
TableColumn<Message, Boolean> readCol = new TableColumn<>("Read");
readCol.setCellValueFactory(new PropertyValueFactory<>("read"));
readCol.setCellFactory(CheckBoxTableCell.forTableColumn(readCol));
readCol.setEditable(true);
tableView.getColumns().add(readCol);
tableView.setItems(messages);
VBox root = new VBox(10, tree, tableView);
root.setPadding(new Insets(10));
stage.setScene(new Scene(root, 300, 250));
stage.show();
}
public class Message {
private StringProperty text = new SimpleStringProperty();
private BooleanProperty read = new SimpleBooleanProperty(false);
public Message(String msgText) {
text.set(msgText);
}
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
public void setText(String text) {
this.text.set(text);
}
public boolean isRead() {
return read.get();
}
public BooleanProperty readProperty() {
return read;
}
public void setRead(boolean read) {
this.read.set(read);
}
}
}
I'm trying to the bind the graphicProperty to the same BooleanProperty and change the image based on the value.
Example using a binding of an Image within an ImageView associated with the cell.
Image unreadImage = new Image("http://icons.iconarchive.com/icons/oxygen-icons.org/oxygen/16/Status-mail-unread-new-icon.png");
Image readImage = new Image("http://icons.iconarchive.com/icons/icons8/ios7/16/Messaging-Read-Message-icon.png");
. . .
return new TreeCell<Message>() {
ImageView imageView = new ImageView();
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
styleProperty().unbind();
imageView.imageProperty().unbind();
if (empty || item == null || item.getText() == null) {
setText(null);
setGraphic(null);
styleProperty().set(null);
} else {
setText(item.getText());
setGraphic(imageView);
imageView.imageProperty().bind(
Bindings.when(
item.readProperty()
).then(readImage)
.otherwise(unreadImage)
);
styleProperty().bind(
Bindings.when(
item.readProperty()
).then("-fx-background-color: red;")
.otherwise("-fx-background-color: null;")
);
}
}
};
An alternate (and possibly preferable) way to handle this from above is to instead get the style class or psuedoclass of the cell and update that based upon the boolean property. Then define the style in a separate CSS stylesheet. The output of the sample below is the same as the graphic based sample above.
mail.css
.readable:read {
-fx-background-color: red;
-fx-graphic: url(
"http://icons.iconarchive.com/icons/icons8/ios7/16/Messaging-Read-Message-icon.png"
);
}
.readable:unread {
-fx-graphic: url(
"http://icons.iconarchive.com/icons/oxygen-icons.org/oxygen/16/Status-mail-unread-new-icon.png"
);
}
Pseudo-class based code snippet:
PseudoClass READ_PSEUDO_CLASS = PseudoClass.getPseudoClass("read");
PseudoClass UNREAD_PSEUDO_CLASS = PseudoClass.getPseudoClass("unread");
tree.setCellFactory(new Callback<TreeView<Message>, TreeCell<Message>>() {
#Override
public TreeCell<Message> call(TreeView<Message> param) {
return new TreeCell<Message>() {
private ChangeListener<Boolean> readChangeListener = (observable, oldValue, newValue) -> {
pseudoClassStateChanged(READ_PSEUDO_CLASS, newValue);
pseudoClassStateChanged(UNREAD_PSEUDO_CLASS, !newValue);
};
Message priorItem = null;
{
getStyleClass().add("readable");
}
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
if (priorItem != null) {
priorItem.readProperty().removeListener(readChangeListener);
}
priorItem = item;
if (empty || item == null || item.getText() == null) {
setText(null);
pseudoClassStateChanged(READ_PSEUDO_CLASS, false);
pseudoClassStateChanged(UNREAD_PSEUDO_CLASS, false);
} else {
item.readProperty().addListener(readChangeListener);
setText(item.getText());
pseudoClassStateChanged(READ_PSEUDO_CLASS, item.isRead());
pseudoClassStateChanged(UNREAD_PSEUDO_CLASS, !item.isRead());
}
}
};
}
});

How to make a JavaFX TableView cell editable without first pressing Enter?

I am learning JavaFX and implementing a TableView class. I'd like to make a cell editable without first pressing Enter or double clicking on it. I wonder if it's possible to start entering a new value without first hitting Enter? Thank you.
Looks like I've found a solution to the problem of missing first entered symbols. Data can be entered into a cell as soon as the cell is in focus. There is no necessity to press Enter first or double click on a cell before data input.
Class CellField
//Text box cell
public class CellField {
private static StringBuffer text = new StringBuffer("");
public static String getText() {
return text.toString();
}
public static void setText(String text) {
CellField.text = new StringBuffer(text);
}
//true, if the length of more than one character
public static boolean isLessOrEqualOneSym(){
return CellField.text.length() <= 1;
}
//add character to the end of line
public static void addSymbol(String symbol){
text.append(symbol);
}
public static void clearText() {
setText("");
}
}
Class NewOrderCtrl(part of the code)
class public class NewOrderCtrl extends HBox implements Initializable {
#FXML private TableView<OrderItem> catalogTable;
#FXML private TableColumn<OrderItem, String> numCatalogColumn;
public void initialize(URL url, ResourceBundle resourceBundle) {
numCatalogColumn.setCellFactory(new Callback<TableColumn<OrderItem, String>, TableCell<OrderItem, String>>() {
#Override
public TableCell<OrderItem, String> call(TableColumn<OrderItem, String> orderItemStringTableColumn) {
return new EditingCell();
}
});
catalogTable.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent keyEvent) {
KeyCode keyCode = keyEvent.getCode();
if (keyCode == KeyCode.ENTER || keyCode == KeyCode.ESCAPE){
CellField.clearText();
}
if (keyCode.isDigitKey()) {
int row = catalogTable.getSelectionModel().getSelectedIndex();
catalogTable.edit(row, numCatalogColumn);
}
}
});
}
#FXML
private void onEditStart() {
CellField.clearText();
}
}
Class EditingCell
public class EditingCell extends TableCell<OrderItem, String> {
private TextField textField;
#Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
if (textField == null) {
createTextField();
}
setText(null);
setGraphic(textField);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
textField.requestFocus();
}
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText(String.valueOf(getItem()));
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setGraphic(textField);
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
} else {
setText(getString());
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
}
private void createTextField() {
textField = new TextField(getString());
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
textField.setOnKeyPressed(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent t) {
if (t.getCode() == KeyCode.ENTER) {
commitEdit(textField.getText());
EditingCell.this.getTableView().requestFocus();//why does it lose focus??
EditingCell.this.getTableView().getSelectionModel().selectBelowCell();
} else if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
}
});
textField.setOnKeyReleased(new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent t) {
if (t.getCode().isDigitKey()) {
if (CellField.isLessOrEqualOneSym()) {
CellField.addSymbol(t.getText());
} else {
CellField.setText(textField.getText());
}
textField.setText(CellField.getText());
textField.deselect();
textField.end();
textField.positionCaret(textField.getLength() + 2);//works sometimes
}
}
});
}
private String getString() {
return getItem() == null ? "" : getItem();
}
}
I've finally got everything working how I like it. I've added some formatting stuff since I needed to test that. Users will have to enter some data and the closer it is to excel the easier it will be for most people to use.
Make a new javaFX project called TableTest in package easyedit and paste these files in the right class names.
TableTest.java
package easyedit;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableView;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TableTest extends Application {
#Override
public void start(Stage primaryStage) {
ObservableList<LineItem> items = FXCollections.observableArrayList();
items.addAll(new LineItem("hello",123.45,6),
new LineItem("world",0.01,11));
TableView table = new EasyEditTable().makeTable(items);
Button focusableNode = new Button("Nada");
VBox root = new VBox();
root.getChildren().addAll(table, focusableNode);
Scene scene = new Scene(root, 300, 250);
//css to remove empty lines in table
scene.getStylesheets().add(this.getClass().getResource("css.css").toExternalForm());
primaryStage.setTitle("Easy edit table test");
primaryStage.setScene(scene);
primaryStage.show();
}
}
LineItem.java
package easyedit;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class LineItem {
private final StringProperty desc = new SimpleStringProperty();
private final DoubleProperty amount = new SimpleDoubleProperty();
private final IntegerProperty sort = new SimpleIntegerProperty();
public StringProperty descProperty() {return desc;}
public DoubleProperty amountProperty() {return amount;}
public IntegerProperty sortProperty() {return sort;}
public LineItem(String dsc, double amt, int srt) {
desc.set(dsc); amount.set(amt); sort.set(srt);
}
}
EasyEditTable.java
package easyedit;
import java.text.NumberFormat;
import java.util.Stack;
import javafx.application.Platform;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ObservableList;
import javafx.geometry.Pos;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.util.Callback;
public class EasyEditTable{
private String lastKey = null;
public TableView makeTable(ObservableList<LineItem> items) {
TableView tv = new TableView(items);
tv.setEditable(true);
Stack<LineItem> deletedLines = new Stack<>();
tv.setUserData(deletedLines);
Callback<TableColumn<LineItem,String>, TableCell<LineItem,String>> txtCellFactory =
(TableColumn<LineItem,String> p) -> {return new EditingCell();};
TableColumn<LineItem,String> descCol = new TableColumn<>("desc");
descCol.setCellValueFactory(new PropertyValueFactory<>("desc"));
descCol.setCellFactory(txtCellFactory);
descCol.setOnEditCommit((TableColumn.CellEditEvent<LineItem, String> evt) -> {
evt.getTableView().getItems().get(evt.getTablePosition().getRow())
.descProperty().setValue(evt.getNewValue());
});
final NumberFormat currFmt = NumberFormat.getCurrencyInstance();
TableColumn<LineItem, String> amountCol = new TableColumn<>("amount");
amountCol.setCellValueFactory((TableColumn.CellDataFeatures<LineItem, String> p) -> {
return new SimpleStringProperty(currFmt.format(p.getValue().amountProperty().get()));
});
amountCol.setCellFactory(txtCellFactory);
amountCol.setOnEditCommit((TableColumn.CellEditEvent<LineItem, String> evt) -> {
try {
evt.getTableView().getItems().get(evt.getTablePosition().getRow())
.amountProperty().setValue(Double.parseDouble(evt.getNewValue().replace("$","")));
} catch (NumberFormatException nfe) {
//handle error properly somehow
}
});
amountCol.setComparator((String o1, String o2) -> {
try {//only works in $ countries, use currFmt.parse() instead
return Double.compare(Double.parseDouble(o1.replace("$", "")),
Double.parseDouble(o2.replace("$", "")));
} catch (NumberFormatException numberFormatException) {
return 0;
}
});
TableColumn<LineItem,String> sortCol = new TableColumn<>("sort");
sortCol.setCellValueFactory(new PropertyValueFactory("sort"));
sortCol.setCellFactory(txtCellFactory);
sortCol.setOnEditCommit((TableColumn.CellEditEvent<LineItem, String> evt) -> {
evt.getTableView().getItems().get(evt.getTablePosition().getRow())
.sortProperty().setValue(Integer.parseInt(evt.getNewValue()));//throws nfe
});
tv.getColumns().setAll(descCol, amountCol, sortCol);
tv.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
tv.getSelectionModel().setCellSelectionEnabled(true);
tv.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
tv.addEventFilter(KeyEvent.KEY_PRESSED, (KeyEvent t) -> {
if (tv.getEditingCell() == null && t.getCode() == KeyCode.ENTER) {
if (t.isShiftDown()) {
tv.getSelectionModel().selectAboveCell();
} else {
tv.getSelectionModel().selectBelowCell();
}
t.consume();
}
//I decided not to override the default tab behavior
//using ctrl tab for cell traversal, but arrow keys are better
if (t.isControlDown() && t.getCode() == KeyCode.TAB) {
if (t.isShiftDown()) {
tv.getSelectionModel().selectLeftCell();
} else {
tv.getSelectionModel().selectRightCell();
}
t.consume();
}
});
tv.setOnKeyPressed((KeyEvent t) -> {
TablePosition tp;
if (!t.isControlDown() &&
(t.getCode().isLetterKey() || t.getCode().isDigitKey())) {
lastKey = t.getText();
tp = tv.getFocusModel().getFocusedCell();
tv.edit(tp.getRow(),tp.getTableColumn());
lastKey = null;
}
});
tv.setOnKeyReleased((KeyEvent t) -> {
TablePosition tp;
switch (t.getCode()) {
case INSERT:
items.add(new LineItem("",0d,0));//maybe try adding at position
break;
case DELETE:
tp = tv.getFocusModel().getFocusedCell();
if (tp.getTableColumn() == descCol) {
deletedLines.push(items.remove(tp.getRow()));
} else { //maybe delete cell value
}
break;
case Z:
if (t.isControlDown()) {
if (!deletedLines.isEmpty()) {
items.add(deletedLines.pop());
}
}
}
});
return tv;
}
private class EditingCell extends TableCell{
private TextField textField;
#Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
createTextField();
setText(null);
setGraphic(textField);
//setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
Platform.runLater(() -> {//without this space erases text, f2 doesn't
textField.requestFocus();//also selects
});
if (lastKey != null) {
textField.setText(lastKey);
Platform.runLater(() -> {
textField.deselect();
textField.end();
});
}
}
}
public void commit(){
commitEdit(textField.getText());
}
#Override
public void cancelEdit() {
super.cancelEdit();
try {
setText(getItem().toString());
} catch (Exception e) {}
setGraphic(null);
}
#Override
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(null);
if (getTableColumn().getText().equals("amount"))
setAlignment(Pos.CENTER_RIGHT);
}
}
private void createTextField() {
textField = new TextField(getString());
//doesn't work if clicking a different cell, only focusing out of table
textField.focusedProperty().addListener(
(ObservableValue<? extends Boolean> arg0, Boolean arg1, Boolean arg2) -> {
if (!arg2) commitEdit(textField.getText());
});
textField.setOnKeyReleased((KeyEvent t) -> {
if (t.getCode() == KeyCode.ENTER) {
commitEdit(textField.getText());
EditingCell.this.getTableView().getSelectionModel().selectBelowCell();
}
if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
});
textField.addEventFilter(KeyEvent.KEY_RELEASED, (KeyEvent t) -> {
if (t.getCode() == KeyCode.DELETE) {
t.consume();//stop from deleting line in table keyevent
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
}
}
The css.css file if you want to use it. It goes in the same package.
.table-row-cell:empty {
-fx-background-color: ivory;
}
.table-row-cell:empty .table-cell {
-fx-border-width: 0px;
}
I don't have any problems with characters not showing up or blank cells. Just the blinking cursor is sometimes in the wrong place. Using 8-b127 on XP sp3. I don't like how the textField focusListener doesn't work very well, but it's a small issue.
For the TableView (named tv here) I do this
tv.setOnKeyReleased((KeyEvent t) -> {
TablePosition tp;
switch (t.getCode()) {
//other code cut out here
case Z:
if (t.isControlDown()) {
if (!deletedLines.isEmpty()) {
items.add(deletedLines.pop());
}
break; //don't break for regular Z
}
default:
if (t.getCode().isLetterKey() || t.getCode().isDigitKey()) {
lastKey = t.getText();
tp = tv.getFocusModel().getFocusedCell();
tv.edit(tp.getRow(), tp.getTableColumn());
lastKey = null;
}
}
});
And then when I make the TextField editing cell
#Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
createTextField();
setText(null);
setGraphic(textField);
Platform.runLater(() -> {//without this space erases text, f2 doesn't
textField.requestFocus();//also selects
});
if (lastKey != null) {
textField.setText(lastKey);
Platform.runLater(() -> {
textField.deselect();
textField.end();
textField.positionCaret(textField.getLength()+2);//works sometimes
});
}
}
}
Sometimes the blinking cursor shows up at the front of lastKey but when I keep typing the characters go at the end and the cursor moves to the correct position. If you type really fast, the second character gets ignored.
If you can make it better let me know. I'd like it to work more like excel. I also add this to the standard textField code.
textField.setOnKeyReleased((KeyEvent t) -> {
if (t.getCode() == KeyCode.ENTER) {
commitEdit(textField.getText());
EditingCell.this.getTableView().requestFocus();//why does it lose focus??
EditingCell.this.getTableView().getSelectionModel().selectBelowCell();
} else if (t.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
});

Resources