Stage.show() changes value of ComboBox - java

Consider the following MCVE. Of course, the functionality of this MCVE is completely pointless, but I need it to work this way in the real implementation.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
#SuppressWarnings("all")
public class MCVE extends Application {
private static final String OPTION_1 = "Option 1 (www.option1.com)";
private static final String OPTION_2 = "Option 2 (www.option2.com)";
private static final String OPTION_3 = "Option 3 (www.option3.com)";
private static final String OPTION_4 = "Option 4 (www.option4.com)";
private static final String OPTION_5 = "Option 5 (www.option5.com)";
ComboBox<String> cb;
#Override
public void start(Stage primaryStage) throws Exception {
VBox outer = new VBox();
cb = new ComboBox<String>();
outer.getChildren().add(cb);
Scene scene = new Scene(outer, 640, 480);
primaryStage.setScene(scene);
Task<Void> task = new Task<Void>() {
#Override
public Void call() {
cb.getItems().addAll(OPTION_1, OPTION_2, OPTION_3, OPTION_4, OPTION_5);
cb.setEditable(true);
// Adds a listener to the selectedItemProperty that gets the
// value inside the parenthesis of the selected item and sets
// this as the text of the ComboBox.
cb.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> {
String[] valSplit = newValue.split("[\\(\\)]");
if (valSplit.length > 1) {
Platform.runLater(() -> cb.getEditor().setText(valSplit[1]));
}
});
cb.getEditor().textProperty().addListener((obs, oldValue, newValue) -> {
System.out.println("CB value: " + newValue);
});
setURL("www.option2.com");
return null;
}
};
task.setOnSucceeded(e -> {
primaryStage.show();
});
new Thread(task).start();
}
public void setURL(String url) {
// First we check if the classValue is the URL of one of the options in
// the ComboBox. If it is we select that option.
for (String option : cb.getItems()) {
// We retrieve the URL of the option.
String opURL = option.split("[\\(\\)]")[1];
// If the URL of the option is equals to the provided URL, we select
// this option and break the for loop.
if (opURL.equals(url)) {
cb.getSelectionModel().select(option);
break;
}
}
}
public static void main(String[] args) {
launch(args);
}
}
Since I invoke setURL("www.option2.com"), I expect it to first select the option in the ComboBox with that URL, and then get the value inside the parenthesis and set that as the text of the ComboBox. So I except the final value of the ComboBox to be "www.option2.com". But this doesn't happen. Instead the final value is "Option 2 (www.option2.com)".
Since I have added a listener to the textProperty of the ComboBox, I can see that the value is first the expected "www.option2.com", but then changes back to "Option 2 (www.option2.com)". After some further investigation, I've found out that it's the invocation of primaryStage.show() that changes the value. More specifically, it's the invocation of the deprecated Parent.impl_processCSS that changes the value.
So if I set the URL after primaryStage.show(), everything works as I except. But if I want to do all of the work before I show the dialog, like I do now, it doesn't.
So why does primaryStage.show() change the value of my ComboBox, and how can I prevent this? Should I maybe use another approach when trying to set the value of a ComboBox?

You could exchange the part of you code which sets the text of the editor of the ComboBox with some code that sets up a cell factory and a converter.
cb.setConverter(new StringConverter<String>(){
#Override
public String toString(String object) {
if(object != null) {
String[] valSplit = object.split("[\\(\\)]");
return valSplit[1];
} else
return null;
}
#Override
public String fromString(String string) {
List<String> collect = cb.getItems().stream().filter(s -> s.contains(string)).collect(Collectors.toList());
if(collect.size() == 1)
return collect.get(0);
else
return null;
}
});
cb.setCellFactory(item -> {
return new ListCell<String>(){
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if(item == null || empty)
setText("");
else
setText(item);
}
};
});
The toString method of your converter will format the selected item in the needed form, and the cell factory ensures that the items in the drop down list are displayed in the original format.
Note: I have also filled the fromString method of the converter. This method is executed, when the user types into the editor then presses enter. This implementation checks all the items in the list, and if there is only one single item which contains the typed string, that item will be selected.

Related

Can I check whether a TableCell's contents will overrun when building it's CellValueFactory

