diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index ecf11a7806..3d3ba531f6 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -51,6 +51,7 @@ from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob from cura.Arranging.ShapeArray import ShapeArray from cura.MultiplyObjectsJob import MultiplyObjectsJob +from cura.PrintersModel import PrintersModel from cura.Scene.ConvexHullDecorator import ConvexHullDecorator from cura.Operations.SetParentOperation import SetParentOperation from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator @@ -113,6 +114,7 @@ from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions from cura.ObjectsModel import ObjectsModel +from cura.PrinterOutputDevice import PrinterOutputDevice from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage from cura import CuraConstants @@ -972,6 +974,7 @@ class CuraApplication(QtApplication): qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel") qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer") qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel") + qmlRegisterType(PrintersModel, "Cura", 1, 0, "PrintersModel") qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel") qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel") @@ -993,6 +996,8 @@ class CuraApplication(QtApplication): qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance) qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel") + qmlRegisterType(PrinterOutputDevice, "Cura", 1, 0, "PrinterOutputDevice") + from cura.API import CuraAPI qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI) diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py index b0c8b54a67..d0a0b5076a 100644 --- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py +++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py @@ -7,7 +7,7 @@ from UM.Scene.SceneNode import SceneNode #For typing. from cura.API import Account from cura.CuraApplication import CuraApplication -from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState +from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication @@ -29,8 +29,8 @@ class AuthState(IntEnum): class NetworkedPrinterOutputDevice(PrinterOutputDevice): authenticationStateChanged = pyqtSignal() - def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], parent: QObject = None) -> None: - super().__init__(device_id = device_id, parent = parent) + def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType = ConnectionType.NetworkConnection, parent: QObject = None) -> None: + super().__init__(device_id = device_id, connection_type = connection_type, parent = parent) self._manager = None # type: Optional[QNetworkAccessManager] self._last_manager_create_time = None # type: Optional[float] self._recreate_network_manager_time = 30 @@ -128,7 +128,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if self._connection_state_before_timeout is None: self._connection_state_before_timeout = self._connection_state - self.setConnectionState(ConnectionState.closed) + self.setConnectionState(ConnectionState.Closed) # We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to # sleep. @@ -136,7 +136,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): if self._last_manager_create_time is None or time() - self._last_manager_create_time > self._recreate_network_manager_time: self._createNetworkManager() assert(self._manager is not None) - elif self._connection_state == ConnectionState.closed: + elif self._connection_state == ConnectionState.Closed: # Go out of timeout. if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here self.setConnectionState(self._connection_state_before_timeout) @@ -327,8 +327,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice): self._last_response_time = time() - if self._connection_state == ConnectionState.connecting: - self.setConnectionState(ConnectionState.connected) + if self._connection_state == ConnectionState.Connecting: + self.setConnectionState(ConnectionState.Connected) callback_key = reply.url().toString() + str(reply.operation()) try: diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py index 8c00ea1aa6..d16242d9c2 100644 --- a/cura/PrinterOutputDevice.py +++ b/cura/PrinterOutputDevice.py @@ -4,7 +4,7 @@ from UM.Decorators import deprecated from UM.i18n import i18nCatalog from UM.OutputDevice.OutputDevice import OutputDevice -from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl +from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl, Q_ENUMS from PyQt5.QtWidgets import QMessageBox from UM.Logger import Logger @@ -28,11 +28,18 @@ i18n_catalog = i18nCatalog("cura") ## The current processing state of the backend. class ConnectionState(IntEnum): - closed = 0 - connecting = 1 - connected = 2 - busy = 3 - error = 4 + Closed = 0 + Connecting = 1 + Connected = 2 + Busy = 3 + Error = 4 + + +class ConnectionType(IntEnum): + Unknown = 0 + UsbConnection = 1 + NetworkConnection = 2 + CloudConnection = 3 ## Printer output device adds extra interface options on top of output device. @@ -46,6 +53,11 @@ class ConnectionState(IntEnum): # For all other uses it should be used in the same way as a "regular" OutputDevice. @signalemitter class PrinterOutputDevice(QObject, OutputDevice): + + # Put ConnectionType here with Q_ENUMS() so it can be registered as a QML type and accessible via QML, and there is + # no need to remember what those Enum integer values mean. + Q_ENUMS(ConnectionType) + printersChanged = pyqtSignal() connectionStateChanged = pyqtSignal(str) acceptsCommandsChanged = pyqtSignal() @@ -62,7 +74,7 @@ class PrinterOutputDevice(QObject, OutputDevice): # Signal to indicate that the configuration of one of the printers has changed. uniqueConfigurationsChanged = pyqtSignal() - def __init__(self, device_id: str, parent: QObject = None) -> None: + def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.Unknown, parent: QObject = None) -> None: super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance self._printers = [] # type: List[PrinterOutputModel] @@ -83,7 +95,8 @@ class PrinterOutputDevice(QObject, OutputDevice): self._update_timer.setSingleShot(False) self._update_timer.timeout.connect(self._update) - self._connection_state = ConnectionState.closed #type: ConnectionState + self._connection_state = ConnectionState.Closed #type: ConnectionState + self._connection_type = connection_type self._firmware_updater = None #type: Optional[FirmwareUpdater] self._firmware_name = None #type: Optional[str] @@ -110,15 +123,18 @@ class PrinterOutputDevice(QObject, OutputDevice): callback(QMessageBox.Yes) def isConnected(self) -> bool: - return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error + return self._connection_state != ConnectionState.Closed and self._connection_state != ConnectionState.Error - def setConnectionState(self, connection_state: ConnectionState) -> None: + def setConnectionState(self, connection_state: "ConnectionState") -> None: if self._connection_state != connection_state: self._connection_state = connection_state self.connectionStateChanged.emit(self._id) + def getConnectionType(self) -> "ConnectionType": + return self._connection_type + @pyqtProperty(str, notify = connectionStateChanged) - def connectionState(self) -> ConnectionState: + def connectionState(self) -> "ConnectionState": return self._connection_state def _update(self) -> None: @@ -175,13 +191,13 @@ class PrinterOutputDevice(QObject, OutputDevice): ## Attempt to establish connection def connect(self) -> None: - self.setConnectionState(ConnectionState.connecting) + self.setConnectionState(ConnectionState.Connecting) self._update_timer.start() ## Attempt to close the connection def close(self) -> None: self._update_timer.stop() - self.setConnectionState(ConnectionState.closed) + self.setConnectionState(ConnectionState.Closed) ## Ensure that close gets called when object is destroyed def __del__(self) -> None: diff --git a/cura/PrintersModel.py b/cura/PrintersModel.py new file mode 100644 index 0000000000..8b5d2f6cc9 --- /dev/null +++ b/cura/PrintersModel.py @@ -0,0 +1,69 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from UM.Qt.ListModel import ListModel + +from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal + +from UM.Settings.ContainerRegistry import ContainerRegistry +from UM.Settings.ContainerStack import ContainerStack + +from cura.PrinterOutputDevice import ConnectionType + +from cura.Settings.GlobalStack import GlobalStack + +class PrintersModel(ListModel): + NameRole = Qt.UserRole + 1 + IdRole = Qt.UserRole + 2 + HasRemoteConnectionRole = Qt.UserRole + 3 + ConnectionTypeRole = Qt.UserRole + 4 + MetaDataRole = Qt.UserRole + 5 + + def __init__(self, parent = None): + super().__init__(parent) + self.addRoleName(self.NameRole, "name") + self.addRoleName(self.IdRole, "id") + self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection") + self.addRoleName(self.ConnectionTypeRole, "connectionType") + self.addRoleName(self.MetaDataRole, "metadata") + self._container_stacks = [] + + # Listen to changes + ContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged) + ContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged) + ContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged) + self._filter_dict = {} + self._update() + + ## Handler for container added/removed events from registry + def _onContainerChanged(self, container): + # We only need to update when the added / removed container GlobalStack + if isinstance(container, GlobalStack): + self._update() + + ## Handler for container name change events. + def _onContainerNameChanged(self): + self._update() + + def _update(self) -> None: + items = [] + for container in self._container_stacks: + container.nameChanged.disconnect(self._onContainerNameChanged) + + container_stacks = ContainerRegistry.getInstance().findContainerStacks(type = "machine") + + for container_stack in container_stacks: + connection_type = container_stack.getMetaDataEntry("connection_type") + has_remote_connection = connection_type in [ConnectionType.NetworkConnection.value, ConnectionType.CloudConnection.value] + + if container_stack.getMetaDataEntry("hidden", False) in ["True", True]: + continue + + # TODO: Remove reference to connect group name. + items.append({"name": container_stack.getMetaDataEntry("connect_group_name", container_stack.getName()), + "id": container_stack.getId(), + "hasRemoteConnection": has_remote_connection, + "connectionType": connection_type, + "metadata": container_stack.getMetaData().copy()}) + items.sort(key=lambda i: not i["hasRemoteConnection"]) + self.setItems(items) \ No newline at end of file diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py index e26b82e487..cd8ca09447 100755 --- a/cura/Settings/MachineManager.py +++ b/cura/Settings/MachineManager.py @@ -23,7 +23,7 @@ from UM.Settings.SettingFunction import SettingFunction from UM.Signal import postponeSignals, CompressTechnique from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch -from cura.PrinterOutputDevice import PrinterOutputDevice +from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionType from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel @@ -523,7 +523,13 @@ class MachineManager(QObject): def printerConnected(self): return bool(self._printer_output_devices) - @pyqtProperty(str, notify = printerConnectedStatusChanged) + @pyqtProperty(bool, notify = printerConnectedStatusChanged) + def activeMachineHasRemoteConnection(self) -> bool: + if self._global_container_stack: + connection_type = self._global_container_stack.getMetaDataEntry("connection_type") + return connection_type in [ConnectionType.NetworkConnection.value, ConnectionType.CloudConnection.value] + return False + def activeMachineNetworkKey(self) -> str: if self._global_container_stack: return self._global_container_stack.getMetaDataEntry("um_network_key", "") @@ -751,7 +757,7 @@ class MachineManager(QObject): self.setActiveMachine(other_machine_stacks[0]["id"]) metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0] - network_key = metadata["um_network_key"] if "um_network_key" in metadata else None + network_key = metadata.get("um_network_key", None) ExtruderManager.getInstance().removeMachineExtruders(machine_id) containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id) for container in containers: @@ -1317,17 +1323,18 @@ class MachineManager(QObject): # Get the definition id corresponding to this machine name machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId() # Try to find a machine with the same network key - new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey}) + new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey()}) # If there is no machine, then create a new one and set it to the non-hidden instance if not new_machine: new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id) if not new_machine: return - new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey) + new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey()) new_machine.setMetaDataEntry("connect_group_name", self.activeMachineNetworkGroupName) new_machine.setMetaDataEntry("hidden", False) + new_machine.setMetaDataEntry("connection_type", self._global_container_stack.getMetaDataEntry("connection_type")) else: - Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey) + Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey()) new_machine.setMetaDataEntry("hidden", False) # Set the current printer instance to hidden (the metadata entry must exist) @@ -1387,10 +1394,10 @@ class MachineManager(QObject): # After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group # then all the container stacks are updated, both the current and the hidden ones. def checkCorrectGroupName(self, device_id: str, group_name: str) -> None: - if self._global_container_stack and device_id == self.activeMachineNetworkKey: + if self._global_container_stack and device_id == self.activeMachineNetworkKey(): # Check if the connect_group_name is correct. If not, update all the containers connected to the same printer if self.activeMachineNetworkGroupName != group_name: - metadata_filter = {"um_network_key": self.activeMachineNetworkKey} + metadata_filter = {"um_network_key": self.activeMachineNetworkKey()} containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) for container in containers: container.setMetaDataEntry("connect_group_name", group_name) @@ -1529,6 +1536,10 @@ class MachineManager(QObject): def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]: return self._current_quality_changes_group + @pyqtProperty(bool, notify = activeQualityChangesGroupChanged) + def hasCustomQuality(self) -> bool: + return self._current_quality_changes_group is not None + @pyqtProperty(str, notify = activeQualityGroupChanged) def activeQualityOrQualityChangesName(self) -> str: name = empty_quality_container.getName() diff --git a/cura/Settings/SimpleModeSettingsManager.py b/cura/Settings/SimpleModeSettingsManager.py index b22aea15ea..b1896a9205 100644 --- a/cura/Settings/SimpleModeSettingsManager.py +++ b/cura/Settings/SimpleModeSettingsManager.py @@ -17,15 +17,11 @@ class SimpleModeSettingsManager(QObject): self._is_profile_user_created = False # True when profile was custom created by user self._machine_manager.activeStackValueChanged.connect(self._updateIsProfileCustomized) - self._machine_manager.activeQualityGroupChanged.connect(self.updateIsProfileUserCreated) - self._machine_manager.activeQualityChangesGroupChanged.connect(self.updateIsProfileUserCreated) # update on create as the activeQualityChanged signal is emitted before this manager is created when Cura starts self._updateIsProfileCustomized() - self.updateIsProfileUserCreated() isProfileCustomizedChanged = pyqtSignal() - isProfileUserCreatedChanged = pyqtSignal() @pyqtProperty(bool, notify = isProfileCustomizedChanged) def isProfileCustomized(self): @@ -58,34 +54,6 @@ class SimpleModeSettingsManager(QObject): self._is_profile_customized = has_customized_user_settings self.isProfileCustomizedChanged.emit() - @pyqtProperty(bool, notify = isProfileUserCreatedChanged) - def isProfileUserCreated(self): - return self._is_profile_user_created - - @pyqtSlot() - def updateIsProfileUserCreated(self) -> None: - quality_changes_keys = set() # type: Set[str] - - if not self._machine_manager.activeMachine: - return - - global_stack = self._machine_manager.activeMachine - - # check quality changes settings in the global stack - quality_changes_keys.update(global_stack.qualityChanges.getAllKeys()) - - # check quality changes settings in the extruder stacks - if global_stack.extruders: - for extruder_stack in global_stack.extruders.values(): - quality_changes_keys.update(extruder_stack.qualityChanges.getAllKeys()) - - # check if the qualityChanges container is not empty (meaning it is a user created profile) - has_quality_changes = len(quality_changes_keys) > 0 - - if has_quality_changes != self._is_profile_user_created: - self._is_profile_user_created = has_quality_changes - self.isProfileUserCreatedChanged.emit() - # These are the settings included in the Simple ("Recommended") Mode, so only when the other settings have been # changed, we consider it as a user customized profile in the Simple ("Recommended") Mode. __ignored_custom_setting_keys = ["support_enable", diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index 9ee2ef0dd4..55296979b5 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -794,7 +794,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # Clear all existing containers quality_changes_info.global_info.container.clear() for container_info in quality_changes_info.extruder_info_dict.values(): - container_info.container.clear() + if container_info.container: + container_info.container.clear() # Loop over everything and override the existing containers global_info = quality_changes_info.global_info diff --git a/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml index e5f668c70d..80b5c2f99e 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml @@ -28,7 +28,7 @@ Cura.MachineAction // Check if there is another instance with the same key if (!manager.existsKey(printerKey)) { - manager.setKey(printerKey) + manager.associateActiveMachineWithPrinterDevice(base.selectedDevice) manager.setGroupName(printerName) // TODO To change when the groups have a name completed() } diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml index 476e3c2aa1..a3e2517b45 100644 --- a/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml +++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml @@ -211,7 +211,7 @@ Item } } - Item + Row { id: navigationDots anchors @@ -220,23 +220,20 @@ Item top: centerSection.bottom topMargin: 36 * screenScaleFactor // TODO: Theme! } - Row + spacing: 8 * screenScaleFactor // TODO: Theme! + Repeater { - spacing: 8 * screenScaleFactor // TODO: Theme! - Repeater + model: OutputDevice.printers + Button { - model: OutputDevice.printers - Button + background: Rectangle { - background: Rectangle - { - color: model.index == currentIndex ? "#777777" : "#d8d8d8" // TODO: Theme! - radius: Math.floor(width / 2) - width: 12 * screenScaleFactor // TODO: Theme! - height: width // TODO: Theme! - } - onClicked: navigateTo(model.index) + color: model.index == currentIndex ? "#777777" : "#d8d8d8" // TODO: Theme! + radius: Math.floor(width / 2) + width: 12 * screenScaleFactor // TODO: Theme! + height: width // TODO: Theme! } + onClicked: navigateTo(model.index) } } } diff --git a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py index 1896ffac9c..a9a002aed7 100644 --- a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py @@ -3,7 +3,6 @@ from typing import Any, cast, Tuple, Union, Optional, Dict, List from time import time -from datetime import datetime import io # To create the correct buffers for sending data to the printer. import json @@ -16,7 +15,6 @@ from UM.Settings.ContainerRegistry import ContainerRegistry from UM.i18n import i18nCatalog from UM.Message import Message -from UM.Qt.Duration import Duration, DurationFormat from UM.Scene.SceneNode import SceneNode # For typing. from cura.CuraApplication import CuraApplication @@ -25,8 +23,9 @@ from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationM from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel -from plugins.UM3NetworkPrinting.src.Cloud.Utils import formatTimeCompleted, formatDateCompleted +from cura.PrinterOutputDevice import ConnectionType +from .Cloud.Utils import formatTimeCompleted, formatDateCompleted from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController from .ConfigurationChangeModel import ConfigurationChangeModel from .MeshFormatHandler import MeshFormatHandler @@ -51,7 +50,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): _clusterPrintersChanged = pyqtSignal() def __init__(self, device_id, address, properties, parent = None) -> None: - super().__init__(device_id = device_id, address = address, properties=properties, parent = parent) + super().__init__(device_id = device_id, address = address, properties=properties, connection_type = ConnectionType.NetworkConnection, parent = parent) self._api_prefix = "/cluster-api/v1/" self._number_of_extruders = 2 diff --git a/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py index be83e04585..6ce99e4891 100644 --- a/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py +++ b/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py @@ -3,7 +3,7 @@ import os.path import time -from typing import cast, Optional +from typing import Optional, TYPE_CHECKING from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject @@ -16,6 +16,9 @@ from cura.MachineAction import MachineAction from .UM3OutputDevicePlugin import UM3OutputDevicePlugin +if TYPE_CHECKING: + from cura.PrinterOutputDevice import PrinterOutputDevice + catalog = i18nCatalog("cura") @@ -116,22 +119,30 @@ class DiscoverUM3Action(MachineAction): # Ensure that the connection states are refreshed. self._network_plugin.reCheckConnections() - @pyqtSlot(str) - def setKey(self, key: str) -> None: - Logger.log("d", "Attempting to set the network key of the active machine to %s", key) + # Associates the currently active machine with the given printer device. The network connection information will be + # stored into the metadata of the currently active machine. + @pyqtSlot(QObject) + def associateActiveMachineWithPrinterDevice(self, printer_device: Optional["PrinterOutputDevice"]) -> None: + Logger.log("d", "Attempting to set the network key of the active machine to %s", printer_device.key) global_container_stack = CuraApplication.getInstance().getGlobalContainerStack() if global_container_stack: meta_data = global_container_stack.getMetaData() if "um_network_key" in meta_data: previous_network_key= meta_data["um_network_key"] - global_container_stack.setMetaDataEntry("um_network_key", key) + global_container_stack.setMetaDataEntry("um_network_key", printer_device.key) # Delete old authentication data. - Logger.log("d", "Removing old authentication id %s for device %s", global_container_stack.getMetaDataEntry("network_authentication_id", None), key) + Logger.log("d", "Removing old authentication id %s for device %s", global_container_stack.getMetaDataEntry("network_authentication_id", None), printer_device.key) global_container_stack.removeMetaDataEntry("network_authentication_id") global_container_stack.removeMetaDataEntry("network_authentication_key") - CuraApplication.getInstance().getMachineManager().replaceContainersMetadata(key = "um_network_key", value = previous_network_key, new_value = key) + CuraApplication.getInstance().getMachineManager().replaceContainersMetadata(key = "um_network_key", value = previous_network_key, new_value = printer_device.key) + + if "connection_type" in meta_data: + previous_connection_type = meta_data["connection_type"] + global_container_stack.setMetaDataEntry("connection_type", printer_device.getConnectionType().value) + CuraApplication.getInstance().getMachineManager().replaceContainersMetadata(key = "connection_type", value = previous_connection_type, new_value = printer_device.getConnectionType().value) else: - global_container_stack.setMetaDataEntry("um_network_key", key) + global_container_stack.setMetaDataEntry("um_network_key", printer_device.key) + global_container_stack.setMetaDataEntry("connection_type", printer_device.getConnectionType().value) if self._network_plugin: # Ensure that the connection states are refreshed. diff --git a/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py index 18af22e72f..3ce0460d6b 100644 --- a/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py +++ b/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py @@ -7,6 +7,7 @@ from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutp from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel +from cura.PrinterOutputDevice import ConnectionType from cura.Settings.ContainerManager import ContainerManager from cura.Settings.ExtruderManager import ExtruderManager @@ -43,7 +44,7 @@ i18n_catalog = i18nCatalog("cura") # 5. As a final step, we verify the authentication, as this forces the QT manager to setup the authenticator. class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice): def __init__(self, device_id, address: str, properties, parent = None) -> None: - super().__init__(device_id = device_id, address = address, properties = properties, parent = parent) + super().__init__(device_id = device_id, address = address, properties = properties, connection_type = ConnectionType.NetworkConnection, parent = parent) self._api_prefix = "/api/v1/" self._number_of_extruders = 2 diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py index 52fbf31e3c..4642811f39 100644 --- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py +++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py @@ -288,6 +288,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin): global_container_stack = Application.getInstance().getGlobalContainerStack() if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"): + global_container_stack.setMetaDataEntry("connection_type", device.getConnectionType().value) device.connect() device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged) diff --git a/plugins/USBPrinting/MonitorItem.qml b/plugins/USBPrinting/MonitorItem.qml index 8041698ef0..c86353f814 100644 --- a/plugins/USBPrinting/MonitorItem.qml +++ b/plugins/USBPrinting/MonitorItem.qml @@ -13,6 +13,8 @@ Component { Rectangle { + color: UM.Theme.getColor("main_background") + anchors.right: parent.right width: parent.width * 0.3 anchors.top: parent.top diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py index 1d42e70366..19a703e605 100644 --- a/plugins/USBPrinting/USBPrinterOutputDevice.py +++ b/plugins/USBPrinting/USBPrinterOutputDevice.py @@ -7,7 +7,7 @@ from UM.i18n import i18nCatalog from UM.Qt.Duration import DurationFormat from cura.CuraApplication import CuraApplication -from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState +from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.GenericOutputController import GenericOutputController @@ -29,7 +29,7 @@ catalog = i18nCatalog("cura") class USBPrinterOutputDevice(PrinterOutputDevice): def __init__(self, serial_port: str, baud_rate: Optional[int] = None) -> None: - super().__init__(serial_port) + super().__init__(serial_port, connection_type = ConnectionType.UsbConnection) self.setName(catalog.i18nc("@item:inmenu", "USB printing")) self.setShortDescription(catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print via USB")) self.setDescription(catalog.i18nc("@info:tooltip", "Print via USB")) @@ -179,7 +179,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): return CuraApplication.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) self._onGlobalContainerStackChanged() - self.setConnectionState(ConnectionState.connected) + self.setConnectionState(ConnectionState.Connected) self._update_thread.start() def _onGlobalContainerStackChanged(self): @@ -208,7 +208,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._sendCommand(command) def _sendCommand(self, command: Union[str, bytes]): - if self._serial is None or self._connection_state != ConnectionState.connected: + if self._serial is None or self._connection_state != ConnectionState.Connected: return new_command = cast(bytes, command) if type(command) is bytes else cast(str, command).encode() # type: bytes @@ -222,7 +222,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice): self._command_received.set() def _update(self): - while self._connection_state == ConnectionState.connected and self._serial is not None: + while self._connection_state == ConnectionState.Connected and self._serial is not None: try: line = self._serial.readline() except: diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py index bd207d9d96..d4c0d1828e 100644 --- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py +++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py @@ -66,7 +66,7 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin): return changed_device = self._usb_output_devices[serial_port] - if changed_device.connectionState == ConnectionState.connected: + if changed_device.connectionState == ConnectionState.Connected: self.getOutputDeviceManager().addOutputDevice(changed_device) else: self.getOutputDeviceManager().removeOutputDevice(serial_port) diff --git a/resources/definitions/ultimaker_original_plus.def.json b/resources/definitions/ultimaker_original_plus.def.json index 5ad7ae66e8..bdb8a3d788 100644 --- a/resources/definitions/ultimaker_original_plus.def.json +++ b/resources/definitions/ultimaker_original_plus.def.json @@ -16,7 +16,8 @@ { "0": "ultimaker_original_plus_extruder_0" }, - "firmware_file": "MarlinUltimaker-UMOP-{baudrate}.hex" + "firmware_file": "MarlinUltimaker-UMOP-{baudrate}.hex", + "firmware_hbk_file": "MarlinUltimaker-UMOP-{baudrate}.hex" }, "overrides": { diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml index 8ab943b93b..8a34c7e219 100644 --- a/resources/qml/Cura.qml +++ b/resources/qml/Cura.qml @@ -123,6 +123,17 @@ UM.MainWindow } } } + + // This is a placehoder for adding a pattern in the header + Image + { + id: backgroundPattern + anchors.fill: parent + fillMode: Image.Tile + source: UM.Theme.getImage("header_pattern") + horizontalAlignment: Image.AlignLeft + verticalAlignment: Image.AlignTop + } } MainWindowHeader diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationMenu.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationMenu.qml index c6790ebbec..207b65afc7 100644 --- a/resources/qml/Menus/ConfigurationMenu/ConfigurationMenu.qml +++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationMenu.qml @@ -136,7 +136,7 @@ Cura.ExpandablePopup onVisibleChanged: { - is_connected = Cura.MachineManager.activeMachineNetworkKey !== "" && Cura.MachineManager.printerConnected // Re-evaluate. + is_connected = Cura.MachineManager.activeMachineHasRemoteConnection && Cura.MachineManager.printerConnected //Re-evaluate. // If the printer is not connected, we switch always to the custom mode. If is connected instead, the auto mode // or the previous state is selected diff --git a/resources/qml/Preferences/MachinesPage.qml b/resources/qml/Preferences/MachinesPage.qml index bc75b9bc72..4acefdb493 100644 --- a/resources/qml/Preferences/MachinesPage.qml +++ b/resources/qml/Preferences/MachinesPage.qml @@ -1,4 +1,4 @@ -// Copyright (c) 2016 Ultimaker B.V. +// Copyright (c) 2018 Ultimaker B.V. // Cura is released under the terms of the LGPLv3 or higher. import QtQuick 2.7 @@ -91,7 +91,7 @@ UM.ManagementPage Item { - width: childrenRect.width + 2 * screenScaleFactor + width: Math.round(childrenRect.width + 2 * screenScaleFactor) height: childrenRect.height Button { diff --git a/resources/qml/PrintMonitor.qml b/resources/qml/PrintMonitor.qml index 6d8edf0deb..d44acf0adb 100644 --- a/resources/qml/PrintMonitor.qml +++ b/resources/qml/PrintMonitor.qml @@ -12,7 +12,7 @@ import Cura 1.0 as Cura import "PrinterOutput" -Rectangle +Item { id: base UM.I18nCatalog { id: catalog; name: "cura"} diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml index e6b3f1b9eb..1e71134404 100644 --- a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml +++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml @@ -39,17 +39,6 @@ Item { target: Cura.QualityProfilesDropDownMenuModel onItemsChanged: qualityModel.update() - onDataChanged: - { - // If a custom profile is selected and then a user decides to change any of setting the slider should show - // the reset button. After clicking the reset button the QualityProfilesDropDownMenuModel(ListModel) is - // updated before the property isProfileCustomized is called to update. - if (Cura.SimpleModeSettingsManager.isProfileCustomized) - { - Cura.SimpleModeSettingsManager.updateIsProfileUserCreated() - } - qualityModel.update() - } } Connections { @@ -97,7 +86,7 @@ Item if (Cura.MachineManager.activeQualityType == qualityItem.quality_type) { // set to -1 when switching to user created profile so all ticks are clickable - if (Cura.SimpleModeSettingsManager.isProfileUserCreated) + if (Cura.MachineManager.hasCustomQuality) { qualityModel.qualitySliderActiveIndex = -1 } @@ -192,7 +181,7 @@ Item { id: customisedSettings - visible: Cura.SimpleModeSettingsManager.isProfileCustomized || Cura.SimpleModeSettingsManager.isProfileUserCreated + visible: Cura.SimpleModeSettingsManager.isProfileCustomized || Cura.MachineManager.hasCustomQuality height: visible ? UM.Theme.getSize("print_setup_icon").height : 0 width: height anchors @@ -358,7 +347,7 @@ Item { anchors.fill: parent hoverEnabled: true - enabled: !Cura.SimpleModeSettingsManager.isProfileUserCreated + enabled: !Cura.MachineManager.hasCustomQuality onEntered: { var tooltipContent = catalog.i18nc("@tooltip", "This quality profile is not available for your current material and nozzle configuration. Please change these to enable this quality profile") @@ -417,7 +406,7 @@ Item implicitWidth: UM.Theme.getSize("print_setup_slider_handle").width implicitHeight: implicitWidth radius: Math.round(implicitWidth / 2) - visible: !Cura.SimpleModeSettingsManager.isProfileCustomized && !Cura.SimpleModeSettingsManager.isProfileUserCreated && qualityModel.existingQualityProfile + visible: !Cura.SimpleModeSettingsManager.isProfileCustomized && !Cura.MachineManager.hasCustomQuality && qualityModel.existingQualityProfile } } @@ -441,7 +430,7 @@ Item anchors.fill: parent hoverEnabled: true acceptedButtons: Qt.NoButton - enabled: !Cura.SimpleModeSettingsManager.isProfileUserCreated + enabled: !Cura.MachineManager.hasCustomQuality } } @@ -451,7 +440,7 @@ Item { anchors.fill: parent hoverEnabled: true - visible: Cura.SimpleModeSettingsManager.isProfileUserCreated + visible: Cura.MachineManager.hasCustomQuality onEntered: { diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml index fb8f0a58e1..28e01c7ae9 100644 --- a/resources/qml/PrinterSelector/MachineSelector.qml +++ b/resources/qml/PrinterSelector/MachineSelector.qml @@ -11,7 +11,7 @@ Cura.ExpandablePopup { id: machineSelector - property bool isNetworkPrinter: Cura.MachineManager.activeMachineNetworkKey != "" + property bool isNetworkPrinter: Cura.MachineManager.activeMachineHasRemoteConnection property bool isPrinterConnected: Cura.MachineManager.printerConnected property var outputDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null @@ -98,6 +98,12 @@ Cura.ExpandablePopup scroll.height = Math.min(height, maximumHeight) popup.height = scroll.height + buttonRow.height } + Component.onCompleted: + { + scroll.height = Math.min(height, maximumHeight) + popup.height = scroll.height + buttonRow.height + } + } } diff --git a/resources/qml/PrinterSelector/MachineSelectorList.qml b/resources/qml/PrinterSelector/MachineSelectorList.qml index d831f4eb5c..9450502760 100644 --- a/resources/qml/PrinterSelector/MachineSelectorList.qml +++ b/resources/qml/PrinterSelector/MachineSelectorList.qml @@ -7,80 +7,40 @@ import QtQuick.Controls 2.3 import UM 1.2 as UM import Cura 1.0 as Cura -Column +ListView { - id: machineSelectorList + id: listView + height: childrenRect.height + model: Cura.PrintersModel {} + section.property: "hasRemoteConnection" - Label + section.delegate: Label { - text: catalog.i18nc("@label", "Connected printers") - visible: networkedPrintersModel.items.length > 0 - leftPadding: UM.Theme.getSize("default_margin").width + text: section == "true" ? catalog.i18nc("@label", "Connected printers") : catalog.i18nc("@label", "Preset printers") + width: parent.width height: visible ? contentHeight + 2 * UM.Theme.getSize("default_margin").height : 0 + leftPadding: UM.Theme.getSize("default_margin").width renderType: Text.NativeRendering font: UM.Theme.getFont("medium") color: UM.Theme.getColor("text_medium") verticalAlignment: Text.AlignVCenter } - Repeater + delegate: MachineSelectorButton { - id: networkedPrinters + text: model.name + width: listView.width + outputDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null - model: UM.ContainerStacksModel + checked: { - id: networkedPrintersModel - filter: + // If the machine has a remote connection + var result = Cura.MachineManager.activeMachineId == model.id + if (Cura.MachineManager.activeMachineHasRemoteConnection) { - "type": "machine", - "um_network_key": "*" - } - } - - delegate: MachineSelectorButton - { - text: model.metadata["connect_group_name"] - checked: Cura.MachineManager.activeMachineNetworkGroupName == model.metadata["connect_group_name"] - outputDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null - - Connections - { - target: Cura.MachineManager - onActiveMachineNetworkGroupNameChanged: checked = Cura.MachineManager.activeMachineNetworkGroupName == model.metadata["connect_group_name"] + result |= Cura.MachineManager.activeMachineNetworkGroupName == model.metadata["connect_group_name"] } + return result } } - - Label - { - text: catalog.i18nc("@label", "Preset printers") - visible: virtualPrintersModel.items.length > 0 - leftPadding: UM.Theme.getSize("default_margin").width - height: visible ? contentHeight + 2 * UM.Theme.getSize("default_margin").height : 0 - renderType: Text.NativeRendering - font: UM.Theme.getFont("medium") - color: UM.Theme.getColor("text_medium") - verticalAlignment: Text.AlignVCenter - } - - Repeater - { - id: virtualPrinters - - model: UM.ContainerStacksModel - { - id: virtualPrintersModel - filter: - { - "type": "machine", - "um_network_key": null - } - } - - delegate: MachineSelectorButton - { - text: model.name - checked: Cura.MachineManager.activeMachineId == model.id - } - } -} \ No newline at end of file +} diff --git a/resources/themes/cura-light/images/header_pattern.svg b/resources/themes/cura-light/images/header_pattern.svg deleted file mode 100644 index eff5f01cfa..0000000000 --- a/resources/themes/cura-light/images/header_pattern.svg +++ /dev/null @@ -1,1901 +0,0 @@ - - - - Desktop HD - Created with Sketch. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file