diff --git a/cura/API/Account.py b/cura/API/Account.py index 8a8b708cfa..30401454b3 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -76,6 +76,9 @@ class Account(QObject): self._error_message.hide() self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed")) self._error_message.show() + self._logged_in = False + self.loginStateChanged.emit(False) + return if self._logged_in != logged_in: self._logged_in = logged_in diff --git a/cura/Arranging/ArrangeObjectsJob.py b/cura/Arranging/ArrangeObjectsJob.py index ce11556b5b..aef051c838 100644 --- a/cura/Arranging/ArrangeObjectsJob.py +++ b/cura/Arranging/ArrangeObjectsJob.py @@ -1,4 +1,4 @@ -# Copyright (c) 2017 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. from UM.Application import Application @@ -39,10 +39,17 @@ class ArrangeObjectsJob(Job): arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes, min_offset = self._min_offset) + # Build set to exclude children (those get arranged together with the parents). + included_as_child = set() + for node in self._nodes: + included_as_child.update(node.getAllChildren()) + # Collect nodes to be placed nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) for node in self._nodes: - offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset) + if node in included_as_child: + continue + offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset, include_children = True) if offset_shape_arr is None: Logger.log("w", "Node [%s] could not be converted to an array for arranging...", str(node)) continue diff --git a/cura/Arranging/ShapeArray.py b/cura/Arranging/ShapeArray.py index ab785cc3e1..64b78d6f17 100644 --- a/cura/Arranging/ShapeArray.py +++ b/cura/Arranging/ShapeArray.py @@ -42,7 +42,7 @@ class ShapeArray: # \param min_offset offset for the offset ShapeArray # \param scale scale the coordinates @classmethod - def fromNode(cls, node, min_offset, scale = 0.5): + def fromNode(cls, node, min_offset, scale = 0.5, include_children = False): transform = node._transformation transform_x = transform._data[0][3] transform_y = transform._data[2][3] @@ -52,6 +52,21 @@ class ShapeArray: return None, None # For one_at_a_time printing you need the convex hull head. hull_head_verts = node.callDecoration("getConvexHullHead") or hull_verts + if hull_head_verts is None: + hull_head_verts = Polygon() + + # If the child-nodes are included, adjust convex hulls as well: + if include_children: + children = node.getAllChildren() + if not children is None: + for child in children: + # 'Inefficient' combination of convex hulls through known code rather than mess it up: + child_hull = child.callDecoration("getConvexHull") + if not child_hull is None: + hull_verts = hull_verts.unionConvexHulls(child_hull) + child_hull_head = child.callDecoration("getConvexHullHead") or child_hull + if not child_hull_head is None: + hull_head_verts = hull_head_verts.unionConvexHulls(child_hull_head) offset_verts = hull_head_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset)) offset_points = copy.deepcopy(offset_verts._points) # x, y diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index fb13a32732..aa6b7fb63c 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -1,6 +1,6 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. - +from UM.Scene.Camera import Camera from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Settings.ExtruderManager import ExtruderManager from UM.Application import Application #To modify the maximum zoom level. @@ -112,8 +112,6 @@ class BuildVolume(SceneNode): self._setting_change_timer.setSingleShot(True) self._setting_change_timer.timeout.connect(self._onSettingChangeTimerFinished) - - # Must be after setting _build_volume_message, apparently that is used in getMachineManager. # activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality. # Therefore this works. @@ -131,7 +129,9 @@ class BuildVolume(SceneNode): def _onSceneChanged(self, source): if self._global_container_stack: - self._scene_change_timer.start() + # Ignore anything that is not something we can slice in the first place! + if source.callDecoration("isSliceable"): + self._scene_change_timer.start() def _onSceneChangeTimerFinished(self): root = self._application.getController().getScene().getRoot() @@ -148,7 +148,7 @@ class BuildVolume(SceneNode): if active_extruder_changed is not None: node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild) node.decoratorsChanged.disconnect(self._updateNodeListeners) - self._updateDisallowedAreasAndRebuild() # make sure we didn't miss anything before we updated the node listeners + self.rebuild() self._scene_objects = new_scene_objects self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered. @@ -667,6 +667,7 @@ class BuildVolume(SceneNode): # ``_updateDisallowedAreas`` method itself shouldn't call ``rebuild``, # since there may be other changes before it needs to be rebuilt, which # would hit performance. + def _updateDisallowedAreasAndRebuild(self): self._updateDisallowedAreas() self._updateRaftThickness() diff --git a/cura/Machines/Models/MultiBuildPlateModel.py b/cura/Machines/Models/MultiBuildPlateModel.py index 958e93837a..add960a545 100644 --- a/cura/Machines/Models/MultiBuildPlateModel.py +++ b/cura/Machines/Models/MultiBuildPlateModel.py @@ -4,6 +4,7 @@ from PyQt5.QtCore import QTimer, pyqtSignal, pyqtProperty from UM.Application import Application +from UM.Scene.Camera import Camera from UM.Scene.Selection import Selection from UM.Qt.ListModel import ListModel @@ -34,8 +35,9 @@ class MultiBuildPlateModel(ListModel): self._active_build_plate = -1 def setMaxBuildPlate(self, max_build_plate): - self._max_build_plate = max_build_plate - self.maxBuildPlateChanged.emit() + if self._max_build_plate != max_build_plate: + self._max_build_plate = max_build_plate + self.maxBuildPlateChanged.emit() ## Return the highest build plate number @pyqtProperty(int, notify = maxBuildPlateChanged) @@ -43,15 +45,17 @@ class MultiBuildPlateModel(ListModel): return self._max_build_plate def setActiveBuildPlate(self, nr): - self._active_build_plate = nr - self.activeBuildPlateChanged.emit() + if self._active_build_plate != nr: + self._active_build_plate = nr + self.activeBuildPlateChanged.emit() @pyqtProperty(int, notify = activeBuildPlateChanged) def activeBuildPlate(self): return self._active_build_plate def _updateSelectedObjectBuildPlateNumbersDelayed(self, *args): - self._update_timer.start() + if not isinstance(args[0], Camera): + self._update_timer.start() def _updateSelectedObjectBuildPlateNumbers(self, *args): result = set() diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index a76e8cf304..3c2f66d037 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -1,5 +1,6 @@ -# Copyright (c) 2018 Ultimaker B.V. +# Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. + import json import webbrowser from datetime import datetime, timedelta @@ -9,12 +10,16 @@ import requests.exceptions from UM.Logger import Logger +from UM.Message import Message from UM.Signal import Signal from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT from cura.OAuth2.Models import AuthenticationResponse +from UM.i18n import i18nCatalog +i18n_catalog = i18nCatalog("cura") + if TYPE_CHECKING: from cura.OAuth2.Models import UserProfile, OAuth2Settings from UM.Preferences import Preferences @@ -41,6 +46,14 @@ class AuthorizationService: self._preferences = preferences self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True) + self._unable_to_get_data_message = None # type: Optional[Message] + + self.onAuthStateChanged.connect(self._authChanged) + + def _authChanged(self, logged_in): + if logged_in and self._unable_to_get_data_message is not None: + self._unable_to_get_data_message.hide() + def initialize(self, preferences: Optional["Preferences"] = None) -> None: if preferences is not None: self._preferences = preferences @@ -162,7 +175,18 @@ class AuthorizationService: preferences_data = json.loads(self._preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY)) if preferences_data: self._auth_data = AuthenticationResponse(**preferences_data) - self.onAuthStateChanged.emit(logged_in=True) + # Also check if we can actually get the user profile information. + user_profile = self.getUserProfile() + if user_profile is not None: + self.onAuthStateChanged.emit(logged_in=True) + else: + if self._unable_to_get_data_message is not None: + self._unable_to_get_data_message.hide() + + self._unable_to_get_data_message = Message(i18n_catalog.i18nc("@info", "Unable to reach the Ultimaker account server."), title = i18n_catalog.i18nc("@info:title", "Warning")) + self._unable_to_get_data_message.addAction("retry", i18n_catalog.i18nc("@action:button", "Retry"), "[no_icon]", "[no_description]") + self._unable_to_get_data_message.actionTriggered.connect(self._onMessageActionTriggered) + self._unable_to_get_data_message.show() except ValueError: Logger.logException("w", "Could not load auth data from preferences") @@ -179,3 +203,7 @@ class AuthorizationService: else: self._user_profile = None self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY) + + def _onMessageActionTriggered(self, _, action): + if action == "retry": + self.loadAuthDataFromPreferences() diff --git a/cura/ObjectsModel.py b/cura/ObjectsModel.py index 8354540783..f9f923b31d 100644 --- a/cura/ObjectsModel.py +++ b/cura/ObjectsModel.py @@ -5,6 +5,7 @@ from PyQt5.QtCore import QTimer from UM.Application import Application from UM.Qt.ListModel import ListModel +from UM.Scene.Camera import Camera from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.SceneNode import SceneNode from UM.Scene.Selection import Selection @@ -19,19 +20,24 @@ class ObjectsModel(ListModel): def __init__(self): super().__init__() - Application.getInstance().getController().getScene().sceneChanged.connect(self._updateDelayed) + Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSceneDelayed) Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed) self._update_timer = QTimer() - self._update_timer.setInterval(100) + self._update_timer.setInterval(200) self._update_timer.setSingleShot(True) self._update_timer.timeout.connect(self._update) self._build_plate_number = -1 def setActiveBuildPlate(self, nr): - self._build_plate_number = nr - self._update() + if self._build_plate_number != nr: + self._build_plate_number = nr + self._update() + + def _updateSceneDelayed(self, source): + if not isinstance(source, Camera): + self._update_timer.start() def _updateDelayed(self, *args): self._update_timer.start() diff --git a/cura/PlatformPhysics.py b/cura/PlatformPhysics.py index 8ddcdbfb2f..8fffac4501 100755 --- a/cura/PlatformPhysics.py +++ b/cura/PlatformPhysics.py @@ -17,7 +17,6 @@ from cura.Scene import ZOffsetDecorator import random # used for list shuffling - class PlatformPhysics: def __init__(self, controller, volume): super().__init__() @@ -40,8 +39,9 @@ class PlatformPhysics: Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True) def _onSceneChanged(self, source): - if not source.getMeshData(): + if not source.callDecoration("isSliceable"): return + self._change_timer.start() def _onChangeTimerFinished(self): diff --git a/cura/PrinterOutput/PrinterOutputModel.py b/cura/PrinterOutput/PrinterOutputModel.py index 4189b9fcbd..ea8fca5bac 100644 --- a/cura/PrinterOutput/PrinterOutputModel.py +++ b/cura/PrinterOutput/PrinterOutputModel.py @@ -44,7 +44,7 @@ class PrinterOutputModel(QObject): self._printer_state = "unknown" self._is_preheating = False self._printer_type = "" - self._buildplate_name = "" + self._buildplate = "" self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._extruders] @@ -86,12 +86,12 @@ class PrinterOutputModel(QObject): @pyqtProperty(str, notify = buildplateChanged) def buildplate(self) -> str: - return self._buildplate_name + return self._buildplate - def updateBuildplateName(self, buildplate_name: str) -> None: - if self._buildplate_name != buildplate_name: - self._buildplate_name = buildplate_name - self._printer_configuration.buildplateConfiguration = self._buildplate_name + def updateBuildplate(self, buildplate: str) -> None: + if self._buildplate != buildplate: + self._buildplate = buildplate + self._printer_configuration.buildplateConfiguration = self._buildplate self.buildplateChanged.emit() self.configurationChanged.emit() diff --git a/cura/Scene/CuraSceneController.py b/cura/Scene/CuraSceneController.py index 4b19271538..9f26ea7cc3 100644 --- a/cura/Scene/CuraSceneController.py +++ b/cura/Scene/CuraSceneController.py @@ -3,6 +3,7 @@ from UM.Logger import Logger from PyQt5.QtCore import Qt, pyqtSlot, QObject from PyQt5.QtWidgets import QApplication +from UM.Scene.Camera import Camera from cura.ObjectsModel import ObjectsModel from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel @@ -33,7 +34,7 @@ class CuraSceneController(QObject): source = args[0] else: source = None - if not isinstance(source, SceneNode): + if not isinstance(source, SceneNode) or isinstance(source, Camera): return max_build_plate = self._calcMaxBuildPlate() changed = False diff --git a/cura/Utils/Threading.py b/cura/Utils/Threading.py index 3cd6200513..550a5421ff 100644 --- a/cura/Utils/Threading.py +++ b/cura/Utils/Threading.py @@ -1,3 +1,4 @@ +import functools import threading from cura.CuraApplication import CuraApplication @@ -6,7 +7,7 @@ from cura.CuraApplication import CuraApplication # # HACK: # -# In project loading, when override the existing machine is selected, the stacks and containers that are correctly +# In project loading, when override the existing machine is selected, the stacks and containers that are currently # active in the system will be overridden at runtime. Because the project loading is done in a different thread than # the Qt thread, something else can kick in the middle of the process. One of them is the rendering. It will access # the current stacks and container, which have not completely been updated yet, so Cura will crash in this case. @@ -22,7 +23,13 @@ class InterCallObject: def call_on_qt_thread(func): + @functools.wraps(func) def _call_on_qt_thread_wrapper(*args, **kwargs): + # If the current thread is the main thread, which is the Qt thread, directly call the function. + current_thread = threading.current_thread() + if isinstance(current_thread, threading._MainThread): + return func(*args, **kwargs) + def _handle_call(ico, *args, **kwargs): ico.result = func(*args, **kwargs) ico.finish_event.set() diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index 7c1f8faa83..6a828e32d6 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -40,10 +40,13 @@ class DriveApiService: if not access_token: Logger.log("w", "Could not get access token.") return [] - - backup_list_request = requests.get(self.BACKUP_URL, headers = { - "Authorization": "Bearer {}".format(access_token) - }) + try: + backup_list_request = requests.get(self.BACKUP_URL, headers = { + "Authorization": "Bearer {}".format(access_token) + }) + except requests.exceptions.ConnectionError: + Logger.log("w", "Unable to connect with the server.") + return [] # HTTP status 300s mean redirection. 400s and 500s are errors. # Technically 300s are not errors, but the use case here relies on "requests" to handle redirects automatically. diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index d5531a2773..ceba5f3006 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -10,6 +10,7 @@ from time import time from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING from UM.Backend.Backend import Backend, BackendState +from UM.Scene.Camera import Camera from UM.Scene.SceneNode import SceneNode from UM.Signal import Signal from UM.Logger import Logger @@ -476,7 +477,7 @@ class CuraEngineBackend(QObject, Backend): # # \param source The scene node that was changed. def _onSceneChanged(self, source: SceneNode) -> None: - if not isinstance(source, SceneNode): + if not source.callDecoration("isSliceable"): return # This case checks if the source node is a node that contains GCode. In this case the diff --git a/plugins/ModelChecker/ModelChecker.py b/plugins/ModelChecker/ModelChecker.py index d2c2eefac2..1913f39d96 100644 --- a/plugins/ModelChecker/ModelChecker.py +++ b/plugins/ModelChecker/ModelChecker.py @@ -9,6 +9,7 @@ from UM.Application import Application from UM.Extension import Extension from UM.Logger import Logger from UM.Message import Message +from UM.Scene.Camera import Camera from UM.i18n import i18nCatalog from UM.PluginRegistry import PluginRegistry from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator @@ -35,7 +36,12 @@ class ModelChecker(QObject, Extension): ## Pass-through to allow UM.Signal to connect with a pyqtSignal. def _onChanged(self, *args, **kwargs): - self.onChanged.emit() + # Ignore camera updates. + if len(args) == 0: + self.onChanged.emit() + return + if not isinstance(args[0], Camera): + self.onChanged.emit() ## Called when plug-ins are initialized. # diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py index 797d6dabec..ec00329f86 100644 --- a/plugins/SolidView/SolidView.py +++ b/plugins/SolidView/SolidView.py @@ -69,8 +69,7 @@ class SolidView(View): if support_angle_stack is not None and Application.getInstance().getPreferences().getValue("view/show_overhang"): angle = support_angle_stack.getProperty("support_angle", "value") # Make sure the overhang angle is valid before passing it to the shader - # Note: if the overhang angle is set to its default value, it does not need to get validated (validationState = None) - if angle is not None and global_container_stack.getProperty("support_angle", "validationState") in [None, ValidatorState.Valid]: + if angle is not None and angle >= 0 and angle <= 90: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(90 - angle))) else: self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) #Overhang angle of 0 causes no area at all to be marked as overhang. diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 33968beb6d..7b5add276a 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -61,8 +61,19 @@ class CloudOutputDevice(NetworkedPrinterOutputDevice): # \param cluster: The device response received from the cloud API. # \param parent: The optional parent of this output device. def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None: + + # The following properties are expected on each networked output device. + # Because the cloud connection does not off all of these, we manually construct this version here. + # An example of why this is needed is the selection of the compatible file type when exporting the tool path. + properties = { + b"address": b"", + b"name": cluster.host_name.encode() if cluster.host_name else b"", + b"firmware_version": cluster.host_version.encode() if cluster.host_version else b"", + b"printer_type": b"" + } + super().__init__(device_id = cluster.cluster_id, address = "", - connection_type = ConnectionType.CloudConnection, properties = {}, parent = parent) + connection_type = ConnectionType.CloudConnection, properties = properties, parent = parent) self._api = api_client self._cluster = cluster diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterStatus.py index a8165ff69c..bd3e482bde 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterStatus.py @@ -65,6 +65,7 @@ class CloudClusterPrinterStatus(BaseCloudModel): model.updateName(self.friendly_name) model.updateType(self.machine_variant) model.updateState(self.status if self.enabled else "disabled") + model.updateBuildplate(self.build_plate.type if self.build_plate else "glass") for configuration, extruder_output, extruder_config in \ zip(self.configuration, model.extruders, model.printerConfiguration.extruderConfigurations): diff --git a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py index 790d0c430b..4bbb7ddb5f 100644 --- a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py @@ -627,7 +627,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): # Do not store the build plate information that comes from connect if the current printer has not build plate information if "build_plate" in data and machine_definition.getMetaDataEntry("has_variant_buildplates", False): - printer.updateBuildplateName(data["build_plate"]["type"]) + printer.updateBuildplate(data["build_plate"]["type"]) if not data["enabled"]: printer.updateState("disabled") else: diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index e57cd15960..ceeeec0382 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -455,8 +455,8 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._start_cloud_flow_message = Message( text = i18n_catalog.i18nc("@info:status", "Send and monitor print jobs from anywhere using your Ultimaker account."), lifetime = 0, - image_source = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "resources", "svg", - "cloud-flow-start.svg"), + image_source = QUrl.fromLocalFile(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", + "resources", "svg", "cloud-flow-start.svg")), image_caption = i18n_catalog.i18nc("@info:status", "Connect to Ultimaker Cloud"), option_text = i18n_catalog.i18nc("@action", "Don't ask me again for this printer."), option_state = False @@ -477,8 +477,8 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._cloud_flow_complete_message = Message( text = i18n_catalog.i18nc("@info:status", "You can now send and monitor print jobs from anywhere using your Ultimaker account."), lifetime = 30, - image_source = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "resources", "svg", - "cloud-flow-completed.svg"), + image_source = QUrl.fromLocalFile(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", + "resources", "svg", "cloud-flow-completed.svg")), image_caption = i18n_catalog.i18nc("@info:status", "Connected!") ) self._cloud_flow_complete_message.addAction("", i18n_catalog.i18nc("@action", "Review your connection"), "", "", 1) # TODO: Icon @@ -491,11 +491,12 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): active_machine.setMetaDataEntry("do_not_show_cloud_message", True) return - def _onDontAskMeAgain(self, messageId: str) -> None: + def _onDontAskMeAgain(self, checked: bool) -> None: active_machine = self._application.getMachineManager().activeMachine # type: Optional["GlobalStack"] if active_machine: - active_machine.setMetaDataEntry("do_not_show_cloud_message", True) - Logger.log("d", "Will not ask the user again to cloud connect for current printer.") + active_machine.setMetaDataEntry("do_not_show_cloud_message", checked) + if checked: + Logger.log("d", "Will not ask the user again to cloud connect for current printer.") return def _onCloudFlowStarted(self, messageId: str, actionId: str) -> None: @@ -521,4 +522,4 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): self._cloud_flow_complete_message.hide() self._cloud_flow_complete_message = None - self.checkCloudFlowIsPossible() \ No newline at end of file + self.checkCloudFlowIsPossible() diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py index 14b1f4feba..c4d891302e 100644 --- a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py @@ -21,6 +21,7 @@ class TestCloudOutputDevice(TestCase): JOB_ID = "ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE=" HOST_NAME = "ultimakersystem-ccbdd30044ec" HOST_GUID = "e90ae0ac-1257-4403-91ee-a44c9b7e8050" + HOST_VERSION = "5.2.0" STATUS_URL = "{}/connect/v1/clusters/{}/status".format(CuraCloudAPIRoot, CLUSTER_ID) PRINT_URL = "{}/connect/v1/clusters/{}/print/{}".format(CuraCloudAPIRoot, CLUSTER_ID, JOB_ID) @@ -36,7 +37,7 @@ class TestCloudOutputDevice(TestCase): patched_method.start() self.cluster = CloudClusterResponse(self.CLUSTER_ID, self.HOST_GUID, self.HOST_NAME, is_online=True, - status="active") + status="active", host_version=self.HOST_VERSION) self.network = NetworkManagerMock() self.account = MagicMock(isLoggedIn=True, accessToken="TestAccessToken") @@ -56,6 +57,11 @@ class TestCloudOutputDevice(TestCase): for patched_method in self.patches: patched_method.stop() + # We test for these in order to make sure the correct file type is selected depending on the firmware version. + def test_properties(self): + self.assertEqual(self.device.firmwareVersion, self.HOST_VERSION) + self.assertEqual(self.device.name, self.HOST_NAME) + def test_status(self): self.device._update() self.network.flushReplies() @@ -114,7 +120,7 @@ class TestCloudOutputDevice(TestCase): def test_print_to_cloud(self): active_machine_mock = self.app.getGlobalContainerStack.return_value - active_machine_mock.getMetaDataEntry.side_effect = {"file_formats": "application/gzip"}.get + active_machine_mock.getMetaDataEntry.side_effect = {"file_formats": "application/x-ufp"}.get request_upload_response = parseFixture("putJobUploadResponse") request_print_response = parseFixture("postJobPrintResponse") @@ -124,6 +130,10 @@ class TestCloudOutputDevice(TestCase): file_handler = MagicMock() file_handler.getSupportedFileTypesWrite.return_value = [{ + "extension": "ufp", + "mime_type": "application/x-ufp", + "mode": 2 + }, { "extension": "gcode.gz", "mime_type": "application/gzip", "mode": 2, @@ -137,10 +147,9 @@ class TestCloudOutputDevice(TestCase): self.network.flushReplies() self.assertEqual( - {"data": {"content_type": "application/gzip", "file_size": len(expected_mesh), "job_name": "FileName"}}, + {"data": {"content_type": "application/x-ufp", "file_size": len(expected_mesh), "job_name": "FileName"}}, json.loads(self.network.getRequestBody("PUT", self.REQUEST_UPLOAD_URL).decode()) ) self.assertEqual(expected_mesh, self.network.getRequestBody("PUT", request_upload_response["data"]["upload_url"])) - self.assertIsNone(self.network.getRequestBody("POST", self.PRINT_URL)) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 407923fb4e..04c69c9497 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -1792,7 +1792,7 @@ "skin_overlap": { "label": "Skin Overlap Percentage", - "description": "The amount of overlap between the skin and the walls as a percentage of the skin line width. A slight overlap allows the walls to connect firmly to the skin. This is a percentage of the average line widths of the skin lines and the innermost wall.", + "description": "Adjust the amount of overlap between the walls and (the endpoints of) the skin-centerlines, as a percentage of the line widths of the skin lines and the innermost wall. A slight overlap allows the walls to connect firmly to the skin. Note that, given an equal skin and wall line-width, any percentage over 50% may already cause any skin to go past the wall, because at that point the position of the nozzle of the skin-extruder may already reach past the middle of the wall.", "unit": "%", "type": "float", "default_value": 5, @@ -1807,7 +1807,7 @@ "skin_overlap_mm": { "label": "Skin Overlap", - "description": "The amount of overlap between the skin and the walls. A slight overlap allows the walls to connect firmly to the skin.", + "description": "Adjust the amount of overlap between the walls and (the endpoints of) the skin-centerlines. A slight overlap allows the walls to connect firmly to the skin. Note that, given an equal skin and wall line-width, any value over half the width of the wall may already cause any skin to go past the wall, because at that point the position of the nozzle of the skin-extruder may already reach past the middle of the wall.", "unit": "mm", "type": "float", "default_value": 0.02, diff --git a/resources/qml/Account/AvatarImage.qml b/resources/qml/Account/AvatarImage.qml index bcbc9f0542..a4f922a10d 100644 --- a/resources/qml/Account/AvatarImage.qml +++ b/resources/qml/Account/AvatarImage.qml @@ -22,7 +22,6 @@ Item { id: profileImage anchors.fill: parent - source: UM.Theme.getImage("avatar_default") fillMode: Image.PreserveAspectCrop visible: false mipmap: true diff --git a/resources/qml/ActionPanel/OutputDevicesActionButton.qml b/resources/qml/ActionPanel/OutputDevicesActionButton.qml index 3bfaab0fc1..866b8cc627 100644 --- a/resources/qml/ActionPanel/OutputDevicesActionButton.qml +++ b/resources/qml/ActionPanel/OutputDevicesActionButton.qml @@ -90,6 +90,10 @@ Item cornerRadius: 0 hoverColor: UM.Theme.getColor("primary") Layout.fillWidth: true + // The total width of the popup should be defined by the largest button. By stating that each + // button should be minimally the size of it's content (aka; implicitWidth) we can ensure that. + Layout.minimumWidth: implicitWidth + Layout.preferredHeight: widget.height onClicked: { UM.OutputDeviceManager.setActiveDevice(model.id) diff --git a/resources/qml/Dialogs/AboutDialog.qml b/resources/qml/Dialogs/AboutDialog.qml index 604cbc16ba..584903dd60 100644 --- a/resources/qml/Dialogs/AboutDialog.qml +++ b/resources/qml/Dialogs/AboutDialog.qml @@ -21,41 +21,45 @@ UM.Dialog Rectangle { + id: header width: parent.width + 2 * margin // margin from Dialog.qml - height: version.y + version.height + margin + height: childrenRect.height + topPadding anchors.top: parent.top - anchors.topMargin: - margin + anchors.topMargin: -margin anchors.horizontalCenter: parent.horizontalCenter + property real topPadding: UM.Theme.getSize("wide_margin").height + color: UM.Theme.getColor("main_window_header_background") - } - Image - { - id: logo - width: (base.minimumWidth * 0.85) | 0 - source: UM.Theme.getImage("logo") - sourceSize.width: width - sourceSize.height: height + Image + { + id: logo + width: (base.minimumWidth * 0.85) | 0 + height: (width * (UM.Theme.getSize("logo").height / UM.Theme.getSize("logo").width)) | 0 + source: UM.Theme.getImage("logo") + sourceSize.width: width + sourceSize.height: height - anchors.top: parent.top - anchors.topMargin: ((base.minimumWidth - width) / 2) | 0 - anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: parent.topPadding + anchors.horizontalCenter: parent.horizontalCenter - UM.I18nCatalog{id: catalog; name: "cura"} - } + UM.I18nCatalog{id: catalog; name: "cura"} + } - Label - { - id: version + Label + { + id: version - text: catalog.i18nc("@label","version: %1").arg(UM.Application.version) - font: UM.Theme.getFont("large_bold") - color: UM.Theme.getColor("button_text") - anchors.right : logo.right - anchors.top: logo.bottom - anchors.topMargin: (UM.Theme.getSize("default_margin").height / 2) | 0 + text: catalog.i18nc("@label","version: %1").arg(UM.Application.version) + font: UM.Theme.getFont("large_bold") + color: UM.Theme.getColor("button_text") + anchors.right : logo.right + anchors.top: logo.bottom + anchors.topMargin: (UM.Theme.getSize("default_margin").height / 2) | 0 + } } Label @@ -67,7 +71,7 @@ UM.Dialog text: catalog.i18nc("@label","End-to-end solution for fused filament 3D printing.") font: UM.Theme.getFont("system") wrapMode: Text.WordWrap - anchors.top: version.bottom + anchors.top: header.bottom anchors.topMargin: UM.Theme.getSize("default_margin").height } diff --git a/resources/themes/cura-light/images/avatar_default.svg b/resources/themes/cura-light/images/avatar_default.svg deleted file mode 100644 index 7ec704bc8c..0000000000 --- a/resources/themes/cura-light/images/avatar_default.svg +++ /dev/null @@ -1,76 +0,0 @@ - - - - - - - - - - image/svg+xml - - - - - - - - - - - - diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json index 573fe8bcfa..92308537dd 100644 --- a/resources/themes/cura-light/theme.json +++ b/resources/themes/cura-light/theme.json @@ -473,7 +473,7 @@ "default_lining": [0.08, 0.08], "default_arrow": [0.8, 0.8], - "logo": [8, 2.4], + "logo": [8, 1.75], "wide_margin": [2.0, 2.0], "thick_margin": [1.71, 1.43],