Validator for ListView Selection Model? - java

I'm trying to make it so a selection is required in order to proceed and I am wondering if it is possible to achieve this using the ControlsFX Validator and ValidationSupport.
public static void test(ValidationSupport support, ListView listView) {
Validator<ObservableListWrapper> validator = (control, value) -> {
boolean condition = listView.getSelectionModel().getSelectedItem() == null;
return ValidationResult.fromMessageIf(control, "Please select an item", Severity.ERROR, condition);
};
support.registerValidator(listView, true, validator);
}
This does not work. Selecting an item on the list does not seem to effect this in any way. I am thinking this is because I'm using the listView parameter instead of the value one, but I do not know how to get the value one to correspond with the list view's selection model.

ValidationSupport uses ValueExtractor to extract an observable value which when changes a revalidation will be made. ValueExtractor uses ListView#itemsProperty as default for all ListViews, which means that ValidationSupport will revalidate only when itemsProperty is changed.
In order to change this for all instances of ListView, you can set an observable value extractor using the static method addObservableValueExtractor like this:
ValueExtractor.addObservableValueExtractor(c -> c instanceof ListView,
c -> ((ListView) c).getSelectionModel().selectedItemProperty());
If you want to change it for a specific instance of ListView you can try doing this:
ValueExtractor.addObservableValueExtractor(c -> c == listView,
c -> ((ListView) c).getSelectionModel().selectedItemProperty());
An example using ListView<File>:
ListView<File> listView = new ListView<>();
ValidationSupport support = new ValidationSupport();
ValueExtractor.addObservableValueExtractor(c -> c == listView,
c -> ((ListView<File>) c).getSelectionModel().selectedItemProperty());
Validator<File> validator = new Validator<>() {
#Override
public ValidationResult apply(Control control, File file) {
boolean condition = file == null;
return ValidationResult.fromMessageIf(control, "Please select a file", Severity.ERROR, condition);
}
};
support.registerValidator(listView, true, validator);

Related

How to select first item without long press using RecyclerView's SelectionTracker

I'm building an application that will allow users to pick from a RecyclerView list, highlighting their choice. The problem is that in order to highlight an item for the first time, a long press is needed. (Afterwards, a short click is enough to do the selection.)
I haven't found anything in the documentation to indicate why this happens.
I'm using SelectionTracker
Specifically following this guide
Here's the code :
https://github.com/marcosholgado/multiselection
Expectations:
I expect the item on the RecyclerView to be selected every time someone short clicks on it.
Reality:
In order to select an item for the first time, the user needs to long press it.
Any Ideas?
Just override SelectionHotspot to return true. That's all you need
fun getItemDetails(): ItemDetailsLookup.ItemDetails<Long> =
object : ItemDetailsLookup.ItemDetails<Long>() {
override fun getPosition(): Int = adapterPosition
override fun getSelectionKey(): Long? = itemId
override fun inSelectionHotspot(e: MotionEvent): Boolean { return true }
}
While I couldn't think of a solution that doesn't involve reimplementing both a MotionInputHandler and a SelectionTracker.Builder (as mentioned in the guide), there is a neat trick to achieve the behaviour you want.
We know that the TouchInputHandler selects items on single click as long as the SelectionTracker is not empty. That means if we have some special key saved in the SelectoinTracker that's not associated with a real list item we practically 'activate' the on single click selection mode this way. However we also have to make sure that our KeyProvider doesn't provide that same special key to keep our data consistent.
So assuming you have picked a special key, say ghostKey, activating and deactivating the selection mode is now a matter of calling mSelectionTracker.select(ghostkey) or mSelectionTracker.clearSelection(). You can then execute these calls however you like, be that having a button that activates and deactivates the selection mode or simply calling that during the hosting view creation process i.e onCreate, onCreateView etc..
If you are using Kotlin you can also define some Extensions that wrap these calls for you, so you'd be able to do things like mSelectionTracker.enable() or mSelectionTracker.disable()
Use this line
selectionTracker.select(item.getSelectionKey());
On this override method
.withOnItemActivatedListener(new OnItemActivatedListener() {
#Override
public boolean onItemActivated(#NonNull ItemDetailsLookup.ItemDetails item, #NonNull MotionEvent e) {
selectionTracker.select(item.getSelectionKey());
return true;
}
})
I solved like this. Keep coding..
In your ViewHolder, set onClickListener and in that check whether you have any selected items using SelectionTracker.hasSelection() and if it returns false, just select that item using SelectionTracker.select(getItemDetails().getSelectionKey())
like this:
itemView.setOnClickListener(v -> {
if (!tracker.hasSelection())
tracker.select(getItemDetails().getSelectionKey());
});
This is what I did:
binding.root.setOnClickListener {
tracker?.let {
if(!it.hasSelection()) it.select(binding.currItem?.uuid!!)
binding.setVariable(BR.selected, it.isSelected(binding.currItem?.uuid))
}
binding.executePendingBindings()
}
I implemented a click listener on the view with the check if the selection tracker has any selected entity or not. If it does not have any selection then I explicitly added the selection and the selection tracker gets enabled automatically.
I think this must the solution you were looking for.
The RecyclerView-selection framework is not designed to handle either of the following scenarios!!
A.) An 'enable multiselect' button that toggles the multi-selection on-and-off.
B.) RecyclerView Adapters that utilize more than one row ViewType that must not be selectable.
*If your scope requires A.) or B.) (above), do NOT use the RecyclerView-selection framework.
Instead, remove it and do the following:
1.) Add a Post-Api data field to your list item model:
var isMultiSelected: Boolean = false // User has highlighted this inbox's row for multi-selection
2.) Create a Listener to communicate from the Adapter to the Fragment/Activity:
class MyFragment : MyFragmentListener {
interface MyFragmentListener {
fun myDataModelOnClick(myDataModel: MyDataModel)
fun engageMultiSelect(selectedDataModelCount: Int)
fun disengageMultiSelect()
}
override fun engageMultiSelect(selectedMyModelCount: Int) {
// Update the fragment view for multi-select engaged conditions
selectedCount_textView.text = selectedMyModelCount.toString()
}
override fun disengageMultiSelect() {
// Update the fragment view for multi-select engaged conditions
}
3.) Update your Adapter.onBindViewHolder() to use the new field
holder.enableMultiSelectionButton.setOnClickListener {
myModel.isMultiSelected = true
myModel.engageMultiSelect(getMultiSelectionCount()) // Enable multi-selection
notifyItemChanged(position)
}
holder.rowConstraintLayout.setOnClickListener {
if (getMultiSelectionCount() > 0) { // Is multi-selection engaged?
myModel.isMultiSelected = !myModel.isMultiSelected
if (getMultiSelectionCount() > 0) { // New Item Count may still be larger than 0
myModelListener.engageMultiSelect(getMultiSelectionCount())
} else {
myModelListener.disengageMultiSelect()
}
notifyItemChanged(position)
} else {
// Normal 'tap' when multi-selection is not engaged
}
}
holder.rowConstraintLayout.setOnLongClickListener {
myModel.isMultiSelected = !myModel.isMultiSelected
if (getMultiSelectionCount() > 0) { // Is multi-selection engaged?
myModelListener.engageMultiSelect(getMultiSelectionCount())
} else {
myModelListener.disengageMultiSelect()
}
notifyItemChanged(position)
true
}
if (myModel.isMultiSelected) {
holder.rowConstraintLayout.setBackgroundColor(R.id.coolcolor)
} else {
holder.rowConstraintLayout.setBackgroundColor(R.id.white)
}

