Browse Source

Merge branch 'master' into bremco-graphics_buffer_update

Remco Burema 3 years ago
parent
commit
6dff2bebb5

+ 2 - 2
CITATION.cff

@@ -7,5 +7,5 @@ license: "LGPL-3.0"
 message: "If you use this software, please cite it using these metadata."
 message: "If you use this software, please cite it using these metadata."
 repository-code: "https://github.com/ultimaker/cura/"
 repository-code: "https://github.com/ultimaker/cura/"
 title: "Ultimaker Cura"
 title: "Ultimaker Cura"
-version: "4.10.0"
-...
+version: "4.12.0"
+...

+ 1 - 1
README.md

@@ -2,7 +2,7 @@ Cura
 ====
 ====
 Ultimaker Cura is a state-of-the-art slicer application to prepare your 3D models for printing with a 3D printer. With hundreds of settings and hundreds of community-managed print profiles, Ultimaker Cura is sure to lead your next project to a success.
 Ultimaker Cura is a state-of-the-art slicer application to prepare your 3D models for printing with a 3D printer. With hundreds of settings and hundreds of community-managed print profiles, Ultimaker Cura is sure to lead your next project to a success.
 
 
-![Screenshot](screenshot.png)
+![Screenshot](cura-logo.PNG)
 
 
 Logging Issues
 Logging Issues
 ------------
 ------------

BIN
cura-logo.PNG


+ 1 - 1
cura/API/Account.py

@@ -62,7 +62,7 @@ class Account(QObject):
     updatePackagesEnabledChanged = pyqtSignal(bool)
     updatePackagesEnabledChanged = pyqtSignal(bool)
 
 
     CLIENT_SCOPES = "account.user.read drive.backup.read drive.backup.write packages.download " \
     CLIENT_SCOPES = "account.user.read drive.backup.read drive.backup.write packages.download " \
-                    "packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write " \
+                    "packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write connect.material.write " \
                     "library.project.read library.project.write cura.printjob.read cura.printjob.write " \
                     "library.project.read library.project.write cura.printjob.read cura.printjob.write " \
                     "cura.mesh.read cura.mesh.write"
                     "cura.mesh.read cura.mesh.write"
 
 

+ 1 - 1
cura/Arranging/Nest2DArrange.py

