From 2a9be0cdfedc02d3892f59c4be2ae370e232d347 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 13 Aug 2019 12:55:54 +0200 Subject: [PATCH 01/63] Only create API client when actually used --- .../src/Network/LocalClusterOutputDevice.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py index 2c1ac2279d..cb373e7e1e 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py @@ -38,16 +38,13 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): parent=parent ) - # API client for making requests to the print cluster. - self._cluster_api = ClusterApiClient(address, on_error=lambda error: print(error)) + self._cluster_api = None # type: Optional[ClusterApiClient] + # We don't have authentication over local networking, so we're always authenticated. self.setAuthenticationState(AuthState.Authenticated) self._setInterfaceElements() self._active_camera_url = QUrl() # type: QUrl - # Get the printers of this cluster to check if this device is a group host or not. - self._cluster_api.getPrinters(self._updatePrinters) - ## Set all the interface elements and texts for this output device. def _setInterfaceElements(self) -> None: self.setPriority(3) # Make sure the output device gets selected above local file output @@ -81,11 +78,11 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): @pyqtSlot(str, name="sendJobToTop") def sendJobToTop(self, print_job_uuid: str) -> None: - self._cluster_api.movePrintJobToTop(print_job_uuid) + self._getApiClient().movePrintJobToTop(print_job_uuid) @pyqtSlot(str, name="deleteJobFromQueue") def deleteJobFromQueue(self, print_job_uuid: str) -> None: - self._cluster_api.deletePrintJob(print_job_uuid) + self._getApiClient().deletePrintJob(print_job_uuid) @pyqtSlot(str, name="forceSendJob") def forceSendJob(self, print_job_uuid: str) -> None: @@ -95,12 +92,12 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): # \param print_job_uuid: The UUID of the print job to set the state for. # \param action: The action to undertake ('pause', 'resume', 'abort'). def setJobState(self, print_job_uuid: str, action: str) -> None: - self._cluster_api.setPrintJobState(print_job_uuid, action) + self._getApiClient().setPrintJobState(print_job_uuid, action) def _update(self) -> None: super()._update() - self._cluster_api.getPrinters(self._updatePrinters) - self._cluster_api.getPrintJobs(self._updatePrintJobs) + self._getApiClient().getPrinters(self._updatePrinters) + self._getApiClient().getPrintJobs(self._updatePrintJobs) self._updatePrintJobPreviewImages() ## Sync the material profiles in Cura with the printer. @@ -162,4 +159,10 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): def _updatePrintJobPreviewImages(self): for print_job in self._print_jobs: if print_job.getPreviewImage() is None: - self._cluster_api.getPrintJobPreviewImage(print_job.key, print_job.updatePreviewImageData) + self._getApiClient().getPrintJobPreviewImage(print_job.key, print_job.updatePreviewImageData) + + ## Get the API client instance. + def _getApiClient(self) -> ClusterApiClient: + if not self._cluster_api: + self._cluster_api = ClusterApiClient(self.address, on_error=lambda error: print(error)) + return self._cluster_api From 33876e9ca91e4b2f07823c62adee0db20c38881f Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 13 Aug 2019 13:01:47 +0200 Subject: [PATCH 02/63] Set group_name correct with cloud devices, possibly fixes CS-225 --- .../src/Cloud/CloudOutputDeviceManager.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py index e6cd98426f..168d209db8 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py @@ -161,15 +161,17 @@ class CloudOutputDeviceManager: self._connectToOutputDevice(device, active_machine) elif local_network_key and device.matchesNetworkKey(local_network_key): # Connect to it if we can match the local network key that was already present. - active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) self._connectToOutputDevice(device, active_machine) elif device.key in output_device_manager.getOutputDeviceIds(): # Remove device if it is not meant for the active machine. output_device_manager.removeOutputDevice(device.key) ## Connects to an output device and makes sure it is registered in the output device manager. - @staticmethod - def _connectToOutputDevice(device: CloudOutputDevice, active_machine: GlobalStack) -> None: + def _connectToOutputDevice(self, device: CloudOutputDevice, machine: GlobalStack) -> None: + machine.setName(device.name) + machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key) + machine.setMetaDataEntry("group_name", device.name) + device.connect() - active_machine.addConfiguredConnectionType(device.connectionType.value) + machine.addConfiguredConnectionType(device.connectionType.value) CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) From 998f58d3fa176770bf357374fc1636e6860ab1cb Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 13 Aug 2019 15:19:30 +0200 Subject: [PATCH 03/63] Tweak offline check interval for cloud device --- plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py | 8 ++++++-- .../src/UltimakerNetworkedPrinterOutputDevice.py | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index fc514d1fca..fbd0ea4e2c 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -42,7 +42,11 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): # The interval with which the remote cluster is checked. # We can do this relatively often as this API call is quite fast. - CHECK_CLUSTER_INTERVAL = 8.0 # seconds + CHECK_CLUSTER_INTERVAL = 10.0 # seconds + + # Override the network response timeout in seconds after which we consider the device offline. + # For cloud this needs to be higher because the interval at which we check the status is higher as well. + NETWORK_RESPONSE_CONSIDER_OFFLINE = 15.0 # seconds # The minimum version of firmware that support print job actions over cloud. PRINT_JOB_ACTIONS_MIN_VERSION = Version("5.3.0") @@ -274,7 +278,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): @clusterData.setter def clusterData(self, value: CloudClusterResponse) -> None: self._cluster = value - + ## Gets the URL on which to monitor the cluster via the cloud. @property def clusterCloudUrl(self) -> str: diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index f2c24e4802..9230ed8a56 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -26,7 +26,7 @@ from .Models.Http.ClusterPrintJobStatus import ClusterPrintJobStatus # Currently used for local networking and cloud printing using Ultimaker Connect. # This base class primarily contains all the Qt properties and slots needed for the monitor page to work. class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): - + META_NETWORK_KEY = "um_network_key" META_CLUSTER_ID = "um_cloud_cluster_id" @@ -42,10 +42,10 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # States indicating if a print job is queued. QUEUED_PRINT_JOBS_STATES = {"queued", "error"} - + # Time in seconds since last network response after which we consider this device offline. # We set this a bit higher than some of the other intervals to make sure they don't overlap. - NETWORK_RESPONSE_CONSIDER_OFFLINE = 12.0 + NETWORK_RESPONSE_CONSIDER_OFFLINE = 10.0 # seconds def __init__(self, device_id: str, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType, parent=None) -> None: @@ -54,7 +54,7 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # Trigger the printersChanged signal when the private signal is triggered. self.printersChanged.connect(self._clusterPrintersChanged) - + # Keeps track the last network response to determine if we are still connected. self._time_of_last_response = time() From fcd5a563e461972e6124e7524f4865ee84472291 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 13 Aug 2019 21:42:44 +0200 Subject: [PATCH 04/63] Use cluster_size property from zeroconf if available --- .../src/Network/LocalClusterOutputDeviceManager.py | 2 +- .../src/UltimakerNetworkedPrinterOutputDevice.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py index 6c45fd9fc0..e5ae7b83ac 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDeviceManager.py @@ -236,7 +236,7 @@ class LocalClusterOutputDeviceManager: machine.setName(device.name) machine.setMetaDataEntry(self.META_NETWORK_KEY, device.key) machine.setMetaDataEntry("group_name", device.name) - + device.connect() machine.addConfiguredConnectionType(device.connectionType.value) CuraApplication.getInstance().getOutputDeviceManager().addOutputDevice(device) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 9230ed8a56..7bebd58a46 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -109,7 +109,10 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): @pyqtProperty(int, notify=_clusterPrintersChanged) def clusterSize(self) -> int: if not self._has_received_printers: - return 1 # prevent false positives when discovering new devices + discovered_size = self.getProperty("cluster_size") + if discovered_size == "": + return 1 # prevent false positives for new devices + return int(discovered_size) return len(self._printers) # Get the amount of printer in the cluster per type. From 4947757688525c4b20e61c6ff2f641245bd3b7c9 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 14 Aug 2019 10:55:10 +0200 Subject: [PATCH 05/63] Hide default material print temperature CURA-6716 --- resources/definitions/fdmprinter.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index 8327834b7a..fbb6e9f93b 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -2087,7 +2087,7 @@ "default_value": 210, "minimum_value_warning": "0", "maximum_value_warning": "285", - "enabled": "machine_nozzle_temp_enabled", + "enabled": false, "settable_per_extruder": true, "settable_per_mesh": false, "minimum_value": "-273.15" From 00334ee5a9f812fcaf78f4ccaae8fd1ce10b30fe Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 14 Aug 2019 10:58:46 +0200 Subject: [PATCH 06/63] Hide default build plate temperature CURA-6716 --- resources/definitions/fdmprinter.def.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index fbb6e9f93b..7c72a9170e 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -2192,7 +2192,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "0", "maximum_value_warning": "130", - "enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"", + "enabled": false, "settable_per_mesh": false, "settable_per_extruder": false, "settable_per_meshgroup": false From f947269cf88b9d716c29195a1a0ceb442b093cee Mon Sep 17 00:00:00 2001 From: Remco Burema Date: Wed, 14 Aug 2019 11:59:13 +0200 Subject: [PATCH 07/63] Add machine_heated_build_volume. See also the engine. part of CURA-6717 --- .../MachineSettingsPrinterTab.qml | 12 ++++++++++++ resources/definitions/fdmprinter.def.json | 12 +++++++++++- resources/definitions/ultimaker_s5.def.json | 1 + 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/plugins/MachineSettingsAction/MachineSettingsPrinterTab.qml b/plugins/MachineSettingsAction/MachineSettingsPrinterTab.qml index 2556eb3a9c..d817450f41 100644 --- a/plugins/MachineSettingsAction/MachineSettingsPrinterTab.qml +++ b/plugins/MachineSettingsAction/MachineSettingsPrinterTab.qml @@ -142,6 +142,18 @@ Item forceUpdateOnChangeFunction: forceUpdateFunction } + Cura.SimpleCheckBox // "Heated build volume" + { + id: heatedVolumeCheckBox + containerStackId: machineStackId + settingKey: "machine_heated_build_volume" + settingStoreIndex: propertyStoreIndex + labelText: catalog.i18nc("@label", "Heated build volume") + labelFont: base.labelFont + labelWidth: base.labelWidth + forceUpdateOnChangeFunction: forceUpdateFunction + } + Cura.ComboBoxWithOptions // "G-code flavor" { id: gcodeFlavorComboBox diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index bb643c473e..adb294568b 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -204,6 +204,16 @@ "settable_per_extruder": false, "settable_per_meshgroup": false }, + "machine_heated_build_volume": + { + "label": "Has Build Volume Temperature Stabilization", + "description": "Whether the machine is able to stabilize the build volume temperature.", + "default_value": false, + "type": "bool", + "settable_per_mesh": false, + "settable_per_extruder": false, + "settable_per_meshgroup": false + }, "machine_center_is_zero": { "label": "Is Center Origin", @@ -2103,7 +2113,7 @@ "minimum_value": "-273.15", "minimum_value_warning": "0", "maximum_value_warning": "285", - "enabled": true, + "enabled": "machine_heated_build_volume", "settable_per_mesh": false, "settable_per_extruder": false }, diff --git a/resources/definitions/ultimaker_s5.def.json b/resources/definitions/ultimaker_s5.def.json index 38d761f875..bf60d84890 100644 --- a/resources/definitions/ultimaker_s5.def.json +++ b/resources/definitions/ultimaker_s5.def.json @@ -44,6 +44,7 @@ "machine_depth": { "default_value": 240 }, "machine_height": { "default_value": 300 }, "machine_heated_bed": { "default_value": true }, + "machine_heated_build_volume": { "default_value": true }, "machine_nozzle_heat_up_speed": { "default_value": 1.4 }, "machine_nozzle_cool_down_speed": { "default_value": 0.8 }, "machine_head_with_fans_polygon": From e6d30516aa8903525b996aeab03f06b909ce054a Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Wed, 14 Aug 2019 14:21:16 +0200 Subject: [PATCH 08/63] Fix status interval check for cloud devices --- .../UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py | 10 +++++----- .../src/UltimakerNetworkedPrinterOutputDevice.py | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index fbd0ea4e2c..3a256e2860 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -148,8 +148,9 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): ## Called when the network data should be updated. def _update(self) -> None: super()._update() - if self._last_request_time and time() - self._last_request_time < self.CHECK_CLUSTER_INTERVAL: - return # Avoid calling the cloud too often + if time() - self._time_of_last_request < self.CHECK_CLUSTER_INTERVAL: + return # avoid calling the cloud too often + self._time_of_last_request = time() if self._account.isLoggedIn: self.setAuthenticationState(AuthState.Authenticated) self._last_request_time = time() @@ -160,9 +161,8 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): ## Method called when HTTP request to status endpoint is finished. # Contains both printers and print jobs statuses in a single response. def _onStatusCallFinished(self, status: CloudClusterStatus) -> None: - # Update all data from the cluster. - self._last_response_time = time() - if self._received_printers != status.printers: + self._responseReceived() + if status.printers != self._received_printers: self._received_printers = status.printers self._updatePrinters(status.printers) if status.print_jobs != self._received_print_jobs: diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 7bebd58a46..09cfe25a3a 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -57,6 +57,7 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): # Keeps track the last network response to determine if we are still connected. self._time_of_last_response = time() + self._time_of_last_request = time() # Set the display name from the properties self.setName(self.getProperty("name")) From 6b93c97a5e910c918108044afe6cbe922f5a8fab Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 14 Aug 2019 14:55:08 +0200 Subject: [PATCH 09/63] Use certifi for uploading crash reports CURA-6698 --- cura/CrashHandler.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index 6e6da99b0f..1d85a1da54 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -12,9 +12,10 @@ import json import ssl import urllib.request import urllib.error -import shutil -from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, Qt, QUrl +import certifi + +from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QUrl from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton from PyQt5.QtGui import QDesktopServices @@ -22,7 +23,6 @@ from UM.Application import Application from UM.Logger import Logger from UM.View.GL.OpenGL import OpenGL from UM.i18n import i18nCatalog -from UM.Platform import Platform from UM.Resources import Resources catalog = i18nCatalog("cura") @@ -352,11 +352,13 @@ class CrashHandler: # Convert data to bytes binary_data = json.dumps(self.data).encode("utf-8") + # CURA-6698 Create an SSL context and use certifi CA certificates for verification. + context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLSv1_2) + context.load_verify_locations(cafile = certifi.where()) # Submit data - kwoptions = {"data": binary_data, "timeout": 5} - - if Platform.isOSX(): - kwoptions["context"] = ssl._create_unverified_context() + kwoptions = {"data": binary_data, + "timeout": 5, + "context": context} Logger.log("i", "Sending crash report info to [%s]...", self.crash_url) if not self.has_started: From a2dcbc3be70d9f6f21611d2cb84bdbdebbfbbe70 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 14 Aug 2019 14:57:47 +0200 Subject: [PATCH 10/63] Use certifi for firmware update checker CURA-6698 --- .../FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py index ad10a4f075..ca4d4d964a 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py @@ -10,6 +10,9 @@ from UM.Version import Version import urllib.request from urllib.error import URLError from typing import Dict, Optional +import ssl + +import certifi from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage @@ -39,8 +42,12 @@ class FirmwareUpdateCheckerJob(Job): result = self.STRING_ZERO_VERSION try: + # CURA-6698 Create an SSL context and use certifi CA certificates for verification. + context = ssl.SSLContext(protocol = ssl.PROTOCOL_TLSv1_2) + context.load_verify_locations(cafile = certifi.where()) + request = urllib.request.Request(url, headers = self._headers) - response = urllib.request.urlopen(request) + response = urllib.request.urlopen(request, context = context) result = response.read().decode("utf-8") except URLError: Logger.log("w", "Could not reach '{0}', if this URL is old, consider removal.".format(url)) From 4c792419e30eeca389f7796a4f5feadd32b59963 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Wed, 14 Aug 2019 08:53:18 +0200 Subject: [PATCH 11/63] Use QDesktopServices.openUrl() instead of webbrowser --- cura/OAuth2/AuthorizationService.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index 27041b1f80..f455c135ae 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -2,12 +2,14 @@ # Cura is released under the terms of the LGPLv3 or higher. import json -import webbrowser from datetime import datetime, timedelta from typing import Optional, TYPE_CHECKING from urllib.parse import urlencode + import requests.exceptions +from PyQt5.QtCore import QUrl +from PyQt5.QtGui import QDesktopServices from UM.Logger import Logger from UM.Message import Message @@ -163,7 +165,7 @@ class AuthorizationService: }) # Open the authorization page in a new browser window. - webbrowser.open_new("{}?{}".format(self._auth_url, query_string)) + QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string))) # Start a local web server to receive the callback URL on. self._server.start(verification_code) From e8fd013329923a5ce2e2800f4142cbfc04ba4b74 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Aug 2019 09:23:05 +0200 Subject: [PATCH 12/63] Fix OAuth2 test --- tests/TestOAuth2.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/TestOAuth2.py b/tests/TestOAuth2.py index 358ed5afbb..1e305c6549 100644 --- a/tests/TestOAuth2.py +++ b/tests/TestOAuth2.py @@ -1,9 +1,10 @@ -import webbrowser from datetime import datetime from unittest.mock import MagicMock, patch import requests +from PyQt5.QtGui import QDesktopServices + from UM.Preferences import Preferences from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT from cura.OAuth2.AuthorizationService import AuthorizationService @@ -172,12 +173,12 @@ def test_storeAuthData(get_user_profile) -> None: @patch.object(LocalAuthorizationServer, "stop") @patch.object(LocalAuthorizationServer, "start") -@patch.object(webbrowser, "open_new") -def test_localAuthServer(webbrowser_open, start_auth_server, stop_auth_server) -> None: +@patch.object(QDesktopServices, "openUrl") +def test_localAuthServer(QDesktopServices_openUrl, start_auth_server, stop_auth_server) -> None: preferences = Preferences() authorization_service = AuthorizationService(OAUTH_SETTINGS, preferences) authorization_service.startAuthorizationFlow() - assert webbrowser_open.call_count == 1 + assert QDesktopServices_openUrl.call_count == 1 # Ensure that the Authorization service tried to start the server. assert start_auth_server.call_count == 1 From f440535581c1b96511cc24a440cea14d2ef755ac Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 15 Aug 2019 11:31:02 +0200 Subject: [PATCH 13/63] Add platform mesh for Creality Ender-3 Provided by Sprint8 in #6204, but I think the mesh was obtained from an older Cura version. This fixes #6204. --- resources/definitions/creality_ender3.def.json | 10 +++++----- resources/meshes/creality_ender3.stl | Bin 0 -> 142084 bytes 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 resources/meshes/creality_ender3.stl diff --git a/resources/definitions/creality_ender3.def.json b/resources/definitions/creality_ender3.def.json index e00e6eab63..4b7da65e4e 100644 --- a/resources/definitions/creality_ender3.def.json +++ b/resources/definitions/creality_ender3.def.json @@ -2,6 +2,11 @@ "name": "Creality Ender-3", "version": 2, "inherits": "creality_base", + "metadata": { + "quality_definition": "creality_base", + "visible": true, + "platform": "creality_ender3.stl" + }, "overrides": { "machine_name": { "default_value": "Creality Ender-3" }, "machine_width": { "default_value": 220 }, @@ -23,10 +28,5 @@ }, "gantry_height": { "value": 25 } - - }, - "metadata": { - "quality_definition": "creality_base", - "visible": true } } \ No newline at end of file diff --git a/resources/meshes/creality_ender3.stl b/resources/meshes/creality_ender3.stl new file mode 100644 index 0000000000000000000000000000000000000000..b1fd101aad0cc22e131d6e99e41d12def5baff54 GIT binary patch literal 142084 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 literal 0 HcmV?d00001 From f20696966d1ff7fdd69e6d3b7b261ec5512ba04d Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Aug 2019 13:03:28 +0200 Subject: [PATCH 14/63] Fix logging in FirmwareUpdateChecker --- plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py index ca4d4d964a..f286662bc4 100644 --- a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py +++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py @@ -111,7 +111,7 @@ class FirmwareUpdateCheckerJob(Job): # because the new version of Cura will be release before the firmware and we don't want to # notify the user when no new firmware version is available. if (checked_version != "") and (checked_version != current_version): - Logger.log("i", "Showing firmware update message for new version: {version}".format(current_version)) + Logger.log("i", "Showing firmware update message for new version: {version}".format(version = current_version)) message = FirmwareUpdateCheckerMessage(machine_id, self._machine_name, self._lookups.getRedirectUserUrl()) message.actionTriggered.connect(self._callback) @@ -120,7 +120,7 @@ class FirmwareUpdateCheckerJob(Job): Logger.log("i", "No machine with name {0} in list of firmware to check.".format(self._machine_name)) except Exception as e: - Logger.log("w", "Failed to check for new version: %s", e) + Logger.logException("w", "Failed to check for new version: %s", e) if not self.silent: Message(i18n_catalog.i18nc("@info", "Could not access update information.")).show() return From eb5e1904abeb0f5437a24b1e3566d20ccc0d7c0f Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 15 Aug 2019 13:21:10 +0200 Subject: [PATCH 15/63] Fix setting printer type on print job --- .../UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py | 1 + .../src/UltimakerNetworkedPrinterOutputDevice.py | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py index 8b35fb7b5a..e54d99f1e6 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintJobStatus.py @@ -101,6 +101,7 @@ class ClusterPrintJobStatus(BaseModel): extruders = [extruder.createConfigurationModel() for extruder in self.configuration or ()] configuration = PrinterConfigurationModel() configuration.setExtruderConfigurations(extruders) + configuration.setPrinterType(self.machine_variant) return configuration ## Updates an UM3 print job output model based on this cloud cluster print job. diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 09cfe25a3a..8e62f641cb 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -49,6 +49,7 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): def __init__(self, device_id: str, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType, parent=None) -> None: + super().__init__(device_id=device_id, address=address, properties=properties, connection_type=connection_type, parent=parent) From 4feb22e065dab12bb395f039994842ad3dbe0287 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Aug 2019 13:33:15 +0200 Subject: [PATCH 16/63] Fix version upgrade for imade3d jellybox CURA-6709 --- .../VersionUpgrade42to43.py | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py b/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py index 71b665ad7c..6bcf43dc71 100644 --- a/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py +++ b/plugins/VersionUpgrade/VersionUpgrade42to43/VersionUpgrade42to43.py @@ -14,6 +14,40 @@ _renamed_profiles = {"generic_pla_0.4_coarse": "jbo_generic_pla_0.4_coarse", "generic_petg_0.4_medium": "jbo_generic_petg_medium", } +# - The variant "imade3d jellybox 0.4 mm 2-fans" for machine definition "imade3d_jellybox" +# is now "0.4 mm" for machine definition "imade3d jellybox_2". +# - Materials "imade3d_petg_green" and "imade3d_petg_pink" are now "imade3d_petg_175". +# - Materials "imade3d_pla_green" and "imade3d_pla_pink" are now "imade3d_petg_175". +# +# Note: Theoretically, the old material profiles with "_2-fans" at the end should be updated to: +# - machine definition: imade3d_jellybox_2 +# - variant: 0.4 mm (for jellybox 2) +# - material: (as an example) imade3d_petg_175_imade3d_jellybox_2_0.4_mm +# +# But this involves changing the definition of the global stack and the extruder stacks, which can cause more trouble +# than what we can fix. So, here, we update all material variants, regardless of having "_2-fans" at the end or not, to +# jellybox_0.4_mm. +# +_renamed_material_profiles = { # PETG + "imade3d_petg_green": "imade3d_petg_175", + "imade3d_petg_green_imade3d_jellybox": "imade3d_petg_175_imade3d_jellybox", + "imade3d_petg_green_imade3d_jellybox_0.4_mm": "imade3d_petg_175_imade3d_jellybox_0.4_mm", + "imade3d_petg_green_imade3d_jellybox_0.4_mm_2-fans": "imade3d_petg_175_imade3d_jellybox_0.4_mm", + "imade3d_petg_pink": "imade3d_petg_175", + "imade3d_petg_pink_imade3d_jellybox": "imade3d_petg_175_imade3d_jellybox", + "imade3d_petg_pink_imade3d_jellybox_0.4_mm": "imade3d_petg_175_imade3d_jellybox_0.4_mm", + "imade3d_petg_pink_imade3d_jellybox_0.4_mm_2-fans": "imade3d_petg_175_imade3d_jellybox_0.4_mm", + # PLA + "imade3d_pla_green": "imade3d_pla_175", + "imade3d_pla_green_imade3d_jellybox": "imade3d_pla_175_imade3d_jellybox", + "imade3d_pla_green_imade3d_jellybox_0.4_mm": "imade3d_pla_175_imade3d_jellybox_0.4_mm", + "imade3d_pla_green_imade3d_jellybox_0.4_mm_2-fans": "imade3d_pla_175_imade3d_jellybox_0.4_mm", + "imade3d_pla_pink": "imade3d_pla_175", + "imade3d_pla_pink_imade3d_jellybox": "imade3d_pla_175_imade3d_jellybox", + "imade3d_pla_pink_imade3d_jellybox_0.4_mm": "imade3d_pla_175_imade3d_jellybox_0.4_mm", + "imade3d_pla_pink_imade3d_jellybox_0.4_mm_2-fans": "imade3d_pla_175_imade3d_jellybox_0.4_mm", + } + _removed_settings = { "start_layers_at_same_position" } @@ -114,8 +148,8 @@ class VersionUpgrade42to43(VersionUpgrade): parser["containers"]["2"] = _renamed_profiles[parser["containers"]["2"]] material_id = parser["containers"]["3"] - if material_id.endswith("_2-fans"): - parser["containers"]["3"] = material_id.replace("_2-fans", "") + if material_id in _renamed_material_profiles: + parser["containers"]["3"] = _renamed_material_profiles[material_id] variant_id = parser["containers"]["4"] if variant_id.endswith("_2-fans"): From 581cde1ddfddaf5be18638bac6c5df0e8984dd33 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Thu, 15 Aug 2019 13:41:47 +0200 Subject: [PATCH 17/63] Use HTTPS for firmware update check URLs --- resources/definitions/ultimaker3.def.json | 2 +- resources/definitions/ultimaker3_extended.def.json | 2 +- resources/definitions/ultimaker_s5.def.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/definitions/ultimaker3.def.json b/resources/definitions/ultimaker3.def.json index ae36d6a3ae..bd7e96448a 100644 --- a/resources/definitions/ultimaker3.def.json +++ b/resources/definitions/ultimaker3.def.json @@ -30,7 +30,7 @@ "id": 9066, "check_urls": [ - "http://software.ultimaker.com/releases/firmware/9066/stable/um-update.swu.version" + "https://software.ultimaker.com/releases/firmware/9066/stable/um-update.swu.version" ], "update_url": "https://ultimaker.com/firmware" } diff --git a/resources/definitions/ultimaker3_extended.def.json b/resources/definitions/ultimaker3_extended.def.json index b3fe48ca11..c0d099366d 100644 --- a/resources/definitions/ultimaker3_extended.def.json +++ b/resources/definitions/ultimaker3_extended.def.json @@ -27,7 +27,7 @@ "id": 9511, "check_urls": [ - "http://software.ultimaker.com/releases/firmware/9066/stable/um-update.swu.version" + "https://software.ultimaker.com/releases/firmware/9066/stable/um-update.swu.version" ], "update_url": "https://ultimaker.com/firmware" } diff --git a/resources/definitions/ultimaker_s5.def.json b/resources/definitions/ultimaker_s5.def.json index bf60d84890..81b3a704ff 100644 --- a/resources/definitions/ultimaker_s5.def.json +++ b/resources/definitions/ultimaker_s5.def.json @@ -33,7 +33,7 @@ "weight": -2, "firmware_update_info": { "id": 9051, - "check_urls": ["http://software.ultimaker.com/releases/firmware/9051/stable/um-update.swu.version"], + "check_urls": ["https://software.ultimaker.com/releases/firmware/9051/stable/um-update.swu.version"], "update_url": "https://ultimaker.com/firmware" } }, From 6833323845258d5649fd075c4815d854bdfa5244 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 15 Aug 2019 14:30:11 +0200 Subject: [PATCH 18/63] UM3PrinterAction - refactor to property, remove discovery start from reset --- .../src/UltimakerNetworkedPrinterAction.py | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py index 5a37e1aeba..f179f7a7d9 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py @@ -18,7 +18,7 @@ I18N_CATALOG = i18nCatalog("cura") ## Machine action that allows to connect the active machine to a networked devices. # TODO: in the future this should be part of the new discovery workflow baked into Cura. class UltimakerNetworkedPrinterAction(MachineAction): - + # Signal emitted when discovered devices have changed. discoveredDevicesChanged = pyqtSignal() @@ -34,58 +34,54 @@ class UltimakerNetworkedPrinterAction(MachineAction): ## Start listening to network discovery events via the plugin. @pyqtSlot(name = "startDiscovery") def startDiscovery(self) -> None: - network_plugin = self._getNetworkPlugin() - network_plugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged) + self._networkPlugin.discoveredDevicesChanged.connect(self._onDeviceDiscoveryChanged) self.discoveredDevicesChanged.emit() # trigger at least once to populate the list ## Reset the discovered devices. @pyqtSlot(name = "reset") def reset(self) -> None: - self.restartDiscovery() + self.discoveredDevicesChanged.emit() # trigger to reset the list ## Reset the discovered devices. @pyqtSlot(name = "restartDiscovery") def restartDiscovery(self) -> None: - network_plugin = self._getNetworkPlugin() - network_plugin.startDiscovery() + self._networkPlugin.startDiscovery() self.discoveredDevicesChanged.emit() # trigger to reset the list ## Remove a manually added device. @pyqtSlot(str, str, name = "removeManualDevice") def removeManualDevice(self, key: str, address: str) -> None: - network_plugin = self._getNetworkPlugin() - network_plugin.removeManualDevice(key, address) + self._networkPlugin.removeManualDevice(key, address) ## Add a new manual device. Can replace an existing one by key. @pyqtSlot(str, str, name = "setManualDevice") def setManualDevice(self, key: str, address: str) -> None: - network_plugin = self._getNetworkPlugin() if key != "": - network_plugin.removeManualDevice(key) + self._networkPlugin.removeManualDevice(key) if address != "": - network_plugin.addManualDevice(address) + self._networkPlugin.addManualDevice(address) ## Get the devices discovered in the local network sorted by name. @pyqtProperty("QVariantList", notify = discoveredDevicesChanged) def foundDevices(self): - network_plugin = self._getNetworkPlugin() - discovered_devices = list(network_plugin.getDiscoveredDevices().values()) + discovered_devices = list(self._networkPlugin.getDiscoveredDevices().values()) discovered_devices.sort(key = lambda d: d.name) return discovered_devices ## Connect a device selected in the list with the active machine. @pyqtSlot(QObject, name = "associateActiveMachineWithPrinterDevice") def associateActiveMachineWithPrinterDevice(self, device: LocalClusterOutputDevice) -> None: - network_plugin = self._getNetworkPlugin() - network_plugin.associateActiveMachineWithPrinterDevice(device) + self._networkPlugin.associateActiveMachineWithPrinterDevice(device) ## Callback for when the list of discovered devices in the plugin was changed. def _onDeviceDiscoveryChanged(self) -> None: self.discoveredDevicesChanged.emit() ## Get the network manager from the plugin. - def _getNetworkPlugin(self) -> UM3OutputDevicePlugin: + @property + def _networkPlugin(self) -> Optional[UM3OutputDevicePlugin]: if not self._network_plugin: - plugin = CuraApplication.getInstance().getOutputDeviceManager().getOutputDevicePlugin("UM3NetworkPrinting") - self._network_plugin = cast(UM3OutputDevicePlugin, plugin) + output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() + network_plugin = output_device_manager.getOutputDevicePlugin("UM3NetworkPrinting") + self._network_plugin = network_plugin return self._network_plugin From 36f6bdca18ba80fbad49b9a507484c4f561d4c6a Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 15 Aug 2019 14:36:25 +0200 Subject: [PATCH 19/63] Bring cast back to fix typing --- .../UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py index f179f7a7d9..8c5f5c12ea 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterAction.py @@ -79,9 +79,9 @@ class UltimakerNetworkedPrinterAction(MachineAction): ## Get the network manager from the plugin. @property - def _networkPlugin(self) -> Optional[UM3OutputDevicePlugin]: + def _networkPlugin(self) -> UM3OutputDevicePlugin: if not self._network_plugin: output_device_manager = CuraApplication.getInstance().getOutputDeviceManager() network_plugin = output_device_manager.getOutputDevicePlugin("UM3NetworkPrinting") - self._network_plugin = network_plugin + self._network_plugin = cast(UM3OutputDevicePlugin, network_plugin) return self._network_plugin From de3f82610a0dc75817d2c8a9291770e6ed74a0ab Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 15 Aug 2019 19:42:25 +0200 Subject: [PATCH 20/63] Add tooltips for firmware update back, fix print job override state --- .../resources/qml/MonitorPrintJobCard.qml | 14 +++++++------- .../resources/qml/MonitorPrinterCard.qml | 13 ++++++------- .../resources/qml/MonitorQueue.qml | 1 - .../UM3NetworkPrinting/src/Cloud/CloudApiClient.py | 2 +- .../src/UltimakerNetworkedPrinterOutputDevice.py | 4 ++++ 5 files changed, 18 insertions(+), 16 deletions(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml index 14e95559ec..c01f778bba 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml @@ -64,6 +64,7 @@ Item visible: printJob // FIXED-LINE-HEIGHT: + width: parent.width height: parent.height verticalAlignment: Text.AlignVCenter renderType: Text.NativeRendering @@ -241,11 +242,10 @@ Item enabled: !contextMenuButton.enabled } - // TODO: uncomment this tooltip as soon as the required firmware is released - // MonitorInfoBlurb - // { - // id: contextMenuDisabledInfo - // text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.") - // target: contextMenuButton - // } + MonitorInfoBlurb + { + id: contextMenuDisabledInfo + text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.") + target: contextMenuButton + } } diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml index 0175d5a2ad..7259312577 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml @@ -250,13 +250,12 @@ Item enabled: !contextMenuButton.enabled } - // TODO: uncomment this tooltip as soon as the required firmware is released - // MonitorInfoBlurb - // { - // id: contextMenuDisabledInfo - // text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.") - // target: contextMenuButton - // } + MonitorInfoBlurb + { + id: contextMenuDisabledInfo + text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.") + target: contextMenuButton + } CameraButton { diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml index b70759454a..4e52ac0b13 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml @@ -102,7 +102,6 @@ Item elide: Text.ElideRight font: UM.Theme.getFont("medium") // 14pt, regular anchors.verticalCenter: parent.verticalCenter - width: 600 * screenScaleFactor // TODO: Theme! (Should match column size) // FIXED-LINE-HEIGHT: height: 18 * screenScaleFactor // TODO: Theme! diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 21a7f4aa57..ed8d22a478 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -56,7 +56,7 @@ class CloudApiClient: ## Retrieves all the clusters for the user that is currently logged in. # \param on_finished: The function to be called after the result is parsed. def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any]) -> None: - url = "{}/clusters".format(self.CLUSTER_API_ROOT) + url = "{}/clusters?status=active".format(self.CLUSTER_API_ROOT) reply = self._manager.get(self._createEmptyRequest(url)) self._addCallback(reply, on_finished, CloudClusterResponse) diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index 8e62f641cb..fd8b71fa80 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -299,6 +299,8 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): print_job_data.updateOutputModel(print_job) if print_job_data.printer_uuid: self._updateAssignedPrinter(print_job, print_job_data.printer_uuid) + if print_job_data.assigned_to: + self._updateAssignedPrinter(print_job, print_job_data.assigned_to) new_print_jobs.append(print_job) # Check which print job need to be removed (de-referenced). @@ -317,6 +319,8 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): model = remote_job.createOutputModel(ClusterOutputController(self)) if remote_job.printer_uuid: self._updateAssignedPrinter(model, remote_job.printer_uuid) + if remote_job.assigned_to: + self._updateAssignedPrinter(model, remote_job.assigned_to) return model ## Updates the printer assignment for the given print job model. From 9e6e9a4beb0004f96fe52ed2e12946e3d98a756e Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 15 Aug 2019 20:16:55 +0200 Subject: [PATCH 21/63] Enable force override print job in local network, fix override button not enabled on older firmwares --- .../resources/qml/MonitorPrinterCard.qml | 19 +++++++++++++++++++ .../src/Network/ClusterApiClient.py | 5 +++++ .../src/Network/LocalClusterOutputDevice.py | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml index 7259312577..9242abacdd 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml @@ -494,6 +494,25 @@ Item implicitWidth: 96 * screenScaleFactor // TODO: Theme! visible: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 && !printerStatus.visible onClicked: base.enabled ? overrideConfirmationDialog.open() : {} + enabled: OutputDevice.supportsPrintJobActions + } + + // For cloud printing, add this mouse area over the disabled details button to indicate that it's not available + MouseArea + { + id: detailsButtonDisabledButtonArea + anchors.fill: detailsButton + hoverEnabled: detailsButton.visible && !detailsButton.enabled + onEntered: overrideButtonDisabledInfo.open() + onExited: overrideButtonDisabledInfo.close() + enabled: !detailsButton.enabled + } + + MonitorInfoBlurb + { + id: overrideButtonDisabledInfo + text: catalog.i18nc("@info", "Please update your printer's firmware to manage the queue remotely.") + target: detailsButton } } diff --git a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py index 3925ac364e..982c3a885d 100644 --- a/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Network/ClusterApiClient.py @@ -62,6 +62,11 @@ class ClusterApiClient: def movePrintJobToTop(self, print_job_uuid: str) -> None: url = "{}/print_jobs/{}/action/move".format(self.CLUSTER_API_PREFIX, print_job_uuid) self._manager.post(self._createEmptyRequest(url), json.dumps({"to_position": 0, "list": "queued"}).encode()) + + ## Override print job configuration and force it to be printed. + def forcePrintJob(self, print_job_uuid: str) -> None: + url = "{}/print_jobs/{}".format(self.CLUSTER_API_PREFIX, print_job_uuid) + self._manager.put(self._createEmptyRequest(url), json.dumps({"force": True}).encode()) ## Delete a print job from the queue. def deletePrintJob(self, print_job_uuid: str) -> None: diff --git a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py index cb373e7e1e..3d71429ef8 100644 --- a/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Network/LocalClusterOutputDevice.py @@ -86,7 +86,7 @@ class LocalClusterOutputDevice(UltimakerNetworkedPrinterOutputDevice): @pyqtSlot(str, name="forceSendJob") def forceSendJob(self, print_job_uuid: str) -> None: - pass # TODO + self._getApiClient().forcePrintJob(print_job_uuid) ## Set the remote print job state. # \param print_job_uuid: The UUID of the print job to set the state for. From 7e9662e30ea666633c59143c92006f46148ca6c1 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 15 Aug 2019 20:25:59 +0200 Subject: [PATCH 22/63] Fix monitor tab UI not updating for cloud device due to overridden signals --- .../src/Cloud/CloudOutputDevice.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py index 3a256e2860..75e2b30ff1 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py @@ -51,15 +51,9 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): # The minimum version of firmware that support print job actions over cloud. PRINT_JOB_ACTIONS_MIN_VERSION = Version("5.3.0") - # Signal triggered when the print jobs in the queue were changed. - printJobsChanged = pyqtSignal() - - # Signal triggered when the selected printer in the UI should be changed. - activePrinterChanged = pyqtSignal() - # Notify can only use signals that are defined by the class that they are in, not inherited ones. # Therefore we create a private signal used to trigger the printersChanged signal. - _clusterPrintersChanged = pyqtSignal() + _cloudClusterPrintersChanged = pyqtSignal() ## Creates a new cloud output device # \param api_client: The client that will run the API calls @@ -93,7 +87,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self._setInterfaceElements() # Trigger the printersChanged signal when the private signal is triggered. - self.printersChanged.connect(self._clusterPrintersChanged) + self.printersChanged.connect(self._cloudClusterPrintersChanged) # Keep server string of the last generated time to avoid updating models more than once for the same response self._received_printers = None # type: Optional[List[ClusterPrinterStatus]] @@ -236,7 +230,7 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice): self.writeError.emit() ## Whether the printer that this output device represents supports print job actions via the cloud. - @pyqtProperty(bool, notify=_clusterPrintersChanged) + @pyqtProperty(bool, notify=_cloudClusterPrintersChanged) def supportsPrintJobActions(self) -> bool: if not self._printers: return False From 75f52c5a247ae3f44dd1369f22672ead923aaf0e Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 15 Aug 2019 21:25:56 +0200 Subject: [PATCH 23/63] Bring back skeleton loading for monitor stage --- .../UM3NetworkPrinting/resources/qml/MonitorQueue.qml | 9 ++++++++- .../UM3NetworkPrinting/resources/qml/MonitorStage.qml | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml index 4e52ac0b13..6b3a9078c9 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml @@ -185,7 +185,14 @@ Item } printJob: modelData } - model: OutputDevice.queuedPrintJobs + model: + { + if (OutputDevice.receivedPrintJobs) + { + return OutputDevice.queuedPrintJobs + } + return [null, null] + } spacing: 6 // TODO: Theme! } } diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml index 58e4263d2d..d8ffba547a 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml @@ -50,7 +50,14 @@ Component MonitorCarousel { id: carousel - printers: OutputDevice.printers + printers: + { + if (OutputDevice.receivedPrintJobs) + { + return OutputDevice.printers + } + return [null] + } } } From 60758093f1ac7511f4f72daeb1c13fc235b8257e Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 15 Aug 2019 21:29:20 +0200 Subject: [PATCH 24/63] Determine skeleton loading properly --- plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml | 2 +- plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml | 2 +- .../src/UltimakerNetworkedPrinterOutputDevice.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml index 6b3a9078c9..ce692168c3 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml @@ -187,7 +187,7 @@ Item } model: { - if (OutputDevice.receivedPrintJobs) + if (OutputDevice.receivedData) { return OutputDevice.queuedPrintJobs } diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml index d8ffba547a..47c45f8b11 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml @@ -52,7 +52,7 @@ Component id: carousel printers: { - if (OutputDevice.receivedPrintJobs) + if (OutputDevice.receivedData) { return OutputDevice.printers } diff --git a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py index fd8b71fa80..02ce91800d 100644 --- a/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/UltimakerNetworkedPrinterOutputDevice.py @@ -103,9 +103,9 @@ class UltimakerNetworkedPrinterOutputDevice(NetworkedPrinterOutputDevice): return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state not in self.QUEUED_PRINT_JOBS_STATES] - @pyqtProperty(bool, notify=printJobsChanged) - def receivedPrintJobs(self) -> bool: - return bool(self._print_jobs) + @pyqtProperty(bool, notify=_clusterPrintersChanged) + def receivedData(self) -> bool: + return self._has_received_printers # Get the amount of printers in the cluster. @pyqtProperty(int, notify=_clusterPrintersChanged) From 04d5423d814eaf1937190cb321f02c71d99a6778 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 16 Aug 2019 13:36:15 +0200 Subject: [PATCH 25/63] Fix scrolling all the way down in Qt 5.13 It didn't properly connect the height of the column to the scrolled length, I think. Perhaps a Qt bug. It didn't occur in 5.11 or 5.10 yet. --- plugins/Toolbox/resources/qml/ToolboxDownloadsPage.qml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsPage.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsPage.qml index 5ea24d17ba..bd89829604 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsPage.qml @@ -10,9 +10,11 @@ ScrollView clip: true width: parent.width height: parent.height + contentHeight: mainColumn.height Column { + id: mainColumn width: base.width spacing: UM.Theme.getSize("default_margin").height @@ -30,13 +32,13 @@ ScrollView model: toolbox.viewCategory === "material" ? toolbox.materialsAvailableModel : toolbox.pluginsAvailableModel } - ToolboxDownloadsGrid + /*ToolboxDownloadsGrid { id: genericMaterials visible: toolbox.viewCategory === "material" width: parent.width heading: catalog.i18nc("@label", "Generic Materials") model: toolbox.materialsGenericModel - } + }*/ } } From 362c7d09fb290e00de0e03ca64ebb4ea9f27e3ff Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Fri, 16 Aug 2019 13:38:14 +0200 Subject: [PATCH 26/63] Fix displaying generic materials I had that commented out for debugging. --- plugins/Toolbox/resources/qml/ToolboxDownloadsPage.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsPage.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsPage.qml index bd89829604..57fb3a9279 100644 --- a/plugins/Toolbox/resources/qml/ToolboxDownloadsPage.qml +++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsPage.qml @@ -32,13 +32,13 @@ ScrollView model: toolbox.viewCategory === "material" ? toolbox.materialsAvailableModel : toolbox.pluginsAvailableModel } - /*ToolboxDownloadsGrid + ToolboxDownloadsGrid { id: genericMaterials visible: toolbox.viewCategory === "material" width: parent.width heading: catalog.i18nc("@label", "Generic Materials") model: toolbox.materialsGenericModel - }*/ + } } } From 53efad7e6f5e97ccc68271b8d2f17d6dc20a70d5 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 19 Aug 2019 12:39:54 +0200 Subject: [PATCH 27/63] Fix translation of horizontal expansion in French It's not 'horizontal printing speed'. --- resources/i18n/fr_FR/fdmprinter.def.json.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/i18n/fr_FR/fdmprinter.def.json.po b/resources/i18n/fr_FR/fdmprinter.def.json.po index 80a36ffa5e..5e7190fc47 100644 --- a/resources/i18n/fr_FR/fdmprinter.def.json.po +++ b/resources/i18n/fr_FR/fdmprinter.def.json.po @@ -1223,7 +1223,7 @@ msgstr "Imprimer les parties du modèle qui sont horizontalement plus fines que #: fdmprinter.def.json msgctxt "xy_offset label" msgid "Horizontal Expansion" -msgstr "Vitesse d’impression horizontale" +msgstr "Expansion horizontale" #: fdmprinter.def.json msgctxt "xy_offset description" From f4dc9cc6a2fa290461409cafc84a35a1e8bc6c65 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 20 Aug 2019 08:24:43 +0200 Subject: [PATCH 28/63] Fix QDesktopServices.openUrl() for AppImage on Linux --- cura/OAuth2/AuthorizationService.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index f455c135ae..cd28f5a73f 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -3,6 +3,7 @@ import json from datetime import datetime, timedelta +import os from typing import Optional, TYPE_CHECKING from urllib.parse import urlencode @@ -13,6 +14,7 @@ from PyQt5.QtGui import QDesktopServices from UM.Logger import Logger from UM.Message import Message +from UM.Platform import Platform from UM.Signal import Signal from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer @@ -164,9 +166,24 @@ class AuthorizationService: "code_challenge_method": "S512" }) + # GITHUB issue #6194: https://github.com/Ultimaker/Cura/issues/6194 + # With AppImage 2 on Linux, the current working directory will be somewhere in /tmp//usr, which is owned + # by root. For some reason, QDesktopServices.openUrl() requires to have a usable current working directory, + # otherwise it doesn't work. This is a workaround on Linux that before we call QDesktopServices.openUrl(), we + # switch to a directory where the user has the ownership. + old_work_dir = "" + if Platform.isLinux(): + # Change the working directory to user home + old_work_dir = os.getcwd() + os.chdir(os.path.expanduser("~")) + # Open the authorization page in a new browser window. QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string))) + if Platform.isLinux(): + # Change the working directory back + os.chdir(old_work_dir) + # Start a local web server to receive the callback URL on. self._server.start(verification_code) From 4762711c3374c8f6372e5d8ec73fedb52baf59da Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 20 Aug 2019 11:04:54 +0200 Subject: [PATCH 29/63] chdir to ~ on Linux if frozen --- cura/OAuth2/AuthorizationService.py | 15 --------------- cura_app.py | 8 ++++++++ 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py index cd28f5a73f..95ea47112e 100644 --- a/cura/OAuth2/AuthorizationService.py +++ b/cura/OAuth2/AuthorizationService.py @@ -166,24 +166,9 @@ class AuthorizationService: "code_challenge_method": "S512" }) - # GITHUB issue #6194: https://github.com/Ultimaker/Cura/issues/6194 - # With AppImage 2 on Linux, the current working directory will be somewhere in /tmp//usr, which is owned - # by root. For some reason, QDesktopServices.openUrl() requires to have a usable current working directory, - # otherwise it doesn't work. This is a workaround on Linux that before we call QDesktopServices.openUrl(), we - # switch to a directory where the user has the ownership. - old_work_dir = "" - if Platform.isLinux(): - # Change the working directory to user home - old_work_dir = os.getcwd() - os.chdir(os.path.expanduser("~")) - # Open the authorization page in a new browser window. QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string))) - if Platform.isLinux(): - # Change the working directory back - os.chdir(old_work_dir) - # Start a local web server to receive the callback URL on. self._server.start(verification_code) diff --git a/cura_app.py b/cura_app.py index 3599f127cc..b2cd317243 100755 --- a/cura_app.py +++ b/cura_app.py @@ -60,6 +60,14 @@ if Platform.isWindows() and hasattr(sys, "frozen"): except KeyError: pass +# GITHUB issue #6194: https://github.com/Ultimaker/Cura/issues/6194 +# With AppImage 2 on Linux, the current working directory will be somewhere in /tmp//usr, which is owned +# by root. For some reason, QDesktopServices.openUrl() requires to have a usable current working directory, +# otherwise it doesn't work. This is a workaround on Linux that before we call QDesktopServices.openUrl(), we +# switch to a directory where the user has the ownership. +if Platform.isLinux() and hasattr(sys, "frozen"): + os.chdir(os.path.expanduser("~")) + # WORKAROUND: GITHUB-704 GITHUB-708 # It looks like setuptools creates a .pth file in # the default /usr/lib which causes the default site-packages From a179d7118d8575166f8e60d73aaa7fc3540abb40 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Tue, 20 Aug 2019 17:17:52 +0200 Subject: [PATCH 30/63] Fix renaming mistake from 1fa5628cb2e35425d1b42f9dbb20a9bda45da032 --- cura/Machines/Models/DiscoveredPrintersModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/Machines/Models/DiscoveredPrintersModel.py b/cura/Machines/Models/DiscoveredPrintersModel.py index 33e0b7a4d9..a1b68ee1ae 100644 --- a/cura/Machines/Models/DiscoveredPrintersModel.py +++ b/cura/Machines/Models/DiscoveredPrintersModel.py @@ -75,7 +75,7 @@ class DiscoveredPrinter(QObject): def readableMachineType(self) -> str: from cura.CuraApplication import CuraApplication machine_manager = CuraApplication.getInstance().getMachineManager() - # In LocalClusterOutputDevice, when it updates a printer information, it updates the machine type using the field + # In NetworkOutputDevice, when it updates a printer information, it updates the machine type using the field # "machine_variant", and for some reason, it's not the machine type ID/codename/... but a human-readable string # like "Ultimaker 3". The code below handles this case. if self._hasHumanReadableMachineTypeName(self._machine_type): From 9122d0cca44d684d937898afa6aa7b39e5180d02 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Wed, 21 Aug 2019 09:56:32 +0200 Subject: [PATCH 31/63] Remove misleading text that says you could enter hostnames We don't resolve the hostnames ever. You can only enter IP addresses. --- plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml | 2 +- resources/qml/WelcomePages/AddPrinterByIpContent.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml index f600083f36..b27416e199 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml @@ -331,7 +331,7 @@ Cura.MachineAction Label { - text: catalog.i18nc("@label", "Enter the IP address or hostname of your printer on the network.") + text: catalog.i18nc("@label", "Enter the IP address of your printer on the network.") width: parent.width wrapMode: Text.WordWrap renderType: Text.NativeRendering diff --git a/resources/qml/WelcomePages/AddPrinterByIpContent.qml b/resources/qml/WelcomePages/AddPrinterByIpContent.qml index 4aec5879c1..5ab0217f01 100644 --- a/resources/qml/WelcomePages/AddPrinterByIpContent.qml +++ b/resources/qml/WelcomePages/AddPrinterByIpContent.qml @@ -99,7 +99,7 @@ Item font: UM.Theme.getFont("default") color: UM.Theme.getColor("text") renderType: Text.NativeRendering - text: catalog.i18nc("@label", "Enter the IP address or hostname of your printer on the network.") + text: catalog.i18nc("@label", "Enter the IP address of your printer on the network.") } Item From 9f16973215a10d9b60d9a5477600a8e88e39fcb1 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 22 Aug 2019 09:46:49 +0200 Subject: [PATCH 32/63] Don't remove skirt when printing with support According to tests by theWaldschrat and Liger0, the skirt is still necessary when support is enabled. Fixes #6229. --- resources/definitions/creality_base.def.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/definitions/creality_base.def.json b/resources/definitions/creality_base.def.json index 440e93a948..de51cb1a53 100644 --- a/resources/definitions/creality_base.def.json +++ b/resources/definitions/creality_base.def.json @@ -107,8 +107,8 @@ "cool_fan_enabled": { "value": true }, "cool_min_layer_time": { "value": 10 }, - "adhesion_type": { "value": "'none' if support_enable else 'skirt'" }, - "brim_replaces_support": { "value": false}, + "adhesion_type": { "value": "'skirt'" }, + "brim_replaces_support": { "value": false }, "skirt_gap": { "value": 10.0 }, "skirt_line_count": { "value": 4 }, From 7ccf46124f047a0a33f0a6af9bf9589f76eb0152 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Thu, 22 Aug 2019 13:37:30 +0200 Subject: [PATCH 33/63] Give warning when build plate temperature is below volume temperature It's not going to become lower than the ambient temperature after all. --- resources/definitions/fdmprinter.def.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json index d8064eebd5..3a0d80c27b 100644 --- a/resources/definitions/fdmprinter.def.json +++ b/resources/definitions/fdmprinter.def.json @@ -2200,7 +2200,7 @@ "resolve": "max(extruderValues('default_material_bed_temperature'))", "default_value": 60, "minimum_value": "-273.15", - "minimum_value_warning": "0", + "minimum_value_warning": "build_volume_temperature", "maximum_value_warning": "130", "enabled": false, "settable_per_mesh": false, @@ -2217,7 +2217,7 @@ "value": "default_material_bed_temperature", "resolve": "max(extruderValues('material_bed_temperature'))", "minimum_value": "-273.15", - "minimum_value_warning": "0", + "minimum_value_warning": "build_volume_temperature", "maximum_value_warning": "130", "enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"", "settable_per_mesh": false, @@ -2234,7 +2234,7 @@ "default_value": 60, "value": "resolveOrValue('material_bed_temperature')", "minimum_value": "-273.15", - "minimum_value_warning": "max(extruderValues('material_bed_temperature'))", + "minimum_value_warning": "max(build_volume_temperature, max(extruderValues('material_bed_temperature')))", "maximum_value_warning": "130", "enabled": "machine_heated_bed and machine_gcode_flavor != \"UltiGCode\"", "settable_per_mesh": false, From d2e97154093657b0e75d695648bd3e5dd87dd8c1 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 22 Aug 2019 14:02:29 +0200 Subject: [PATCH 34/63] Add availableConfiguration property to the output model CURA-6732 --- .../Models/PrinterOutputModel.py | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/cura/PrinterOutput/Models/PrinterOutputModel.py b/cura/PrinterOutput/Models/PrinterOutputModel.py index 13fe85e674..105ead96f5 100644 --- a/cura/PrinterOutput/Models/PrinterOutputModel.py +++ b/cura/PrinterOutput/Models/PrinterOutputModel.py @@ -7,6 +7,7 @@ from UM.Math.Vector import Vector from cura.PrinterOutput.Peripheral import Peripheral from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel +from UM.Logger import Logger if TYPE_CHECKING: from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel @@ -50,6 +51,8 @@ class PrinterOutputModel(QObject): self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._extruders] + self._available_printer_configurations = [] # type: List[PrinterConfigurationModel] + self._camera_url = QUrl() # type: QUrl @pyqtProperty(str, constant = True) @@ -290,7 +293,7 @@ class PrinterOutputModel(QObject): def _onControllerCanUpdateFirmwareChanged(self) -> None: self.canUpdateFirmwareChanged.emit() - # Returns the configuration (material, variant and buildplate) of the current printer + # Returns the active configuration (material, variant and buildplate) of the current printer @pyqtProperty(QObject, notify = configurationChanged) def printerConfiguration(self) -> Optional[PrinterConfigurationModel]: if self._printer_configuration.isValid(): @@ -309,4 +312,26 @@ class PrinterOutputModel(QObject): def removePeripheral(self, peripheral: Peripheral) -> None: self._peripherals.remove(peripheral) - self.peripheralsChanged.emit() \ No newline at end of file + self.peripheralsChanged.emit() + + availableConfigurationsChanged = pyqtSignal() + + @pyqtProperty("QVariantList", notify = availableConfigurationsChanged) + def availableConfigurations(self) -> List[PrinterConfigurationModel]: + return self._available_printer_configurations + + def addAvailableConfiguration(self, new_configuration: PrinterConfigurationModel) -> None: + self._available_printer_configurations.append(new_configuration) + self.availableConfigurationsChanged.emit() + + def removeAvailableConfiguration(self, config_to_remove: PrinterConfigurationModel) -> None: + try: + self._available_printer_configurations.remove(config_to_remove) + except ValueError: + Logger.log("w", "Unable to remove configuration that isn't in the list of available configurations") + else: + self.availableConfigurationsChanged.emit() + + def setAvailableConfigurations(self, new_configurations: List[PrinterConfigurationModel]) -> None: + self._available_printer_configurations = new_configurations + self.availableConfigurationsChanged.emit() From d1720db5ad92d9832cf4706d87caf96550c90fef Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 22 Aug 2019 14:07:44 +0200 Subject: [PATCH 35/63] Ensure that the available configurations are also used in the uniqueConfigurations CURA-6372 --- cura/PrinterOutput/PrinterOutputDevice.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cura/PrinterOutput/PrinterOutputDevice.py b/cura/PrinterOutput/PrinterOutputDevice.py index d4a37b3d68..66a507ab9a 100644 --- a/cura/PrinterOutput/PrinterOutputDevice.py +++ b/cura/PrinterOutput/PrinterOutputDevice.py @@ -220,10 +220,12 @@ class PrinterOutputDevice(QObject, OutputDevice): return self._unique_configurations def _updateUniqueConfigurations(self) -> None: - self._unique_configurations = sorted( - {printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None}, - key=lambda config: config.printerType, - ) + all_configurations = set() + for printer in self._printers: + if printer.printerConfiguration is not None: + all_configurations.add(printer.printerConfiguration) + all_configurations.update(printer.availableConfigurations) + self._unique_configurations = sorted(all_configurations, key = lambda config: config.printerType) self.uniqueConfigurationsChanged.emit() # Returns the unique configurations of the printers within this output device @@ -234,6 +236,7 @@ class PrinterOutputDevice(QObject, OutputDevice): def _onPrintersChanged(self) -> None: for printer in self._printers: printer.configurationChanged.connect(self._updateUniqueConfigurations) + printer.availableConfigurationsChanged.connect(self._updateUniqueConfigurations) # At this point there may be non-updated configurations self._updateUniqueConfigurations() From 561a3e53e5e60230c8a848debac0cea74807767a Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 22 Aug 2019 14:09:39 +0200 Subject: [PATCH 36/63] Only add available configuration if it wasn't already in the list CURA-6732 --- cura/PrinterOutput/Models/PrinterOutputModel.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cura/PrinterOutput/Models/PrinterOutputModel.py b/cura/PrinterOutput/Models/PrinterOutputModel.py index 105ead96f5..ccdbb500b9 100644 --- a/cura/PrinterOutput/Models/PrinterOutputModel.py +++ b/cura/PrinterOutput/Models/PrinterOutputModel.py @@ -321,8 +321,9 @@ class PrinterOutputModel(QObject): return self._available_printer_configurations def addAvailableConfiguration(self, new_configuration: PrinterConfigurationModel) -> None: - self._available_printer_configurations.append(new_configuration) - self.availableConfigurationsChanged.emit() + if new_configuration not in self._available_printer_configurations: + self._available_printer_configurations.append(new_configuration) + self.availableConfigurationsChanged.emit() def removeAvailableConfiguration(self, config_to_remove: PrinterConfigurationModel) -> None: try: From 73c6676673d6a8f9411141277c99377ad96efcb1 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 22 Aug 2019 14:17:40 +0200 Subject: [PATCH 37/63] Added missing test for camera URL --- tests/PrinterOutput/Models/TestPrinterOutputModel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/PrinterOutput/Models/TestPrinterOutputModel.py b/tests/PrinterOutput/Models/TestPrinterOutputModel.py index 3fdb61adbd..577a27bd6f 100644 --- a/tests/PrinterOutput/Models/TestPrinterOutputModel.py +++ b/tests/PrinterOutput/Models/TestPrinterOutputModel.py @@ -10,6 +10,7 @@ from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel test_validate_data_get_set = [ {"attribute": "name", "value": "YAY"}, {"attribute": "targetBedTemperature", "value": 192}, + {"attribute": "cameraUrl", "value": "YAY!"} ] test_validate_data_get_update = [ @@ -22,6 +23,7 @@ test_validate_data_get_update = [ {"attribute": "targetBedTemperature", "value": 9001}, {"attribute": "activePrintJob", "value": PrintJobOutputModel(MagicMock())}, {"attribute": "state", "value": "BEEPBOOP"}, + ] From 34c3a0474464f621fa5d35e7f5a9bbe09f4503ac Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 22 Aug 2019 14:24:19 +0200 Subject: [PATCH 38/63] Added missing tests for peripheral Not part of the ticket, but I'm boyscouting this. --- .../Models/PrinterOutputModel.py | 2 +- .../Models/TestPrinterOutputModel.py | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/cura/PrinterOutput/Models/PrinterOutputModel.py b/cura/PrinterOutput/Models/PrinterOutputModel.py index ccdbb500b9..3927da0313 100644 --- a/cura/PrinterOutput/Models/PrinterOutputModel.py +++ b/cura/PrinterOutput/Models/PrinterOutputModel.py @@ -304,7 +304,7 @@ class PrinterOutputModel(QObject): @pyqtProperty(str, notify = peripheralsChanged) def peripherals(self) -> str: - return ", ".join(*[peripheral.name for peripheral in self._peripherals]) + return ", ".join([peripheral.name for peripheral in self._peripherals]) def addPeripheral(self, peripheral: Peripheral) -> None: self._peripherals.append(peripheral) diff --git a/tests/PrinterOutput/Models/TestPrinterOutputModel.py b/tests/PrinterOutput/Models/TestPrinterOutputModel.py index 577a27bd6f..8136e670b7 100644 --- a/tests/PrinterOutput/Models/TestPrinterOutputModel.py +++ b/tests/PrinterOutput/Models/TestPrinterOutputModel.py @@ -6,6 +6,7 @@ import pytest from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel +from cura.PrinterOutput.Peripheral import Peripheral test_validate_data_get_set = [ {"attribute": "name", "value": "YAY"}, @@ -81,3 +82,24 @@ def test_getAndUpdate(data): getattr(model, "update" + attribute)(data["value"]) # The signal should not fire again assert signal.emit.call_count == 1 + + +def test_peripherals(): + model = PrinterOutputModel(MagicMock()) + model.peripheralsChanged = MagicMock() + + peripheral = MagicMock(spec=Peripheral) + peripheral.name = "test" + peripheral2 = MagicMock(spec=Peripheral) + peripheral2.name = "test2" + + model.addPeripheral(peripheral) + assert model.peripheralsChanged.emit.call_count == 1 + model.addPeripheral(peripheral2) + assert model.peripheralsChanged.emit.call_count == 2 + + assert model.peripherals == "test, test2" + + model.removePeripheral(peripheral) + assert model.peripheralsChanged.emit.call_count == 3 + assert model.peripherals == "test2" From 89260891e6cc156358a2e0ce5fb8e0a2e31cfb74 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 22 Aug 2019 14:32:33 +0200 Subject: [PATCH 39/63] Add tests for the available configurations CURA-6732 --- .../Models/TestPrinterOutputModel.py | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/PrinterOutput/Models/TestPrinterOutputModel.py b/tests/PrinterOutput/Models/TestPrinterOutputModel.py index 8136e670b7..9848e0a5fa 100644 --- a/tests/PrinterOutput/Models/TestPrinterOutputModel.py +++ b/tests/PrinterOutput/Models/TestPrinterOutputModel.py @@ -5,6 +5,7 @@ from unittest.mock import MagicMock import pytest from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel +from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.Peripheral import Peripheral @@ -103,3 +104,46 @@ def test_peripherals(): model.removePeripheral(peripheral) assert model.peripheralsChanged.emit.call_count == 3 assert model.peripherals == "test2" + + +def test_availableConfigurations_addConfiguration(): + model = PrinterOutputModel(MagicMock()) + + configuration = MagicMock(spec = PrinterConfigurationModel) + + model.addAvailableConfiguration(configuration) + assert model.availableConfigurations == [configuration] + + +def test_availableConfigurations_addConfigTwice(): + model = PrinterOutputModel(MagicMock()) + + configuration = MagicMock(spec=PrinterConfigurationModel) + + model.setAvailableConfigurations([configuration]) + assert model.availableConfigurations == [configuration] + + # Adding it again should not have any effect + model.addAvailableConfiguration(configuration) + assert model.availableConfigurations == [configuration] + + +def test_availableConfigurations_removeConfig(): + model = PrinterOutputModel(MagicMock()) + + configuration = MagicMock(spec=PrinterConfigurationModel) + + model.addAvailableConfiguration(configuration) + model.removeAvailableConfiguration(configuration) + assert model.availableConfigurations == [] + + +def test_removeAlreadyRemovedConfiguration(): + model = PrinterOutputModel(MagicMock()) + + configuration = MagicMock(spec=PrinterConfigurationModel) + model.availableConfigurationsChanged = MagicMock() + model.removeAvailableConfiguration(configuration) + assert model.availableConfigurationsChanged.emit.call_count == 0 + assert model.availableConfigurations == [] + From 1e2f5ddecd91433d0e62e8f009edbc2122173f43 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 22 Aug 2019 14:53:13 +0200 Subject: [PATCH 40/63] Add test for unique printer configuration CURA-6732 --- .../PrinterOutput/TestPrinterOutputDevice.py | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/tests/PrinterOutput/TestPrinterOutputDevice.py b/tests/PrinterOutput/TestPrinterOutputDevice.py index 4c12a34859..e0415295c1 100644 --- a/tests/PrinterOutput/TestPrinterOutputDevice.py +++ b/tests/PrinterOutput/TestPrinterOutputDevice.py @@ -1,6 +1,10 @@ from unittest.mock import MagicMock import pytest +from unittest.mock import patch + +from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel +from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice test_validate_data_get_set = [ @@ -8,10 +12,15 @@ test_validate_data_get_set = [ {"attribute": "connectionState", "value": 1}, ] +@pytest.fixture() +def printer_output_device(): + with patch("UM.Application.Application.getInstance"): + return PrinterOutputDevice("whatever") + @pytest.mark.parametrize("data", test_validate_data_get_set) -def test_getAndSet(data): - model = PrinterOutputDevice("whatever") +def test_getAndSet(data, printer_output_device): + model = printer_output_device # Convert the first letter into a capital attribute = list(data["attribute"]) @@ -35,3 +44,21 @@ def test_getAndSet(data): getattr(model, "set" + attribute)(data["value"]) # The signal should not fire again assert signal.emit.call_count == 1 + + +def test_uniqueConfigurations(printer_output_device): + printer = PrinterOutputModel(MagicMock()) + # Add a printer and fire the signal that ensures they get hooked up correctly. + printer_output_device._printers = [printer] + printer_output_device._onPrintersChanged() + + assert printer_output_device.uniqueConfigurations == [] + configuration = PrinterConfigurationModel() + printer.addAvailableConfiguration(configuration) + + assert printer_output_device.uniqueConfigurations == [configuration] + + # Once the type of printer is set, it's active configuration counts as being set. + # In that case, that should also be added to the list of available configurations + printer.updateType("blarg!") + assert printer_output_device.uniqueConfigurations == [configuration, printer.printerConfiguration] \ No newline at end of file From 5cb485d4d39a91885f743947c2e04a205e1c11bc Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 22 Aug 2019 15:07:57 +0200 Subject: [PATCH 41/63] Only emit uniqueConfigurationsChanged signal if the set changed CURA-6732 --- cura/PrinterOutput/PrinterOutputDevice.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cura/PrinterOutput/PrinterOutputDevice.py b/cura/PrinterOutput/PrinterOutputDevice.py index 66a507ab9a..bb4f9e79fb 100644 --- a/cura/PrinterOutput/PrinterOutputDevice.py +++ b/cura/PrinterOutput/PrinterOutputDevice.py @@ -225,8 +225,10 @@ class PrinterOutputDevice(QObject, OutputDevice): if printer.printerConfiguration is not None: all_configurations.add(printer.printerConfiguration) all_configurations.update(printer.availableConfigurations) - self._unique_configurations = sorted(all_configurations, key = lambda config: config.printerType) - self.uniqueConfigurationsChanged.emit() + new_configurations = sorted(all_configurations, key = lambda config: config.printerType) + if new_configurations != self._unique_configurations: + self._unique_configurations = new_configurations + self.uniqueConfigurationsChanged.emit() # Returns the unique configurations of the printers within this output device @pyqtProperty("QStringList", notify = uniqueConfigurationsChanged) From a1ca705de99e384362579fdf4798ebc6c9eac718 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 22 Aug 2019 15:10:17 +0200 Subject: [PATCH 42/63] Add documentation CURA-6732 --- cura/PrinterOutput/Models/PrinterOutputModel.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cura/PrinterOutput/Models/PrinterOutputModel.py b/cura/PrinterOutput/Models/PrinterOutputModel.py index 3927da0313..d04fccca1b 100644 --- a/cura/PrinterOutput/Models/PrinterOutputModel.py +++ b/cura/PrinterOutput/Models/PrinterOutputModel.py @@ -316,6 +316,8 @@ class PrinterOutputModel(QObject): availableConfigurationsChanged = pyqtSignal() + # The availableConfigurations are configuration options that a printer can switch to, but doesn't currently have + # active (eg; Automatic tool changes, material loaders, etc). @pyqtProperty("QVariantList", notify = availableConfigurationsChanged) def availableConfigurations(self) -> List[PrinterConfigurationModel]: return self._available_printer_configurations From bc4b2a596a4e4fbeaafea05a95a4b464c0866bed Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Thu, 22 Aug 2019 15:58:12 +0200 Subject: [PATCH 43/63] Rename _printer_configuration to _active_printer_configuration CURA-6732 --- cura/PrinterOutput/Models/PrinterOutputModel.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/cura/PrinterOutput/Models/PrinterOutputModel.py b/cura/PrinterOutput/Models/PrinterOutputModel.py index d04fccca1b..b8bea999c7 100644 --- a/cura/PrinterOutput/Models/PrinterOutputModel.py +++ b/cura/PrinterOutput/Models/PrinterOutputModel.py @@ -38,7 +38,7 @@ class PrinterOutputModel(QObject): self._controller = output_controller self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged) self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)] - self._printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer + self._active_printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer self._head_position = Vector(0, 0, 0) self._active_print_job = None # type: Optional[PrintJobOutputModel] self._firmware_version = firmware_version @@ -48,8 +48,8 @@ class PrinterOutputModel(QObject): self._buildplate = "" self._peripherals = [] # type: List[Peripheral] - self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in - self._extruders] + self._active_printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in + self._extruders] self._available_printer_configurations = [] # type: List[PrinterConfigurationModel] @@ -84,7 +84,7 @@ class PrinterOutputModel(QObject): def updateType(self, printer_type: str) -> None: if self._printer_type != printer_type: self._printer_type = printer_type - self._printer_configuration.printerType = self._printer_type + self._active_printer_configuration.printerType = self._printer_type self.typeChanged.emit() self.configurationChanged.emit() @@ -95,7 +95,7 @@ class PrinterOutputModel(QObject): def updateBuildplate(self, buildplate: str) -> None: if self._buildplate != buildplate: self._buildplate = buildplate - self._printer_configuration.buildplateConfiguration = self._buildplate + self._active_printer_configuration.buildplateConfiguration = self._buildplate self.buildplateChanged.emit() self.configurationChanged.emit() @@ -296,8 +296,8 @@ class PrinterOutputModel(QObject): # Returns the active configuration (material, variant and buildplate) of the current printer @pyqtProperty(QObject, notify = configurationChanged) def printerConfiguration(self) -> Optional[PrinterConfigurationModel]: - if self._printer_configuration.isValid(): - return self._printer_configuration + if self._active_printer_configuration.isValid(): + return self._active_printer_configuration return None peripheralsChanged = pyqtSignal() From 3578afd4ac2b94beea703f1534c06f422cfa1417 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Thu, 22 Aug 2019 23:47:07 +0200 Subject: [PATCH 44/63] Add support for multiple available configurations via network and cloud --- plugins/ThingiBrowser | 1 + .../Http/ClusterPrintCoreConfiguration.py | 5 +- .../Http/ClusterPrinterMaterialStation.py | 23 ++++++ .../Http/ClusterPrinterMaterialStationSlot.py | 17 ++++ .../src/Models/Http/ClusterPrinterStatus.py | 77 ++++++++++++++++--- 5 files changed, 111 insertions(+), 12 deletions(-) create mode 120000 plugins/ThingiBrowser create mode 100644 plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py create mode 100644 plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py diff --git a/plugins/ThingiBrowser b/plugins/ThingiBrowser new file mode 120000 index 0000000000..8126b65fd5 --- /dev/null +++ b/plugins/ThingiBrowser @@ -0,0 +1 @@ +/Users/chris/Code/ChrisTerBeke/ThingiBrowser \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py index 24c9a577f9..e11d2be2d2 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrintCoreConfiguration.py @@ -9,7 +9,8 @@ from .ClusterPrinterConfigurationMaterial import ClusterPrinterConfigurationMate from ..BaseModel import BaseModel -## Class representing a cloud cluster printer configuration +## Class representing a cloud cluster printer configuration +# Also used for representing slots in a Material Station (as from Cura's perspective these are the same). class ClusterPrintCoreConfiguration(BaseModel): ## Creates a new cloud cluster printer configuration object @@ -18,7 +19,7 @@ class ClusterPrintCoreConfiguration(BaseModel): # \param nozzle_diameter: The diameter of the print core at this position in millimeters, e.g. '0.4'. # \param print_core_id: The type of print core inserted at this position, e.g. 'AA 0.4'. def __init__(self, extruder_index: int, - material: Union[None, Dict[str, Any], ClusterPrinterConfigurationMaterial], + material: Union[None, Dict[str, Any], ClusterPrinterConfigurationMaterial] = None, print_core_id: Optional[str] = None, **kwargs) -> None: self.extruder_index = extruder_index self.material = self.parseModel(ClusterPrinterConfigurationMaterial, material) if material else None diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py new file mode 100644 index 0000000000..295044b957 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStation.py @@ -0,0 +1,23 @@ +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from typing import Union, Dict, Any, List + +from ..BaseModel import BaseModel +from .ClusterPrinterMaterialStationSlot import ClusterPrinterMaterialStationSlot + + +## Class representing the data of a Material Station in the cluster. +class ClusterPrinterMaterialStation(BaseModel): + + ## Creates a new Material Station status. + # \param status: The status of the material station. + # \param: supported: Whether the material station is supported on this machine or not. + # \param material_slots: The active slots configurations of this material station. + def __init__(self, status: str, supported: bool = False, + material_slots: Union[None, Dict[str, Any], ClusterPrinterMaterialStationSlot] = None, + **kwargs) -> None: + self.status = status + self.supported = supported + self.material_slots = self.parseModels(ClusterPrinterMaterialStationSlot, material_slots)\ + if material_slots else [] # type: List[ClusterPrinterMaterialStationSlot] + super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py new file mode 100644 index 0000000000..2e6bb6e7a5 --- /dev/null +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterMaterialStationSlot.py @@ -0,0 +1,17 @@ +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration + + +## Class representing the data of a single slot in the material station. +class ClusterPrinterMaterialStationSlot(ClusterPrintCoreConfiguration): + + ## Create a new material station slot object. + # \param slot_index: The index of the slot in the material station (ranging 0 to 5). + # \param compatible: Whether the configuration is compatible with the print core. + # \param material_remaining: How much material is remaining on the spool (between 0 and 1, or -1 for missing data). + def __init__(self, slot_index: int, compatible: bool, material_remaining: float, **kwargs): + self.slot_index = slot_index + self.compatible = compatible + self.material_remaining = material_remaining + super().__init__(**kwargs) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index 7ab2082451..6e971e2bd3 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -1,14 +1,18 @@ # Copyright (c) 2019 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from itertools import product from typing import List, Union, Dict, Optional, Any from PyQt5.QtCore import QUrl +from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from .ClusterBuildPlate import ClusterBuildPlate from .ClusterPrintCoreConfiguration import ClusterPrintCoreConfiguration +from .ClusterPrinterMaterialStation import ClusterPrinterMaterialStation +from .ClusterPrinterMaterialStationSlot import ClusterPrinterMaterialStationSlot from ..BaseModel import BaseModel @@ -26,17 +30,19 @@ class ClusterPrinterStatus(BaseModel): # \param uuid: The unique ID of the printer, also known as GUID. # \param configuration: The active print core configurations of this printer. # \param reserved_by: A printer can be claimed by a specific print job. - # \param maintenance_required: Indicates if maintenance is necessary + # \param maintenance_required: Indicates if maintenance is necessary. # \param firmware_update_status: Whether the printer's firmware is up-to-date, value is one of: "up_to_date", - # "pending_update", "update_available", "update_in_progress", "update_failed", "update_impossible" - # \param latest_available_firmware: The version of the latest firmware that is available - # \param build_plate: The build plate that is on the printer + # "pending_update", "update_available", "update_in_progress", "update_failed", "update_impossible". + # \param latest_available_firmware: The version of the latest firmware that is available. + # \param build_plate: The build plate that is on the printer. + # \param material_station: The material station that is on the printer. def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str, status: str, unique_name: str, uuid: str, configuration: List[Union[Dict[str, Any], ClusterPrintCoreConfiguration]], reserved_by: Optional[str] = None, maintenance_required: Optional[bool] = None, firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None, - build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, **kwargs) -> None: + build_plate: Union[Dict[str, Any], ClusterBuildPlate] = None, + material_station: Union[Dict[str, Any], ClusterPrinterMaterialStation] = None, **kwargs) -> None: self.configuration = self.parseModels(ClusterPrintCoreConfiguration, configuration) self.enabled = enabled @@ -52,6 +58,8 @@ class ClusterPrinterStatus(BaseModel): self.firmware_update_status = firmware_update_status self.latest_available_firmware = latest_available_firmware self.build_plate = self.parseModel(ClusterBuildPlate, build_plate) if build_plate else None + self.material_station = self.parseModel(ClusterPrinterMaterialStation, + material_station) if material_station else None super().__init__(**kwargs) ## Creates a new output model. @@ -71,8 +79,57 @@ class ClusterPrinterStatus(BaseModel): model.updateBuildplate(self.build_plate.type if self.build_plate else "glass") model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address))) - if model.printerConfiguration is not None: - for configuration, extruder_output, extruder_config in \ - zip(self.configuration, model.extruders, model.printerConfiguration.extruderConfigurations): - configuration.updateOutputModel(extruder_output) - configuration.updateConfigurationModel(extruder_config) + # Set the possible configurations based on whether a Material Station is present or not. + if self.material_station is not None: + self._updateAvailableConfigurations(model) + if self.configuration is not None: + self._updateActiveConfiguration(model) + + def _updateActiveConfiguration(self, model: PrinterOutputModel) -> None: + configurations = zip(self.configuration, model.extruders, model.printerConfiguration.extruderConfigurations) + for configuration, extruder_output, extruder_config in configurations: + configuration.updateOutputModel(extruder_output) + configuration.updateConfigurationModel(extruder_config) + + def _updateAvailableConfigurations(self, model: PrinterOutputModel) -> None: + + # Generate a list of configurations for the left extruder. + left_configurations = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration( + slot = slot, + extruder_index = 0 + )] + + # Generate a list of configurations for the right extruder. + right_configurations = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration( + slot = slot, + extruder_index = 1 + )] + + # Create a list of all available combinations between both print cores. + available_configurations = [self._createAvailableConfigurationFromPrinterConfiguration( + left_slot = left_slot, + right_slot = right_slot, + printer_configuration = model.printerConfiguration + ) for left_slot, right_slot in product(left_configurations, right_configurations)] + + # Let Cura know which available configurations there are. + model.setAvailableConfigurations(available_configurations) + + ## Check if a configuration is supported in order to make it selectable by the user. + # We filter out any slot that is not supported by the extruder index, print core type or if the material is empty. + @staticmethod + def _isSupportedConfiguration(slot: ClusterPrinterMaterialStationSlot, extruder_index: int) -> bool: + return slot.extruder_index == extruder_index and slot.compatible and slot.material and \ + slot.material_remaining != 0 + + @staticmethod + def _createAvailableConfigurationFromPrinterConfiguration(left_slot: ClusterPrinterMaterialStationSlot, + right_slot: ClusterPrinterMaterialStationSlot, + printer_configuration: PrinterConfigurationModel + ) -> PrinterConfigurationModel: + available_configuration = PrinterConfigurationModel() + available_configuration.setExtruderConfigurations([left_slot.createConfigurationModel(), + right_slot.createConfigurationModel()]) + available_configuration.setPrinterType(printer_configuration.printerType) + available_configuration.setBuildplateConfiguration(printer_configuration.buildplateConfiguration) + return available_configuration From d674494cdba9332fce4068c51e07338936d34ab8 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 23 Aug 2019 10:10:44 +0200 Subject: [PATCH 45/63] Don't look at available configurations when no slots are available --- plugins/ThingiBrowser | 1 - .../UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 120000 plugins/ThingiBrowser diff --git a/plugins/ThingiBrowser b/plugins/ThingiBrowser deleted file mode 120000 index 8126b65fd5..0000000000 --- a/plugins/ThingiBrowser +++ /dev/null @@ -1 +0,0 @@ -/Users/chris/Code/ChrisTerBeke/ThingiBrowser \ No newline at end of file diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index 6e971e2bd3..323ecf5c75 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -80,7 +80,7 @@ class ClusterPrinterStatus(BaseModel): model.setCameraUrl(QUrl("http://{}:8080/?action=stream".format(self.ip_address))) # Set the possible configurations based on whether a Material Station is present or not. - if self.material_station is not None: + if self.material_station is not None and len(self.material_station.material_slots): self._updateAvailableConfigurations(model) if self.configuration is not None: self._updateActiveConfiguration(model) From c491d7f3e2864a6b552082db5195cc462a8a4493 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 23 Aug 2019 10:40:17 +0200 Subject: [PATCH 46/63] Some reformatting --- .../src/Models/Http/ClusterPrinterStatus.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py index 323ecf5c75..841cfd9fa1 100644 --- a/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py +++ b/plugins/UM3NetworkPrinting/src/Models/Http/ClusterPrinterStatus.py @@ -92,26 +92,22 @@ class ClusterPrinterStatus(BaseModel): configuration.updateConfigurationModel(extruder_config) def _updateAvailableConfigurations(self, model: PrinterOutputModel) -> None: - # Generate a list of configurations for the left extruder. left_configurations = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration( slot = slot, extruder_index = 0 )] - # Generate a list of configurations for the right extruder. right_configurations = [slot for slot in self.material_station.material_slots if self._isSupportedConfiguration( slot = slot, extruder_index = 1 )] - # Create a list of all available combinations between both print cores. available_configurations = [self._createAvailableConfigurationFromPrinterConfiguration( left_slot = left_slot, right_slot = right_slot, printer_configuration = model.printerConfiguration ) for left_slot, right_slot in product(left_configurations, right_configurations)] - # Let Cura know which available configurations there are. model.setAvailableConfigurations(available_configurations) From 36f6dba2fcbb6f6c36304025233329a42258f5f4 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 23 Aug 2019 16:23:51 +0200 Subject: [PATCH 47/63] Fix not displaying configuration with both extruders empty --- cura/PrinterOutput/Models/PrinterConfigurationModel.py | 8 ++++++++ cura/PrinterOutput/PrinterOutputDevice.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/cura/PrinterOutput/Models/PrinterConfigurationModel.py b/cura/PrinterOutput/Models/PrinterConfigurationModel.py index 47b9532080..52c7b6f960 100644 --- a/cura/PrinterOutput/Models/PrinterConfigurationModel.py +++ b/cura/PrinterOutput/Models/PrinterConfigurationModel.py @@ -58,6 +58,14 @@ class PrinterConfigurationModel(QObject): return False return self._printer_type != "" + def hasAnyMaterialLoaded(self) -> bool: + if not self.isValid(): + return False + for configuration in self._extruder_configurations: + if configuration.activeMaterial and configuration.activeMaterial.type != "empty": + return True + return False + def __str__(self): message_chunks = [] message_chunks.append("Printer type: " + self._printer_type) diff --git a/cura/PrinterOutput/PrinterOutputDevice.py b/cura/PrinterOutput/PrinterOutputDevice.py index bb4f9e79fb..31daacbccc 100644 --- a/cura/PrinterOutput/PrinterOutputDevice.py +++ b/cura/PrinterOutput/PrinterOutputDevice.py @@ -222,7 +222,7 @@ class PrinterOutputDevice(QObject, OutputDevice): def _updateUniqueConfigurations(self) -> None: all_configurations = set() for printer in self._printers: - if printer.printerConfiguration is not None: + if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded(): all_configurations.add(printer.printerConfiguration) all_configurations.update(printer.availableConfigurations) new_configurations = sorted(all_configurations, key = lambda config: config.printerType) From 73b423138adc1b9f6c3763498c37ba700f1476a6 Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 23 Aug 2019 16:35:15 +0200 Subject: [PATCH 48/63] Add a test to ensure empty configurations are not shown in the list --- .../PrinterOutput/TestPrinterOutputDevice.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/PrinterOutput/TestPrinterOutputDevice.py b/tests/PrinterOutput/TestPrinterOutputDevice.py index e0415295c1..d690297009 100644 --- a/tests/PrinterOutput/TestPrinterOutputDevice.py +++ b/tests/PrinterOutput/TestPrinterOutputDevice.py @@ -3,6 +3,8 @@ from unittest.mock import MagicMock import pytest from unittest.mock import patch +from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel +from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice @@ -61,4 +63,19 @@ def test_uniqueConfigurations(printer_output_device): # Once the type of printer is set, it's active configuration counts as being set. # In that case, that should also be added to the list of available configurations printer.updateType("blarg!") - assert printer_output_device.uniqueConfigurations == [configuration, printer.printerConfiguration] \ No newline at end of file + assert printer_output_device.uniqueConfigurations == [configuration, printer.printerConfiguration] + + +def test_uniqueConfigurations_empty_is_filtered_out(printer_output_device): + printer = PrinterOutputModel(MagicMock()) + # Add a printer and fire the signal that ensures they get hooked up correctly. + printer_output_device._printers = [printer] + printer_output_device._onPrintersChanged() + + empty_material = MaterialOutputModel(guid = "", type = "empty", color = "empty", brand = "Generic", name = "Empty") + empty_left_extruder = ExtruderConfigurationModel(0) + empty_left_extruder.setMaterial(empty_material) + empty_right_extruder = ExtruderConfigurationModel(1) + empty_right_extruder.setMaterial(empty_material) + printer.printerConfiguration.setExtruderConfigurations([empty_left_extruder, empty_right_extruder]) + assert printer_output_device.uniqueConfiguration == [] From 882352c99dbe91cad989c9d01de004722e762c8d Mon Sep 17 00:00:00 2001 From: ChrisTerBeke Date: Fri, 23 Aug 2019 17:03:39 +0200 Subject: [PATCH 49/63] Test fixes, not working yet --- cura/PrinterOutput/Models/ExtruderConfigurationModel.py | 4 ++-- tests/PrinterOutput/TestPrinterOutputDevice.py | 9 ++++++++- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py index 5b4cb5d6f5..04a3c95afd 100644 --- a/cura/PrinterOutput/Models/ExtruderConfigurationModel.py +++ b/cura/PrinterOutput/Models/ExtruderConfigurationModel.py @@ -25,7 +25,7 @@ class ExtruderConfigurationModel(QObject): return self._position def setMaterial(self, material: Optional[MaterialOutputModel]) -> None: - if self._hotend_id != material: + if self._material != material: self._material = material self.extruderConfigurationChanged.emit() @@ -33,7 +33,7 @@ class ExtruderConfigurationModel(QObject): def activeMaterial(self) -> Optional[MaterialOutputModel]: return self._material - @pyqtProperty(QObject, fset=setMaterial, notify=extruderConfigurationChanged) + @pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged) def material(self) -> Optional[MaterialOutputModel]: return self._material diff --git a/tests/PrinterOutput/TestPrinterOutputDevice.py b/tests/PrinterOutput/TestPrinterOutputDevice.py index d690297009..7a9e4e2cc5 100644 --- a/tests/PrinterOutput/TestPrinterOutputDevice.py +++ b/tests/PrinterOutput/TestPrinterOutputDevice.py @@ -63,6 +63,12 @@ def test_uniqueConfigurations(printer_output_device): # Once the type of printer is set, it's active configuration counts as being set. # In that case, that should also be added to the list of available configurations printer.updateType("blarg!") + loaded_material = MaterialOutputModel(guid = "", type = "PLA", color = "Blue", brand = "Generic", name = "Blue PLA") + loaded_left_extruder = ExtruderConfigurationModel(0) + loaded_left_extruder.setMaterial(loaded_material) + loaded_right_extruder = ExtruderConfigurationModel(1) + loaded_right_extruder.setMaterial(loaded_material) + printer.printerConfiguration.setExtruderConfigurations([loaded_left_extruder, loaded_right_extruder]) assert printer_output_device.uniqueConfigurations == [configuration, printer.printerConfiguration] @@ -72,10 +78,11 @@ def test_uniqueConfigurations_empty_is_filtered_out(printer_output_device): printer_output_device._printers = [printer] printer_output_device._onPrintersChanged() + printer.updateType("blarg!") empty_material = MaterialOutputModel(guid = "", type = "empty", color = "empty", brand = "Generic", name = "Empty") empty_left_extruder = ExtruderConfigurationModel(0) empty_left_extruder.setMaterial(empty_material) empty_right_extruder = ExtruderConfigurationModel(1) empty_right_extruder.setMaterial(empty_material) printer.printerConfiguration.setExtruderConfigurations([empty_left_extruder, empty_right_extruder]) - assert printer_output_device.uniqueConfiguration == [] + assert printer_output_device.uniqueConfigurations == [] From 63f94830377b485d1a973ca5484877602cf8f205 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Fri, 23 Aug 2019 17:15:08 +0200 Subject: [PATCH 50/63] Connect the config changed of the configuration to that of the output model --- cura/PrinterOutput/Models/PrinterOutputModel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cura/PrinterOutput/Models/PrinterOutputModel.py b/cura/PrinterOutput/Models/PrinterOutputModel.py index b8bea999c7..a1a23201fb 100644 --- a/cura/PrinterOutput/Models/PrinterOutputModel.py +++ b/cura/PrinterOutput/Models/PrinterOutputModel.py @@ -50,7 +50,7 @@ class PrinterOutputModel(QObject): self._active_printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._extruders] - + self._active_printer_configuration.configurationChanged.connect(self.configurationChanged) self._available_printer_configurations = [] # type: List[PrinterConfigurationModel] self._camera_url = QUrl() # type: QUrl From 23f4aa6e4f6d02b0342fb727cbed2991f3ea16d1 Mon Sep 17 00:00:00 2001 From: Ghostkeeper Date: Mon, 26 Aug 2019 09:15:37 +0200 Subject: [PATCH 51/63] Fix potential race condition when slice messages arrive after clearing build plate Fixes #6245. --- plugins/CuraEngineBackend/CuraEngineBackend.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py index ae18e76e5a..2aae1fff21 100755 --- a/plugins/CuraEngineBackend/CuraEngineBackend.py +++ b/plugins/CuraEngineBackend/CuraEngineBackend.py @@ -671,14 +671,20 @@ class CuraEngineBackend(QObject, Backend): # # \param message The protobuf message containing g-code, encoded as UTF-8. def _onGCodeLayerMessage(self, message: Arcus.PythonMessage) -> None: - self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically. + try: + self._scene.gcode_dict[self._start_slice_job_build_plate].append(message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically. + except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. + pass # Throw the message away. ## Called when a g-code prefix message is received from the engine. # # \param message The protobuf message containing the g-code prefix, # encoded as UTF-8. def _onGCodePrefixMessage(self, message: Arcus.PythonMessage) -> None: - self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically. + try: + self._scene.gcode_dict[self._start_slice_job_build_plate].insert(0, message.data.decode("utf-8", "replace")) #type: ignore #Because we generate this attribute dynamically. + except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end. + pass # Throw the message away. ## Creates a new socket connection. def _createSocket(self, protocol_file: str = None) -> None: From 15e91bb32a21c4b63e666c8c565e9620040e2608 Mon Sep 17 00:00:00 2001 From: pinchies Date: Mon, 26 Aug 2019 18:34:58 +1000 Subject: [PATCH 52/63] Add JGAurora A3S (#6235) * Add profile for JGAurora JGMaker Magic CURA-6734 --- resources/definitions/jgaurora_a3s.def.json | 93 +++++++++++++++++++ .../jgaurora_a3s_extruder_0.def.json | 16 ++++ 2 files changed, 109 insertions(+) create mode 100644 resources/definitions/jgaurora_a3s.def.json create mode 100644 resources/extruders/jgaurora_a3s_extruder_0.def.json diff --git a/resources/definitions/jgaurora_a3s.def.json b/resources/definitions/jgaurora_a3s.def.json new file mode 100644 index 0000000000..bd8d0bd0e3 --- /dev/null +++ b/resources/definitions/jgaurora_a3s.def.json @@ -0,0 +1,93 @@ +{ + "name": "JGAurora A3S", + "version": 2, + "inherits": "fdmprinter", + "metadata": { + "visible": true, + "author": "Samuel Pinches", + "manufacturer": "JGAurora", + "file_formats": "text/x-gcode", + "preferred_quality_type": "normal", + "machine_extruder_trains": + { + "0": "jgaurora_a3s_extruder_0" + } + }, + "overrides": { + "machine_name": { + "default_value": "JGAurora A3S" + }, + "machine_start_gcode": { + "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --" + }, + "machine_end_gcode": { + "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y200 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --" + }, + "machine_width": { + "default_value": 205 + }, + "machine_height": { + "default_value": 205 + }, + "machine_depth": { + "default_value": 205 + }, + "machine_heated_bed": { + "default_value": true + }, + "machine_center_is_zero": { + "default_value": false + }, + "gantry_height": { + "value": "10" + }, + "machine_gcode_flavor": { + "default_value": "RepRap (Marlin/Sprinter)" + }, + "material_diameter": { + "default_value": 1.75 + }, + "material_print_temperature": { + "default_value": 210 + }, + "material_bed_temperature": { + "default_value": 65 + }, + "layer_height_0": { + "default_value": 0.12 + }, + "wall_thickness": { + "default_value": 1.2 + }, + "speed_print": { + "default_value": 35 + }, + "speed_infill": { + "default_value": 40 + }, + "speed_wall": { + "default_value": 30 + }, + "speed_topbottom": { + "default_value": 20 + }, + "speed_travel": { + "default_value": 100 + }, + "speed_layer_0": { + "default_value": 12 + }, + "support_enable": { + "default_value": true + }, + "retraction_enable": { + "default_value": true + }, + "retraction_amount": { + "default_value": 8 + }, + "retraction_speed": { + "default_value": 45 + } + } +} diff --git a/resources/extruders/jgaurora_a3s_extruder_0.def.json b/resources/extruders/jgaurora_a3s_extruder_0.def.json new file mode 100644 index 0000000000..430867b38b --- /dev/null +++ b/resources/extruders/jgaurora_a3s_extruder_0.def.json @@ -0,0 +1,16 @@ +{ + "id": "jgaurora_a3s_extruder_0", + "version": 2, + "name": "Extruder 1", + "inherits": "fdmextruder", + "metadata": { + "machine": "jgaurora_a3s", + "position": "0" + }, + + "overrides": { + "extruder_nr": { "default_value": 0 }, + "machine_nozzle_size": { "default_value": 0.4 }, + "material_diameter": { "default_value": 1.75 } + } +} From 108b22932bc2f5acdf50de59740b13df84f82195 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Mon, 26 Aug 2019 14:55:30 +0200 Subject: [PATCH 53/63] Override saveDirtyContainers with Cura specific logic --- cura/Settings/CuraContainerRegistry.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index bc0d99ead9..c563568cba 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -396,6 +396,25 @@ class CuraContainerRegistry(ContainerRegistry): return None + @override(ContainerRegistry) + def saveDirtyContainers(self) -> None: + # Lock file for "more" atomically loading and saving to/from config dir. + with self.lockFile(): + # Save base files first + for instance in self.findDirtyContainers(container_type=InstanceContainer): + if instance.getMetaDataEntry("removed"): + continue + if instance.getId() == instance.getMetaData().get("base_file"): + self.saveContainer(instance) + + for instance in self.findDirtyContainers(container_type=InstanceContainer): + if instance.getMetaDataEntry("removed"): + continue + self.saveContainer(instance) + + for stack in self.findContainerStacks(): + self.saveContainer(stack) + ## Gets a list of profile writer plugins # \return List of tuples of (plugin_id, meta_data). def _getIOPlugins(self, io_type): From 946ec1d32e3a3a0e34d384ffbf297bf01e930a5a Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Mon, 26 Aug 2019 15:40:27 +0200 Subject: [PATCH 54/63] Apply missing metadata fields from project files CURA-6388 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index a9142c6d12..c909955e53 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -59,6 +59,9 @@ class MachineInfo: self.container_id = None self.name = None self.definition_id = None + + self.metadata_dict = {} # type: Dict[str, str] + self.quality_type = None self.custom_quality_name = None self.quality_changes_info = None @@ -342,6 +345,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): global_stack_id = self._stripFileToId(global_stack_file) serialized = archive.open(global_stack_file).read().decode("utf-8") machine_name = self._getMachineNameFromSerializedStack(serialized) + self._machine_info.metadata_dict = self._getMetaDataDictFromSerializedStack(serialized) + stacks = self._container_registry.findContainerStacks(name = machine_name, type = "machine") self._is_same_machine_type = True existing_global_stack = None @@ -981,6 +986,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader): extruder_stack.setMetaDataEntry("enabled", "True") extruder_stack.setMetaDataEntry("enabled", str(extruder_info.enabled)) + # Set metadata fields that are missing from the global stack + for key, value in self._machine_info.metadata_dict.items(): + if key not in global_stack.getMetaData(): + global_stack.setMetaDataEntry(key, value) + def _updateActiveMachine(self, global_stack): # Actually change the active machine. machine_manager = Application.getInstance().getMachineManager() @@ -993,6 +1003,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader): machine_manager.setActiveMachine(global_stack.getId()) + # Set metadata fields that are missing from the global stack + for key, value in self._machine_info.metadata_dict.items(): + if key not in global_stack.getMetaData(): + global_stack.setMetaDataEntry(key, value) + if self._quality_changes_to_apply: quality_changes_group_dict = quality_manager.getQualityChangesGroups(global_stack) if self._quality_changes_to_apply not in quality_changes_group_dict: @@ -1054,6 +1069,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader): parser.read_string(serialized) return parser["general"].get("name", "") + def _getMetaDataDictFromSerializedStack(self, serialized: str) -> Dict[str, str]: + parser = ConfigParser(interpolation = None, empty_lines_in_values = False) + parser.read_string(serialized) + return parser["metadata"] + def _getMaterialLabelFromSerialized(self, serialized): data = ET.fromstring(serialized) metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"}) From 69e9dc13130815d7bcd1bb2ec5ae697e57083858 Mon Sep 17 00:00:00 2001 From: Lipu Fei Date: Tue, 27 Aug 2019 09:08:17 +0200 Subject: [PATCH 55/63] Allow importing "not supported" profiles CURA-6542 --- cura/Settings/CuraContainerRegistry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index c563568cba..314adeeb54 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -9,7 +9,6 @@ from typing import Any, cast, Dict, Optional, List, Union from PyQt5.QtWidgets import QMessageBox from UM.Decorators import override -from UM.PluginObject import PluginObject from UM.Settings.ContainerFormatError import ContainerFormatError from UM.Settings.Interfaces import ContainerInterface from UM.Settings.ContainerRegistry import ContainerRegistry @@ -21,7 +20,6 @@ from UM.Logger import Logger from UM.Message import Message from UM.Platform import Platform from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with. -from UM.Util import parseBool from UM.Resources import Resources from cura.ReaderWriters.ProfileWriter import ProfileWriter @@ -29,6 +27,7 @@ from . import ExtruderStack from . import GlobalStack import cura.CuraApplication +from cura.Settings.cura_empty_instance_containers import empty_quality_container from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader @@ -389,7 +388,8 @@ class CuraContainerRegistry(ContainerRegistry): # successfully imported but then fail to show up. quality_manager = cura.CuraApplication.CuraApplication.getInstance()._quality_manager quality_group_dict = quality_manager.getQualityGroupsForMachineDefinition(global_stack) - if quality_type not in quality_group_dict: + # "not_supported" profiles can be imported. + if quality_type != empty_quality_container.getMetaDataEntry("quality_type") and quality_type not in quality_group_dict: return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type) ContainerRegistry.getInstance().addContainer(profile) From 43d1157aa18bb0a24fcb8f396f43b2d105bd38cb Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Aug 2019 11:38:25 +0200 Subject: [PATCH 56/63] Fix typing error CURA-6388 --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index c909955e53..5179fe5685 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -1072,7 +1072,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): def _getMetaDataDictFromSerializedStack(self, serialized: str) -> Dict[str, str]: parser = ConfigParser(interpolation = None, empty_lines_in_values = False) parser.read_string(serialized) - return parser["metadata"] + return dict(parser["metadata"]) def _getMaterialLabelFromSerialized(self, serialized): data = ET.fromstring(serialized) From bf66388939114c916507068e48d4a7befa476b2d Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Aug 2019 11:41:21 +0200 Subject: [PATCH 57/63] Make functions that should have been static, static. --- plugins/3MFReader/ThreeMFWorkspaceReader.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 5179fe5685..de1049403e 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -837,7 +837,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): self._machine_info.quality_changes_info.name = quality_changes_name - def _clearStack(self, stack): + @staticmethod + def _clearStack(stack): application = CuraApplication.getInstance() stack.definitionChanges.clear() @@ -1034,7 +1035,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Notify everything/one that is to notify about changes. global_stack.containersChanged.emit(global_stack.getTop()) - def _stripFileToId(self, file): + @staticmethod + def _stripFileToId(file): mime_type = MimeTypeDatabase.getMimeTypeForFile(file) file = mime_type.stripExtension(file) return file.replace("Cura/", "") @@ -1043,7 +1045,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile")) ## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data. - def _getContainerIdListFromSerialized(self, serialized): + @staticmethod + def _getContainerIdListFromSerialized(serialized): parser = ConfigParser(interpolation = None, empty_lines_in_values = False) parser.read_string(serialized) @@ -1064,17 +1067,20 @@ class ThreeMFWorkspaceReader(WorkspaceReader): return container_ids - def _getMachineNameFromSerializedStack(self, serialized): + @staticmethod + def _getMachineNameFromSerializedStack(serialized): parser = ConfigParser(interpolation = None, empty_lines_in_values = False) parser.read_string(serialized) return parser["general"].get("name", "") - def _getMetaDataDictFromSerializedStack(self, serialized: str) -> Dict[str, str]: + @staticmethod + def _getMetaDataDictFromSerializedStack(serialized: str) -> Dict[str, str]: parser = ConfigParser(interpolation = None, empty_lines_in_values = False) parser.read_string(serialized) return dict(parser["metadata"]) - def _getMaterialLabelFromSerialized(self, serialized): + @staticmethod + def _getMaterialLabelFromSerialized(serialized): data = ET.fromstring(serialized) metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"}) for entry in metadata: From 9c2f8a94d54c63499fa382c78b5a3162a9089810 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Aug 2019 13:03:23 +0200 Subject: [PATCH 58/63] Greatly simplify the SimulationViewProxy --- plugins/SimulationView/SimulationView.py | 4 +- plugins/SimulationView/SimulationViewProxy.py | 180 ++++++------------ 2 files changed, 60 insertions(+), 124 deletions(-) diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 20471f9763..33f713ced0 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -84,7 +84,7 @@ class SimulationView(CuraView): self._old_composite_shader = None # type: Optional["ShaderProgram"] self._global_container_stack = None # type: Optional[ContainerStack] - self._proxy = SimulationViewProxy() + self._proxy = None self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged) self._resetSettings() @@ -441,6 +441,8 @@ class SimulationView(CuraView): ## Hackish way to ensure the proxy is already created, which ensures that the layerview.qml is already created # as this caused some issues. def getProxy(self, engine, script_engine): + if self._proxy is None: + self._proxy = SimulationViewProxy(self) return self._proxy def endRendering(self) -> None: diff --git a/plugins/SimulationView/SimulationViewProxy.py b/plugins/SimulationView/SimulationViewProxy.py index a84b151983..58a004cc31 100644 --- a/plugins/SimulationView/SimulationViewProxy.py +++ b/plugins/SimulationView/SimulationViewProxy.py @@ -1,21 +1,24 @@ # Copyright (c) 2018 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. +from typing import TYPE_CHECKING from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty from UM.FlameProfiler import pyqtSlot from UM.Application import Application -import SimulationView +if TYPE_CHECKING: + from .SimulationView import SimulationView class SimulationViewProxy(QObject): - def __init__(self, parent=None): + def __init__(self, simulation_view: "SimulationView", parent=None): super().__init__(parent) + self._simulation_view = simulation_view self._current_layer = 0 self._controller = Application.getInstance().getController() self._controller.activeViewChanged.connect(self._onActiveViewChanged) - self._onActiveViewChanged() self.is_simulationView_selected = False + self._onActiveViewChanged() currentLayerChanged = pyqtSignal() currentPathChanged = pyqtSignal() @@ -28,182 +31,112 @@ class SimulationViewProxy(QObject): @pyqtProperty(bool, notify=activityChanged) def layerActivity(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.getActivity() - return False + return self._simulation_view.getActivity() @pyqtProperty(int, notify=maxLayersChanged) def numLayers(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.getMaxLayers() - return 0 + return self._simulation_view.getMaxLayers() @pyqtProperty(int, notify=currentLayerChanged) def currentLayer(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.getCurrentLayer() - return 0 + return self._simulation_view.getCurrentLayer() @pyqtProperty(int, notify=currentLayerChanged) def minimumLayer(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.getMinimumLayer() - return 0 + return self._simulation_view.getMinimumLayer() @pyqtProperty(int, notify=maxPathsChanged) def numPaths(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.getMaxPaths() - return 0 + return self._simulation_view.getMaxPaths() @pyqtProperty(int, notify=currentPathChanged) def currentPath(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.getCurrentPath() - return 0 + return self._simulation_view.getCurrentPath() @pyqtProperty(int, notify=currentPathChanged) def minimumPath(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.getMinimumPath() - return 0 + return self._simulation_view.getMinimumPath() @pyqtProperty(bool, notify=busyChanged) def busy(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.isBusy() - return False + return self._simulation_view.isBusy() @pyqtProperty(bool, notify=preferencesChanged) def compatibilityMode(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.getCompatibilityMode() - return False + return self._simulation_view.getCompatibilityMode() + + @pyqtProperty(int, notify=globalStackChanged) + def extruderCount(self): + return self._simulation_view.getExtruderCount() @pyqtSlot(int) def setCurrentLayer(self, layer_num): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - active_view.setLayer(layer_num) + self._simulation_view.setLayer(layer_num) @pyqtSlot(int) def setMinimumLayer(self, layer_num): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - active_view.setMinimumLayer(layer_num) + self._simulation_view.setMinimumLayer(layer_num) @pyqtSlot(int) def setCurrentPath(self, path_num): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - active_view.setPath(path_num) + self._simulation_view.setPath(path_num) @pyqtSlot(int) def setMinimumPath(self, path_num): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - active_view.setMinimumPath(path_num) + self._simulation_view.setMinimumPath(path_num) @pyqtSlot(int) def setSimulationViewType(self, layer_view_type): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - active_view.setSimulationViewType(layer_view_type) + self._simulation_view.setSimulationViewType(layer_view_type) @pyqtSlot(result=int) def getSimulationViewType(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.getSimulationViewType() - return 0 + return self._simulation_view.getSimulationViewType() @pyqtSlot(bool) def setSimulationRunning(self, running): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - active_view.setSimulationRunning(running) + self._simulation_view.setSimulationRunning(running) @pyqtSlot(result=bool) def getSimulationRunning(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.isSimulationRunning() - return False + return self._simulation_view.isSimulationRunning() @pyqtSlot(result=float) def getMinFeedrate(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.getMinFeedrate() - return 0 + return self._simulation_view.getMinFeedrate() @pyqtSlot(result=float) def getMaxFeedrate(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.getMaxFeedrate() - return 0 + return self._simulation_view.getMaxFeedrate() @pyqtSlot(result=float) def getMinThickness(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.getMinThickness() - return 0 + return self._simulation_view.getMinThickness() @pyqtSlot(result=float) def getMaxThickness(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.getMaxThickness() - return 0 + return self._simulation_view.getMaxThickness() # Opacity 0..1 @pyqtSlot(int, float) def setExtruderOpacity(self, extruder_nr, opacity): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - active_view.setExtruderOpacity(extruder_nr, opacity) + self._simulation_view.setExtruderOpacity(extruder_nr, opacity) @pyqtSlot(int) def setShowTravelMoves(self, show): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - active_view.setShowTravelMoves(show) + self._simulation_view.setShowTravelMoves(show) @pyqtSlot(int) def setShowHelpers(self, show): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - active_view.setShowHelpers(show) + self._simulation_view.setShowHelpers(show) @pyqtSlot(int) def setShowSkin(self, show): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - active_view.setShowSkin(show) + self._simulation_view.setShowSkin(show) @pyqtSlot(int) def setShowInfill(self, show): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - active_view.setShowInfill(show) - - @pyqtProperty(int, notify=globalStackChanged) - def extruderCount(self): - active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - return active_view.getExtruderCount() - return 0 + self._simulation_view.setShowInfill(show) def _layerActivityChanged(self): self.activityChanged.emit() @@ -236,24 +169,25 @@ class SimulationViewProxy(QObject): def _onActiveViewChanged(self): active_view = self._controller.getActiveView() - if isinstance(active_view, SimulationView.SimulationView.SimulationView): - # remove other connection if once the SimulationView was created. - if self.is_simulationView_selected: - active_view.currentLayerNumChanged.disconnect(self._onLayerChanged) - active_view.currentPathNumChanged.disconnect(self._onPathChanged) - active_view.maxLayersChanged.disconnect(self._onMaxLayersChanged) - active_view.maxPathsChanged.disconnect(self._onMaxPathsChanged) - active_view.busyChanged.disconnect(self._onBusyChanged) - active_view.activityChanged.disconnect(self._onActivityChanged) - active_view.globalStackChanged.disconnect(self._onGlobalStackChanged) - active_view.preferencesChanged.disconnect(self._onPreferencesChanged) - + if active_view == self._simulation_view: + self._simulation_view.currentLayerNumChanged.connect(self._onLayerChanged) + self._simulation_view.currentPathNumChanged.connect(self._onPathChanged) + self._simulation_view.maxLayersChanged.connect(self._onMaxLayersChanged) + self._simulation_view.maxPathsChanged.connect(self._onMaxPathsChanged) + self._simulation_view.busyChanged.connect(self._onBusyChanged) + self._simulation_view.activityChanged.connect(self._onActivityChanged) + self._simulation_view.globalStackChanged.connect(self._onGlobalStackChanged) + self._simulation_view.preferencesChanged.connect(self._onPreferencesChanged) self.is_simulationView_selected = True - active_view.currentLayerNumChanged.connect(self._onLayerChanged) - active_view.currentPathNumChanged.connect(self._onPathChanged) - active_view.maxLayersChanged.connect(self._onMaxLayersChanged) - active_view.maxPathsChanged.connect(self._onMaxPathsChanged) - active_view.busyChanged.connect(self._onBusyChanged) - active_view.activityChanged.connect(self._onActivityChanged) - active_view.globalStackChanged.connect(self._onGlobalStackChanged) - active_view.preferencesChanged.connect(self._onPreferencesChanged) + elif self.is_simulationView_selected: + # Disconnect all of em again. + self.is_simulationView_selected = False + self._simulation_view.currentLayerNumChanged.disconnect(self._onLayerChanged) + self._simulation_view.currentPathNumChanged.disconnect(self._onPathChanged) + self._simulation_view.maxLayersChanged.disconnect(self._onMaxLayersChanged) + self._simulation_view.maxPathsChanged.disconnect(self._onMaxPathsChanged) + self._simulation_view.busyChanged.disconnect(self._onBusyChanged) + self._simulation_view.activityChanged.disconnect(self._onActivityChanged) + self._simulation_view.globalStackChanged.disconnect(self._onGlobalStackChanged) + self._simulation_view.preferencesChanged.disconnect(self._onPreferencesChanged) + From 511eba28b69e60f9085848bd5396001661d76aa9 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Aug 2019 13:06:33 +0200 Subject: [PATCH 59/63] Ensure that min/max feedrate & thickness gets defined in init --- plugins/SimulationView/SimulationView.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index 33f713ced0..edc4e7dcf0 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -83,6 +83,11 @@ class SimulationView(CuraView): self._simulationview_composite_shader = None # type: Optional["ShaderProgram"] self._old_composite_shader = None # type: Optional["ShaderProgram"] + self._max_feedrate = sys.float_info.min + self._min_feedrate = sys.float_info.max + self._max_thickness = sys.float_info.min + self._min_thickness = sys.float_info.max + self._global_container_stack = None # type: Optional[ContainerStack] self._proxy = None self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged) From ea11187eaf73dc2a2f8f06348394eb6901011f63 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Aug 2019 13:16:29 +0200 Subject: [PATCH 60/63] Don't reset the data when the root updates Resolves #6258 --- plugins/SimulationView/SimulationView.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index edc4e7dcf0..e7bb88e1ae 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -184,9 +184,6 @@ class SimulationView(CuraView): return self._nozzle_node def _onSceneChanged(self, node: "SceneNode") -> None: - if node.getMeshData() is None: - self.resetLayerData() - self.setActivity(False) self.calculateMaxLayers() self.calculateMaxPathsOnLayer(self._current_layer_num) From e8cd5723c97d956f2629f13ef0df8fd3370e8685 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Aug 2019 13:26:55 +0200 Subject: [PATCH 61/63] Speedup the layerview We were doing a lot of re-calculations that served no purpose (and even slowed down the rest of the application) --- plugins/SimulationView/SimulationView.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py index e7bb88e1ae..72bf1274ea 100644 --- a/plugins/SimulationView/SimulationView.py +++ b/plugins/SimulationView/SimulationView.py @@ -90,7 +90,6 @@ class SimulationView(CuraView): self._global_container_stack = None # type: Optional[ContainerStack] self._proxy = None - self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged) self._resetSettings() self._legend_items = None @@ -109,7 +108,6 @@ class SimulationView(CuraView): Application.getInstance().getPreferences().addPreference("layerview/show_skin", True) Application.getInstance().getPreferences().addPreference("layerview/show_infill", True) - Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged) self._updateWithPreferences() self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count")) @@ -184,6 +182,8 @@ class SimulationView(CuraView): return self._nozzle_node def _onSceneChanged(self, node: "SceneNode") -> None: + if node.getMeshData() is None: + return self.setActivity(False) self.calculateMaxLayers() self.calculateMaxPathsOnLayer(self._current_layer_num) @@ -464,6 +464,10 @@ class SimulationView(CuraView): return True if event.type == Event.ViewActivateEvent: + # Start listening to changes. + Application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged) + self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged) + # FIX: on Max OS X, somehow QOpenGLContext.currentContext() can become None during View switching. # This can happen when you do the following steps: # 1. Start Cura @@ -510,6 +514,8 @@ class SimulationView(CuraView): self._composite_pass.setCompositeShader(self._simulationview_composite_shader) elif event.type == Event.ViewDeactivateEvent: + self._controller.getScene().getRoot().childrenChanged.disconnect(self._onSceneChanged) + Application.getInstance().getPreferences().preferenceChanged.disconnect(self._onPreferencesChanged) self._wireprint_warning_message.hide() Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged) if self._global_container_stack: From 867a881de91deb017fb05518113ed975c0246397 Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Aug 2019 13:38:55 +0200 Subject: [PATCH 62/63] Ensure bool and enum settings get control highlighted on hover --- resources/qml/Settings/SettingCheckBox.qml | 2 +- resources/qml/Settings/SettingComboBox.qml | 2 +- resources/qml/Widgets/ComboBox.qml | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/qml/Settings/SettingCheckBox.qml b/resources/qml/Settings/SettingCheckBox.qml index 694c4125ea..f5100eab74 100644 --- a/resources/qml/Settings/SettingCheckBox.qml +++ b/resources/qml/Settings/SettingCheckBox.qml @@ -112,7 +112,7 @@ SettingItem return UM.Theme.getColor("setting_validation_warning"); } // Validation is OK. - if (control.containsMouse || control.activeFocus) + if (control.containsMouse || control.activeFocus || hovered) { return UM.Theme.getColor("setting_control_border_highlight") } diff --git a/resources/qml/Settings/SettingComboBox.qml b/resources/qml/Settings/SettingComboBox.qml index 6fcc1951a4..0b7f494a7d 100644 --- a/resources/qml/Settings/SettingComboBox.qml +++ b/resources/qml/Settings/SettingComboBox.qml @@ -12,7 +12,6 @@ SettingItem { id: base property var focusItem: control - contents: Cura.ComboBox { id: control @@ -21,6 +20,7 @@ SettingItem textRole: "value" anchors.fill: parent + highlighted: base.hovered onActivated: { diff --git a/resources/qml/Widgets/ComboBox.qml b/resources/qml/Widgets/ComboBox.qml index 6ce7c6da45..d1edcca69c 100644 --- a/resources/qml/Widgets/ComboBox.qml +++ b/resources/qml/Widgets/ComboBox.qml @@ -14,7 +14,7 @@ import Cura 1.1 as Cura ComboBox { id: control - + property bool highlighted: False background: Rectangle { color: @@ -24,7 +24,7 @@ ComboBox return UM.Theme.getColor("setting_control_disabled") } - if (control.hovered || control.activeFocus) + if (control.hovered || control.activeFocus || control.highlighted) { return UM.Theme.getColor("setting_control_highlight") } @@ -41,7 +41,7 @@ ComboBox return UM.Theme.getColor("setting_control_disabled_border") } - if (control.hovered || control.activeFocus) + if (control.hovered || control.activeFocus || control.highlighted) { return UM.Theme.getColor("setting_control_border_highlight") } From 8c98773f5582d05e9d61c33c3395bba58ff62b6c Mon Sep 17 00:00:00 2001 From: Jaime van Kessel Date: Tue, 27 Aug 2019 14:18:07 +0200 Subject: [PATCH 63/63] Fix issues with sorting if no printer type is set --- cura/PrinterOutput/PrinterOutputDevice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cura/PrinterOutput/PrinterOutputDevice.py b/cura/PrinterOutput/PrinterOutputDevice.py index 31daacbccc..980ee7864d 100644 --- a/cura/PrinterOutput/PrinterOutputDevice.py +++ b/cura/PrinterOutput/PrinterOutputDevice.py @@ -225,7 +225,7 @@ class PrinterOutputDevice(QObject, OutputDevice): if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded(): all_configurations.add(printer.printerConfiguration) all_configurations.update(printer.availableConfigurations) - new_configurations = sorted(all_configurations, key = lambda config: config.printerType) + new_configurations = sorted(all_configurations, key = lambda config: config.printerType or "") if new_configurations != self._unique_configurations: self._unique_configurations = new_configurations self.uniqueConfigurationsChanged.emit() @@ -233,7 +233,7 @@ class PrinterOutputDevice(QObject, OutputDevice): # Returns the unique configurations of the printers within this output device @pyqtProperty("QStringList", notify = uniqueConfigurationsChanged) def uniquePrinterTypes(self) -> List[str]: - return list(sorted(set([configuration.printerType for configuration in self._unique_configurations]))) + return list(sorted(set([configuration.printerType or "" for configuration in self._unique_configurations]))) def _onPrintersChanged(self) -> None: for printer in self._printers: