I have an I18N implementation that binds JavaFX UI elements through properties, for e.g.:
def translateLabel(l: Label, key: String, args: Any*): Unit =
l.textProperty().bind(createStringBinding(key, args))
Having a property binding is easy and works well. However I struggle with ComboBox as it takes an ObservableList (of Strings in my case) and I have no idea how to bind my translator functions to that. I am conflicted about the difference between ObservableValue, ObservableList and Property interfaces as they all sound the same.
It has itemsProperty() and valueProperty() however the documentation for these is lacking and vague so I am not sure where they can be used.
What I want to do is have a ComboBox where all elements (or at least the selected / visible one) changes the language dynamically (I18N) as if it was bound, just like a property.
EDIT:
Just to make it easier understand, my current implementation is:
private def setAggregatorComboBox(a: Any): Unit = {
val items: ObservableList[String] = FXCollections.observableArrayList(
noneOptionText.getValue,
"COUNT()",
"AVG()",
"SUM()"
)
measureAggregatorComboBox.getItems.clear()
measureAggregatorComboBox.getItems.addAll(items)
}
Where noneOptionText is a StringProperty that's already bound to a StringBinding that's translated upon class instantiation in this manner:
def translateString(sp: StringProperty, key: String, args: Any*): Unit =
sp.bind(createStringBinding(key, args))
The itemsProperty() is the list of items to show in the combo box popup; it's value is an ObservableList.
The valueProperty() is the selected item (or the value input by the user if the combo box is editable).
What I'd recommend is to have the data in the combo box be the list of keys, and use custom cells to bind the text in each cell to the translation of those keys. I don't speak scala, but in Java it looks like:
ComboBox<String> comboBox = new ComboBox<>();
comboBox.getItems().setAll(getAllKeys());
class TranslationCell extends ListCell<String> {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
textProperty().unbind();
if (empty || item == null) {
setText("");
} else {
textProperty().bind(createStringBinding(item));
}
}
}
comboBox.setCellFactory(lv -> new TranslationCell());
comboBox.setButtonCell(new TranslationCell());
Note now that the valueProperty() contains the key for the selected value.
If you really want to bind the items to an ObservableValue<ObservableList<String>> you can do something like:
comboBox.itemsProperty().bind(Bindings.createObjectBinding(() ->
FXCollections.observableArrayList(...),
...));
where the first ... is a varargs of String values, and the second ... is an observable value, changes in which would prompt the list to be recomputed. (So in your case, I'm guessing you have an ObservableValue<Locale> somewhere representing the current locale; you would use that for the second argument.)
In your specific use case (where only the first element of the list is internationalizable), it might be easier simply to use a listener:
comboBox.getItems().setAll(
noneOptionTest.getValue(),
"COUNT()",
"AVG()",
"SUM");
noneOptionTest.addListener((obs, oldVal, newVal) ->
comboBox.getItems().set(0, newVal));
though I agree this is slightly less elegant.
For completeness:
I am conflicted about the difference between ObservableValue,
ObservableList and Property interfaces as they all sound the same.
ObservableValue<T>: represents a single value of type T which can be observed (meaning that code can be executed when it changes).
Property<T>: represents a writable ObservableValue<T>; the intention is that implementations would have an actual variable representing the value. It defines additional functionality allowing its value to be bound to other ObservableValue<T>.
So, for example:
DoubleProperty x = new SimpleDoubleProperty(6);
DoubleProperty y = new SimpleDoubleProperty(9);
ObservableValue<Number> product = x.multiply(y);
x and y are both Property<Number>; the implementation of SimpleDoubleProperty has an actual double variable representing this value, and you can do things like y.set(7); to change the value.
On the other hand, product is not a Property<Number>; you can't change its value (because doing so would violate the binding: the declared invariant that product.getValue() == x.getValue() * y.getValue()); however it is observable, so you can bind to it:
BooleanProperty answerCorrect = new SimpleBooleanProperty();
answerCorrect.bind(product.isEqualTo(42));
etc.
An ObservableList is somewhat different: it is a java.util.List (a collection of elements), and you can observe it to respond to operations on the list. I.e. if you add a listener to an ObservableList, the listener can determine if elements were added or removed, etc.
Related
I have TreeView filled by my own tree. In class Node I have field "type" which is one of NodeType. The problem is that I want have style for each type of NodeType, e.g. "type1" text color should be green, "type2" text color should be red. I'm new in javaFX. I found solution by james-d ( https://github.com/james-d/heterogeneous-tree-example ), but in this example css style depends on the class name, how can I make it for class field ?
View of TreeView
My understanding is you want a TreeCell that styles differently depending on the NodeType of the Node contained within the TreeItem of said TreeCell. All via CSS. Am I correct?
Assuming I am correct, there are 2 ways I can think of to accomplish this; both of which work best if there is a small number of known NodeTypes. The first involves the use of PseudoClass and the second uses the same strategy as the JavaFX Chart API.
First Option
Create a custom TreeCell that is tailored to using your Node type (i.e. specify the generic signature appropriately). In this custom TreeCell you declare as many PseudoClass static final fields as you need; one for each NodeType. Then you observe the NodeType of the whatever Node is currently displayed in the TreeCell and update the PseudoClass states accordingly.
Here is an example assuming NodeType is an enum that has two constants: HAPPY and SAD.
public class CustomTreeCell<T extends Node> extends TreeCell<T> {
private static final PseudoClass HAPPY = PseudoClass.getPseudoClass("happy");
private static final PseudoClass SAD = PseudoClass.getPseudoClass("sad");
// this listener will activate/deactivate the appropriate PseudoClass states
private final ChangeListener<NodeType> listener = (obs, oldVal, newVal) -> {
pseudoClassStateChanged(HAPPY, newVal == NodeType.HAPPY);
pseudoClassStateChanged(SAD, newVal == NodeType.SAD);
};
// use a weak listener to avoid a memory leak
private final WeakChangeListener<NodeType> weakListener = /* wrap listener */;
public CustomTreeCell() {
getStyleClass().add("custom-tree-cell");
itemProperty().addListener((obs, oldVal, newVal) -> {
if (oldVal != null) {
oldVal.nodeTypeProperty().removeListener(weakListener);
}
if (newVal != null) {
newVal.nodeTypeProperty().addListener(weakListener);
// need to "observe" the initial NodeType of the new Node item.
// You could call the listener manually to avoid code duplication
pseudoClassStateChanged(HAPPY, newVal.getNodeType() == NodeType.HAPPY);
pseudoClassStateChanged(SAD, newVal.getNodeType() == NodeType.SAD);
} else {
// no item in this cell so deactivate all PseudoClass's
pseudoClassStateChanged(HAPPY, false);
pseudoClassStateChanged(SAD, false);
}
});
}
}
Then in your CSS file you can use:
.custom-tree-cell:happy {
/* style when happy */
}
.custom-tree-cell:sad {
/* style when sad */
}
Second Option
Do what the JavaFX Chart API does when dealing with multiple series of data. What it does is dynamically update the style class of the nodes depending on the series' index in a list (e.g. .line-chart-series-data-<index> <-- probably not exactly this).
/*
* Create a custom TreeCell like in the first option but
* without any of the PseudoClass code. This listener should
* be added/removed from the Node item just like weakListener
* is above.
*/
ChangeListener<NodeType> listener = (obs, oldVal, newVal) -> {
// You have to make sure you keep "cell", "indexed-cell", and "tree-cell"
// in order to keep the basic modena styling.
if (newVal == NodeType.HAPPY) {
getStyleClass().setAll("cell", "indexed-cell", "tree-cell", "custom-tree-cell-happy");
} else if (newVal == NodeType.HAPPY) {
getStyleClass().setAll("cell", "indexed-cell", "tree-cell", "custom-tree-cell-sad");
} else {
getStyleClass().setAll("cell", "indexed-cell", "tree-cell"); // revert to regular TreeCell style
}
};
Then in CSS:
.custom-tree-cell-happy {
/* styles */
}
.custom-tree-cell-sad {
/* styles */
}
Both of these options are really only viable when there is a small set of known types. It might become unmaintainable when you have something like 10+ NodeTypes. It becomes pretty much impossible if the number of NodeTypes is dynamic at runtime.
It might be easier to have NodeType, or some intermediate class/data structure, know what color the text should be and set the color programmatically based on the NodeType.
Note: I quickly typed up the code in my answer and did not test it. There may be compiler errors, runtime exceptions, or logic errors in my code.
Edit
Something else came to mind. My code above assumes that NodeType is held in a property and can be changed during runtime. If NodeType is static (unchanging) for each Node then the code can be vastly simplified. Instead of using any listeners you can simple override the following method declared in javafx.scene.control.Cell:
protected void updateItem(Node item, boolean empty)
This method is called every time a new item is set on the cell. Read the documentation, however, as overriding this method requires certain things from the developer (such as calling the super implementation).
I am using cell factory for listview with checkboxes like:
listView.setCellFactory(CheckBoxListCell.forListView(new Callback < Bean, ObservableValue < Boolean >> () {
#Override
public ObservableValue < Boolean > call(Bean item) {
BooleanProperty observable = new SimpleBooleanProperty();
observable.addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected) {
if (!beanChoices.contains(item.toString())) {
beanChoices.add(item.toString());
observable.setValue(true);
//listView.scrollTo(listView.getItems().size() - 1);
}
} else if (wasSelected) {
if (beanChoices.contains(item.toString())) {
beanChoices.remove(item.toString());
observable.setValue(false);
}
}
});
/* [Code] which compares values with bean item string value and select observable to true for that for edit mode
but here the observer not called for beanItem that are under scrollpane of listview. But on scroll it gets called. */
return observable;
}
}));
It works fine but not for all cases.
Case: When I have say more than 10 entries, the scrollpane comes. Say I have beanChoices to be checked that are at 8 or 9 index(you have to scroll to view them). The listener is not called for the items not visible(that are under scrollpane). On Debug, I found that listener is called when I scroll down.
Problem: when I get checked values from beanChoices for above case, it return empty.
Detail: I have beanChoices which I need to make checked for listview items (edit mode). When I update without changing anything. (Assume that the value which is under the scrollpane of listview will be selected and added to beanChoices)
The Callback is used to retrieve the property for the checked state when the item is associated with a cell. The item may be removed from a cell and put in a new one at any time. This is how ListView (and similar controls like TableView) works. CheckBoxListCell simply gets the checked state property every time a new item is associated with the cell.
The return value is also used to set the initial state of the CheckBox. Since you do not properly initialize the property with the correct value the initial state is not preserved.
Also note that it makes little sense to update the value of the property to the new value in the change listener. It happens anyway.
Since BooleanProperty is a wrapper for primitive boolean the possible values are true and false; the ChangeListener only gets called when !Objects.equals(oldValue, newValue) you can be sure that isNowSelected = !wasSelected.
Of course you also need to return the value:
#Override
public ObservableValue < Boolean > call(Bean item) {
final String value = item.toString();
BooleanProperty observable = new SimpleBooleanProperty(beanChoices.contains(value));
observable.addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected) {
beanChoices.add(value);
} else {
beanChoices.remove(value);
}
});
return observable;
}
I also recommend using a Collection of Beans instead of relying on the string representation of the objects. toString many not produce unique results and Beans.equals would be the better choice to compare the objects.
I have a class called "Product", with a double attribute "price". I'm showing it on a table column inside a table view, but i wanted to show the price formatted -- "US$ 20.00" instead of just "20.00".
Here's my code for populating the table view:
priceProductColumn.setCellValueFactory(cellData -> cellData.getValue().priceProperty());
I tried everything: convert the returned value to a string, using the method toString that priceProperty has, etc, but not seems to work.
Do i need to bind an event of something like that?
Use the cellValueFactory as you have it to determine the data that is displayed. The cell value factory is basically a function that takes a CellDataFeatures object and returns an ObservableValue wrapping up the value to be displayed in the table cell. You usually want to call getValue() on the CellDataFeatures object to get the value for the row, and then retrieve a property from it, exactly as you do in your posted code.
Use a cellFactory to determine how to display those data. The cellFactory is a function that takes a TableColumn (which you usually don't need) and returns a TableCell object. Typically you return a subclass of TableCell that override the updateItem() method to set the text (and sometimes the graphic) for the cell, based on the new value it is displaying. In your case you get the price as a Number, and just need to format it as you require and pass the formatted value to the cell's setText(...) method.
It's worth reading the relevant Javadocs: TableColumn.cellFactoryProperty(), and also Cell for a general discussion of cells and cell factories.
priceProductColumn.setCellValueFactory(cellData -> cellData.getValue().priceProperty());
priceProductColumn.setCellFactory(col ->
new TableCell<Product, Number>() {
#Override
public void updateItem(Number price, boolean empty) {
super.updateItem(price, empty);
if (empty) {
setText(null);
} else {
setText(String.format("US$%.2f", price.doubleValue()));
}
}
});
(I'm assuming priceProductColumn is a TableColumn<Product, Number> and Product.priceProperty() returns a DoubleProperty.)
If you have not, read this together with #James_D post.
https://docs.oracle.com/javafx/2/ui_controls/table-view.htm
I have a comboBox cb and an ObservableList<StringProperty> data
I have bound the cb's Items to data as follows:
Bindings.bindContent(cb.getItems(), data);
Suppose data has the following items: str1, str2, str3, str4
When I change data, the combobox gets the new list without any problem.
But if str3 is selected in cb and I change the value of str3 to NewStr3 in data, that change is not getting displayed in cb. And sometimes the list displayed is also wrong (it shows str3 instead of NewStr3) eventhough underlying data it refers is correct.
How can I force combobox to display new values when the underlying model is changed?
The selected item in a combo box is not required to be an element of the combo box's items list. (For example, in an editable combo box, you can type in an item which is not in the list.) If you think about your example from this perspective, it's no surprise that it behaves as you describe.
If you want to force the selected value to be an element of the underlying list when that list may change, you need to define how the selected item should change if the list changes in a way in which it no longer contains the selected item (it is not obvious how you will do this, and probably depends on your application logic). Once you know what you want to do, you can implement it with a ListChangeListener:
cb.getItems().addListener((ListChangeListener.Change change) -> {
String newSelectedItem = ... ; // figure item that should be selected instead
cb.setValue(newSelectedItem);
});
The simplest implementation would be just cb.setValue(null);, which would mean no item was selected if the list changed so that it no longer contained the currently selected item.
Oops ... mis-read the comboBox for a choiceBox - while the basics of this answer apply to both combo- and choiceBox, I don't have a custom ComboBoxX - yet :-)
Basically, it's the responsibility of the SelectionModel to update itself on changes to the items. The intended behaviour implemented in core is to completely clear the selection - that is, null the selectedItem and set selectedIndex to -1 - if the old item was the selectedItem and is replaced or removed. The typical solution for custom behaviour is to implement a custom selection model and set it:
/**
* A SelectionModel that updates the selectedItem if it is contained in
* the data list and was replaced/updated.
*
* #author Jeanette Winzenburg, Berlin
*/
public static class MySelectionModel<T> extends ChoiceBoxSelectionModel<T> {
public MySelectionModel(ChoiceBoxX<T> cb) {
super(cb);
}
#Override
protected void itemsChanged(Change<? extends T> c) {
// selection is in list
if (getSelectedIndex() != -1) {
while (c.next()) {
if (c.wasReplaced() || c.wasUpdated()) {
if (getSelectedIndex() >= c.getFrom()
&& getSelectedIndex() < c.getTo()) {
setSelectedItem(getModelItem(getSelectedIndex()));
return;
}
}
}
}
// super expects a clean change
c.reset();
super.itemsChanged(c);
}
}
// usage
myChoiceBox.setSelectionModel(new MySelectionModel(myChoiceBox));
Unfortunately, core choiceBox doesn't play by the rule - it severely interferes with model's responsibilities (probably because the model implementation doesn't stand up to its duties) which requires a complete re-write of the whole collaborator-stack (choiceBox, -skin, copied -behaviour) such as ChoiceBoxX - which I did just to learn a bit, try remove some of its smells and fix some bugs.
the Original code of set selected Item is:
public void setSelectedItem(Object anObject) {
Object oldSelection = selectedItemReminder;
Object objectToSelect = anObject;
if (oldSelection == null || !oldSelection.equals(anObject)) {
if (anObject != null && !isEditable()) {
// For non editable combo boxes, an invalid selection
// will be rejected.
boolean found = false;
for (int i = 0; i < dataModel.getSize(); i++) {
E element = dataModel.getElementAt(i);
if (anObject.equals(element)) {
found = true;
objectToSelect = element;
break;
}
}
if (!found) {
return;
}
}
in my opinion the line
if (anObject.equals(element)) {
should be
if (element.equals(anObject)) {
Consider a Combo box displaying eg. languages
then you hav a class like
class Language {
String code; // eg. "en"
String name; // eg. "English"
...
}
if you add Language items to your ComboBox the toString function is used to Display an Item. In the above class the toString function would return the name. A call setSelectedItem("en") fails because
String.equals(Language) will fail because Language.toString() will return "English"
the other way round Language.equals(String) would be helping because the class Language could override
boolean equals(String comp) {
return comp.equals(code)
}
Just for clarification, I know how to create a Combobox with the desired behaviour, my question is: is the comparison in the original code a bug or did I miss something fundamental?
The properly implemented Object.equals is symmetric, meaning that there should be no difference between anObject.equals(element) and element.equals(anObject).
You are describing a situation where the combobox model contains objects of type Item, but you want to select select an item by specifying an object of type Prop, where the value of Prop describes some property of Item.
Using technically incorrect implementation of equals() method you can select a combobox item by passing an instance of Prop instead of Item.
With the original code, you will have to provide broken equals() implementation in Prop class, and with your modification you will have to provide broken equals() implementation in Item class. If the Prop is some library class (as String in your example) then the former case is, of course, impossible, and i assume that the reason for your proposed modification is to allow the latter case.
I am not sure that library creators tried to prevent programmers from implementing broken equals() by choosing that specific anObject.equals(element) expression, but even if it was element.equals(anObject) it would still be bad practice to provide deliberately incorrect equals() implementation just for the sake of simplifying the combobox selection.
The proper way to perform selection by property would be to search the combobox data for the item with the required properties or to create a completely new instance of Item with the desired properties and then pass that item into the setSelectedItem.
If you are lucky to already use Java 8 then selecting the required item from a list is one-liner, and if not then you will have to write some boilerplate code with cycle, but at least you will have the proper equals implementation and clean conscience.
To override the inherited equals method, you should pass an object as parameter and not String
public boolean equals(Object obj){
//code goes here
}