I have a JavaFX table column which I would like to display a comma-separated list of strings, unless the text does not fit within the current bounds of the cell, at which point it would display, for example, "Foo and 3 others...", or "3 Bars", i.e. reflecting the number of elements in the list.
Is there a way to check, when building a CellValueFactory for a table column, whether the text would overrun the cell, so I could switch between these two behaviors?
You can specify an overrun style for Labeled controls like TableCells.
Overrun style ELLIPSIS will automatically add these ellipses as needed to indicate if the content would have extended outside of the label.
I recommend doing this in a cell factory, like so:
column.setCellFactory(() -> {
TableCell<?, ?> cell = new TableCell<>();
cell.setTextOverrun(OverrunStyle.ELLIPSIS);
return cell;
});
So you would need to use the cell factory instead of the cell value factory.
The reason I recommend cell factory is because the table creates and destroys cells on its own as needed, so you'd have a hard time getting all those instances and setting their overrun behavior if you didn't have control of those cells creation like you do with the cell factory.
New attempt
Try something along these lines, you might need to tweak the method to get the length of your string, and you might want to try to figure out the current length of the table cell whenever you update it, but this should get you started. Think it's a decent approach?
public class TestApplication extends Application {
public static void main(String[] args) {
Application.launch(args);
}
public void start(final Stage stage) {
stage.setResizable(true);
TestTableView table = new TestTableView();
ObservableList<String> items = table.getItems();
items.add("this,is,short,list");
items.add("this,is,long,list,it,just,keeps,going,on,and,on,and,on");
Scene scene = new Scene(table, 400, 200);
stage.setScene(scene);
stage.show();
}
/**
* Note: this does not take into account font or any styles.
* <p>
* You might want to modify this to put the text in a label, apply fonts and css, layout the label,
* then get the width.
*/
private static double calculatePixelWidthOfString(String str) {
return new Text(str).getBoundsInLocal().getWidth();
}
public class TestTableView extends TableView<String> {
public TestTableView() {
final TableColumn<String, CsvString> column = new TableColumn<>("COL1");
column.setCellValueFactory(cdf -> {
return new ReadOnlyObjectWrapper<>(new CsvString(cdf.getValue()));
});
column.setCellFactory(col -> {
return new TableCell<String, CsvString>() {
#Override
protected void updateItem(CsvString item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
} else {
String text = item.getText();
// get the width, might need to tweak this.
double textWidth = calculatePixelWidthOfString(text);
// might want to compare against current cell width
if (textWidth > 100) {
// modify the text here
text = item.getNumElements() + " elements";
}
setText(text);
}
}
};
});
this.getColumns().add(column);
}
}
private static class CsvString {
private final String text;
private final String[] elements;
public CsvString(String string) {
Objects.requireNonNull(string);
this.text = string;
this.elements = string.split(" *, *");
}
public int getNumElements() {
return elements.length;
}
public String getText() {
return text;
}
}
}

How to programatically select ComboBox value within Cell Factory?

