JavaFX - Notify when user drags mouse from one Node to another? - java

I have the following basic GUI for demonstration:
I'm trying to achieve the following functionality but I've exhausted all avenues that I've attempted.
User can left click on any of the ImageView's and it will create an
arrow that follows the user's cursor around until the user let's go of
the mouse button. (arrow start x,y is where he clicked and end x,y is
where his mouse currently is) If the user clicked on the Red
ImageView and dragged it over the Blue ImageView and then let go,
the program would print User just clicked from R to B
If the user clicked on the Red ImageView and let go of the mouse but
was not over a different ImageView, the program would print User
just clicked from R but did not target a different ImageView.
Under all circumstances, the arrow will appear when the user clicks on
the ImageView and will disappear the second he lets go of the mouse.
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Line;
import javafx.stage.Stage;
import java.util.HashMap;
public class Test extends Application
{
public static int HEIGHT = 500, WIDTH = 600;
#Override
public void start(Stage primaryStage) throws Exception
{
ImageView blue = new ImageView(new Image("blue.png")),
red = new ImageView(new Image("red.png")),
dark = new ImageView(new Image("dark.png"));
// Final array as to bypass the `final` requirement of event handler inner classes.
final ImageView[] hoveredOver = new ImageView[1];
final Line[] linePtr = new Line[1];
linePtr[0] = new Line();
linePtr[0].setStrokeWidth(10);
HashMap<ImageView, Character> lookup = new HashMap<ImageView, Character>(3)
{{
put(blue, 'B');
put(red, 'R');
put(dark, 'D');
}};
for (ImageView i : new ImageView[] { blue, red, dark })
{
i.setFitWidth(150);
i.setFitHeight(150);
// Set the anchor points of the click and display the arrow.
i.setOnMousePressed(e -> {
linePtr[0].setStartX(e.getX());
linePtr[0].setStartY(e.getY());
linePtr[0].setVisible(true);
});
// Move the arrow as the mouse moves.
i.setOnMouseDragged(e -> {
linePtr[0].setEndX(e.getX());
linePtr[0].setEndY(e.getY());
});
i.setOnMouseReleased(e -> {
// Not null means that the user WAS actually just now hovering over an imageview.
if (hoveredOver[0] != null)
System.out.printf("The user clicked from %c to %c!\n", lookup.get(i), lookup.get(hoveredOver[0]));
// Null means the user is not over an ImageView.
else
System.out.printf("The user initially clicked %c but did not drag to another Imageview.\n", lookup.get(i));
linePtr[0].setVisible(false);
});
// If the user enters ANY of the ImageViews,
// Set a variable so that the drag release listener
// can know about it!
i.setOnMouseDragOver(e -> hoveredOver[0] = i);
i.setOnMouseDragExited(e -> hoveredOver[0] = null);
}
blue.setX(400);
blue.setY(250);
red.setY(300);
red.setX(50);
/*
In this example I'm using a Pane but in my real program
I might be using a VBOX HBOX etc where I cannot freely move stuff around as I'd like.
This makes things extremely difficult and without using a 'Pane'
I don't know how this can even be done. Suggestions?
*/
Pane pneRoot = new Pane(blue, red, dark, linePtr[0]);
Scene scene = new Scene(pneRoot, WIDTH, HEIGHT);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args)
{
launch(args);
}
}
This was my best attempt and it's not even close. It moves a line (not an arrow, and ideally I want my arrow to curve as it moves much like this example image from a popular video game) but does not suit my needs. It cannot detect however when I let go while 'dragging over' an ImageView.
Is there a better way to do this? I feel like I can't simply the code I have down any further but there MUST be another way.

