Dialog causes drag done events to not propagate

2019-07-29 10:20发布

问题:

I'm using a popup dialog during a drag and drop operation. When the drop happens a dialog pops up and when it is dismissed the event chain should continue and allow something to happen when the drag operation ends. If the popup dialog is FX then there is no problem but if it's Gluon the drag done operation doesn't happen.

Here is a sample code:

import javafx.scene.control.Alert.AlertType;
import javafx.scene.control.Label;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DataFormat;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.Background;
import javafx.scene.layout.BackgroundFill;
import javafx.scene.layout.HBox;
import javafx.scene.paint.Color;

import com.gluonhq.charm.glisten.mvc.View;

public class MainView extends View {

    HBox root;

    public MainView(String name) {
        super(name);

        Label source = new Label("Source");
        configureDragSource(source);
        Label target = new Label("Target");
        configureDragTarget(target);
        Label popupTarget = new Label("Popup Target");
        configureDragPopupTarget(popupTarget);
        root = new HBox(40, source, target, popupTarget);
        setCenter(root);
    }

    private void configureDragSource(Label source) {
        source.setOnDragDetected(e -> {
            root.setBackground(new Background(new BackgroundFill(Color.RED, null, null)));
            Dragboard db = source.startDragAndDrop(TransferMode.ANY);
            ClipboardContent content = new ClipboardContent();
            content.put(DataFormat.PLAIN_TEXT, source.getText());
            db.setContent(content);
        });
        source.setOnDragDone(e -> root.setBackground(new Background(new BackgroundFill(null, null, null))));
    }

    private void configureDragTarget(Label target) {
        target.setOnDragOver(e -> e.acceptTransferModes(TransferMode.ANY));

    }

    private void configureDragPopupTarget(Label popupTarget) {
        popupTarget.setOnDragOver(e -> e.acceptTransferModes(TransferMode.ANY));
        popupTarget.setOnDragDropped(e -> {
            javafx.scene.control.Alert popup1 = new javafx.scene.control.Alert(AlertType.INFORMATION);
            com.gluonhq.charm.glisten.control.Alert popup2 = new com.gluonhq.charm.glisten.control.Alert(AlertType.INFORMATION);
            popup1.showAndWait();
        });
    }
}

The source should be dragged upon which the background changes to red. When the drag operation is done the backgrounds should return to default. The regular drop target does nothing and the color change works. But when dropping on the popup target the dialog appears and when it is closed the color changes only for the FX dialog and not for the gluon dialog. Change popup1.showAndWait(); to popup2.

If important this is the application class

import com.gluonhq.charm.glisten.application.MobileApplication;

public class TestApplication extends MobileApplication {

    @Override
    public void init() {
        addViewFactory(HOME_VIEW, () -> new MainView(HOME_VIEW));
    }

    public static void main(String[] args) {
        launch(args);
    }
}

and this is the gradle build file:

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'org.javafxports:jfxmobile-plugin:1.3.5'
    }
}

apply plugin: 'org.javafxports.jfxmobile'
apply plugin: 'eclipse'

jar {
    manifest {
        attributes 'Main-Class': 'com.test.TestApplication'
    }
}

jfxmobile {
    downConfig {
        version = '3.3.0'
        plugins 'display', 'lifecycle', 'statusbar', 'storage'
    }
    android {
        compileSdkVersion = 19
//      manifest = 'src/android/AndroidManifest.xml'
    }
    ios {
        infoPList = file('src/ios/Default-Info.plist')
        forceLinkClasses = [
                'com.gluonhq.**.*',
                'javax.annotations.**.*',
                'javax.inject.**.*',
                'javax.json.**.*',
                'org.glassfish.json.**.*'
        ]
    }
}

eclipse {
    classpath {
        downloadJavadoc = true
        downloadSources = true
    }
}

repositories {
    jcenter()
    maven {
        url 'http://nexus.gluonhq.com/nexus/content/repositories/releases'
    }
}

mainClassName = 'com.test.TestApplication'

dependencies {
    compile 'com.gluonhq:charm:4.3.5'
}

task wrapper(type: Wrapper) {
    gradleVersion = '4.2'
}

Happens also on compile 'com.gluonhq:charm:4.3.7' and 4.4.0.

Running on Java 8 b141. Why does this happen? Is this a bug?

回答1:

JavaFX built-in dialogs and Gluon Dialogs are not the same. In fact, the latter extends from Layer, while being modal and blocking, like the former.

Running on Mac the posted code, I get a NPE after dropping over popupTarget, that can be easily solved using:

Platform.runLater(() -> popup2.showAndWait());

Also, given that the drag event is consumed by the dialog, a possible solution could be:

Platform.runLater(() -> 
    popup2.showAndWait()
        .ifPresent(r -> 
            root.setBackground(new Background(new BackgroundFill(null, null, null)))));

So you could refactor the setOnDragDone method, and just create a method that could be called after the dialog is closed as well.

EDIT

These are the drag events that both target and source receive once the dnd event has started:

The target receives these events on a drag and drop gesture:

 DRAG_OVER
 DRAG_OVER
 ... // until drop is done:
 // show Dialog
 ...
 // hide Dialog
 DRAG_DROPPED
 DRAG_EXITED
 DRAG_EXITED_TARGET

those work exactly the same with JavaFX and Gluon dialogs (at least on Windows. On Mac because of the NPE, using Platform.runLater() delays obviously the show Dialog and hide Dialog events, but lets just focus on Windows for now).

Right after that last event, the source receives:

DRAG_DONE

but only with a JavaFX dialog.

After some debugging, the reason why the Gluon's Dialog aborts the drag done event can be explained as follows:

The Scene class has a DnDGesture private class:

/**
 * A Drag and Drop gesture has a lifespan that lasts from mouse
 * PRESSED event to mouse RELEASED event.
 */
class DnDGesture {
   ...
}

And as it is explained in the comment, it has a lifespan from mouse pressed event to mouse release event.

Using JavaFX built-in dialog, this is displayed in a new Stage and therefore a new Scene. The key here is that this dialog (and all the set of mouse events) is displayed in a new modal stage, so once the dialog is hidden, the primary stage takes the focus again and resumes, finishing properly the dnd gesture as the mouse is released.

But using Gluon's dialog, there is no second stage. Everything happens in the same stage, and once the mouse is released, the Scene.DnDGesture becomes null, so when the DRAG_EXITED_TARGET event happens in the target, the proper call is done to finish the dnd process, but at this point dndGesture is null, and that call doesn't reach the source anymore.

I don't consider this a bug, but more like a tradeoff, as there are a number of reasons to avoid a second stage/scene on a mobile environment and keep the View/Layer (in one single stage) design.