How to delete a CheckBox from a TableView in JavaFX?

I'm writing a seating chart program using JavaFX. I have a table that keeps a list of students together that holds their name, grade, and whether they are present or absent (using a checkbox). I have a delete button that allows me to delete the students from the list. This works fine, however, whenever I delete the student object, the checkbox does not go along with it. I'm not sure what I would need to add to get that to work. Here is a snippet of the delete code. There are also two images below that show my problem. This is my first post so please let me know if I missed something. Please help! Thanks!
ObservableList<Student> items, sel;
items = currentTable.getItems();
sel = currentTable.getSelectionModel().getSelectedItems();
Student s = new Student("", "", 0, "");
for (Student p : sel) {
items.remove(p);
s = p;
}
Before Delete
After Delete
This has nothing to do with the delete or remove method. It has to do with what you did in TableColumn.setCellFactory().
To get the checkbox you shown, you should have used (in general) one of the two methods:
Overriding updateItem() in TableCell while setting Cell Factory
There is this empty parameter in updateItem() which indicates whether the row is empty or not. You need to use that to determine when not to show the checkbox.
column.setCellFactory(col -> {
return new TableCell<Foo, Boolean>() {
final CheckBox checkBox = new CheckBox();
#Override
public void updateItem(final Boolean selected, final boolean empty) {
super.updateItem(selected, empty);
if (!this.isEmpty()) {
setGraphic(checkBox);
setText("");
}
else {
setGraphic(null); // Remove checkbox if row is empty
setText("");
}
}
}
}
Using CheckBoxTableCell
JavaFX API has this convenient class CheckBoxTableCell that would do all these for you. Most people find this class hard to use because there are 2 things that you need to ensure to use it correctly:
The TableView that the column belongs to must be editable.
The TableColumn itself must be editable.
Example:
tableView.setEditable(true);
tableColumnSelected.setCellFactory(CheckBoxTableCell.forTableColumn(tableColumnSelected));
tableColumnSelected.setEditable(true);
As for whether which entry you want to be removed with the delete button, you just need to remove the correct items from the TableView.

JavaFX8: CheckboxListCell in ListView unchecks cells when scrolling [duplicate]

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.

Work with Value from PropertyValueFactory in TableView - JavaFx

In my JavaFx project I have a scene with a few views. For the footer of the window I have a class extending from TableView:
public class FooterView extends TableView<Area> implements ViewTemplate {...}
This displays a Table with some data from a .csv-file.
When it comes to assigning the value of the specific presentationmodel property to the cells I do it like that:
TableColumn<Area,Double> powerColumn = new TableColumn<>("Power Overview");
powerColumn.setCellValueFactory(new PropertyValueFactory<>("powerPerArea"));
this.setItems(filePM.getAreas()); //filePm is my filehandler
this.getColumns().addAll(powerColumn, other_columns);
getAreas() looks like this:
public List<Area> readCantonsFromFile() {
try (Stream<String> stream = getStreamOfLines(AREA_FILE_NAME)) {
return stream.skip(1)
.map(l -> new Area(l.split(DELIMITER, 12)))
.collect(Collectors.toList());
}
}
In the constructor of Area i set the properties. One of the properties is the mentioned powerPerArea
private final DoubleProperty powerPerArea = new SimpleDoubleProperty();
...
public void setPowerPerCanton(double powerPerCanton) {
this.powerPerCanton.set(powerPerCanton);
}
My question: Is there a way to change the value in the FooterView before the value is displayed? I tried something like this:
powerColumn.setCellValueFactory(Math.round(new PropertyValueFactory<>("powerPerArea")));
But it seems that I mixed up DoubleProperty, Double and ObservableDouble. Can I even modify the value in here?
The problem: I can not round the value in the setter because I add a double value in a loop through this function:
public void addPowerPerArea(double power){
double sum = getPowerPerCanton() + power;
setPowerPerCanton(sum);
}
And rounding the value in here would give me a wrong result. (rounding not precise enough). I need to do it in the end when all sums are added
You should use the cellValueFactory to determine which data are displayed in the cells: in this case the data returned by your PropertyValueFactory is the actual double value returned from powerPerAreaProperty().get(), which is exactly what you want.
If you want to control how the data are displayed, you should use a cellFactory. So to display the data in a particular format, including limiting the number of decimal places, you can do:
powerColumn.setCellFactory(tc -> new TableCell<Area, Double>() {
#Override
protected void updateItem(Double power, boolean empty) {
super.updateItem(power, empty);
if (empty) {
setText(null);
} else {
setText(String.format("%.0f", power.doubleValue()));
}
}
});
The point here is that you should not modify the data based on how you want to display it; the purpose of having both cellValueFactory and cellFactory is to separate the display of the data from the actual data itself.
An alternative to returning custom cells from a cellFactory would be to use a custom cellValueFactory to return the property formatted as string:
TableColumn<Area, String> powerColumn = new TableColumn<>("Power Overview");
powerColumn.setCellValueFactory(cd -> cd.getValue().powerPerAreaProperty().asString(""%.0f""));
I see two ways to do this. You could:
use the setCellFactory method and in the updateItem method you format it. Should look something like this, haven't tested
powerColumn.setCellFactory(column -> {
return new TableCell<Area, Double>() {
#Override
protected void updateItem(Double item, boolean empty) {
super.updateItem(Math.round(item), empty);
}
};
});
OR: You could make another property of your Area class that is bound to the existing powerOfArea property but returns the value rounded. I am sure that is somehow possible, you could just override some functions of a readOnlyDoubleProperty but there should be a better way. Maybe via DoubleBindings.

Adding Context Menu to TableView designed using ObservableMap in JAVAFX

I have created a table using ObservableMap instead of ObservableList. You can see the code (not written by me) here. Now I need to add context menu to every row. So I wrote the code as below:
public MapTableView<String, LineItem> initialize(MapTableView<String, LineItem> tableView) {
tableView.setRowFactory((TableView<Entry<String, LineItem>> tableView1) -> {
final TableRow<Entry<String, LineItem>> row = new TableRow<>();
final ContextMenu contextMenu = new ContextMenu();
final MenuItem cancelMenuItem = new MenuItem("Cancel");
cancelMenuItem.setOnAction((ActionEvent event) -> {
LineItem item = tableView1.getSelectionModel().getSelectedItem().getValue();
System.out.println(item.getLineNo()); // gives me null
});
});
contextMenu.getItems().add(cancelMenuItem);
// Set context menu on row, but use a binding to make it only show for non-empty rows:
row.contextMenuProperty().bind(Bindings.when(row.emptyProperty()).then((ContextMenu) null)
.otherwise(contextMenu)
); tableView1.setContextMenu(contextMenu);
return row;
});
return tableView;
}
Actually it is retrieving the order from the table but when I am accessing the variables of the object it is giving me "null". I couldn't find out what wrong I am doing. Please help me with this. More-over I am initializing the table with:
tableView.setEditable(false);
tableView.setColumnResizePolicy(TableView.UNCONSTRAINED_RESIZE_POLICY);
Should I have to use anything like:
tableView.getSelectionModel().setCellSelectionEnabled(true);
Everything is working fine, but when I am trying to access the variables of the "item" I am getting the "Null Pointer Exception" as the values are null, but the object is not null. Thanks in advance.
As #James_D said, using
LineItem item = row.getItem().getValue() solved the issue.

Categories

Resources