diff --git a/Jenkinsfile b/Jenkinsfile index 4f755dcae2..274e383ffa 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -52,10 +52,53 @@ parallel_nodes(['linux && cura', 'windows && cura']) { // Try and run the unit tests. If this stage fails, we consider the build to be "unstable". stage('Unit Test') { - try { - make('test') - } catch(e) { - currentBuild.result = "UNSTABLE" + if (isUnix()) { + // For Linux to show everything + def branch = env.BRANCH_NAME + if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) { + branch = "master" + } + def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}") + + try { + sh """ + cd .. + export PYTHONPATH=.:"${uranium_dir}" + ${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/pytest -x --verbose --full-trace --capture=no ./tests + """ + } catch(e) { + currentBuild.result = "UNSTABLE" + } + } + else { + // For Windows + try { + // This also does code style checks. + bat 'ctest -V' + } catch(e) { + currentBuild.result = "UNSTABLE" + } + } + } + + stage('Code Style') { + if (isUnix()) { + // For Linux to show everything + def branch = env.BRANCH_NAME + if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) { + branch = "master" + } + def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}") + + try { + sh """ + cd .. + export PYTHONPATH=.:"${uranium_dir}" + ${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/python3 run_mypy.py + """ + } catch(e) { + currentBuild.result = "UNSTABLE" + } } } } diff --git a/cmake/CuraTests.cmake b/cmake/CuraTests.cmake index 801f054bc3..30794ed608 100644 --- a/cmake/CuraTests.cmake +++ b/cmake/CuraTests.cmake @@ -34,7 +34,7 @@ function(cura_add_test) if (NOT ${test_exists}) add_test( NAME ${_NAME} - COMMAND ${PYTHON_EXECUTABLE} -m pytest --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY} + COMMAND ${PYTHON_EXECUTABLE} -m pytest --verbose --full-trace --capture=no --no-print-log --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY} ) set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C) set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}") diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 4059283a32..9d2f5c1f90 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -479,8 +479,6 @@ class BuildVolume(SceneNode): maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1) ) - self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds - self.updateNodeBoundaryCheck() def getBoundingBox(self) -> AxisAlignedBox: diff --git a/cura/Machines/ContainerNode.py b/cura/Machines/ContainerNode.py index 0d44c7c4a3..eef1c63127 100644 --- a/cura/Machines/ContainerNode.py +++ b/cura/Machines/ContainerNode.py @@ -9,9 +9,6 @@ from UM.ConfigurationErrorMessage import ConfigurationErrorMessage from UM.Logger import Logger from UM.Settings.InstanceContainer import InstanceContainer -if TYPE_CHECKING: - from cura.Machines.QualityGroup import QualityGroup - ## # A metadata / container combination. Use getContainer() to get the container corresponding to the metadata. @@ -24,11 +21,11 @@ if TYPE_CHECKING: # This is used in Variant, Material, and Quality Managers. # class ContainerNode: - __slots__ = ("_metadata", "container", "children_map") + __slots__ = ("_metadata", "_container", "children_map") def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None: self._metadata = metadata - self.container = None + self._container = None # type: Optional[InstanceContainer] self.children_map = OrderedDict() # type: ignore # This is because it's children are supposed to override it. ## Get an entry value from the metadata @@ -50,7 +47,7 @@ class ContainerNode: Logger.log("e", "Cannot get container for a ContainerNode without metadata.") return None - if self.container is None: + if self._container is None: container_id = self._metadata["id"] from UM.Settings.ContainerRegistry import ContainerRegistry container_list = ContainerRegistry.getInstance().findInstanceContainers(id = container_id) @@ -59,9 +56,9 @@ class ContainerNode: error_message = ConfigurationErrorMessage.getInstance() error_message.addFaultyContainers(container_id) return None - self.container = container_list[0] + self._container = container_list[0] - return self.container + return self._container def __str__(self) -> str: return "%s[%s]" % (self.__class__.__name__, self.getMetaDataEntry("id")) diff --git a/cura/Machines/MaterialManager.py b/cura/Machines/MaterialManager.py index 0ca9047620..be97fbc161 100644 --- a/cura/Machines/MaterialManager.py +++ b/cura/Machines/MaterialManager.py @@ -21,7 +21,6 @@ from .VariantType import VariantType if TYPE_CHECKING: from UM.Settings.DefinitionContainer import DefinitionContainer - from UM.Settings.InstanceContainer import InstanceContainer from cura.Settings.GlobalStack import GlobalStack from cura.Settings.ExtruderStack import ExtruderStack diff --git a/cura/Machines/Models/SettingVisibilityPresetsModel.py b/cura/Machines/Models/SettingVisibilityPresetsModel.py index 3062e83889..d5fa51d20a 100644 --- a/cura/Machines/Models/SettingVisibilityPresetsModel.py +++ b/cura/Machines/Models/SettingVisibilityPresetsModel.py @@ -58,7 +58,7 @@ class SettingVisibilityPresetsModel(ListModel): break return result - def _populate(self): + def _populate(self) -> None: from cura.CuraApplication import CuraApplication items = [] for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset): @@ -147,7 +147,7 @@ class SettingVisibilityPresetsModel(ListModel): def activePreset(self) -> str: return self._active_preset_item["id"] - def _onPreferencesChanged(self, name: str): + def _onPreferencesChanged(self, name: str) -> None: if name != "general/visible_settings": return diff --git a/cura/Machines/QualityChangesGroup.py b/cura/Machines/QualityChangesGroup.py index 3dcf2ab1c8..7844b935dc 100644 --- a/cura/Machines/QualityChangesGroup.py +++ b/cura/Machines/QualityChangesGroup.py @@ -24,9 +24,9 @@ class QualityChangesGroup(QualityGroup): ConfigurationErrorMessage.getInstance().addFaultyContainers(node.getMetaDataEntry("id")) return - if extruder_position is None: #Then we're a global quality changes profile. + if extruder_position is None: # Then we're a global quality changes profile. self.node_for_global = node - else: #This is an extruder's quality changes profile. + else: # This is an extruder's quality changes profile. self.nodes_for_extruders[extruder_position] = node def __str__(self) -> str: diff --git a/cura/Machines/QualityNode.py b/cura/Machines/QualityNode.py index a821a1e15d..991388a4bd 100644 --- a/cura/Machines/QualityNode.py +++ b/cura/Machines/QualityNode.py @@ -1,7 +1,7 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -from typing import Optional, Dict, cast +from typing import Optional, Dict, cast, Any from .ContainerNode import ContainerNode from .QualityChangesGroup import QualityChangesGroup @@ -12,21 +12,21 @@ from .QualityChangesGroup import QualityChangesGroup # class QualityNode(ContainerNode): - def __init__(self, metadata: Optional[dict] = None) -> None: + def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None: super().__init__(metadata = metadata) self.quality_type_map = {} # type: Dict[str, QualityNode] # quality_type -> QualityNode for InstanceContainer def getChildNode(self, child_key: str) -> Optional["QualityNode"]: return self.children_map.get(child_key) - def addQualityMetadata(self, quality_type: str, metadata: dict): + def addQualityMetadata(self, quality_type: str, metadata: Dict[str, Any]): if quality_type not in self.quality_type_map: self.quality_type_map[quality_type] = QualityNode(metadata) def getQualityNode(self, quality_type: str) -> Optional["QualityNode"]: return self.quality_type_map.get(quality_type) - def addQualityChangesMetadata(self, quality_type: str, metadata: dict): + def addQualityChangesMetadata(self, quality_type: str, metadata: Dict[str, Any]): if quality_type not in self.quality_type_map: self.quality_type_map[quality_type] = QualityNode() quality_type_node = self.quality_type_map[quality_type] diff --git a/cura/ObjectsModel.py b/cura/ObjectsModel.py index f3c703d424..8354540783 100644 --- a/cura/ObjectsModel.py +++ b/cura/ObjectsModel.py @@ -9,6 +9,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode from UM.Scene.Selection import Selection from UM.i18n import i18nCatalog +from collections import defaultdict catalog = i18nCatalog("cura") @@ -40,6 +41,8 @@ class ObjectsModel(ListModel): filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate") active_build_plate_number = self._build_plate_number group_nr = 1 + name_count_dict = defaultdict(int) + for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): if not isinstance(node, SceneNode): continue @@ -55,6 +58,7 @@ class ObjectsModel(ListModel): if not node.callDecoration("isGroup"): name = node.getName() + else: name = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = str(group_nr)) group_nr += 1 @@ -63,6 +67,14 @@ class ObjectsModel(ListModel): is_outside_build_area = node.isOutsideBuildArea() else: is_outside_build_area = False + + #check if we already have an instance of the object based on name + name_count_dict[name] += 1 + name_count = name_count_dict[name] + + if name_count > 1: + name = "{0}({1})".format(name, name_count-1) + node.setName(name) nodes.append({ "name": name, @@ -71,6 +83,7 @@ class ObjectsModel(ListModel): "buildPlateNumber": node_build_plate_number, "node": node }) + nodes = sorted(nodes, key=lambda n: n["name"]) self.setItems(nodes) diff --git a/cura/PrinterOutput/ConfigurationModel.py b/cura/PrinterOutput/ConfigurationModel.py index a3d6afd01d..89e609c913 100644 --- a/cura/PrinterOutput/ConfigurationModel.py +++ b/cura/PrinterOutput/ConfigurationModel.py @@ -13,20 +13,20 @@ class ConfigurationModel(QObject): configurationChanged = pyqtSignal() - def __init__(self): + def __init__(self) -> None: super().__init__() - self._printer_type = None + self._printer_type = "" self._extruder_configurations = [] # type: List[ExtruderConfigurationModel] - self._buildplate_configuration = None + self._buildplate_configuration = "" def setPrinterType(self, printer_type): self._printer_type = printer_type @pyqtProperty(str, fset = setPrinterType, notify = configurationChanged) - def printerType(self): + def printerType(self) -> str: return self._printer_type - def setExtruderConfigurations(self, extruder_configurations): + def setExtruderConfigurations(self, extruder_configurations: List["ExtruderConfigurationModel"]): if self._extruder_configurations != extruder_configurations: self._extruder_configurations = extruder_configurations @@ -39,16 +39,16 @@ class ConfigurationModel(QObject): def extruderConfigurations(self): return self._extruder_configurations - def setBuildplateConfiguration(self, buildplate_configuration): + def setBuildplateConfiguration(self, buildplate_configuration: str) -> None: self._buildplate_configuration = buildplate_configuration @pyqtProperty(str, fset = setBuildplateConfiguration, notify = configurationChanged) - def buildplateConfiguration(self): + def buildplateConfiguration(self) -> str: return self._buildplate_configuration ## This method is intended to indicate whether the configuration is valid or not. # The method checks if the mandatory fields are or not set - def isValid(self): + def isValid(self) -> bool: if not self._extruder_configurations: return False for configuration in self._extruder_configurations: diff --git a/cura/PrinterOutput/GenericOutputController.py b/cura/PrinterOutput/GenericOutputController.py index e6310e5bff..95e65b2f0b 100644 --- a/cura/PrinterOutput/GenericOutputController.py +++ b/cura/PrinterOutput/GenericOutputController.py @@ -66,7 +66,7 @@ class GenericOutputController(PrinterOutputController): self._output_device.sendCommand("G28 Z") def sendRawCommand(self, printer: "PrinterOutputModel", command: str): - self._output_device.sendCommand(command) + self._output_device.sendCommand(command.upper()) #Most printers only understand uppercase g-code commands. def setJobState(self, job: "PrintJobOutputModel", state: str): if state == "pause": diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index 94f86f19a3..d9c5707a03 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -53,21 +53,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._sending_gcode = False self._compressing_gcode = False self._gcode = [] # type: List[str] - self._connection_state_before_timeout = None # type: Optional[ConnectionState] - printer_type = self._properties.get(b"machine", b"").decode("utf-8") - printer_type_identifiers = { - "9066": "ultimaker3", - "9511": "ultimaker3_extended", - "9051": "ultimaker_s5" - } - self._printer_type = "Unknown" - for key, value in printer_type_identifiers.items(): - if printer_type.startswith(key): - self._printer_type = value - break - def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: raise NotImplementedError("requestWrite needs to be implemented") @@ -341,7 +328,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): @pyqtProperty(str, constant = True) def printerType(self) -> str: - return self._printer_type + return self._properties.get(b"printer_type", b"Unknown").decode("utf-8") ## IP adress of this printer @pyqtProperty(str, constant = True) diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py index 85d1e8e309..aca5d866be 100644 --- a/cura/Scene/ConvexHullDecorator.py +++ b/cura/Scene/ConvexHullDecorator.py @@ -15,8 +15,6 @@ import numpy from typing import TYPE_CHECKING, Any, Optional - - if TYPE_CHECKING: from UM.Scene.SceneNode import SceneNode from cura.Settings.GlobalStack import GlobalStack @@ -35,10 +33,11 @@ class ConvexHullDecorator(SceneNodeDecorator): # Make sure the timer is created on the main thread self._recompute_convex_hull_timer = None # type: Optional[QTimer] - Application.getInstance().callLater(self.createRecomputeConvexHullTimer) + + if Application.getInstance() is not None: + Application.getInstance().callLater(self.createRecomputeConvexHullTimer) self._raft_thickness = 0.0 - # For raft thickness, DRY self._build_volume = Application.getInstance().getBuildVolume() self._build_volume.raftThicknessChanged.connect(self._onChanged) @@ -72,14 +71,14 @@ class ConvexHullDecorator(SceneNodeDecorator): def __deepcopy__(self, memo): return ConvexHullDecorator() - ## Get the unmodified 2D projected convex hull of the node + ## Get the unmodified 2D projected convex hull of the node (if any) def getConvexHull(self) -> Optional[Polygon]: if self._node is None: return None hull = self._compute2DConvexHull() - if self._global_stack and self._node: + if self._global_stack and self._node and hull is not None: # Parent can be None if node is just loaded. if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")): hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32))) @@ -120,6 +119,7 @@ class ConvexHullDecorator(SceneNodeDecorator): return self._compute2DConvexHull() return None + ## The same as recomputeConvexHull, but using a timer if it was set. def recomputeConvexHullDelayed(self) -> None: if self._recompute_convex_hull_timer is not None: self._recompute_convex_hull_timer.start() @@ -142,13 +142,13 @@ class ConvexHullDecorator(SceneNodeDecorator): self._convex_hull_node = hull_node def _onSettingValueChanged(self, key: str, property_name: str) -> None: - if property_name != "value": #Not the value that was changed. + if property_name != "value": # Not the value that was changed. return if key in self._affected_settings: self._onChanged() if key in self._influencing_settings: - self._init2DConvexHullCache() #Invalidate the cache. + self._init2DConvexHullCache() # Invalidate the cache. self._onChanged() def _init2DConvexHullCache(self) -> None: @@ -161,7 +161,7 @@ class ConvexHullDecorator(SceneNodeDecorator): self._2d_convex_hull_mesh_world_transform = None self._2d_convex_hull_mesh_result = None - def _compute2DConvexHull(self) -> Polygon: + def _compute2DConvexHull(self) -> Optional[Polygon]: if self._node.callDecoration("isGroup"): points = numpy.zeros((0, 2), dtype=numpy.int32) for child in self._node.getChildren(): @@ -188,8 +188,6 @@ class ConvexHullDecorator(SceneNodeDecorator): else: offset_hull = None - mesh = None - world_transform = None if self._node.getMeshData(): mesh = self._node.getMeshData() world_transform = self._node.getWorldTransformation() @@ -242,17 +240,22 @@ class ConvexHullDecorator(SceneNodeDecorator): return Polygon(numpy.array(self._global_stack.getHeadAndFansCoordinates(), numpy.float32)) return Polygon() - def _compute2DConvexHeadFull(self): - return self._compute2DConvexHull().getMinkowskiHull(self._getHeadAndFans()) + def _compute2DConvexHeadFull(self) -> Optional[Polygon]: + convex_hull = self._compute2DConvexHull() + if convex_hull: + return convex_hull.getMinkowskiHull(self._getHeadAndFans()) + return None - def _compute2DConvexHeadMin(self): - headAndFans = self._getHeadAndFans() - mirrored = headAndFans.mirror([0, 0], [0, 1]).mirror([0, 0], [1, 0]) # Mirror horizontally & vertically. + def _compute2DConvexHeadMin(self) -> Optional[Polygon]: + head_and_fans = self._getHeadAndFans() + mirrored = head_and_fans.mirror([0, 0], [0, 1]).mirror([0, 0], [1, 0]) # Mirror horizontally & vertically. head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored) # Min head hull is used for the push free - min_head_hull = self._compute2DConvexHull().getMinkowskiHull(head_and_fans) - return min_head_hull + convex_hull = self._compute2DConvexHeadFull() + if convex_hull: + return convex_hull.getMinkowskiHull(head_and_fans) + return None ## Compensate given 2D polygon with adhesion margin # \return 2D polygon with added margin @@ -276,7 +279,7 @@ class ConvexHullDecorator(SceneNodeDecorator): else: raise Exception("Unknown bed adhesion type. Did you forget to update the convex hull calculations for your new bed adhesion type?") - # adjust head_and_fans with extra margin + # Adjust head_and_fans with extra margin if extra_margin > 0: extra_margin_polygon = Polygon.approximatedCircle(extra_margin) poly = poly.getMinkowskiHull(extra_margin_polygon) @@ -354,7 +357,7 @@ class ConvexHullDecorator(SceneNodeDecorator): # Limit_to_extruder is set. The global stack handles this then return self._global_stack.getProperty(setting_key, prop) - ## Returns true if node is a descendant or the same as the root node. + ## Returns True if node is a descendant or the same as the root node. def __isDescendant(self, root: "SceneNode", node: "SceneNode") -> bool: if node is None: return False diff --git a/cura/Settings/GlobalStack.py b/cura/Settings/GlobalStack.py index e2f7df41ea..517b45eb98 100755 --- a/cura/Settings/GlobalStack.py +++ b/cura/Settings/GlobalStack.py @@ -23,6 +23,7 @@ from .CuraContainerStack import CuraContainerStack if TYPE_CHECKING: from cura.Settings.ExtruderStack import ExtruderStack + ## Represents the Global or Machine stack and its related containers. # class GlobalStack(CuraContainerStack): diff --git a/plugins/CuraEngineBackend/Cura.proto b/plugins/CuraEngineBackend/Cura.proto index 69612210ec..292330576b 100644 --- a/plugins/CuraEngineBackend/Cura.proto +++ b/plugins/CuraEngineBackend/Cura.proto @@ -29,6 +29,7 @@ message Object bytes normals = 3; //An array of 3 floats. bytes indices = 4; //An array of ints. repeated Setting settings = 5; // Setting override per object, overruling the global settings. + string name = 6; } message Progress diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index dd0d9db0a2..688a2906e1 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -270,7 +270,7 @@ class StartSliceJob(Job): obj = group_message.addRepeatedMessage("objects") obj.id = id(object) - + obj.name = object.getName() indices = mesh_data.getIndices() if indices is not None: flat_verts = numpy.take(verts, indices.flatten(), axis=0) diff --git a/plugins/GCodeWriter/GCodeWriter.py b/plugins/GCodeWriter/GCodeWriter.py index 5d5e3578cd..3e5bf59e73 100644 --- a/plugins/GCodeWriter/GCodeWriter.py +++ b/plugins/GCodeWriter/GCodeWriter.py @@ -70,7 +70,7 @@ class GCodeWriter(MeshWriter): active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate scene = Application.getInstance().getController().getScene() if not hasattr(scene, "gcode_dict"): - self.setInformation(catalog.i18nc("@warning:status", "Please generate G-code before saving.")) + self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting.")) return False gcode_dict = getattr(scene, "gcode_dict") gcode_list = gcode_dict.get(active_build_plate, None) @@ -86,7 +86,7 @@ class GCodeWriter(MeshWriter): stream.write(settings) return True - self.setInformation(catalog.i18nc("@warning:status", "Please generate G-code before saving.")) + self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting.")) return False ## Create a new container with container 2 as base and container 1 written over it. diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index f4749a6747..9c070f2de2 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -260,6 +260,19 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): # or "Legacy" UM3 device. cluster_size = int(properties.get(b"cluster_size", -1)) + printer_type = properties.get(b"machine", b"").decode("utf-8") + printer_type_identifiers = { + "9066": "ultimaker3", + "9511": "ultimaker3_extended", + "9051": "ultimaker_s5" + } + + for key, value in printer_type_identifiers.items(): + if printer_type.startswith(key): + properties[b"printer_type"] = bytes(value, encoding="utf8") + break + else: + properties[b"printer_type"] = b"Unknown" if cluster_size >= 0: device = ClusterUM3OutputDevice.ClusterUM3OutputDevice(name, address, properties) else: diff --git a/resources/qml/Actions.qml b/resources/qml/Actions.qml index 82c177951d..7d898eed2c 100644 --- a/resources/qml/Actions.qml +++ b/resources/qml/Actions.qml @@ -75,6 +75,7 @@ Item Action { id:toggleFullScreenAction + shortcut: StandardKey.FullScreen; text: catalog.i18nc("@action:inmenu","Toggle Full Screen"); iconName: "view-fullscreen"; } diff --git a/resources/qml/Menus/ViewMenu.qml b/resources/qml/Menus/ViewMenu.qml index 6bbb0b1e2e..9a2e603673 100644 --- a/resources/qml/Menus/ViewMenu.qml +++ b/resources/qml/Menus/ViewMenu.qml @@ -73,4 +73,7 @@ Menu MenuSeparator {} MenuItem { action: Cura.Actions.expandSidebar; } + + MenuSeparator {} + MenuItem { action: Cura.Actions.toggleFullScreen; } } diff --git a/tests/Settings/TestGlobalStack.py b/tests/Settings/TestGlobalStack.py index f8052aa4bb..0f1579f78b 100755 --- a/tests/Settings/TestGlobalStack.py +++ b/tests/Settings/TestGlobalStack.py @@ -15,6 +15,7 @@ import UM.Settings.SettingDefinition #To add settings to the definition. from cura.Settings.cura_empty_instance_containers import empty_container + ## Gets an instance container with a specified container type. # # \param container_type The type metadata for the instance container. @@ -24,22 +25,27 @@ def getInstanceContainer(container_type) -> InstanceContainer: container.setMetaDataEntry("type", container_type) return container + class DefinitionContainerSubClass(DefinitionContainer): def __init__(self): super().__init__(container_id = "SubDefinitionContainer") + class InstanceContainerSubClass(InstanceContainer): def __init__(self, container_type): super().__init__(container_id = "SubInstanceContainer") self.setMetaDataEntry("type", container_type) + #############################START OF TEST CASES################################ + ## Tests whether adding a container is properly forbidden. def test_addContainer(global_stack): with pytest.raises(InvalidOperationError): global_stack.addContainer(unittest.mock.MagicMock()) + ## Tests adding extruders to the global stack. def test_addExtruder(global_stack): mock_definition = unittest.mock.MagicMock() @@ -67,6 +73,7 @@ def test_addExtruder(global_stack): # global_stack.addExtruder(unittest.mock.MagicMock()) assert len(global_stack.extruders) == 2 #Didn't add the faulty extruder. + #Tests setting user changes profiles to invalid containers. @pytest.mark.parametrize("container", [ getInstanceContainer(container_type = "wrong container type"), @@ -77,6 +84,7 @@ def test_constrainUserChangesInvalid(container, global_stack): with pytest.raises(InvalidContainerError): #Invalid container, should raise an error. global_stack.userChanges = container + #Tests setting user changes profiles. @pytest.mark.parametrize("container", [ getInstanceContainer(container_type = "user"), @@ -85,6 +93,7 @@ def test_constrainUserChangesInvalid(container, global_stack): def test_constrainUserChangesValid(container, global_stack): global_stack.userChanges = container #Should not give an error. + #Tests setting quality changes profiles to invalid containers. @pytest.mark.parametrize("container", [ getInstanceContainer(container_type = "wrong container type"), @@ -95,6 +104,7 @@ def test_constrainQualityChangesInvalid(container, global_stack): with pytest.raises(InvalidContainerError): #Invalid container, should raise an error. global_stack.qualityChanges = container + #Test setting quality changes profiles. @pytest.mark.parametrize("container", [ getInstanceContainer(container_type = "quality_changes"), @@ -103,6 +113,7 @@ def test_constrainQualityChangesInvalid(container, global_stack): def test_constrainQualityChangesValid(container, global_stack): global_stack.qualityChanges = container #Should not give an error. + #Tests setting quality profiles to invalid containers. @pytest.mark.parametrize("container", [ getInstanceContainer(container_type = "wrong container type"), @@ -113,6 +124,7 @@ def test_constrainQualityInvalid(container, global_stack): with pytest.raises(InvalidContainerError): #Invalid container, should raise an error. global_stack.quality = container + #Test setting quality profiles. @pytest.mark.parametrize("container", [ getInstanceContainer(container_type = "quality"), @@ -121,6 +133,7 @@ def test_constrainQualityInvalid(container, global_stack): def test_constrainQualityValid(container, global_stack): global_stack.quality = container #Should not give an error. + #Tests setting materials to invalid containers. @pytest.mark.parametrize("container", [ getInstanceContainer(container_type = "wrong container type"), @@ -131,6 +144,7 @@ def test_constrainMaterialInvalid(container, global_stack): with pytest.raises(InvalidContainerError): #Invalid container, should raise an error. global_stack.material = container + #Test setting materials. @pytest.mark.parametrize("container", [ getInstanceContainer(container_type = "material"), @@ -139,6 +153,7 @@ def test_constrainMaterialInvalid(container, global_stack): def test_constrainMaterialValid(container, global_stack): global_stack.material = container #Should not give an error. + #Tests setting variants to invalid containers. @pytest.mark.parametrize("container", [ getInstanceContainer(container_type = "wrong container type"), @@ -149,6 +164,7 @@ def test_constrainVariantInvalid(container, global_stack): with pytest.raises(InvalidContainerError): #Invalid container, should raise an error. global_stack.variant = container + #Test setting variants. @pytest.mark.parametrize("container", [ getInstanceContainer(container_type = "variant"), @@ -157,6 +173,7 @@ def test_constrainVariantInvalid(container, global_stack): def test_constrainVariantValid(container, global_stack): global_stack.variant = container #Should not give an error. + #Tests setting definition changes profiles to invalid containers. @pytest.mark.parametrize("container", [ getInstanceContainer(container_type = "wrong container type"), @@ -167,6 +184,7 @@ def test_constrainDefinitionChangesInvalid(container, global_stack): with pytest.raises(InvalidContainerError): #Invalid container, should raise an error. global_stack.definitionChanges = container + #Test setting definition changes profiles. @pytest.mark.parametrize("container", [ getInstanceContainer(container_type = "definition_changes"), @@ -175,6 +193,7 @@ def test_constrainDefinitionChangesInvalid(container, global_stack): def test_constrainDefinitionChangesValid(container, global_stack): global_stack.definitionChanges = container #Should not give an error. + #Tests setting definitions to invalid containers. @pytest.mark.parametrize("container", [ getInstanceContainer(container_type = "wrong class"), @@ -184,6 +203,7 @@ def test_constrainDefinitionInvalid(container, global_stack): with pytest.raises(InvalidContainerError): #Invalid container, should raise an error. global_stack.definition = container + #Test setting definitions. @pytest.mark.parametrize("container", [ DefinitionContainer(container_id = "DefinitionContainer"), @@ -192,6 +212,7 @@ def test_constrainDefinitionInvalid(container, global_stack): def test_constrainDefinitionValid(container, global_stack): global_stack.definition = container #Should not give an error. + ## Tests whether deserialising completes the missing containers with empty ones. The initial containers are just the # definition and the definition_changes (that cannot be empty after CURA-5281) def test_deserializeCompletesEmptyContainers(global_stack): @@ -207,6 +228,7 @@ def test_deserializeCompletesEmptyContainers(global_stack): continue assert global_stack.getContainer(container_type_index) == empty_container #All others need to be empty. + ## Tests whether an instance container with the wrong type gets removed when deserialising. def test_deserializeRemovesWrongInstanceContainer(global_stack): global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "wrong type") @@ -217,6 +239,7 @@ def test_deserializeRemovesWrongInstanceContainer(global_stack): assert global_stack.quality == global_stack._empty_instance_container #Replaced with empty. + ## Tests whether a container with the wrong class gets removed when deserialising. def test_deserializeRemovesWrongContainerClass(global_stack): global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = DefinitionContainer(container_id = "wrong class") @@ -227,6 +250,7 @@ def test_deserializeRemovesWrongContainerClass(global_stack): assert global_stack.quality == global_stack._empty_instance_container #Replaced with empty. + ## Tests whether an instance container in the definition spot results in an error. def test_deserializeWrongDefinitionClass(global_stack): global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Definition] = getInstanceContainer(container_type = "definition") #Correct type but wrong class. @@ -235,6 +259,7 @@ def test_deserializeWrongDefinitionClass(global_stack): with pytest.raises(UM.Settings.ContainerStack.InvalidContainerStackError): #Must raise an error that there is no definition container. global_stack.deserialize("") + ## Tests whether an instance container with the wrong type is moved into the correct slot by deserialising. def test_deserializeMoveInstanceContainer(global_stack): global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Quality] = getInstanceContainer(container_type = "material") #Not in the correct spot. @@ -246,6 +271,7 @@ def test_deserializeMoveInstanceContainer(global_stack): assert global_stack.quality == empty_container assert global_stack.material != empty_container + ## Tests whether a definition container in the wrong spot is moved into the correct spot by deserialising. def test_deserializeMoveDefinitionContainer(global_stack): global_stack._containers[cura.Settings.CuraContainerStack._ContainerIndexes.Material] = DefinitionContainer(container_id = "some definition") #Not in the correct spot. @@ -256,6 +282,7 @@ def test_deserializeMoveDefinitionContainer(global_stack): assert global_stack.material == empty_container assert global_stack.definition != empty_container + ## Tests whether getProperty properly applies the stack-like behaviour on its containers. def test_getPropertyFallThrough(global_stack): #A few instance container mocks to put in the stack. @@ -298,6 +325,7 @@ def test_getPropertyFallThrough(global_stack): global_stack.userChanges = mock_layer_heights[container_indexes.UserChanges] assert global_stack.getProperty("layer_height", "value") == container_indexes.UserChanges + ## In definitions, test whether having no resolve allows us to find the value. def test_getPropertyNoResolveInDefinition(global_stack): value = unittest.mock.MagicMock() #Just sets the value for bed temperature. @@ -307,6 +335,7 @@ def test_getPropertyNoResolveInDefinition(global_stack): global_stack.definition = value assert global_stack.getProperty("material_bed_temperature", "value") == 10 #No resolve, so fall through to value. + ## In definitions, when the value is asked and there is a resolve function, it must get the resolve first. def test_getPropertyResolveInDefinition(global_stack): resolve_and_value = unittest.mock.MagicMock() #Sets the resolve and value for bed temperature. @@ -316,6 +345,7 @@ def test_getPropertyResolveInDefinition(global_stack): global_stack.definition = resolve_and_value assert global_stack.getProperty("material_bed_temperature", "value") == 7.5 #Resolve wins in the definition. + ## In instance containers, when the value is asked and there is a resolve function, it must get the value first. def test_getPropertyResolveInInstance(global_stack): container_indices = cura.Settings.CuraContainerStack._ContainerIndexes @@ -342,6 +372,7 @@ def test_getPropertyResolveInInstance(global_stack): global_stack.userChanges = instance_containers[container_indices.UserChanges] assert global_stack.getProperty("material_bed_temperature", "value") == 5 + ## Tests whether the value in instances gets evaluated before the resolve in definitions. def test_getPropertyInstancesBeforeResolve(global_stack): value = unittest.mock.MagicMock() #Sets just the value. @@ -356,6 +387,7 @@ def test_getPropertyInstancesBeforeResolve(global_stack): assert global_stack.getProperty("material_bed_temperature", "value") == 10 + ## Tests whether the hasUserValue returns true for settings that are changed in the user-changes container. def test_hasUserValueUserChanges(global_stack): container = unittest.mock.MagicMock() @@ -367,6 +399,7 @@ def test_hasUserValueUserChanges(global_stack): assert not global_stack.hasUserValue("infill_sparse_density") assert not global_stack.hasUserValue("") + ## Tests whether the hasUserValue returns true for settings that are changed in the quality-changes container. def test_hasUserValueQualityChanges(global_stack): container = unittest.mock.MagicMock() @@ -378,6 +411,7 @@ def test_hasUserValueQualityChanges(global_stack): assert not global_stack.hasUserValue("infill_sparse_density") assert not global_stack.hasUserValue("") + ## Tests whether a container in some other place on the stack is correctly not recognised as user value. def test_hasNoUserValue(global_stack): container = unittest.mock.MagicMock() @@ -387,21 +421,25 @@ def test_hasNoUserValue(global_stack): assert not global_stack.hasUserValue("layer_height") #However this container is quality, so it's not a user value. + ## Tests whether inserting a container is properly forbidden. def test_insertContainer(global_stack): with pytest.raises(InvalidOperationError): global_stack.insertContainer(0, unittest.mock.MagicMock()) + ## Tests whether removing a container is properly forbidden. def test_removeContainer(global_stack): with pytest.raises(InvalidOperationError): global_stack.removeContainer(unittest.mock.MagicMock()) + ## Tests whether changing the next stack is properly forbidden. def test_setNextStack(global_stack): with pytest.raises(InvalidOperationError): global_stack.setNextStack(unittest.mock.MagicMock()) + ## Tests setting properties directly on the global stack. @pytest.mark.parametrize("key, property, value", [ ("layer_height", "value", 0.1337), @@ -415,6 +453,7 @@ def test_setPropertyUser(key, property, value, global_stack): user_changes.getMetaDataEntry = unittest.mock.MagicMock(return_value = "user") global_stack.userChanges = user_changes - global_stack.setProperty(key, property, value) #The actual test. + global_stack.setProperty(key, property, value) # The actual test. - global_stack.userChanges.setProperty.assert_called_once_with(key, property, value, None, False) #Make sure that the user container gets a setProperty call. \ No newline at end of file + # Make sure that the user container gets a setProperty call. + global_stack.userChanges.setProperty.assert_called_once_with(key, property, value, None, False) \ No newline at end of file