Don't pass Python Future objects to QML

Returning a Future doesn't work on Windows for some reason
(https://github.com/thp/pyotherside/issues/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.
This commit is contained in:
miruka 2020-09-28 23:06:50 -04:00
parent 53d2ab17af
commit ee1091b4dc
20 changed files with 161 additions and 178 deletions

View File

@ -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

View File

@ -21,7 +21,7 @@ import traceback
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
@ -52,6 +52,8 @@ class QMLBridge:
from .backend import Backend
self.backend: Backend = Backend()
self._running_futures: Dict[str, Future] = {}
Thread(target=self._start_asyncio_loop).start()
@ -73,7 +75,7 @@ class QMLBridge:
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:
@ -87,27 +89,37 @@ class QMLBridge:
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:

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
import "../PythonBridge"
HImage {
id: image
@ -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://")
@ -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
@ -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)
}

View File

@ -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
@ -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
importFutureId = ""
callback(
type === "EncryptionError" ?
@ -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
}
}
}

View File

@ -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
@ -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 {
@ -217,14 +216,14 @@ 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
loadEventsFutureId = ""
moreToLoad = more
}
)
@ -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 = "" },
)
}
}

View File

@ -7,7 +7,6 @@ import QtQuick.Layouts 1.12
import "../.."
import "../../Base"
import "../../Base/Buttons"
import "../../PythonBridge"
import "../../ShortcutBundles"
HColumnPage {
@ -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)
@ -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 } }

View File

