Skip to content

Commit

Permalink
Don't pass Python Future objects to QML
Browse files Browse the repository at this point in the history
Returning a Future doesn't work on Windows for some reason
(thp/pyotherside#116).

Instead of using these objects from QML to cancel running coroutines,
call a Python QMLBridge function that takes a coroutine UUID and will
take care of the cancelling.
  • Loading branch information
mirukana committed Sep 29, 2020
1 parent 037abcc commit 87fcb0a
Show file tree
Hide file tree
Showing 20 changed files with 161 additions and 178 deletions.
2 changes: 2 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# TODO

- Encrypted rooms don't show invites in member list after Mirage restart
- Room display name not updated when someone removes theirs
- Fix right margin of own `<image url>\n<image url>` messages

- filter > enter > room list is always scrolled to top
Expand Down
30 changes: 21 additions & 9 deletions src/backend/qml_bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
from concurrent.futures import Future
from operator import attrgetter
from threading import Thread
from typing import Coroutine, Sequence
from typing import Coroutine, Dict, Sequence

import pyotherside

Expand Down Expand Up @@ -52,6 +52,8 @@ def __init__(self) -> None:
from .backend import Backend
self.backend: Backend = Backend()

self._running_futures: Dict[str, Future] = {}

Thread(target=self._start_asyncio_loop).start()


Expand All @@ -73,7 +75,7 @@ def _start_asyncio_loop(self) -> None:
self._loop.run_forever()


def _call_coro(self, coro: Coroutine, uuid: str) -> Future:
def _call_coro(self, coro: Coroutine, uuid: str) -> None:
"""Schedule a coroutine to run in our thread and return a `Future`."""

def on_done(future: Future) -> None:
Expand All @@ -87,27 +89,37 @@ def on_done(future: Future) -> None:
trace = traceback.format_exc().rstrip()

CoroutineDone(uuid, result, exception, trace)
del self._running_futures[uuid]

future = asyncio.run_coroutine_threadsafe(coro, self._loop)
self._running_futures[uuid] = future
future.add_done_callback(on_done)
return future


def call_backend_coro(
self, name: str, uuid: str, args: Sequence[str] = (),
) -> Future:
"""Schedule a `Backend` coroutine and return a `Future`."""
) -> None:
"""Schedule a coroutine from the `QMLBridge.backend` object."""

return self._call_coro(attrgetter(name)(self.backend)(*args), uuid)
self._call_coro(attrgetter(name)(self.backend)(*args), uuid)


def call_client_coro(
self, user_id: str, name: str, uuid: str, args: Sequence[str] = (),
) -> Future:
"""Schedule a `MatrixClient` coroutine and return a `Future`."""
) -> None:
"""Schedule a coroutine from a `QMLBridge.backend.clients` client."""

client = self.backend.clients[user_id]
return self._call_coro(attrgetter(name)(client)(*args), uuid)
self._call_coro(attrgetter(name)(client)(*args), uuid)


def cancel_coro(self, uuid: str) -> None:
"""Cancel a couroutine scheduled by the `QMLBridge` methods."""

try:
self._running_futures[uuid].cancel()
except KeyError:
log.warning("Couldn't cancel coroutine %s, future not found", uuid)


def pdb(self, additional_data: Sequence = ()) -> None:
Expand Down
8 changes: 4 additions & 4 deletions src/gui/Base/HMxcImage.qml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// SPDX-License-Identifier: LGPL-3.0-or-later

import QtQuick 2.12
import "../PythonBridge"

