JavaFX 8, ListView with Checkboxes scrollpane issue - java

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.

Related

JavaFx ComboBox binding confusion

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.

How do you iterate through the rows in a JFXTreeTableView?

I'm making a JavaFX project and using the Jfoenix custom library for nicer components. In a schedule table I have, I need the rows to become red if the start date of the event has passed already however I cannot find any answers online anywhere as to how I should iterate through the rows.
In my CSS file, I need this line to set the rows to red if they match the given criteria with the pseudo class toggleRed.
.jfx-tree-table-view > .virtual-flow > .clipped-container > .sheet > .tree-table-row-cell:filled:toggleRed {
-fx-background-color: red;
}
So in my controller initialize method, I'm going to have this line if the row object is valid
row.pseudoClassStateChanged(PseudoClass.getPseudoClass("toggleRed"), true);
I need some kind of for-loop to get every table row in the table to call on this line but haven't found anything that works yet. Please help. I'm completely lost and have wasted far too much time on this. Thanks!!!
You need to change the rowFactory and update the pseudoclass state according to the item's data property and the current time.
The following example should provide you with an idea of how to implement this:
final PseudoClass toggleRed = PseudoClass.getPseudoClass("toggleRed");
ObjectProperty<LocalDate> currentDate = ...;
treeTableView.setRowFactory(ttv -> new JFXTreeTableRow<Job>() {
private final InvalidationListener listener = o -> {
Job item = getItem();
pseudoClassStateChanged(toggleRed, item != null && item.getStartDate().isAfter(currentDate.get()));
};
private final WeakInvalidationListener l = new WeakInvalidationListener(listener);
{
// listen to changes of the currentDate property
currentDate.addListener(l);
}
#Override
protected void updateItem(Job item, boolean empty) {
// stop listening to property of old object
Job oldItem = getItem();
if (oldItem != null) {
oldItem.startDateProperty().removeListener(l);
}
super.updateItem(item, empty);
// listen to property of new object
if (item != null) {
item.startDateProperty().addListener(l);
}
// update pseudoclass
listener.invalidated(null);
}
});
If the start dates and/or the current date are immutable, you could reduce the amount of listeners used.

Hiding Multiple Elements in JTable via ButtonClick

I am currently working on a tool which edits data dynamically in a JTable. I want to hide the targeted row whenever a button is clicked. Right now I am using RowFilter. Whenever the button isClicked, a new filter is created:
RowFilter<MyTableModel, Object> rowFilter = null;
try {
rowFilter = RowFilter.notFilter(RowFilter.regexFilter(((String)dataTable.getValueAt(dataTable.getSelectedRow(), 0)),0));
} catch (java.util.regex.PatternSyntaxException e) {
return;
}
sorter.setRowFilter(rowFilter);
This only works for one element each time the button is clicked. I want to stay them hidden, so you can continously hide elemtens in the table. It is important to mention that I do not want to delete the rows, just hide them.
I hope someone has an easy answer for this, looking for quite a while now.
This method sorter.setRowFilter(rowFilter); is replacing the filter every time you "add" a new filter. So, it's "forgetting" the old rules. What you have to do is edit the existing filter to include the new rules for filtering.
Check out the documentation for more details.
In any case, I extracted a part of the documentation which you should try to implement.
From RowFilter Javadoc:
Subclasses must override the include method to indicate whether the
entry should be shown in the view. The Entry argument can be used to
obtain the values in each of the columns in that entry. The following
example shows an include method that allows only entries containing
one or more values starting with the string "a":
RowFilter<Object,Object> startsWithAFilter = new RowFilter<Object,Object>() {
public boolean include(Entry<? extends Object, ? extends Object> entry) {
for (int i = entry.getValueCount() - 1; i >= 0; i--) {
if (entry.getStringValue(i).startsWith("a")) {
// The value starts with "a", include it
return true;
}
}
// None of the columns start with "a"; return false so that this
// entry is not shown
return false;
}
};
This means that the include() method is going to return true or false depending if an item should be shown.
Therefore, you should only set the RowFilter once, and reimplment the include() method to match all the rules you currently have set upon your view.

