Manage wait cursor for task

2019-09-09 20:02发布

问题:

  1. I'm outside the UI and wish to display a wait cursor while stuff is happening and using this basic pattern:

    on UI - primaryStage.scene.cursor = Cursor.WAIT try { do stuff off UI... } finally { on UI - primaryStage.scene.cursor = Cursor.DEFAULT }

While running I can start another process which completes quickly and the Cursor is restored before the first task completes.

I don't mind "waiting" while the first task completes, but I don't think this means doing the work on the UI thread? Is there any built in solution for this pattern provided in javafx?

  1. My tab contains 2 Combo Box. When I hit the 2nd Combo Box drop down, a WAIT cursor sometimes appears over the list even though the Cursor is currently DEFAULT state. If I move the mouse pointer outside/back on the list the cursor is correctly displayed as Default. Would this be a separate issue or somehow related?

VIEW

label 'From'
comboBox(items: bind(model.wcomboFromItemsProperty()), value: bind(model.wcomboFromProperty()), selectFromAction)
label 'To'
comboBox(items: bind(model.wcomboFromItemsProperty()), value: bind(model.wcomboToProperty()), selectToAction)

MODEL

@FXObservable ListElement wcomboFrom = new ListElement()
@FXObservable ListElement wcomboTo = new ListElement()
@FXObservable List wcomboFromItems = FXCollections.observableArrayList()
@FXObservable List wcomboToItems = FXCollections.observableArrayList()

final ObjectProperty<Cursor> CURSOR_DEFAULT = new SimpleObjectProperty<>(Cursor.DEFAULT)
final ObjectProperty<Cursor> CURSOR_WAIT = new SimpleObjectProperty<>(Cursor.WAIT)

CONTROLLER

//lifecycle
void onReadyStart(GriffonApplication application) {
    loadWindowData()
}

// both combo boxes contain the same items
protected void loadWindowData() {
    def list = [new ListElement(textValue: '')]
    list.addAll dataService.getData().collect {
        new ListElement(textValue: it.name, objectValue: it)
    }

    runInsideUIAsync {
        model.wcomboFromItems.addAll(list)
        model.wcomboToItems.addAll(list)
    }
}

void selectFrom() {
    performAction {
        gcListFrom = getControlList(model.wcomboFrom.objectValue)
        setTreeItems(model.wtreeGcFrom, gcListFrom, model.wcomboFrom)
        setTreeItems(model.wtreeGcTo, gcListTo, model.wcomboTo)
    }
}

void selectTo() {
    performAction {
        gcListTo = getControlList(model.wcomboTo.objectValue)
        setTreeItems(model.wtreeGcTo, gcListTo, model.wcomboTo)
    }
}

def performAction = {c ->
    Task<Void> t = new Task() {
        @Override protected Void call() {
            println "Running closure " + isUIThread()
            c.call()
        }
    }

    runInsideUISync {
        application.primaryStage.scene.cursorProperty().bind(Bindings.when(t.runningProperty())
            .then(model.CURSOR_WAIT).otherwise(model.CURSOR_DEFAULT))
    }

    runOutsideUI(t)
}

OTHER

@EqualsAndHashCode(includes = 'textValue')
class ListElement implements Serializable {
    String textValue = ""
    Serializable objectValue // Serializable object from business model

    @Override
    String toString() {
        textValue
    }
}

The Griffon framework automatically invokes the onAction controller events outside the UI thread. GroovyFX contains some magic which adds an "onSelect" action bound to selectionModel.selectedItemProperty i.e.

class GroovyFXEnhancer {
    static void enhanceClasses() {
        ...
        ComboBox.metaClass {
            cellFactory << { Closure closure -> delegate.setCellFactory(closure as Callback)}
            onSelect << { Closure closure ->
                delegate.selectionModel.selectedItemProperty().addListener(closure as ChangeListener);
        }
        ...
    }
}

回答1:

Is there any built in solution for this pattern provided in javafx?

I would advice you to use the built in Task ;)

It has predefined methods to handle everything you need.

private Task<Void> backgroundTask = new Task() {
    @Override
    protected Void call() throws Exception {
        // Something to do on background thread ;
        return null;
    }
};

It has a runningProperty(), which can bind to the cursorProperty() of the scene.

You can create two ObjectProperty<Cursor> containing Cursor.DEFAULT and CURSOR.WAIT.

final ObjectProperty<Cursor> CURSOR_DEFAULT = new SimpleObjectProperty<>(Cursor.DEFAULT);
final ObjectProperty<Cursor> CURSOR_WAIT = new SimpleObjectProperty<>(Cursor.WAIT);

Then you can bind them to the task :

scene.cursorProperty().bind(Bindings.when(backgroundTask.runningProperty())
                                            .then(CURSOR_WAIT).otherwise(CURSOR_DEFAULT));

Would this be a separate issue or somehow related?

If your action on the ComboBox is somehow invoking the background thread, then it might be related, else it is difficult to comment.



回答2:

You can also use the griffon-tasks-plugin http://griffon-plugins.github.io/griffon-tasks-plugin/

This plugin delivers and UI toolkit agnostik SwingWorker-like API for executing tasks in a background thread.