@@ -159,4 +159,4 @@ def arrange(nodes_to_arrange: List["SceneNode"],
 
 
     grouped_operation, not_fit_count = createGroupOperationForArrange(nodes_to_arrange, build_volume, fixed_nodes, factor, add_new_nodes_in_scene)
     grouped_operation, not_fit_count = createGroupOperationForArrange(nodes_to_arrange, build_volume, fixed_nodes, factor, add_new_nodes_in_scene)
     grouped_operation.push()
     grouped_operation.push()
-    return not_fit_count != 0
+    return not_fit_count == 0

+ 1 - 1
cura/BuildVolume.py

@@ -289,7 +289,7 @@ class BuildVolume(SceneNode):
                 # Mark the node as outside build volume if the set extruder is disabled
                 # Mark the node as outside build volume if the set extruder is disabled
                 extruder_position = node.callDecoration("getActiveExtruderPosition")
                 extruder_position = node.callDecoration("getActiveExtruderPosition")
                 try:
                 try:
-                    if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled:
+                    if not self._global_container_stack.extruderList[int(extruder_position)].isEnabled and not node.callDecoration("isGroup"):
                         node.setOutsideBuildArea(True)
                         node.setOutsideBuildArea(True)
                         continue
                         continue
                 except IndexError:  # Happens when the extruder list is too short. We're not done building the printer in memory yet.
                 except IndexError:  # Happens when the extruder list is too short. We're not done building the printer in memory yet.

+ 1 - 0
cura/CuraApplication.py

@@ -162,6 +162,7 @@ class CuraApplication(QtApplication):
         self.default_theme = "cura-light"
         self.default_theme = "cura-light"
 
 
         self.change_log_url = "https://ultimaker.com/ultimaker-cura-latest-features?utm_source=cura&utm_medium=software&utm_campaign=cura-update-features"
         self.change_log_url = "https://ultimaker.com/ultimaker-cura-latest-features?utm_source=cura&utm_medium=software&utm_campaign=cura-update-features"
+        self.beta_change_log_url = "https://ultimaker.com/ultimaker-cura-beta-features?utm_source=cura&utm_medium=software&utm_campaign=cura-update-features"
 
 
         self._boot_loading_time = time.time()
         self._boot_loading_time = time.time()
 
 

+ 45 - 3
cura/Machines/Models/GlobalStacksModel.py

@@ -1,7 +1,8 @@
-# Copyright (c) 2018 Ultimaker B.V.
+# Copyright (c) 2021 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
 
 
-from PyQt5.QtCore import Qt, QTimer
+from PyQt5.QtCore import Qt, QTimer, pyqtProperty, pyqtSignal
+from typing import Optional
 
 
 from UM.Qt.ListModel import ListModel
 from UM.Qt.ListModel import ListModel
 from UM.i18n import i18nCatalog
 from UM.i18n import i18nCatalog
@@ -20,6 +21,7 @@ class GlobalStacksModel(ListModel):
     MetaDataRole = Qt.UserRole + 5
     MetaDataRole = Qt.UserRole + 5
     DiscoverySourceRole = Qt.UserRole + 6  # For separating local and remote printers in the machine management page
     DiscoverySourceRole = Qt.UserRole + 6  # For separating local and remote printers in the machine management page
     RemovalWarningRole = Qt.UserRole + 7
     RemovalWarningRole = Qt.UserRole + 7
+    IsOnlineRole = Qt.UserRole + 8
 
 
     def __init__(self, parent = None) -> None:
     def __init__(self, parent = None) -> None:
         super().__init__(parent)
         super().__init__(parent)
@@ -31,18 +33,49 @@ class GlobalStacksModel(ListModel):
         self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection")
         self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection")
         self.addRoleName(self.MetaDataRole, "metadata")
         self.addRoleName(self.MetaDataRole, "metadata")
         self.addRoleName(self.DiscoverySourceRole, "discoverySource")
         self.addRoleName(self.DiscoverySourceRole, "discoverySource")
+        self.addRoleName(self.IsOnlineRole, "isOnline")
 
 
         self._change_timer = QTimer()
         self._change_timer = QTimer()
         self._change_timer.setInterval(200)
         self._change_timer.setInterval(200)
         self._change_timer.setSingleShot(True)
         self._change_timer.setSingleShot(True)
         self._change_timer.timeout.connect(self._update)
         self._change_timer.timeout.connect(self._update)
 
 
+        self._filter_connection_type = None  # type: Optional[ConnectionType]
+        self._filter_online_only = False
+
         # Listen to changes
         # Listen to changes
         CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
         CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
         CuraContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
         CuraContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
         CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
         CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
         self._updateDelayed()
         self._updateDelayed()
 
 
+    filterConnectionTypeChanged = pyqtSignal()
+    def setFilterConnectionType(self, new_filter: Optional[ConnectionType]) -> None:
+        self._filter_connection_type = new_filter
+
+    @pyqtProperty(int, fset = setFilterConnectionType, notify = filterConnectionTypeChanged)
+    def filterConnectionType(self) -> int:
+        """
+        The connection type to filter the list of printers by.
+
+        Only printers that match this connection type will be listed in the
+        model.
+        """
+        if self._filter_connection_type is None:
+            return -1
+        return self._filter_connection_type.value
+
+    filterOnlineOnlyChanged = pyqtSignal()
+    def setFilterOnlineOnly(self, new_filter: bool) -> None:
+        self._filter_online_only = new_filter
+
+    @pyqtProperty(bool, fset = setFilterOnlineOnly, notify = filterOnlineOnlyChanged)
+    def filterOnlineOnly(self) -> bool:
+        """
+        Whether to filter the global stacks to show only printers that are online.
+        """
+        return self._filter_online_only
+
     def _onContainerChanged(self, container) -> None:
     def _onContainerChanged(self, container) -> None:
         """Handler for container added/removed events from registry"""
         """Handler for container added/removed events from registry"""
 
 
@@ -58,6 +91,10 @@ class GlobalStacksModel(ListModel):
 
 
         container_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine")
         container_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine")
         for container_stack in container_stacks:
         for container_stack in container_stacks:
+            if self._filter_connection_type is not None:  # We want to filter on connection types.
+                if not any((connection_type == self._filter_connection_type for connection_type in container_stack.configuredConnectionTypes)):
+                    continue  # No connection type on this printer matches the filter.
+
             has_remote_connection = False
             has_remote_connection = False
 
 
             for connection_type in container_stack.configuredConnectionTypes:
             for connection_type in container_stack.configuredConnectionTypes:
@@ -67,6 +104,10 @@ class GlobalStacksModel(ListModel):
             if parseBool(container_stack.getMetaDataEntry("hidden", False)):
             if parseBool(container_stack.getMetaDataEntry("hidden", False)):
                 continue
                 continue
 
 
+            is_online = container_stack.getMetaDataEntry("is_online", False)
+            if self._filter_online_only and not is_online:
+                continue
+
             device_name = container_stack.getMetaDataEntry("group_name", container_stack.getName())
             device_name = container_stack.getMetaDataEntry("group_name", container_stack.getName())
             section_name = "Connected printers" if has_remote_connection else "Preset printers"
             section_name = "Connected printers" if has_remote_connection else "Preset printers"
             section_name = self._catalog.i18nc("@info:title", section_name)
             section_name = self._catalog.i18nc("@info:title", section_name)
@@ -82,6 +123,7 @@ class GlobalStacksModel(ListModel):
                           "hasRemoteConnection": has_remote_connection,
                           "hasRemoteConnection": has_remote_connection,
                           "metadata": container_stack.getMetaData().copy(),
                           "metadata": container_stack.getMetaData().copy(),
                           "discoverySource": section_name,
                           "discoverySource": section_name,
-                          "removalWarning": removal_warning})
+                          "removalWarning": removal_warning,
+                          "isOnline": is_online})
         items.sort(key=lambda i: (not i["hasRemoteConnection"], i["name"]))
         items.sort(key=lambda i: (not i["hasRemoteConnection"], i["name"]))
         self.setItems(items)
         self.setItems(items)

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

