Binding for userdata change of nodes inside a list? - java

I'm building a JavaFX application where I have a GridPane of 10 labels, and the user can assign objects to these labels per drag and drop. I used the userdata property of nodes, so when the user drops an object onto one label, it sets that object as userdata of this label.
Now I want to set a bind for the disableProperty of a button, so that the button only gets enabled when the user "filled" all of these labels with data. I've tried several things, finally I came to something like this:
FilteredList<Node> emptySlots = gridPane.getChildren().filtered(node -> node.getUserData() == null);
SimpleListProperty<Node> listProperty = new SimpleListProperty<>(emptySlots);
BooleanProperty hasEmptySlots = new SimpleBooleanProperty();
hasEmptySlots.bind(not(listProperty.emptyProperty()));
button.disableProperty().bind(hasEmptySlots);
But sadly the emptySlots list doesn't seem to update when the userdata property of the labels gets changed. I already tried to find ways to update this list, but I only find ways that involve ObservableLists with own classes and custom setup when the list is created like this (from here):
ObservableList<Model> masterData = FXCollections.observableArrayList<>(model ->
new Observable[]{model.statusProperty()});
But I don't have that option here.
Has anyone an idea how to archieve this?

Using an extractor could have worked using a mirror of the children list, but unfortunately the userData is not stored in a property (i.e. it's not Observable). To do what you want you'll have to use a different mechanism to store your user's objects.
An alternative to using the userData, in the same spirit of your current approach, is to use a Node's properties. These properties are stored in an ObservableMap<Object, Object> which means they can be observed for changes. To use this option you'll still want to use an extractor and mirror the children list.
class Foo {
private static final String USER_OBJECT_KEY = "USER_OBJECT";
private final GridPane gridPane = ...;
private final Button button = ...;
Foo() {
ObservableList<Node> mirror = FXCollections.observableArrayList(
n -> new Observable[]{n.getProperties()}
);
Bindings.bindContent(mirror, gridPane.getChildren());
FilteredList<Node> filtered = mirror.filtered(
n -> !n.getProperties().containsKey(USER_OBJECT_KEY)
);
button.disableProperty(Bindings.isEmpty(filtered));
}
}
The above uses Bindings.isEmpty(ObservableList) rather than wrapping the observable list in a ListProperty.
You may also want to consider moving this state out into a model. That way you avoid coupling the state of the application with JavaFX GUI objects (the model should know nothing about the view).

Related

TableViewer.getElementAt() returning a filtered element

I'm trying to automatically select the first item in a filtered table.
I'm essentially doing the following:
table = new TableViewer(...);
table.addFilter(filter);
table.setContentProvider(contentProvider);
table.setInput(input);
first = table.getElementAt(0);
table.setSelection(new StructuredSelection(first));
The surprising thing is that (depending on the filter) I can get an element that is filtered out from getElementAt(0). The result is that ultimately, no item will be selected.
I have tried calling table.refresh() before getting the element with the same results.
If I call getElementAt(0) at a later point, I do indeed get the correct first element (that is not filtered out).
How can I make getElementAt respect the filtering immediately?
In my experience, the most reliable way to select the first (visible) element is - for once only - to bypass JFace, rely on its internal data model, and use SWT API to select the first TableItem like this:
static Object selectFirstElement(TableViewer tableViewer) {
Object firstElement = null;
Table table = tableViewer.getTable();
if (table.getItemCount() > 0) {
firstElement = table.getItem(0).getData();
tableViewer.setSelection(new StructuredSelection(firstElement), true); // true == reveal
}
return firstElement;
}
I've been using this code successfully for several years with sorted, filtered, and virtual tables.
Well, I found what was wrong and it was my own fault. The filter I set is mutable so it can filter more or less strictly. The problem was that I activated the more strict filtering after I set the selection.
Thanks everyone for the help anyways.

ClassCastException in JavaFX

My code gets the following error.
Exception in thread "JavaFX Application Thread" java.lang.ClassCastException: javafx.scene.Group cannot be cast to javafx.scene.control.TreeCell
Source code
private TreeItem getClickedTreeItem(EventTarget eventTarget){
TreeItem clickedTreeItem = null;
if(eventTarget instanceof TreeCellSkin){
clickedTreeItem = (TreeItem) ((TreeCell) ((TreeCellSkin)eventTarget).getParent()).getTreeItem();
}else if(eventTarget instanceof LabeledText){
clickedTreeItem = (TreeItem) ((TreeCell) ((LabeledText)eventTarget).getParent().getParent()).getTreeItem();
}else if(eventTarget instanceof ImageView){
clickedTreeItem = (TreeItem) ((TreeCell) ((ImageView)eventTarget).getParent().getParent()).getTreeItem();
}
return clickedTreeItem;
}
The console says this line:
clickedTreeItem = (TreeItem) ((TreeCell) ((ImageView)eventTarget).getParent().getParent()).getTreeItem();
This is legacy code that worked with Java 6, but gives the above exception using Java8?
What could be causing the ClassCastException now, and how to fix it for Java8?
The current code is brittle as it relies on the internal structure of the TreeCell. Also, TreeCellSkin1 and LabeledText are both internal classes. Internal code is subject to change without notice and without regards to third party reliance on it. Since this worked in Java 6 but not Java 8 I can only assume that the ImageView's grandparent changed from being the TreeCell to being a Group between the two versions.
To fix this you could look into the implementation and see what you need to do so you reach the TreeCell again, but that wouldn't really solve the problem. The use of EventTarget says to me this code was implemented while not fully understanding how event handling works in JavaFX. From the apparent goal of this code you should be using the source of the event, not the target. In JavaFX, the source of the event is always the object for which the EventHandler currently handling said Event was added to2. In other words, if you added the EventHandler to the TreeCell then the source will be the TreeCell. Using the source, and assuming EventHandler is added to the TreeCell, you can simply do:
TreeItem<?> item = ((TreeCell<?>) event.getSource()).getTreeItem();
Of course, if you're adding the EventHandler to the TreeCell you likely needn't bother with the source as you'll have access to the TreeCell directly. For example:
TreeView<String> treeView = new TreeView<>();
treeView.setCellFactory(tv -> {
TreeCell<String> cell = new TreeCell<>(); // or some custom implementation
cell.setOnMouseClicked(event -> {
TreeItem<String> item = cell.getTreeItem();
// do something with item...
});
return cell;
});
1. TreeCellSkin became public API in JavaFX 9 along with many (all?) skin implementations. They are part of the javafx.scene.control.skin package.
2. There's more to it but that's beyond the scope of this answer.

JavaFX filtering categories

I'm writing a simple JavaFX7 application where I display data pulled out of database using a StackedBarChart. I also provide the user with the ability to filter the displayed data sets based on a specific property's value. The problem that I'm facing is that there seems to be some caching issues. Consider the following scenario
Initial load, display everything to the user - no filtering involved.
Say our categories are named 1,2,3,4 and 5, and are rendered in that order (consider them sorted)
The user now selects a filter value. This leads to only categories 1,2,4 and 5 being on the screen (again, in that order - this is the expected behavior)
The user now resets the filter to "do-not-filter".
The expected output of step 3 would be 1,2,3,4 and 5, in that order. However, it is 1,2,4,5,3. Notice the category that got filtered out is added back at the end of the array instead of where it should be.
Things I've tried so far:
Assigning a new ObservableList via Axis.setCategory. this doesn't work.
Same as above, but also force the category list to null before hand.
Sorting the category list. This doesn't work either.
I can't (yet) update to Java 8 - I also can't just leave this as a broken feature because this is expected to roll out to users before we upgrade to Java 8. So JavaFX 8's FilteredList is out of question (and a backport is very much annoying just from looking at the changes to ObservableList). I also don't want to entirely recreate the graph if I can avoid it.
At this point, I'm out of ideas. Any suggestions are welcome. Below is the function that populates the chart.
private void refreshContents() {
this.vaguesTotal.getData().clear();
this.vaguesDone.getData().clear();
this.vaguesPending.getData().clear();
this.xAxis.setCategories(null);
this.chartCategories = FXCollections.observableArrayList();
// Already sorted here
for (VagueLight vagueInfo : context.getVagues()) {
if (this.categoryFilter != null && this.categoryFilter != vagueInfo.getCategory())
continue;
int dossiersTraites = vagueInfo.getNbDossiersTraites();
int dossiersPending = vagueInfo.getNbDossiersATraiter();
String vagueIdentifier = Integer.toString(vagueInfo.getId());
this.vaguesTotal.getData().add(new Data<String, Number>(vagueIdentifier, 0, vagueInfo));
this.vaguesDone.getData().add(new Data<String, Number>(vagueIdentifier, dossiersTraites, vagueInfo));
this.vaguesPending.getData().add(new Data<String, Number>(vagueIdentifier, dossiersPending, vagueInfo));
this.chartCategories.add(vagueIdentifier);
}
// This just sets up event handlers and styles for the series
for (Series<String, Number> dataSeries : this.barChart.getData()) {
for (Data<String, Number> dataNode : dataSeries.getData()) {
initializeDataNode(dataNode, dataSeries);
}
}
// This is where the "bug" happens
this.xAxis.setCategories(this.chartCategories);
layout(true);
}

Chain of responsibility pattern - tight coupling with descendants

I came to problem how to put the actions into a chain with possibility to pass additional parameters during processing of a action.
Let's consider simple chain of a processes. Input is an object representing an image.
First image is resized, then deployed to ftp and saved to db.
Using a chain of responsibility pattern, calling could look like this:
ImageProcessor p = new ImageResizer(desiredSize);
p.setNext(new (new ImageDeployer(ftpServer, path)));
p.setNext(new (new ImageDbSaver(dbConnection, table)));
p.process(image);
p.close();
This is working perfectly for one image.
I would like to process images in a loop and set desiredSize and path there.
I cannot create and close connections every time, so code has to be spread:
ImageProcessor p = new ImageResizer();
p.setNext(new (new ImageDeployer(ftpServer)));
p.setNext(new (new ImageDbSaver(dbConnection, table)));
for(Image image : images) {
p.process(image, size, path);
}
p.close();
A problem of the solution is that the ImageProcessor shouldn't know about a size and a path. In case when is used only the ImageDbSaver parameters like size and path doesn't make sense.
What is a better way how to do it?
I guess the most robust solution in your case it so add some kind of processing context.
In simplest case, you can (1) use, for example Map<String, Object> for this, (2) pack it with arguments specific for various processors, (3) pass it into p.process(...) and (4) then extract, for example size in the processor that resizes the image.
This way you'll get flexibility to add new arguments without need to change signature of ImageProcessor and keeping implementers decoupled form one another.
The real world example of something similar would be request\session\servlet contexts in Java EE. You can put stuff into them on various lifecycle stages (for example security configuration options about what urls should require auth) and then fetch this stuff where needed (for example in Filter to block\allow access to resource based on auth requirements).
UPDATE
Updating the answer with code example to demonstrate the idea.
So, somewhere in your code you have place where you build your processor chain:
ImageProcessor p = new ImageResizer(desiredSize);
p.setNext(new (new ImageDeployer(ftpServer, path)));
p.setNext(new (new ImageDbSaver(dbConnection, table)));
In (possibly) other place you create and configure your processing context (it is not really required to configure all processors in one place):
Map<String, Object> context = new HashMap<String, Object>();
context.put("image_width", new Integer(640));
In another place in your code you do your processing, passing context to it (I reuse context for all processings but you might use different contexts for every image):
for(Image image : images) {
p.process(image, context);
}
p.close();
Now, somewhere in your ImageResizer:
#Override
void process(Image image, Map<String, Object> context) {
// ...
Integer imageWidth = (Integer) context.get("image_width");
// ...
}
So, context is a common place that helps to deliver data to specific processors, abstracting the details about this data and thus decoupling specific processors from one another.
You could simply store all processors into variables and then chain them.
ImageResizer resizer = new ImageResizer();
ImageDeployer deployer = new ImageDeployer(ftpServer);
ImageDbSaver saver = new ImageDbSaver(dbConnection, table);
resizer.setNext(deployer).setNext(saver);
for(Image image : images) {
deployer.setPath(somePath);
resizer.setDesiredSize(desiredSize);
resizer.process(image);
}
resizer.close();
A simple way of solving it would be to construct the invariants outside your loop. For example:
ImageDeployer deployer = new ImageDeployer(ftpServer);
ImageDbSaver dbSaver = new ImageDbSaver(dbConnection, );
for(Image image : images) {
ImageProcessor p = new ImageResizer(desiredSize);
p.setNext(deployer);
p.setNext(dbSaver);
p.process(image);
}
dbSaver.close();
deployer.close();
If ImageResizer needs to be closed you'll need some way of detaching things in the loop.
How would you code this if performance was not a problem? I think constructing a custom processor chain for every image would be a good solution then because you seem to have different values for every image.
Now if it turns out that this is too slow (which certainly can happen), why not pass a DatabaseConnection and FtpConnection object (which are initialized outside the loop) to the processors that need these?

Create a JTree from a list of dot-delimited Strings

I am creating an editor application, and I am having a problem with my menus. In the object menu, I want to display several objects types using a JTree. These object types are dynamically registered by plugins and follow this style:
trigger.button
trigger.lever
out.door.fallgate
trigger.plate
out.door.door
...
This list of names is unsorted and I want to build a TreeNode structure for a JTree like this:
trigger
button
lever
plate
out
door
fallgate
door
Additionally, if the user selects a leaf node, I need to recreate the object name (e.g. trigger.button) from the TreePath. Could someone please advise how this can be done.
In psuedocode, this is what you need to do...
public TreeNode buildTree(){
String[] names = new String[]; // fill this with the names of your plugins
TreeNode tree;
// for each plugin name...
for (int i=0;i<names.length;i++){
String currentName = names[i];
String[] splitName = currentName.split(".");
// loop over the split name and see if the nodes exist in the tree. If not, create them
TreeNode parent = tree;
for (int n=0;n<splitName.length;n++){
if (parent.hasChild(splitName[n])){
// the parent node exists, so it doesn't need to be created. Store the node as 'parent' to use in the next loop run
parent = parent.getChild(splitName[n]);
}
else {
// the node doesn't exist, so create it. Then set it as 'parent' for use by the next loop run
TreeNode child = new TreeNode(splitName[n]);
parent.addChild(child);
parent = child;
}
}
return tree;
}
This is only psuedocode - you'll need to do the work of implementing the TreeNode methods properly, etc. Give it a try yourself - if you have any more problems, create a question and show us that you've attempted to do it yourself, then we will be more willing to help you solve the minor problems.
see Oracle JTree tutorial
part Dynamically Changing a Tree, check the code example
similair idea

Categories

Resources