This is a little bit novel – using FXExperience for a post that isn’t the weekly links! 🙂 I wish I could post here more often, but time constraints pretty much stop that in its tracks (even the weekly links take longer than I would like). Anyway, I often get asked about node picking in JavaFX, and if the Node.impl_pickNode(...)
API will ever lose the ‘impl’ and become proper API.
The answer is: I don’t know – picking API is not ‘mine’ to worry about. Frankly, I’m barely qualified to discuss picking in any depth, as it is not something I use. Despite this, I will give my unqualified summary for those of you who are unfamiliar with what picking is: it is basically about getting the best matching (i.e. top-most) Node under a given x, y coordinate.
A few months back I was working in the Scene Builder code base to remove dependencies on these impl methods (and to generally clean it up to work on JDK 9 given all the changes due to project Jigsaw (i.e. modularity) and new APIs in JavaFX 9). One thing I had to replace was the use of Node.impl_pickNode(...)
. I ended up writing the code below, and given that I’m asked about this functionality a lot, I thought I would post it here in case others found it useful.
I should be very clear that this is not a performant implementation, and it is built with my naivety regarding picking (so I am happy to be sent suggestions for improving it). Nevertheless, I hope it helps.
public static Node pick(Node node, double sceneX, double sceneY) { Point2D p = node.sceneToLocal(sceneX, sceneY, true /* rootScene */); // check if the given node has the point inside it, or else we drop out if (!node.contains(p)) return null; // at this point we know that _at least_ the given node is a valid // answer to the given point, so we will return that if we don't find // a better child option if (node instanceof Parent) { // we iterate through all children in reverse order, and stop when we find a match. // We do this as we know the elements at the end of the list have a higher // z-order, and are therefore the better match, compared to children that // might also intersect (but that would be underneath the element). Node bestMatchingChild = null; List<Node> children = ((Parent)node).getChildrenUnmodifiable(); for (int i = children.size() - 1; i >= 0; i--) { Node child = children.get(i); p = child.sceneToLocal(sceneX, sceneY, true /* rootScene */); if (child.isVisible() && !child.isMouseTransparent() && child.contains(p)) { bestMatchingChild = child; break; } } if (bestMatchingChild != null) { return pick(bestMatchingChild, sceneX, sceneY); } } return node; }
I haven’t see the code in depth but it appears the code above could it be more efficient if you exclude non visible children? Although I still want to pick nodes that are of opacity of zero (not seen != not visible)
Maybe like below:
If (child.isVisible() && child.contains(p)) …
I guess you could also go in reverse order of the unmodifiedChildren list because it would follow z-order of things on top.
List a = node.getUnmodifiedChildren();
ListIterator li = a.listIterator(a.size());
// Iterate in reverse.
while(li.hasPrevious()) {
Child child = li.previous();
… Zorder
Found then break;
}
I’m sure you probably know, I just wanted to offer suggestions because picking needs/should be super fast .
Both good points. I have updated the code snippet to include these, although I have done so only on wordpress – I haven’t yet tried them out in my IDE for correctness!
Does picking care whether DepthTest and the depth buffer are turned on?
In the case of the code above….no.
Omitting disabled and mouseTransparent nodes is also often desired
I can agree on mouseTransparent – but not convinced about ignoring disabled nodes. I’ll update the code to check mouseTransparent now.
The check against disabled was something I saw in Node.impl_pickNode().
So now I’m wondering why disabled controls should be picked?
With our gesture recognizer we want the target selection to conform to the JavaFX hit-testing strategy.
I was thinking, for example, the case of a disabled button. It seems to me that if there were a rectangle behind it, and I clicked on the button, that I would still expect to be told that I have picked the button. Just because it is disabled doesn’t mean that it isn’t part of the scenegraph. Does this seem incorrect to you?
I agree! But it seems that JavaFX does not work this way: “A disabled Node does not receive mouse or key events.” https://docs.oracle.com/javafx/2/api/javafx/scene/Node.html#disabledProperty%28%29
Jonathan,
Not exactly why is the check for !isMouseTransparent() there?
I thought some touch screen devices replace touch events as mouse events and make the mouse pointer transparent. I don’t know if it could effect picking when using touch events.
Carl
Hi carl,
I think it depends on your use case.
For our gesture recognition system we are using Node.impl_pickNode() to determine the nodes the users are trying to select. That is, on which node a touch event has occurred and should therefore be notified about this touch event.
In our case, you don’t want to sent touch/mouse events to nodes mark isMouseTransparent
David
David,
I’m not targeting a particular device, but I thought some might translate touch events into mouse events and code will hide the mouse pointer. We just should consider interesting scenarios before filtering. I am only guessing btw.
I have an older BeagleBone XM touch and I’ve not tested it for a very long time.
Not sure but I believe the Official Raspberry Pi Touch 7 inch may also fire mouse events also.
https://www.raspberrypi.org/blog/the-eagerly-awaited-raspberry-pi-display/
Excerpt:
“The driver for this touchscreen outputs both standard mouse events and full multi-touch events, and therefore can work with X as a mouse (although not brilliantly – X was never designed to work with a touchscreen!).”
I don’t know the best way to handle this situation I’m only suggesting we understand what/how it means to pick a node.
Thanks!
Carl
What do you mean by “the code will hide the mouse pointer” ? I was thinking that the code above will only filter Nodes marked as transparent to MouseEvents.
btw, from our experience sending only touch events is not enough because the default behaviour of some JavaFX controls rely on key and
mouse inputs.
David
David,
Not sure, but is hiding a mouse pointer the same as isMouseTransparent()?
I don’t own the new computers with all-in-one touch monitors with Windows 10 or MS surface etc., but usually I’ve seen them in the stores, where when you touch the screen the mouse pointer is shown just under your finger which is annoying.
I thought when writing code for instance a Kiosk using Windows 10 or Raspbian, you will need to hide the mouse pointer. I am assuming if you run a JavaFX application full screen you will want code to “hide” (not show) the mouse pointer if you are using the touch. But if the OS turns the touch events or also fires mouse events.
****Keep in mind I don’t own any of these devices and haven’t tested any of these scenarios.
If you hide the mouse pointer and in the code above the node will not get picked I assume. Is that what you want?
I’m just saying we should think of all scenarios before excluding nodes.
Maybe I’m over thinking things and not a big deal. Hopefully, I make sense ;-).
Carl
Carl,
interesting scenario.
But Node.setMouseTransparent(false|true) does not hide the cursor.
If I understand it right JavaFX only uses the isMouseTransparent property in connection with the Node.impl_pickNode()
So if you hide the cursor (maybe using setCursor(NONE)) the code above should still work.
David
David,
My mistake! It helps if I actually read the documentation. I misunderstood what the isMouseTransparent actually is used for.
My first few messages were submitted via my iPhone.
Mouse transparency is about ignoring mouse events. The javadoc:
“If true, this node (together with all its children) is completely transparent to mouse events. When choosing target for mouse event, nodes with mouseTransparent set to true and their subtrees won’t be taken into account.”
Thanks man!
Carl
Why don’t you check the visibility of the node in the beginning of the pick method?
It also seems that at each iteration you convert coordinates and call contains() again. It would have been a clearer implementation to invoke pick() for each child and compare result with null.
you must check the visibility for each children. Therefore it must be done inside the for-loop
Hey,
Do you know if any “pick” method is gonna be promoted to JavaFX API in java 9 ?
There is no pick method being added in JDK 9 for JavaFX.