Java is an object-oriented language. The basic idea is that you create classes to represent the data you are modeling and then create objects from those classes. If you are tying things together with arbitrary maps to look things up, and arrays kicking around for no apparent reason, you are starting in the wrong place.
JavaFX has a system of observable properties. These wrap objects in a mutable way and can be observed so you can respond to changes.
Make sure you read and understand the documentation on MouseEvents and MouseDragEvents. There are three different modes for handling dragging. For events (mouse drag events) to be sent to nodes other than the one on which the drag was initiated during a mouse drag, you need to be in full "press-drag-release gesture" mode. You can activate this mode by calling startFullDrag() on the node when responding to a dragDetected event.
I would start with something like
public class NamedDragAwareImageView {
private final ObjectProperty<NamedDragAwareImageView> source ;
private final ObjectProperty<NamedDragAwareImageView> destination ;
private final String name ;
private final ImageView imageView ;
public NamedDragAwareImageView(ObjectProperty<NamedDragAwareImageView> source,
ObjectProperty<NamedDragAwareImageView> destination,
String name, String resource) {
this.source = source ;
this.destination = destination ;
this.name = name ;
this.imageView = new ImageView(new Image(resource));
imageView.setOnDragDetected(e -> {
source.set(this);
destination.set(null);
imageView.startFullDrag();
});
imageView.setOnMouseDragReleased(e -> {
if (source.get() != null && source.get() != this) {
destination.set(this);
}
});
// other image view config...
}
public ImageView getView() {
return imageView ;
}
public String getName() {
return name ;
}
}
Then you can do things like:
// observable properties to represent start and end nodes for drag:
ObjectProperty<NamedDragAwareImageView> source = new SimpleObjectProperty<>();
ObjectProperty<NamedDragAwareImageView> destination = new SimpleObjectProperty<>();
Pane root = new Pane();
// create your named image views, referencing the source and destination
// and add their image views to root, e.g.
NamedDragAwareImageView red = new NamedDragAwareImageView(source, destination, "Red", "red.png");
root.getChildren().add(red.getView());
// recommend using SVG paths (i.e. javafx.scene.shape.Path) for the arrow
// easy to draw programmatically, easy to manipulate elements etc:
Path arrowHead = new Path();
MoveTo arrowHeadStart = new MoveTo();
arrowHead.getElements().add(arrowHeadStart);
arrowHead.getElements().addAll(/* draw an arrow head with relative path elements... */);
arrowHead.setVisible(false);
// avoid arrowHead interfering with dragging:
arrowHead.setMouseTransparent(true);
// this will contain a MoveTo and a bunch of LineTo to follow the mouse:
Path arrowLine = new Path();
arrowLine.setMouseTransparent(true);
root.getChildren().addAll(arrowHead, arrowLine);
// change listener for source. source is set when drag starts:
source.addListener((obs, oldSource, newSource) -> {
if (newSource == null) return ;
arrowHeadStart.setX(/* x coord based on newSource */);
arrowHeadStart.setY(/* similarly */);
arrowHead.setVisible(true);
});
// change listener for destination. destination is only set
// when drag complete:
destination.addListener((obs, oldDestination, newDestination) -> {
if (newDestination != null) {
System.out.println("User dragged from "+source.get().getName()+
" to "+destination.get().getName());
}
});
root.setOnMouseDragOver(e -> {
if (source.get()==null && destination.get()!=null) {
// update arrowStart position
// add line element to arrowLine
}
});
root.setOnMouseReleased(e -> {
// clear arrow:
arrowHead.setVisible(false);
arrowLine.getElements().clear();
});

Related

Mouse release event doesn't happen on Ubuntu when target Node changes scene?

