Browse Source

Merge remote-tracking branch 'origin/master' into doxygen_to_restructuredtext_comments

# Conflicts:
#	cura/API/__init__.py
#	cura/Settings/CuraContainerRegistry.py
#	cura/Settings/ExtruderManager.py
#	plugins/PostProcessingPlugin/scripts/PauseAtHeight.py
#	plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py
#	plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py
#	plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py
Nino van Hooff 4 years ago
parent
commit
58ffc9dcae

+ 44 - 13
cura/API/Account.py

@@ -23,6 +23,7 @@ class SyncState:
     SYNCING = 0
     SUCCESS = 1
     ERROR = 2
+    IDLE = 3
 
 class Account(QObject):
     """The account API provides a version-proof bridge to use Ultimaker Accounts
@@ -54,6 +55,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)
@@ -62,7 +64,8 @@ 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 = "-"
 
         self._callback_port = 32118
@@ -110,16 +113,21 @@ 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
 
         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)
@@ -162,11 +170,31 @@ class Account(QObject):
             self._logged_in = logged_in
             self.loginStateChanged.emit(logged_in)
             if logged_in:
-                self.sync()
+                self._setManualSyncEnabled(False)
+                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()
+
+    def _setManualSyncEnabled(self, enabled: bool) -> None:
+        if self._manual_sync_enabled != enabled:
+            self._manual_sync_enabled = enabled
+            self.manualSyncEnabledChanged.emit(enabled)
+
     @pyqtSlot()
     @pyqtSlot(bool)
     def login(self, force_logout_before_login: bool = False) -> None:
@@ -217,20 +245,23 @@ class Account(QObject):
     def lastSyncDateTime(self) -> str:
         return self._last_sync_str
 
-    @pyqtSlot()
-    def sync(self) -> None:
-        """Signals all sync services to start syncing
+    @pyqtProperty(bool, notify=manualSyncEnabledChanged)
+    def manualSyncEnabled(self) -> bool:
+        return self._manual_sync_enabled
 
-        This can be considered a forced sync: even when a
-        sync is currently running, a sync will be requested.
-        """
+    @pyqtSlot()
+    @pyqtSlot(bool)
+    def sync(self, user_initiated: bool = False) -> None:
+        if user_initiated:
+            self._setManualSyncEnabled(False)
 
-        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._sync()
 
-        self.syncRequested.emit()
+    @pyqtSlot()
+    def popupOpened(self) -> None:
+        self._setManualSyncEnabled(True)
+        self._sync_state = SyncState.IDLE
+        self.syncStateChanged.emit(self._sync_state)
 
     @pyqtSlot()
     def logout(self) -> None:

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

+ 9 - 1
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
 
@@ -14,7 +15,7 @@ if TYPE_CHECKING:
 
 class CuraAPI(QObject):
     """The official Cura API that plug-ins can use to interact with Cura.
-    
+
     Python does not technically prevent talking to other classes as well, but this API provides a version-safe
     interface with proper deprecation warnings etc. Usage of any other methods than the ones provided in this API can
     cause plug-ins to be unstable.
@@ -44,6 +45,9 @@ class CuraAPI(QObject):
 
         self._backups = Backups(self._application)
 
+        self._connectionStatus = ConnectionStatus()
+
+        # Interface API
         self._interface = Interface(self._application)
 
     def initialize(self) -> None:
@@ -55,6 +59,10 @@ class CuraAPI(QObject):
 
         return self._account
 
+    @pyqtProperty(QObject, constant = True)
+    def connectionStatus(self) -> "ConnectionStatus":
+        return self._connectionStatus
+
     @property
     def backups(self) -> "Backups":
         """Backups API"""

+ 3 - 3
cura/CuraPackageManager.py

@@ -40,10 +40,10 @@ class CuraPackageManager(PackageManager):
         machine_with_qualities = []
         for container_id in ids:
             for global_stack in global_stacks:
-                for extruder_nr, extruder_stack in global_stack.extruders.items():
+                for extruder_nr, extruder_stack in enumerate(global_stack.extruderList):
                     if container_id in (extruder_stack.material.getId(), extruder_stack.material.getMetaData().get("base_file")):
-                        machine_with_materials.append((global_stack, extruder_nr, container_id))
+                        machine_with_materials.append((global_stack, str(extruder_nr), container_id))
                     if container_id == extruder_stack.quality.getId():