@@ -107,7 +107,7 @@ class IntentCategoryModel(ListModel):
             qualities = IntentModel()
             qualities = IntentModel()
             qualities.setIntentCategory(category)
             qualities.setIntentCategory(category)
             result.append({
             result.append({
-                "name": IntentCategoryModel.translation(category, "name", catalog.i18nc("@label", "Unknown")),
+                "name": IntentCategoryModel.translation(category, "name", category),
                 "description": IntentCategoryModel.translation(category, "description", None),
                 "description": IntentCategoryModel.translation(category, "description", None),
                 "intent_category": category,
                 "intent_category": category,
                 "weight": list(IntentCategoryModel._get_translations().keys()).index(category),
                 "weight": list(IntentCategoryModel._get_translations().keys()).index(category),

+ 12 - 52
cura/Machines/Models/MaterialManagementModel.py

@@ -2,21 +2,21 @@
 # Cura is released under the terms of the LGPLv3 or higher.
 # Cura is released under the terms of the LGPLv3 or higher.
 
 
 import copy  # To duplicate materials.
 import copy  # To duplicate materials.
-from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
+from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
+from PyQt5.QtGui import QDesktopServices
 from typing import Any, Dict, Optional, TYPE_CHECKING
 from typing import Any, Dict, Optional, TYPE_CHECKING
 import uuid  # To generate new GUIDs for new materials.
 import uuid  # To generate new GUIDs for new materials.
-import zipfile  # To export all materials in a .zip archive.
-
-from PyQt5.QtGui import QDesktopServices
 
 
+from UM.Message import Message
 from UM.i18n import i18nCatalog
 from UM.i18n import i18nCatalog
 from UM.Logger import Logger
 from UM.Logger import Logger
-from UM.Message import Message
+from UM.Resources import Resources  # To find QML files.
 from UM.Signal import postponeSignals, CompressTechnique
 from UM.Signal import postponeSignals, CompressTechnique
 
 
-import cura.CuraApplication  # Imported like this to prevent circular imports.
+import cura.CuraApplication  # Imported like this to prevent cirmanagecular imports.
 from cura.Machines.ContainerTree import ContainerTree
 from cura.Machines.ContainerTree import ContainerTree
 from cura.Settings.CuraContainerRegistry import CuraContainerRegistry  # To find the sets of materials belonging to each other, and currently loaded extruder stacks.
 from cura.Settings.CuraContainerRegistry import CuraContainerRegistry  # To find the sets of materials belonging to each other, and currently loaded extruder stacks.
+from cura.UltimakerCloud.CloudMaterialSync import CloudMaterialSync
 
 
 if TYPE_CHECKING:
 if TYPE_CHECKING:
     from cura.Machines.MaterialNode import MaterialNode
     from cura.Machines.MaterialNode import MaterialNode
@@ -33,6 +33,7 @@ class MaterialManagementModel(QObject):
 
 
     def __init__(self, parent: Optional[QObject] = None) -> None:
     def __init__(self, parent: Optional[QObject] = None) -> None:
         super().__init__(parent = parent)
         super().__init__(parent = parent)
+        self._material_sync = CloudMaterialSync(parent=self)
         self._checkIfNewMaterialsWereInstalled()
         self._checkIfNewMaterialsWereInstalled()
 
 
     def _checkIfNewMaterialsWereInstalled(self) -> None:
     def _checkIfNewMaterialsWereInstalled(self) -> None:
@@ -89,6 +90,7 @@ class MaterialManagementModel(QObject):
         elif sync_message_action == "learn_more":
         elif sync_message_action == "learn_more":
             QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360013137919?utm_source=cura&utm_medium=software&utm_campaign=sync-material-printer-message"))
             QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360013137919?utm_source=cura&utm_medium=software&utm_campaign=sync-material-printer-message"))
 
 
+
     @pyqtSlot("QVariant", result = bool)
     @pyqtSlot("QVariant", result = bool)
     def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool:
     def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool:
         """Can a certain material be deleted, or is it still in use in one of the container stacks anywhere?
         """Can a certain material be deleted, or is it still in use in one of the container stacks anywhere?
@@ -323,52 +325,10 @@ class MaterialManagementModel(QObject):
         except ValueError:  # Material was not in the favorites list.
         except ValueError:  # Material was not in the favorites list.
             Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file))
             Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file))
 
 