ComboBox not showing bound values

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.

JavaFx Combobox lazy loading images

I am using the JavaFX Combobox for the first time... and I have over 2000 icons in the combobox (they can be filtered via AutoCompleteComboBoxListener that I found from StackOverflow).
I am planning to use the ExecutorService to fetch the images from a zip-file.
Now, the problem is that how can I figure out the currently visible items in the Combobox?
I am setting a custom ListCellFactory for the ComboBox, and I have a custom ListCell class, that also displays the icon. Can I somehow check from within the ListCell object whether the item "is showing" ?
Thanks.
Just note first that if you are loading the images from individual files instead of from a zip file, there is a mechanism that avoids having to work directly with any kind of threading at all:
ComboBox<MyDataType> comboBox = new ComboBox<>();
comboBox.setCellFactory(listView -> new ListCell<MyDataType>() {
private ImageView imageView = new ImageView();
#Override
public void updateItem(MyDataType item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
String imageURL = item.getImageURL();
Image image = new Image(imageURL, true); // true means load in background
imageView.setImage(image);
setGraphic(imageView);
}
}
});
Unfortunately, if you're loading from a zip file, I don't think you can use this, so you'll need to create your own background tasks. You need to just be a little careful to make sure that you don't use an image loaded in the background if the item in the cell changes during the loading process (which is pretty likely if the user scrolls a lot).
(Update note: I changed this to listen for changes in the itemProperty() of the cell, instead of using the updateItem(...) method. The updateItem(...) method can be called more frequently than when the actual item displayed by the cell changes, so this approach avoids unnecessary "reloading" of images.)
Something like:
ExecutorService exec = ... ;
ComboBox<MyDataType> comboBox = new ComboBox<>();
comboBox.setCellFactory(listView -> {
ListCell<MyDataType> cell = new ListCell<MyDataType>() ;
ImageView imageView = new ImageView();
ObjectProperty<Task<Image>> loadingTask = new SimpleObjectProperty<>();
cell.emptyProperty().addListener((obs, wasEmpty, isNotEmpty) -> {
if (isNowEmpty) {
cell.setGraphic(null);
cell.setText(null);
} else {
cell.setGraphic(imageView);
}
});
cell.itemProperty().addListener((obs, oldItem, newItem) -> {
if (loadingTask.get() != null &&
loadingTask.get().getState() != Worker.State.SUCCEEDED &&
loadingTask.get().getState() != Worker.State.FAILED) {
loadingTask.get().cancel();
}
loadingTask.set(null) ;
if (newItem != null) {
Task<Image> task = new Task<Image>() {
#Override
public Image call() throws Exception {
Image image = ... ; // retrieve image for item
return image ;
}
};
loadingTask.set(task);
task.setOnSucceeded(event -> imageView.setImage(task.getValue()));
exec.submit(task);
cell.setText(...); // some text from item...
}
});
return cell ;
});
Some thoughts on performance here:
First, the "virtualized" mechanism of the ComboBox means that only a small number of these cells will ever be created, so you don't need to worry that you're immediately starting thousands of threads loading images, or indeed that you will ever have thousands of images in memory.
When the user scrolls through the list, the itemProperty(...) may change quite frequently as the cells are reused for new items. It's important to make sure you don't use images from threads that are started but don't finish before the item changes again; that's the purpose of canceling an existing task at the beginning of the item change listener. Canceling the task will prevent the onSucceeded handler from being invoked. However, you will still have those threads running, so if possible the implementation of your call() method should check the isCancelled() flag and abort as quickly as possible if it returns true. This might be tricky to implement: I would experiment and see how it works with a simple implementation first.
Even if your list has 2000 items javafx will only create listcell objects for the visible cells (plus one or two more for half visible cells) so there's not really a lot TODO for you to load Images lazy - just load them when updateItem is called - and maybe cache already loaded Images in a lifo Cache so that not all of them stay in memory
Current visible item implies the current selected item on combobox. You can get the selected item using
comboboxname.getSelectionModel().getSelectedItem();

Categories