My TableView uses a custom CellFactory to display a ComboBox in one column, allowing the user to select from available options. Those options are loaded after the TableView is populated (as they can change based on the user's selections elsewhere in the scene).
In the MCVE below, I have two columns for my Item class: Name and Color. Within the Color column, I have the ComboBox which will display the current value of the Item's itemColor property.
You will see that the ComboBox is not populated with a list of values yet and item "Three" has no value selected.
What I need is this:
When the user clicks on the "Load Available Colors" button, the list for the ComboBox is created. The user can now select any of the available colors. However, if there is not already a value for the item's color, I want the first color in the ComboBoxes to be selected automatically; so item "Three" would now show the color "Red" as being selected.
THE MCVE
Item.java:
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
public class Item {
private StringProperty itemName = new SimpleStringProperty();
private StringProperty itemColor = new SimpleStringProperty();
public Item(String name, String color) {
this.itemName.set(name);
this.itemColor.set(color);
}
public String getItemName() {
return itemName.get();
}
public void setItemName(String itemName) {
this.itemName.set(itemName);
}
public StringProperty itemNameProperty() {
return itemName;
}
public String getItemColor() {
return itemColor.get();
}
public void setItemColor(String itemColor) {
this.itemColor.set(itemColor);
}
public StringProperty itemColorProperty() {
return itemColor;
}
}
Main.java:
import javafx.application.Application;
import javafx.beans.property.SimpleObjectProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class Main extends Application {
// List of items
private static ObservableList<Item> listOfItems = FXCollections.observableArrayList();
// List of available Colors. These will be selectable from the ComboBox
private static ObservableList<String> availableColors = FXCollections.observableArrayList();
public static void main(String[] args) {
launch(args);
}
private static void buildSampleData() {
availableColors.addAll("Red", "Blue", "Green", "Yellow", "Black");
}
#Override
public void start(Stage primaryStage) {
// Simple Interface
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
// Build a list of sample data. This data is loaded from my data model and passed to the constructor
// of this editor in my real application.
listOfItems.addAll(
new Item("One", "Black"),
new Item("Two", "Black"),
new Item("Three", null),
new Item("Four", "Green"),
new Item("Five", "Red")
);
// TableView to display the list of items
TableView<Item> tableView = new TableView<>();
// Create the TableColumn
TableColumn<Item, String> colName = new TableColumn<>("Name");
TableColumn<Item, String> colColor = new TableColumn<>("Color");
// Cell Property Factories
colName.setCellValueFactory(column -> new SimpleObjectProperty<>(column.getValue().getItemName()));
colColor.setCellValueFactory(column -> new SimpleObjectProperty<>(column.getValue().getItemColor()));
// Add ComboBox to the Color column, populated with the list of availableColors
colColor.setCellFactory(tc -> {
ComboBox<String> comboBox = new ComboBox<>(availableColors);
comboBox.setMaxWidth(Double.MAX_VALUE);
TableCell<Item, String> cell = new TableCell<Item, String>() {
#Override
protected void updateItem(String color, boolean empty) {
super.updateItem(color, empty);
if (empty) {
setGraphic(null);
} else {
setGraphic(comboBox);
comboBox.setValue(color);
}
}
};
// Set the action of the ComboBox to set the right Value to the ValuePair
comboBox.setOnAction(event -> {
listOfItems.get(cell.getIndex()).setItemColor(comboBox.getValue());
});
return cell;
});
// Add the column to the TableView
tableView.getColumns().addAll(colName, colColor);
tableView.setItems(listOfItems);
// Add button to load the data
Button btnLoadData = new Button("Load Available Colors");
btnLoadData.setOnAction(event -> {
buildSampleData();
});
root.getChildren().add(btnLoadData);
// Add the TableView to the root layout
root.getChildren().add(tableView);
Button btnPrintAll = new Button("Print All");
btnPrintAll.setOnAction(event -> {
for (Item item : listOfItems) {
System.out.println(item.getItemName() + " : " + item.getItemColor());
}
});
root.getChildren().add(btnPrintAll);
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Sample");
primaryStage.show();
}
}
Now, with a regular ComboBox, a simple call to comboBox.getSelectionModel().selectFirst() after loading the availableColors would be fine. But since this ComboBox is created within the CellFactory, I am not sure how to update it once the list of colors is populated.
Indidentally, I use this CellFactory implementation instead of a ComboBoxTableCell because I want them to be visible without having to enter edit mode on the TableView.
I actually took kleopatra's advice and updated my data model to include a default value instead. I agree this is cleaner and more appropriate approach.

JavaFX combobox, on item clicked

My problem is as follows,
For the sake of this question I reproduced the problem in a new project.
Say I have this application with a combobox in it, there could be 1 or more items in there. And I would like it to be so that when the user clicks an item in the combobox that 'something' happens.
I produced the following code:
obsvList.add("item1");
cbTest.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
System.out.println("Item clicked");
}
});
This works when the application starts and an item is selected for the first time. This also works when there are 2 or more items in the combobox (when the user clicks item 1, then item 2, then item 1 for example)
However my problem is that when there is only 1 item in the combobox, let's say "item1". And the user reopens the combobox and clicks "item1" again then it won't redo the action.
It will only print the line "Item Clicked" when a 'new' item is clicked.
I hope it made it clear what the problem i'm experiencing is, if not please ask for clarification and I will give so where needed.
Thanks in advance!
The functionality of a combo box is to present the user with a list of options from which to choose. When you are using a control which implies selection, you should really ensure that the UI is always consistent with the option that is selected. If you do this, then it makes no sense to "repeat an action" when the user "reselects" the same option (because the UI is already in the required state). One approach to this is to use binding or listeners on the combo box's value:
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.scene.Scene;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class ComboBoxExample extends Application {
#Override
public void start(Stage primaryStage) {
ComboBox<Item> choices = new ComboBox<>();
for (int i = 1 ; i <=3 ; i++) {
choices.getItems().add(new Item("Choice "+i, "These are the details for choice "+i));
}
Label label = new Label();
choices.valueProperty().addListener((obs, oldItem, newItem) -> {
label.textProperty().unbind();
if (newItem == null) {
label.setText("");
} else {
label.textProperty().bind(newItem.detailsProperty());
}
});
BorderPane root = new BorderPane();
root.setCenter(label);
root.setTop(choices);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public class Item {
private final String name ;
private final StringProperty details = new SimpleStringProperty() ;
public Item(String name, String details) {
this.name = name ;
setDetails(details) ;
}
public String getName() {
return name ;
}
#Override
public String toString() {
return getName();
}
public final StringProperty detailsProperty() {
return this.details;
}
public final String getDetails() {
return this.detailsProperty().get();
}
public final void setDetails(final String details) {
this.detailsProperty().set(details);
}
}
public static void main(String[] args) {
launch(args);
}
}
In this case, there is never a need to repeat an action when the user "reselects" the same option, because the code always assures that the UI is consistent with what is selected anyway (there is necessarily nothing to do if the user selects the option that is already selected). By using bindings in the part of the UI showing the details (just a simple label in this case), we are assured that the UI stays up to date if the data changes externally. (Obviously in a real application, this may be far more complex, but the basic strategy is still exactly the same.)
On the other hand, functionality that requires an action to be repeated if the user selects the same functionality is better considered as presenting the user with a set of "actions". The appropriate controls for this are things like menus, toolbars with buttons, and MenuButtons.
An example of a set of repeatable actions is:
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.MenuButton;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class MenuButtonExample extends Application {
#Override
public void start(Stage primaryStage) {
MenuButton menuButton = new MenuButton("Items");
Label label = new Label();
Item[] items = new Item[3];
for (int i = 1 ; i <=3 ; i++) {
items[i-1] = new Item("Item "+i);
}
for (Item item : items) {
MenuItem menuItem = new MenuItem(item.getName());
menuItem.setOnAction(e -> item.setTimesChosen(item.getTimesChosen() + 1));
menuButton.getItems().add(menuItem);
}
label.textProperty().bind(Bindings.createStringBinding(() ->
Stream.of(items)
.map(item -> String.format("%s chosen %d times", item.getName(), item.getTimesChosen()))
.collect(Collectors.joining("\n")),
Stream.of(items)
.map(Item::timesChosenProperty)
.collect(Collectors.toList()).toArray(new IntegerProperty[0])));
BorderPane root = new BorderPane();
root.setCenter(label);
root.setTop(menuButton);
Scene scene = new Scene(root, 400, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Item {
private final String name ;
private final IntegerProperty timesChosen = new SimpleIntegerProperty();
public Item(String name) {
this.name = name ;
}
public String getName() {
return name ;
}
#Override
public String toString() {
return getName();
}
public final IntegerProperty timesChosenProperty() {
return this.timesChosen;
}
public final int getTimesChosen() {
return this.timesChosenProperty().get();
}
public final void setTimesChosen(final int timesChosen) {
this.timesChosenProperty().set(timesChosen);
}
}
public static void main(String[] args) {
launch(args);
}
}
The idea is to set a listener on the ListView pane, that appears whenever you click on the ComboBox. The ListView instance is created once the ComboBox is first loaded in the JavaFX scene. Therefore, we add a listener on the ComboBox to check when it appears on the scene, and then through the "lookup" method we get the ListView and add a listener to it.
private EventHandler<MouseEvent> cboxMouseEventHandler;
private void initComboBox() {
ComboBox<String> comboBox = new ComboBox<String>();
comboBox.getItems().add("Item 1");
comboBox.getItems().add("Item 2");
comboBox.getItems().add("Item 3");
comboBox.sceneProperty().addListener((a,oldScene,newScene) -> {
if(newScene == null || cboxMouseEventHandler != null)
return;
ListView<?> listView = (ListView<?>) comboBox.lookup(".list-view");
if(listView != null) {
cboxMouseEventHandler = (e) -> {
Platform.runLater(()-> {
String selectedValue = (String) listView.getSelectionModel().getSelectedItem();
if(selectedValue.equals("Item 1"))
System.out.println("Item 1 clicked");
});
}; // cboxMouseEventHandler
listView.addEventFilter(MouseEvent.MOUSE_PRESSED, cboxMouseEventHandler);
} // if
});
} // initComboBox

JavaFX 8, ListView with Checkboxes

I want to create a simple ListView. I have figured out I can use the method setCellFactory() but I don't understand how to use them correctly. So far I have:
myListView.setCellFactory(CheckBoxListCell.forListView(property));
With "property" being something called a Callback--I think Callback has something to do with bidirectional bounding. So I created a
property = new CallBack<String, ObservableValue<Boolean>>();
My compiler is telling me if I create a new Callback, I need to overwrite the method call.
And here I am stuck. What do I do with that method call? I can implement it, but what should I return, or use it for? I want to click my checkbox on any listItem and have it display "hi" in console.
If you have a ListView<String>, then each item in the ListView is a String, and the CheckBoxListCell.forListView(...) method expects a Callback<String, ObservableValue<Boolean>>.
In the pre-Java 8 way of thinking of things, a Callback<String, ObservableValue<Boolean>> is an interface that defines a single method,
public ObservableValue<Boolean> call(String s) ;
So you need something that implements that interface, and you pass in an object of that type.
The documentation also tells you how that callback is used:
A Callback that, given an object of type T (which is a value taken out
of the ListView.items list), will return an
ObservableValue that represents whether the given item is
selected or not. This ObservableValue will be bound bidirectionally
(meaning that the CheckBox in the cell will set/unset this property
based on user interactions, and the CheckBox will reflect the state of
the ObservableValue, if it changes externally).
(Since you have a ListView<String>, here T is String.) So, for each element in the list view (each element is a String), the callback is used to determine an ObservableValue<Boolean> which is bidirectionally bound to the state of the checkbox. I.e. if the checkbox is checked, that property is set to true, and if unchecked it is set to false. Conversely, if the property is set to true (or false) programmatically, the checkbox is checked (or unchecked).
The typical use case here is that the type of item in the ListView would have a BooleanProperty as part of its state. So you would typically use this with some kind of custom class representing your data, as follows with the inner Item class:
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Callback;
public class ListViewWithCheckBox extends Application {
#Override
public void start(Stage primaryStage) {
ListView<Item> listView = new ListView<>();
for (int i=1; i<=20; i++) {
Item item = new Item("Item "+i, false);
// observe item's on property and display message if it changes:
item.onProperty().addListener((obs, wasOn, isNowOn) -> {
System.out.println(item.getName() + " changed on state from "+wasOn+" to "+isNowOn);
});
listView.getItems().add(item);
}
listView.setCellFactory(CheckBoxListCell.forListView(new Callback<Item, ObservableValue<Boolean>>() {
#Override
public ObservableValue<Boolean> call(Item item) {
return item.onProperty();
}
}));
BorderPane root = new BorderPane(listView);
Scene scene = new Scene(root, 250, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final BooleanProperty on = new SimpleBooleanProperty();
public Item(String name, boolean on) {
setName(name);
setOn(on);
}
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 BooleanProperty onProperty() {
return this.on;
}
public final boolean isOn() {
return this.onProperty().get();
}
public final void setOn(final boolean on) {
this.onProperty().set(on);
}
#Override
public String toString() {
return getName();
}
}
public static void main(String[] args) {
launch(args);
}
}
If you genuinely have a ListView<String>, it's not really clear what the property you are setting by clicking on the check box would be. But there's nothing to stop you creating one in the callback just for the purpose of binding to the check box's selected state:
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.ListView;
import javafx.scene.control.cell.CheckBoxListCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Callback;
public class ListViewWithStringAndCheckBox extends Application {
#Override
public void start(Stage primaryStage) {
ListView<String> listView = new ListView<>();
for (int i = 1; i <= 20 ; i++) {
String item = "Item "+i ;
listView.getItems().add(item);
}
listView.setCellFactory(CheckBoxListCell.forListView(new Callback<String, ObservableValue<Boolean>>() {
#Override
public ObservableValue<Boolean> call(String item) {
BooleanProperty observable = new SimpleBooleanProperty();
observable.addListener((obs, wasSelected, isNowSelected) ->
System.out.println("Check box for "+item+" changed from "+wasSelected+" to "+isNowSelected)
);
return observable ;
}
}));
BorderPane root = new BorderPane(listView);
Scene scene = new Scene(root, 250, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
Notice that in this case, the BooleanPropertys are potentially being created and discarded frequently. This probably isn't a problem in practice, but it does mean the first version, with the dedicated model class, may perform better.
In Java 8, you can simplify the code. Because the Callback interface has only one abstract method (making it a Functional Interface), you can think of a Callback<Item, ObservableValue<Boolean>> as a function which takes a Item and generates an ObservableValue<Boolean>. So the cell factory in the first example could be written with a lambda expression:
listView.setCellFactory(CheckBoxListCell.forListView(item -> item.onProperty()));
or, even more succinctly using method references:
listView.setCellFactory(CheckBoxListCell.forListView(Item::onProperty));
listView.setCellFactory(CheckBoxListCell.forListView(new Callback<String, ObservableValue<Boolean>>() {
#Override
public ObservableValue<Boolean> call(String item) {
BooleanProperty observable = new SimpleBooleanProperty();
observable.addListener((obs, wasSelected, isNowSelected) ->
System.out.println("Check box for "+item+" changed from "+wasSelected+" to "+isNowSelected)
);
return observable ;
}
}));
Thank you!
This helps me to solve my problem.
Thanks for previous answers.
I miss the information that setCellValueFactory is not needed, but value assigned should also be done in setCellFactory. Here is my approach (much copied from previous solution).
public TreeTableColumn<RowContainer, Boolean> treetblcolHide;
...
treetblcolHide.setCellFactory(CheckBoxTreeTableCell.<RowContainer, Boolean>forTreeTableColumn(new Callback<Integer, ObservableValue<Boolean>>() {
#Override
public ObservableValue<Boolean> call(final Integer param) {
final RowContainer rowitem = treetblcolHide.getTreeTableView().getTreeItem(param).getValue();
BooleanProperty observable = new SimpleBooleanProperty();
observable.addListener(new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
rowitem.setHideMenuItem(newValue.toString());
}
}
);
observable.setValue(Boolean.parseBoolean(rowitem.getHideMenuItem()));
return observable ;
}
}));

JavaFX HTMLEditor text change listener

I'm fairly new to the JavaFX world, and I can't seem to figure out how to listen for text-modify events in the HTMLEditor component.
I need this since I'm hooking this widget to a model, which needs updating.
The addEventFilter API, with a KeyEvent.KEY_TYPED event type doesn't seem to be working as it should. When its handler is called, the getHTMLText() isn't updated yet with the most recent character (if someone doesn't understand this paragraph, I'll provide a step-by-step example).
The TextField has a textProperty() on which a listener can be attached.
Now what about the HTMLEditor?
Also, it would be nice to have the listener called ONLY on text modify events (and not on CTRL+A, for example). You know... like SWT Text's addModifyListener().
While using JavaFX HTMLEditor in one of my project application, I also faced a similar situation. I ended up adding a button, upon whose click the parsing of the HTML text would happen, and further tasks executed. With AnchorPane, I was able to add the button on the HTMLEditor seamlessly, and it looked like a part of it.
Anyways, here's a little example of how you can achieve what you want without any extra button:
package application;
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.web.HTMLEditor;
import javafx.stage.Stage;
public class Main extends Application
{
#Override
public void start(Stage primaryStage)
{
try
{
final HTMLEditor editor = new HTMLEditor();
Scene scene = new Scene(editor);
primaryStage.setScene(scene);
editor.setOnKeyReleased(new EventHandler<KeyEvent>()
{
#Override
public void handle(KeyEvent event)
{
if (isValidEvent(event))
{
System.out.println(editor.getHtmlText());
}
}
private boolean isValidEvent(KeyEvent event)
{
return !isSelectAllEvent(event)
&& ((isPasteEvent(event)) || isCharacterKeyReleased(event));
}
private boolean isSelectAllEvent(KeyEvent event)
{
return event.isShortcutDown() && event.getCode() == KeyCode.A;
}
private boolean isPasteEvent(KeyEvent event)
{
return event.isShortcutDown() && event.getCode() == KeyCode.V;
}
private boolean isCharacterKeyReleased(KeyEvent event)
{
// Make custom changes here..
switch (event.getCode())
{
case ALT:
case COMMAND:
case CONTROL:
case SHIFT:
return false;
default:
return true;
}
}
});
primaryStage.show();
}
catch (Exception e)
{
e.printStackTrace();
}
}
public static void main(String[] args)
{
launch(args);
}
}
UPDATE:
Upon a bit more of thinking, I found a way to get event handling done even on those button clicks. Here's how:
EventHandler<MouseEvent> onMouseExitedHandler = new EventHandler<MouseEvent>()
{
#Override
public void handle(MouseEvent event)
{
System.out.println(editor.getHtmlText());
}
};
for (Node node : editor.lookupAll("ToolBar"))
{
node.setOnMouseExited(onMouseExitedHandler);
}
If you see the HTMLEditor, it has two ToolBars.
What I'm doing in the code is looking up for those two toolbars, and setting an onMouseExited event handler. The analogy is that if the user enters and makes some changes on the HTML Text and exits the toolbar, an event will be fired, which can then be handled.
You can even set different kind of event handlers on these two toolbars, based on your needs, but in my opinion, these onMouseExited event handlers provide a very wide coverage when used with the onKeyReleased event handlers. The coverage based on onMouseExited handler is not exact though.
here is a simple one
public class HtmlEditorListener {
private final BooleanProperty editedProperty;
private String htmlRef;
public HtmlEditorListener(final HTMLEditor editor) {
editedProperty = new SimpleBooleanProperty();
editedProperty.addListener((ov, o, n) -> htmlRef = n? null: editor.getHtmlText());
editedProperty.set(false);
editor.setOnMouseClicked(e -> checkEdition(editor.getHtmlText()));
editor.addEventFilter(KeyEvent.KEY_TYPED, e -> checkEdition(editor.getHtmlText()));
}
public BooleanProperty editedProperty() {
return editedProperty;
}
private void checkEdition(final String html) {
if (editedProperty.get()) {
return;
}
editedProperty.set(htmlRef != null
&& html.length() != htmlRef.length()
|| !html.equals(htmlRef));
}
}
HtmlEditor is based on Web view
HTMLEditor editor = getEditor();
WebView webView = (WebView) getEditor().lookup("WebView");
new WebViewEditorListener(webView, new ChangeListener<String>() {
#Override
public void changed(ObservableValue<? extends String> observable, String oldValue, String newValue) {
}
});
Add Callback for tracking html changes.
public static class WebViewEditorListener {
private final ChangeListener<String> listener;
private final WebPage webPage;
private String htmlRef, innerText;
public WebViewEditorListener(final WebView editor, ChangeListener<String> listener) {
this.listener = listener;
webPage = Accessor.getPageFor(editor.getEngine());
editor.setOnMouseClicked(e -> onKeyTyped(webPage.getHtml(webPage.getMainFrame())));
editor.addEventFilter(KeyEvent.KEY_TYPED, e -> onKeyTyped(webPage.getHtml(webPage.getMainFrame())));
}
public String getHtmlContent(){
return htmlRef == null ? "" : htmlRef ;
}
private void onKeyTyped(final String html) {
boolean isEqual = htmlRef != null ? htmlRef.length() == html.length() : html == null;
if (!isEqual){
String text = webPage.getInnerText(webPage.getMainFrame());
listener.changed(null, innerText, text);
innerText = text;
htmlRef = html;
}
}
}

Categories

Resources