-    @pyqtSlot(result = QUrl)
-    def getPreferredExportAllPath(self) -> QUrl:
+    @pyqtSlot()
+    def openSyncAllWindow(self) -> None:
         """
         """
-        Get the preferred path to export materials to.
-
-        If there is a removable drive, that should be the preferred path. Otherwise it should be the most recent local
-        file path.
-        :return: The preferred path to export all materials to.
-        """
-        cura_application = cura.CuraApplication.CuraApplication.getInstance()
-        device_manager = cura_application.getOutputDeviceManager()
-        devices = device_manager.getOutputDevices()
-        for device in devices:
-            if device.__class__.__name__ == "RemovableDriveOutputDevice":
-                return QUrl.fromLocalFile(device.getId())
-        else:  # No removable drives? Use local path.
-            return cura_application.getDefaultPath("dialog_material_path")
-
-    @pyqtSlot(QUrl)
-    def exportAll(self, file_path: QUrl) -> None:
-        """
-        Export all materials to a certain file path.
-        :param file_path: The path to export the materials to.
+        Opens the window to sync all materials.
         """
         """
-        registry = CuraContainerRegistry.getInstance()
+        self._material_sync.openSyncAllWindow()
 
 
-        try:
-            archive = zipfile.ZipFile(file_path.toLocalFile(), "w", compression = zipfile.ZIP_DEFLATED)
-        except OSError as e:
-            Logger.log("e", f"Can't write to destination {file_path.toLocalFile()}: {type(e)} - {str(e)}")
-            error_message = Message(
-                text = catalog.i18nc("@error:text Followed by an error message of why it could not save", "Could not save material archive to {filename}:").format(filename = file_path.toLocalFile()) + " " + str(e),
-                title = catalog.i18nc("@error:title", "Failed to save material archive"),
-                message_type = Message.MessageType.ERROR
-            )
-            error_message.show()
-            return
-        for metadata in registry.findInstanceContainersMetadata(type = "material"):
-            if metadata["base_file"] != metadata["id"]:  # Only process base files.
-                continue
-            if metadata["id"] == "empty_material":  # Don't export the empty material.
-                continue
-            material = registry.findContainers(id = metadata["id"])[0]
-            suffix = registry.getMimeTypeForContainer(type(material)).preferredSuffix
-            filename = metadata["id"] + "." + suffix
-            try:
-                archive.writestr(filename, material.serialize())
-            except OSError as e:
-                Logger.log("e", f"An error has occurred while writing the material \'{metadata['id']}\' in the file \'{filename}\': {e}.")

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