@ -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
@ -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") ||
@ -75,7 +75,7 @@ HBox {
console.error(traceback)
connectTimeout.stop()
connectFuture = null
connectFutureId = ""
let text = qsTr("Unexpected error: %1 [%2]").arg(type).arg(args)
@ -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()
@ -209,7 +209,7 @@ HBox {
onAccepted: window.saveState(this)
Component.onDestruction:
if (fetchServersFuture) fetchServersFuture.cancel()
if (fetchServersFutureId) py.cancelCoro(fetchServersFutureId)
Timer {
id: connectTimeout
@ -230,7 +230,7 @@ HBox {
Timer {
interval: 1000
running:
fetchServersFuture === null &&
fetchServersFutureId === "" &&
ModelStore.get("homeservers").count === 0
repeat: true
@ -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 } }

View File

@ -14,7 +14,7 @@ HFlickableColumnPage {
property string serverUrl
property string displayUrl: serverUrl
property var loginFuture: null
property string loginFutureId: null
readonly property int security:
serverUrl.startsWith("https://") ?
@ -36,7 +36,7 @@ HFlickableColumnPage {
function finishSignIn(receivedUserId) {
errorMessage.text = ""
page.loginFuture = null
page.loginFutureId = ""
py.callCoro(
rememberAccount.checked ?
@ -53,13 +53,13 @@ HFlickableColumnPage {
}
function cancel() {
if (! page.loginFuture) {
if (! page.loginFutureId) {
page.exitRequested()
return
}
page.loginFuture.cancel()
page.loginFuture = null
py.cancelCoro(page.loginFutureId)
page.loginFutureId = ""
}
@ -72,7 +72,7 @@ HFlickableColumnPage {
text: qsTr("Sign in")
icon.name: "sign-in"
loading: page.loginFuture !== null
loading: page.loginFutureId !== ""
disableWhileLoading: false
}
@ -83,7 +83,7 @@ HFlickableColumnPage {
onKeyboardAccept: if (applyButton.enabled) applyButton.clicked()
onKeyboardCancel: page.cancel()
Component.onDestruction: if (loginFuture) loginFuture.cancel()
Component.onDestruction: if (loginFutureId) py.cancelCoro(loginFutureId)
HButton {
icon.name: "sign-in-" + (

View File

@ -12,17 +12,17 @@ SignInBase {
function takeFocus() { idField.item.forceActiveFocus() }
function signIn() {
if (page.loginFuture) page.loginFuture.cancel()
if (page.loginFutureId) page.loginFutureId = ""
errorMessage.text = ""
page.loginFuture = py.callCoro(
page.loginFutureId = py.callCoro(
"password_auth",
[idField.item.text.trim(), passField.item.text, page.serverUrl],
page.finishSignIn,
(type, args, error, traceback, uuid) => {
page.loginFuture = null
page.loginFutureId = ""
let txt = qsTr(
"Invalid request, login type or unknown error: %1",

View File

@ -14,23 +14,23 @@ SignInBase {
function startSignIn() {
errorMessage.text = ""
page.loginFuture = py.callCoro("start_sso_auth", [serverUrl], url => {
page.loginFutureId = py.callCoro("start_sso_auth",[serverUrl], url => {
urlArea.text = url
urlArea.cursorPosition = 0
Qt.openUrlExternally(url)
page.loginFuture = py.callCoro("continue_sso_auth", [], userId => {
page.loginFuture = null
page.loginFutureId = py.callCoro("continue_sso_auth",[],userId => {
page.loginFutureId = ""
page.finishSignIn(userId)
})
})
}
function cancel() {
if (loginFuture) {
page.loginFuture.cancel()
page.loginFuture = null
if (loginFutureId) {
py.cancelCoro(page.loginFutureId)
page.loginFutureId = ""
}
page.exitRequested()

View File

@ -13,7 +13,7 @@ import "Timeline"
HColumnPage {
id: chatPage
property var loadMembersFuture: null
property string loadMembersFutureId: ""
readonly property alias roomHeader: roomHeader
readonly property alias eventList: eventList
@ -29,16 +29,17 @@ HColumnPage {
padding: 0
column.spacing: 0
Component.onDestruction: if (loadMembersFuture) loadMembersFuture.cancel()
Component.onDestruction:
if (loadMembersFutureId) py.cancelCoro(loadMembersFutureId)
Timer {
interval: 200
running: ! chat.roomInfo.inviter_id && ! chat.roomInfo.left
onTriggered: loadMembersFuture = py.callClientCoro(
onTriggered: loadMembersFutureId = py.callClientCoro(
chat.userId,
"load_all_room_members",
[chat.roomId],
() => { loadMembersFuture = null },
() => { loadMembersFutureId = "" },
)
}

View File

@ -7,13 +7,12 @@ import "../../../.."
import "../../../../Base"
import "../../../../Base/HTile"
import "../../../../Popups"
import "../../../../PythonBridge"
HTile {
id: member
property bool colorName: hovered
property Future getPresenceFuture: null
property string getPresenceFutureId: ""
backgroundColor: theme.chat.roomPane.listView.member.background
contentOpacity:
@ -155,11 +154,15 @@ HTile {
Component.onCompleted:
if (model.presence === "offline" && model.last_active_at < new Date(1))
getPresenceFuture = py.callClientCoro(
chat.userId, "get_offline_presence", [model.id],
getPresenceFutureId = py.callClientCoro(
chat.userId,
"get_offline_presence",
[model.id],
() => { getPresenceFutureId = "" }
)
Component.onDestruction: if (getPresenceFuture) getPresenceFuture.cancel()
Component.onDestruction:
if (getPresenceFutureId) py.cancelCoro(getPresenceFutureId)
Behavior on contentOpacity { HNumberAnimation {} }
Behavior on spacing { HNumberAnimation {} }

View File

@ -6,7 +6,6 @@ import QtQuick.Layouts 1.12
import "../../../.."
import "../../../../Base"
import "../../../../Base/Buttons"
import "../../../../PythonBridge"
HListView {
id: root
@ -21,8 +20,8 @@ HListView {
property bool powerLevelFieldFocused: false
property Future setPowerFuture: null
property Future getPresenceFuture: null
property string setPowerFutureId: ""
property string getPresenceFutureId: ""
function loadDevices() {
py.callClientCoro(userId, "member_devices", [member.id], devices => {
@ -246,14 +245,14 @@ HListView {
ApplyButton {
id: applyButton
enabled: ! powerLevel.item.fieldOverMaximum
loading: setPowerFuture !== null
loading: setPowerFutureId !== ""
text: ""
onClicked: {
setPowerFuture = py.callClientCoro(
setPowerFutureId = py.callClientCoro(
userId,
"room_set_member_power",
[roomId, member.id, powerLevel.item.level],
() => { setPowerFuture = null }
() => { setPowerFutureId = "" }
)
}
@ -264,8 +263,8 @@ HListView {
CancelButton {
text: ""
onClicked: {
setPowerFuture.cancel()
setPowerFuture = null
py.cancelCoro(setPowerFutureId)
setPowerFutureId = ""
powerLevel.item.reset()
}
@ -289,14 +288,18 @@ HListView {
if (member.presence === "offline" &&
member.last_active_at < new Date(1))
{
getPresenceFuture =
py.callClientCoro(userId, "get_offline_presence", [member.id])
getPresenceFutureId = py.callClientCoro(
userId,
"get_offline_presence",
[member.id],
() => { getPresenceFutureId = "" }
)
}
}
Component.onDestruction: {
if (setPowerFuture) setPowerFuture.cancel()
if (getPresenceFuture) getPresenceFuture.cancel()
if (setPowerFutureId) py.cancelCoro(setPowerFutureId)
if (getPresenceFutureId) py.cancelCoro(getPresenceFutureId)
}
Keys.onEnterPressed: Keys.onReturnPressed(event)

View File

@ -10,7 +10,7 @@ import "../../../Base/Buttons"
HFlickableColumnPage {
id: settingsView
property var saveFuture: null
property string saveFutureId: ""
readonly property bool anyChange:
nameField.item.changed || topicArea.item.area.changed ||
@ -20,7 +20,7 @@ HFlickableColumnPage {
readonly property Item keybindFocusItem: nameField.item
function save() {
if (saveFuture) saveFuture.cancel()
if (saveFutureId) py.cancelCoro(saveFutureId)
const args = [
chat.roomId,
@ -35,17 +35,17 @@ HFlickableColumnPage {
forbidGuestsCheckBox.checked : undefined,
]
function onDone() { saveFuture = null }
function onDone() { saveFutureId = "" }
saveFuture = py.callClientCoro(
saveFutureId = py.callClientCoro(
chat.userId, "room_set", args, onDone, onDone,
)
}
function cancel() {
if (saveFuture) {
saveFuture.cancel()
saveFuture = null
if (saveFutureId) {
py.cancelCoro(saveFutureId)
saveFutureId = ""
}
nameField.item.reset()
@ -66,14 +66,14 @@ HFlickableColumnPage {
ApplyButton {
id: applyButton
enabled: anyChange
loading: saveFuture !== null
loading: saveFutureId !== ""
disableWhileLoading: false
onClicked: save()
}
CancelButton {
enabled: anyChange || saveFuture !== null
enabled: anyChange || saveFutureId !== ""
onClicked: cancel()
}
}

View File

@ -6,12 +6,11 @@ import QtQuick.Layouts 1.12
import Clipboard 0.1
import "../../.."
import "../../../Base"
import "../../../PythonBridge"
HColumnLayout {
id: eventDelegate
property var fetchProfilesFuture: null
property string fetchProfilesFutureId: ""
// Remember timeline goes from newest message at index 0 to oldest
readonly property var previousModel: eventList.model.get(model.index + 1)
@ -72,16 +71,16 @@ HColumnLayout {
onCursorShapeChanged: eventList.cursorShape = cursorShape
Component.onCompleted: if (model.fetch_profile)
fetchProfilesFuture = py.callClientCoro(
fetchProfilesFutureId = py.callClientCoro(
chat.userId,
"get_event_profiles",
[chat.roomId, model.id],
// The if avoids segfault if eventDelegate is already destroyed
() => { if (eventDelegate) fetchProfilesFuture = null }
() => { if (eventDelegate) fetchProfilesFutureId = "" }
)
Component.onDestruction:
if (fetchProfilesFuture) fetchProfilesFuture.cancel()
if (fetchProfilesFutureId) py.cancelCoro(fetchProfilesFutureId)
ListView.onRemove: eventList.uncheck(model.id)

View File

@ -218,8 +218,8 @@ Rectangle {
HListView {
id: eventList
property Future updateMarkerFuture: null
property Future loadPastEventsFuture: null
property string updateMarkerFutureId: ""
property string loadPastEventsFutureId: ""
property bool moreToLoad: true
property bool ownEventsOnLeft:
@ -355,13 +355,13 @@ Rectangle {
}
function loadPastEvents() {
loadPastEventsFuture = py.callClientCoro(
loadPastEventsFutureId = py.callClientCoro(
chat.userId,
"load_past_events",
[chat.roomId],
more => {
moreToLoad = more
loadPastEventsFuture = null
loadPastEventsFutureId = ""
}
)
}
@ -519,7 +519,7 @@ Rectangle {
footer: Item {
width: eventList.width
height: (button.height + theme.spacing * 2) * opacity
opacity: eventList.loadPastEventsFuture ? 1 : 0
opacity: eventList.loadPastEventsFutureId ? 1 : 0
visible: opacity > 0
Behavior on opacity { HNumberAnimation {} }
@ -554,14 +554,14 @@ Rectangle {
interval: 200
running:
eventList.shouldLoadPastEvents &&
! eventList.loadPastEventsFuture
! eventList.loadPastEventsFutureId
triggeredOnStart: true
onTriggered: eventList.loadPastEvents()
}
Component.onDestruction: {
if (loadPastEventsFuture) loadPastEventsFuture.cancel()
if (loadPastEventsFutureId) py.cancelCoro(loadPastEventsFutureId)
}
MouseArea {
@ -585,7 +585,7 @@ Rectangle {
interval: Math.max(100, window.settings.markRoomReadMsecDelay)
running:
! eventList.updateMarkerFuture &&
! eventList.updateMarkerFutureId &&
(
chat.roomInfo.unreads ||
chat.roomInfo.highlights ||
@ -600,11 +600,11 @@ Rectangle {
const item = eventList.model.get(i)
if (item.sender !== chat.userId) {
eventList.updateMarkerFuture = py.callCoro(
eventList.updateMarkerFutureId = py.callCoro(
"update_room_read_marker",
[chat.roomId, item.event_id],
() => { eventList.updateMarkerFuture = null },
() => { eventList.updateMarkerFuture = null },
() => { eventList.updateMarkerFutureId = "" },
() => { eventList.updateMarkerFutureId = "" },
)
return
}

View File

@ -4,7 +4,6 @@
import QtQuick 2.12
import "../Base"
import "../Base/Buttons"
import "../PythonBridge"
PasswordPopup {
id: popup
@ -13,15 +12,15 @@ PasswordPopup {
property var deviceIds // array
property var deletedCallback: null
property Future deleteFuture: null
property string deleteFutureId: ""
function verifyPassword(pass, callback) {
deleteFuture = py.callClientCoro(
deleteFutureId = py.callClientCoro(
userId,
"delete_devices_with_password",
[deviceIds, pass],
() => {
deleteFuture = null
deleteFutureId = ""
callback(true)
},
(type, args) => {
@ -47,9 +46,9 @@ PasswordPopup {
validateButton.icon.name: "sign-out"
onClosed: {
if (deleteFuture) deleteFuture.cancel()
if (deleteFutureId) py.cancelCoro(deleteFutureId)
if (deleteFuture || acceptedPassword && deletedCallback)
if (deleteFutureId || acceptedPassword && deletedCallback)
deletedCallback()
}
}

View File

@ -15,7 +15,7 @@ HColumnPopup {
property string roomName
property bool invitingAllowed: true
property var inviteFuture: null
property string inviteFutureId: ""
property var successfulInvites: []
property var failedInvites: []
@ -26,12 +26,14 @@ HColumnPopup {
user => ! successfulInvites.includes(user)
)
inviteFuture = py.callClientCoro(
inviteFutureId = py.callClientCoro(
userId,
"room_mass_invite",
[roomId, ...inviteesLeft],
([successes, errors]) => {
inviteFutureId = ""
if (errors.length < 1) {
popup.close()
return
@ -61,10 +63,10 @@ HColumnPopup {
}
onOpened: inviteArea.forceActiveFocus()
onClosed: if (inviteFuture) inviteFuture.cancel()
onClosed: if (inviteFutureId) py.cancelCoro(inviteFutureId)
onInvitingAllowedChanged:
if (! invitingAllowed && inviteFuture) inviteFuture.cancel()
if (! invitingAllowed && inviteFutureId) py.cancelCoro(inviteFutureId)
SummaryLabel {
text: qsTr("Invite members to <i>%1</i>").arg(roomName)

View File

@ -1,26 +0,0 @@
// Copyright Mirage authors & contributors <https://github.com/mirukana/mirage>
// SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12
QtObject {
id: future
property PythonBridge bridge
readonly property QtObject privates: QtObject {
property var pythonFuture: null
property bool cancelPending: false
onPythonFutureChanged: if (cancelPending) future.cancel()
}
function cancel() {
if (! privates.pythonFuture) {
privates.cancelPending = true
return
}
bridge.call(bridge.getattr(privates.pythonFuture, "cancel"))
}
}

View File

@ -11,48 +11,39 @@ Python {
readonly property var pendingCoroutines: Globals.pendingCoroutines
function makeFuture(callback) {
return Qt.createComponent("Future.qml").createObject(py, {bridge: py})
}
function setattr(obj, attr, value, callback=null) {
py.call(py.getattr(obj, "__setattr__"), [attr, value], callback)
}
function callCoro(name, args=[], onSuccess=null, onError=null) {
const uuid = name + "." + CppUtils.uuid()
const future = makeFuture()
Globals.pendingCoroutines[uuid] = {future, onSuccess, onError}
Globals.pendingCoroutines[uuid] = {onSuccess, onError}
Globals.pendingCoroutinesChanged()
// if (name === "models.ensure_exists_from_qml") { print("r"); return}
call("BRIDGE.call_backend_coro", [name, uuid, args], pyFuture => {
future.privates.pythonFuture = pyFuture
})
return future
call("BRIDGE.call_backend_coro", [name, uuid, args])
return uuid
}
function callClientCoro(
accountId, name, args=[], onSuccess=null, onError=null
) {
const future = makeFuture()
callCoro("get_client", [accountId, [name, args]], () => {
const uuid = accountId + "." + name + "." + CppUtils.uuid()
Globals.pendingCoroutines[uuid] = {onSuccess, onError}
Globals.pendingCoroutinesChanged()
const call_args = [accountId, name, uuid, args]
call("BRIDGE.call_client_coro", call_args, pyFuture => {
future.privates.pythonFuture = pyFuture
})
// Ensure the client exists or wait for it to exist
callCoro("get_client", [accountId, [name, args]], () => {
// Now that we're sure it won't error, run that client's function
call("BRIDGE.call_client_coro", [accountId, name, uuid, args])
})
return future
return uuid
}
function cancelCoro(uuid) {
call("BRIDGE.cancel_coro", [uuid])
}
function saveConfig(backend_attribute, data, callback=null) {