HImage {
id: image
Expand All @@ -18,7 +17,7 @@ HImage {
property bool canUpdate: true
property bool show: ! canUpdate

property Future getFuture: null
property string getFutureId: ""

readonly property bool isMxc: mxc.startsWith("mxc://")

Expand All @@ -45,10 +44,11 @@ HImage {
[clientUserId, image.mxc, image.title, w, h, cryptDict] :
[clientUserId, image.mxc, image.title, cryptDict]

getFuture = py.callCoro("media_cache." + method, args, path => {
getFutureId = py.callCoro("media_cache." + method, args, path => {
if (! image) return
if (image.cachedPath !== path) image.cachedPath = path

getFutureId = ""
image.broken = Qt.binding(() => image.status === Image.Error)
image.show = image.visible

Expand All @@ -68,5 +68,5 @@ HImage {
onHeightChanged: Qt.callLater(reload)
onVisibleChanged: Qt.callLater(reload)
onMxcChanged: Qt.callLater(reload)
Component.onDestruction: if (getFuture) getFuture.cancel()
Component.onDestruction: if (getFutureId) py.cancelCoro(getFutureId)
}
17 changes: 8 additions & 9 deletions src/gui/Dialogs/ImportKeys.qml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@ import QtQuick 2.12
import QtQuick.Controls 2.12
import Qt.labs.platform 1.1
import "../Popups"
import "../PythonBridge"

HFileDialogOpener {
property string userId: ""
property Future importFuture: null
property string importFutureId: ""


fill: false
Expand All @@ -28,13 +27,13 @@ HFileDialogOpener {
const call = py.callClientCoro
const path = file.toString().replace(/^file:\/\//, "")

importFuture = call(userId, "import_keys", [path, pass], () => {
importFuture = null
importFutureId = call(userId, "import_keys", [path, pass], () => {
importFutureId = ""
callback(true)

}, (type, args, error, traceback, uuid) => {
let unknown = false
importFuture = null
let unknown = false
importFutureId = ""

callback(
type === "EncryptionError" ?
Expand Down Expand Up @@ -63,17 +62,17 @@ HFileDialogOpener {
}

summary.text:
importFuture ?
importFutureId ?
qsTr("This might take a while...") :
qsTr("Passphrase used to protect this file:")
validateButton.text: qsTr("Import")
validateButton.icon.name: "import-keys"

onClosed: if (importFuture) importFuture.cancel()
onClosed: if (importFutureId) py.cancelCoro(importFutureId)

Binding on closePolicy {
value: Popup.CloseOnEscape
when: importFuture
when: importFutureId
}
}
}
23 changes: 11 additions & 12 deletions src/gui/MainPane/RoomDelegate.qml
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,12 @@ import Clipboard 0.1
import ".."
import "../Base"
import "../Base/HTile"
import "../PythonBridge"

HTile {
id: room

property Future fetchProfilesFuture: null
property Future loadEventsFuture: null
property string fetchProfilesFutureId: ""
property string loadEventsFutureId: ""
property bool moreToLoad: true

readonly property bool joined: ! invited && ! parted
Expand Down Expand Up @@ -204,8 +203,8 @@ HTile {
}

Component.onDestruction: {
if (fetchProfilesFuture) fetchProfilesFuture.cancel()
if (loadEventsFuture) loadEventsFuture.cancel()
if (fetchProfilesFutureId) py.cancelCoro(fetchProfilesFutureId)
if (loadEventsFutureId) py.cancelCoro(loadEventsFutureId)
}

Timer {
Expand All @@ -217,15 +216,15 @@ HTile {
! lastEvent &&
moreToLoad

onTriggered: if (! loadEventsFuture) {
loadEventsFuture = py.callClientCoro(
onTriggered: if (! loadEventsFutureId) {
loadEventsFutureId = py.callClientCoro(
model.for_account,
"load_past_events",
[model.id],
more => {
if (! room) return // delegate was destroyed
loadEventsFuture = null
moreToLoad = more
loadEventsFutureId = ""
moreToLoad = more
}
)
}
Expand All @@ -242,13 +241,13 @@ HTile {
lastEvent.fetch_profile

onTriggered: {
if (fetchProfilesFuture) fetchProfilesFuture.cancel()
if (fetchProfilesFutureId) py.cancelCoro(fetchProfilesFutureId)

fetchProfilesFuture = py.callClientCoro(
fetchProfilesFutureId = py.callClientCoro(
model.for_account,
"get_event_profiles",
[model.id, lastEvent.id],
() => { if (room) fetchProfilesFuture = null },
() => { if (room) fetchProfilesFutureId = "" },
)
}
}
Expand Down
9 changes: 4 additions & 5 deletions src/gui/Pages/AccountSettings/Sessions.qml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import QtQuick.Layouts 1.12
import "../.."
import "../../Base"
import "../../Base/Buttons"
import "../../PythonBridge"
import "../../ShortcutBundles"

HColumnPage {
Expand All @@ -18,19 +17,19 @@ HColumnPage {
property bool enableFlickShortcuts:
SwipeView ? SwipeView.isCurrentItem : true

property Future loadFuture: null
property string loadFutureId: ""

function takeFocus() {} // TODO

function loadDevices() {
loadFuture = py.callClientCoro(userId, "devices_info", [], devices => {
loadFutureId = py.callClientCoro(userId, "devices_info",[],devices => {
deviceList.uncheckAll()
deviceList.model.clear()

for (const device of devices)
deviceList.model.append(device)

loadFuture = null
loadFutureId = ""
deviceList.sectionItemCounts = getSectionItemCounts()

if (page.enabled && ! deviceList.currentItem)
Expand Down Expand Up @@ -187,7 +186,7 @@ HColumnPage {
height: width

source: "../../Base/HBusyIndicator.qml"
active: page.loadFuture
active: page.loadFutureId
opacity: active ? 1 : 0

Behavior on opacity { HNumberAnimation { factor: 2 } }
Expand Down
28 changes: 14 additions & 14 deletions src/gui/Pages/AddAccount/ServerBrowser.qml
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,26 @@ HBox {
property var saveProperties: ["acceptedUserUrl", "knownHttps"]
property string loadingIconStep: "server-ping-bad"

property Future connectFuture: null
property Future fetchServersFuture: null
property string connectFutureId: ""
property string fetchServersFutureId: ""

signal accepted()

function takeFocus() { serverField.item.field.forceActiveFocus() }

function fetchServers() {
if (fetchServersFuture) fetchServersFuture.cancel()
if (fetchServersFutureId) py.cancelCoro(fetchServersFutureId)

fetchServersFuture = py.callCoro("fetch_homeservers", [], () => {
fetchServersFuture = null
fetchServersFutureId = py.callCoro("fetch_homeservers", [], () => {
fetchServersFutureId = ""
}, (type, args, error, traceback) => {
fetchServersFuture = null
fetchServersFutureId = ""
print( traceback) // TODO: display error graphically
})
}

function connect() {
if (connectFuture) connectFuture.cancel()
if (connectFutureId) py.cancelCoro(connectFutureId)
connectTimeout.restart()

const typedUrl = serverField.item.field.cleanText
Expand All @@ -49,10 +49,10 @@ HBox {
if (box.knownHttps)
args[0] = args[0].replace(/^(https?:\/\/)?/, "https://")

connectFuture = py.callCoro("server_info", args, ([url, flows]) => {
connectFutureId = py.callCoro("server_info", args, ([url, flows]) => {
connectTimeout.stop()
serverField.errorLabel.text = ""
connectFuture = null
connectFutureId = ""

if (! (
flows.includes("m.login.password") ||
Expand All @@ -75,7 +75,7 @@ HBox {
console.error(traceback)

connectTimeout.stop()
connectFuture = null
connectFutureId = ""

let text = qsTr("Unexpected error: %1 [%2]").arg(type).arg(args)

Expand Down Expand Up @@ -194,7 +194,7 @@ HBox {
enabled: field.cleanText && ! field.error
icon.name: "server-connect-to-address"
icon.color: theme.colors.positiveBackground
loading: box.connectFuture !== null
loading: box.connectFutureId !== ""
disableWhileLoading: false
onClicked: box.connect()

Expand All @@ -209,7 +209,7 @@ HBox {
onAccepted: window.saveState(this)

Component.onDestruction:
if (fetchServersFuture) fetchServersFuture.cancel()
if (fetchServersFutureId) py.cancelCoro(fetchServersFutureId)

Timer {
id: connectTimeout
Expand All @@ -230,7 +230,7 @@ HBox {
Timer {
interval: 1000
running:
fetchServersFuture === null &&
fetchServersFutureId === "" &&
ModelStore.get("homeservers").count === 0

repeat: true
Expand Down Expand Up @@ -292,7 +292,7 @@ HBox {
height: width

source: "../../Base/HBusyIndicator.qml"
active: box.fetchServersFuture && ! serverList.count
active: box.fetchServersFutureId && ! serverList.count
opacity: active ? 1 : 0

Behavior on opacity { HNumberAnimation { factor: 2 } }
Expand Down

0 comments on commit 87fcb0a

Please sign in to comment.