-                        machine_with_qualities.append((global_stack, extruder_nr, container_id))
+                        machine_with_qualities.append((global_stack, str(extruder_nr), container_id))
 
         return machine_with_materials, machine_with_qualities

+ 3 - 2
cura/Machines/Models/BaseMaterialsModel.py

@@ -156,9 +156,10 @@ class BaseMaterialsModel(ListModel):
         global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
         if not global_stack.hasMaterials:
             return  # There are no materials for this machine, so nothing to do.
-        extruder_stack = global_stack.extruders.get(str(self._extruder_position))
-        if not extruder_stack:
+        extruder_list = global_stack.extruderList
+        if self._extruder_position > len(extruder_list):
             return
+        extruder_stack = extruder_list[self._extruder_position]
         nozzle_name = extruder_stack.variant.getName()
         machine_node = ContainerTree.getInstance().machines[global_stack.definition.getId()]
         if nozzle_name not in machine_node.variants:

+ 20 - 9
cura/Machines/Models/DiscoveredPrintersModel.py

@@ -128,6 +128,7 @@ class DiscoveredPrintersModel(QObject):
         self._discovered_printer_by_ip_dict = dict()  # type: Dict[str, DiscoveredPrinter]
 
         self._plugin_for_manual_device = None  # type: Optional[OutputDevicePlugin]
+        self._network_plugin_queue = []  # type: List[OutputDevicePlugin]
         self._manual_device_address = ""
 
         self._manual_device_request_timeout_in_seconds = 5  # timeout for adding a manual device in seconds
@@ -152,20 +153,25 @@ class DiscoveredPrintersModel(QObject):
 
         all_plugins_dict = self._application.getOutputDeviceManager().getAllOutputDevicePlugins()
 
