diff --git a/cura/API/Account.py b/cura/API/Account.py index 9864de1aaa..7273479de4 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -29,7 +29,6 @@ class Account(QObject): # Signal emitted when user logged in or out. loginStateChanged = pyqtSignal(bool) accessTokenChanged = pyqtSignal() - cloudPrintersDetectedChanged = pyqtSignal(bool) def __init__(self, application: "CuraApplication", parent = None) -> None: super().__init__(parent) @@ -76,10 +75,6 @@ class Account(QObject): def isLoggedIn(self) -> bool: return self._logged_in - @pyqtProperty(bool, notify=cloudPrintersDetectedChanged) - def newCloudPrintersDetected(self) -> bool: - return self._new_cloud_printers_detected - def _onLoginStateChanged(self, logged_in: bool = False, error_message: Optional[str] = None) -> None: if error_message: if self._error_message: diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 993bb15ae2..67a9451282 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -56,6 +56,7 @@ from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Machines.Models.BuildPlateModel import BuildPlateModel from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel from cura.Machines.Models.DiscoveredPrintersModel import DiscoveredPrintersModel +from cura.Machines.Models.DiscoveredCloudPrintersModel import DiscoveredCloudPrintersModel from cura.Machines.Models.ExtrudersModel import ExtrudersModel from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachineActionsModel @@ -201,6 +202,7 @@ class CuraApplication(QtApplication): self._quality_management_model = None self._discovered_printer_model = DiscoveredPrintersModel(self, parent = self) + self._discovered_cloud_printers_model = DiscoveredCloudPrintersModel(self, parent = self) self._first_start_machine_actions_model = None self._welcome_pages_model = WelcomePagesModel(self, parent = self) self._add_printer_pages_model = AddPrinterPagesModel(self, parent = self) @@ -886,6 +888,10 @@ class CuraApplication(QtApplication): def getDiscoveredPrintersModel(self, *args) -> "DiscoveredPrintersModel": return self._discovered_printer_model + @pyqtSlot(result=QObject) + def getDiscoveredCloudPrintersModel(self, *args) -> "DiscoveredCloudPrintersModel": + return self._discovered_cloud_printers_model + @pyqtSlot(result = QObject) def getFirstStartMachineActionsModel(self, *args) -> "FirstStartMachineActionsModel": if self._first_start_machine_actions_model is None: @@ -1084,6 +1090,7 @@ class CuraApplication(QtApplication): self.processEvents() qmlRegisterType(DiscoveredPrintersModel, "Cura", 1, 0, "DiscoveredPrintersModel") + qmlRegisterType(DiscoveredCloudPrintersModel, "Cura", 1, 7, "DiscoveredCloudPrintersModel") qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0, "QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel) qmlRegisterSingletonType(CustomQualityProfilesDropDownMenuModel, "Cura", 1, 0, diff --git a/cura/Machines/Models/DiscoveredCloudPrintersModel.py b/cura/Machines/Models/DiscoveredCloudPrintersModel.py new file mode 100644 index 0000000000..23dcba6de7 --- /dev/null +++ b/cura/Machines/Models/DiscoveredCloudPrintersModel.py @@ -0,0 +1,71 @@ +from typing import Optional, TYPE_CHECKING, List, Dict + +from PyQt5.QtCore import QObject, pyqtSlot, Qt, pyqtSignal, pyqtProperty + +from UM.Qt.ListModel import ListModel + +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication + + +class DiscoveredCloudPrintersModel(ListModel): + """ + Model used to inform the application about newly added cloud printers, which are discovered from the user's account + """ + DeviceKeyRole = Qt.UserRole + 1 + DeviceNameRole = Qt.UserRole + 2 + DeviceTypeRole = Qt.UserRole + 3 + DeviceFirmwareVersionRole = Qt.UserRole + 4 + + cloudPrintersDetectedChanged = pyqtSignal(bool) + + def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None: + super().__init__(parent) + + self.addRoleName(self.DeviceKeyRole, "key") + self.addRoleName(self.DeviceNameRole, "name") + self.addRoleName(self.DeviceTypeRole, "machine_type") + self.addRoleName(self.DeviceFirmwareVersionRole, "firmware_version") + + self._discovered_cloud_printers_list = [] # type: List[Dict[str, str]] + self._application = application # type: CuraApplication + + def addDiscoveredCloudPrinters(self, new_devices: List[Dict[str, str]]) -> None: + """ + Adds all the newly discovered cloud printers into the DiscoveredCloudPrintersModel. + + :param new_devices: List of dictionaries which contain information about added cloud printers. Example: + { + "key": "YjW8pwGYcaUvaa0YgVyWeFkX3z", + "name": "NG 001", + "machine_type": "Ultimaker S5", + "firmware_version": "5.5.12.202001" + } + :return: None + """ + self._discovered_cloud_printers_list.extend(new_devices) + self._update() + + # Inform whether new cloud printers have been detected. If they have, the welcome wizard can close. + self.cloudPrintersDetectedChanged.emit(len(new_devices) > 0) + + @pyqtSlot() + def clear(self) -> None: + """ + Clears the contents of the DiscoveredCloudPrintersModel. + + :return: None + """ + self._discovered_cloud_printers_list = [] + self._update() + self.cloudPrintersDetectedChanged.emit(False) + + def _update(self) -> None: + """ + Sorts the newly discovered cloud printers by name and then updates the ListModel. + + :return: None + """ + items = self._discovered_cloud_printers_list[:] + items.sort(key = lambda k: k["name"]) + self.setItems(items) diff --git a/cura/UI/AddPrinterPagesModel.py b/cura/UI/AddPrinterPagesModel.py index d40da59b2a..b06f220374 100644 --- a/cura/UI/AddPrinterPagesModel.py +++ b/cura/UI/AddPrinterPagesModel.py @@ -21,6 +21,11 @@ class AddPrinterPagesModel(WelcomePagesModel): "page_url": self._getBuiltinWelcomePagePath("AddPrinterByIpContent.qml"), "next_page_id": "machine_actions", }) + self._pages.append({"id": "add_cloud_printers", + "page_url": self._getBuiltinWelcomePagePath("AddCloudPrintersView.qml"), + "is_final_page": True, + "next_page_button_text": self._catalog.i18nc("@action:button", "Finish"), + }) self._pages.append({"id": "machine_actions", "page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"), "should_show_function": self.shouldShowMachineActions, diff --git a/cura/UI/WelcomePagesModel.py b/cura/UI/WelcomePagesModel.py index 611e62cfd6..b816833d67 100644 --- a/cura/UI/WelcomePagesModel.py +++ b/cura/UI/WelcomePagesModel.py @@ -119,8 +119,10 @@ class WelcomePagesModel(ListModel): return next_page_index = idx + is_final_page = page_item.get("is_final_page") + # If we have reached the last page, emit allFinished signal and reset. - if next_page_index == len(self._items): + if next_page_index == len(self._items) or is_final_page: self.atEnd() return @@ -255,6 +257,11 @@ class WelcomePagesModel(ListModel): "page_url": self._getBuiltinWelcomePagePath("AddPrinterByIpContent.qml"), "next_page_id": "machine_actions", }, + {"id": "add_cloud_printers", + "page_url": self._getBuiltinWelcomePagePath("AddCloudPrintersView.qml"), + "is_final_page": True, # If we end up in this page, the next button will close the dialog + "next_page_button_text": self._catalog.i18nc("@action:button", "Finish"), + }, {"id": "machine_actions", "page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"), "should_show_function": self.shouldShowMachineActions, diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index 46136e3a1b..1ed765d154 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -106,10 +106,6 @@ class CloudOutputDeviceManager: self._onDevicesDiscovered(new_clusters) - # Inform whether new cloud printers have been detected. If they have, the welcome wizard can close. - self._account._new_cloud_printers_detected = len(new_clusters) > 0 - self._account.cloudPrintersDetectedChanged.emit(len(new_clusters) > 0) - removed_device_keys = set(self._remote_clusters.keys()) - set(online_clusters.keys()) for device_id in removed_device_keys: self._onDiscoveredDeviceRemoved(device_id) @@ -141,10 +137,20 @@ class CloudOutputDeviceManager: if machine_manager.getMachine(device.printerType, {self.META_CLUSTER_ID: device.key}) is None \ and machine_manager.getMachine(device.printerType, {self.META_NETWORK_KEY: cluster_data.host_name + "*"}) is None: # The host name is part of the network key. new_devices.append(device) + elif device.getId() not in self._remote_clusters: self._remote_clusters[device.getId()] = device remote_clusters_added = True + # Inform the Cloud printers model about new devices. + new_devices_list_of_dicts = [{ + "key": d.getId(), + "name": d.name, + "machine_type": d.printerTypeName, + "firmware_version": d.firmwareVersion} for d in new_devices] + discovered_cloud_printers_model = CuraApplication.getInstance().getDiscoveredCloudPrintersModel() + discovered_cloud_printers_model.addDiscoveredCloudPrinters(new_devices_list_of_dicts) + if not new_devices: if remote_clusters_added: self._connectToActiveMachine() diff --git a/resources/qml/WelcomePages/AddCloudPrintersView.qml b/resources/qml/WelcomePages/AddCloudPrintersView.qml new file mode 100644 index 0000000000..f97d68f776 --- /dev/null +++ b/resources/qml/WelcomePages/AddCloudPrintersView.qml @@ -0,0 +1,192 @@ +// Copyright (c) 2019 Ultimaker B.V. +// Cura is released under the terms of the LGPLv3 or higher. + +import QtQuick 2.10 +import QtQuick.Controls 2.3 +import QtQuick.Layouts 1.3 + +import UM 1.3 as UM +import Cura 1.7 as Cura + + +// +// This component gets activated when the user presses the "Add cloud printers" button from the "Add a Printer" page. +// It contains a busy indicator that remains active until the user logs in and adds a cloud printer in his/her account. +// Once a cloud printer is added in mycloud.ultimaker.com, Cura discovers it (in a time window of 30 sec) and displays +// the newly added printers in this page. +// +Item +{ + UM.I18nCatalog { id: catalog; name: "cura" } + + property bool searchingForCloudPrinters: true + property var discoveredCloudPrintersModel: CuraApplication.getDiscoveredCloudPrintersModel() + + // The area where either the discoveredCloudPrintersScrollView or the busyIndicator will be displayed + Rectangle + { + id: cloudPrintersContent + width: parent.width + height: parent.height + anchors + { + top: parent.top + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + bottom: finishButton.top + bottomMargin: UM.Theme.getSize("default_margin").height + } + + Label + { + id: titleLabel + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + text: catalog.i18nc("@label", "Add a Cloud printer") + color: UM.Theme.getColor("primary_button") + font: UM.Theme.getFont("huge") + renderType: Text.NativeRendering + } + + // Component that contains a busy indicator and a message, while it waits for Cura to discover a cloud printer + Rectangle + { + id: waitingContent + width: parent.width + height: waitingIndicator.height + waitingLabel.height + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + BusyIndicator + { + id: waitingIndicator + anchors.horizontalCenter: parent.horizontalCenter + running: searchingForCloudPrinters + } + Label + { + id: waitingLabel + anchors.top: waitingIndicator.bottom + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + text: catalog.i18nc("@label", "Waiting for Cloud response") + font: UM.Theme.getFont("large") + renderType: Text.NativeRendering + } + visible: discoveredCloudPrintersModel.count == 0 + } + + // Label displayed when a new cloud printer is discovered + Label + { + anchors.top: titleLabel.bottom + anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height + id: cloudPrintersAddedTitle + font: UM.Theme.getFont("medium") + text: catalog.i18nc("@label", "The following printers in your account have been added in Cura:") + height: contentHeight + 2 * UM.Theme.getSize("default_margin").height + visible: discoveredCloudPrintersModel.count > 0 + } + + // The scrollView that contains the list of newly discovered Ultimaker Cloud printers. Visible only when + // there is at least a new cloud printer. + ScrollView + { + id: discoveredCloudPrintersScrollView + width: parent.width + clip : true + ScrollBar.horizontal.policy: ScrollBar.AsNeeded + ScrollBar.vertical.policy: ScrollBar.AsNeeded + visible: discoveredCloudPrintersModel.count > 0 + anchors + { + top: cloudPrintersAddedTitle.bottom + topMargin: UM.Theme.getSize("default_margin").height + left: parent.left + leftMargin: UM.Theme.getSize("default_margin").width + right: parent.right + bottom: parent.bottom + } + + Column + { + id: discoveredPrintersColumn + spacing: 2 * UM.Theme.getSize("default_margin").height + + Repeater + { + id: discoveredCloudPrintersRepeater + model: discoveredCloudPrintersModel + delegate: Item + { + width: discoveredCloudPrintersScrollView.width + height: contentColumn.height + + Column + { + id: contentColumn + Label + { + id: cloudPrinterNameLabel + leftPadding: UM.Theme.getSize("default_margin").width + text: model.name + font: UM.Theme.getFont("large_bold") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + Label + { + id: cloudPrinterTypeLabel + leftPadding: 2 * UM.Theme.getSize("default_margin").width + topPadding: UM.Theme.getSize("thin_margin").height + text: {"Type: " + model.machine_type} + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + Label + { + id: cloudPrinterFirmwareVersionLabel + leftPadding: 2 * UM.Theme.getSize("default_margin").width + text: {"Firmware version: " + model.firmware_version} + font: UM.Theme.getFont("medium") + color: UM.Theme.getColor("text") + elide: Text.ElideRight + } + } + } + } + } + } + } + + Cura.SecondaryButton + { + id: backButton + anchors.left: parent.left + anchors.bottom: parent.bottom + text: catalog.i18nc("@button", "Add printer manually") + onClicked: + { + discoveredCloudPrintersModel.clear() + base.showPreviousPage() + } + visible: discoveredCloudPrintersModel.count == 0 + } + + Cura.PrimaryButton + { + id: finishButton + anchors.right: parent.right + anchors.bottom: parent.bottom + text: catalog.i18nc("@button", "Finish") + onClicked: + { + discoveredCloudPrintersModel.clear() + base.showNextPage() + } + + enabled: !waitingContent.visible + } +} diff --git a/resources/qml/WelcomePages/AddNetworkOrLocalPrinterContent.qml b/resources/qml/WelcomePages/AddNetworkOrLocalPrinterContent.qml index 6a2accf97d..9e892e5521 100644 --- a/resources/qml/WelcomePages/AddNetworkOrLocalPrinterContent.qml +++ b/resources/qml/WelcomePages/AddNetworkOrLocalPrinterContent.qml @@ -65,6 +65,20 @@ Item { base.goToPage("add_printer_by_ip") } + + onAddCloudPrinterButtonClicked: + { + base.goToPage("add_cloud_printers") + if (!Cura.API.account.isLoggedIn) + { + Cura.API.account.login() + } + else + { + Qt.openUrlExternally("https://mycloud.ultimaker.com/app/manage/printers") + } + + } } } } diff --git a/resources/qml/WelcomePages/AddNetworkPrinterScrollView.qml b/resources/qml/WelcomePages/AddNetworkPrinterScrollView.qml index b4d4fee42c..af60c9c723 100644 --- a/resources/qml/WelcomePages/AddNetworkPrinterScrollView.qml +++ b/resources/qml/WelcomePages/AddNetworkPrinterScrollView.qml @@ -24,6 +24,7 @@ Item signal refreshButtonClicked() signal addByIpButtonClicked() + signal addCloudPrinterButtonClicked() Item { @@ -193,6 +194,20 @@ Item onClicked: base.addByIpButtonClicked() } + Cura.SecondaryButton + { + id: addCloudPrinterButton + anchors.left: addPrinterByIpButton.right + anchors.leftMargin: UM.Theme.getSize("default_margin").width + anchors.verticalCenter: parent.verticalCenter + text: catalog.i18nc("@label", "Add cloud printer") + height: UM.Theme.getSize("message_action_button").height + onClicked: { + CuraApplication.getDiscoveredCloudPrintersModel().clear() + base.addCloudPrinterButtonClicked() + } + } + Item { id: troubleshootingButton diff --git a/resources/qml/WelcomePages/CloudContent.qml b/resources/qml/WelcomePages/CloudContent.qml index 36a7b9a923..48410f7f12 100644 --- a/resources/qml/WelcomePages/CloudContent.qml +++ b/resources/qml/WelcomePages/CloudContent.qml @@ -15,17 +15,18 @@ Item { UM.I18nCatalog { id: catalog; name: "cura" } - property bool newCloudPrintersDetected: Cura.API.account.newCloudPrintersDetected signal cloudPrintersDetected(bool newCloudPrintersDetected) - Component.onCompleted: Cura.API.account.cloudPrintersDetectedChanged.connect(cloudPrintersDetected) + Component.onCompleted: CuraApplication.getDiscoveredCloudPrintersModel().cloudPrintersDetectedChanged.connect(cloudPrintersDetected) + onCloudPrintersDetected: { // When the user signs in successfully, it will be checked whether he/she has cloud printers connected to - // the account. If he/she does, then the welcome wizard can close. If not, then proceed to the next page (if any) + // the account. If he/she does, then the welcome wizard will show a summary of the Cloud printers linked to the + // account. If there are no cloud printers, then proceed to the next page (if any) if(newCloudPrintersDetected) { - base.endWizard() + base.goToPage("add_cloud_printers") } else { diff --git a/resources/qml/WelcomePages/WelcomeDialogItem.qml b/resources/qml/WelcomePages/WelcomeDialogItem.qml index 7da4c6e897..5b90a8732e 100644 --- a/resources/qml/WelcomePages/WelcomeDialogItem.qml +++ b/resources/qml/WelcomePages/WelcomeDialogItem.qml @@ -21,8 +21,8 @@ Item anchors.centerIn: parent - width: 580 * screenScaleFactor - height: 600 * screenScaleFactor + width: UM.Theme.getSize("welcome_wizard_window").width + height: UM.Theme.getSize("welcome_wizard_window").height property int shadowOffset: 1 * screenScaleFactor diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 1640395c0b..b870603bd9 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -573,6 +573,7 @@ "monitor_preheat_temperature_control": [4.5, 2.0], + "welcome_wizard_window": [46.0, 45], "modal_window_minimum": [60.0, 45], "license_window_minimum": [45, 45], "wizard_progress": [10.0, 0.0], diff --git a/tests/Machines/Models/TestDiscoveredCloudPrintersModel.py b/tests/Machines/Models/TestDiscoveredCloudPrintersModel.py new file mode 100644 index 0000000000..5b19178531 --- /dev/null +++ b/tests/Machines/Models/TestDiscoveredCloudPrintersModel.py @@ -0,0 +1,34 @@ +from unittest.mock import MagicMock + +import pytest + +from cura.Machines.Models.DiscoveredCloudPrintersModel import DiscoveredCloudPrintersModel + + +@pytest.fixture() +def discovered_cloud_printers_model(application) -> DiscoveredCloudPrintersModel: + return DiscoveredCloudPrintersModel(application) + + +def test_discoveredCloudPrinters(discovered_cloud_printers_model): + new_devices = [{ + "key": "Bite my shiny metal a$$", + "name": "Bender", + "machine_type": "Bender robot", + "firmware_version": "8.0.0.8.5" + }] + discovered_cloud_printers_model.cloudPrintersDetectedChanged = MagicMock() + + # Test if adding a cloud printer in the model works + discovered_cloud_printers_model.addDiscoveredCloudPrinters(new_devices) + assert len(discovered_cloud_printers_model._discovered_cloud_printers_list) == 1 + assert discovered_cloud_printers_model.cloudPrintersDetectedChanged.emit.call_count == 1 + # Make sure that the signal was called with "True" as input + discovered_cloud_printers_model.cloudPrintersDetectedChanged.emit.assert_called_with(True) + + # Test if clearing the model works + discovered_cloud_printers_model.clear() + assert len(discovered_cloud_printers_model._discovered_cloud_printers_list) == 0 + assert discovered_cloud_printers_model.cloudPrintersDetectedChanged.emit.call_count == 2 + # Make sure that the signal was called with "False" as input + discovered_cloud_printers_model.cloudPrintersDetectedChanged.emit.assert_called_with(False)