JavaFx Combobox lazy loading images - java

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

Related

Make Buttons function like keyboard

I have an application that should work on a tablet, and there's a page where a recyclerview is present and in its row item there is 2 edit texts (with numberDecimal as input), the default keyboard shouldn't appear because it's covering a considerable large portion of screen.
I created buttons in the activity to act like the keyboard buttons however the problem is how to make the button from activity to communicate with the edit texts in the adapter.
how can i know that if I press (Button "1") for example that it should display 1 in the focused edittext in the adapter, and how if I pressed "<" or ">" button it should know the previous and next edit texts
please help
I'll provide example in Kotlin, I assume that is what you are using.
So you can simply override the key handling in the parent class like:
Make a method that you call one time from onCreate like:
fun keydown_onClick(){
try{
myActivity.setOnKeyListener { _, keyCode, event ->
if (keyCode == android.view.KeyEvent.KEYS_YOU_CARE_ABOUT) {
//handle it
if(activeViewHolder != null){
var displayText = activeViewHolder.yourEditText.text
keyPressedChar = (char)event.getUnicodeChar()+""
//if it's a special key you care about, then handle it in a when statement or if and pass to your adapter if needed to go to next row for example.
displayText += keyPressedChar
activeViewHolder.yourEditText.text = displayText
}
return#setOnKeyListener true//we've processed it
} else
return#setOnKeyListener false// pass on to be processed as normal
}
}catch (ex: Exception){
A35Log.e(mClassTag, "Error handling onkeydown listener: ${ex.message}")
}
}
Next up is how do you handle it. well you should keep track of the active row. You can do this by creating a callback in your activity that gets notified when a different row is selected or gains focus.
interface IFocusChangeListener {
fun onNewItemFocused(holder: MyApdaterViewHolder, item: DataItem, index: Int)
}
This will be passed into your adapter and used to fire back to your activity class.
//in activity
var activeViewHolder: MyAdapterViewHolder? = null
var activeIndex: Int? = null
var activeItem: DataItem? = null
fun onNewItemFocused(holder: MyAdapterViewHolder, item: DataItem, index: Int){
activeViewHolder = holder
activeIndex = index
activeItem = item
}
//now in your key down event you simply pass through the value to the editText in the activeViewHolder,
So last piece is in the adapter.
//on bind View
create View holder, the usual bloat.
Then when you have your editText you simply add.
var item = currentDataItem
var index = currentDataItemIndex
var viewHolder = currentViewHolder //from onBind
viewHolder.setOnFocusChangeListener(object: View.OnFocusChangeListener{
override fun onFocusChange(v: View?, hasFocus: Boolean) {
if(hasFocus){
mFocusListener?.onNewItemFocused(viewHolder, item, index)
}
}
})
//in adapter you may also need
fun onNextEditTextClicked(item: DataItem, index: Int){
//get next data Item by index + 1 if exist and get it's viewholder.editText and set focus to it, this will automatically trigger the focus event back to the activity
}
fun onPreviousEditTextClicked(item: DataItem, index: Int){
//get next data Item by index - 1 if exist and get it's viewholder.editText and set focus to it, this will automatically trigger the focus event back to the activity
}
Now you have the focused viewholder in your calling activity, you can catch the keys you care to catch, probably all of them. You can pass them in, you should be good to go.
NOTE*
For the record, if you are using modern practices, aka Data Binding, then you should be able to just update a bindable string in your model and it will show up on the screen without having to pass it around. You could also bind the focus to being selected and just update the selected boolean of the models. There are cleaner ways to do this if you use binding. But for now, just helping you without complicating it.
This solution may need tweaked, I just typed it here so could be off a bit, but should mostly get you there.
Happy Coding.
Intiliased the Adapter before clicking the buttons. because, required the instance of your Adapter to perform.
create a method add() in your Adapter
ArrayList<String> dataList = new ArrayList<>();
public void add(String element) {
dataList.add(element);
notifyItemInserted(dataList.size() - 1);//this will update the recyclerview with data inserted
}
from your button click call this method with your Adapter instance
yourAdapter.add(btn1.getText().toString());

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.

JavaFX 8, ListView with Checkboxes scrollpane issue

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.

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.

How to hide TableView column header in JavaFX 8?

I need to have an observable list of a type that will be displayed in a TableView with one single column, that when selected will display the rest of its information on the right. The TableView is wrapped in a TitledPane, which is wrapped in an Accordion. See image below:
As you can see in this scenario I don't want to show the Column Header.
I tried following the instruction here, which leads to here:
Pane header = (Pane) list.lookup("TableHeaderRow");
header.setMaxHeight(0);
header.setMinHeight(0);
header.setPrefHeight(0);
header.setVisible(false);
However, it appears to not be working for JavaFX 8. The lookup("TableHeaderRow") method returns null which makes me think that the "TableHeaderRow" selector no longer exist.
Is there an updated workaround for removing/hiding the table header in JavaFX 8?
I faced the problem of hiding column headers recently and could solve it using css.
I created a styleclass:
.noheader .column-header-background {
-fx-max-height: 0;
-fx-pref-height: 0;
-fx-min-height: 0;
}
and added it to the TableView:
tableView.getStyleClass().add("noheader");
Just in case someone needs an alternative approach. It also gives the flexibility of toggling column headers.
As observed in the comments, lookups do not work until after CSS has been applied to a node, which is typically on the first frame rendering that displays the node. Your suggested solution works fine as long as you execute the code you have posted after the table has been displayed.
For a better approach in this case, a single-column "table" without a header is just a ListView. The ListView has a cell rendering mechanism that is similar to that used for TableColumns (but is simpler as you don't have to worry about multiple columns). I would use a ListView in your scenario, instead of hacking the css to make the header disappear:
ListView<Album> albumList = new ListView<>();
albumList.setCellFactory((ListView<Album> lv) ->
new ListCell<Album>() {
#Override
public void updateItem(Album album, boolean empty) {
super.updateItem(album, empty);
if (empty) {
setText(null);
} else {
// use whatever data you need from the album
// object to get the correct displayed value:
setText(album.getTitle());
}
}
}
);
albumList.getSelectionModel().selectedItemProperty()
.addListener((ObservableValue<? extends Album> obs, Album oldAlbum, Album selectedAlbum) -> {
if (selectedAlbum != null) {
// do something with selectedAlbum
}
);
There's no need for CSS or style or skin manipulation. Simply make a subclass of TableView and override resize, like this
class XTableView extends TableView {
#Override
public void resize(double width, double height) {
super.resize(width, height);
Pane header = (Pane) lookup("TableHeaderRow");
header.setMinHeight(0);
header.setPrefHeight(0);
header.setMaxHeight(0);
header.setVisible(false);
}
}
This works fine as of June 2017 in Java 8.
Also, I would recommend using this nowadays.
tableView.skinProperty().addListener((a, b, newSkin) -> {
TableHeaderRow headerRow = ((TableViewSkinBase)
newSkin).getTableHeaderRow();
...
});
This can be executed during initialization, the other method as mention above, will return null, if run during initialization.
Combining the last two answers for a more generic solution without the need to override methods because getTableHeaderRow is no longer visible to be accessed. Tested with Java 11:
private void hideHeaders() {
table.skinProperty().addListener((a, b, newSkin) ->
{
Pane header = (Pane) table.lookup("TableHeaderRow");
header.setMinHeight(0);
header.setPrefHeight(0);
header.setMaxHeight(0);
header.setVisible(false);
});
}

Categories