From 97d1c3200b3914d78494e78d529653dbb896ebad Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Wed, 13 May 2020 16:14:21 +0200 Subject: [PATCH 01/20] Remove unused aliases from SyncState.qml CURA-7290 --- resources/qml/Account/SyncState.qml | 26 ++++++++++-------------- resources/qml/Account/UserOperations.qml | 7 +------ 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/resources/qml/Account/SyncState.qml b/resources/qml/Account/SyncState.qml index 7126aec314..4457ece465 100644 --- a/resources/qml/Account/SyncState.qml +++ b/resources/qml/Account/SyncState.qml @@ -7,11 +7,7 @@ import Cura 1.1 as Cura Row // sync state icon + message { - property alias iconSource: icon.source - property alias labelText: stateLabel.text - property alias syncButtonVisible: accountSyncButton.visible - property alias animateIconRotation: updateAnimator.running - + id: syncRow width: childrenRect.width height: childrenRect.height anchors.horizontalCenter: parent.horizontalCenter @@ -83,24 +79,24 @@ Row // sync state icon + message onSyncStateChanged: { if(newState == Cura.AccountSyncState.SYNCING){ - syncRow.iconSource = UM.Theme.getIcon("update") - syncRow.labelText = catalog.i18nc("@label", "Checking...") + icon.source = UM.Theme.getIcon("update") + stateLabel.text = catalog.i18nc("@label", "Checking...") } else if (newState == Cura.AccountSyncState.SUCCESS) { - syncRow.iconSource = UM.Theme.getIcon("checked") - syncRow.labelText = catalog.i18nc("@label", "You are up to date") + icon.source = UM.Theme.getIcon("checked") + stateLabel.text = catalog.i18nc("@label", "You are up to date") } else if (newState == Cura.AccountSyncState.ERROR) { - syncRow.iconSource = UM.Theme.getIcon("warning_light") - syncRow.labelText = catalog.i18nc("@label", "Something went wrong...") + icon.source = UM.Theme.getIcon("warning_light") + stateLabel.text = catalog.i18nc("@label", "Something went wrong...") } else { print("Error: unexpected sync state: " + newState) } if(newState == Cura.AccountSyncState.SYNCING){ - syncRow.animateIconRotation = true - syncRow.syncButtonVisible = false + updateAnimator.running = true + accountSyncButton.visible = false } else { - syncRow.animateIconRotation = false - syncRow.syncButtonVisible = true + updateAnimator.running = false + accountSyncButton.visible = true } } diff --git a/resources/qml/Account/UserOperations.qml b/resources/qml/Account/UserOperations.qml index f292c501f3..c8e3b81d08 100644 --- a/resources/qml/Account/UserOperations.qml +++ b/resources/qml/Account/UserOperations.qml @@ -29,12 +29,7 @@ Column color: UM.Theme.getColor("text") } - SyncState - { - id: syncRow - } - - + SyncState {} Label { From de0ef8ae62216d33408bca50d28ae2149c148b44 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Wed, 13 May 2020 16:15:29 +0200 Subject: [PATCH 02/20] Change account sync date format to mm/dd/YYYY CURA-7290 --- cura/API/Account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/API/Account.py b/cura/API/Account.py index d1fda63d2b..872fe815da 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -121,7 +121,7 @@ class Account(QObject): self.syncStateChanged.emit(self._sync_state) if self._sync_state == SyncState.SUCCESS: - self._last_sync_str = datetime.now().strftime("%d/%m/%Y %H:%M") + self._last_sync_str = datetime.now().strftime("%m/%d/%Y %H:%M") self.lastSyncDateTimeChanged.emit() if self._sync_state != SyncState.SYNCING: From 6caa0360b9783e50b7ab6ace006cde6cde6cb4e0 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Wed, 13 May 2020 16:35:39 +0200 Subject: [PATCH 03/20] Change SyncRow copy CURA-7290 --- resources/qml/Account/SyncState.qml | 4 ++-- resources/qml/Account/UserOperations.qml | 9 +++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/resources/qml/Account/SyncState.qml b/resources/qml/Account/SyncState.qml index 4457ece465..3f96ddad1f 100644 --- a/resources/qml/Account/SyncState.qml +++ b/resources/qml/Account/SyncState.qml @@ -50,7 +50,7 @@ Row // sync state icon + message Label { id: stateLabel - text: catalog.i18nc("@state", "Checking...") + text: catalog.i18nc("@state", catalog.i18nc("@label", "You are in sync with your account")) color: UM.Theme.getColor("text") font: UM.Theme.getFont("medium") renderType: Text.NativeRendering @@ -83,7 +83,7 @@ Row // sync state icon + message stateLabel.text = catalog.i18nc("@label", "Checking...") } else if (newState == Cura.AccountSyncState.SUCCESS) { icon.source = UM.Theme.getIcon("checked") - stateLabel.text = catalog.i18nc("@label", "You are up to date") + stateLabel.text = catalog.i18nc("@label", "You are in sync with your account") } else if (newState == Cura.AccountSyncState.ERROR) { icon.source = UM.Theme.getIcon("warning_light") stateLabel.text = catalog.i18nc("@label", "Something went wrong...") diff --git a/resources/qml/Account/UserOperations.qml b/resources/qml/Account/UserOperations.qml index c8e3b81d08..7578f34623 100644 --- a/resources/qml/Account/UserOperations.qml +++ b/resources/qml/Account/UserOperations.qml @@ -9,7 +9,10 @@ import Cura 1.1 as Cura Column { - width: Math.max(title.width, accountButton.width) + 2 * UM.Theme.getSize("default_margin").width + width: Match.max( + Math.max(title.width, accountButton.width) + 2 * UM.Theme.getSize("default_margin").width, + syncRow.width + ) spacing: UM.Theme.getSize("default_margin").height @@ -29,7 +32,9 @@ Column color: UM.Theme.getColor("text") } - SyncState {} + SyncState { + id: syncRow + } Label { From 72657f15bed9fb2c6deb56db772f315668d6a217 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Wed, 13 May 2020 16:38:42 +0200 Subject: [PATCH 04/20] Add padding to checked icon CURA-7290 --- resources/themes/cura-light/icons/checked.svg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/themes/cura-light/icons/checked.svg b/resources/themes/cura-light/icons/checked.svg index e98e2abcd7..22d1278667 100644 --- a/resources/themes/cura-light/icons/checked.svg +++ b/resources/themes/cura-light/icons/checked.svg @@ -4,9 +4,9 @@ checked Created with Sketch. - - - + + + \ No newline at end of file From f78fa884c195eaebb9420053845bbac289d02b13 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Wed, 13 May 2020 17:19:20 +0200 Subject: [PATCH 05/20] Only show the manual sync button after the account popup was closed CURA-7290 --- cura/API/Account.py | 43 ++++++++++++++++++------- resources/qml/Account/AccountWidget.qml | 10 +++++- resources/qml/Account/SyncState.qml | 5 ++- 3 files changed, 42 insertions(+), 16 deletions(-) diff --git a/cura/API/Account.py b/cura/API/Account.py index 872fe815da..3463fe6951 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -50,6 +50,7 @@ class Account(QObject): """ lastSyncDateTimeChanged = pyqtSignal() syncStateChanged = pyqtSignal(int) # because SyncState is an int Enum + manualSyncEnabledChanged = pyqtSignal(bool) def __init__(self, application: "CuraApplication", parent = None) -> None: super().__init__(parent) @@ -59,6 +60,7 @@ class Account(QObject): self._error_message = None # type: Optional[Message] self._logged_in = False self._sync_state = SyncState.SUCCESS + self._manual_sync_enabled = False self._last_sync_str = "-" self._callback_port = 32118 @@ -157,11 +159,25 @@ class Account(QObject): self._logged_in = logged_in self.loginStateChanged.emit(logged_in) if logged_in: - self.sync() + self._sync() else: if self._update_timer.isActive(): self._update_timer.stop() + def _sync(self) -> None: + """Signals all sync services to start syncing + + This can be considered a forced sync: even when a + sync is currently running, a sync will be requested. + """ + + if self._update_timer.isActive(): + self._update_timer.stop() + elif self._sync_state == SyncState.SYNCING: + Logger.warning("Starting a new sync while previous sync was not completed\n{}", str(self._sync_services)) + + self.syncRequested.emit() + @pyqtSlot() def login(self) -> None: if self._logged_in: @@ -200,20 +216,23 @@ class Account(QObject): def lastSyncDateTime(self) -> str: return self._last_sync_str + @pyqtProperty(bool, notify=manualSyncEnabledChanged) + def manualSyncEnabled(self) -> bool: + return self._manual_sync_enabled + @pyqtSlot() - def sync(self) -> None: - """Signals all sync services to start syncing + @pyqtSlot(bool) + def sync(self, user_initiated=False): + if user_initiated: + self._manual_sync_enabled = False + self.manualSyncEnabledChanged.emit(self._manual_sync_enabled) - This can be considered a forced sync: even when a - sync is currently running, a sync will be requested. - """ + self._sync() - if self._update_timer.isActive(): - self._update_timer.stop() - elif self._sync_state == SyncState.SYNCING: - Logger.warning("Starting a new sync while previous sync was not completed\n{}", str(self._sync_services)) - - self.syncRequested.emit() + @pyqtSlot() + def popupClosed(self): + self._manual_sync_enabled = True + self.manualSyncEnabledChanged.emit(self._manual_sync_enabled) @pyqtSlot() def logout(self) -> None: diff --git a/resources/qml/Account/AccountWidget.qml b/resources/qml/Account/AccountWidget.qml index 26b491ce15..00ac954f22 100644 --- a/resources/qml/Account/AccountWidget.qml +++ b/resources/qml/Account/AccountWidget.qml @@ -108,7 +108,15 @@ Item } } - onClicked: popup.opened ? popup.close() : popup.open() + onClicked: { + if (popup.opened) + { + popup.close() + Cura.API.account.popupClosed() + } else { + popup.open() + } + } } Popup diff --git a/resources/qml/Account/SyncState.qml b/resources/qml/Account/SyncState.qml index 3f96ddad1f..b419f150f9 100644 --- a/resources/qml/Account/SyncState.qml +++ b/resources/qml/Account/SyncState.qml @@ -63,11 +63,12 @@ Row // sync state icon + message color: UM.Theme.getColor("secondary_button_text") font: UM.Theme.getFont("medium") renderType: Text.NativeRendering + visible: Cura.API.account.manualSyncEnabled MouseArea { anchors.fill: parent - onClicked: Cura.API.account.sync() + onClicked: Cura.API.account.sync(true) hoverEnabled: true onEntered: accountSyncButton.font.underline = true onExited: accountSyncButton.font.underline = false @@ -93,10 +94,8 @@ Row // sync state icon + message if(newState == Cura.AccountSyncState.SYNCING){ updateAnimator.running = true - accountSyncButton.visible = false } else { updateAnimator.running = false - accountSyncButton.visible = true } } From 59b40c72f080dcc0e29b323245db1047592b9f45 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Wed, 13 May 2020 17:32:57 +0200 Subject: [PATCH 06/20] Additional scenarios for enabling/disabling the manual sync button CURA-7290 --- cura/API/Account.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cura/API/Account.py b/cura/API/Account.py index 3463fe6951..1c12bdc1be 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -116,6 +116,8 @@ class Account(QObject): self._sync_state = SyncState.SYNCING elif any(val == SyncState.ERROR for val in self._sync_services.values()): self._sync_state = SyncState.ERROR + self._manual_sync_enabled = True + self.manualSyncEnabledChanged.emit(self._manual_sync_enabled) else: self._sync_state = SyncState.SUCCESS @@ -159,6 +161,8 @@ class Account(QObject): self._logged_in = logged_in self.loginStateChanged.emit(logged_in) if logged_in: + self._manual_sync_enabled = False + self.manualSyncEnabledChanged.emit(self._manual_sync_enabled) self._sync() else: if self._update_timer.isActive(): From e6639eb8ebe69892047fefbb7a5cce7c7fca5b01 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Wed, 13 May 2020 17:36:24 +0200 Subject: [PATCH 07/20] Do not reserve height for manual sync button CURA-7290 --- resources/qml/Account/SyncState.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/qml/Account/SyncState.qml b/resources/qml/Account/SyncState.qml index b419f150f9..eb71e81ecc 100644 --- a/resources/qml/Account/SyncState.qml +++ b/resources/qml/Account/SyncState.qml @@ -64,6 +64,7 @@ Row // sync state icon + message font: UM.Theme.getFont("medium") renderType: Text.NativeRendering visible: Cura.API.account.manualSyncEnabled + height: visible ? accountSyncButton.intrinsicHeight : 0 MouseArea { From b6b6a3998946f9c8a24eecf4a9d1719f2c93f9ff Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 14 May 2020 15:13:57 +0200 Subject: [PATCH 08/20] Fix QML typo CURA-7290 --- resources/qml/Account/UserOperations.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/qml/Account/UserOperations.qml b/resources/qml/Account/UserOperations.qml index 7578f34623..c0f33c74cd 100644 --- a/resources/qml/Account/UserOperations.qml +++ b/resources/qml/Account/UserOperations.qml @@ -9,7 +9,7 @@ import Cura 1.1 as Cura Column { - width: Match.max( + width: Math.max( Math.max(title.width, accountButton.width) + 2 * UM.Theme.getSize("default_margin").width, syncRow.width ) From a3f968188fa8d7c8d6f84921769dee240495619f Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 14 May 2020 15:14:51 +0200 Subject: [PATCH 09/20] Add timeout to CloudPackageChecker request Prevents it from getting stuck in the SYNCING state CURA-7290 --- plugins/Toolbox/src/CloudSync/CloudPackageChecker.py | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py index 9c372096af..ef8e82f576 100644 --- a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py +++ b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py @@ -67,6 +67,7 @@ class CloudPackageChecker(QObject): self._application.getHttpRequestManager().get(url, callback = self._onUserPackagesRequestFinished, error_callback = self._onUserPackagesRequestFinished, + timeout=10, scope = self._scope) def _onUserPackagesRequestFinished(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None: From a9692e3d2776967efa1149f514e5e355a75316fc Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 14 May 2020 15:15:16 +0200 Subject: [PATCH 10/20] Refactor setManualSyncEnabled CURA-7290 --- cura/API/Account.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/cura/API/Account.py b/cura/API/Account.py index 1c12bdc1be..2bfbf41f53 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -116,8 +116,7 @@ class Account(QObject): self._sync_state = SyncState.SYNCING elif any(val == SyncState.ERROR for val in self._sync_services.values()): self._sync_state = SyncState.ERROR - self._manual_sync_enabled = True - self.manualSyncEnabledChanged.emit(self._manual_sync_enabled) + self._setManualSyncEnabled(True) else: self._sync_state = SyncState.SUCCESS @@ -161,8 +160,7 @@ class Account(QObject): self._logged_in = logged_in self.loginStateChanged.emit(logged_in) if logged_in: - self._manual_sync_enabled = False - self.manualSyncEnabledChanged.emit(self._manual_sync_enabled) + self._setManualSyncEnabled(False) self._sync() else: if self._update_timer.isActive(): @@ -182,6 +180,11 @@ class Account(QObject): self.syncRequested.emit() + def _setManualSyncEnabled(self, enabled: bool) -> None: + if self._manual_sync_enabled != enabled: + self._manual_sync_enabled = enabled + self.manualSyncEnabledChanged.emit(enabled) + @pyqtSlot() def login(self) -> None: if self._logged_in: @@ -228,15 +231,13 @@ class Account(QObject): @pyqtSlot(bool) def sync(self, user_initiated=False): if user_initiated: - self._manual_sync_enabled = False - self.manualSyncEnabledChanged.emit(self._manual_sync_enabled) + self._setManualSyncEnabled(False) self._sync() @pyqtSlot() def popupClosed(self): - self._manual_sync_enabled = True - self.manualSyncEnabledChanged.emit(self._manual_sync_enabled) + self._setManualSyncEnabled(True) @pyqtSlot() def logout(self) -> None: From 15f813a4ffd7a4da59b386428d2393d12fec8bf9 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Thu, 14 May 2020 15:47:45 +0200 Subject: [PATCH 11/20] Add a sync timeout to CloudOutputDeviceManager Fixes an issue where printer syncing breaks when switching networks etc. CURA-7290 --- .../src/Cloud/CloudOutputDeviceManager.py | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index f233e59fe5..8a87438b0e 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -26,6 +26,9 @@ class CloudOutputDeviceManager: API spec is available on https://api.ultimaker.com/docs/connect/spec/. """ + SYNC_TIMEOUT = 50.0 + """seconds. Adding many different kinds of printers can take a long time""" + META_CLUSTER_ID = "um_cloud_cluster_id" META_NETWORK_KEY = "um_network_key" SYNC_SERVICE_NAME = "CloudOutputDeviceManager" @@ -46,7 +49,13 @@ class CloudOutputDeviceManager: # Ensure we don't start twice. self._running = False - self._syncing = False + # Unfortunately, not all cases of a failed request result in an error callback, such as VPN connection + # being broken or possibly switching wifi networks. Better solution: Refactor CloudApiClient to use + # HttpRequestManager, which supports timeout. + self._sync_timeout_timer = QTimer() + self._sync_timeout_timer.setInterval(int(self.SYNC_TIMEOUT * 1000)) + self._sync_timeout_timer.setSingleShot(True) + self._sync_timeout_timer.timeout.connect(self._onSyncTimeout) def start(self): """Starts running the cloud output device manager, thus periodically requesting cloud data.""" @@ -84,12 +93,13 @@ class CloudOutputDeviceManager: def _getRemoteClusters(self) -> None: """Gets all remote clusters from the API.""" - if self._syncing: + if self._sync_timeout_timer.isActive(): + # A sync is running return Logger.info("Syncing cloud printer clusters") - self._syncing = True + self._sync_timeout_timer.start() self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SYNCING) self._api.getClusters(self._onGetRemoteClustersFinished, self._onGetRemoteClusterFailed) @@ -115,12 +125,10 @@ class CloudOutputDeviceManager: # If the removed device was active we should connect to the new active device self._connectToActiveMachine() - self._syncing = False - self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS) + self._onSyncFinished(True) def _onGetRemoteClusterFailed(self): - self._syncing = False - self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) + self._onSyncFinished(False) def _onDevicesDiscovered(self, clusters: List[CloudClusterResponse]) -> None: """**Synchronously** create machines for discovered devices @@ -223,6 +231,19 @@ class CloudOutputDeviceManager: if device.key in output_device_manager.getOutputDeviceIds(): output_device_manager.removeOutputDevice(device.key) + def _onSyncTimeout(self): + Logger.warning("Cloud printer sync timed out after {} seconds".format(self.SYNC_TIMEOUT)) + self._onSyncFinished(False) + + def _onSyncFinished(self, success: bool): + if self._sync_timeout_timer.isActive(): + self._sync_timeout_timer.stop() + + if success: + self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS) + else: + self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) + def _createMachineFromDiscoveredDevice(self, key: str, activate: bool = True) -> None: device = self._remote_clusters[key] if not device: @@ -277,4 +298,4 @@ class CloudOutputDeviceManager: output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() if device.key not in output_device_manager.getOutputDeviceIds(): - output_device_manager.addOutputDevice(device) \ No newline at end of file + output_device_manager.addOutputDevice(device) From f3c66c31898a4167def7b2b89860e5495621b84c Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 15 May 2020 11:28:17 +0200 Subject: [PATCH 12/20] Refactor CloudApiClient (and ToolpathUploader) to use HttpRequestManager Has the benefit of a more unified Http request management + timeouts CURA-7290 --- .../src/Cloud/CloudApiClient.py | 72 ++++++++++++------- .../src/Cloud/CloudOutputDeviceManager.py | 5 +- .../src/Cloud/ToolPathUploader.py | 55 +++++++------- 3 files changed, 74 insertions(+), 58 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 1c9670d87f..b45f549b3c 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -6,11 +6,15 @@ from time import time from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any, cast from PyQt5.QtCore import QUrl -from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager +from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from UM.Logger import Logger +from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope from cura.API import Account +from cura.CuraApplication import CuraApplication from cura.UltimakerCloud import UltimakerCloudAuthentication +from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope from .ToolPathUploader import ToolPathUploader from ..Models.BaseModel import BaseModel from ..Models.Http.CloudClusterResponse import CloudClusterResponse @@ -33,16 +37,20 @@ class CloudApiClient: CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH) CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH) + DEFAULT_REQUEST_TIMEOUT = 10 # seconds + # In order to avoid garbage collection we keep the callbacks in this list. - _anti_gc_callbacks = [] # type: List[Callable[[], None]] + _anti_gc_callbacks = [] # type: List[Callable[[Any], None]] ## Initializes a new cloud API client. # \param account: The user's account object # \param on_error: The callback to be called whenever we receive errors from the server. - def __init__(self, account: Account, on_error: Callable[[List[CloudError]], None]) -> None: + def __init__(self, app: CuraApplication, on_error: Callable[[List[CloudError]], None]) -> None: super().__init__() - self._manager = QNetworkAccessManager() - self._account = account + self._app = app + self._account = app.getCuraAPI().account + self._scope = JsonDecoratorScope(UltimakerCloudScope(app)) + self._http = HttpRequestManager.getInstance() self._on_error = on_error self._upload = None # type: Optional[ToolPathUploader] @@ -55,16 +63,21 @@ class CloudApiClient: # \param on_finished: The function to be called after the result is parsed. def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any], failed: Callable) -> None: url = "{}/clusters?status=active".format(self.CLUSTER_API_ROOT) - reply = self._manager.get(self._createEmptyRequest(url)) - self._addCallback(reply, on_finished, CloudClusterResponse, failed) + self._http.get(url, + scope = self._scope, + callback = self._parseCallback(on_finished, CloudClusterResponse, failed), + error_callback = failed, + timeout = self.DEFAULT_REQUEST_TIMEOUT) ## Retrieves the status of the given cluster. # \param cluster_id: The ID of the cluster. # \param on_finished: The function to be called after the result is parsed. def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None: url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id) - reply = self._manager.get(self._createEmptyRequest(url)) - self._addCallback(reply, on_finished, CloudClusterStatus) + self._http.get(url, + scope = self._scope, + callback = self._parseCallback(on_finished, CloudClusterStatus), + timeout = self.DEFAULT_REQUEST_TIMEOUT) ## Requests the cloud to register the upload of a print job mesh. # \param request: The request object. @@ -72,9 +85,13 @@ class CloudApiClient: def requestUpload(self, request: CloudPrintJobUploadRequest, on_finished: Callable[[CloudPrintJobResponse], Any]) -> None: url = "{}/jobs/upload".format(self.CURA_API_ROOT) - body = json.dumps({"data": request.toDict()}) - reply = self._manager.put(self._createEmptyRequest(url), body.encode()) - self._addCallback(reply, on_finished, CloudPrintJobResponse) + data = json.dumps({"data": request.toDict()}).encode() + + self._http.put(url, + scope = self._scope, + data = data, + callback = self._parseCallback(on_finished, CloudPrintJobResponse), + timeout = self.DEFAULT_REQUEST_TIMEOUT) ## Uploads a print job tool path to the cloud. # \param print_job: The object received after requesting an upload with `self.requestUpload`. @@ -84,7 +101,7 @@ class CloudApiClient: # \param on_error: A function to be called if the upload fails. def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any]): - self._upload = ToolPathUploader(self._manager, print_job, mesh, on_finished, on_progress, on_error) + self._upload = ToolPathUploader(self._http, print_job, mesh, on_finished, on_progress, on_error) self._upload.start() # Requests a cluster to print the given print job. @@ -93,8 +110,11 @@ class CloudApiClient: # \param on_finished: The function to be called after the result is parsed. def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any]) -> None: url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id) - reply = self._manager.post(self._createEmptyRequest(url), b"") - self._addCallback(reply, on_finished, CloudPrintResponse) + self._http.post(url, + scope = self._scope, + data = b"", + callback = self._parseCallback(on_finished, CloudPrintResponse), + timeout = self.DEFAULT_REQUEST_TIMEOUT) ## Send a print job action to the cluster for the given print job. # \param cluster_id: The ID of the cluster. @@ -104,7 +124,10 @@ class CloudApiClient: data: Optional[Dict[str, Any]] = None) -> None: body = json.dumps({"data": data}).encode() if data else b"" url = "{}/clusters/{}/print_jobs/{}/action/{}".format(self.CLUSTER_API_ROOT, cluster_id, cluster_job_id, action) - self._manager.post(self._createEmptyRequest(url), body) + self._http.post(url, + scope = self._scope, + data = body, + timeout = self.DEFAULT_REQUEST_TIMEOUT) ## We override _createEmptyRequest in order to add the user credentials. # \param url: The URL to request @@ -162,13 +185,12 @@ class CloudApiClient: # \param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either # a list or a single item. # \param model: The type of the model to convert the response to. - def _addCallback(self, - reply: QNetworkReply, - on_finished: Union[Callable[[CloudApiClientModel], Any], - Callable[[List[CloudApiClientModel]], Any]], - model: Type[CloudApiClientModel], - on_error: Optional[Callable] = None) -> None: - def parse() -> None: + def _parseCallback(self, + on_finished: Union[Callable[[CloudApiClientModel], Any], + Callable[[List[CloudApiClientModel]], Any]], + model: Type[CloudApiClientModel], + on_error: Optional[Callable] = None) -> Callable[[QNetworkReply], None]: + def parse(reply: QNetworkReply) -> None: self._anti_gc_callbacks.remove(parse) # Don't try to parse the reply if we didn't get one @@ -184,6 +206,4 @@ class CloudApiClient: self._parseModels(response, on_finished, model) self._anti_gc_callbacks.append(parse) - reply.finished.connect(parse) - if on_error is not None: - reply.error.connect(on_error) + return parse diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 8a87438b0e..2d941281c7 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -4,6 +4,7 @@ import os from typing import Dict, List, Optional from PyQt5.QtCore import QTimer +from PyQt5.QtNetwork import QNetworkReply from UM import i18nCatalog from UM.Logger import Logger # To log errors talking to the API. @@ -43,7 +44,7 @@ class CloudOutputDeviceManager: # Persistent dict containing the remote clusters for the authenticated user. self._remote_clusters = {} # type: Dict[str, CloudOutputDevice] self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account - self._api = CloudApiClient(self._account, on_error = lambda error: Logger.log("e", str(error))) + self._api = CloudApiClient(CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error))) self._account.loginStateChanged.connect(self._onLoginStateChanged) # Ensure we don't start twice. @@ -127,7 +128,7 @@ class CloudOutputDeviceManager: self._onSyncFinished(True) - def _onGetRemoteClusterFailed(self): + def _onGetRemoteClusterFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError): self._onSyncFinished(False) def _onDevicesDiscovered(self, clusters: List[CloudClusterResponse]) -> None: diff --git a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py index 6aa341c0e5..6317dff347 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py @@ -1,11 +1,11 @@ # Copyright (c) 2019 Ultimaker B.V. # !/usr/bin/env python # -*- coding: utf-8 -*- -from PyQt5.QtCore import QUrl -from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager -from typing import Optional, Callable, Any, Tuple, cast +from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply +from typing import Callable, Any, Tuple from UM.Logger import Logger +from UM.TaskManagement.HttpRequestManager import HttpRequestManager from ..Models.Http.CloudPrintJobResponse import CloudPrintJobResponse @@ -23,16 +23,16 @@ class ToolPathUploader: BYTES_PER_REQUEST = 256 * 1024 ## Creates a mesh upload object. - # \param manager: The network access manager that will handle the HTTP requests. + # \param http: The HttpRequestManager that will handle the HTTP requests. # \param print_job: The print job response that was returned by the cloud after registering the upload. # \param data: The mesh bytes to be uploaded. # \param on_finished: The method to be called when done. # \param on_progress: The method to be called when the progress changes (receives a percentage 0-100). # \param on_error: The method to be called when an error occurs. - def __init__(self, manager: QNetworkAccessManager, print_job: CloudPrintJobResponse, data: bytes, + def __init__(self, http: HttpRequestManager, print_job: CloudPrintJobResponse, data: bytes, on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any] ) -> None: - self._manager = manager + self._http = http self._print_job = print_job self._data = data @@ -43,25 +43,12 @@ class ToolPathUploader: self._sent_bytes = 0 self._retries = 0 self._finished = False - self._reply = None # type: Optional[QNetworkReply] ## Returns the print job for which this object was created. @property def printJob(self): return self._print_job - ## Creates a network request to the print job upload URL, adding the needed content range header. - def _createRequest(self) -> QNetworkRequest: - request = QNetworkRequest(QUrl(self._print_job.upload_url)) - request.setHeader(QNetworkRequest.ContentTypeHeader, self._print_job.content_type) - - first_byte, last_byte = self._chunkRange() - content_range = "bytes {}-{}/{}".format(first_byte, last_byte - 1, len(self._data)) - request.setRawHeader(b"Content-Range", content_range.encode()) - Logger.log("i", "Uploading %s to %s", content_range, self._print_job.upload_url) - - return request - ## Determines the bytes that should be uploaded next. # \return: A tuple with the first and the last byte to upload. def _chunkRange(self) -> Tuple[int, int]: @@ -88,13 +75,23 @@ class ToolPathUploader: raise ValueError("The upload is already finished") first_byte, last_byte = self._chunkRange() - request = self._createRequest() + content_range = "bytes {}-{}/{}".format(first_byte, last_byte - 1, len(self._data)) - # now send the reply and subscribe to the results - self._reply = self._manager.put(request, self._data[first_byte:last_byte]) - self._reply.finished.connect(self._finishedCallback) - self._reply.uploadProgress.connect(self._progressCallback) - self._reply.error.connect(self._errorCallback) + headers = { + "Content-Type": self._print_job.content_type, + "Content-Range": content_range + } + + Logger.log("i", "Uploading %s to %s", content_range, self._print_job.upload_url) + + self._http.put( + url = self._print_job.upload_url, + headers_dict = headers, + data = self._data[first_byte:last_byte], + callback = self._finishedCallback, + error_callback = self._errorCallback, + upload_progress_callback = self._progressCallback + ) ## Handles an update to the upload progress # \param bytes_sent: The amount of bytes sent in the current request. @@ -106,16 +103,14 @@ class ToolPathUploader: self._on_progress(int(total_sent / len(self._data) * 100)) ## Handles an error uploading. - def _errorCallback(self) -> None: - reply = cast(QNetworkReply, self._reply) + def _errorCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None: body = bytes(reply.readAll()).decode() Logger.log("e", "Received error while uploading: %s", body) self.stop() self._on_error() ## Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed. - def _finishedCallback(self) -> None: - reply = cast(QNetworkReply, self._reply) + def _finishedCallback(self, reply: QNetworkReply) -> None: Logger.log("i", "Finished callback %s %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString()) @@ -133,7 +128,7 @@ class ToolPathUploader: # Http codes that are not to be retried are assumed to be errors. if status_code > 308: - self._errorCallback() + self._errorCallback(reply, None) return Logger.log("d", "status_code: %s, Headers: %s, body: %s", status_code, From 9acf8b6122148897119f70da64f4245d93345562 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 15 May 2020 11:32:49 +0200 Subject: [PATCH 13/20] Revert "Add a sync timeout to CloudOutputDeviceManager" Stopgap solution is not necessary anymore after CloudOutputDeviceManager after implementing timeouts on the http-level. This reverts commit 15f813a4 --- .../src/Cloud/CloudOutputDeviceManager.py | 37 ++++--------------- 1 file changed, 8 insertions(+), 29 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 2d941281c7..25ee4cf5e5 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -27,9 +27,6 @@ class CloudOutputDeviceManager: API spec is available on https://api.ultimaker.com/docs/connect/spec/. """ - SYNC_TIMEOUT = 50.0 - """seconds. Adding many different kinds of printers can take a long time""" - META_CLUSTER_ID = "um_cloud_cluster_id" META_NETWORK_KEY = "um_network_key" SYNC_SERVICE_NAME = "CloudOutputDeviceManager" @@ -50,13 +47,7 @@ class CloudOutputDeviceManager: # Ensure we don't start twice. self._running = False - # Unfortunately, not all cases of a failed request result in an error callback, such as VPN connection - # being broken or possibly switching wifi networks. Better solution: Refactor CloudApiClient to use - # HttpRequestManager, which supports timeout. - self._sync_timeout_timer = QTimer() - self._sync_timeout_timer.setInterval(int(self.SYNC_TIMEOUT * 1000)) - self._sync_timeout_timer.setSingleShot(True) - self._sync_timeout_timer.timeout.connect(self._onSyncTimeout) + self._syncing = False def start(self): """Starts running the cloud output device manager, thus periodically requesting cloud data.""" @@ -94,13 +85,12 @@ class CloudOutputDeviceManager: def _getRemoteClusters(self) -> None: """Gets all remote clusters from the API.""" - if self._sync_timeout_timer.isActive(): - # A sync is running + if self._syncing: return Logger.info("Syncing cloud printer clusters") - self._sync_timeout_timer.start() + self._syncing = True self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SYNCING) self._api.getClusters(self._onGetRemoteClustersFinished, self._onGetRemoteClusterFailed) @@ -126,10 +116,12 @@ class CloudOutputDeviceManager: # If the removed device was active we should connect to the new active device self._connectToActiveMachine() - self._onSyncFinished(True) + self._syncing = False + self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS) def _onGetRemoteClusterFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError): - self._onSyncFinished(False) + self._syncing = False + self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) def _onDevicesDiscovered(self, clusters: List[CloudClusterResponse]) -> None: """**Synchronously** create machines for discovered devices @@ -232,19 +224,6 @@ class CloudOutputDeviceManager: if device.key in output_device_manager.getOutputDeviceIds(): output_device_manager.removeOutputDevice(device.key) - def _onSyncTimeout(self): - Logger.warning("Cloud printer sync timed out after {} seconds".format(self.SYNC_TIMEOUT)) - self._onSyncFinished(False) - - def _onSyncFinished(self, success: bool): - if self._sync_timeout_timer.isActive(): - self._sync_timeout_timer.stop() - - if success: - self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS) - else: - self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) - def _createMachineFromDiscoveredDevice(self, key: str, activate: bool = True) -> None: device = self._remote_clusters[key] if not device: @@ -299,4 +278,4 @@ class CloudOutputDeviceManager: output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() if device.key not in output_device_manager.getOutputDeviceIds(): - output_device_manager.addOutputDevice(device) + output_device_manager.addOutputDevice(device) \ No newline at end of file From 94f094380b1e9cb35ae50ef9689559746de6acfc Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Fri, 15 May 2020 11:59:22 +0200 Subject: [PATCH 14/20] Log sync state transitions CURA-7290 --- cura/API/Account.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cura/API/Account.py b/cura/API/Account.py index 2bfbf41f53..4df5765f58 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -108,6 +108,8 @@ class Account(QObject): :param state: One of SyncState """ + Logger.info("Service {service} enters sync state {state}", service = service_name, state = state) + prev_state = self._sync_state self._sync_services[service_name] = state From 8f22e626752130f12de1164e42ed5badf015f3fe Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 18 May 2020 13:30:13 +0200 Subject: [PATCH 15/20] Fix typing issues CURA-7290 --- plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py index 6317dff347..79178049da 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py @@ -2,7 +2,7 @@ # !/usr/bin/env python # -*- coding: utf-8 -*- from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply -from typing import Callable, Any, Tuple +from typing import Callable, Any, Tuple, cast, Dict, Optional from UM.Logger import Logger from UM.TaskManagement.HttpRequestManager import HttpRequestManager @@ -78,14 +78,14 @@ class ToolPathUploader: content_range = "bytes {}-{}/{}".format(first_byte, last_byte - 1, len(self._data)) headers = { - "Content-Type": self._print_job.content_type, + "Content-Type": cast(str, self._print_job.content_type), "Content-Range": content_range - } + } # type: Dict[str, str] Logger.log("i", "Uploading %s to %s", content_range, self._print_job.upload_url) self._http.put( - url = self._print_job.upload_url, + url = cast(str, self._print_job.upload_url), headers_dict = headers, data = self._data[first_byte:last_byte], callback = self._finishedCallback, From 050878bbfe56a7323d747279bdb677903ef456d5 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Tue, 19 May 2020 14:14:10 +0200 Subject: [PATCH 16/20] Update cura/API/Account.py Co-authored-by: Jaime van Kessel --- cura/API/Account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/API/Account.py b/cura/API/Account.py index 4df5765f58..e0d2bb6d7a 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -231,7 +231,7 @@ class Account(QObject): @pyqtSlot() @pyqtSlot(bool) - def sync(self, user_initiated=False): + def sync(self, user_initiated: bool = False) -> None: if user_initiated: self._setManualSyncEnabled(False) From 23c039a8733c1915b93dcaf431de771611a74733 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Tue, 19 May 2020 14:14:39 +0200 Subject: [PATCH 17/20] Add type hint in cura/API/Account.py Co-authored-by: Jaime van Kessel --- cura/API/Account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/API/Account.py b/cura/API/Account.py index e0d2bb6d7a..49cec55eb3 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -238,7 +238,7 @@ class Account(QObject): self._sync() @pyqtSlot() - def popupClosed(self): + def popupClosed(self) -> None: self._setManualSyncEnabled(True) @pyqtSlot() From e7af27ff808167f37eeced3d1026d5619f283f9d Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Tue, 19 May 2020 14:14:59 +0200 Subject: [PATCH 18/20] Add type hint to plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py Co-authored-by: Jaime van Kessel --- .../UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 25ee4cf5e5..7edff4ab67 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -119,7 +119,7 @@ class CloudOutputDeviceManager: self._syncing = False self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS) - def _onGetRemoteClusterFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError): + def _onGetRemoteClusterFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None: self._syncing = False self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR) @@ -278,4 +278,4 @@ class CloudOutputDeviceManager: output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() if device.key not in output_device_manager.getOutputDeviceIds(): - output_device_manager.addOutputDevice(device) \ No newline at end of file + output_device_manager.addOutputDevice(device) From 7388829fc10122625fbaf2fee4533b190a721f07 Mon Sep 17 00:00:00 2001 From: Nino van Hooff Date: Tue, 19 May 2020 14:32:02 +0200 Subject: [PATCH 19/20] Change account sync date format back to dd/mm/YYYY For consistency with the rest of the application --- cura/API/Account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/API/Account.py b/cura/API/Account.py index 49cec55eb3..00afe9e528 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -126,7 +126,7 @@ class Account(QObject): self.syncStateChanged.emit(self._sync_state) if self._sync_state == SyncState.SUCCESS: - self._last_sync_str = datetime.now().strftime("%m/%d/%Y %H:%M") + self._last_sync_str = datetime.now().strftime("%d/%m/%Y %H:%M") self.lastSyncDateTimeChanged.emit() if self._sync_state != SyncState.SYNCING: From fad02193abad63c80abc00e42c212f1bad35d831 Mon Sep 17 00:00:00 2001 From: Kostas Karmas Date: Tue, 26 May 2020 09:49:58 +0200 Subject: [PATCH 20/20] Fix sync button not appearing when opening popup after clicking away This was achieved by adding an IDLE state, which is the default state when opening the account management popup. The state is now reseted when the popup opens instead of when it closes. In addition, now either the "You are in sync with your account" label or the "Check account for updates" button will appear in the popup based on the state, not both. Finally, with theses changes, if the popup is open and an autosync occurs, the user will be informed that the account is synced and he/she will have to close and reopen the popup in order to trigger a manual update. CURA-7290 --- cura/API/Account.py | 9 +++++++-- resources/qml/Account/AccountWidget.qml | 3 ++- resources/qml/Account/SyncState.qml | 7 +++++-- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/cura/API/Account.py b/cura/API/Account.py index 00afe9e528..06125d4819 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -23,6 +23,7 @@ class SyncState: SYNCING = 0 SUCCESS = 1 ERROR = 2 + IDLE = 3 ## The account API provides a version-proof bridge to use Ultimaker Accounts @@ -59,7 +60,7 @@ class Account(QObject): self._error_message = None # type: Optional[Message] self._logged_in = False - self._sync_state = SyncState.SUCCESS + self._sync_state = SyncState.IDLE self._manual_sync_enabled = False self._last_sync_str = "-" @@ -116,11 +117,13 @@ class Account(QObject): if any(val == SyncState.SYNCING for val in self._sync_services.values()): self._sync_state = SyncState.SYNCING + self._setManualSyncEnabled(False) elif any(val == SyncState.ERROR for val in self._sync_services.values()): self._sync_state = SyncState.ERROR self._setManualSyncEnabled(True) else: self._sync_state = SyncState.SUCCESS + self._setManualSyncEnabled(False) if self._sync_state != prev_state: self.syncStateChanged.emit(self._sync_state) @@ -238,8 +241,10 @@ class Account(QObject): self._sync() @pyqtSlot() - def popupClosed(self) -> None: + def popupOpened(self) -> None: self._setManualSyncEnabled(True) + self._sync_state = SyncState.IDLE + self.syncStateChanged.emit(self._sync_state) @pyqtSlot() def logout(self) -> None: diff --git a/resources/qml/Account/AccountWidget.qml b/resources/qml/Account/AccountWidget.qml index 00ac954f22..eed711e745 100644 --- a/resources/qml/Account/AccountWidget.qml +++ b/resources/qml/Account/AccountWidget.qml @@ -112,8 +112,8 @@ Item if (popup.opened) { popup.close() - Cura.API.account.popupClosed() } else { + Cura.API.account.popupOpened() popup.open() } } @@ -127,6 +127,7 @@ Item x: parent.width - width closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent + onOpened: Cura.API.account.popupOpened() opacity: opened ? 1 : 0 Behavior on opacity { NumberAnimation { duration: 100 } } diff --git a/resources/qml/Account/SyncState.qml b/resources/qml/Account/SyncState.qml index eb71e81ecc..98e5991b5a 100644 --- a/resources/qml/Account/SyncState.qml +++ b/resources/qml/Account/SyncState.qml @@ -19,7 +19,7 @@ Row // sync state icon + message width: 20 * screenScaleFactor height: width - source: UM.Theme.getIcon("update") + source: Cura.API.account.manualSyncEnabled ? UM.Theme.getIcon("update") : UM.Theme.getIcon("checked") color: palette.text RotationAnimator @@ -54,6 +54,7 @@ Row // sync state icon + message color: UM.Theme.getColor("text") font: UM.Theme.getFont("medium") renderType: Text.NativeRendering + visible: !Cura.API.account.manualSyncEnabled } Label @@ -80,7 +81,9 @@ Row // sync state icon + message signal syncStateChanged(string newState) onSyncStateChanged: { - if(newState == Cura.AccountSyncState.SYNCING){ + if(newState == Cura.AccountSyncState.IDLE){ + icon.source = UM.Theme.getIcon("update") + } else if(newState == Cura.AccountSyncState.SYNCING){ icon.source = UM.Theme.getIcon("update") stateLabel.text = catalog.i18nc("@label", "Checking...") } else if (newState == Cura.AccountSyncState.SUCCESS) {