-        can_add_manual_plugins = [item for item in filter(
+        self._network_plugin_queue = [item for item in filter(
             lambda plugin_item: plugin_item.canAddManualDevice(address) in priority_order,
             all_plugins_dict.values())]
 
-        if not can_add_manual_plugins:
+        if not self._network_plugin_queue:
             Logger.log("d", "Could not find a plugin to accept adding %s manually via address.", address)
             return
 
-        plugin = max(can_add_manual_plugins, key = lambda p: priority_order.index(p.canAddManualDevice(address)))
-        self._plugin_for_manual_device = plugin
-        self._plugin_for_manual_device.addManualDevice(address, callback = self._onManualDeviceRequestFinished)
-        self._manual_device_address = address
-        self._manual_device_request_timer.start()
-        self.hasManualDeviceRequestInProgressChanged.emit()
+        self._attemptToAddManualDevice(address)
+
+    def _attemptToAddManualDevice(self, address: str) -> None:
+        if self._network_plugin_queue:
+            self._plugin_for_manual_device = self._network_plugin_queue.pop()
+            Logger.log("d", "Network plugin %s: attempting to add manual device with address %s.",
+                       self._plugin_for_manual_device.getId(), address)
+            self._plugin_for_manual_device.addManualDevice(address, callback=self._onManualDeviceRequestFinished)
+            self._manual_device_address = address
+            self._manual_device_request_timer.start()
+            self.hasManualDeviceRequestInProgressChanged.emit()
 
     @pyqtSlot()
     def cancelCurrentManualDeviceRequest(self) -> None:
@@ -180,8 +186,11 @@ class DiscoveredPrintersModel(QObject):
             self.manualDeviceRequestFinished.emit(False)
 
     def _onManualRequestTimeout(self) -> None:
-        Logger.log("w", "Manual printer [%s] request timed out. Cancel the current request.", self._manual_device_address)
+        address = self._manual_device_address
+        Logger.log("w", "Manual printer [%s] request timed out. Cancel the current request.", address)
         self.cancelCurrentManualDeviceRequest()
+        if self._network_plugin_queue:
+            self._attemptToAddManualDevice(address)
 
     hasManualDeviceRequestInProgressChanged = pyqtSignal()
 
@@ -197,6 +206,8 @@ class DiscoveredPrintersModel(QObject):
             self._manual_device_address = ""
             self.hasManualDeviceRequestInProgressChanged.emit()
             self.manualDeviceRequestFinished.emit(success)
+        if not success and self._network_plugin_queue:
+            self._attemptToAddManualDevice(address)
 
     @pyqtProperty("QVariantMap", notify = discoveredPrintersChanged)
     def discoveredPrintersByAddress(self) -> Dict[str, DiscoveredPrinter]:

+ 13 - 4
cura/Machines/Models/GlobalStacksModel.py

@@ -19,6 +19,7 @@ class GlobalStacksModel(ListModel):
     ConnectionTypeRole = Qt.UserRole + 4
     MetaDataRole = Qt.UserRole + 5
     DiscoverySourceRole = Qt.UserRole + 6  # For separating local and remote printers in the machine management page
+    RemovalWarningRole = Qt.UserRole + 7
 
     def __init__(self, parent = None) -> None:
         super().__init__(parent)
@@ -66,13 +67,21 @@ class GlobalStacksModel(ListModel):
             if parseBool(container_stack.getMetaDataEntry("hidden", False)):
                 continue
 
-            section_name = "Network enabled printers" if has_remote_connection else "Local printers"
+            device_name = container_stack.getMetaDataEntry("group_name", container_stack.getName())
+            section_name = "Connected printers" if has_remote_connection else "Preset printers"
             section_name = self._catalog.i18nc("@info:title", section_name)
 
-            items.append({"name": container_stack.getMetaDataEntry("group_name", container_stack.getName()),
+            default_removal_warning = self._catalog.i18nc(
+                "@label ({} is object name)",
+                "Are you sure you wish to remove {}? This cannot be undone!", device_name
+            )
+            removal_warning = container_stack.getMetaDataEntry("removal_warning", default_removal_warning)
+
+            items.append({"name": device_name,
                           "id": container_stack.getId(),
                           "hasRemoteConnection": has_remote_connection,
                           "metadata": container_stack.getMetaData().copy(),
-                          "discoverySource": section_name})
-        items.sort(key = lambda i: (not i["hasRemoteConnection"], i["name"]))
+                          "discoverySource": section_name,
+                          "removalWarning": removal_warning})
+        items.sort(key=lambda i: (not i["hasRemoteConnection"], i["name"]))
         self.setItems(items)

+ 1 - 1
cura/Machines/Models/QualitySettingsModel.py

@@ -151,7 +151,7 @@ class QualitySettingsModel(ListModel):
             if self._selected_position == self.GLOBAL_STACK_POSITION:
                 user_value = global_container_stack.userChanges.getProperty(definition.key, "value")
             else:
-                extruder_stack = global_container_stack.extruders[str(self._selected_position)]
+                extruder_stack = global_container_stack.extruderList[self._selected_position]
                 user_value = extruder_stack.userChanges.getProperty(definition.key, "value")
 
             if profile_value is None and user_value is None:

+ 2 - 2
cura/Scene/ConvexHullDecorator.py

@@ -241,7 +241,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
         if self._node is None:
             return None
         if self._node.callDecoration("isGroup"):
-            points = numpy.zeros((0, 2), dtype=numpy.int32)
+            points = numpy.zeros((0, 2), dtype = numpy.int32)
             for child in self._node.getChildren():
                 child_hull = child.callDecoration("_compute2DConvexHull")
                 if child_hull:
@@ -285,7 +285,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
             # Do not throw away vertices: the convex hull may be too small and objects can collide.
             # vertex_data = vertex_data[vertex_data[:,1] >= -0.01]
 
-            if len(vertex_data) >= 4:  # type: ignore # mypy and numpy don't play along well just yet.
+            if vertex_data is not None and len(vertex_data) >= 4:  # type: ignore # mypy and numpy don't play along well just yet.
                 # Round the vertex data to 1/10th of a mm, then remove all duplicate vertices
                 # This is done to greatly speed up further convex hull calculations as the convex hull
                 # becomes much less complex when dealing with highly detailed models.

+ 13 - 15
cura/Settings/CuraContainerRegistry.py

@@ -47,9 +47,9 @@ class CuraContainerRegistry(ContainerRegistry):
     @override(ContainerRegistry)
     def addContainer(self, container: ContainerInterface) -> None:
         """Overridden from ContainerRegistry
-        
+
         Adds a container to the registry.
-        
+
         This will also try to convert a ContainerStack to either Extruder or
         Global stack based on metadata information.
         """
@@ -70,7 +70,7 @@ class CuraContainerRegistry(ContainerRegistry):
 
     def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str:
         """Create a name that is not empty and unique
-        
+
         :param container_type: :type{string} Type of the container (machine, quality, ...)
         :param current_name: :type{} Current name of the container, which may be an acceptable option
         :param new_name: :type{string} Base name, which may not be unique
@@ -95,7 +95,7 @@ class CuraContainerRegistry(ContainerRegistry):
 
     def _containerExists(self, container_type: str, container_name: str):
         """Check if a container with of a certain type and a certain name or id exists
-        
+
         Both the id and the name are checked, because they may not be the same and it is better if they are both unique
         :param container_type: :type{string} Type of the container (machine, quality, ...)
         :param container_name: :type{string} Name to check
@@ -107,7 +107,7 @@ class CuraContainerRegistry(ContainerRegistry):
 
     def exportQualityProfile(self, container_list: List[InstanceContainer], file_name: str, file_type: str) -> bool:
         """Exports an profile to a file
-        
+
         :param container_list: :type{list} the containers to export. This is not
         necessarily in any order!
         :param file_name: :type{str} the full path and filename to export to.
@@ -160,7 +160,7 @@ class CuraContainerRegistry(ContainerRegistry):
 
     def _findProfileWriter(self, extension: str, description: str) -> Optional[ProfileWriter]:
         """Gets the plugin object matching the criteria
-        
+
         :param extension:
         :param description:
         :return: The plugin object matching the given extension and description.
@@ -177,7 +177,7 @@ class CuraContainerRegistry(ContainerRegistry):
 
     def importProfile(self, file_name: str) -> Dict[str, str]:
         """Imports a profile from a file
-        
+
         :param file_name: The full path and filename of the profile to import.
         :return: Dict with a 'status' key containing the string 'ok' or 'error',
             and a 'message' key containing a message for the user.
@@ -192,9 +192,7 @@ class CuraContainerRegistry(ContainerRegistry):
             return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Can't import profile from <filename>{0}</filename> before a printer is added.", file_name)}
         container_tree = ContainerTree.getInstance()
 
-        machine_extruders = []
-        for position in sorted(global_stack.extruders):
-            machine_extruders.append(global_stack.extruders[position])
+        machine_extruders = global_stack.extruderList
 
         plugin_registry = PluginRegistry.getInstance()
         extension = file_name.split(".")[-1]
@@ -275,7 +273,7 @@ class CuraContainerRegistry(ContainerRegistry):
                 if len(profile_or_list) == 1:
                     global_profile = profile_or_list[0]
                     extruder_profiles = []
-                    for idx, extruder in enumerate(global_stack.extruders.values()):
+                    for idx, extruder in enumerate(global_stack.extruderList):
                         profile_id = ContainerRegistry.getInstance().uniqueName(global_stack.getId() + "_extruder_" + str(idx + 1))
                         profile = InstanceContainer(profile_id)
                         profile.setName(quality_name)
@@ -353,7 +351,7 @@ class CuraContainerRegistry(ContainerRegistry):
     @override(ContainerRegistry)
     def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool:
         """Check if the metadata for a container is okay before adding it.
-        
+
         This overrides the one from UM.Settings.ContainerRegistry because we
         also require that the setting_version is correct.
         """
@@ -371,11 +369,11 @@ class CuraContainerRegistry(ContainerRegistry):
 
     def _configureProfile(self, profile: InstanceContainer, id_seed: str, new_name: str, machine_definition_id: str) -> Optional[str]:
         """Update an imported profile to match the current machine configuration.
-        
+
         :param profile: The profile to configure.
         :param id_seed: The base ID for the profile. May be changed so it does not conflict with existing containers.
         :param new_name: The new name for the profile.
-        
+
         :return: None if configuring was successful or an error message if an error occurred.
         """
 
@@ -438,7 +436,7 @@ class CuraContainerRegistry(ContainerRegistry):
 
     def _getIOPlugins(self, io_type):
         """Gets a list of profile writer plugins
-        
+
         :return: List of tuples of (plugin_id, meta_data).
         """
         plugin_registry = PluginRegistry.getInstance()

Some files were not shown because too many files changed in this diff