This question deals with mouse behavior across operating systems; specifically, my code works on Windows and Mac OS X, but not on Ubuntu.
Ultimately what I am trying to do is make a special Pane subclass ("ConvertiblePane") that exists within a parent Pane on a main stage/scene, but is magically transferred into its own temporary stage/scene when dragged, thus becoming independent and able to be placed anywhere on the screen. When the user releases the mouse button, the ConvertiblePane should drop back onto its original parent Pane and lose the temporary stage. (In my full program, the original parent Stage resizes/repositions itself to accommodate the ConvertiblePane wherever it is dropped.)
This brings me to my issue. When I press the mouse on the ConvertiblePane, it triggers a MousePress within the main scene as expected, at which point the ConvertiblePane is moved to the temporary stage. As I drag the mouse, it triggers MouseDrag within the temporary scene and moves the temp stage. Ok, great.
When I release the mouse button, however, I experience different behavior on different operating systems. On Windows (7) and Mac OS X (10.12.6), a MouseRelease occurs in the temp scene, sending the pane back to its original parent in the main stage as expected. On Ubuntu, however, no MouseRelease seems to be generated in either the main scene or the temp scene.
Here's the relevant code as an MCV example:
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.geometry.Point2D;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.CornerRadii;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
import javafx.stage.StageStyle;
public class ConvertibleTest extends Application {
#Override
public void start(Stage primaryStage) {
// Set up the main stage and scene with a
// Pane as root and a ConvertiblePane child:
primaryStage.initStyle(StageStyle.TRANSPARENT);
Pane root = new Pane();
ConvertiblePane conv = new ConvertiblePane();
root.getChildren().add(conv);
Scene scene = new Scene(root, 400, 400, Color.PINK);
primaryStage.setTitle("Convertible Test");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
class ConvertiblePane extends Pane
{
private final Group TEMP_ROOT = new Group();
private final Stage TEMP_STAGE = new Stage(StageStyle.TRANSPARENT);
private final Scene TEMP_SCENE = new Scene(TEMP_ROOT);
private Pane originalParent = null;
private double deltaX = 0.0;
private double deltaY = 0.0;
private String name = null;
public void onMousePress(MouseEvent event)
{
// Save deltaX/Y for later:
Point2D delta = this.sceneToLocal(event.getX(), event.getY());
deltaX = delta.getX();
deltaY = delta.getY();
if (!isIndependent())
{
makeIndependent();
}
}
public void onMouseDrag(MouseEvent event)
{
// Keep the TEMP_STAGE relative to the original click point:
TEMP_STAGE.setX(event.getScreenX()-deltaX);
TEMP_STAGE.setY(event.getScreenY()-deltaY);
}
public void onMouseRelease(MouseEvent event)
{
if (isIndependent())
{
returnToParent();
}
}
public ConvertiblePane()
{
this.setPrefSize(100, 100);
this.setBackground(new Background(new BackgroundFill(Color.GREEN, new CornerRadii(10), Insets.EMPTY)));
this.setVisible(true);
// Attach event code and report to System.out what is happening:
this.setOnMousePressed((MouseEvent event) -> {
if (this.getScene() == TEMP_SCENE)
System.out.println("Pressed as Independent");
else
System.out.println("Pressed as Child");
onMousePress(event);
});
this.setOnMouseDragged((MouseEvent event) -> {
if (this.getScene() == TEMP_SCENE)
System.out.println("Dragged as Independent");
else
System.out.println("Dragged as Child");
onMouseDrag(event);
});
this.setOnMouseReleased((MouseEvent event) -> {
if (this.getScene() == TEMP_SCENE)
System.out.println("Released as Independent");
else
System.out.println("Released as Child");
onMouseRelease(event);
});
}
public boolean isIndependent()
{
// Return whether this ConvertiblePane is "independent" (exists in its own temp scene)
return this.getScene() == TEMP_SCENE;
}
public void makeIndependent()
{
// Get the point where this ConvertiblePane appears on screen:
Point2D screenPt = this.localToScreen(0, 0);
// Save the originaParent of this ConvertiblePane; we will return to it later:
originalParent = (Pane)getParent();
// Remove this ConvertiblePane from its originalParent:
originalParent.getChildren().remove(this);
// Set this ConvertiblePane as the root of the TEMP_SCENE on the TEMP_STAGE:
TEMP_SCENE.setRoot(this);
TEMP_STAGE.setScene(TEMP_SCENE);
System.out.println("Transferred to TEMP.");
this.relocate(0, 0);
// Show the TEMP_STAGE in the same location on screen where this ConvertiblePane originally was:
TEMP_STAGE.setX(screenPt.getX());
TEMP_STAGE.setY(screenPt.getY());
TEMP_STAGE.show();
}
public void returnToParent()
{
// Reset deltas:
deltaX = 0;
deltaY = 0;
// Get the location of this ConvertiblePane on screen:
Point2D screenPt = this.localToScreen(0, 0);
// Set TEMP_ROOT as the root of TEMP_SCENE; this will allow us to detach
// this ConvertiblePane from being the scene root (since root cannot == null).
TEMP_SCENE.setRoot(TEMP_ROOT);
// Hide the TEMP_STAGE:
TEMP_STAGE.hide();
// Add this ConvertiblePane back to the originalParent:
originalParent.getChildren().add(this);
System.out.println("Transferred to MAIN.");
// Relocate this ConvertiblePane within the originalParent to maintain its position on screen
Point2D parentPt = originalParent.screenToLocal(screenPt);
this.relocate(parentPt.getX(), parentPt.getY());
}
}
As you can see, there is some basic reporting in the event handling methods; and the makeIndependent() and returnToParent() methods output "Transferred to TEMP." and "Transferred to MAIN." respectively.
If I click the mouse on the ConvertiblePane, drag a few pixels, and release it, this is the output:
(on Windows or Mac OS X)
Pressed as Child
Transferred to TEMP.
Dragged as Independent
Dragged as Independent
Dragged as Independent
Released as Independent
Transferred to MAIN.
(on Ubuntu)
Pressed as Child
Transferred to TEMP.
Dragged as Independent
Dragged as Independent
Dragged as Independent
I have also tried adding event filters to the two Scenes; but the result is the same: MouseRelease occurs on Win/Mac, but not Ubuntu.
If anyone can explain this behavior, or suggest something, that would be great. Alternatively... is there any "global" (pre-scene) creation of MouseEvents that I could catch? I mean, I don't really care about the details of the mouse release; I just want an event to tell me when to add the ConvertiblePane back to the main stage.
Thanks!
After spending several weeks on this, I couldn't find a way to fire a proper MouseReleased event on Ubuntu for this situation; however, I did come up with a hack that does the job well enough. Basically, instead of being notified when MouseReleased occurs, I'm checking every 10 milliseconds to see whether the mouse button is no longer down.
Explanation: When the node is transferred to the temporary Scene, the Timeline is started to "move" the mouse pointer in place every 10 milliseconds. This triggers either a MouseDragged event (if the mouse button is still down) or a MouseMoved event (if the mouse button is up); so I can then simulate the MouseReleased event and call my procedure for adding the node back to the main stage. At that point, of course, I also stop the Timeline.
Here's the relevant code to demonstrate this; perhaps it will be of use to someone else as well.
// The robot is needed to "move" the mouse in place,
// triggering a MOUSE_MOVED event.
private static Robot robot = null;
static {
try {
robot = new Robot();
} catch (AWTException ex) {
Logger.getLogger(ConvertiblePane.class.getName()).log(Level.SEVERE, null, ex);
}
}
// clickWaiter will move the mouse in place every 10 milliseconds,
// triggering a MOUSE_MOVED event if the mouse is no longer pressed.
private final Timeline clickWaiter = new Timeline(new KeyFrame(Duration.millis(10), (ActionEvent event) -> {
// Get the mouse position from MouseInfo class.
Point mouse = MouseInfo.getPointerInfo().getLocation();
// "Move" the mouse in place to trigger a mouseMove or mouseDrag event.
robot.mouseMove(mouse.x, mouse.y);
}));
public ConvertiblePane()
{
...
// MOUSE_MOVED will be triggered by the robot when the mouse button is no longer pressed:
TEMP_SCENE.addEventFilter(MouseEvent.MOUSE_MOVED, (MouseEvent event) ->
{
if (!event.isPrimaryButtonDown())
{
System.out.println("Temp.release (sim)");
clickWaiter.stop();
// Simulate MOUSE_RELEASED event.
}
});
}
public void makeIndependent()
{
...
// Start the clickWaiter, part of the Linux hack:
clickWaiter.playFromStart();
}

Draw a polygon in javafx

I'm trying to draw a polygon in javafx adding to its array of point the mouse coordinates. My problem is that I don't know how to make it stop when I click something else from the mouse left click.
Polygon polygon = new Polygon();
rootPane.setOnMouseClicked((MouseEvent mouseEvent) -> {
do {
polygon.getPoints().addAll(mouseEvent.getX(),mouseEvent.getY());
} while(mouseEvent.getButton().equals(mouseEvent.isSecondaryButtonDown()));
});
rootPane.getChildren().add(polygon);
You can create a reference to a polygon. If it's the first click, then the polygon will be null, so create a new one and add it to your pane. Then keep adding points until you get a right click, at which point you just set the polygon back to null, so that the next left click starts a new polygon again.
SSCCE:
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Polygon;
import javafx.stage.Stage;
public class DrawPolygon extends Application {
private Polygon currentPolygon ;
#Override
public void start(Stage primaryStage) {
Pane rootPane = new Pane();
rootPane.setMinSize(600, 600);
rootPane.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
if (currentPolygon == null) {
currentPolygon = new Polygon();
currentPolygon.setStroke(Color.BLACK);
rootPane.getChildren().add(currentPolygon);
}
currentPolygon.getPoints().addAll(e.getX(), e.getY());
} else {
currentPolygon = null ;
}
});
Scene scene = new Scene(rootPane);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
You can play with various ideas around this to get different user experiences, e.g.
rootPane.setOnMouseClicked(e -> {
if (e.getButton() == MouseButton.PRIMARY) {
if (currentPolygon == null) {
currentPolygon = new Polygon();
currentPolygon.getPoints().addAll(e.getX(), e.getY());
currentPolygon.setStroke(Color.BLACK);
rootPane.getChildren().add(currentPolygon);
}
currentPolygon.getPoints().addAll(e.getX(), e.getY());
} else {
currentPolygon = null ;
}
});
rootPane.setOnMouseMoved(e -> {
if (currentPolygon != null) {
currentPolygon.getPoints().set(currentPolygon.getPoints().size()-2, e.getX());
currentPolygon.getPoints().set(currentPolygon.getPoints().size()-1, e.getY());
}
});
The problem is that your code gets stuck in one single event. Even when you move your mouse, or release the mouse button, the values of that event instance you are working with, are not going to change.
Think of the event as a single state. When something important happens (in your case, a mousebutton is clicked), javafx will call your mouseEventHandler using a MouseEvent instance with the state of the mouse at that verry moment. When you click again javafx will create a new instance with new values, and call the eventHandler again.
In order to make this work, you need a different mouse event (or modify it slightly so its only setting a point on a single mouse click). You need to lose the (infinite) while-loop as it is both blocking the EventThread, and not working for the stuff you need it to do. So something like this might work a little better.
// this will add a point for every (secondary)mousebutton click
rootPane.setOnMouseClicked((MouseEvent me) -> {
if(me.isSecondaryButtonDown())
polygon.getPoints().addAll(me.getX(),me.getY());
});
// this will add a point for every mousemovement while the secondary mousebutton is down.
rootPane.setOnMouseMoved((MouseEvent) me -> {
if(me.isSecondaryButtonDown())
polygon.getPoints().addAll(me.getX(),me.getY());
});
Now there is a MouseDragEvent, but thats mostly for moving data (like images and files), but I wouldnt recommend it. In your case it isnt much usefull, and its behavior is quit buggy still.
I hope this helped you in the right direction.

JavaFX program not starting when I use a while loop

Ok, so I'm trying to make a text-based game with a GUI using JavaFX. Everything was working out fine before I put in the actual game loop. Although once I put coded in the game loop, the program wouldn't start. It would run without errors or exceptions, but the window simply wouldn't pop up. When I close the window, all the command line in IntelliJ says is "Process finished with exit code 130" which apparently means that the program was closed because the user press Ctrl+C. which isn't the case because the window isn't even popping up, so it's impossible for any user to click Ctrl+C to terminate the program. So my question is what can I do be able to run my program with a game loop (while loop) without the window refusing to pop up. Here's is my Code:
package sample;
/**
* Created by Angel on 7/26/16.
*/
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
public class Game {
// Inventory is a class that i made,
// but im sure it has nothing to do with my problem
private Inventory inv = new Inventory();
private String levels;
private Scene scene;
private Label topText;
private Button btn1;
private Button btn2;
private Button btn3;
private boolean gameOn;
public void gameStart(){
// Setting the GUI
// Setting up the window
VBox window = new VBox(210);
// setting up the top text holder.
HBox textHolder = new HBox();
textHolder.setAlignment(Pos.CENTER);
// setting up the label
topText = new Label();
topText.setText("You in a dark room, what do you do?");
// setting up the button holder
HBox buttonHolder = new HBox(50);
buttonHolder.setAlignment(Pos.CENTER);
// setting up the buttons
btn1 = new Button();
btn1.setPrefWidth(260);
btn1.setPrefHeight(30);
btn1.getStyleClass().add("gameButtons");
btn1.setText("1");
btn2 = new Button();
btn2.setPrefWidth(260);
btn2.setPrefHeight(30);
btn2.getStyleClass().add("gameButtons");
btn2.setText("2");
btn3 = new Button();
btn3.setPrefWidth(260);
btn3.setPrefHeight(30);
btn3.getStyleClass().add("gameButtons");
btn3.setText("3");
// finalizing the gui, by putting it all together
textHolder.getChildren().add(topText);
buttonHolder.getChildren().addAll(btn1, btn2, btn3);
window.getChildren().addAll(textHolder, buttonHolder);
// setting up the scene
scene = new Scene(window, 800, 600);
//adding the css script
// adding css script
scene.getStylesheets().add(this.getClass().getResource("game.css").toExternalForm());
// the game loop.
levels = "Storyline";
gameOn = true;
while(gameOn) {
switch (levels) {
case "Storyline":
btn1.setText("search around");
btn2.setText("turn the light on");
btn3.setText("stand in fear");
btn2.setOnAction(e -> levels = "level1");
break;
case "level1":
topText.setText("You turned the light on");
break;
}
}
}
public Scene getScene(){
return scene;
}
}
This shouldn't be a game loop. The button click triggers an event. It should be handled there. It's unnecessary to update button texts ect again and again, which is what you're currently doing.
By executing a loop there, you block the application thread, which is the thread that is responsible for redrawing and handling user interaction, making it impossible to do it's task.
Refractor your code to work event based, i.e.
User interaction (button click) triggers ui update (a change of the state and a single update of the UI).
E.g. something like this:
public class Game {
...
public void gameStart(){
...
// setting up the scene
scene = new Scene(window, 800, 600);
// adding css
scene.getStylesheets().add(this.getClass().getResource("game.css").toExternalForm());
setLevel("Storyline");
}
public void setLevel(String newLevel) {
// TODO: integrate gameOn value
if (newLevel != null && !newLevel.equals(levels)) {
levels = newLevel;
switch (levels) {
case "Storyline":
btn1.setText("search around");
btn2.setText("turn the light on");
btn3.setText("stand in fear");
// user interaction triggers setting the level
btn2.setOnAction(e -> setLevel("level1"));
break;
case "level1":
topText.setText("You turned the light on");
break;
}
}
}
...
}
Additional recommendation: Don't represent the level as String, represent it as instance implementing a interface:
public interface Level {
// do updates for level start on game
void updateGameLevelStart(Game game);
// cleanup code e.g. unregistering event handlers...
void updateGameLevelEnd(Game game);
}
this would allow you to change the type of levels to Level and simplify the updates:
public void setLevel(Level newLevel) {
if (!Objects.equals(levels, newLevel)) {
if (levels != null) {
// cleanup old level
levels.updateGameLevelEnd(this);
}
levels = newLevel;
if (newLevel != null) {
// start new level
newLevel.updateGameLevelStart(this);
}
}
}
This would of course require you to make the relevant parts of the ui accessible to the Level implementations.

JavaFX: how to handle dragging an item from a TreeView

For an application I'm developing I have a TreeView with (my own type of) TreeItems. This is working fine, and I get the items to display as expected.
I now want to be able to handle dragging an item from this TreeView to another part of the app window and have it perform some action there. I am now faced with two (at least…) issues:
Whenever you click in the TreeView, the item is always selected. Can this be prevented?
When adding a MouseEvent listener on the TreeView, I get the events with which I would be able to detect dragging and respond to that. I have, however not been able to determine the corresponding TreeItem for the mouse event. I need to know the exact TreeItem, of course, for the drag to work. Is this possible?
Some things I have tried:
I added my own cell factory and even when handling and consuming all mouse events on a cell, the item in the tree is still selected???
If I add a MouseEvent Handler to each and every cell, I will be able to manage the drag and drop, but given there could be thousands (potentially >> 100,000, not all expanded tough) of rows in the TreeView, isn't this a tremendous overhead and would it not be better to have just one event handler for the TreeView? (but then, how do I determine the corresponding TreeItem?)
The TreeView mouse events give me the following info:
No cell clicked: MouseEvent [source = TreeView[id=templateTreeView, styleClass=tree-view], target = TreeViewSkin$1#32a37c7a[styleClass=cell indexed-cell tree-cell]'null', eventType = MOUSE_PRESSED, consumed = false, x = 193.0, y = 289.0, z = 0.0, button = PRIMARY, primaryButtonDown, pickResult = PickResult [node = TreeViewSkin$1#32a37c7a[styleClass=cell indexed-cell tree-cell]'null', point = Point3D [x = 192.0, y = 8.0, z = 0.0], distance = 1492.820323027551]
Cell with text "Attributes" clicked: MouseEvent [source = TreeView[id=templateTreeView, styleClass=tree-view], target = TreeViewSkin$1#16aa9102[styleClass=cell indexed-cell tree-cell]'Attributes', eventType = MOUSE_PRESSED, consumed = false, x = 76.0, y = 34.0, z = 0.0, button = PRIMARY, primaryButtonDown, pickResult = PickResult [node = TreeViewSkin$1#16aa9102[styleClass=cell indexed-cell tree-cell]'Attributes', point = Point3D [x = 75.0, y = 13.0, z = 0.0], distance = 1492.820323027551]
I guess the secret is somewhere in the Node of the PickResult, but from there I'm still unable to see how to get to the TreeItem.
Hope there is an (easy) answer to this...
You are committing the sin of premature optimization :).
TreeCells are essentially only created for the currently visible items in a TreeView. When you expand or collapse nodes in the tree, or when you scroll, those TreeCells are reused to display different TreeItems. This is the purpose of the updateItem(...) and similar methods in TreeCell; they are called when the item displayed by that TreeCell instance changes.
A TreeCell on my system is about 1/4 inch high; to display 100,000 TreeCells would take a monitor more than 2,000 feet / 630 meters tall. At that point, you probably have more serious memory allocation issues than some extra listeners.... But at any rate, a listener would only be invoked if an event occurs on that particular cell, and occupies a fairly small footprint in comparison to the cell itself, so unless you have any direct evidence registering listeners on the cells (which as you've observed, massively reduces your code complexity) adversely affects performance, you should use the "listener per cell" approach.
Here is an example of a tree that holds 1,000,000 Integer-valued tree items. It tracks the number of TreeCells created (on my system it never seems to exceed 20 with the window size I set). It also displays a label; you can drag the values from the tree to the label and the label will display a running total of the values dropped there.
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.concurrent.Task;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TreeViewNoSelection extends Application {
private static int cellCount = 0 ;
private final DataFormat objectDataFormat = new DataFormat("application/x-java-serialized-object");
#Override
public void start(Stage primaryStage) {
TreeView<Integer> tree = new TreeView<>();
tree.setShowRoot(false);
Task<TreeItem<Integer>> buildTreeTask = new Task<TreeItem<Integer>>() {
#Override
protected TreeItem<Integer> call() throws Exception {
TreeItem<Integer> treeRoot = new TreeItem<>(0);
IntStream.range(1, 10).mapToObj(this::createItem)
.forEach(treeRoot.getChildren()::add);
return treeRoot ;
}
private TreeItem<Integer> createItem(int value) {
TreeItem<Integer> item = new TreeItem<>(value);
if (value < 100_000) {
for (int i = 0; i < 10; i++) {
item.getChildren().add(createItem(value * 10 + i));
}
}
return item ;
}
};
tree.setCellFactory(tv -> new TreeCell<Integer>() {
{
System.out.println("Cells created: "+(++cellCount));
setOnDragDetected(e -> {
if (! isEmpty()) {
Dragboard db = startDragAndDrop(TransferMode.COPY);
ClipboardContent cc = new ClipboardContent();
cc.put(objectDataFormat, getItem());
db.setContent(cc);
Label label = new Label(String.format("Add %,d", getItem()));
new Scene(label);
db.setDragView(label.snapshot(null, null));
}
});
}
#Override
public void updateItem(Integer value, boolean empty) {
super.updateItem(value, empty);
if (empty) {
setText(null);
} else {
setText(String.format("%,d", value));
}
}
});
IntegerProperty total = new SimpleIntegerProperty();
Label label = new Label();
label.textProperty().bind(total.asString("Total: %,d"));
label.setOnDragOver(e ->
e.acceptTransferModes(TransferMode.COPY));
// in real life use a CSS pseudoclass and external CSS file for the background:
label.setOnDragEntered(e -> label.setStyle("-fx-background-color: yellow;"));
label.setOnDragExited(e -> label.setStyle(""));
label.setOnDragDropped(e -> {
Dragboard db = e.getDragboard();
if (db.hasContent(objectDataFormat)) {
Integer value = (Integer) db.getContent(objectDataFormat);
total.set(total.get() + value);
e.setDropCompleted(true);
}
});
BorderPane.setMargin(label, new Insets(10));
label.setMaxWidth(Double.MAX_VALUE);
label.setAlignment(Pos.CENTER);
BorderPane root = new BorderPane(new Label("Loading..."));
buildTreeTask.setOnSucceeded(e -> {
tree.setRoot(buildTreeTask.getValue());
root.setCenter(tree);
root.setBottom(label);
});
primaryStage.setScene(new Scene(root, 250, 400));
primaryStage.show();
Thread t = new Thread(buildTreeTask);
t.setDaemon(true);
t.start();
}
public static void main(String[] args) {
launch(args);
}
}
For the selection issue: I would question why you want to do this; it would create an unusual user experience. The issue is probably that the "baked-in" event handlers which manage selection are being invoked before the handlers you define, so by the time you consume the event, selection has already been changed. You can try adding an event filter instead:
cell.addEventFilter(MouseEvent.MOUSE_PRESSED, Event::consume);
but this will also disable expanding/collapsing the nodes in the tree.
So you can try something like:
cell.addEventFilter(MouseEvent.MOUSE_PRESSED, e -> {
if (getTreeItem() != null) {
Object target = e.getTarget();
if (target instanceof Node && ((Node)target).getStyleClass().contains("arrow")) {
getTreeItem().setExpanded(! getTreeItem().isExpanded());
}
}
e.consume();
});
at which point it starts to look like something of a hack...
If you want to entirely disable selection, another option might be to create a custom selection model for the tree which just always returns an empty selection.

how to obtain mouse click coordinates outside my window in Java

I need to implement a class, using Swing, which can obtain the mouse coordinates when the user clicks anywhere on the screen. if I wanted to obtain the mouse coordinates inside my own window, I'd use a MouseListener, but I want it to work even when the user clicks outside my program.
I want my class to behave just like KColorChooser: the user clicks on the drop button and he can click anywhere on the screen to obtain the color of that spot. but I don't know if that's possible using pure Java.
It is possible though limited:
Add an AWTEventListener for focus events. As long as your app has focus before the button is clicked you'll receive a focus lost event. Then query for the pointer position.
The limitation is that, of course, your app loses focus. So depending on what you are ultimately trying to achieve this might not be useful.
If you don't want to lose focus then you will have to temporarily take a screenshot of the whole screen and display that in a screen filling window which listens for a mouse click as usual.
Proof of first method:
import java.awt.AWTEvent;
import java.awt.MouseInfo;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import javax.swing.JFrame;
public class Application1 {
public static void main(String[] args) {
Toolkit.getDefaultToolkit().addAWTEventListener(
new Listener(), AWTEvent.MOUSE_EVENT_MASK | AWTEvent.FOCUS_EVENT_MASK);
JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
private static class Listener implements AWTEventListener {
public void eventDispatched(AWTEvent event) {
System.out.print(MouseInfo.getPointerInfo().getLocation() + " | ");
System.out.println(event);
}
}
}
Clicking outside of the app produced:
java.awt.Point[x=198,y=59] | java.awt.event.MouseEvent[MOUSE_EXITED, ...
java.awt.Point[x=976,y=503] | java.awt.FocusEvent[FOCUS_LOST, ...
The second point is outside of the app.
Forget about GlassPane, there's another 100% native Java way to do it that works both on OS X and on Windows.
Java has always supported translucency for its windows on OS X and Java now supports translucency for its windows on Windows too (since Java 1.6.0_10 or so, needs to be checked).
So the trick is: upon clicking on the "pick a color" tool, you create a nearly transparent borderless Java window covering the entire screen. You set its alpha to 10 (alpha goes from 0 to 255). That alpha is so low the user won't notice that there's a very thin "nearly transparent but only very very very translucent" borderless window covering the entire screen.
Now when the user clicks on your "alpha set to 10 translucent borderless window" covering the entire screen, you get your (x,y).
Discard the borderless Java window.
Use Robot's getRgb(x,y) and you're done.
Why set the alpha to 10 and not 0? Because otherwise clicks aren't intercepted by Java but go directly to the OS (at least that's how it works for a fact on OS X). There's a treshold and I know it's not set at '1', nor '2', it's around 10 or so.
EDIT I just realized you know need to pick several colors, this is trickier but can still be done using 100% Java. Either you can live with "slightly off" colors (affected by the "nearly transparent" 'invisible' layer) or upon getting a click you must remove the layer, get the correct pixel color, and put again a "nearly transparent" layer. Now of course that is one heck of a hack but it can be done in 100% Java.
Use
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.PointerInfo;
PointerInfo inf = MouseInfo.getPointerInfo();
Point p = inf.getLocation();
p.x and p.y will give you co-ordinates outside your window.
I don't know if that's possible using
pure Java.
Its not possible using pure Java, since Java is only aware of MouseEvents on Windows belonging to Java.
These events are directed to the window which has the focus, from all events on the desktop you can only get the mouse position.
As already shown by Keilly it's only possible to get the mouse postion.
You need to include a native lib
I haven't tried this myself, but maybe you could create a full-screen, transparent panel/frame/etc, and add a MouseListener to that.
It is possible with a little trick. Should be 100% cross-platform (tested on Linux & Windows). Basically, you create a small JWindow, make it "alwaysOnTop" and move it around with the mouse using a timer.
For details, see my answer here.
The location (x,y) and the time interval
(d) between each click is supplied thru command line arguments. Here is the
program
import java.awt.* ;
import java.util.* ;
public final class ClickMouse extends TimerTask {
public static int x, y, d ;
public static void main(String[] args) {
TimerTask clikMouse = new ClickMouse();
Timer t = new Timer();
/*
x = Integer.parseInt(args[0]) ;
y = Integer.parseInt(args[1]) ;
d = Integer.parseInt(ares[2]) ;
*/
x = 500;
y = 200;
d = 5;
t.schedule(clikMouse,1000,d*1000);
}
public void run() {
try
{
Robot bot = new Robot();
bot.mouseMove(x,y);
bot.mousePress(java.awt.event.InputEvent.BUTTON1_MASK );
bot.mouseRelease(java.awt.event.InputEvent.BUTTON1_MASK);
}
catch (Exception e)
{
System.out.println("Exception occured :" + e.getMessage());
}
}
}
https://github.com/kwhat/jnativehook JNativeHook: Global keyboard and mouse listeners for Java.
I don't have enough rep yet to leave comments, but here are my comments on the other techniques:
Use a native lib: will work, but has obvious distribution limitations
Use GlassPane to fill entire screen: GlassPanes must be contained within a Window.
Create a Window containing a picture of the desktop and fill the entire screen: Will work, but it will suddenly make the desktop static. The cursor will no longer change, any animations or video in other windows or desktop will become eerily static.
Alternative solution:
A refinement of the screen filling window, if you are using Java 6u10 or later is to make the window completely transparent. Put this window in front of all others and listen for mouse clicks. It still has shortcomings, such as no cursor changes, but it depends on what you want to do.
Based on SyntaxT3rr0r's answer I created a sample color picker in groovy which shows how it can work.
import java.awt.*
import java.awt.datatransfer.*
//import com.sun.awt.AWTUtilities;
import javax.swing.WindowConstants as WC;
import javax.swing.SwingConstants as SWC
import groovy.swing.SwingBuilder
class ColorPicker {
SwingBuilder swb = new SwingBuilder()
def window;
def overlayWindow
def mainPanel;
def mainLabel;
def menu;
def transparent = new Color(0, 0, 0, 0);
def nearlyTransparent = new Color(0, 0, 0, 26);
Color color = new Color(150, 150, 255);
def colorHex = { col ->
col = col?: color;
"#"+Integer.toHexString(col.getRGB())[2..-1]
}
def getTextColor = { baseColor ->
baseColor = baseColor?: color;
(baseColor.red*1.5 + baseColor.green*1.5 + baseColor.blue > 400) ? Color.BLACK : Color.WHITE;
}
def setDisplayColor = {newColor ->
mainPanel.background = newColor
mainLabel.foreground = getTextColor(newColor)
mainLabel.text = colorHex(newColor)
}
def show(){
menu = swb.popupMenu { // invoker: mainPanel
menuItem(text: "Pick Color", actionPerformed: capturePixelColor)
menuItem(text: "Copy to Clipboard", actionPerformed: {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
clipboard.setContents(new StringSelection(colorHex()), null);
})
separator()
menuItem(text: "Close", actionPerformed: {dispose()})
}
window = swb.frame(
title: "Color Picker",
location:[50,50],
size:[60, 60],
resizable: false,
undecorated: true,
alwaysOnTop: true,
defaultCloseOperation:WC.EXIT_ON_CLOSE
){
def textColor = getTextColor()
mainPanel = panel( constraints: BorderLayout.CENTER,
border: lineBorder(color: Color.BLACK),
componentPopupMenu: menu){
borderLayout()
mainLabel = label(text: "--",
constraints: BorderLayout.CENTER,
horizontalAlignment: SWC.CENTER)
}
}
setDisplayColor(color);
window.show();
}
def capturePixelColor = {
def screenSize = Toolkit.getDefaultToolkit().screenSize
overlayWindow = swb.frame(
location:[0,0],
size: screenSize,
resizable: false,
undecorated: true,
alwaysOnTop: true,
defaultCloseOperation:WC.DISPOSE_ON_CLOSE,
show: true,
background: nearlyTransparent, // AWTUtilities.setWindowOpacity(overlayWindow, 0.1f);
cursor: Cursor.CROSSHAIR_CURSOR,
mouseClicked: {event ->
int x = event.getXOnScreen() // or maybe getX() is enough
int y = event.getYOnScreen()
overlayWindow.dispose()
overlayWindow = null
color = new Robot().getPixelColor(x, y)
setDisplayColor(color)
}
)
}
public static void main(String...args){
println "Welcome to ColorPicker"
def picker = new ColorPicker()
picker.show()
}
}
Look, I understand I am 7 years late...
This is a re-make of Keilly's answer, which allows to get when the mouse button is clicked, anywhere. The main problem is that fullscreen games are always unfocused, and it becomes annoying to handle.
Here is the code:
import java.awt.AWTEvent;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.AWTEventListener;
import javax.swing.JFrame;
public class Main {
public static JFrame frame = new JFrame();
public static void main(String[] args) {
Toolkit.getDefaultToolkit().addAWTEventListener(
new Listener(), AWTEvent.MOUSE_EVENT_MASK | AWTEvent.FOCUS_EVENT_MASK);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
frame.setAlwaysOnTop(true);
frame.setLocation(1, 1);
}
private static class Listener implements AWTEventListener {
public void eventDispatched(AWTEvent event) {
// We do not want the event to show twice,
// as it shows for focusing and unfocusing
if(event.getID() == 1004) {
Point p = MouseInfo.getPointerInfo().getLocation();
System.out.println("Mouse Clicked at " + p.x + ", " + p.y);
}
// The frame was just unfocused! To make
// sure we get the next mouse click, we
// need to focus it again!
frame.setVisible(true);
}
}
}

Categories