mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-13 03:29:02 +08:00
Merge pull request #10607 from Ultimaker/CURA-8609_sync_materials_to_printer
Sync materials to printers via cloud
This commit is contained in:
commit
f47738f558
@ -62,7 +62,7 @@ class Account(QObject):
|
||||
updatePackagesEnabledChanged = pyqtSignal(bool)
|
||||
|
||||
CLIENT_SCOPES = "account.user.read drive.backup.read drive.backup.write packages.download " \
|
||||
"packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write " \
|
||||
"packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write connect.material.write " \
|
||||
"library.project.read library.project.write cura.printjob.read cura.printjob.write " \
|
||||
"cura.mesh.read cura.mesh.write"
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtProperty, pyqtSignal
|
||||
from typing import Optional
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.i18n import i18nCatalog
|
||||
@ -20,6 +21,7 @@ class GlobalStacksModel(ListModel):
|
||||
MetaDataRole = Qt.UserRole + 5
|
||||
DiscoverySourceRole = Qt.UserRole + 6 # For separating local and remote printers in the machine management page
|
||||
RemovalWarningRole = Qt.UserRole + 7
|
||||
IsOnlineRole = Qt.UserRole + 8
|
||||
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
@ -31,18 +33,49 @@ class GlobalStacksModel(ListModel):
|
||||
self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection")
|
||||
self.addRoleName(self.MetaDataRole, "metadata")
|
||||
self.addRoleName(self.DiscoverySourceRole, "discoverySource")
|
||||
self.addRoleName(self.IsOnlineRole, "isOnline")
|
||||
|
||||
self._change_timer = QTimer()
|
||||
self._change_timer.setInterval(200)
|
||||
self._change_timer.setSingleShot(True)
|
||||
self._change_timer.timeout.connect(self._update)
|
||||
|
||||
self._filter_connection_type = None # type: Optional[ConnectionType]
|
||||
self._filter_online_only = False
|
||||
|
||||
# Listen to changes
|
||||
CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
|
||||
CuraContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
|
||||
CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
|
||||
self._updateDelayed()
|
||||
|
||||
filterConnectionTypeChanged = pyqtSignal()
|
||||
def setFilterConnectionType(self, new_filter: Optional[ConnectionType]) -> None:
|
||||
self._filter_connection_type = new_filter
|
||||
|
||||
@pyqtProperty(int, fset = setFilterConnectionType, notify = filterConnectionTypeChanged)
|
||||
def filterConnectionType(self) -> int:
|
||||
"""
|
||||
The connection type to filter the list of printers by.
|
||||
|
||||
Only printers that match this connection type will be listed in the
|
||||
model.
|
||||
"""
|
||||
if self._filter_connection_type is None:
|
||||
return -1
|
||||
return self._filter_connection_type.value
|
||||
|
||||
filterOnlineOnlyChanged = pyqtSignal()
|
||||
def setFilterOnlineOnly(self, new_filter: bool) -> None:
|
||||
self._filter_online_only = new_filter
|
||||
|
||||
@pyqtProperty(bool, fset = setFilterOnlineOnly, notify = filterOnlineOnlyChanged)
|
||||
def filterOnlineOnly(self) -> bool:
|
||||
"""
|
||||
Whether to filter the global stacks to show only printers that are online.
|
||||
"""
|
||||
return self._filter_online_only
|
||||
|
||||
def _onContainerChanged(self, container) -> None:
|
||||
"""Handler for container added/removed events from registry"""
|
||||
|
||||
@ -58,6 +91,10 @@ class GlobalStacksModel(ListModel):
|
||||
|
||||
container_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
||||
for container_stack in container_stacks:
|
||||
if self._filter_connection_type is not None: # We want to filter on connection types.
|
||||
if not any((connection_type == self._filter_connection_type for connection_type in container_stack.configuredConnectionTypes)):
|
||||
continue # No connection type on this printer matches the filter.
|
||||
|
||||
has_remote_connection = False
|
||||
|
||||
for connection_type in container_stack.configuredConnectionTypes:
|
||||
@ -67,6 +104,10 @@ class GlobalStacksModel(ListModel):
|
||||
if parseBool(container_stack.getMetaDataEntry("hidden", False)):
|
||||
continue
|
||||
|
||||
is_online = container_stack.getMetaDataEntry("is_online", False)
|
||||
if self._filter_online_only and not is_online:
|
||||
continue
|
||||
|
||||
device_name = container_stack.getMetaDataEntry("group_name", container_stack.getName())
|
||||
section_name = "Connected printers" if has_remote_connection else "Preset printers"
|
||||
section_name = self._catalog.i18nc("@info:title", section_name)
|
||||
@ -82,6 +123,7 @@ class GlobalStacksModel(ListModel):
|
||||
"hasRemoteConnection": has_remote_connection,
|
||||
"metadata": container_stack.getMetaData().copy(),
|
||||
"discoverySource": section_name,
|
||||
"removalWarning": removal_warning})
|
||||
"removalWarning": removal_warning,
|
||||
"isOnline": is_online})
|
||||
items.sort(key=lambda i: (not i["hasRemoteConnection"], i["name"]))
|
||||
self.setItems(items)
|
||||
|
@ -2,21 +2,21 @@
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import copy # To duplicate materials.
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from typing import Any, Dict, Optional, TYPE_CHECKING
|
||||
import uuid # To generate new GUIDs for new materials.
|
||||
import zipfile # To export all materials in a .zip archive.
|
||||
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
|
||||
from UM.Message import Message
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Resources import Resources # To find QML files.
|
||||
from UM.Signal import postponeSignals, CompressTechnique
|
||||
|
||||
import cura.CuraApplication # Imported like this to prevent circular imports.
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To find the sets of materials belonging to each other, and currently loaded extruder stacks.
|
||||
from cura.UltimakerCloud.CloudMaterialSync import CloudMaterialSync
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Machines.MaterialNode import MaterialNode
|
||||
@ -33,6 +33,7 @@ class MaterialManagementModel(QObject):
|
||||
|
||||
def __init__(self, parent: Optional[QObject] = None) -> None:
|
||||
super().__init__(parent = parent)
|
||||
self._material_sync = CloudMaterialSync(parent=self)
|
||||
self._checkIfNewMaterialsWereInstalled()
|
||||
|
||||
def _checkIfNewMaterialsWereInstalled(self) -> None:
|
||||
@ -89,6 +90,7 @@ class MaterialManagementModel(QObject):
|
||||
elif sync_message_action == "learn_more":
|
||||
QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360013137919?utm_source=cura&utm_medium=software&utm_campaign=sync-material-printer-message"))
|
||||
|
||||
|
||||
@pyqtSlot("QVariant", result = bool)
|
||||
def canMaterialBeRemoved(self, material_node: "MaterialNode") -> bool:
|
||||
"""Can a certain material be deleted, or is it still in use in one of the container stacks anywhere?
|
||||
@ -323,52 +325,18 @@ class MaterialManagementModel(QObject):
|
||||
except ValueError: # Material was not in the favorites list.
|
||||
Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file))
|
||||
|
||||
@pyqtSlot(result = QUrl)
|
||||
def getPreferredExportAllPath(self) -> QUrl:
|
||||
@pyqtSlot()
|
||||
def openSyncAllWindow(self) -> None:
|
||||
"""
|
||||
Get the preferred path to export materials to.
|
||||
Opens the window to sync all materials.
|
||||
"""
|
||||
self._material_sync.reset()
|
||||
|
||||
If there is a removable drive, that should be the preferred path. Otherwise it should be the most recent local
|
||||
file path.
|
||||
:return: The preferred path to export all materials to.
|
||||
"""
|
||||
cura_application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
device_manager = cura_application.getOutputDeviceManager()
|
||||
devices = device_manager.getOutputDevices()
|
||||
for device in devices:
|
||||
if device.__class__.__name__ == "RemovableDriveOutputDevice":
|
||||
return QUrl.fromLocalFile(device.getId())
|
||||
else: # No removable drives? Use local path.
|
||||
return cura_application.getDefaultPath("dialog_material_path")
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def exportAll(self, file_path: QUrl) -> None:
|
||||
"""
|
||||
Export all materials to a certain file path.
|
||||
:param file_path: The path to export the materials to.
|
||||
"""
|
||||
registry = CuraContainerRegistry.getInstance()
|
||||
|
||||
try:
|
||||
archive = zipfile.ZipFile(file_path.toLocalFile(), "w", compression = zipfile.ZIP_DEFLATED)
|
||||
except OSError as e:
|
||||
Logger.log("e", f"Can't write to destination {file_path.toLocalFile()}: {type(e)} - {str(e)}")
|
||||
error_message = Message(
|
||||
text = catalog.i18nc("@error:text Followed by an error message of why it could not save", "Could not save material archive to {filename}:").format(filename = file_path.toLocalFile()) + " " + str(e),
|
||||
title = catalog.i18nc("@error:title", "Failed to save material archive"),
|
||||
message_type = Message.MessageType.ERROR
|
||||
)
|
||||
error_message.show()
|
||||
if self._material_sync.sync_all_dialog is None:
|
||||
qml_path = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QmlFiles, "Preferences", "Materials", "MaterialsSyncDialog.qml")
|
||||
self._material_sync.sync_all_dialog = cura.CuraApplication.CuraApplication.getInstance().createQmlComponent(qml_path, {})
|
||||
if self._material_sync.sync_all_dialog is None: # Failed to load QML file.
|
||||
return
|
||||
for metadata in registry.findInstanceContainersMetadata(type = "material"):
|
||||
if metadata["base_file"] != metadata["id"]: # Only process base files.
|
||||
continue
|
||||
if metadata["id"] == "empty_material": # Don't export the empty material.
|
||||
continue
|
||||
material = registry.findContainers(id = metadata["id"])[0]
|
||||
suffix = registry.getMimeTypeForContainer(type(material)).preferredSuffix
|
||||
filename = metadata["id"] + "." + suffix
|
||||
try:
|
||||
archive.writestr(filename, material.serialize())
|
||||
except OSError as e:
|
||||
Logger.log("e", f"An error has occurred while writing the material \'{metadata['id']}\' in the file \'{filename}\': {e}.")
|
||||
self._material_sync.sync_all_dialog.setProperty("syncModel", self._material_sync)
|
||||
self._material_sync.sync_all_dialog.setProperty("pageIndex", 0) # Return to first page.
|
||||
self._material_sync.sync_all_dialog.show()
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
||||
@ -114,6 +114,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
return b"".join(file_data_bytes_list)
|
||||
|
||||
def _update(self) -> None:
|
||||
"""
|
||||
Update the connection state of this device.
|
||||
|
||||
This is called on regular intervals.
|
||||
"""
|
||||
if self._last_response_time:
|
||||
time_since_last_response = time() - self._last_response_time
|
||||
else:
|
||||
@ -127,11 +132,11 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
if time_since_last_response > self._timeout_time >= time_since_last_request:
|
||||
# Go (or stay) into timeout.
|
||||
if self._connection_state_before_timeout is None:
|
||||
self._connection_state_before_timeout = self._connection_state
|
||||
self._connection_state_before_timeout = self.connectionState
|
||||
|
||||
self.setConnectionState(ConnectionState.Closed)
|
||||
|
||||
elif self._connection_state == ConnectionState.Closed:
|
||||
elif self.connectionState == 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)
|
||||
@ -361,7 +366,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
|
||||
self._last_response_time = time()
|
||||
|
||||
if self._connection_state == ConnectionState.Connecting:
|
||||
if self.connectionState == ConnectionState.Connecting:
|
||||
self.setConnectionState(ConnectionState.Connected)
|
||||
|
||||
callback_key = reply.url().toString() + str(reply.operation())
|
||||
|
@ -1,11 +1,13 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from enum import IntEnum
|
||||
from typing import Callable, List, Optional, Union
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
import cura.CuraApplication # Imported like this to prevent circular imports.
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import signalemitter
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
@ -120,11 +122,22 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
callback(QMessageBox.Yes)
|
||||
|
||||
def isConnected(self) -> bool:
|
||||
return self._connection_state != ConnectionState.Closed and self._connection_state != ConnectionState.Error
|
||||
"""
|
||||
Returns whether we could theoretically send commands to this printer.
|
||||
:return: `True` if we are connected, or `False` if not.
|
||||
"""
|
||||
return self.connectionState != ConnectionState.Closed and self.connectionState != ConnectionState.Error
|
||||
|
||||
def setConnectionState(self, connection_state: "ConnectionState") -> None:
|
||||
if self._connection_state != connection_state:
|
||||
"""
|
||||
Store the connection state of the printer.
|
||||
|
||||
Causes everything that displays the connection state to update its QML models.
|
||||
:param connection_state: The new connection state to store.
|
||||
"""
|
||||
if self.connectionState != connection_state:
|
||||
self._connection_state = connection_state
|
||||
cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack().setMetaDataEntry("is_online", self.isConnected())
|
||||
self.connectionStateChanged.emit(self._id)
|
||||
|
||||
@pyqtProperty(int, constant = True)
|
||||
@ -133,6 +146,10 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
|
||||
@pyqtProperty(int, notify = connectionStateChanged)
|
||||
def connectionState(self) -> "ConnectionState":
|
||||
"""
|
||||
Get the connection state of the printer, e.g. whether it is connected, still connecting, error state, etc.
|
||||
:return: The current connection state of this output device.
|
||||
"""
|
||||
return self._connection_state
|
||||
|
||||
def _update(self) -> None:
|
||||
|
256
cura/PrinterOutput/UploadMaterialsJob.py
Normal file
256
cura/PrinterOutput/UploadMaterialsJob.py
Normal file
@ -0,0 +1,256 @@
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import enum
|
||||
import functools # For partial methods to use as callbacks with information pre-filled.
|
||||
import json # To serialise metadata for API calls.
|
||||
import os # To delete the archive when we're done.
|
||||
from PyQt5.QtCore import QUrl
|
||||
import tempfile # To create an archive before we upload it.
|
||||
|
||||
import cura.CuraApplication # Imported like this to prevent circular imports.
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To find all printers to upload to.
|
||||
from cura.UltimakerCloud import UltimakerCloudConstants # To know where the API is.
|
||||
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To know how to communicate with this server.
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import Signal
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To call the API.
|
||||
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
|
||||
|
||||
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
from cura.UltimakerCloud.CloudMaterialSync import CloudMaterialSync
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class UploadMaterialsError(Exception):
|
||||
"""
|
||||
Class to indicate something went wrong while uploading.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class UploadMaterialsJob(Job):
|
||||
"""
|
||||
Job that uploads a set of materials to the Digital Factory.
|
||||
|
||||
The job has a number of stages:
|
||||
- First, it generates an archive of all materials. This typically takes a lot of processing power during which the
|
||||
GIL remains locked.
|
||||
- Then it requests the API to upload an archive.
|
||||
- Then it uploads the archive to the URL given by the first request.
|
||||
- Then it tells the API that the archive can be distributed to the printers.
|
||||
"""
|
||||
|
||||
UPLOAD_REQUEST_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/connect/v1/materials/upload"
|
||||
UPLOAD_CONFIRM_URL = UltimakerCloudConstants.CuraCloudAPIRoot + "/connect/v1/clusters/{cluster_id}/printers/{cluster_printer_id}/action/import_material"
|
||||
|
||||
class Result(enum.IntEnum):
|
||||
SUCCESS = 0
|
||||
FAILED = 1
|
||||
|
||||
class PrinterStatus(enum.Enum):
|
||||
UPLOADING = "uploading"
|
||||
SUCCESS = "success"
|
||||
FAILED = "failed"
|
||||
|
||||
def __init__(self, material_sync: "CloudMaterialSync"):
|
||||
super().__init__()
|
||||
self._material_sync = material_sync
|
||||
self._scope = JsonDecoratorScope(UltimakerCloudScope(cura.CuraApplication.CuraApplication.getInstance())) # type: JsonDecoratorScope
|
||||
self._archive_filename = None # type: Optional[str]
|
||||
self._archive_remote_id = None # type: Optional[str] # ID that the server gives to this archive. Used to communicate about the archive to the server.
|
||||
self._printer_sync_status = {} # type: Dict[str, str]
|
||||
self._printer_metadata = [] # type: List[Dict[str, Any]]
|
||||
self.processProgressChanged.connect(self._onProcessProgressChanged)
|
||||
|
||||
uploadCompleted = Signal() # Triggered when the job is really complete, including uploading to the cloud.
|
||||
processProgressChanged = Signal() # Triggered when we've made progress creating the archive.
|
||||
uploadProgressChanged = Signal() # Triggered when we've made progress with the complete job. This signal emits a progress fraction (0-1) as well as the status of every printer.
|
||||
|
||||
def run(self) -> None:
|
||||
"""
|
||||
Generates an archive of materials and starts uploading that archive to the cloud.
|
||||
"""
|
||||
self._printer_metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(
|
||||
type = "machine",
|
||||
connection_type = "3", # Only cloud printers.
|
||||
is_online = "True", # Only online printers. Otherwise the server gives an error.
|
||||
host_guid = "*", # Required metadata field. Otherwise we get a KeyError.
|
||||
um_cloud_cluster_id = "*" # Required metadata field. Otherwise we get a KeyError.
|
||||
)
|
||||
for printer in self._printer_metadata:
|
||||
self._printer_sync_status[printer["host_guid"]] = self.PrinterStatus.UPLOADING.value
|
||||
|
||||
try:
|
||||
archive_file = tempfile.NamedTemporaryFile("wb", delete = False)
|
||||
archive_file.close()
|
||||
self._archive_filename = archive_file.name
|
||||
self._material_sync.exportAll(QUrl.fromLocalFile(self._archive_filename), notify_progress = self.processProgressChanged)
|
||||
except OSError as e:
|
||||
Logger.error(f"Failed to create archive of materials to sync with printers: {type(e)} - {e}")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "Failed to create archive of materials to sync with printers.")))
|
||||
return
|
||||
|
||||
try:
|
||||
file_size = os.path.getsize(self._archive_filename)
|
||||
except OSError as e:
|
||||
Logger.error(f"Failed to load the archive of materials to sync it with printers: {type(e)} - {e}")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "Failed to load the archive of materials to sync it with printers.")))
|
||||
return
|
||||
|
||||
request_metadata = {
|
||||
"data": {
|
||||
"file_size": file_size,
|
||||
"material_profile_name": "cura.umm", # File name can be anything as long as it's .umm. It's not used by anyone.
|
||||
"content_type": "application/zip", # This endpoint won't receive files of different MIME types.
|
||||
"origin": "cura" # Some identifier against hackers intercepting this upload request, apparently.
|
||||
}
|
||||
}
|
||||
request_payload = json.dumps(request_metadata).encode("UTF-8")
|
||||
|
||||
http = HttpRequestManager.getInstance()
|
||||
http.put(
|
||||
url = self.UPLOAD_REQUEST_URL,
|
||||
data = request_payload,
|
||||
callback = self.onUploadRequestCompleted,
|
||||
error_callback = self.onError,
|
||||
scope = self._scope
|
||||
)
|
||||
|
||||
def onUploadRequestCompleted(self, reply: "QNetworkReply") -> None:
|
||||
"""
|
||||
Triggered when we successfully requested to upload a material archive.
|
||||
|
||||
We then need to start uploading the material archive to the URL that the request answered with.
|
||||
:param reply: The reply from the server to our request to upload an archive.
|
||||
"""
|
||||
response_data = HttpRequestManager.readJSON(reply)
|
||||
if response_data is None:
|
||||
Logger.error(f"Invalid response to material upload request. Could not parse JSON data.")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "The response from Digital Factory appears to be corrupted.")))
|
||||
return
|
||||
if "data" not in response_data:
|
||||
Logger.error(f"Invalid response to material upload request: Missing 'data' field that contains the entire response.")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "The response from Digital Factory is missing important information.")))
|
||||
return
|
||||
if "upload_url" not in response_data["data"]:
|
||||
Logger.error(f"Invalid response to material upload request: Missing 'upload_url' field to upload archive to.")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "The response from Digital Factory is missing important information.")))
|
||||
return
|
||||
if "material_profile_id" not in response_data["data"]:
|
||||
Logger.error(f"Invalid response to material upload request: Missing 'material_profile_id' to communicate about the materials with the server.")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "The response from Digital Factory is missing important information.")))
|
||||
return
|
||||
|
||||
upload_url = response_data["data"]["upload_url"]
|
||||
self._archive_remote_id = response_data["data"]["material_profile_id"]
|
||||
try:
|
||||
with open(cast(str, self._archive_filename), "rb") as f:
|
||||
file_data = f.read()
|
||||
except OSError as e:
|
||||
Logger.error(f"Failed to load archive back in for sending to cloud: {type(e)} - {e}")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "Failed to load the archive of materials to sync it with printers.")))
|
||||
return
|
||||
http = HttpRequestManager.getInstance()
|
||||
http.put(
|
||||
url = upload_url,
|
||||
data = file_data,
|
||||
callback = self.onUploadCompleted,
|
||||
error_callback = self.onError,
|
||||
scope = self._scope
|
||||
)
|
||||
|
||||
def onUploadCompleted(self, reply: "QNetworkReply") -> None:
|
||||
"""
|
||||
When we've successfully uploaded the archive to the cloud, we need to notify the API to start syncing that
|
||||
archive to every printer.
|
||||
:param reply: The reply from the cloud storage when the upload succeeded.
|
||||
"""
|
||||
for container_stack in self._printer_metadata:
|
||||
cluster_id = container_stack["um_cloud_cluster_id"]
|
||||
printer_id = container_stack["host_guid"]
|
||||
|
||||
http = HttpRequestManager.getInstance()
|
||||
http.post(
|
||||
url = self.UPLOAD_CONFIRM_URL.format(cluster_id = cluster_id, cluster_printer_id = printer_id),
|
||||
callback = functools.partial(self.onUploadConfirmed, printer_id),
|
||||
error_callback = functools.partial(self.onUploadConfirmed, printer_id), # Let this same function handle the error too.
|
||||
scope = self._scope,
|
||||
data = json.dumps({"data": {"material_profile_id": self._archive_remote_id}}).encode("UTF-8")
|
||||
)
|
||||
|
||||
def onUploadConfirmed(self, printer_id: str, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None:
|
||||
"""
|
||||
Triggered when we've got a confirmation that the material is synced with the printer, or that syncing failed.
|
||||
|
||||
If syncing succeeded we mark this printer as having the status "success". If it failed we mark the printer as
|
||||
"failed". If this is the last upload that needed to be completed, we complete the job with either a success
|
||||
state (every printer successfully synced) or a failed state (any printer failed).
|
||||
:param printer_id: The printer host_guid that we completed syncing with.
|
||||
:param reply: The reply that the server gave to confirm.
|
||||
:param error: If the request failed, this error gives an indication what happened.
|
||||
"""
|
||||
if error is not None:
|
||||
Logger.error(f"Failed to confirm uploading material archive to printer {printer_id}: {error}")
|
||||
self._printer_sync_status[printer_id] = self.PrinterStatus.FAILED.value
|
||||
else:
|
||||
self._printer_sync_status[printer_id] = self.PrinterStatus.SUCCESS.value
|
||||
|
||||
still_uploading = len([val for val in self._printer_sync_status.values() if val == self.PrinterStatus.UPLOADING.value])
|
||||
self.uploadProgressChanged.emit(0.8 + (len(self._printer_sync_status) - still_uploading) / len(self._printer_sync_status), self.getPrinterSyncStatus())
|
||||
|
||||
if still_uploading == 0: # This is the last response to be processed.
|
||||
if self.PrinterStatus.FAILED.value in self._printer_sync_status.values():
|
||||
self.setResult(self.Result.FAILED)
|
||||
self.setError(UploadMaterialsError(catalog.i18nc("@text:error", "Failed to connect to Digital Factory to sync materials with some of the printers.")))
|
||||
else:
|
||||
self.setResult(self.Result.SUCCESS)
|
||||
self.uploadCompleted.emit(self.getResult(), self.getError())
|
||||
|
||||
def onError(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"]) -> None:
|
||||
"""
|
||||
Used as callback from HTTP requests when the request failed.
|
||||
|
||||
The given network error from the `HttpRequestManager` is logged, and the job is marked as failed.
|
||||
:param reply: The main reply of the server. This reply will most likely not be valid.
|
||||
:param error: The network error (Qt's enum) that occurred.
|
||||
"""
|
||||
Logger.error(f"Failed to upload material archive: {error}")
|
||||
self.failed(UploadMaterialsError(catalog.i18nc("@text:error", "Failed to connect to Digital Factory.")))
|
||||
|
||||
def getPrinterSyncStatus(self) -> Dict[str, str]:
|
||||
"""
|
||||
For each printer, identified by host_guid, this gives the current status of uploading the material archive.
|
||||
|
||||
The possible states are given in the PrinterStatus enum.
|
||||
:return: A dictionary with printer host_guids as keys, and their status as values.
|
||||
"""
|
||||
return self._printer_sync_status
|
||||
|
||||
def failed(self, error: UploadMaterialsError) -> None:
|
||||
"""
|
||||
Helper function for when we have a general failure.
|
||||
|
||||
This sets the sync status for all printers to failed, sets the error on
|
||||
the job and the result of the job to FAILED.
|
||||
:param error: An error to show to the user.
|
||||
"""
|
||||
self.setResult(self.Result.FAILED)
|
||||
self.setError(error)
|
||||
for printer_id in self._printer_sync_status:
|
||||
self._printer_sync_status[printer_id] = self.PrinterStatus.FAILED.value
|
||||
self.uploadProgressChanged.emit(1.0, self.getPrinterSyncStatus())
|
||||
self.uploadCompleted.emit(self.getResult(), self.getError())
|
||||
|
||||
def _onProcessProgressChanged(self, progress: float) -> None:
|
||||
"""
|
||||
When we progress in the process of uploading materials, we not only signal the new progress (float from 0 to 1)
|
||||
but we also signal the current status of every printer. These are emitted as the two parameters of the signal.
|
||||
:param progress: The progress of this job, between 0 and 1.
|
||||
"""
|
||||
self.uploadProgressChanged.emit(progress * 0.8, self.getPrinterSyncStatus()) # The processing is 80% of the progress bar.
|
200
cura/UltimakerCloud/CloudMaterialSync.py
Normal file
200
cura/UltimakerCloud/CloudMaterialSync.py
Normal file
@ -0,0 +1,200 @@
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from typing import Dict, Optional, TYPE_CHECKING
|
||||
import zipfile # To export all materials in a .zip archive.
|
||||
|
||||
import cura.CuraApplication # Imported like this to prevent circular imports.
|
||||
from cura.PrinterOutput.UploadMaterialsJob import UploadMaterialsJob, UploadMaterialsError # To export materials to the output printer.
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Signal import Signal
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
class CloudMaterialSync(QObject):
|
||||
"""
|
||||
Handles the synchronisation of material profiles with cloud accounts.
|
||||
"""
|
||||
|
||||
def __init__(self, parent: QObject = None):
|
||||
super().__init__(parent)
|
||||
self.sync_all_dialog = None # type: Optional[QObject]
|
||||
self._export_upload_status = "idle"
|
||||
self._checkIfNewMaterialsWereInstalled()
|
||||
self._export_progress = 0.0
|
||||
self._printer_status = {} # type: Dict[str, str]
|
||||
|
||||
def _checkIfNewMaterialsWereInstalled(self) -> None:
|
||||
"""
|
||||
Checks whether new material packages were installed in the latest startup. If there were, then it shows
|
||||
a message prompting the user to sync the materials with their printers.
|
||||
"""
|
||||
application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
for package_id, package_data in application.getPackageManager().getPackagesInstalledOnStartup().items():
|
||||
if package_data["package_info"]["package_type"] == "material":
|
||||
# At least one new material was installed
|
||||
self._showSyncNewMaterialsMessage()
|
||||
break
|
||||
|
||||
def _showSyncNewMaterialsMessage(self) -> None:
|
||||
sync_materials_message = Message(
|
||||
text = catalog.i18nc("@action:button",
|
||||
"Please sync the material profiles with your printers before starting to print."),
|
||||
title = catalog.i18nc("@action:button", "New materials installed"),
|
||||
message_type = Message.MessageType.WARNING,
|
||||
lifetime = 0
|
||||
)
|
||||
|
||||
sync_materials_message.addAction(
|
||||
"sync",
|
||||
name = catalog.i18nc("@action:button", "Sync materials with printers"),
|
||||
icon = "",
|
||||
description = "Sync your newly installed materials with your printers.",
|
||||
button_align = Message.ActionButtonAlignment.ALIGN_RIGHT
|
||||
)
|
||||
|
||||
sync_materials_message.addAction(
|
||||
"learn_more",
|
||||
name = catalog.i18nc("@action:button", "Learn more"),
|
||||
icon = "",
|
||||
description = "Learn more about syncing your newly installed materials with your printers.",
|
||||
button_align = Message.ActionButtonAlignment.ALIGN_LEFT,
|
||||
button_style = Message.ActionButtonStyle.LINK
|
||||
)
|
||||
sync_materials_message.actionTriggered.connect(self._onSyncMaterialsMessageActionTriggered)
|
||||
|
||||
# Show the message only if there are printers that support material export
|
||||
container_registry = cura.CuraApplication.CuraApplication.getInstance().getContainerRegistry()
|
||||
global_stacks = container_registry.findContainerStacks(type = "machine")
|
||||
if any([stack.supportsMaterialExport for stack in global_stacks]):
|
||||
sync_materials_message.show()
|
||||
|
||||
def _onSyncMaterialsMessageActionTriggered(self, sync_message: Message, sync_message_action: str):
|
||||
if sync_message_action == "sync":
|
||||
self.openSyncAllWindow()
|
||||
sync_message.hide()
|
||||
elif sync_message_action == "learn_more":
|
||||
QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360013137919?utm_source=cura&utm_medium=software&utm_campaign=sync-material-printer-message"))
|
||||
|
||||
@pyqtSlot(result = QUrl)
|
||||
def getPreferredExportAllPath(self) -> QUrl:
|
||||
"""
|
||||
Get the preferred path to export materials to.
|
||||
|
||||
If there is a removable drive, that should be the preferred path. Otherwise it should be the most recent local
|
||||
file path.
|
||||
:return: The preferred path to export all materials to.
|
||||
"""
|
||||
cura_application = cura.CuraApplication.CuraApplication.getInstance()
|
||||
device_manager = cura_application.getOutputDeviceManager()
|
||||
devices = device_manager.getOutputDevices()
|
||||
for device in devices:
|
||||
if device.__class__.__name__ == "RemovableDriveOutputDevice":
|
||||
return QUrl.fromLocalFile(device.getId())
|
||||
else: # No removable drives? Use local path.
|
||||
return cura_application.getDefaultPath("dialog_material_path")
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def exportAll(self, file_path: QUrl, notify_progress: Optional["Signal"] = None) -> None:
|
||||
"""
|
||||
Export all materials to a certain file path.
|
||||
:param file_path: The path to export the materials to.
|
||||
"""
|
||||
registry = CuraContainerRegistry.getInstance()
|
||||
|
||||
# Create empty archive.
|
||||
try:
|
||||
archive = zipfile.ZipFile(file_path.toLocalFile(), "w", compression = zipfile.ZIP_DEFLATED)
|
||||
except OSError as e:
|
||||
Logger.log("e", f"Can't write to destination {file_path.toLocalFile()}: {type(e)} - {str(e)}")
|
||||
error_message = Message(
|
||||
text = catalog.i18nc("@message:text", "Could not save material archive to {}:").format(file_path.toLocalFile()) + " " + str(e),
|
||||
title = catalog.i18nc("@message:title", "Failed to save material archive"),
|
||||
message_type = Message.MessageType.ERROR
|
||||
)
|
||||
error_message.show()
|
||||
return
|
||||
|
||||
materials_metadata = registry.findInstanceContainersMetadata(type = "material")
|
||||
for index, metadata in enumerate(materials_metadata):
|
||||
if notify_progress is not None:
|
||||
progress = index / len(materials_metadata)
|
||||
notify_progress.emit(progress)
|
||||
if metadata["base_file"] != metadata["id"]: # Only process base files.
|
||||
continue
|
||||
if metadata["id"] == "empty_material": # Don't export the empty material.
|
||||
continue
|
||||
material = registry.findContainers(id = metadata["id"])[0]
|
||||
suffix = registry.getMimeTypeForContainer(type(material)).preferredSuffix
|
||||
filename = metadata["id"] + "." + suffix
|
||||
try:
|
||||
archive.writestr(filename, material.serialize())
|
||||
except OSError as e:
|
||||
Logger.log("e", f"An error has occurred while writing the material \'{metadata['id']}\' in the file \'{filename}\': {e}.")
|
||||
|
||||
exportUploadStatusChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(str, notify = exportUploadStatusChanged)
|
||||
def exportUploadStatus(self) -> str:
|
||||
return self._export_upload_status
|
||||
|
||||
@pyqtSlot()
|
||||
def exportUpload(self) -> None:
|
||||
"""
|
||||
Export all materials and upload them to the user's account.
|
||||
"""
|
||||
self._export_upload_status = "uploading"
|
||||
self.exportUploadStatusChanged.emit()
|
||||
job = UploadMaterialsJob(self)
|
||||
job.uploadProgressChanged.connect(self._onUploadProgressChanged)
|
||||
job.uploadCompleted.connect(self.exportUploadCompleted)
|
||||
job.start()
|
||||
|
||||
def _onUploadProgressChanged(self, progress: float, printers_status: Dict[str, str]):
|
||||
self.setExportProgress(progress)
|
||||
self.setPrinterStatus(printers_status)
|
||||
|
||||
def exportUploadCompleted(self, job_result: UploadMaterialsJob.Result, job_error: Optional[Exception]):
|
||||
if not self.sync_all_dialog: # Shouldn't get triggered before the dialog is open, but better to check anyway.
|
||||
return
|
||||
if job_result == UploadMaterialsJob.Result.FAILED:
|
||||
if isinstance(job_error, UploadMaterialsError):
|
||||
self.sync_all_dialog.setProperty("syncStatusText", catalog.i18nc("@text", "Error sending materials to the Digital Factory:") + " " + str(job_error))
|
||||
else: # Could be "None"
|
||||
self.sync_all_dialog.setProperty("syncStatusText", catalog.i18nc("@text", "Unknown error."))
|
||||
self._export_upload_status = "error"
|
||||
else:
|
||||
self._export_upload_status = "success"
|
||||
self.exportUploadStatusChanged.emit()
|
||||
|
||||
exportProgressChanged = pyqtSignal(float)
|
||||
|
||||
def setExportProgress(self, progress: float) -> None:
|
||||
self._export_progress = progress
|
||||
self.exportProgressChanged.emit(self._export_progress)
|
||||
|
||||
@pyqtProperty(float, fset = setExportProgress, notify = exportProgressChanged)
|
||||
def exportProgress(self) -> float:
|
||||
return self._export_progress
|
||||
|
||||
printerStatusChanged = pyqtSignal()
|
||||
|
||||
def setPrinterStatus(self, new_status: Dict[str, str]) -> None:
|
||||
self._printer_status = new_status
|
||||
self.printerStatusChanged.emit()
|
||||
|
||||
@pyqtProperty("QVariantMap", fset = setPrinterStatus, notify = printerStatusChanged)
|
||||
def printerStatus(self) -> Dict[str, str]:
|
||||
return self._printer_status
|
||||
|
||||
def reset(self) -> None:
|
||||
self.setPrinterStatus({})
|
||||
self.setExportProgress(0.0)
|
||||
self._export_upload_status = "idle"
|
||||
self.exportUploadStatusChanged.emit()
|
@ -1,9 +1,15 @@
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkRequest
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.TaskManagement.HttpRequestScope import DefaultUserAgentScope
|
||||
from cura.API import Account
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.API.Account import Account
|
||||
|
||||
|
||||
class UltimakerCloudScope(DefaultUserAgentScope):
|
||||
@ -12,7 +18,7 @@ class UltimakerCloudScope(DefaultUserAgentScope):
|
||||
Also add the user agent headers (see DefaultUserAgentScope).
|
||||
"""
|
||||
|
||||
def __init__(self, application: CuraApplication):
|
||||
def __init__(self, application: "CuraApplication"):
|
||||
super().__init__(application)
|
||||
api = application.getCuraAPI()
|
||||
self._account = api.account # type: Account
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
@ -16,6 +16,7 @@ from UM.Util import parseBool
|
||||
from cura.API import Account
|
||||
from cura.API.Account import SyncState
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To update printer metadata with information received about cloud printers.
|
||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from cura.UltimakerCloud.UltimakerCloudConstants import META_UM_LINKED_TO_ACCOUNT
|
||||
@ -129,6 +130,8 @@ class CloudOutputDeviceManager:
|
||||
self._um_cloud_printers[device_id].setMetaDataEntry(META_UM_LINKED_TO_ACCOUNT, True)
|
||||
self._onDevicesDiscovered(new_clusters)
|
||||
|
||||
self._updateOnlinePrinters(all_clusters)
|
||||
|
||||
# Hide the current removed_printers_message, if there is any
|
||||
if self._removed_printers_message:
|
||||
self._removed_printers_message.actionTriggered.disconnect(self._onRemovedPrintersMessageActionTriggered)
|
||||
@ -154,6 +157,8 @@ class CloudOutputDeviceManager:
|
||||
self._syncing = False
|
||||
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS)
|
||||
|
||||
Logger.debug("Synced cloud printers with account.")
|
||||
|
||||
def _onGetRemoteClusterFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
|
||||
self._syncing = False
|
||||
self._account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
|
||||
@ -255,6 +260,16 @@ class CloudOutputDeviceManager:
|
||||
message_text = self.i18n_catalog.i18nc("info:status", "Printers added from Digital Factory:") + "<ul>" + device_names + "</ul>"
|
||||
message.setText(message_text)
|
||||
|
||||
def _updateOnlinePrinters(self, printer_responses: Dict[str, CloudClusterResponse]) -> None:
|
||||
"""
|
||||
Update the metadata of the printers to store whether they are online or not.
|
||||
:param printer_responses: The responses received from the API about the printer statuses.
|
||||
"""
|
||||
for container_stack in CuraContainerRegistry.getInstance().findContainerStacks(type = "machine"):
|
||||
cluster_id = container_stack.getMetaDataEntry("um_cloud_cluster_id", "")
|
||||
if cluster_id in printer_responses:
|
||||
container_stack.setMetaDataEntry("is_online", printer_responses[cluster_id].is_online)
|
||||
|
||||
def _updateOutdatedMachine(self, outdated_machine: GlobalStack, new_cloud_output_device: CloudOutputDevice) -> None:
|
||||
"""
|
||||
Update the cloud metadata of a pre-existing machine that is rediscovered (e.g. if the printer was removed and
|
||||
|
@ -3,6 +3,7 @@
|
||||
|
||||
import configparser
|
||||
import io
|
||||
import json
|
||||
import os.path
|
||||
from typing import List, Tuple
|
||||
|
||||
@ -49,6 +50,28 @@ class VersionUpgrade411to412(VersionUpgrade):
|
||||
# Update version number.
|
||||
parser["metadata"]["setting_version"] = "19"
|
||||
|
||||
# If the account scope in 4.11 is outdated, delete it so that the user is enforced to log in again and get the
|
||||
# correct permissions.
|
||||
new_scopes = {"account.user.read",
|
||||
"drive.backup.read",
|
||||
"drive.backup.write",
|
||||
"packages.download",
|
||||
"packages.rating.read",
|
||||
"packages.rating.write",
|
||||
"connect.cluster.read",
|
||||
"connect.cluster.write",
|
||||
"library.project.read",
|
||||
"library.project.write",
|
||||
"cura.printjob.read",
|
||||
"cura.printjob.write",
|
||||
"cura.mesh.read",
|
||||
"cura.mesh.write",
|
||||
"cura.material.write"}
|
||||
if "ultimaker_auth_data" in parser["general"]:
|
||||
ultimaker_auth_data = json.loads(parser["general"]["ultimaker_auth_data"])
|
||||
if new_scopes - set(ultimaker_auth_data["scope"].split(" ")):
|
||||
parser["general"]["ultimaker_auth_data"] = "{}"
|
||||
|
||||
result = io.StringIO()
|
||||
parser.write(result)
|
||||
return [filename], [result.getvalue()]
|
||||
|
@ -201,8 +201,7 @@ Item
|
||||
onClicked:
|
||||
{
|
||||
forceActiveFocus();
|
||||
exportAllMaterialsDialog.folder = base.materialManagementModel.getPreferredExportAllPath();
|
||||
exportAllMaterialsDialog.open();
|
||||
base.materialManagementModel.openSyncAllWindow();
|
||||
}
|
||||
visible: Cura.MachineManager.activeMachine.supportsMaterialExport
|
||||
}
|
||||
@ -383,19 +382,6 @@ Item
|
||||
}
|
||||
}
|
||||
|
||||
FileDialog
|
||||
{
|
||||
id: exportAllMaterialsDialog
|
||||
title: catalog.i18nc("@title:window", "Export All Materials")
|
||||
selectExisting: false
|
||||
nameFilters: ["Material archives (*.umm)", "All files (*)"]
|
||||
onAccepted:
|
||||
{
|
||||
base.materialManagementModel.exportAll(fileUrl);
|
||||
CuraApplication.setDefaultPath("dialog_material_path", folder);
|
||||
}
|
||||
}
|
||||
|
||||
MessageDialog
|
||||
{
|
||||
id: messageDialog
|
||||
|
724
resources/qml/Preferences/Materials/MaterialsSyncDialog.qml
Normal file
724
resources/qml/Preferences/Materials/MaterialsSyncDialog.qml
Normal file
@ -0,0 +1,724 @@
|
||||
//Copyright (c) 2021 Ultimaker B.V.
|
||||
//Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Dialogs 1.2
|
||||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Window 2.1
|
||||
import Cura 1.1 as Cura
|
||||
import UM 1.4 as UM
|
||||
|
||||
Window
|
||||
{
|
||||
id: materialsSyncDialog
|
||||
property variant catalog: UM.I18nCatalog { name: "cura" }
|
||||
|
||||
title: catalog.i18nc("@title:window", "Sync materials with printers")
|
||||
minimumWidth: UM.Theme.getSize("modal_window_minimum").width
|
||||
minimumHeight: UM.Theme.getSize("modal_window_minimum").height
|
||||
width: minimumWidth
|
||||
height: minimumHeight
|
||||
modality: Qt.ApplicationModal
|
||||
|
||||
property variant syncModel
|
||||
property alias pageIndex: swipeView.currentIndex
|
||||
property alias syncStatusText: syncStatusLabel.text
|
||||
|
||||
SwipeView
|
||||
{
|
||||
id: swipeView
|
||||
anchors.fill: parent
|
||||
interactive: false
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: introPage
|
||||
color: UM.Theme.getColor("main_background")
|
||||
Column
|
||||
{
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
anchors.fill: parent
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@title:header", "Sync materials with printers")
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@text", "Following a few simple steps, you will be able to synchronize all your material profiles with your printers.")
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width
|
||||
}
|
||||
Image
|
||||
{
|
||||
source: UM.Theme.getImage("material_ecosystem")
|
||||
width: parent.width
|
||||
sourceSize.width: width
|
||||
}
|
||||
}
|
||||
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: startButton
|
||||
anchors
|
||||
{
|
||||
right: parent.right
|
||||
rightMargin: UM.Theme.getSize("default_margin").width
|
||||
bottom: parent.bottom
|
||||
bottomMargin: UM.Theme.getSize("default_margin").height
|
||||
}
|
||||
text: catalog.i18nc("@button", "Start")
|
||||
onClicked:
|
||||
{
|
||||
if(Cura.API.account.isLoggedIn)
|
||||
{
|
||||
swipeView.currentIndex += 2; //Skip sign in page.
|
||||
}
|
||||
else
|
||||
{
|
||||
swipeView.currentIndex += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
anchors
|
||||
{
|
||||
left: parent.left
|
||||
leftMargin: UM.Theme.getSize("default_margin").width
|
||||
verticalCenter: startButton.verticalCenter
|
||||
}
|
||||
text: catalog.i18nc("@button", "Why do I need to sync material profiles?")
|
||||
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||
isIconOnRightSide: true
|
||||
onClicked: Qt.openUrlExternally("https://support.ultimaker.com/hc/en-us/articles/360013137919?utm_source=cura&utm_medium=software&utm_campaign=sync-material-printer-why")
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: signinPage
|
||||
color: UM.Theme.getColor("main_background")
|
||||
|
||||
Connections //While this page is active, continue to the next page if the user logs in.
|
||||
{
|
||||
target: Cura.API.account
|
||||
function onLoginStateChanged(is_logged_in)
|
||||
{
|
||||
if(is_logged_in && signinPage.SwipeView.isCurrentItem)
|
||||
{
|
||||
swipeView.currentIndex += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout
|
||||
{
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
anchors.fill: parent
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@title:header", "Sign in")
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
Layout.preferredHeight: height
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@text", "To automatically sync the material profiles with all your printers connected to Digital Factory you need to be signed in in Cura.")
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width
|
||||
Layout.maximumWidth: width
|
||||
Layout.preferredHeight: height
|
||||
}
|
||||
Item
|
||||
{
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.fillHeight: true
|
||||
Image
|
||||
{
|
||||
source: UM.Theme.getImage("first_run_ultimaker_cloud")
|
||||
width: parent.width / 2
|
||||
sourceSize.width: width
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
Layout.preferredHeight: height
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
anchors.left: parent.left
|
||||
text: catalog.i18nc("@button", "Sync materials with USB")
|
||||
onClicked: swipeView.currentIndex = removableDriveSyncPage.SwipeView.index
|
||||
}
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
anchors.right: parent.right
|
||||
text: catalog.i18nc("@button", "Sign in")
|
||||
onClicked: Cura.API.account.login()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: printerListPage
|
||||
color: UM.Theme.getColor("main_background")
|
||||
|
||||
ColumnLayout
|
||||
{
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
anchors.fill: parent
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
visible: cloudPrinterList.count > 0
|
||||
|
||||
Row
|
||||
{
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
|
||||
states: [
|
||||
State
|
||||
{
|
||||
name: "idle"
|
||||
when: typeof syncModel === "undefined" || syncModel.exportUploadStatus == "idle" || syncModel.exportUploadStatus == "uploading"
|
||||
PropertyChanges { target: printerListHeader; text: catalog.i18nc("@title:header", "The following printers will receive the new material profiles:") }
|
||||
PropertyChanges { target: printerListHeaderIcon; status: UM.StatusIcon.Status.NEUTRAL }
|
||||
},
|
||||
State
|
||||
{
|
||||
name: "error"
|
||||
when: typeof syncModel !== "undefined" && syncModel.exportUploadStatus == "error"
|
||||
PropertyChanges { target: printerListHeader; text: catalog.i18nc("@title:header", "Something went wrong when sending the materials to the printers.") }
|
||||
PropertyChanges { target: printerListHeaderIcon; status: UM.StatusIcon.Status.ERROR }
|
||||
},
|
||||
State
|
||||
{
|
||||
name: "success"
|
||||
when: typeof syncModel !== "undefined" && syncModel.exportUploadStatus == "success"
|
||||
PropertyChanges { target: printerListHeader; text: catalog.i18nc("@title:header", "Material profiles successfully synced with the following printers:") }
|
||||
PropertyChanges { target: printerListHeaderIcon; status: UM.StatusIcon.Status.POSITIVE }
|
||||
}
|
||||
]
|
||||
|
||||
UM.StatusIcon
|
||||
{
|
||||
id: printerListHeaderIcon
|
||||
width: UM.Theme.getSize("section_icon").width
|
||||
height: width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
Label
|
||||
{
|
||||
id: printerListHeader
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
//Text is always defined by the states above.
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
}
|
||||
Row
|
||||
{
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
|
||||
Label
|
||||
{
|
||||
id: syncStatusLabel
|
||||
|
||||
width: parent.width - UM.Theme.getSize("default_margin").width - troubleshootingLink.width
|
||||
anchors.verticalCenter: troubleshootingLink.verticalCenter
|
||||
|
||||
elide: Text.ElideRight
|
||||
visible: text !== ""
|
||||
text: ""
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("medium")
|
||||
}
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
id: troubleshootingLink
|
||||
text: catalog.i18nc("@button", "Troubleshooting")
|
||||
visible: typeof syncModel !== "undefined" && syncModel.exportUploadStatus == "error"
|
||||
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||
Layout.preferredHeight: height
|
||||
onClicked: Qt.openUrlExternally("https://support.ultimaker.com/hc/en-us/articles/360012019239?utm_source=cura&utm_medium=software&utm_campaign=sync-material-wizard-troubleshoot-cloud-printer")
|
||||
}
|
||||
}
|
||||
ScrollView
|
||||
{
|
||||
id: printerListScrollView
|
||||
width: parent.width
|
||||
Layout.preferredWidth: width
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
|
||||
|
||||
ListView
|
||||
{
|
||||
id: printerList
|
||||
width: parent.width
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
|
||||
model: cloudPrinterList
|
||||
delegate: Rectangle
|
||||
{
|
||||
id: delegateContainer
|
||||
border.color: UM.Theme.getColor("lining")
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
width: printerListScrollView.width
|
||||
height: UM.Theme.getSize("card").height
|
||||
|
||||
property string syncStatus:
|
||||
{
|
||||
var printer_id = model.metadata["host_guid"]
|
||||
if(syncModel.printerStatus[printer_id] === undefined) //No status information available. Could be added after we started syncing.
|
||||
{
|
||||
return "idle";
|
||||
}
|
||||
return syncModel.printerStatus[printer_id];
|
||||
}
|
||||
|
||||
Cura.IconWithText
|
||||
{
|
||||
anchors
|
||||
{
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
leftMargin: Math.round(parent.height - height) / 2 //Equal margin on the left as above and below.
|
||||
right: parent.right
|
||||
rightMargin: Math.round(parent.height - height) / 2
|
||||
}
|
||||
|
||||
text: model.name
|
||||
font: UM.Theme.getFont("medium")
|
||||
|
||||
source: UM.Theme.getIcon("Printer", "medium")
|
||||
iconColor: UM.Theme.getColor("machine_selector_printer_icon")
|
||||
iconSize: UM.Theme.getSize("machine_selector_icon").width
|
||||
|
||||
//Printer status badge (always cloud, but whether it's online or offline).
|
||||
UM.RecolorImage
|
||||
{
|
||||
width: UM.Theme.getSize("printer_status_icon").width
|
||||
height: UM.Theme.getSize("printer_status_icon").height
|
||||
anchors
|
||||
{
|
||||
bottom: parent.bottom
|
||||
bottomMargin: -Math.round(height / 6)
|
||||
left: parent.left
|
||||
leftMargin: parent.iconSize - Math.round(width * 5 / 6)
|
||||
}
|
||||
|
||||
source: UM.Theme.getIcon("CloudBadge", "low")
|
||||
color: UM.Theme.getColor("primary")
|
||||
|
||||
//Make a themeable circle in the background so we can change it in other themes.
|
||||
Rectangle
|
||||
{
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 1.5 //1.5 pixels smaller (at least sqrt(2), regardless of pixel scale) so that the circle doesn't show up behind the icon due to anti-aliasing.
|
||||
height: parent.height - 1.5
|
||||
radius: width / 2
|
||||
color: UM.Theme.getColor("connection_badge_background")
|
||||
z: parent.z - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: printerSpinner
|
||||
width: UM.Theme.getSize("section_icon").width
|
||||
height: width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Math.round((parent.height - height) / 2) //Same margin on the right as above and below.
|
||||
|
||||
visible: delegateContainer.syncStatus === "uploading"
|
||||
source: UM.Theme.getIcon("ArrowDoubleCircleRight")
|
||||
color: UM.Theme.getColor("primary")
|
||||
|
||||
RotationAnimator
|
||||
{
|
||||
target: printerSpinner
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
running: true
|
||||
}
|
||||
}
|
||||
UM.StatusIcon
|
||||
{
|
||||
width: UM.Theme.getSize("section_icon").width
|
||||
height: width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Math.round((parent.height - height) / 2) //Same margin on the right as above and below.
|
||||
|
||||
visible: delegateContainer.syncStatus === "failed" || delegateContainer.syncStatus === "success"
|
||||
status: delegateContainer.syncStatus === "success" ? UM.StatusIcon.Status.POSITIVE : UM.StatusIcon.Status.ERROR
|
||||
}
|
||||
}
|
||||
|
||||
footer: Item
|
||||
{
|
||||
width: printerListScrollView.width
|
||||
height: visible ? UM.Theme.getSize("card").height + UM.Theme.getSize("default_margin").height : 0
|
||||
visible: includeOfflinePrinterList.count - cloudPrinterList.count > 0
|
||||
Rectangle
|
||||
{
|
||||
border.color: UM.Theme.getColor("lining")
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
anchors.fill: parent
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
|
||||
RowLayout
|
||||
{
|
||||
anchors
|
||||
{
|
||||
fill: parent
|
||||
leftMargin: (parent.height - infoIcon.height) / 2 //Same margin on the left as top and bottom.
|
||||
rightMargin: (parent.height - infoIcon.height) / 2
|
||||
}
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
|
||||
UM.StatusIcon
|
||||
{
|
||||
id: infoIcon
|
||||
width: UM.Theme.getSize("section_icon").width
|
||||
height: width
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
|
||||
status: UM.StatusIcon.Status.WARNING
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@text Asking the user whether printers are missing in a list.", "Printers missing?")
|
||||
+ "\n"
|
||||
+ catalog.i18nc("@text", "Make sure all your printers are turned ON and connected to Digital Factory.")
|
||||
font: UM.Theme.getFont("medium")
|
||||
elide: Text.ElideRight
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: refreshListButton
|
||||
text: catalog.i18nc("@button", "Refresh List")
|
||||
iconSource: UM.Theme.getIcon("ArrowDoubleCircleRight")
|
||||
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.preferredWidth: width
|
||||
|
||||
onClicked: Cura.API.account.sync(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
Layout.preferredWidth: width
|
||||
Layout.preferredHeight: height
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
anchors.left: parent.left
|
||||
text: catalog.i18nc("@button", "Sync materials with USB")
|
||||
onClicked: swipeView.currentIndex = removableDriveSyncPage.SwipeView.index
|
||||
}
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: syncButton
|
||||
anchors.right: parent.right
|
||||
text:
|
||||
{
|
||||
if(typeof syncModel !== "undefined" && syncModel.exportUploadStatus == "error")
|
||||
{
|
||||
return catalog.i18nc("@button", "Try again");
|
||||
}
|
||||
if(typeof syncModel !== "undefined" && syncModel.exportUploadStatus == "success")
|
||||
{
|
||||
return catalog.i18nc("@button", "Done");
|
||||
}
|
||||
return catalog.i18nc("@button", "Sync");
|
||||
}
|
||||
onClicked:
|
||||
{
|
||||
if(typeof syncModel !== "undefined" && syncModel.exportUploadStatus == "success")
|
||||
{
|
||||
materialsSyncDialog.close();
|
||||
}
|
||||
else
|
||||
{
|
||||
syncModel.exportUpload();
|
||||
}
|
||||
}
|
||||
visible:
|
||||
{
|
||||
if(!syncModel) //When the dialog is created, this is not set yet.
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return syncModel.exportUploadStatus != "uploading";
|
||||
}
|
||||
}
|
||||
Item
|
||||
{
|
||||
anchors.right: parent.right
|
||||
width: childrenRect.width
|
||||
height: syncButton.height
|
||||
|
||||
visible: !syncButton.visible
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: syncingIcon
|
||||
height: UM.Theme.getSize("action_button_icon").height
|
||||
width: height
|
||||
anchors.verticalCenter: syncingLabel.verticalCenter
|
||||
|
||||
source: UM.Theme.getIcon("ArrowDoubleCircleRight")
|
||||
color: UM.Theme.getColor("primary")
|
||||
|
||||
RotationAnimator
|
||||
{
|
||||
target: syncingIcon
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
running: true
|
||||
}
|
||||
}
|
||||
Label
|
||||
{
|
||||
id: syncingLabel
|
||||
anchors.left: syncingIcon.right
|
||||
anchors.leftMargin: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
text: catalog.i18nc("@button", "Syncing")
|
||||
color: UM.Theme.getColor("primary")
|
||||
font: UM.Theme.getFont("medium")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout //Placeholder for when the user has no cloud printers.
|
||||
{
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
anchors.fill: parent
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
visible: cloudPrinterList.count == 0
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@title:header", "No printers found")
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
Layout.preferredWidth: width
|
||||
Layout.preferredHeight: height
|
||||
}
|
||||
Image
|
||||
{
|
||||
source: UM.Theme.getImage("3d_printer_faded")
|
||||
sourceSize.width: width
|
||||
fillMode: Image.PreserveAspectFit
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.preferredWidth: parent.width / 3
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@text", "It seems like you don't have access to any printers connected to Digital Factory.")
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
wrapMode: Text.Wrap
|
||||
Layout.preferredWidth: width
|
||||
Layout.preferredHeight: height
|
||||
}
|
||||
Item
|
||||
{
|
||||
Layout.preferredWidth: parent.width
|
||||
Layout.fillHeight: true
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
text: catalog.i18nc("@button", "Learn how to connect your printer to Digital Factory")
|
||||
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||
onClicked: Qt.openUrlExternally("https://support.ultimaker.com/hc/en-us/articles/360012019239?utm_source=cura&utm_medium=software&utm_campaign=sync-material-wizard-add-cloud-printer")
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
}
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
Layout.preferredWidth: width
|
||||
Layout.preferredHeight: height
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
anchors.left: parent.left
|
||||
text: catalog.i18nc("@button", "Sync materials with USB")
|
||||
onClicked: swipeView.currentIndex = removableDriveSyncPage.SwipeView.index
|
||||
}
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: disabledSyncButton
|
||||
anchors.right: parent.right
|
||||
text: catalog.i18nc("@button", "Sync")
|
||||
enabled: false //If there are no printers, always disable this button.
|
||||
}
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
anchors.right: disabledSyncButton.left
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
text: catalog.i18nc("@button", "Refresh")
|
||||
iconSource: UM.Theme.getIcon("ArrowDoubleCircleRight")
|
||||
outlineColor: "transparent"
|
||||
onClicked: Cura.API.account.sync(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: removableDriveSyncPage
|
||||
color: UM.Theme.getColor("main_background")
|
||||
|
||||
ColumnLayout
|
||||
{
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
anchors.fill: parent
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@title:header", "Sync material profiles via USB")
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
Layout.preferredHeight: height
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@text In the UI this is followed by a list of steps the user needs to take.", "Follow the following steps to load the new material profiles to your printer.")
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width
|
||||
Layout.maximumWidth: width
|
||||
Layout.preferredHeight: height
|
||||
}
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
Layout.preferredWidth: width
|
||||
Layout.fillHeight: true
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
|
||||
Image
|
||||
{
|
||||
source: UM.Theme.getImage("insert_usb")
|
||||
width: parent.width / 3
|
||||
height: width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
sourceSize.width: width
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: "1. " + catalog.i18nc("@text", "Click the export material archive button.")
|
||||
+ "\n2. " + catalog.i18nc("@text", "Save the .umm file on a USB stick.")
|
||||
+ "\n3. " + catalog.i18nc("@text", "Insert the USB stick into your printer and launch the procedure to load new material profiles.")
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Text.Wrap
|
||||
width: parent.width * 2 / 3 - UM.Theme.getSize("default_margin").width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
text: catalog.i18nc("@button", "How to load new material profiles to my printer")
|
||||
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||
onClicked: Qt.openUrlExternally("https://support.ultimaker.com/hc/en-us/articles/360013137919?utm_source=cura&utm_medium=software&utm_campaign=sync-material-wizard-how-usb")
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
Layout.preferredWidth: width
|
||||
Layout.preferredHeight: height
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
anchors.left: parent.left
|
||||
text: catalog.i18nc("@button", "Back")
|
||||
onClicked: swipeView.currentIndex = 0 //Reset to first page.
|
||||
}
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
anchors.right: parent.right
|
||||
text: catalog.i18nc("@button", "Export material archive")
|
||||
onClicked:
|
||||
{
|
||||
exportUsbDialog.folder = syncModel.getPreferredExportAllPath();
|
||||
exportUsbDialog.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cura.GlobalStacksModel
|
||||
{
|
||||
id: cloudPrinterList
|
||||
filterConnectionType: 3 //Only show cloud connections.
|
||||
filterOnlineOnly: true //Only show printers that are online.
|
||||
}
|
||||
Cura.GlobalStacksModel
|
||||
{
|
||||
//In order to show a refresh button only when there are offline cloud printers, we need to know if there are any offline printers.
|
||||
//A global stacks model without the filter for online-only printers allows this.
|
||||
id: includeOfflinePrinterList
|
||||
filterConnectionType: 3 //Still only show cloud connections.
|
||||
}
|
||||
|
||||
FileDialog
|
||||
{
|
||||
id: exportUsbDialog
|
||||
title: catalog.i18nc("@title:window", "Export All Materials")
|
||||
selectExisting: false
|
||||
nameFilters: ["Material archives (*.umm)", "All files (*)"]
|
||||
onAccepted:
|
||||
{
|
||||
syncModel.exportAll(fileUrl);
|
||||
CuraApplication.setDefaultPath("dialog_material_path", folder);
|
||||
}
|
||||
}
|
||||
}
|
3
resources/themes/cura-light/icons/default/EmptyInfo.svg
Normal file
3
resources/themes/cura-light/icons/default/EmptyInfo.svg
Normal file
@ -0,0 +1,3 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M13 6H11V8H13V6ZM13 10H11V17H13V10Z" />
|
||||
</svg>
|
After Width: | Height: | Size: 140 B |
29
resources/themes/cura-light/images/3d_printer_faded.svg
Normal file
29
resources/themes/cura-light/images/3d_printer_faded.svg
Normal file
@ -0,0 +1,29 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 895.1194 1107.2273">
|
||||
<g transform="scale(4.2)">
|
||||
<path d="M47.1391,70.7713S77.3619-1.4269,95.1241,2.127,104.66,69.6252,104.66,69.6252" style="fill:none;stroke:#b4b4b4;stroke-linecap:round;stroke-miterlimit:10;stroke-width:4px"/>
|
||||
<path d="M165.9845,80.7713S135.7618,8.5731,118,12.127s-9.5354,67.4982-9.5354,67.4982" style="fill:none;stroke:#b4b4b4;stroke-linecap:round;stroke-miterlimit:10;stroke-width:4px"/>
|
||||
<path d="M185.3682,262.6255l-.2889-.2722c-.0522-.0487-5.4914-5.0249-15.1015-5.0249H42.9639a19.5359,19.5359,0,0,0-13.2,4.9922l-.2946.3034-8.0293.0015a4.1443,4.1443,0,0,1-4.14-4.14V73.911a4.1443,4.1443,0,0,1,4.14-4.14H191.6833a4.1443,4.1443,0,0,1,4.14,4.14V258.4859a4.1443,4.1443,0,0,1-4.14,4.14Z" style="fill:#f3f8fe"/>
|
||||
<path d="M191.6834,70.7714a3.14,3.14,0,0,1,3.14,3.14V258.4857a3.14,3.14,0,0,1-3.14,3.14H185.765s-5.6206-5.2969-15.7872-5.2969H42.9637a20.3309,20.3309,0,0,0-13.9185,5.2969H21.44a3.14,3.14,0,0,1-3.14-3.14V73.911a3.14,3.14,0,0,1,3.14-3.14H191.6834m0-2H21.44a5.1455,5.1455,0,0,0-5.14,5.14V258.4857a5.1456,5.1456,0,0,0,5.14,5.14h8.4555l.59-.6128a18.5748,18.5748,0,0,1,12.4776-4.6841H169.9778c9.1729,0,14.3707,4.711,14.4219,4.7588l.5752.5317.79.0064h5.9184a5.1456,5.1456,0,0,0,5.14-5.14V73.911a5.1455,5.1455,0,0,0-5.14-5.14Z" style="fill:#b4b4b4"/>
|
||||
<path d="M41.31,230.6177a11.6933,11.6933,0,0,1-11.68-11.68V91.084a1.99,1.99,0,0,1,1.9872-1.9872h149.89a1.9894,1.9894,0,0,1,1.9871,1.9872V218.9377a11.6932,11.6932,0,0,1-11.68,11.68Z" style="fill:#fff"/>
|
||||
<path d="M181.5067,90.0971a.9882.9882,0,0,1,.9873.9868v127.854a10.6921,10.6921,0,0,1-10.68,10.68H41.31a10.692,10.692,0,0,1-10.68-10.68V91.0839a.9882.9882,0,0,1,.9873-.9868h149.89m0-2H31.617A2.9868,2.9868,0,0,0,28.63,91.0839v127.854a12.68,12.68,0,0,0,12.68,12.68H171.8138a12.68,12.68,0,0,0,12.68-12.68V91.0839a2.9868,2.9868,0,0,0-2.9873-2.9868Z" style="fill:#fbfbfb"/>
|
||||
<path d="M181.5067,88.0971a2.9868,2.9868,0,0,1,2.9873,2.9868v127.854a12.68,12.68,0,0,1-12.68,12.68H41.31a12.68,12.68,0,0,1-12.68-12.68V91.0839a2.9868,2.9868,0,0,1,2.9873-2.9868h149.89m0-2H31.617A4.9929,4.9929,0,0,0,26.63,91.0839v127.854a14.6967,14.6967,0,0,0,14.68,14.68H171.8138a14.6967,14.6967,0,0,0,14.68-14.68V91.0839a4.9929,4.9929,0,0,0-4.9873-4.9868Z" style="fill:#b4b4b4"/>
|
||||
<circle cx="180.2767" cy="238.787" r="1.6497" style="fill:#b4b4b4"/>
|
||||
<circle cx="32.4163" cy="238.787" r="1.6497" style="fill:#b4b4b4"/>
|
||||
<circle cx="180.3485" cy="74.2255" r="1.6497" style="fill:#b4b4b4"/>
|
||||
<circle cx="132.403" cy="74.2255" r="1.6497" style="fill:#b4b4b4"/>
|
||||
<circle cx="81.7859" cy="74.2255" r="1.6497" style="fill:#b4b4b4"/>
|
||||
<circle cx="32.4881" cy="74.2255" r="1.6497" style="fill:#b4b4b4"/>
|
||||
<rect x="53.3813" y="242.7051" width="7.0114" height="2.4746" style="fill:#b4b4b4"/>
|
||||
<polygon points="106.562 146.818 77.797 157.287 62.492 183.796 67.808 213.942 91.257 233.618 121.867 233.618 145.316 213.942 150.631 183.796 135.326 157.287 106.562 146.818" style="fill:#e5e5e5"/>
|
||||
<rect x="81.3571" y="225.2077" width="50.4095" height="26.4368" rx="2.3351" style="fill:#fff"/>
|
||||
<path d="M129.4315,226.2079a1.3349,1.3349,0,0,1,1.3349,1.335V249.31a1.3348,1.3348,0,0,1-1.3349,1.3349H83.6922a1.3349,1.3349,0,0,1-1.335-1.3349V227.5429a1.335,1.335,0,0,1,1.335-1.335h45.7393m0-2H83.6922a3.3388,3.3388,0,0,0-3.335,3.335V249.31a3.3388,3.3388,0,0,0,3.335,3.3349h45.7393a3.3388,3.3388,0,0,0,3.3349-3.3349V227.5429a3.3388,3.3388,0,0,0-3.3349-3.335Z" style="fill:#b4b4b4"/>
|
||||
<path d="M128.7664,228.2079v20.4365H84.3572V228.2079h44.4092m.6651-2H83.6922a1.335,1.335,0,0,0-1.335,1.335V249.31a1.3349,1.3349,0,0,0,1.335,1.3349h45.7393a1.3348,1.3348,0,0,0,1.3349-1.3349V227.5429a1.3349,1.3349,0,0,0-1.3349-1.335Z" style="fill:#ececec"/>
|
||||
<line x1="1" y1="262.6255" x2="212.1237" y2="262.6255" style="fill:none;stroke:#b4b4b4;stroke-linecap:round;stroke-miterlimit:10;stroke-width:2px"/>
|
||||
<path d="M95.6985,110.2825a3.674,3.674,0,0,1-3.4684-2.4677l-4.372-11.9761a3.7037,3.7037,0,0,1-.2253-1.274v-4.61h37.8627l-.0219,4.75a3.7031,3.7031,0,0,1-.2314,1.2739l-4.4035,11.8829a3.6769,3.6769,0,0,1-3.4517,2.4207Z" style="fill:#fff"/>
|
||||
<path d="M124.491,90.955l-.0175,3.7456a2.7135,2.7135,0,0,1-.169.9306l-4.4033,11.8833a2.6721,2.6721,0,0,1-2.5142,1.7681H95.6986a2.6709,2.6709,0,0,1-2.5235-1.7954L88.7977,95.496a2.7054,2.7054,0,0,1-.1651-.9312v-3.61H124.491m2.0093-2H86.6326v5.61a4.6967,4.6967,0,0,0,.2871,1.62l4.3765,11.9887a4.6634,4.6634,0,0,0,4.4024,3.1094H117.387a4.6825,4.6825,0,0,0,4.396-3.0913l4.397-11.8652a4.6957,4.6957,0,0,0,.2935-1.6162l.0175-3.7456L126.5,88.955Z" style="fill:#b4b4b4"/>
|
||||
<path d="M99.383,112.1642a2.1419,2.1419,0,0,1-2.1381-2.2687L98.07,95.9931a1.9542,1.9542,0,0,1,1.9486-1.8365h13.0875a1.9543,1.9543,0,0,1,1.9486,1.8365l.8246,13.9024a2.1419,2.1419,0,0,1-2.1381,2.2687Z" style="fill:#fff"/>
|
||||
<path d="M113.1058,95.1566a.9518.9518,0,0,1,.95.8955l.8247,13.9024a1.1422,1.1422,0,0,1-1.14,1.21H99.3831a1.1422,1.1422,0,0,1-1.14-1.21l.8247-13.9024a.9518.9518,0,0,1,.95-.8955h13.0879m0-2H100.0179a2.9552,2.9552,0,0,0-2.9468,2.7774l-.8247,13.9018a3.1421,3.1421,0,0,0,3.1367,3.3282h14.3574a3.142,3.142,0,0,0,3.1368-3.3267l-.8247-13.9038a2.9552,2.9552,0,0,0-2.9468-2.7769Z" style="fill:#b4b4b4"/>
|
||||
<path d="M85.9586,94.6615a5.0986,5.0986,0,1,1,0-10.1972h41.2066a5.0986,5.0986,0,0,1,0,10.1972Z" style="fill:#fff"/>
|
||||
<path d="M127.1653,85.4643a4.0986,4.0986,0,1,1,0,8.1972H85.9588a4.0986,4.0986,0,1,1,0-8.1972h41.2065m0-2H85.9588a6.0986,6.0986,0,1,0,0,12.1972h41.2065a6.0986,6.0986,0,1,0,0-12.1972Z" style="fill:#b4b4b4"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 5.4 KiB |
33
resources/themes/cura-light/images/insert_usb.svg
Normal file
33
resources/themes/cura-light/images/insert_usb.svg
Normal file
@ -0,0 +1,33 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 312 312">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1,.cls-10{fill:#fff;}
|
||||
.cls-2,.cls-9{fill:#e8f1fe;}
|
||||
.cls-11,.cls-3,.cls-5,.cls-8{fill:none;}
|
||||
.cls-10,.cls-3,.cls-4,.cls-9{stroke:#061884;}
|
||||
.cls-10,.cls-11,.cls-3,.cls-4,.cls-8,.cls-9{stroke-miterlimit:10;}
|
||||
.cls-10,.cls-11,.cls-3,.cls-4,.cls-9{stroke-width:2px;}
|
||||
.cls-4{fill:#a3c5f9;}
|
||||
.cls-6{fill:#f7f7f7;}
|
||||
.cls-7{fill:#061884;}
|
||||
.cls-8{stroke:#fff;stroke-width:6px;}
|
||||
.cls-11{stroke:#f7f7f7;}
|
||||
</style>
|
||||
</defs>
|
||||
<circle class="cls-1" cx="156" cy="156" r="132"/>
|
||||
<path class="cls-2" d="M81.72,186.93c10.32-5.88,21.48-9.31,33.36-9.31H286.23A132.89,132.89,0,0,0,288,156c0-1.78-.05-3.55-.12-5.31H241.17a5.47,5.47,0,0,1-5.48-5.47V104.37H90.31a59.16,59.16,0,0,1-6.57-.38A53.25,53.25,0,0,1,46.33,82.52,132.23,132.23,0,0,0,30.5,197h34Zm0-54.66a5.5,5.5,0,0,1-5.5,5.5h-.84a5.5,5.5,0,0,1-5.5-5.5v-.85a5.5,5.5,0,0,1,5.5-5.5h.84a5.5,5.5,0,0,1,5.5,5.5Z"/>
|
||||
<path class="cls-2" d="M281.49,144.69h-39.8V71.62h15.8c-1.71-2.06-3.47-4.06-5.3-6h-11a5.47,5.47,0,0,0-5.48,5.47v74.13a5.47,5.47,0,0,0,5.48,5.47h46.71c-.07-2-.21-4-.38-6Z"/>
|
||||
<path class="cls-3" d="M252.19,65.62h-11a5.47,5.47,0,0,0-5.48,5.47v74.13a5.47,5.47,0,0,0,5.48,5.47h46.71"/>
|
||||
<rect class="cls-4" x="136" y="127" width="40" height="13"/>
|
||||
<path class="cls-5" d="M83.74,104a59.16,59.16,0,0,0,6.57.38H235.69V104Z"/>
|
||||
<path class="cls-6" d="M115.08,177.62c-11.88,0-23,3.43-33.36,9.31L64.46,197h-34a132,132,0,0,0,255.73-19.38Z"/>
|
||||
<path class="cls-3" d="M287.88,150.69H241.17a5.47,5.47,0,0,1-5.48-5.47V104.37H90.31a59.16,59.16,0,0,1-6.57-.38A53.25,53.25,0,0,1,46.33,82.52M30.5,197h34l17.26-10.07c10.32-5.88,21.48-9.31,33.36-9.31H286.23M81.69,132.27a5.5,5.5,0,0,1-5.5,5.5h-.84a5.5,5.5,0,0,1-5.5-5.5v-.85a5.5,5.5,0,0,1,5.5-5.5h.84a5.5,5.5,0,0,1,5.5,5.5Z"/>
|
||||
<rect class="cls-7" x="69.85" y="125.92" width="11.85" height="11.85" rx="5.5"/>
|
||||
<polyline class="cls-3" points="281.55 197.02 30.55 197.02 64.51 197.02"/>
|
||||
<path class="cls-8" d="M171.68,187.56h.95V159H139.46v28.56h1a6.48,6.48,0,0,0-6.48,6.48v75.48a6.48,6.48,0,0,0,6.48,6.48h31.26a6.48,6.48,0,0,0,6.48-6.48V194A6.48,6.48,0,0,0,171.68,187.56Z"/>
|
||||
<rect class="cls-9" x="133.94" y="187.56" width="44.22" height="88.44" rx="6.48"/>
|
||||
<rect class="cls-10" x="139.46" y="159" width="33.17" height="28.56"/>
|
||||
<rect class="cls-7" x="146.83" y="166.58" width="7.37" height="13.4"/>
|
||||
<rect class="cls-7" x="157.87" y="166.54" width="7.42" height="13.49"/>
|
||||
<circle class="cls-11" cx="156" cy="156" r="132"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.4 KiB |
89
resources/themes/cura-light/images/material_ecosystem.svg
Normal file
89
resources/themes/cura-light/images/material_ecosystem.svg
Normal file
@ -0,0 +1,89 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 335.22 139.01">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1,.cls-6{fill:#f3f8fe;}
|
||||
.cls-2{fill:#061884;}
|
||||
.cls-14,.cls-15,.cls-16,.cls-3{fill:none;}
|
||||
.cls-14,.cls-15,.cls-16,.cls-3,.cls-5,.cls-6,.cls-9{stroke:#061884;}
|
||||
.cls-11,.cls-13,.cls-14,.cls-15,.cls-3,.cls-4,.cls-5,.cls-6,.cls-9{stroke-miterlimit:10;}
|
||||
.cls-11,.cls-13,.cls-4,.cls-5,.cls-7,.cls-9{fill:#fff;}
|
||||
.cls-4{stroke:#f3f8fe;stroke-width:3px;}
|
||||
.cls-14,.cls-15,.cls-16,.cls-5{stroke-linecap:round;}
|
||||
.cls-8{fill:#c5dbfb;}
|
||||
.cls-10{fill:#dde9fd;}
|
||||
.cls-11{stroke:#e8f1fe;}
|
||||
.cls-12{fill:#e8f0fd;}
|
||||
.cls-13{stroke:#d1e2fc;}
|
||||
.cls-15{stroke-dasharray:4.9 4.9;}
|
||||
.cls-16{stroke-linejoin:round;}
|
||||
</style>
|
||||
</defs>
|
||||
<path class="cls-1" d="M194.33,117.61H98.66V118h0a4.37,4.37,0,0,0,4.37,4.37H190a4.37,4.37,0,0,0,4.37-4.37v-.35"/>
|
||||
<path class="cls-2" d="M138.41,116.94a2.69,2.69,0,0,0,2.69,2.69h10.78a2.69,2.69,0,0,0,2.7-2.69H138.41Z"/>
|
||||
<path class="cls-1" d="M186.58,117.28V63.72a2.36,2.36,0,0,0-2.36-2.35H108.77a2.36,2.36,0,0,0-2.36,2.35v53.56Z"/>
|
||||
<path class="cls-3" d="M186.58,117.28V63.72a2.36,2.36,0,0,0-2.36-2.35H108.77a2.36,2.36,0,0,0-2.36,2.35v53.56"/>
|
||||
<path class="cls-4" d="M109.49,117.32V66A1.52,1.52,0,0,1,111,64.51h71.16A1.52,1.52,0,0,1,183.68,66v51.29"/>
|
||||
<path class="cls-3" d="M109.49,117.32V66A1.52,1.52,0,0,1,111,64.51h71.16A1.52,1.52,0,0,1,183.68,66v51.29"/>
|
||||
<path class="cls-3" d="M190,122.67a4.71,4.71,0,0,0,4.7-4.71v-.35a.33.33,0,0,0-.33-.33H98.66a.33.33,0,0,0-.33.33V118a4.71,4.71,0,0,0,4.7,4.71Z"/>
|
||||
<line class="cls-5" x1="217.22" y1="123.01" x2="323.22" y2="123.01"/>
|
||||
<path class="cls-2" d="M266.49,9c-3.56,0-8,5.29-13.58,16.11-4.26,8.27-7.6,16.87-7.64,17a.79.79,0,1,0,1.48.58c5-13,14.95-33.13,20.15-32s4.92,19.66,3.56,31.08a.79.79,0,0,0,1.57.19c.38-3.17,3.46-31-4.8-32.82A3.65,3.65,0,0,0,266.49,9Z"/>
|
||||
<path class="cls-2" d="M277.67,13.75a3.64,3.64,0,0,0-.74.07c-8.26,1.79-5.18,29.65-4.8,32.82a.79.79,0,1,0,1.57-.18c-1.36-11.43-1.6-30,3.57-31.09s15.11,19,20.14,32a.79.79,0,0,0,1.48-.57c0-.09-3.38-8.68-7.64-17C285.68,19,281.23,13.75,277.67,13.75Z"/>
|
||||
<path class="cls-6" d="M309.23,122.69a2.07,2.07,0,0,0,2.06-2.06V43.25a2.11,2.11,0,0,0-2.06-2.11H232.6a2.12,2.12,0,0,0-2.06,2.11v77.38a2.08,2.08,0,0,0,2.06,2.06h5.57l.63-.36c3.28-1.83,3.63-2,8-2h47.5c4.35,0,4.69.19,8,2l.63.36Z"/>
|
||||
<rect class="cls-2" x="248.25" y="114.3" width="3.17" height="0.79"/>
|
||||
<circle class="cls-2" cx="239.54" cy="43.85" r="0.79"/>
|
||||
<circle class="cls-2" cx="260.13" cy="43.85" r="0.79"/>
|
||||
<circle class="cls-2" cx="280.71" cy="43.85" r="0.79"/>
|
||||
<circle class="cls-2" cx="301.29" cy="43.85" r="0.79"/>
|
||||
<circle class="cls-2" cx="239.54" cy="114.3" r="0.79"/>
|
||||
<circle class="cls-2" cx="301.29" cy="114.3" r="0.79"/>
|
||||
<path class="cls-7" d="M300.36,110a6.08,6.08,0,0,0,6.08-6.08V53.41a2.85,2.85,0,0,0-2.84-2.84H238a2.85,2.85,0,0,0-2.84,2.84v50.46a6.08,6.08,0,0,0,6.07,6.08Z"/>
|
||||
<path class="cls-1" d="M303.6,51H238a2.45,2.45,0,0,0-2.45,2.44v50.46a5.69,5.69,0,0,0,5.68,5.68h59.1a5.68,5.68,0,0,0,5.68-5.68V53.41A2.44,2.44,0,0,0,303.6,51m3.23,2.44v50.46h0a6.46,6.46,0,0,1-6.47,6.47h-59.1a6.47,6.47,0,0,1-6.47-6.47V53.41A3.24,3.24,0,0,1,238,50.18H303.6A3.23,3.23,0,0,1,306.83,53.41Z"/>
|
||||
<path class="cls-2" d="M303.6,50.39h0a3,3,0,0,1,3,3v50.46a6.3,6.3,0,0,1-6.27,6.27h-59.1a6.28,6.28,0,0,1-6.26-6.27V53.41a3,3,0,0,1,3-3H303.6m0-1H238a4,4,0,0,0-4,4v50.46a7.27,7.27,0,0,0,7.26,7.27h59.1a7.29,7.29,0,0,0,7.27-7.27V53.41a4,4,0,0,0-4-4Z"/>
|
||||
<path class="cls-8" d="M279.92,106.39h-19v9.5h19v-9.5m0-.8h0a.79.79,0,0,1,.77.78v9.54h0a.78.78,0,0,1-.77.77h-19a.77.77,0,0,1-.77-.77v-9.54a.77.77,0,0,1,.77-.78Z"/>
|
||||
<path class="cls-9" d="M276,58.49a1.23,1.23,0,0,0,1.18-.9l1.51-5.7V49.78H262.1v2.11l1.51,5.71a1.22,1.22,0,0,0,1.18.89Z"/>
|
||||
<path class="cls-9" d="M274.28,60.07h.14a1.24,1.24,0,0,0,1.08-1.36l-.77-6.55h-8.62l-.77,6.55a.66.66,0,0,0,0,.14,1.23,1.23,0,0,0,1.23,1.22Z"/>
|
||||
<path class="cls-9" d="M280.53,52.16a2.39,2.39,0,0,0,0-4.75H260.31a2.39,2.39,0,0,0,0,4.75h20.22Z"/>
|
||||
<polygon class="cls-10" points="279.02 80.01 261.43 80.01 252.63 95.25 261.43 110.48 279.02 110.48 287.82 95.25 279.02 80.01"/>
|
||||
<path class="cls-11" d="M260.9,106a.38.38,0,0,0-.38.38v9.54h0a.38.38,0,0,0,.38.37h19a.37.37,0,0,0,.37-.37v-9.54h0a.37.37,0,0,0-.37-.38Z"/>
|
||||
<path class="cls-2" d="M279.94,105.8a.57.57,0,0,1,.56.57v9.54a.56.56,0,0,1-.56.56h-19a.57.57,0,0,1-.57-.56v-9.54a.58.58,0,0,1,.57-.57h19m0-1h-19a1.57,1.57,0,0,0-1.57,1.57v9.54a1.56,1.56,0,0,0,1.57,1.56h19a1.56,1.56,0,0,0,1.56-1.56v-9.54a1.56,1.56,0,0,0-1.56-1.57Z"/>
|
||||
<line class="cls-9" x1="270.54" y1="47.41" x2="270.54" y2="52.07"/>
|
||||
<circle class="cls-7" cx="42.11" cy="99.88" r="21.04"/>
|
||||
<path class="cls-1" d="M42.11,80.27a19.61,19.61,0,1,0,19.6,19.61,19.65,19.65,0,0,0-19.6-19.61M64.58,99.88A22.47,22.47,0,1,1,42.11,77.41,22.47,22.47,0,0,1,64.58,99.88Z"/>
|
||||
<path class="cls-3" d="M65.05,99.88A22.95,22.95,0,1,1,42.11,76.93,22.94,22.94,0,0,1,65.05,99.88Z"/>
|
||||
<path class="cls-12" d="M45.49,108.05a8.85,8.85,0,1,0-3.38.67A8.86,8.86,0,0,0,45.49,108.05ZM43.3,97A3.11,3.11,0,0,1,45,98.69a3.11,3.11,0,0,1-4.06,4.06,3.09,3.09,0,0,1-1.68-1.69,3.1,3.1,0,0,1,1.68-4,3,3,0,0,1,1.19-.24A3.08,3.08,0,0,1,43.3,97Z"/>
|
||||
<path class="cls-2" d="M42.11,103.22a3.35,3.35,0,0,1-1.28-6.44,3.35,3.35,0,1,1,1.28,6.44m1.09-.69a2.87,2.87,0,0,0,1.56-3.75,2.83,2.83,0,0,0-1.56-1.55,2.78,2.78,0,0,0-2.19,0,2.87,2.87,0,0,0-1.55,1.55,2.82,2.82,0,0,0,0,2.19A2.86,2.86,0,0,0,41,102.53a2.76,2.76,0,0,0,1.1.21A2.71,2.71,0,0,0,43.2,102.53Z"/>
|
||||
<path class="cls-2" d="M41.15,91.27v2.39a1,1,0,1,0,1.91,0V91.27"/>
|
||||
<path class="cls-3" d="M45.49,108.05a8.85,8.85,0,1,0-3.38.67A8.86,8.86,0,0,0,45.49,108.05ZM43.3,97A3.11,3.11,0,0,1,45,98.69a3.11,3.11,0,0,1-4.06,4.06,3.09,3.09,0,0,1-1.68-1.69,3.1,3.1,0,0,1,1.68-4,3,3,0,0,1,1.19-.24A3.08,3.08,0,0,1,43.3,97Z"/>
|
||||
<line class="cls-3" x1="38.81" y1="91.93" x2="40.83" y2="96.78"/>
|
||||
<line class="cls-3" x1="36.02" y1="93.79" x2="39.74" y2="97.51"/>
|
||||
<line class="cls-3" x1="34.16" y1="96.58" x2="39.02" y2="98.59"/>
|
||||
<line class="cls-3" x1="33.5" y1="99.88" x2="38.76" y2="99.88"/>
|
||||
<line class="cls-3" x1="34.16" y1="103.17" x2="39.02" y2="101.16"/>
|
||||
<line class="cls-3" x1="36.02" y1="105.96" x2="39.74" y2="102.24"/>
|
||||
<line class="cls-3" x1="38.81" y1="107.83" x2="40.83" y2="102.97"/>
|
||||
<line class="cls-3" x1="42.11" y1="103.22" x2="42.11" y2="108.48"/>
|
||||
<line class="cls-3" x1="43.39" y1="102.97" x2="45.4" y2="107.83"/>
|
||||
<line class="cls-3" x1="44.47" y1="102.24" x2="48.19" y2="105.96"/>
|
||||
<line class="cls-3" x1="45.2" y1="101.16" x2="50.06" y2="103.17"/>
|
||||
<line class="cls-3" x1="45.45" y1="99.88" x2="50.71" y2="99.88"/>
|
||||
<line class="cls-3" x1="45.2" y1="98.59" x2="50.06" y2="96.58"/>
|
||||
<line class="cls-3" x1="44.47" y1="97.51" x2="48.19" y2="93.79"/>
|
||||
<line class="cls-3" x1="43.39" y1="96.78" x2="45.4" y2="91.93"/>
|
||||
<path class="cls-13" d="M27.21,108.28a1.21,1.21,0,0,0,.6-1,1.2,1.2,0,1,0-1.19,1.2A1.21,1.21,0,0,0,27.21,108.28Z"/>
|
||||
<path class="cls-13" d="M30.47,112a1.21,1.21,0,0,0,.4-.89,1.2,1.2,0,1,0-1.2,1.2A1.18,1.18,0,0,0,30.47,112Z"/>
|
||||
<path class="cls-13" d="M59.2,92.54a1.17,1.17,0,0,0,.59-1A1.2,1.2,0,1,0,58.6,92.7,1.17,1.17,0,0,0,59.2,92.54Z"/>
|
||||
<path class="cls-13" d="M56.34,88.57a1.2,1.2,0,0,0-.8-2.09,1.2,1.2,0,0,0,0,2.39A1.22,1.22,0,0,0,56.34,88.57Z"/>
|
||||
<path class="cls-14" d="M10.55,115.93a13.57,13.57,0,0,0,2,1.1,13.2,13.2,0,0,0,17.12-6"/>
|
||||
<line class="cls-5" x1="19.22" y1="123.01" x2="65.22" y2="123.01"/>
|
||||
<line class="cls-14" x1="70.72" y1="99.51" x2="73.22" y2="99.51"/>
|
||||
<line class="cls-15" x1="78.12" y1="99.51" x2="95.27" y2="99.51"/>
|
||||
<line class="cls-14" x1="97.72" y1="99.51" x2="100.22" y2="99.51"/>
|
||||
<line class="cls-5" x1="93.22" y1="123.01" x2="199.22" y2="123.01"/>
|
||||
<line class="cls-14" x1="192.72" y1="99.51" x2="195.22" y2="99.51"/>
|
||||
<line class="cls-15" x1="200.12" y1="99.51" x2="217.27" y2="99.51"/>
|
||||
<line class="cls-14" x1="219.72" y1="99.51" x2="222.22" y2="99.51"/>
|
||||
<polygon class="cls-6" points="151.1 106.59 132.22 106.59 132.22 87.71 140.31 79.62 159.19 79.62 159.19 98.5 151.1 106.59"/>
|
||||
<path class="cls-16" d="M149.75,86.36H145.7a6.75,6.75,0,0,0,0,13.49h4.05"/>
|
||||
<circle class="cls-7" cx="159.98" cy="80.25" r="7.93"/>
|
||||
<path class="cls-2" d="M160.06,73.81a6.35,6.35,0,1,0,6.35,6.34A6.35,6.35,0,0,0,160.06,73.81Zm5.27,4.12h-3a9.48,9.48,0,0,0-1.57-3.44A5.73,5.73,0,0,1,165.33,77.93ZM162,80.15a8.79,8.79,0,0,1-.14,1.59H158.3a9,9,0,0,1,0-3.17h3.53A8.76,8.76,0,0,1,162,80.15Zm-3.53,2.23h3.25a8.6,8.6,0,0,1-1.63,3.32A8.77,8.77,0,0,1,158.44,82.38Zm0-4.45a8.77,8.77,0,0,1,1.62-3.32,8.6,8.6,0,0,1,1.63,3.32Zm.91-3.44a9.32,9.32,0,0,0-1.57,3.44h-3A5.73,5.73,0,0,1,159.35,74.49Zm-4.77,4.08h3.08a9,9,0,0,0,0,3.17h-3.08a5.58,5.58,0,0,1,0-3.17Zm.22,3.81h3a9.32,9.32,0,0,0,1.57,3.44A5.75,5.75,0,0,1,154.8,82.38Zm6,3.44a9.48,9.48,0,0,0,1.57-3.44h3A5.75,5.75,0,0,1,160.78,85.82Zm4.77-4.08h-3.08a9.73,9.73,0,0,0,0-3.17h3.08a5.58,5.58,0,0,1,0,3.17Z"/>
|
||||
</svg>
|
After Width: | Height: | Size: 8.7 KiB |
@ -540,6 +540,7 @@
|
||||
"section_icon": [2, 2],
|
||||
"section_icon_column": [2.5, 2.5],
|
||||
"rating_star": [1.0, 1.0],
|
||||
"card": [25.0, 6.0],
|
||||
|
||||
"setting": [25.0, 1.8],
|
||||
"setting_control": [11.0, 2.0],
|
||||
|
@ -1,3 +1,6 @@
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import time
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
@ -122,8 +125,9 @@ def test_put():
|
||||
|
||||
def test_timeout():
|
||||
with patch("UM.Qt.QtApplication.QtApplication.getInstance"):
|
||||
output_device = NetworkedPrinterOutputDevice(device_id="test", address="127.0.0.1", properties={})
|
||||
output_device.setConnectionState(ConnectionState.Connected)
|
||||
output_device = NetworkedPrinterOutputDevice(device_id = "test", address = "127.0.0.1", properties = {})
|
||||
with patch("cura.CuraApplication.CuraApplication.getInstance"):
|
||||
output_device.setConnectionState(ConnectionState.Connected)
|
||||
|
||||
assert output_device.connectionState == ConnectionState.Connected
|
||||
output_device._update()
|
||||
@ -131,9 +135,8 @@ def test_timeout():
|
||||
output_device._last_response_time = time.time() - 15
|
||||
# But we did recently ask for a response!
|
||||
output_device._last_request_time = time.time() - 5
|
||||
output_device._update()
|
||||
with patch("cura.CuraApplication.CuraApplication.getInstance"):
|
||||
output_device._update()
|
||||
|
||||
# The connection should now be closed, since it went into timeout.
|
||||
assert output_device.connectionState == ConnectionState.Closed
|
||||
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
from unittest.mock import MagicMock
|
||||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||
from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
|
||||
@ -33,7 +34,8 @@ def test_getAndSet(data, printer_output_device):
|
||||
setattr(model, data["attribute"] + "Changed", MagicMock())
|
||||
|
||||
# Attempt to set the value
|
||||
getattr(model, "set" + attribute)(data["value"])
|
||||
with patch("cura.CuraApplication.CuraApplication.getInstance"):
|
||||
getattr(model, "set" + attribute)(data["value"])
|
||||
|
||||
# Check if signal fired.
|
||||
signal = getattr(model, data["attribute"] + "Changed")
|
||||
|
Loading…
x
Reference in New Issue
Block a user