Raymond Nagel
Raymond Nagel

Reputation: 81

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!

Upvotes: 1

Views: 188

Answers (1)

Raymond Nagel
Raymond Nagel

Reputation: 81

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

Upvotes: 0

Related Questions