diff --git a/cura/API/ConnectionStatus.py b/cura/API/ConnectionStatus.py new file mode 100644 index 0000000000..332e519ca9 --- /dev/null +++ b/cura/API/ConnectionStatus.py @@ -0,0 +1,64 @@ +from typing import Optional + +from PyQt5.QtCore import QObject, pyqtSignal, QTimer, pyqtProperty +from PyQt5.QtNetwork import QNetworkReply + +from UM.TaskManagement.HttpRequestManager import HttpRequestManager +from cura.UltimakerCloud import UltimakerCloudAuthentication + + +class ConnectionStatus(QObject): + """Status info for some web services""" + + UPDATE_INTERVAL = 10.0 # seconds + ULTIMAKER_CLOUD_STATUS_URL = UltimakerCloudAuthentication.CuraCloudAPIRoot + "/connect/v1/" + + __instance = None # type: Optional[ConnectionStatus] + + internetReachableChanged = pyqtSignal() + umCloudReachableChanged = pyqtSignal() + + @classmethod + def getInstance(cls, *args, **kwargs) -> "ConnectionStatus": + if cls.__instance is None: + cls.__instance = cls(*args, **kwargs) + return cls.__instance + + def __init__(self, parent: Optional["QObject"] = None): + super().__init__(parent) + + self._http = HttpRequestManager.getInstance() + self._statuses = { + self.ULTIMAKER_CLOUD_STATUS_URL: True, + "http://example.com": True + } + + # Create a timer for automatic updates + self._update_timer = QTimer() + self._update_timer.setInterval(int(self.UPDATE_INTERVAL * 1000)) + # The timer is restarted automatically + self._update_timer.setSingleShot(False) + self._update_timer.timeout.connect(self._update) + self._update_timer.start() + + @pyqtProperty(bool, notify=internetReachableChanged) + def isInternetReachable(self) -> bool: + # Is any of the test urls reachable? + return any(self._statuses.values()) + + def _update(self): + for url in self._statuses.keys(): + self._http.get( + url = url, + callback = self._statusCallback, + error_callback = self._statusCallback, + timeout = 5 + ) + + def _statusCallback(self, reply: QNetworkReply, error: QNetworkReply.NetworkError = None): + url = reply.request().url().toString() + prev_statuses = self._statuses.copy() + self._statuses[url] = HttpRequestManager.replyIndicatesSuccess(reply, error) + + if any(self._statuses.values()) != any(prev_statuses.values()): + self.internetReachableChanged.emit() diff --git a/cura/API/__init__.py b/cura/API/__init__.py index 26c9a4c829..bcd4032f97 100644 --- a/cura/API/__init__.py +++ b/cura/API/__init__.py @@ -5,6 +5,7 @@ from typing import Optional, TYPE_CHECKING from PyQt5.QtCore import QObject, pyqtProperty from cura.API.Backups import Backups +from cura.API.ConnectionStatus import ConnectionStatus from cura.API.Interface import Interface from cura.API.Account import Account @@ -45,6 +46,8 @@ class CuraAPI(QObject): # Backups API self._backups = Backups(self._application) + self._connectionStatus = ConnectionStatus() + # Interface API self._interface = Interface(self._application) @@ -55,6 +58,10 @@ class CuraAPI(QObject): def account(self) -> "Account": return self._account + @pyqtProperty(QObject, constant = True) + def connectionStatus(self) -> "ConnectionStatus": + return self._connectionStatus + @property def backups(self) -> "Backups": return self._backups diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index 8f1a8bb7f8..effdb3866a 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -490,6 +490,10 @@ class MachineManager(QObject): def activeMachineHasCloudConnection(self) -> bool: # A cloud connection is only available if any output device actually is a cloud connected device. return any(d.connectionType == ConnectionType.CloudConnection for d in self._printer_output_devices) + + @pyqtProperty(bool, notify = printerConnectedStatusChanged) + def activeMachineHasCloudRegistration(self) -> bool: + return self.activeMachine is not None and ConnectionType.CloudConnection in self.activeMachine.configuredConnectionTypes @pyqtProperty(bool, notify = printerConnectedStatusChanged) def activeMachineIsUsingCloudConnection(self) -> bool: diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py index 273c64ef4d..3410898269 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py @@ -265,7 +265,7 @@ class LocalClusterOutputDeviceManager: ## Nudge the user to start using Ultimaker Cloud. @staticmethod def _showCloudFlowMessage(device: LocalClusterOutputDevice) -> None: - if CuraApplication.getInstance().getMachineManager().activeMachineIsUsingCloudConnection: + if CuraApplication.getInstance().getMachineManager().activeMachineHasCloudRegistration: # This printer is already cloud connected, so we do not bother the user anymore. return if not CuraApplication.getInstance().getCuraAPI().account.isLoggedIn: diff --git a/resources/qml/ExpandablePopup.qml b/resources/qml/ExpandablePopup.qml index 18255939ab..eb949a84ec 100644 --- a/resources/qml/ExpandablePopup.qml +++ b/resources/qml/ExpandablePopup.qml @@ -35,7 +35,8 @@ Item property color headerActiveColor: UM.Theme.getColor("secondary") property color headerHoverColor: UM.Theme.getColor("action_button_hovered") - property alias enabled: mouseArea.enabled + property alias mouseArea: headerMouseArea + property alias enabled: headerMouseArea.enabled // Text to show when this component is disabled property alias disabledText: disabledLabel.text @@ -139,6 +140,16 @@ Item anchors.fill: parent visible: base.enabled + MouseArea + { + id: headerMouseArea + anchors.fill: parent + onClicked: toggleContent() + hoverEnabled: true + onEntered: background.color = headerHoverColor + onExited: background.color = base.enabled ? headerBackgroundColor : UM.Theme.getColor("disabled") + } + Loader { id: headerItemLoader @@ -180,15 +191,6 @@ Item } } - MouseArea - { - id: mouseArea - anchors.fill: parent - onClicked: toggleContent() - hoverEnabled: true - onEntered: background.color = headerHoverColor - onExited: background.color = base.enabled ? headerBackgroundColor : UM.Theme.getColor("disabled") - } } DropShadow diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index 2a101e4ae3..2896588341 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -5,16 +5,49 @@ import QtQuick 2.7 import QtQuick.Controls 2.3 import UM 1.2 as UM -import Cura 1.0 as Cura +import Cura 1.1 as Cura Cura.ExpandablePopup { id: machineSelector property bool isNetworkPrinter: Cura.MachineManager.activeMachineHasNetworkConnection - property bool isCloudPrinter: Cura.MachineManager.activeMachineHasCloudConnection + property bool isConnectedCloudPrinter: Cura.MachineManager.activeMachineHasCloudConnection + property bool isCloudRegistered: Cura.MachineManager.activeMachineHasCloudRegistration property bool isGroup: Cura.MachineManager.activeMachineIsGroup + readonly property string connectionStatus: { + if (isNetworkPrinter) + { + return "printer_connected" + } + else if (isConnectedCloudPrinter && Cura.API.connectionStatus.isInternetReachable) + { + return "printer_cloud_connected" + } + else if (isCloudRegistered) + { + return "printer_cloud_not_available" + } + else + { + return "" + } + } + + readonly property string connectionStatusMessage: { + if (connectionStatus == "printer_cloud_not_available") + { + if(Cura.API.connectionStatus.isInternetReachable){ + return catalog.i18nc("@status", "The cloud connection is currently unavailable. Please check your internet connection and sign in to connect to the cloud printer.") + } else { + return catalog.i18nc("@status", "The cloud connection is currently unavailable. Please check your internet connection.") + } + } else { + return "" + } + } + contentPadding: UM.Theme.getSize("default_lining").width contentAlignment: Cura.ExpandablePopup.ContentAlignment.AlignLeft @@ -44,7 +77,7 @@ Cura.ExpandablePopup { return UM.Theme.getIcon("printer_group") } - else if (isNetworkPrinter || isCloudPrinter) + else if (isNetworkPrinter || isCloudRegistered) { return UM.Theme.getIcon("printer_single") } @@ -59,6 +92,7 @@ Cura.ExpandablePopup UM.RecolorImage { + id: connectionStatusImage anchors { bottom: parent.bottom @@ -66,27 +100,14 @@ Cura.ExpandablePopup leftMargin: UM.Theme.getSize("thick_margin").width } - source: - { - if (isNetworkPrinter) - { - return UM.Theme.getIcon("printer_connected") - } - else if (isCloudPrinter) - { - return UM.Theme.getIcon("printer_cloud_connected") - } - else - { - return "" - } - } + source: UM.Theme.getIcon(connectionStatus) width: UM.Theme.getSize("printer_status_icon").width height: UM.Theme.getSize("printer_status_icon").height - color: UM.Theme.getColor("primary") - visible: isNetworkPrinter || isCloudPrinter + color: connectionStatus == "printer_cloud_not_available" ? UM.Theme.getColor("cloud_unavailable") : UM.Theme.getColor("primary") + + visible: isNetworkPrinter || isCloudRegistered // Make a themable circle in the background so we can change it in other themes Rectangle @@ -100,6 +121,38 @@ Cura.ExpandablePopup color: UM.Theme.getColor("main_background") z: parent.z - 1 } + + } + + MouseArea // Connection status tooltip hover area + { + id: connectionStatusTooltipHoverArea + anchors.fill: parent + hoverEnabled: connectionStatusMessage !== "" + acceptedButtons: Qt.NoButton // react to hover only, don't steal clicks + + onEntered: + { + machineSelector.mouseArea.entered() // we want both this and the outer area to be entered + tooltip.show() + } + onExited: { tooltip.hide() } + } + + Cura.ToolTip + { + id: tooltip + + width: 250 * screenScaleFactor + tooltipText: connectionStatusMessage + arrowSize: UM.Theme.getSize("button_tooltip_arrow").width + x: connectionStatusImage.x - UM.Theme.getSize("narrow_margin").width + y: connectionStatusImage.y + connectionStatusImage.height + UM.Theme.getSize("narrow_margin").height + z: popup.z + 1 + targetPoint: Qt.point( + connectionStatusImage.x + Math.round(connectionStatusImage.width / 2), + connectionStatusImage.y + ) } } diff --git a/resources/qml/ToolTip.qml b/resources/qml/ToolTip.qml index e82caf01b2..ad58038d01 100644 --- a/resources/qml/ToolTip.qml +++ b/resources/qml/ToolTip.qml @@ -19,12 +19,20 @@ ToolTip property int contentAlignment: Cura.ToolTip.ContentAlignment.AlignRight property alias tooltipText: tooltip.text + property alias arrowSize: backgroundRect.arrowSize property var targetPoint: Qt.point(parent.x, y + Math.round(height/2)) id: tooltip text: "" delay: 500 font: UM.Theme.getFont("default") + visible: opacity != 0.0 + opacity: 0.0 // initially hidden + + Behavior on opacity + { + NumberAnimation { duration: 100; } + } // If the text is not set, just set the height to 0 to prevent it from showing height: text != "" ? label.contentHeight + 2 * UM.Theme.getSize("thin_margin").width: 0 @@ -60,4 +68,12 @@ ToolTip color: UM.Theme.getColor("tooltip_text") renderType: Text.NativeRendering } + + function show() { + opacity = 1 + } + + function hide() { + opacity = 0 + } } \ No newline at end of file diff --git a/resources/themes/cura-light/icons/printer_cloud_not_available.svg b/resources/themes/cura-light/icons/printer_cloud_not_available.svg new file mode 100644 index 0000000000..248df27338 --- /dev/null +++ b/resources/themes/cura-light/icons/printer_cloud_not_available.svg @@ -0,0 +1,18 @@ + + + + Artboard Copy 4 + Created with Sketch. + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index b870603bd9..0bc09701b2 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -438,7 +438,9 @@ "monitor_shadow": [200, 200, 200, 255], "monitor_carousel_dot": [216, 216, 216, 255], - "monitor_carousel_dot_current": [119, 119, 119, 255] + "monitor_carousel_dot_current": [119, 119, 119, 255], + + "cloud_unavailable": [153, 153, 153, 255] }, "sizes": {