Browse Source

Merge pull request #7831 from Ultimaker/CURA-7437_no_internet_cloud_status

CURA-7437_no_internet_cloud_status
Konstantinos Karmas 4 years ago
parent
commit
56e8827d00

+ 64 - 0
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()

+ 7 - 0
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

+ 4 - 0
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:

+ 1 - 1
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:

+ 12 - 10
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

+ 73 - 20
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
+            )
         }
     }
 

+ 16 - 0
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
+    }
 }

+ 18 - 0
resources/themes/cura-light/icons/printer_cloud_not_available.svg

@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="14px" height="14px" viewBox="0 0 14 14" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <!-- Generator: Sketch 63.1 (92452) - https://sketch.com -->
+    <title>Artboard Copy 4</title>
+    <desc>Created with Sketch.</desc>
+    <defs>
+        <path d="M6,0 C9.3137085,0 12,2.6862915 12,6 C12,9.3137085 9.3137085,12 6,12 C2.6862915,12 0,9.3137085 0,6 C0,2.6862915 2.6862915,0 6,0 Z M5.2,3 C4.08,3 3.2,3.91666667 3.2,5.08333333 L3.2,5.125 L3.2,5.125 C2.52,5.20833333 2,5.83333333 2,6.54166667 C2,7.33333333 2.64,8 3.4,8 L8.6,8 L8.6,8 C9.36,8 10,7.33333333 10,6.54166667 C10,5.79166667 9.48,5.20833333 8.8,5.08333333 C8.72,4.375 8.12,3.83333333 7.4,3.83333333 C7.2,3.83333333 7.04,3.875 6.88,3.95833333 C6.52,3.375 5.88,3 5.2,3 Z" id="path-1"></path>
+    </defs>
+    <g id="Artboard-Copy-4" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <rect id="Rectangle" fill="#999999" transform="translate(7.187184, 7.187184) scale(-1, 1) rotate(45.000000) translate(-7.187184, -7.187184) " x="6.43718434" y="-0.812815665" width="1.5" height="16" rx="0.75"></rect>
+        <g id="Path" transform="translate(1.000000, 1.000000)">
+            <mask id="mask-2" fill="white">
+                <use xlink:href="#path-1"></use>
+            </mask>
+            <use id="Combined-Shape" fill="#999999" xlink:href="#path-1"></use>
+        </g>
+    </g>
+</svg>

+ 3 - 1
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": {