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 # 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 - Fix right margin of own `<image url>\n<image url>` messages
- filter > enter > room list is always scrolled to top - filter > enter > room list is always scrolled to top

View File

@ -21,7 +21,7 @@ import traceback
from concurrent.futures import Future from concurrent.futures import Future
from operator import attrgetter from operator import attrgetter
from threading import Thread from threading import Thread
from typing import Coroutine, Sequence from typing import Coroutine, Dict, Sequence
import pyotherside import pyotherside
@ -52,6 +52,8 @@ class QMLBridge:
from .backend import Backend from .backend import Backend
self.backend: Backend = Backend() self.backend: Backend = Backend()
self._running_futures: Dict[str, Future] = {}
Thread(target=self._start_asyncio_loop).start() Thread(target=self._start_asyncio_loop).start()
@ -73,7 +75,7 @@ class QMLBridge:
self._loop.run_forever() 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`.""" """Schedule a coroutine to run in our thread and return a `Future`."""
def on_done(future: Future) -> None: def on_done(future: Future) -> None:
@ -87,27 +89,37 @@ class QMLBridge:
trace = traceback.format_exc().rstrip() trace = traceback.format_exc().rstrip()
CoroutineDone(uuid, result, exception, trace) CoroutineDone(uuid, result, exception, trace)
del self._running_futures[uuid]
future = asyncio.run_coroutine_threadsafe(coro, self._loop) future = asyncio.run_coroutine_threadsafe(coro, self._loop)
self._running_futures[uuid] = future
future.add_done_callback(on_done) future.add_done_callback(on_done)
return future
def call_backend_coro( def call_backend_coro(
self, name: str, uuid: str, args: Sequence[str] = (), self, name: str, uuid: str, args: Sequence[str] = (),
) -> Future: ) -> None:
"""Schedule a `Backend` coroutine and return a `Future`.""" """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( def call_client_coro(
self, user_id: str, name: str, uuid: str, args: Sequence[str] = (), self, user_id: str, name: str, uuid: str, args: Sequence[str] = (),
) -> Future: ) -> None:
"""Schedule a `MatrixClient` coroutine and return a `Future`.""" """Schedule a coroutine from a `QMLBridge.backend.clients` client."""
client = self.backend.clients[user_id] 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: def pdb(self, additional_data: Sequence = ()) -> None:

View File

@ -2,7 +2,6 @@
// SPDX-License-Identifier: LGPL-3.0-or-later // SPDX-License-Identifier: LGPL-3.0-or-later
import QtQuick 2.12 import QtQuick 2.12
import "../PythonBridge"
HImage { HImage {
id: image id: image
@ -18,7 +17,7 @@ HImage {
property bool canUpdate: true property bool canUpdate: true
property bool show: ! canUpdate property bool show: ! canUpdate
property Future getFuture: null property string getFutureId: ""
readonly property bool isMxc: mxc.startsWith("mxc://") 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, w, h, cryptDict] :
[clientUserId, image.mxc, image.title, 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) return
if (image.cachedPath !== path) image.cachedPath = path if (image.cachedPath !== path) image.cachedPath = path
getFutureId = ""
image.broken = Qt.binding(() => image.status === Image.Error) image.broken = Qt.binding(() => image.status === Image.Error)
image.show = image.visible image.show = image.visible
@ -68,5 +68,5 @@ HImage {
onHeightChanged: Qt.callLater(reload) onHeightChanged: Qt.callLater(reload)
onVisibleChanged: Qt.callLater(reload) onVisibleChanged: Qt.callLater(reload)
onMxcChanged: 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 QtQuick.Controls 2.12
import Qt.labs.platform 1.1 import Qt.labs.platform 1.1
import "../Popups" import "../Popups"
import "../PythonBridge"
HFileDialogOpener { HFileDialogOpener {
property string userId: "" property string userId: ""
property Future importFuture: null property string importFutureId: ""
fill: false fill: false
@ -28,13 +27,13 @@ HFileDialogOpener {
const call = py.callClientCoro const call = py.callClientCoro
const path = file.toString().replace(/^file:\/\//, "") const path = file.toString().replace(/^file:\/\//, "")
importFuture = call(userId, "import_keys", [path, pass], () => { importFutureId = call(userId, "import_keys", [path, pass], () => {
importFuture = null importFutureId = ""
callback(true) callback(true)
}, (type, args, error, traceback, uuid) => { }, (type, args, error, traceback, uuid) => {
let unknown = false let unknown = false
importFuture = null importFutureId = ""
callback( callback(
type === "EncryptionError" ? type === "EncryptionError" ?
@ -63,17 +62,17 @@ HFileDialogOpener {
} }
summary.text: summary.text:
importFuture ? importFutureId ?
qsTr("This might take a while...") : qsTr("This might take a while...") :
qsTr("Passphrase used to protect this file:") qsTr("Passphrase used to protect this file:")
validateButton.text: qsTr("Import") validateButton.text: qsTr("Import")
validateButton.icon.name: "import-keys" validateButton.icon.name: "import-keys"
onClosed: if (importFuture) importFuture.cancel() onClosed: if (importFutureId) py.cancelCoro(importFutureId)
Binding on closePolicy { Binding on closePolicy {
value: Popup.CloseOnEscape value: Popup.CloseOnEscape
when: importFuture when: importFutureId
} }
} }
} }

View File

@ -7,13 +7,12 @@ import Clipboard 0.1
import ".." import ".."
import "../Base" import "../Base"
import "../Base/HTile" import "../Base/HTile"
import "../PythonBridge"
HTile { HTile {
id: room id: room
property Future fetchProfilesFuture: null property string fetchProfilesFutureId: ""
property Future loadEventsFuture: null property string loadEventsFutureId: ""
property bool moreToLoad: true property bool moreToLoad: true
readonly property bool joined: ! invited && ! parted readonly property bool joined: ! invited && ! parted
@ -204,8 +203,8 @@ HTile {
} }
Component.onDestruction: { Component.onDestruction: {
if (fetchProfilesFuture) fetchProfilesFuture.cancel() if (fetchProfilesFutureId) py.cancelCoro(fetchProfilesFutureId)
if (loadEventsFuture) loadEventsFuture.cancel() if (loadEventsFutureId) py.cancelCoro(loadEventsFutureId)
} }
Timer { Timer {
@ -217,15 +216,15 @@ HTile {
! lastEvent && ! lastEvent &&
moreToLoad moreToLoad
onTriggered: if (! loadEventsFuture) { onTriggered: if (! loadEventsFutureId) {
loadEventsFuture = py.callClientCoro( loadEventsFutureId = py.callClientCoro(
model.for_account, model.for_account,
"load_past_events", "load_past_events",
[model.id], [model.id],
more => { more => {
if (! room) return // delegate was destroyed if (! room) return // delegate was destroyed
loadEventsFuture = null loadEventsFutureId = ""
moreToLoad = more moreToLoad = more
} }
) )
} }
@ -242,13 +241,13 @@ HTile {
lastEvent.fetch_profile lastEvent.fetch_profile
onTriggered: { onTriggered: {
if (fetchProfilesFuture) fetchProfilesFuture.cancel() if (fetchProfilesFutureId) py.cancelCoro(fetchProfilesFutureId)
fetchProfilesFuture = py.callClientCoro( fetchProfilesFutureId = py.callClientCoro(
model.for_account, model.for_account,
"get_event_profiles", "get_event_profiles",
[model.id, lastEvent.id], [model.id, lastEvent.id],
() => { if (room) fetchProfilesFuture = null }, () => { if (room) fetchProfilesFutureId = "" },
) )
} }
} }

View File

@ -7,7 +7,6 @@ import QtQuick.Layouts 1.12
import "../.." import "../.."
import "../../Base" import "../../Base"
import "../../Base/Buttons" import "../../Base/Buttons"
import "../../PythonBridge"
import "../../ShortcutBundles" import "../../ShortcutBundles"
HColumnPage { HColumnPage {
@ -18,19 +17,19 @@ HColumnPage {
property bool enableFlickShortcuts: property bool enableFlickShortcuts:
SwipeView ? SwipeView.isCurrentItem : true SwipeView ? SwipeView.isCurrentItem : true
property Future loadFuture: null property string loadFutureId: ""
function takeFocus() {} // TODO function takeFocus() {} // TODO
function loadDevices() { function loadDevices() {
loadFuture = py.callClientCoro(userId, "devices_info", [], devices => { loadFutureId = py.callClientCoro(userId, "devices_info",[],devices => {
deviceList.uncheckAll() deviceList.uncheckAll()
deviceList.model.clear() deviceList.model.clear()
for (const device of devices) for (const device of devices)
deviceList.model.append(device) deviceList.model.append(device)
loadFuture = null loadFutureId = ""
deviceList.sectionItemCounts = getSectionItemCounts() deviceList.sectionItemCounts = getSectionItemCounts()
if (page.enabled && ! deviceList.currentItem) if (page.enabled && ! deviceList.currentItem)
@ -187,7 +186,7 @@ HColumnPage {
height: width height: width
source: "../../Base/HBusyIndicator.qml" source: "../../Base/HBusyIndicator.qml"
active: page.loadFuture active: page.loadFutureId
opacity: active ? 1 : 0 opacity: active ? 1 : 0
Behavior on opacity { HNumberAnimation { factor: 2 } } Behavior on opacity { HNumberAnimation { factor: 2 } }

View File

@ -21,26 +21,26 @@ HBox {
property var saveProperties: ["acceptedUserUrl", "knownHttps"] property var saveProperties: ["acceptedUserUrl", "knownHttps"]
property string loadingIconStep: "server-ping-bad" property string loadingIconStep: "server-ping-bad"
property Future connectFuture: null property string connectFutureId: ""
property Future fetchServersFuture: null property string fetchServersFutureId: ""
signal accepted() signal accepted()
function takeFocus() { serverField.item.field.forceActiveFocus() } function takeFocus() { serverField.item.field.forceActiveFocus() }
function fetchServers() { function fetchServers() {
if (fetchServersFuture) fetchServersFuture.cancel() if (fetchServersFutureId) py.cancelCoro(fetchServersFutureId)
fetchServersFuture = py.callCoro("fetch_homeservers", [], () => { fetchServersFutureId = py.callCoro("fetch_homeservers", [], () => {
fetchServersFuture = null fetchServersFutureId = ""
}, (type, args, error, traceback) => { }, (type, args, error, traceback) => {
fetchServersFuture = null fetchServersFutureId = ""
print( traceback) // TODO: display error graphically print( traceback) // TODO: display error graphically
}) })
} }
function connect() { function connect() {
if (connectFuture) connectFuture.cancel() if (connectFutureId) py.cancelCoro(connectFutureId)
connectTimeout.restart() connectTimeout.restart()
const typedUrl = serverField.item.field.cleanText const typedUrl = serverField.item.field.cleanText
@ -49,10 +49,10 @@ HBox {
if (box.knownHttps) if (box.knownHttps)
args[0] = args[0].replace(/^(https?:\/\/)?/, "https://") 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() connectTimeout.stop()
serverField.errorLabel.text = "" serverField.errorLabel.text = ""
connectFuture = null connectFutureId = ""
if (! ( if (! (
flows.includes("m.login.password") || flows.includes("m.login.password") ||
@ -75,7 +75,7 @@ HBox {
console.error(traceback) console.error(traceback)
connectTimeout.stop() connectTimeout.stop()
connectFuture = null connectFutureId = ""
let text = qsTr("Unexpected error: %1 [%2]").arg(type).arg(args) let text = qsTr("Unexpected error: %1 [%2]").arg(type).arg(args)
@ -194,7 +194,7 @@ HBox {
enabled: field.cleanText && ! field.error enabled: field.cleanText && ! field.error
icon.name: "server-connect-to-address" icon.name: "server-connect-to-address"
icon.color: theme.colors.positiveBackground icon.color: theme.colors.positiveBackground
loading: box.connectFuture !== null loading: box.connectFutureId !== ""
disableWhileLoading: false disableWhileLoading: false
onClicked: box.connect() onClicked: box.connect()
@ -209,7 +209,7 @@ HBox {
onAccepted: window.saveState(this) onAccepted: window.saveState(this)
Component.onDestruction: Component.onDestruction:
if (fetchServersFuture) fetchServersFuture.cancel() if (fetchServersFutureId) py.cancelCoro(fetchServersFutureId)
Timer { Timer {
id: connectTimeout id: connectTimeout
@ -230,7 +230,7 @@ HBox {
Timer { Timer {
interval: 1000 interval: 1000
running: running:
fetchServersFuture === null && fetchServersFutureId === "" &&
ModelStore.get("homeservers").count === 0 ModelStore.get("homeservers").count === 0
repeat: true repeat: true
@ -292,7 +292,7 @@ HBox {
height: width height: width
source: "../../Base/HBusyIndicator.qml" source: "../../Base/HBusyIndicator.qml"
active: box.fetchServersFuture && ! serverList.count active: box.fetchServersFutureId && ! serverList.count
opacity: active ? 1 : 0 opacity: active ? 1 : 0
Behavior on opacity { HNumberAnimation { factor: 2 } } Behavior on opacity { HNumberAnimation { factor: 2 } }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,7 @@ HColumnPopup {
property string roomName property string roomName
property bool invitingAllowed: true property bool invitingAllowed: true
property var inviteFuture: null property string inviteFutureId: ""
property var successfulInvites: [] property var successfulInvites: []
property var failedInvites: [] property var failedInvites: []
@ -26,12 +26,14 @@ HColumnPopup {
user => ! successfulInvites.includes(user) user => ! successfulInvites.includes(user)
) )
inviteFuture = py.callClientCoro( inviteFutureId = py.callClientCoro(
userId, userId,
"room_mass_invite", "room_mass_invite",
[roomId, ...inviteesLeft], [roomId, ...inviteesLeft],
([successes, errors]) => { ([successes, errors]) => {
inviteFutureId = ""
if (errors.length < 1) { if (errors.length < 1) {
popup.close() popup.close()
return return
@ -61,10 +63,10 @@ HColumnPopup {
} }
onOpened: inviteArea.forceActiveFocus() onOpened: inviteArea.forceActiveFocus()
onClosed: if (inviteFuture) inviteFuture.cancel() onClosed: if (inviteFutureId) py.cancelCoro(inviteFutureId)
onInvitingAllowedChanged: onInvitingAllowedChanged:
if (! invitingAllowed && inviteFuture) inviteFuture.cancel() if (! invitingAllowed && inviteFutureId) py.cancelCoro(inviteFutureId)
SummaryLabel { SummaryLabel {
text: qsTr("Invite members to <i>%1</i>").arg(roomName) 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 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) { function setattr(obj, attr, value, callback=null) {
py.call(py.getattr(obj, "__setattr__"), [attr, value], callback) py.call(py.getattr(obj, "__setattr__"), [attr, value], callback)
} }
function callCoro(name, args=[], onSuccess=null, onError=null) { function callCoro(name, args=[], onSuccess=null, onError=null) {
const uuid = name + "." + CppUtils.uuid() const uuid = name + "." + CppUtils.uuid()
const future = makeFuture()
Globals.pendingCoroutines[uuid] = {future, onSuccess, onError} Globals.pendingCoroutines[uuid] = {onSuccess, onError}
Globals.pendingCoroutinesChanged() Globals.pendingCoroutinesChanged()
// if (name === "models.ensure_exists_from_qml") { print("r"); return}
call("BRIDGE.call_backend_coro", [name, uuid, args], pyFuture => { call("BRIDGE.call_backend_coro", [name, uuid, args])
future.privates.pythonFuture = pyFuture return uuid
})
return future
} }
function callClientCoro( function callClientCoro(
accountId, name, args=[], onSuccess=null, onError=null accountId, name, args=[], onSuccess=null, onError=null
) { ) {
const future = makeFuture() const uuid = accountId + "." + name + "." + CppUtils.uuid()
Globals.pendingCoroutines[uuid] = {onSuccess, onError}
Globals.pendingCoroutinesChanged()
// Ensure the client exists or wait for it to exist
callCoro("get_client", [accountId, [name, args]], () => { callCoro("get_client", [accountId, [name, args]], () => {
const uuid = accountId + "." + name + "." + CppUtils.uuid() // Now that we're sure it won't error, run that client's function
call("BRIDGE.call_client_coro", [accountId, name, uuid, args])
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
})
}) })
return future return uuid
}
function cancelCoro(uuid) {
call("BRIDGE.cancel_coro", [uuid])
} }
function saveConfig(backend_attribute, data, callback=null) { function saveConfig(backend_attribute, data, callback=null) {