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

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.

Related

How to get previous table cell value inside cell's updateItem method in JavaFx?

I want to have a TableCell with a custom graphic that animates on value change, where the animation type depends on the nature of the change, so I need to know the previous value to compare to the current one.
Here's your typical custom table cell (Kotlin code):
class MyTableCell<S, T> : TableCell<S, T>() {
override fun updateItem(item: T?, empty: Boolean) {
if (empty || field == null) {
text = null
graphic = null
} else {
// need to get the old value here
}
}
I see that the super method in javafx/scene/control/TableCell.java does know the old value and uses it compare it to the current one, but the override only gets the newValue:
private void updateItem(int oldIndex) {
...
final T oldValue = getItem();
...
final T newValue = currentObservableValue == null ? null : currentObservableValue.getValue();
...
if (oldIndex == index) {
if (!isItemChanged(oldValue, newValue)) {
...
}
...
}
...
updateItem(newValue, false); // sadly, `oldValue` is not passed
I can only think of an ugly workaround, so I wonder if there is some idiomatic way to get the old cell value?
Here is a sample app:
import javafx.application.Application
import javafx.beans.property.SimpleDoubleProperty
import javafx.collections.FXCollections
import javafx.scene.Scene
import javafx.scene.control.*
import javafx.scene.control.cell.PropertyValueFactory
import javafx.stage.Stage
import kotlinx.coroutines.experimental.delay
import kotlinx.coroutines.experimental.launch
import tornadofx.*
class Foo {
val barProperty = SimpleDoubleProperty()
var bar: Double
get() = barProperty.get()
set(value) = barProperty.set(value)
}
class FooApp: Application() {
override fun start(primaryStage: Stage) {
val foos = FXCollections.observableArrayList(
Foo().apply { bar = 42.0 }
)
val table = TableView<Foo>(foos)
val barColumn = TableColumn<Foo, Double>("Bar")
barColumn.cellValueFactory = PropertyValueFactory<Foo, Double>("bar")
barColumn.setCellFactory {
FooTableCell<Foo, Double> { "%.2f".format(it) }
}
table.columns.add(barColumn)
val scene = Scene(table, 400.0, 200.0)
primaryStage.scene = scene
primaryStage.title = "Table Cell"
primaryStage.show()
launch {
while (isActive) {
delay(500)
val oldFoo = foos[0]
// Replacing the old Foo instance with a new one,
// updating the value of the `bar` field:
foos[0] = Foo().apply {
bar = oldFoo.bar - 1.0 + Math.random() * 2.0
}
// because a change to a field cannot be detected by an observable list
// and so does not propagates to the table. This won't result in
// a visible change:
// foos[0].bar = foos[0].bar - 1.0 + Math.random() * 2.0
}
}
}
}
class FooTableCell<S, T>(private val format: (T) -> String) : TableCell<S, T>() {
init {
contentDisplay = ContentDisplay.GRAPHIC_ONLY
itemProperty().addListener(ChangeListener { obs, oldItem, newItem ->
if (newItem != null && oldItem != null && newItem != oldItem) {
// This is never true.
println("!!! Old: $oldItem, New: $newItem")
} else {
println("Change listener:\nOld: $oldItem, New: $newItem\n")
}
})
}
override fun updateItem(item: T?, empty: Boolean) {
val oldItem = this.item
super.updateItem(item, empty)
if (item != null && oldItem != null && item != oldItem) {
// This is never true.
println("!!! Old: $oldItem, New: $item")
} else {
println("updateItem:\nOld: $oldItem, New: $item\n")
}
if (empty || item == null) {
graphic = null
text = null
} else if (tableRow != null) {
val cell = this
graphic = Label().apply {
textProperty().bindBidirectional(cell.textProperty())
}
text = format(item)
}
}
}
fun main(args: Array<String>) {
Application.launch(FooApp::class.java, *args)
}
The actual value of the item property is changed by the default implementation of the updateItem() method, so just get the value before calling the default implementation:
public class MyTableCell<S,T> extends TableCell<S,T> {
#Override
protected void updateItem(T item, boolean empty) {
T oldItem = getItem();
super.updateItem(item, empty) ;
// ...
}
}
Alternatively, you can just register a change listener with the itemProperty():
public class MyTableCell<S,T> extends TableCell<S,T> {
public MyTableCell() {
itemProperty().addListener((obs, oldItem, newItem) -> {
// do animation here...
});
}
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
// other functionality here...
}
}
Here is a SSCCE demonstrating both techniques:
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
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.HBox;
import javafx.stage.Stage;
public class TableCellWithChange extends Application {
public static class ChangeAwareCell<S,T> extends TableCell<S,T> {
public ChangeAwareCell() {
itemProperty().addListener((obs, oldItem, newItem) -> {
System.out.printf("In listener, value for %s changed from %s to %s%n", getTableRow().getItem(), oldItem, newItem);
});
}
#Override
protected void updateItem(T item, boolean empty) {
T oldItem = getItem();
super.updateItem(item, empty);
if (empty) {
setText(null);
} else {
setText(item.toString());
System.out.printf("Change in %s from %s to %s %n", getTableView().getItems().get(getIndex()), oldItem, item);
}
}
}
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> itemCol = column("Item", Item::nameProperty);
table.getColumns().add(itemCol);
TableColumn<Item, Number> valueCol = column("Value", Item:: valueProperty);
table.getColumns().add(valueCol);
valueCol.setCellFactory(tc -> new ChangeAwareCell<>());
TableColumn<Item, Void> changeCol = new TableColumn<>();
changeCol.setCellFactory(tc -> new TableCell<>() {
private Button incButton = new Button("^");
private Button decButton = new Button("v");
private HBox graphic = new HBox(2, incButton, decButton);
{
incButton.setOnAction(e -> {
Item item = getTableRow().getItem();
item.setValue(item.getValue()+1);
});
decButton.setOnAction(e -> {
Item item = getTableRow().getItem();
item.setValue(item.getValue()-1);
});
}
#Override
protected void updateItem(Void item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(graphic);
}
}
});
table.getColumns().add(changeCol);
Random rng = new Random();
for (int i = 1 ; i <= 20 ; i++) {
table.getItems().add(new Item("Item "+i, rng.nextInt(100)));
}
Scene scene = new Scene(table);
primaryStage.setScene(scene);
primaryStage.show();
}
private <S,T> TableColumn<S,T> column(String text, Function<S, ObservableValue<T>> property) {
TableColumn<S,T> col = new TableColumn<>(text);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setPrefWidth(150);
return col ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
#Override
public String toString() {
return getName();
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
Of course, these items will also change, e.g. when the user scrolls around the table, or if the cells are reused in other ways; so this might not be exactly when you want. You might want to add a listener to the appropriate property in the model instead. The simplest way to do this is probably to store a reference to the actual property from the model in the cell, and update that reference when the cell is updated:
public static class ChangeAwareCell<S,T> extends TableCell<S,T> {
private Function<S, ObservableValue<T>> property ;
private ObservableValue<T> lastObservableValue ;
private ChangeListener<T> listener = (obs, oldValue, newValue) -> valueChanged(oldValue, newValue);
public ChangeAwareCell(Function<S, ObservableValue<T>> property) {
this.property = property ;
}
private void valueChanged(T oldValue, T newValue) {
System.out.printf("Value changed from %s to %s %n", oldValue, newValue);
}
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (lastObservableValue != null) {
lastObservableValue.removeListener(listener);
}
if (empty) {
setText(null);
} else {
lastObservableValue = property.apply(getTableRow().getItem());
lastObservableValue.addListener(listener);
setText(item.toString());
}
}
}
And of course make the corresponding change:
valueCol.setCellFactory(tc -> new ChangeAwareCell<>(Item::valueProperty));

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());
}
}
};
}
});

ContextMenu in JavaFX keeps getting duplicates of the same MenuItem

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
}

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