mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-06-04 11:14:21 +08:00
Fix merge conflicts
This commit is contained in:
commit
173f125d3e
1
.gitignore
vendored
1
.gitignore
vendored
@ -42,7 +42,6 @@ plugins/cura-siemensnx-plugin
|
||||
plugins/CuraBlenderPlugin
|
||||
plugins/CuraCloudPlugin
|
||||
plugins/CuraDrivePlugin
|
||||
plugins/CuraDrive
|
||||
plugins/CuraLiveScriptingPlugin
|
||||
plugins/CuraOpenSCADPlugin
|
||||
plugins/CuraPrintProfileCreator
|
||||
|
@ -6,6 +6,7 @@ from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Message import Message
|
||||
from cura import UltimakerCloudAuthentication
|
||||
|
||||
from cura.OAuth2.AuthorizationService import AuthorizationService
|
||||
from cura.OAuth2.Models import OAuth2Settings
|
||||
@ -37,15 +38,16 @@ class Account(QObject):
|
||||
self._logged_in = False
|
||||
|
||||
self._callback_port = 32118
|
||||
self._oauth_root = "https://account.ultimaker.com"
|
||||
self._cloud_api_root = "https://api.ultimaker.com"
|
||||
self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
|
||||
|
||||
self._oauth_settings = OAuth2Settings(
|
||||
OAUTH_SERVER_URL= self._oauth_root,
|
||||
CALLBACK_PORT=self._callback_port,
|
||||
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
|
||||
CLIENT_ID="um----------------------------ultimaker_cura",
|
||||
CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.write",
|
||||
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 "
|
||||
"cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write",
|
||||
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
|
||||
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
|
||||
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
|
||||
@ -60,6 +62,11 @@ class Account(QObject):
|
||||
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
|
||||
self._authorization_service.loadAuthDataFromPreferences()
|
||||
|
||||
## Returns a boolean indicating whether the given authentication is applied against staging or not.
|
||||
@property
|
||||
def is_staging(self) -> bool:
|
||||
return "staging" in self._oauth_root
|
||||
|
||||
@pyqtProperty(bool, notify=loginStateChanged)
|
||||
def isLoggedIn(self) -> bool:
|
||||
return self._logged_in
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Tuple, Optional, TYPE_CHECKING
|
||||
from typing import Tuple, Optional, TYPE_CHECKING, Dict, Any
|
||||
|
||||
from cura.Backups.BackupsManager import BackupsManager
|
||||
|
||||
@ -24,12 +24,12 @@ class Backups:
|
||||
## Create a new back-up using the BackupsManager.
|
||||
# \return Tuple containing a ZIP file with the back-up data and a dict
|
||||
# with metadata about the back-up.
|
||||
def createBackup(self) -> Tuple[Optional[bytes], Optional[dict]]:
|
||||
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]:
|
||||
return self.manager.createBackup()
|
||||
|
||||
## Restore a back-up using the BackupsManager.
|
||||
# \param zip_file A ZIP file containing the actual back-up data.
|
||||
# \param meta_data Some metadata needed for restoring a back-up, like the
|
||||
# Cura version number.
|
||||
def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
|
||||
def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None:
|
||||
return self.manager.restoreBackup(zip_file, meta_data)
|
||||
|
42
cura/ApplicationMetadata.py
Normal file
42
cura/ApplicationMetadata.py
Normal file
@ -0,0 +1,42 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
# ---------
|
||||
# Genearl constants used in Cura
|
||||
# ---------
|
||||
DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
|
||||
DEFAULT_CURA_VERSION = "master"
|
||||
DEFAULT_CURA_BUILD_TYPE = ""
|
||||
DEFAULT_CURA_DEBUG_MODE = False
|
||||
DEFAULT_CURA_SDK_VERSION = "6.0.0"
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraAppDisplayName # type: ignore
|
||||
if CuraAppDisplayName == "":
|
||||
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
|
||||
except ImportError:
|
||||
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraVersion # type: ignore
|
||||
if CuraVersion == "":
|
||||
CuraVersion = DEFAULT_CURA_VERSION
|
||||
except ImportError:
|
||||
CuraVersion = DEFAULT_CURA_VERSION # [CodeStyle: Reflecting imported value]
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraBuildType # type: ignore
|
||||
except ImportError:
|
||||
CuraBuildType = DEFAULT_CURA_BUILD_TYPE
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraDebugMode # type: ignore
|
||||
except ImportError:
|
||||
CuraDebugMode = DEFAULT_CURA_DEBUG_MODE
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraSDKVersion # type: ignore
|
||||
if CuraSDKVersion == "":
|
||||
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION
|
||||
except ImportError:
|
||||
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION
|
@ -1052,6 +1052,12 @@ class BuildVolume(SceneNode):
|
||||
else:
|
||||
raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?")
|
||||
|
||||
max_length_available = 0.5 * min(
|
||||
self._global_container_stack.getProperty("machine_width", "value"),
|
||||
self._global_container_stack.getProperty("machine_depth", "value")
|
||||
)
|
||||
bed_adhesion_size = min(bed_adhesion_size, max_length_available)
|
||||
|
||||
support_expansion = 0
|
||||
support_enabled = self._global_container_stack.getProperty("support_enable", "value")
|
||||
support_offset = self._global_container_stack.getProperty("support_offset", "value")
|
||||
|
@ -36,12 +36,12 @@ class CuraActions(QObject):
|
||||
# Starting a web browser from a signal handler connected to a menu will crash on windows.
|
||||
# So instead, defer the call to the next run of the event loop, since that does work.
|
||||
# Note that weirdly enough, only signal handlers that open a web browser fail like that.
|
||||
event = CallFunctionEvent(self._openUrl, [QUrl("http://ultimaker.com/en/support/software")], {})
|
||||
event = CallFunctionEvent(self._openUrl, [QUrl("https://ultimaker.com/en/resources/manuals/software")], {})
|
||||
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
||||
|
||||
@pyqtSlot()
|
||||
def openBugReportPage(self) -> None:
|
||||
event = CallFunctionEvent(self._openUrl, [QUrl("http://github.com/Ultimaker/Cura/issues")], {})
|
||||
event = CallFunctionEvent(self._openUrl, [QUrl("https://github.com/Ultimaker/Cura/issues")], {})
|
||||
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
||||
|
||||
## Reset camera position and direction to default
|
||||
|
@ -4,7 +4,7 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import cast, TYPE_CHECKING, Optional, Callable
|
||||
from typing import cast, TYPE_CHECKING, Optional, Callable, List
|
||||
|
||||
import numpy
|
||||
|
||||
@ -51,7 +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.GlobalStacksModel import GlobalStacksModel
|
||||
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
||||
from cura.Operations.SetParentOperation import SetParentOperation
|
||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
@ -117,6 +117,8 @@ from cura.ObjectsModel import ObjectsModel
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
|
||||
|
||||
from cura import ApplicationMetadata, UltimakerCloudAuthentication
|
||||
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Decorators import override
|
||||
|
||||
@ -129,21 +131,12 @@ if TYPE_CHECKING:
|
||||
|
||||
numpy.seterr(all = "ignore")
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraAppDisplayName, CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion # type: ignore
|
||||
except ImportError:
|
||||
CuraAppDisplayName = "Ultimaker Cura"
|
||||
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
|
||||
CuraBuildType = ""
|
||||
CuraDebugMode = False
|
||||
CuraSDKVersion = "6.0.0"
|
||||
|
||||
|
||||
class CuraApplication(QtApplication):
|
||||
# SettingVersion represents the set of settings available in the machine/extruder definitions.
|
||||
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
|
||||
# changes of the settings.
|
||||
SettingVersion = 5
|
||||
SettingVersion = 7
|
||||
|
||||
Created = False
|
||||
|
||||
@ -164,11 +157,11 @@ class CuraApplication(QtApplication):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(name = "cura",
|
||||
app_display_name = CuraAppDisplayName,
|
||||
version = CuraVersion,
|
||||
api_version = CuraSDKVersion,
|
||||
buildtype = CuraBuildType,
|
||||
is_debug_mode = CuraDebugMode,
|
||||
app_display_name = ApplicationMetadata.CuraAppDisplayName,
|
||||
version = ApplicationMetadata.CuraVersion,
|
||||
api_version = ApplicationMetadata.CuraSDKVersion,
|
||||
buildtype = ApplicationMetadata.CuraBuildType,
|
||||
is_debug_mode = ApplicationMetadata.CuraDebugMode,
|
||||
tray_icon_name = "cura-icon-32.png",
|
||||
**kwargs)
|
||||
|
||||
@ -263,6 +256,14 @@ class CuraApplication(QtApplication):
|
||||
from cura.CuraPackageManager import CuraPackageManager
|
||||
self._package_manager_class = CuraPackageManager
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def ultimakerCloudApiRootUrl(self) -> str:
|
||||
return UltimakerCloudAuthentication.CuraCloudAPIRoot
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def ultimakerCloudAccountRootUrl(self) -> str:
|
||||
return UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
|
||||
|
||||
# Adds command line options to the command line parser. This should be called after the application is created and
|
||||
# before the pre-start.
|
||||
def addCommandLineOptions(self):
|
||||
@ -434,7 +435,8 @@ class CuraApplication(QtApplication):
|
||||
def startSplashWindowPhase(self) -> None:
|
||||
super().startSplashWindowPhase()
|
||||
|
||||
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
|
||||
if not self.getIsHeadLess():
|
||||
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
|
||||
|
||||
self.setRequiredPlugins([
|
||||
# Misc.:
|
||||
@ -499,7 +501,8 @@ class CuraApplication(QtApplication):
|
||||
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
|
||||
preferences.addPreference("cura/choice_on_open_project", "always_ask")
|
||||
preferences.addPreference("cura/use_multi_build_plate", False)
|
||||
|
||||
preferences.addPreference("view/settings_list_height", 400)
|
||||
preferences.addPreference("view/settings_visible", False)
|
||||
preferences.addPreference("cura/currency", "€")
|
||||
preferences.addPreference("cura/material_settings", "{}")
|
||||
|
||||
@ -664,12 +667,12 @@ class CuraApplication(QtApplication):
|
||||
|
||||
## Handle loading of all plugin types (and the backend explicitly)
|
||||
# \sa PluginRegistry
|
||||
def _loadPlugins(self):
|
||||
def _loadPlugins(self) -> None:
|
||||
self._plugin_registry.addType("profile_reader", self._addProfileReader)
|
||||
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
|
||||
|
||||
if Platform.isLinux():
|
||||
lib_suffixes = {"", "64", "32", "x32"} #A few common ones on different distributions.
|
||||
lib_suffixes = {"", "64", "32", "x32"} # A few common ones on different distributions.
|
||||
else:
|
||||
lib_suffixes = {""}
|
||||
for suffix in lib_suffixes:
|
||||
@ -953,7 +956,7 @@ class CuraApplication(QtApplication):
|
||||
engine.rootContext().setContextProperty("CuraApplication", self)
|
||||
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
|
||||
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
|
||||
engine.rootContext().setContextProperty("CuraSDKVersion", CuraSDKVersion)
|
||||
engine.rootContext().setContextProperty("CuraSDKVersion", ApplicationMetadata.CuraSDKVersion)
|
||||
|
||||
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
|
||||
|
||||
@ -971,7 +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(GlobalStacksModel, "Cura", 1, 0, "GlobalStacksModel")
|
||||
|
||||
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
|
||||
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
|
||||
@ -1105,88 +1108,6 @@ class CuraApplication(QtApplication):
|
||||
self._platform_activity = True if count > 0 else False
|
||||
self.activityChanged.emit()
|
||||
|
||||
# Remove all selected objects from the scene.
|
||||
@pyqtSlot()
|
||||
@deprecated("Moved to CuraActions", "2.6")
|
||||
def deleteSelection(self):
|
||||
if not self.getController().getToolsEnabled():
|
||||
return
|
||||
removed_group_nodes = []
|
||||
op = GroupedOperation()
|
||||
nodes = Selection.getAllSelectedObjects()
|
||||
for node in nodes:
|
||||
op.addOperation(RemoveSceneNodeOperation(node))
|
||||
group_node = node.getParent()
|
||||
if group_node and group_node.callDecoration("isGroup") and group_node not in removed_group_nodes:
|
||||
remaining_nodes_in_group = list(set(group_node.getChildren()) - set(nodes))
|
||||
if len(remaining_nodes_in_group) == 1:
|
||||
removed_group_nodes.append(group_node)
|
||||
op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent()))
|
||||
op.addOperation(RemoveSceneNodeOperation(group_node))
|
||||
op.push()
|
||||
|
||||
## Remove an object from the scene.
|
||||
# Note that this only removes an object if it is selected.
|
||||
@pyqtSlot("quint64")
|
||||
@deprecated("Use deleteSelection instead", "2.6")
|
||||
def deleteObject(self, object_id):
|
||||
if not self.getController().getToolsEnabled():
|
||||
return
|
||||
|
||||
node = self.getController().getScene().findObject(object_id)
|
||||
|
||||
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
|
||||
node = Selection.getSelectedObject(0)
|
||||
|
||||
if node:
|
||||
op = GroupedOperation()
|
||||
op.addOperation(RemoveSceneNodeOperation(node))
|
||||
|
||||
group_node = node.getParent()
|
||||
if group_node:
|
||||
# Note that at this point the node has not yet been deleted
|
||||
if len(group_node.getChildren()) <= 2 and group_node.callDecoration("isGroup"):
|
||||
op.addOperation(SetParentOperation(group_node.getChildren()[0], group_node.getParent()))
|
||||
op.addOperation(RemoveSceneNodeOperation(group_node))
|
||||
|
||||
op.push()
|
||||
|
||||
## Create a number of copies of existing object.
|
||||
# \param object_id
|
||||
# \param count number of copies
|
||||
# \param min_offset minimum offset to other objects.
|
||||
@pyqtSlot("quint64", int)
|
||||
@deprecated("Use CuraActions::multiplySelection", "2.6")
|
||||
def multiplyObject(self, object_id, count, min_offset = 8):
|
||||
node = self.getController().getScene().findObject(object_id)
|
||||
if not node:
|
||||
node = Selection.getSelectedObject(0)
|
||||
|
||||
while node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
node = node.getParent()
|
||||
|
||||
job = MultiplyObjectsJob([node], count, min_offset)
|
||||
job.start()
|
||||
return
|
||||
|
||||
## Center object on platform.
|
||||
@pyqtSlot("quint64")
|
||||
@deprecated("Use CuraActions::centerSelection", "2.6")
|
||||
def centerObject(self, object_id):
|
||||
node = self.getController().getScene().findObject(object_id)
|
||||
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
|
||||
node = Selection.getSelectedObject(0)
|
||||
|
||||
if not node:
|
||||
return
|
||||
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
node = node.getParent()
|
||||
|
||||
if node:
|
||||
op = SetTransformOperation(node, Vector())
|
||||
op.push()
|
||||
|
||||
## Select all nodes containing mesh data in the scene.
|
||||
@pyqtSlot()
|
||||
def selectAll(self):
|
||||
@ -1266,62 +1187,75 @@ class CuraApplication(QtApplication):
|
||||
|
||||
## Arrange all objects.
|
||||
@pyqtSlot()
|
||||
def arrangeObjectsToAllBuildPlates(self):
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
def arrangeObjectsToAllBuildPlates(self) -> None:
|
||||
nodes_to_arrange = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
|
||||
parent_node = node.getParent()
|
||||
if parent_node and parent_node.callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is reset)
|
||||
|
||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||
continue # i.e. node with layer data
|
||||
|
||||
bounding_box = node.getBoundingBox()
|
||||
# Skip nodes that are too big
|
||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
||||
nodes.append(node)
|
||||
job = ArrangeObjectsAllBuildPlatesJob(nodes)
|
||||
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
|
||||
nodes_to_arrange.append(node)
|
||||
job = ArrangeObjectsAllBuildPlatesJob(nodes_to_arrange)
|
||||
job.start()
|
||||
self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
|
||||
|
||||
# Single build plate
|
||||
@pyqtSlot()
|
||||
def arrangeAll(self):
|
||||
nodes = []
|
||||
def arrangeAll(self) -> None:
|
||||
nodes_to_arrange = []
|
||||
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
|
||||
parent_node = node.getParent()
|
||||
if parent_node and parent_node.callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
|
||||
if not node.isSelectable():
|
||||
continue # i.e. node with layer data
|
||||
|
||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||
continue # i.e. node with layer data
|
||||
|
||||
if node.callDecoration("getBuildPlateNumber") == active_build_plate:
|
||||
# Skip nodes that are too big
|
||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
||||
nodes.append(node)
|
||||
self.arrange(nodes, fixed_nodes = [])
|
||||
bounding_box = node.getBoundingBox()
|
||||
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
|
||||
nodes_to_arrange.append(node)
|
||||
self.arrange(nodes_to_arrange, fixed_nodes = [])
|
||||
|
||||
## Arrange a set of nodes given a set of fixed nodes
|
||||
# \param nodes nodes that we have to place
|
||||
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
|
||||
def arrange(self, nodes, fixed_nodes):
|
||||
def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None:
|
||||
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
||||
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
|
||||
job.start()
|
||||
|
||||
## Reload all mesh data on the screen from file.
|
||||
@pyqtSlot()
|
||||
def reloadAll(self):
|
||||
def reloadAll(self) -> None:
|
||||
Logger.log("i", "Reloading all loaded mesh data.")
|
||||
nodes = []
|
||||
has_merged_nodes = False
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not isinstance(node, CuraSceneNode) or not node.getMeshData() :
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||
if not isinstance(node, CuraSceneNode) or not node.getMeshData():
|
||||
if node.getName() == "MergedMesh":
|
||||
has_merged_nodes = True
|
||||
continue
|
||||
@ -1335,7 +1269,7 @@ class CuraApplication(QtApplication):
|
||||
file_name = node.getMeshData().getFileName()
|
||||
if file_name:
|
||||
job = ReadMeshJob(file_name)
|
||||
job._node = node
|
||||
job._node = node # type: ignore
|
||||
job.finished.connect(self._reloadMeshFinished)
|
||||
if has_merged_nodes:
|
||||
job.finished.connect(self.updateOriginOfMergedMeshes)
|
||||
@ -1344,20 +1278,8 @@ class CuraApplication(QtApplication):
|
||||
else:
|
||||
Logger.log("w", "Unable to reload data because we don't have a filename.")
|
||||
|
||||
|
||||
## Get logging data of the backend engine
|
||||
# \returns \type{string} Logging data
|
||||
@pyqtSlot(result = str)
|
||||
def getEngineLog(self):
|
||||
log = ""
|
||||
|
||||
for entry in self.getBackend().getLog():
|
||||
log += entry.decode()
|
||||
|
||||
return log
|
||||
|
||||
@pyqtSlot("QStringList")
|
||||
def setExpandedCategories(self, categories):
|
||||
def setExpandedCategories(self, categories: List[str]) -> None:
|
||||
categories = list(set(categories))
|
||||
categories.sort()
|
||||
joined = ";".join(categories)
|
||||
@ -1368,7 +1290,7 @@ class CuraApplication(QtApplication):
|
||||
expandedCategoriesChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty("QStringList", notify = expandedCategoriesChanged)
|
||||
def expandedCategories(self):
|
||||
def expandedCategories(self) -> List[str]:
|
||||
return self.getPreferences().getValue("cura/categories_expanded").split(";")
|
||||
|
||||
@pyqtSlot()
|
||||
@ -1418,13 +1340,12 @@ class CuraApplication(QtApplication):
|
||||
|
||||
|
||||
## Updates origin position of all merged meshes
|
||||
# \param jobNode \type{Job} empty object which passed which is required by JobQueue
|
||||
def updateOriginOfMergedMeshes(self, jobNode):
|
||||
def updateOriginOfMergedMeshes(self, _):
|
||||
group_nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh":
|
||||
|
||||
#checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator
|
||||
# Checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator
|
||||
for decorator in node.getDecorators():
|
||||
if isinstance(decorator, GroupDecorator):
|
||||
group_nodes.append(node)
|
||||
@ -1468,7 +1389,7 @@ class CuraApplication(QtApplication):
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def groupSelected(self):
|
||||
def groupSelected(self) -> None:
|
||||
# Create a group-node
|
||||
group_node = CuraSceneNode()
|
||||
group_decorator = GroupDecorator()
|
||||
@ -1484,7 +1405,8 @@ class CuraApplication(QtApplication):
|
||||
# Remove nodes that are directly parented to another selected node from the selection so they remain parented
|
||||
selected_nodes = Selection.getAllSelectedObjects().copy()
|
||||
for node in selected_nodes:
|
||||
if node.getParent() in selected_nodes and not node.getParent().callDecoration("isGroup"):
|
||||
parent = node.getParent()
|
||||
if parent is not None and parent in selected_nodes and not parent.callDecoration("isGroup"):
|
||||
Selection.remove(node)
|
||||
|
||||
# Move selected nodes into the group-node
|
||||
@ -1496,7 +1418,7 @@ class CuraApplication(QtApplication):
|
||||
Selection.add(group_node)
|
||||
|
||||
@pyqtSlot()
|
||||
def ungroupSelected(self):
|
||||
def ungroupSelected(self) -> None:
|
||||
selected_objects = Selection.getAllSelectedObjects().copy()
|
||||
for node in selected_objects:
|
||||
if node.callDecoration("isGroup"):
|
||||
@ -1519,7 +1441,7 @@ class CuraApplication(QtApplication):
|
||||
# Note: The group removes itself from the scene once all its children have left it,
|
||||
# see GroupDecorator._onChildrenChanged
|
||||
|
||||
def _createSplashScreen(self):
|
||||
def _createSplashScreen(self) -> Optional[CuraSplashScreen.CuraSplashScreen]:
|
||||
if self._is_headless:
|
||||
return None
|
||||
return CuraSplashScreen.CuraSplashScreen()
|
||||
|
@ -8,3 +8,4 @@ CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
||||
CuraSDKVersion = "@CURA_SDK_VERSION@"
|
||||
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
||||
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
||||
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
|
||||
|
@ -1,18 +1,15 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, Qt
|
||||
|
||||
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):
|
||||
class GlobalStacksModel(ListModel):
|
||||
NameRole = Qt.UserRole + 1
|
||||
IdRole = Qt.UserRole + 2
|
||||
HasRemoteConnectionRole = Qt.UserRole + 3
|
||||
@ -37,25 +34,20 @@ class PrintersModel(ListModel):
|
||||
|
||||
## Handler for container added/removed events from registry
|
||||
def _onContainerChanged(self, container):
|
||||
from cura.Settings.GlobalStack import GlobalStack # otherwise circular imports
|
||||
|
||||
# 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")
|
||||
connection_type = int(container_stack.getMetaDataEntry("connection_type", ConnectionType.NotConnected.value))
|
||||
has_remote_connection = connection_type in [ConnectionType.NetworkConnection.value, ConnectionType.CloudConnection.value]
|
||||
|
||||
if container_stack.getMetaDataEntry("hidden", False) in ["True", True]:
|
||||
continue
|
||||
|
||||
@ -66,4 +58,4 @@ class PrintersModel(ListModel):
|
||||
"connectionType": connection_type,
|
||||
"metadata": container_stack.getMetaData().copy()})
|
||||
items.sort(key=lambda i: not i["hasRemoteConnection"])
|
||||
self.setItems(items)
|
||||
self.setItems(items)
|
@ -5,6 +5,8 @@ from UM.Application import Application
|
||||
from typing import Any
|
||||
import numpy
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
||||
|
||||
class LayerPolygon:
|
||||
NoneType = 0
|
||||
@ -18,7 +20,8 @@ class LayerPolygon:
|
||||
MoveCombingType = 8
|
||||
MoveRetractionType = 9
|
||||
SupportInterfaceType = 10
|
||||
__number_of_types = 11
|
||||
PrimeTower = 11
|
||||
__number_of_types = 12
|
||||
|
||||
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType)
|
||||
|
||||
@ -33,7 +36,8 @@ class LayerPolygon:
|
||||
self._extruder = extruder
|
||||
self._types = line_types
|
||||
for i in range(len(self._types)):
|
||||
if self._types[i] >= self.__number_of_types: #Got faulty line data from the engine.
|
||||
if self._types[i] >= self.__number_of_types: # Got faulty line data from the engine.
|
||||
Logger.log("w", "Found an unknown line type: %s", i)
|
||||
self._types[i] = self.NoneType
|
||||
self._data = data
|
||||
self._line_widths = line_widths
|
||||
@ -236,7 +240,8 @@ class LayerPolygon:
|
||||
theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType
|
||||
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
|
||||
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
|
||||
theme.getColor("layerview_support_interface").getRgbF() # SupportInterfaceType
|
||||
theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType
|
||||
theme.getColor("layerview_prime_tower").getRgbF()
|
||||
])
|
||||
|
||||
return cls.__color_map
|
||||
|
@ -302,6 +302,10 @@ class MaterialManager(QObject):
|
||||
def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]:
|
||||
return self._guid_material_groups_map.get(guid)
|
||||
|
||||
# Returns a dict of all material groups organized by root_material_id.
|
||||
def getAllMaterialGroups(self) -> Dict[str, "MaterialGroup"]:
|
||||
return self._material_group_map
|
||||
|
||||
#
|
||||
# Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
|
||||
#
|
||||
@ -679,7 +683,11 @@ class MaterialManager(QObject):
|
||||
|
||||
@pyqtSlot(str)
|
||||
def removeFavorite(self, root_material_id: str) -> None:
|
||||
self._favorites.remove(root_material_id)
|
||||
try:
|
||||
self._favorites.remove(root_material_id)
|
||||
except KeyError:
|
||||
Logger.log("w", "Could not delete material %s from favorites as it was already deleted", root_material_id)
|
||||
return
|
||||
self.materialsUpdated.emit()
|
||||
|
||||
# Ensure all settings are saved.
|
||||
@ -688,4 +696,4 @@ class MaterialManager(QObject):
|
||||
|
||||
@pyqtSlot()
|
||||
def getFavorites(self):
|
||||
return self._favorites
|
||||
return self._favorites
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
@ -39,15 +39,23 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
||||
self._machine_manager = self._application.getMachineManager()
|
||||
self._quality_manager = Application.getInstance().getQualityManager()
|
||||
|
||||
self._application.globalContainerStackChanged.connect(self._update)
|
||||
self._machine_manager.activeQualityGroupChanged.connect(self._update)
|
||||
self._machine_manager.extruderChanged.connect(self._update)
|
||||
self._quality_manager.qualitiesUpdated.connect(self._update)
|
||||
self._application.globalContainerStackChanged.connect(self._onChange)
|
||||
self._machine_manager.activeQualityGroupChanged.connect(self._onChange)
|
||||
self._machine_manager.extruderChanged.connect(self._onChange)
|
||||
self._quality_manager.qualitiesUpdated.connect(self._onChange)
|
||||
|
||||
self._layer_height_unit = "" # This is cached
|
||||
|
||||
self._update_timer = QTimer() # type: QTimer
|
||||
self._update_timer.setInterval(100)
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self._update)
|
||||
|
||||
self._update()
|
||||
|
||||
def _onChange(self) -> None:
|
||||
self._update_timer.start()
|
||||
|
||||
def _update(self):
|
||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||
|
||||
|
@ -6,6 +6,7 @@ from typing import Optional, List
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Resources import Resources
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
@ -18,14 +19,20 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
onItemsChanged = pyqtSignal()
|
||||
activePresetChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, preferences, parent = None):
|
||||
def __init__(self, preferences: Preferences, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self._items = [] # type: List[SettingVisibilityPreset]
|
||||
self._custom_preset = SettingVisibilityPreset(preset_id = "custom", name = "Custom selection", weight = -100)
|
||||
|
||||
self._populate()
|
||||
|
||||
basic_item = self.getVisibilityPresetById("basic")
|
||||
basic_visibile_settings = ";".join(basic_item.settings)
|
||||
if basic_item is not None:
|
||||
basic_visibile_settings = ";".join(basic_item.settings)
|
||||
else:
|
||||
Logger.log("w", "Unable to find the basic visiblity preset.")
|
||||
basic_visibile_settings = ""
|
||||
|
||||
self._preferences = preferences
|
||||
|
||||
@ -42,7 +49,8 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
visible_settings = self._preferences.getValue("general/visible_settings")
|
||||
|
||||
if not visible_settings:
|
||||
self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item.settings))
|
||||
new_visible_settings = self._active_preset_item.settings if self._active_preset_item is not None else []
|
||||
self._preferences.setValue("general/visible_settings", ";".join(new_visible_settings))
|
||||
else:
|
||||
self._onPreferencesChanged("general/visible_settings")
|
||||
|
||||
@ -59,9 +67,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
def _populate(self) -> None:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
items = [] # type: List[SettingVisibilityPreset]
|
||||
|
||||
custom_preset = SettingVisibilityPreset(preset_id="custom", name ="Custom selection", weight = -100)
|
||||
items.append(custom_preset)
|
||||
items.append(self._custom_preset)
|
||||
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
||||
setting_visibility_preset = SettingVisibilityPreset()
|
||||
try:
|
||||
@ -77,7 +83,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
self.setItems(items)
|
||||
|
||||
@pyqtProperty("QVariantList", notify = onItemsChanged)
|
||||
def items(self):
|
||||
def items(self) -> List[SettingVisibilityPreset]:
|
||||
return self._items
|
||||
|
||||
def setItems(self, items: List[SettingVisibilityPreset]) -> None:
|
||||
@ -87,7 +93,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActivePreset(self, preset_id: str) -> None:
|
||||
if preset_id == self._active_preset_item.presetId:
|
||||
if self._active_preset_item is not None and preset_id == self._active_preset_item.presetId:
|
||||
Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id)
|
||||
return
|
||||
|
||||
@ -96,7 +102,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
|
||||
return
|
||||
|
||||
need_to_save_to_custom = self._active_preset_item.presetId == "custom" and preset_id != "custom"
|
||||
need_to_save_to_custom = self._active_preset_item is None or (self._active_preset_item.presetId == "custom" and preset_id != "custom")
|
||||
if need_to_save_to_custom:
|
||||
# Save the current visibility settings to custom
|
||||
current_visibility_string = self._preferences.getValue("general/visible_settings")
|
||||
@ -117,7 +123,9 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
|
||||
@pyqtProperty(str, notify = activePresetChanged)
|
||||
def activePreset(self) -> str:
|
||||
return self._active_preset_item.presetId
|
||||
if self._active_preset_item is not None:
|
||||
return self._active_preset_item.presetId
|
||||
return ""
|
||||
|
||||
def _onPreferencesChanged(self, name: str) -> None:
|
||||
if name != "general/visible_settings":
|
||||
@ -149,7 +157,12 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
else:
|
||||
item_to_set = matching_preset_item
|
||||
|
||||
# If we didn't find a matching preset, fallback to custom.
|
||||
if item_to_set is None:
|
||||
item_to_set = self._custom_preset
|
||||
|
||||
if self._active_preset_item is None or self._active_preset_item.presetId != item_to_set.presetId:
|
||||
self._active_preset_item = item_to_set
|
||||
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item.presetId)
|
||||
if self._active_preset_item is not None:
|
||||
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item.presetId)
|
||||
self.activePresetChanged.emit()
|
||||
|
@ -25,7 +25,7 @@ class MultiplyObjectsJob(Job):
|
||||
|
||||
def run(self):
|
||||
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
|
||||
dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Object"))
|
||||
dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Objects"))
|
||||
status_message.show()
|
||||
scene = Application.getInstance().getController().getScene()
|
||||
|
||||
|
@ -52,8 +52,11 @@ class AuthorizationService:
|
||||
if not self._user_profile:
|
||||
# If no user profile was stored locally, we try to get it from JWT.
|
||||
self._user_profile = self._parseJWT()
|
||||
if not self._user_profile:
|
||||
|
||||
if not self._user_profile and self._auth_data:
|
||||
# If there is still no user profile from the JWT, we have to log in again.
|
||||
Logger.log("w", "The user profile could not be loaded. The user must log in again!")
|
||||
self.deleteAuthData()
|
||||
return None
|
||||
|
||||
return self._user_profile
|
||||
@ -83,9 +86,11 @@ class AuthorizationService:
|
||||
if not self.getUserProfile():
|
||||
# We check if we can get the user profile.
|
||||
# If we can't get it, that means the access token (JWT) was invalid or expired.
|
||||
Logger.log("w", "Unable to get the user profile.")
|
||||
return None
|
||||
|
||||
if self._auth_data is None:
|
||||
Logger.log("d", "No auth data to retrieve the access_token from")
|
||||
return None
|
||||
|
||||
return self._auth_data.access_token
|
||||
|
@ -54,7 +54,7 @@ class ConfigurationModel(QObject):
|
||||
for configuration in self._extruder_configurations:
|
||||
if configuration is None:
|
||||
return False
|
||||
return self._printer_type is not None
|
||||
return self._printer_type != ""
|
||||
|
||||
def __str__(self):
|
||||
message_chunks = []
|
||||
|
@ -4,6 +4,7 @@
|
||||
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
||||
from UM.Logger import Logger
|
||||
from UM.Scene.SceneNode import SceneNode #For typing.
|
||||
from cura.API import Account
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType
|
||||
@ -11,12 +12,13 @@ from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, Conne
|
||||
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
|
||||
from time import time
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
from typing import Callable, Dict, List, Optional, Union
|
||||
from enum import IntEnum
|
||||
|
||||
import os # To get the username
|
||||
import gzip
|
||||
|
||||
|
||||
class AuthState(IntEnum):
|
||||
NotAuthenticated = 1
|
||||
AuthenticationRequested = 2
|
||||
@ -41,7 +43,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
self._api_prefix = ""
|
||||
self._address = address
|
||||
self._properties = properties
|
||||
self._user_agent = "%s/%s " % (CuraApplication.getInstance().getApplicationName(), CuraApplication.getInstance().getVersion())
|
||||
self._user_agent = "%s/%s " % (CuraApplication.getInstance().getApplicationName(),
|
||||
CuraApplication.getInstance().getVersion())
|
||||
|
||||
self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]]
|
||||
self._authentication_state = AuthState.NotAuthenticated
|
||||
@ -55,7 +58,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
self._gcode = [] # type: List[str]
|
||||
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
|
||||
|
||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||
file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
||||
raise NotImplementedError("requestWrite needs to be implemented")
|
||||
|
||||
def setAuthenticationState(self, authentication_state: AuthState) -> None:
|
||||
@ -143,10 +147,12 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
url = QUrl("http://" + self._address + self._api_prefix + target)
|
||||
request = QNetworkRequest(url)
|
||||
if content_type is not None:
|
||||
request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
|
||||
request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
|
||||
request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
|
||||
return request
|
||||
|
||||
## This method was only available privately before, but it was actually called from SendMaterialJob.py.
|
||||
# We now have a public equivalent as well. We did not remove the private one as plugins might be using that.
|
||||
def createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
|
||||
return self._createFormPart(content_header, data, content_type)
|
||||
|
||||
@ -163,9 +169,15 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
part.setBody(data)
|
||||
return part
|
||||
|
||||
## Convenience function to get the username from the OS.
|
||||
# The code was copied from the getpass module, as we try to use as little dependencies as possible.
|
||||
## Convenience function to get the username, either from the cloud or from the OS.
|
||||
def _getUserName(self) -> str:
|
||||
# check first if we are logged in with the Ultimaker Account
|
||||
account = CuraApplication.getInstance().getCuraAPI().account # type: Account
|
||||
if account and account.isLoggedIn:
|
||||
return account.userName
|
||||
|
||||
# Otherwise get the username from the US
|
||||
# The code below was copied from the getpass module, as we try to use as little dependencies as possible.
|
||||
for name in ("LOGNAME", "USER", "LNAME", "USERNAME"):
|
||||
user = os.environ.get(name)
|
||||
if user:
|
||||
@ -181,49 +193,89 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
self._createNetworkManager()
|
||||
assert (self._manager is not None)
|
||||
|
||||
def put(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||
## Sends a put request to the given path.
|
||||
# \param url: The path after the API prefix.
|
||||
# \param data: The data to be sent in the body
|
||||
# \param content_type: The content type of the body data.
|
||||
# \param on_finished: The function to call when the response is received.
|
||||
# \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
|
||||
def put(self, url: str, data: Union[str, bytes], content_type: Optional[str] = None,
|
||||
on_finished: Optional[Callable[[QNetworkReply], None]] = None,
|
||||
on_progress: Optional[Callable[[int, int], None]] = None) -> None:
|
||||
self._validateManager()
|
||||
request = self._createEmptyRequest(target)
|
||||
self._last_request_time = time()
|
||||
if self._manager is not None:
|
||||
reply = self._manager.put(request, data.encode())
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
else:
|
||||
Logger.log("e", "Could not find manager.")
|
||||
|
||||
def delete(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||
request = self._createEmptyRequest(url, content_type = content_type)
|
||||
self._last_request_time = time()
|
||||
|
||||
if not self._manager:
|
||||
Logger.log("e", "No network manager was created to execute the PUT call with.")
|
||||
return
|
||||
|
||||
body = data if isinstance(data, bytes) else data.encode() # type: bytes
|
||||
reply = self._manager.put(request, body)
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
|
||||
if on_progress is not None:
|
||||
reply.uploadProgress.connect(on_progress)
|
||||
|
||||
## Sends a delete request to the given path.
|
||||
# \param url: The path after the API prefix.
|
||||
# \param on_finished: The function to be call when the response is received.
|
||||
def delete(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||
self._validateManager()
|
||||
request = self._createEmptyRequest(target)
|
||||
self._last_request_time = time()
|
||||
if self._manager is not None:
|
||||
reply = self._manager.deleteResource(request)
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
else:
|
||||
Logger.log("e", "Could not find manager.")
|
||||
|
||||
def get(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||
request = self._createEmptyRequest(url)
|
||||
self._last_request_time = time()
|
||||
|
||||
if not self._manager:
|
||||
Logger.log("e", "No network manager was created to execute the DELETE call with.")
|
||||
return
|
||||
|
||||
reply = self._manager.deleteResource(request)
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
|
||||
## Sends a get request to the given path.
|
||||
# \param url: The path after the API prefix.
|
||||
# \param on_finished: The function to be call when the response is received.
|
||||
def get(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||
self._validateManager()
|
||||
request = self._createEmptyRequest(target)
|
||||
self._last_request_time = time()
|
||||
if self._manager is not None:
|
||||
reply = self._manager.get(request)
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
else:
|
||||
Logger.log("e", "Could not find manager.")
|
||||
|
||||
def post(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None:
|
||||
request = self._createEmptyRequest(url)
|
||||
self._last_request_time = time()
|
||||
|
||||
if not self._manager:
|
||||
Logger.log("e", "No network manager was created to execute the GET call with.")
|
||||
return
|
||||
|
||||
reply = self._manager.get(request)
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
|
||||
## Sends a post request to the given path.
|
||||
# \param url: The path after the API prefix.
|
||||
# \param data: The data to be sent in the body
|
||||
# \param on_finished: The function to call when the response is received.
|
||||
# \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
|
||||
def post(self, url: str, data: Union[str, bytes],
|
||||
on_finished: Optional[Callable[[QNetworkReply], None]],
|
||||
on_progress: Optional[Callable[[int, int], None]] = None) -> None:
|
||||
self._validateManager()
|
||||
request = self._createEmptyRequest(target)
|
||||
self._last_request_time = time()
|
||||
if self._manager is not None:
|
||||
reply = self._manager.post(request, data.encode())
|
||||
if on_progress is not None:
|
||||
reply.uploadProgress.connect(on_progress)
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
else:
|
||||
Logger.log("e", "Could not find manager.")
|
||||
|
||||
def postFormWithParts(self, target: str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> QNetworkReply:
|
||||
request = self._createEmptyRequest(url)
|
||||
self._last_request_time = time()
|
||||
|
||||
if not self._manager:
|
||||
Logger.log("e", "Could not find manager.")
|
||||
return
|
||||
|
||||
body = data if isinstance(data, bytes) else data.encode() # type: bytes
|
||||
reply = self._manager.post(request, body)
|
||||
if on_progress is not None:
|
||||
reply.uploadProgress.connect(on_progress)
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
|
||||
def postFormWithParts(self, target: str, parts: List[QHttpPart],
|
||||
on_finished: Optional[Callable[[QNetworkReply], None]],
|
||||
on_progress: Optional[Callable[[int, int], None]] = None) -> QNetworkReply:
|
||||
self._validateManager()
|
||||
request = self._createEmptyRequest(target, content_type=None)
|
||||
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
|
||||
|
@ -132,9 +132,9 @@ class PrintJobOutputModel(QObject):
|
||||
|
||||
@pyqtProperty(float, notify = timeElapsedChanged)
|
||||
def progress(self) -> float:
|
||||
result = self.timeElapsed / self.timeTotal
|
||||
# Never get a progress past 1.0
|
||||
return min(result, 1.0)
|
||||
time_elapsed = max(float(self.timeElapsed), 1.0) # Prevent a division by zero exception
|
||||
result = time_elapsed / self.timeTotal
|
||||
return min(result, 1.0) # Never get a progress past 1.0
|
||||
|
||||
@pyqtProperty(str, notify=stateChanged)
|
||||
def state(self) -> str:
|
||||
|
@ -1,10 +1,12 @@
|
||||
# Copyright (c) 2018 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 UM.Decorators import deprecated
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl, Q_ENUMS
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from UM.Logger import Logger
|
||||
@ -12,9 +14,6 @@ from UM.Signal import signalemitter
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
|
||||
from enum import IntEnum # For the connection state tracking.
|
||||
from typing import Callable, List, Optional, Union
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
@ -36,7 +35,7 @@ class ConnectionState(IntEnum):
|
||||
|
||||
|
||||
class ConnectionType(IntEnum):
|
||||
Unknown = 0
|
||||
NotConnected = 0
|
||||
UsbConnection = 1
|
||||
NetworkConnection = 2
|
||||
CloudConnection = 3
|
||||
@ -54,10 +53,6 @@ class ConnectionType(IntEnum):
|
||||
@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()
|
||||
@ -74,34 +69,34 @@ 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, connection_type: "ConnectionType" = ConnectionType.Unknown, parent: QObject = None) -> None:
|
||||
def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, 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]
|
||||
self._unique_configurations = [] # type: List[ConfigurationModel]
|
||||
|
||||
self._monitor_view_qml_path = "" #type: str
|
||||
self._monitor_component = None #type: Optional[QObject]
|
||||
self._monitor_item = None #type: Optional[QObject]
|
||||
self._monitor_view_qml_path = "" # type: str
|
||||
self._monitor_component = None # type: Optional[QObject]
|
||||
self._monitor_item = None # type: Optional[QObject]
|
||||
|
||||
self._control_view_qml_path = "" #type: str
|
||||
self._control_component = None #type: Optional[QObject]
|
||||
self._control_item = None #type: Optional[QObject]
|
||||
self._control_view_qml_path = "" # type: str
|
||||
self._control_component = None # type: Optional[QObject]
|
||||
self._control_item = None # type: Optional[QObject]
|
||||
|
||||
self._accepts_commands = False #type: bool
|
||||
self._accepts_commands = False # type: bool
|
||||
|
||||
self._update_timer = QTimer() #type: QTimer
|
||||
self._update_timer = QTimer() # type: QTimer
|
||||
self._update_timer.setInterval(2000) # TODO; Add preference for update interval
|
||||
self._update_timer.setSingleShot(False)
|
||||
self._update_timer.timeout.connect(self._update)
|
||||
|
||||
self._connection_state = ConnectionState.Closed #type: ConnectionState
|
||||
self._connection_type = connection_type
|
||||
self._connection_state = ConnectionState.Closed # type: ConnectionState
|
||||
self._connection_type = connection_type # type: ConnectionType
|
||||
|
||||
self._firmware_updater = None #type: Optional[FirmwareUpdater]
|
||||
self._firmware_name = None #type: Optional[str]
|
||||
self._address = "" #type: str
|
||||
self._connection_text = "" #type: str
|
||||
self._firmware_updater = None # type: Optional[FirmwareUpdater]
|
||||
self._firmware_name = None # type: Optional[str]
|
||||
self._address = "" # type: str
|
||||
self._connection_text = "" # type: str
|
||||
self.printersChanged.connect(self._onPrintersChanged)
|
||||
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
||||
|
||||
@ -130,10 +125,11 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
self._connection_state = connection_state
|
||||
self.connectionStateChanged.emit(self._id)
|
||||
|
||||
def getConnectionType(self) -> "ConnectionType":
|
||||
@pyqtProperty(int, constant = True)
|
||||
def connectionType(self) -> "ConnectionType":
|
||||
return self._connection_type
|
||||
|
||||
@pyqtProperty(str, notify = connectionStateChanged)
|
||||
@pyqtProperty(int, notify = connectionStateChanged)
|
||||
def connectionState(self) -> "ConnectionState":
|
||||
return self._connection_state
|
||||
|
||||
@ -147,7 +143,8 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
|
||||
return None
|
||||
|
||||
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None:
|
||||
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||
file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None:
|
||||
raise NotImplementedError("requestWrite needs to be implemented")
|
||||
|
||||
@pyqtProperty(QObject, notify = printersChanged)
|
||||
@ -223,8 +220,10 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
return self._unique_configurations
|
||||
|
||||
def _updateUniqueConfigurations(self) -> None:
|
||||
self._unique_configurations = list(set([printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None]))
|
||||
self._unique_configurations.sort(key = lambda k: k.printerType)
|
||||
self._unique_configurations = sorted(
|
||||
{printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None},
|
||||
key=lambda config: config.printerType,
|
||||
)
|
||||
self.uniqueConfigurationsChanged.emit()
|
||||
|
||||
# Returns the unique configurations of the printers within this output device
|
||||
|
@ -242,7 +242,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
|
||||
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
|
||||
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
|
||||
_, idx = numpy.unique(vertex_byte_view, return_index=True)
|
||||
_, idx = numpy.unique(vertex_byte_view, return_index = True)
|
||||
vertex_data = vertex_data[idx] # Select the unique rows by index.
|
||||
|
||||
hull = Polygon(vertex_data)
|
||||
@ -289,16 +289,21 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
# Add extra margin depending on adhesion type
|
||||
adhesion_type = self._global_stack.getProperty("adhesion_type", "value")
|
||||
|
||||
max_length_available = 0.5 * min(
|
||||
self._getSettingProperty("machine_width", "value"),
|
||||
self._getSettingProperty("machine_depth", "value")
|
||||
)
|
||||
|
||||
if adhesion_type == "raft":
|
||||
extra_margin = max(0, self._getSettingProperty("raft_margin", "value"))
|
||||
extra_margin = min(max_length_available, max(0, self._getSettingProperty("raft_margin", "value")))
|
||||
elif adhesion_type == "brim":
|
||||
extra_margin = max(0, self._getSettingProperty("brim_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value"))
|
||||
extra_margin = min(max_length_available, max(0, self._getSettingProperty("brim_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value")))
|
||||
elif adhesion_type == "none":
|
||||
extra_margin = 0
|
||||
elif adhesion_type == "skirt":
|
||||
extra_margin = max(
|
||||
extra_margin = min(max_length_available, max(
|
||||
0, self._getSettingProperty("skirt_gap", "value") +
|
||||
self._getSettingProperty("skirt_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value"))
|
||||
self._getSettingProperty("skirt_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value")))
|
||||
else:
|
||||
raise Exception("Unknown bed adhesion type. Did you forget to update the convex hull calculations for your new bed adhesion type?")
|
||||
|
||||
|
@ -54,7 +54,7 @@ class ConvexHullNode(SceneNode):
|
||||
|
||||
if hull_mesh_builder.addConvexPolygonExtrusion(
|
||||
self._hull.getPoints()[::-1], # bottom layer is reversed
|
||||
self._mesh_height-thickness, self._mesh_height, color=self._color):
|
||||
self._mesh_height - thickness, self._mesh_height, color = self._color):
|
||||
|
||||
hull_mesh = hull_mesh_builder.build()
|
||||
self.setMeshData(hull_mesh)
|
||||
|
@ -302,12 +302,7 @@ class ExtruderManager(QObject):
|
||||
global_stack = self._application.getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return []
|
||||
|
||||
result_tuple_list = sorted(list(global_stack.extruders.items()), key = lambda x: int(x[0]))
|
||||
result_list = [item[1] for item in result_tuple_list]
|
||||
|
||||
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
|
||||
return result_list[:machine_extruder_count]
|
||||
return global_stack.extruderList
|
||||
|
||||
def _globalContainerStackChanged(self) -> None:
|
||||
# If the global container changed, the machine changed and might have extruders that were not registered yet
|
||||
|
@ -97,6 +97,7 @@ class GlobalStack(CuraContainerStack):
|
||||
return
|
||||
|
||||
self._extruders[position] = extruder
|
||||
self.extrudersChanged.emit()
|
||||
Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)
|
||||
|
||||
## Overridden from ContainerStack
|
||||
|
@ -64,8 +64,6 @@ class MachineManager(QObject):
|
||||
|
||||
self._default_extruder_position = "0" # to be updated when extruders are switched on and off
|
||||
|
||||
self.machine_extruder_material_update_dict = collections.defaultdict(list) #type: Dict[str, List[Callable[[], None]]]
|
||||
|
||||
self._instance_container_timer = QTimer() # type: QTimer
|
||||
self._instance_container_timer.setInterval(250)
|
||||
self._instance_container_timer.setSingleShot(True)
|
||||
@ -178,6 +176,7 @@ class MachineManager(QObject):
|
||||
self._printer_output_devices.append(printer_output_device)
|
||||
|
||||
self.outputDevicesChanged.emit()
|
||||
self.printerConnectedStatusChanged.emit()
|
||||
|
||||
@pyqtProperty(QObject, notify = currentConfigurationChanged)
|
||||
def currentConfiguration(self) -> ConfigurationModel:
|
||||
@ -275,11 +274,6 @@ class MachineManager(QObject):
|
||||
extruder_stack.propertyChanged.connect(self._onPropertyChanged)
|
||||
extruder_stack.containersChanged.connect(self._onContainersChanged)
|
||||
|
||||
if self._global_container_stack.getId() in self.machine_extruder_material_update_dict:
|
||||
for func in self.machine_extruder_material_update_dict[self._global_container_stack.getId()]:
|
||||
self._application.callLater(func)
|
||||
del self.machine_extruder_material_update_dict[self._global_container_stack.getId()]
|
||||
|
||||
self.activeQualityGroupChanged.emit()
|
||||
|
||||
def _onActiveExtruderStackChanged(self) -> None:
|
||||
@ -443,12 +437,12 @@ class MachineManager(QObject):
|
||||
if not self._global_container_stack:
|
||||
return False
|
||||
|
||||
if self._global_container_stack.getTop().findInstances():
|
||||
if self._global_container_stack.getTop().getNumInstances() != 0:
|
||||
return True
|
||||
|
||||
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
for stack in stacks:
|
||||
if stack.getTop().findInstances():
|
||||
if stack.getTop().getNumInstances() != 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
@ -458,10 +452,10 @@ class MachineManager(QObject):
|
||||
if not self._global_container_stack:
|
||||
return 0
|
||||
num_user_settings = 0
|
||||
num_user_settings += len(self._global_container_stack.getTop().findInstances())
|
||||
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
num_user_settings += self._global_container_stack.getTop().getNumInstances()
|
||||
stacks = self._global_container_stack.extruderList
|
||||
for stack in stacks:
|
||||
num_user_settings += len(stack.getTop().findInstances())
|
||||
num_user_settings += stack.getTop().getNumInstances()
|
||||
return num_user_settings
|
||||
|
||||
## Delete a user setting from the global stack and all extruder stacks.
|
||||
@ -521,16 +515,30 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||
def printerConnected(self):
|
||||
def printerConnected(self) -> bool:
|
||||
return bool(self._printer_output_devices)
|
||||
|
||||
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||
def activeMachineHasRemoteConnection(self) -> bool:
|
||||
if self._global_container_stack:
|
||||
connection_type = self._global_container_stack.getMetaDataEntry("connection_type")
|
||||
connection_type = int(self._global_container_stack.getMetaDataEntry("connection_type", ConnectionType.NotConnected.value))
|
||||
return connection_type in [ConnectionType.NetworkConnection.value, ConnectionType.CloudConnection.value]
|
||||
return False
|
||||
|
||||
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||
def activeMachineIsGroup(self) -> bool:
|
||||
return bool(self._printer_output_devices) and len(self._printer_output_devices[0].printers) > 1
|
||||
|
||||
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||
def activeMachineHasActiveNetworkConnection(self) -> bool:
|
||||
# A network connection is only available if any output device is actually a network connected device.
|
||||
return any(d.connectionType == ConnectionType.NetworkConnection for d in self._printer_output_devices)
|
||||
|
||||
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||
def activeMachineHasActiveCloudConnection(self) -> bool:
|
||||
# A cloud connection is only available if any output device actually is a cloud connected device.
|
||||
return any(d.connectionType == ConnectionType.CloudConnection for d in self._printer_output_devices)
|
||||
|
||||
def activeMachineNetworkKey(self) -> str:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.getMetaDataEntry("um_network_key", "")
|
||||
|
30
cura/UltimakerCloudAuthentication.py
Normal file
30
cura/UltimakerCloudAuthentication.py
Normal file
@ -0,0 +1,30 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
# ---------
|
||||
# Constants used for the Cloud API
|
||||
# ---------
|
||||
DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str
|
||||
DEFAULT_CLOUD_API_VERSION = "1" # type: str
|
||||
DEFAULT_CLOUD_ACCOUNT_API_ROOT = "https://account.ultimaker.com" # type: str
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraCloudAPIRoot # type: ignore
|
||||
if CuraCloudAPIRoot == "":
|
||||
CuraCloudAPIRoot = DEFAULT_CLOUD_API_ROOT
|
||||
except ImportError:
|
||||
CuraCloudAPIRoot = DEFAULT_CLOUD_API_ROOT
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraCloudAPIVersion # type: ignore
|
||||
if CuraCloudAPIVersion == "":
|
||||
CuraCloudAPIVersion = DEFAULT_CLOUD_API_VERSION
|
||||
except ImportError:
|
||||
CuraCloudAPIVersion = DEFAULT_CLOUD_API_VERSION
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraCloudAccountAPIRoot # type: ignore
|
||||
if CuraCloudAccountAPIRoot == "":
|
||||
CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT
|
||||
except ImportError:
|
||||
CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT
|
@ -943,7 +943,7 @@ This release adds support for printers with elliptic buildplates. This feature h
|
||||
*AppImage for Linux
|
||||
The Linux distribution is now in AppImage format, which makes Cura easier to install.
|
||||
|
||||
*bugfixes
|
||||
*Bugfixes
|
||||
The user is now notified when a new version of Cura is available.
|
||||
When searching in the setting visibility preferences, the category for each setting is always displayed.
|
||||
3MF files are now saved and loaded correctly.
|
||||
|
12
plugins/CuraDrive/__init__.py
Normal file
12
plugins/CuraDrive/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from .src.DrivePluginExtension import DrivePluginExtension
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
||||
|
||||
def register(app):
|
||||
return {"extension": DrivePluginExtension()}
|
8
plugins/CuraDrive/plugin.json
Normal file
8
plugins/CuraDrive/plugin.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Cura Backups",
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": "Backup and restore your configuration.",
|
||||
"version": "1.2.0",
|
||||
"api": 6,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
168
plugins/CuraDrive/src/DriveApiService.py
Normal file
168
plugins/CuraDrive/src/DriveApiService.py
Normal file
@ -0,0 +1,168 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from tempfile import NamedTemporaryFile
|
||||
from typing import Any, Optional, List, Dict
|
||||
|
||||
import requests
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal, signalemitter
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from .UploadBackupJob import UploadBackupJob
|
||||
from .Settings import Settings
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling.
|
||||
@signalemitter
|
||||
class DriveApiService:
|
||||
BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL)
|
||||
|
||||
# Emit signal when restoring backup started or finished.
|
||||
restoringStateChanged = Signal()
|
||||
|
||||
# Emit signal when creating backup started or finished.
|
||||
creatingStateChanged = Signal()
|
||||
|
||||
def __init__(self) -> None:
|
||||
self._cura_api = CuraApplication.getInstance().getCuraAPI()
|
||||
|
||||
def getBackups(self) -> List[Dict[str, Any]]:
|
||||
access_token = self._cura_api.account.accessToken
|
||||
if not access_token:
|
||||
Logger.log("w", "Could not get access token.")
|
||||
return []
|
||||
|
||||
backup_list_request = requests.get(self.BACKUP_URL, headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
|
||||
# HTTP status 300s mean redirection. 400s and 500s are errors.
|
||||
# Technically 300s are not errors, but the use case here relies on "requests" to handle redirects automatically.
|
||||
if backup_list_request.status_code >= 300:
|
||||
Logger.log("w", "Could not get backups list from remote: %s", backup_list_request.text)
|
||||
Message(catalog.i18nc("@info:backup_status", "There was an error listing your backups."), title = catalog.i18nc("@info:title", "Backup")).show()
|
||||
return []
|
||||
return backup_list_request.json()["data"]
|
||||
|
||||
def createBackup(self) -> None:
|
||||
self.creatingStateChanged.emit(is_creating = True)
|
||||
|
||||
# Create the backup.
|
||||
backup_zip_file, backup_meta_data = self._cura_api.backups.createBackup()
|
||||
if not backup_zip_file or not backup_meta_data:
|
||||
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not create backup.")
|
||||
return
|
||||
|
||||
# Create an upload entry for the backup.
|
||||
timestamp = datetime.now().isoformat()
|
||||
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
|
||||
backup_upload_url = self._requestBackupUpload(backup_meta_data, len(backup_zip_file))
|
||||
if not backup_upload_url:
|
||||
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not upload backup.")
|
||||
return
|
||||
|
||||
# Upload the backup to storage.
|
||||
upload_backup_job = UploadBackupJob(backup_upload_url, backup_zip_file)
|
||||
upload_backup_job.finished.connect(self._onUploadFinished)
|
||||
upload_backup_job.start()
|
||||
|
||||
def _onUploadFinished(self, job: "UploadBackupJob") -> None:
|
||||
if job.backup_upload_error_message != "":
|
||||
# If the job contains an error message we pass it along so the UI can display it.
|
||||
self.creatingStateChanged.emit(is_creating = False, error_message = job.backup_upload_error_message)
|
||||
else:
|
||||
self.creatingStateChanged.emit(is_creating = False)
|
||||
|
||||
def restoreBackup(self, backup: Dict[str, Any]) -> None:
|
||||
self.restoringStateChanged.emit(is_restoring = True)
|
||||
download_url = backup.get("download_url")
|
||||
if not download_url:
|
||||
# If there is no download URL, we can't restore the backup.
|
||||
return self._emitRestoreError()
|
||||
|
||||
download_package = requests.get(download_url, stream = True)
|
||||
if download_package.status_code >= 300:
|
||||
# Something went wrong when attempting to download the backup.
|
||||
Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text)
|
||||
return self._emitRestoreError()
|
||||
|
||||
# We store the file in a temporary path fist to ensure integrity.
|
||||
temporary_backup_file = NamedTemporaryFile(delete = False)
|
||||
with open(temporary_backup_file.name, "wb") as write_backup:
|
||||
for chunk in download_package:
|
||||
write_backup.write(chunk)
|
||||
|
||||
if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash", "")):
|
||||
# Don't restore the backup if the MD5 hashes do not match.
|
||||
# This can happen if the download was interrupted.
|
||||
Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.")
|
||||
return self._emitRestoreError()
|
||||
|
||||
# Tell Cura to place the backup back in the user data folder.
|
||||
with open(temporary_backup_file.name, "rb") as read_backup:
|
||||
self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("metadata", {}))
|
||||
self.restoringStateChanged.emit(is_restoring = False)
|
||||
|
||||
def _emitRestoreError(self) -> None:
|
||||
self.restoringStateChanged.emit(is_restoring = False,
|
||||
error_message = catalog.i18nc("@info:backup_status",
|
||||
"There was an error trying to restore your backup."))
|
||||
|
||||
# Verify the MD5 hash of a file.
|
||||
# \param file_path Full path to the file.
|
||||
# \param known_hash The known MD5 hash of the file.
|
||||
# \return: Success or not.
|
||||
@staticmethod
|
||||
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
|
||||
with open(file_path, "rb") as read_backup:
|
||||
local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8")
|
||||
return known_hash == local_md5_hash
|
||||
|
||||
def deleteBackup(self, backup_id: str) -> bool:
|
||||
access_token = self._cura_api.account.accessToken
|
||||
if not access_token:
|
||||
Logger.log("w", "Could not get access token.")
|
||||
return False
|
||||
|
||||
delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
if delete_backup.status_code >= 300:
|
||||
Logger.log("w", "Could not delete backup: %s", delete_backup.text)
|
||||
return False
|
||||
return True
|
||||
|
||||
# Request a backup upload slot from the API.
|
||||
# \param backup_metadata: A dict containing some meta data about the backup.
|
||||
# \param backup_size The size of the backup file in bytes.
|
||||
# \return: The upload URL for the actual backup file if successful, otherwise None.
|
||||
def _requestBackupUpload(self, backup_metadata: Dict[str, Any], backup_size: int) -> Optional[str]:
|
||||
access_token = self._cura_api.account.accessToken
|
||||
if not access_token:
|
||||
Logger.log("w", "Could not get access token.")
|
||||
return None
|
||||
|
||||
backup_upload_request = requests.put(self.BACKUP_URL, json = {
|
||||
"data": {
|
||||
"backup_size": backup_size,
|
||||
"metadata": backup_metadata
|
||||
}
|
||||
}, headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
|
||||
# Any status code of 300 or above indicates an error.
|
||||
if backup_upload_request.status_code >= 300:
|
||||
Logger.log("w", "Could not request backup upload: %s", backup_upload_request.text)
|
||||
return None
|
||||
|
||||
return backup_upload_request.json()["data"]["upload_url"]
|
162
plugins/CuraDrive/src/DrivePluginExtension.py
Normal file
162
plugins/CuraDrive/src/DrivePluginExtension.py
Normal file
@ -0,0 +1,162 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
|
||||
from UM.Extension import Extension
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from .Settings import Settings
|
||||
from .DriveApiService import DriveApiService
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
# The DivePluginExtension provides functionality to backup and restore your Cura configuration to Ultimaker's cloud.
|
||||
class DrivePluginExtension(QObject, Extension):
|
||||
|
||||
# Signal emitted when the list of backups changed.
|
||||
backupsChanged = pyqtSignal()
|
||||
|
||||
# Signal emitted when restoring has started. Needed to prevent parallel restoring.
|
||||
restoringStateChanged = pyqtSignal()
|
||||
|
||||
# Signal emitted when creating has started. Needed to prevent parallel creation of backups.
|
||||
creatingStateChanged = pyqtSignal()
|
||||
|
||||
# Signal emitted when preferences changed (like auto-backup).
|
||||
preferencesChanged = pyqtSignal()
|
||||
|
||||
DATE_FORMAT = "%d/%m/%Y %H:%M:%S"
|
||||
|
||||
def __init__(self) -> None:
|
||||
QObject.__init__(self, None)
|
||||
Extension.__init__(self)
|
||||
|
||||
# Local data caching for the UI.
|
||||
self._drive_window = None # type: Optional[QObject]
|
||||
self._backups = [] # type: List[Dict[str, Any]]
|
||||
self._is_restoring_backup = False
|
||||
self._is_creating_backup = False
|
||||
|
||||
# Initialize services.
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
self._drive_api_service = DriveApiService()
|
||||
|
||||
# Attach signals.
|
||||
CuraApplication.getInstance().getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged)
|
||||
self._drive_api_service.restoringStateChanged.connect(self._onRestoringStateChanged)
|
||||
self._drive_api_service.creatingStateChanged.connect(self._onCreatingStateChanged)
|
||||
|
||||
# Register preferences.
|
||||
preferences.addPreference(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, False)
|
||||
preferences.addPreference(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY,
|
||||
datetime.now().strftime(self.DATE_FORMAT))
|
||||
|
||||
# Register the menu item
|
||||
self.addMenuItem(catalog.i18nc("@item:inmenu", "Manage backups"), self.showDriveWindow)
|
||||
|
||||
# Make auto-backup on boot if required.
|
||||
CuraApplication.getInstance().engineCreatedSignal.connect(self._autoBackup)
|
||||
|
||||
def showDriveWindow(self) -> None:
|
||||
if not self._drive_window:
|
||||
plugin_dir_path = CuraApplication.getInstance().getPluginRegistry().getPluginPath("CuraDrive")
|
||||
path = os.path.join(plugin_dir_path, "src", "qml", "main.qml")
|
||||
self._drive_window = CuraApplication.getInstance().createQmlComponent(path, {"CuraDrive": self})
|
||||
self.refreshBackups()
|
||||
if self._drive_window:
|
||||
self._drive_window.show()
|
||||
|
||||
def _autoBackup(self) -> None:
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
if preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._isLastBackupTooLongAgo():
|
||||
self.createBackup()
|
||||
|
||||
def _isLastBackupTooLongAgo(self) -> bool:
|
||||
current_date = datetime.now()
|
||||
last_backup_date = self._getLastBackupDate()
|
||||
date_diff = current_date - last_backup_date
|
||||
return date_diff.days > 1
|
||||
|
||||
def _getLastBackupDate(self) -> "datetime":
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
last_backup_date = preferences.getValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY)
|
||||
return datetime.strptime(last_backup_date, self.DATE_FORMAT)
|
||||
|
||||
def _storeBackupDate(self) -> None:
|
||||
backup_date = datetime.now().strftime(self.DATE_FORMAT)
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
preferences.setValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, backup_date)
|
||||
|
||||
def _onLoginStateChanged(self, logged_in: bool = False) -> None:
|
||||
if logged_in:
|
||||
self.refreshBackups()
|
||||
|
||||
def _onRestoringStateChanged(self, is_restoring: bool = False, error_message: str = None) -> None:
|
||||
self._is_restoring_backup = is_restoring
|
||||
self.restoringStateChanged.emit()
|
||||
if error_message:
|
||||
Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show()
|
||||
|
||||
def _onCreatingStateChanged(self, is_creating: bool = False, error_message: str = None) -> None:
|
||||
self._is_creating_backup = is_creating
|
||||
self.creatingStateChanged.emit()
|
||||
if error_message:
|
||||
Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show()
|
||||
else:
|
||||
self._storeBackupDate()
|
||||
if not is_creating and not error_message:
|
||||
# We've finished creating a new backup, to the list has to be updated.
|
||||
self.refreshBackups()
|
||||
|
||||
@pyqtSlot(bool, name = "toggleAutoBackup")
|
||||
def toggleAutoBackup(self, enabled: bool) -> None:
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
preferences.setValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, enabled)
|
||||
|
||||
@pyqtProperty(bool, notify = preferencesChanged)
|
||||
def autoBackupEnabled(self) -> bool:
|
||||
preferences = CuraApplication.getInstance().getPreferences()
|
||||
return bool(preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY))
|
||||
|
||||
@pyqtProperty("QVariantList", notify = backupsChanged)
|
||||
def backups(self) -> List[Dict[str, Any]]:
|
||||
return self._backups
|
||||
|
||||
@pyqtSlot(name = "refreshBackups")
|
||||
def refreshBackups(self) -> None:
|
||||
self._backups = self._drive_api_service.getBackups()
|
||||
self.backupsChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify = restoringStateChanged)
|
||||
def isRestoringBackup(self) -> bool:
|
||||
return self._is_restoring_backup
|
||||
|
||||
@pyqtProperty(bool, notify = creatingStateChanged)
|
||||
def isCreatingBackup(self) -> bool:
|
||||
return self._is_creating_backup
|
||||
|
||||
@pyqtSlot(str, name = "restoreBackup")
|
||||
def restoreBackup(self, backup_id: str) -> None:
|
||||
for backup in self._backups:
|
||||
if backup.get("backup_id") == backup_id:
|
||||
self._drive_api_service.restoreBackup(backup)
|
||||
return
|
||||
Logger.log("w", "Unable to find backup with the ID %s", backup_id)
|
||||
|
||||
@pyqtSlot(name = "createBackup")
|
||||
def createBackup(self) -> None:
|
||||
self._drive_api_service.createBackup()
|
||||
|
||||
@pyqtSlot(str, name = "deleteBackup")
|
||||
def deleteBackup(self, backup_id: str) -> None:
|
||||
self._drive_api_service.deleteBackup(backup_id)
|
||||
self.refreshBackups()
|
13
plugins/CuraDrive/src/Settings.py
Normal file
13
plugins/CuraDrive/src/Settings.py
Normal file
@ -0,0 +1,13 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from cura import UltimakerCloudAuthentication
|
||||
|
||||
|
||||
class Settings:
|
||||
# Keeps the plugin settings.
|
||||
DRIVE_API_VERSION = 1
|
||||
DRIVE_API_URL = "{}/cura-drive/v{}".format(UltimakerCloudAuthentication.CuraCloudAPIRoot, str(DRIVE_API_VERSION))
|
||||
|
||||
AUTO_BACKUP_ENABLED_PREFERENCE_KEY = "cura_drive/auto_backup_enabled"
|
||||
AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY = "cura_drive/auto_backup_date"
|
41
plugins/CuraDrive/src/UploadBackupJob.py
Normal file
41
plugins/CuraDrive/src/UploadBackupJob.py
Normal file
@ -0,0 +1,41 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import requests
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class UploadBackupJob(Job):
|
||||
MESSAGE_TITLE = catalog.i18nc("@info:title", "Backups")
|
||||
|
||||
# This job is responsible for uploading the backup file to cloud storage.
|
||||
# As it can take longer than some other tasks, we schedule this using a Cura Job.
|
||||
def __init__(self, signed_upload_url: str, backup_zip: bytes) -> None:
|
||||
super().__init__()
|
||||
self._signed_upload_url = signed_upload_url
|
||||
self._backup_zip = backup_zip
|
||||
self._upload_success = False
|
||||
self.backup_upload_error_message = ""
|
||||
|
||||
def run(self) -> None:
|
||||
upload_message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1)
|
||||
upload_message.show()
|
||||
|
||||
backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip)
|
||||
upload_message.hide()
|
||||
|
||||
if backup_upload.status_code >= 300:
|
||||
self.backup_upload_error_message = backup_upload.text
|
||||
Logger.log("w", "Could not upload backup file: %s", backup_upload.text)
|
||||
Message(catalog.i18nc("@info:backup_status", "There was an error while uploading your backup."), title = self.MESSAGE_TITLE).show()
|
||||
else:
|
||||
self._upload_success = True
|
||||
Message(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."), title = self.MESSAGE_TITLE).show()
|
||||
|
||||
self.finished.emit(self)
|
0
plugins/CuraDrive/src/__init__.py
Normal file
0
plugins/CuraDrive/src/__init__.py
Normal file
39
plugins/CuraDrive/src/qml/components/BackupList.qml
Normal file
39
plugins/CuraDrive/src/qml/components/BackupList.qml
Normal file
@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.2
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.1 as UM
|
||||
|
||||
ScrollView
|
||||
{
|
||||
property alias model: backupList.model
|
||||
width: parent.width
|
||||
clip: true
|
||||
ListView
|
||||
{
|
||||
id: backupList
|
||||
width: parent.width
|
||||
delegate: Item
|
||||
{
|
||||
// Add a margin, otherwise the scrollbar is on top of the right most component
|
||||
width: parent.width - UM.Theme.getSize("default_margin").width
|
||||
height: childrenRect.height
|
||||
|
||||
BackupListItem
|
||||
{
|
||||
id: backupListItem
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: divider
|
||||
color: UM.Theme.getColor("lining")
|
||||
height: UM.Theme.getSize("default_lining").height
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
plugins/CuraDrive/src/qml/components/BackupListFooter.qml
Normal file
46
plugins/CuraDrive/src/qml/components/BackupListFooter.qml
Normal file
@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
import "../components"
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: backupListFooter
|
||||
width: parent.width
|
||||
property bool showInfoButton: false
|
||||
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: infoButton
|
||||
text: catalog.i18nc("@button", "Want more?")
|
||||
iconSource: UM.Theme.getIcon("info")
|
||||
onClicked: Qt.openUrlExternally("https://goo.gl/forms/QACEP8pP3RV60QYG2")
|
||||
visible: backupListFooter.showInfoButton
|
||||
}
|
||||
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: createBackupButton
|
||||
text: catalog.i18nc("@button", "Backup Now")
|
||||
iconSource: UM.Theme.getIcon("plus")
|
||||
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
|
||||
onClicked: CuraDrive.createBackup()
|
||||
busy: CuraDrive.isCreatingBackup
|
||||
}
|
||||
|
||||
Cura.CheckBoxWithTooltip
|
||||
{
|
||||
id: autoBackupEnabled
|
||||
checked: CuraDrive.autoBackupEnabled
|
||||
onClicked: CuraDrive.toggleAutoBackup(autoBackupEnabled.checked)
|
||||
text: catalog.i18nc("@checkbox:description", "Auto Backup")
|
||||
tooltip: catalog.i18nc("@checkbox:description", "Automatically create a backup each day that Cura is started.")
|
||||
}
|
||||
}
|
113
plugins/CuraDrive/src/qml/components/BackupListItem.qml
Normal file
113
plugins/CuraDrive/src/qml/components/BackupListItem.qml
Normal file
@ -0,0 +1,113 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Dialogs 1.1
|
||||
|
||||
import UM 1.1 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
Item
|
||||
{
|
||||
id: backupListItem
|
||||
width: parent.width
|
||||
height: showDetails ? dataRow.height + backupDetails.height : dataRow.height
|
||||
property bool showDetails: false
|
||||
|
||||
// Backup details toggle animation.
|
||||
Behavior on height
|
||||
{
|
||||
PropertyAnimation
|
||||
{
|
||||
duration: 70
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: dataRow
|
||||
spacing: UM.Theme.getSize("wide_margin").width
|
||||
width: parent.width
|
||||
height: 50 * screenScaleFactor
|
||||
|
||||
UM.SimpleButton
|
||||
{
|
||||
width: UM.Theme.getSize("section_icon").width
|
||||
height: UM.Theme.getSize("section_icon").height
|
||||
color: UM.Theme.getColor("small_button_text")
|
||||
hoverColor: UM.Theme.getColor("small_button_text_hover")
|
||||
iconSource: UM.Theme.getIcon("info")
|
||||
onClicked: backupListItem.showDetails = !backupListItem.showDetails
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: new Date(modelData.generated_time).toLocaleString(UM.Preferences.getValue("general/language"))
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
Layout.minimumWidth: 100 * screenScaleFactor
|
||||
Layout.maximumWidth: 500 * screenScaleFactor
|
||||
Layout.fillWidth: true
|
||||
font: UM.Theme.getFont("default")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: modelData.metadata.description
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
Layout.minimumWidth: 100 * screenScaleFactor
|
||||
Layout.maximumWidth: 500 * screenScaleFactor
|
||||
Layout.fillWidth: true
|
||||
font: UM.Theme.getFont("default")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
text: catalog.i18nc("@button", "Restore")
|
||||
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
|
||||
onClicked: confirmRestoreDialog.visible = true
|
||||
}
|
||||
|
||||
UM.SimpleButton
|
||||
{
|
||||
width: UM.Theme.getSize("message_close").width
|
||||
height: UM.Theme.getSize("message_close").height
|
||||
color: UM.Theme.getColor("small_button_text")
|
||||
hoverColor: UM.Theme.getColor("small_button_text_hover")
|
||||
iconSource: UM.Theme.getIcon("cross1")
|
||||
onClicked: confirmDeleteDialog.visible = true
|
||||
}
|
||||
}
|
||||
|
||||
BackupListItemDetails
|
||||
{
|
||||
id: backupDetails
|
||||
backupDetailsData: modelData
|
||||
width: parent.width
|
||||
visible: parent.showDetails
|
||||
anchors.top: dataRow.bottom
|
||||
}
|
||||
|
||||
MessageDialog
|
||||
{
|
||||
id: confirmDeleteDialog
|
||||
title: catalog.i18nc("@dialog:title", "Delete Backup")
|
||||
text: catalog.i18nc("@dialog:info", "Are you sure you want to delete this backup? This cannot be undone.")
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
onYes: CuraDrive.deleteBackup(modelData.backup_id)
|
||||
}
|
||||
|
||||
MessageDialog
|
||||
{
|
||||
id: confirmRestoreDialog
|
||||
title: catalog.i18nc("@dialog:title", "Restore Backup")
|
||||
text: catalog.i18nc("@dialog:info", "You will need to restart Cura before your backup is restored. Do you want to close Cura now?")
|
||||
standardButtons: StandardButton.Yes | StandardButton.No
|
||||
onYes: CuraDrive.restoreBackup(modelData.backup_id)
|
||||
}
|
||||
}
|
@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.1 as UM
|
||||
|
||||
ColumnLayout
|
||||
{
|
||||
id: backupDetails
|
||||
width: parent.width
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
property var backupDetailsData
|
||||
|
||||
// Cura version
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("application")
|
||||
label: catalog.i18nc("@backuplist:label", "Cura Version")
|
||||
value: backupDetailsData.metadata.cura_release
|
||||
}
|
||||
|
||||
// Machine count.
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("printer_single")
|
||||
label: catalog.i18nc("@backuplist:label", "Machines")
|
||||
value: backupDetailsData.metadata.machine_count
|
||||
}
|
||||
|
||||
// Material count
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("category_material")
|
||||
label: catalog.i18nc("@backuplist:label", "Materials")
|
||||
value: backupDetailsData.metadata.material_count
|
||||
}
|
||||
|
||||
// Profile count.
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("settings")
|
||||
label: catalog.i18nc("@backuplist:label", "Profiles")
|
||||
value: backupDetailsData.metadata.profile_count
|
||||
}
|
||||
|
||||
// Plugin count.
|
||||
BackupListItemDetailsRow
|
||||
{
|
||||
iconSource: UM.Theme.getIcon("plugin")
|
||||
label: catalog.i18nc("@backuplist:label", "Plugins")
|
||||
value: backupDetailsData.metadata.plugin_count
|
||||
}
|
||||
|
||||
// Spacer.
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("default_margin").height
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: detailsRow
|
||||
width: parent.width
|
||||
height: 40 * screenScaleFactor
|
||||
|
||||
property alias iconSource: icon.source
|
||||
property alias label: detailName.text
|
||||
property alias value: detailValue.text
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: icon
|
||||
width: 18 * screenScaleFactor
|
||||
height: width
|
||||
source: ""
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: detailName
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
Layout.minimumWidth: 50 * screenScaleFactor
|
||||
Layout.maximumWidth: 100 * screenScaleFactor
|
||||
Layout.fillWidth: true
|
||||
font: UM.Theme.getFont("default")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: detailValue
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
Layout.minimumWidth: 50 * screenScaleFactor
|
||||
Layout.maximumWidth: 100 * screenScaleFactor
|
||||
Layout.fillWidth: true
|
||||
font: UM.Theme.getFont("default")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
BIN
plugins/CuraDrive/src/qml/images/icon.png
Normal file
BIN
plugins/CuraDrive/src/qml/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
44
plugins/CuraDrive/src/qml/main.qml
Normal file
44
plugins/CuraDrive/src/qml/main.qml
Normal file
@ -0,0 +1,44 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
import "components"
|
||||
import "pages"
|
||||
|
||||
Window
|
||||
{
|
||||
id: curaDriveDialog
|
||||
minimumWidth: Math.round(UM.Theme.getSize("modal_window_minimum").width)
|
||||
minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height)
|
||||
maximumWidth: Math.round(minimumWidth * 1.2)
|
||||
maximumHeight: Math.round(minimumHeight * 1.2)
|
||||
width: minimumWidth
|
||||
height: minimumHeight
|
||||
color: UM.Theme.getColor("main_background")
|
||||
title: catalog.i18nc("@title:window", "Cura Backups")
|
||||
|
||||
// Globally available.
|
||||
UM.I18nCatalog
|
||||
{
|
||||
id: catalog
|
||||
name: "cura"
|
||||
}
|
||||
|
||||
WelcomePage
|
||||
{
|
||||
id: welcomePage
|
||||
visible: !Cura.API.account.isLoggedIn
|
||||
}
|
||||
|
||||
BackupsPage
|
||||
{
|
||||
id: backupsPage
|
||||
visible: Cura.API.account.isLoggedIn
|
||||
}
|
||||
}
|
75
plugins/CuraDrive/src/qml/pages/BackupsPage.qml
Normal file
75
plugins/CuraDrive/src/qml/pages/BackupsPage.qml
Normal file
@ -0,0 +1,75 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
import "../components"
|
||||
|
||||
Item
|
||||
{
|
||||
id: backupsPage
|
||||
anchors.fill: parent
|
||||
anchors.margins: UM.Theme.getSize("wide_margin").width
|
||||
|
||||
ColumnLayout
|
||||
{
|
||||
spacing: UM.Theme.getSize("wide_margin").height
|
||||
width: parent.width
|
||||
anchors.fill: parent
|
||||
|
||||
Label
|
||||
{
|
||||
id: backupTitle
|
||||
text: catalog.i18nc("@title", "My Backups")
|
||||
font: UM.Theme.getFont("large")
|
||||
color: UM.Theme.getColor("text")
|
||||
Layout.fillWidth: true
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@empty_state",
|
||||
"You don't have any backups currently. Use the 'Backup Now' button to create one.")
|
||||
width: parent.width
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Label.WordWrap
|
||||
visible: backupList.model.length == 0
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
BackupList
|
||||
{
|
||||
id: backupList
|
||||
model: CuraDrive.backups
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: catalog.i18nc("@backup_limit_info",
|
||||
"During the preview phase, you'll be limited to 5 visible backups. Remove a backup to see older ones.")
|
||||
width: parent.width
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Label.WordWrap
|
||||
visible: backupList.model.length > 4
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
BackupListFooter
|
||||
{
|
||||
id: backupListFooter
|
||||
showInfoButton: backupList.model.length > 4
|
||||
}
|
||||
}
|
||||
}
|
56
plugins/CuraDrive/src/qml/pages/WelcomePage.qml
Normal file
56
plugins/CuraDrive/src/qml/pages/WelcomePage.qml
Normal file
@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
import "../components"
|
||||
|
||||
|
||||
Column
|
||||
{
|
||||
id: welcomePage
|
||||
spacing: UM.Theme.getSize("wide_margin").height
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
anchors.centerIn: parent
|
||||
|
||||
Image
|
||||
{
|
||||
id: profileImage
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "../images/icon.png"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: Math.round(parent.width / 4)
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: welcomeTextLabel
|
||||
text: catalog.i18nc("@description", "Backup and synchronize your Cura settings.")
|
||||
width: Math.round(parent.width / 2)
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
wrapMode: Label.WordWrap
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: loginButton
|
||||
width: UM.Theme.getSize("account_button").width
|
||||
height: UM.Theme.getSize("account_button").height
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: catalog.i18nc("@button", "Sign in")
|
||||
onClicked: Cura.API.account.login()
|
||||
fixedWidthMode: true
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ message Object
|
||||
bytes normals = 3; //An array of 3 floats.
|
||||
bytes indices = 4; //An array of ints.
|
||||
repeated Setting settings = 5; // Setting override per object, overruling the global settings.
|
||||
string name = 6;
|
||||
string name = 6; //Mesh name
|
||||
}
|
||||
|
||||
message Progress
|
||||
@ -58,6 +58,7 @@ message Polygon {
|
||||
MoveCombingType = 8;
|
||||
MoveRetractionType = 9;
|
||||
SupportInterfaceType = 10;
|
||||
PrimeTowerType = 11;
|
||||
}
|
||||
Type type = 1; // Type of move
|
||||
bytes points = 2; // The points of the polygon, or two points if only a line segment (Currently only line segments are used)
|
||||
@ -108,8 +109,9 @@ message PrintTimeMaterialEstimates { // The print time for each feature and mate
|
||||
float time_travel = 9;
|
||||
float time_retract = 10;
|
||||
float time_support_interface = 11;
|
||||
float time_prime_tower = 12;
|
||||
|
||||
repeated MaterialEstimates materialEstimates = 12; // materialEstimates data
|
||||
repeated MaterialEstimates materialEstimates = 13; // materialEstimates data
|
||||
}
|
||||
|
||||
message MaterialEstimates {
|
||||
|
@ -86,8 +86,8 @@ class CuraEngineBackend(QObject, Backend):
|
||||
self._layer_view_active = False #type: bool
|
||||
self._onActiveViewChanged()
|
||||
|
||||
self._stored_layer_data = [] #type: List[Arcus.PythonMessage]
|
||||
self._stored_optimized_layer_data = {} #type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
|
||||
self._stored_layer_data = [] # type: List[Arcus.PythonMessage]
|
||||
self._stored_optimized_layer_data = {} # type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
|
||||
|
||||
self._scene = self._application.getController().getScene() #type: Scene
|
||||
self._scene.sceneChanged.connect(self._onSceneChanged)
|
||||
@ -151,7 +151,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
if self._multi_build_plate_model:
|
||||
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveViewChanged)
|
||||
|
||||
self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||
self._application.getMachineManager().globalContainerChanged.connect(self._onGlobalStackChanged)
|
||||
self._onGlobalStackChanged()
|
||||
|
||||
# extruder enable / disable. Actually wanted to use machine manager here, but the initialization order causes it to crash
|
||||
@ -246,7 +246,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
num_objects = self._numObjectsPerBuildPlate()
|
||||
|
||||
self._stored_layer_data = []
|
||||
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
|
||||
|
||||
|
||||
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
|
||||
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #Because we created this attribute above.
|
||||
@ -254,7 +254,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
if self._build_plates_to_be_sliced:
|
||||
self.slice()
|
||||
return
|
||||
|
||||
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
|
||||
if self._application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
|
||||
self._application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
|
||||
|
||||
@ -411,7 +411,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
|
||||
if job.getResult() == StartJobResult.NothingToSlice:
|
||||
if self._application.platformActivity:
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."),
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume or are assigned to a disabled extruder. Please scale or rotate models to fit, or enable an extruder."),
|
||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||
self._error_message.show()
|
||||
self.setState(BackendState.Error)
|
||||
@ -821,7 +821,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
extruder.propertyChanged.disconnect(self._onSettingChanged)
|
||||
extruder.containersChanged.disconnect(self._onChanged)
|
||||
|
||||
self._global_container_stack = self._application.getGlobalContainerStack()
|
||||
self._global_container_stack = self._application.getMachineManager().activeMachine
|
||||
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.connect(self._onSettingChanged) # Note: Only starts slicing when the value changed.
|
||||
@ -833,7 +833,10 @@ class CuraEngineBackend(QObject, Backend):
|
||||
self._onChanged()
|
||||
|
||||
def _onProcessLayersFinished(self, job: ProcessSlicedLayersJob) -> None:
|
||||
del self._stored_optimized_layer_data[job.getBuildPlate()]
|
||||
if job.getBuildPlate() in self._stored_optimized_layer_data:
|
||||
del self._stored_optimized_layer_data[job.getBuildPlate()]
|
||||
else:
|
||||
Logger.log("w", "The optimized layer data was already deleted for buildplate %s", job.getBuildPlate())
|
||||
self._process_layers_job = None
|
||||
Logger.log("d", "See if there is more to slice(2)...")
|
||||
self._invokeSlice()
|
||||
|
@ -137,6 +137,7 @@ class ProcessSlicedLayersJob(Job):
|
||||
extruder = polygon.extruder
|
||||
|
||||
line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array
|
||||
|
||||
line_types = line_types.reshape((-1,1))
|
||||
|
||||
points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array
|
||||
|
@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
|
||||
@ -13,8 +12,6 @@ from UM.Logger import Logger
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob
|
||||
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
|
||||
|
||||
@ -53,6 +50,7 @@ class FirmwareUpdateChecker(Extension):
|
||||
|
||||
def _onContainerAdded(self, container):
|
||||
# Only take care when a new GlobalStack was added
|
||||
from cura.Settings.GlobalStack import GlobalStack # otherwise circular imports
|
||||
if isinstance(container, GlobalStack):
|
||||
self.checkFirmwareVersion(container, True)
|
||||
|
||||
@ -76,7 +74,7 @@ class FirmwareUpdateChecker(Extension):
|
||||
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(container_name))
|
||||
return
|
||||
|
||||
self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent,
|
||||
self._check_job = FirmwareUpdateCheckerJob(silent = silent,
|
||||
machine_name = container_name, metadata = metadata,
|
||||
callback = self._onActionTriggered)
|
||||
self._check_job.start()
|
||||
|
@ -25,15 +25,14 @@ class FirmwareUpdateCheckerJob(Job):
|
||||
ZERO_VERSION = Version(STRING_ZERO_VERSION)
|
||||
EPSILON_VERSION = Version(STRING_EPSILON_VERSION)
|
||||
|
||||
def __init__(self, container, silent, machine_name, metadata, callback) -> None:
|
||||
def __init__(self, silent, machine_name, metadata, callback) -> None:
|
||||
super().__init__()
|
||||
self._container = container
|
||||
self.silent = silent
|
||||
self._callback = callback
|
||||
|
||||
self._machine_name = machine_name
|
||||
self._metadata = metadata
|
||||
self._lookups = None # type:Optional[FirmwareUpdateCheckerLookup]
|
||||
self._lookups = FirmwareUpdateCheckerLookup(self._machine_name, self._metadata)
|
||||
self._headers = {} # type:Dict[str, str] # Don't set headers yet.
|
||||
|
||||
def getUrlResponse(self, url: str) -> str:
|
||||
@ -45,7 +44,6 @@ class FirmwareUpdateCheckerJob(Job):
|
||||
result = response.read().decode("utf-8")
|
||||
except URLError:
|
||||
Logger.log("w", "Could not reach '{0}', if this URL is old, consider removal.".format(url))
|
||||
|
||||
return result
|
||||
|
||||
def parseVersionResponse(self, response: str) -> Version:
|
||||
@ -70,9 +68,6 @@ class FirmwareUpdateCheckerJob(Job):
|
||||
return max_version
|
||||
|
||||
def run(self):
|
||||
if self._lookups is None:
|
||||
self._lookups = FirmwareUpdateCheckerLookup(self._machine_name, self._metadata)
|
||||
|
||||
try:
|
||||
# Initialize a Preference that stores the last version checked for this printer.
|
||||
Application.getInstance().getPreferences().addPreference(
|
||||
@ -83,13 +78,10 @@ class FirmwareUpdateCheckerJob(Job):
|
||||
application_version = Application.getInstance().getVersion()
|
||||
self._headers = {"User-Agent": "%s - %s" % (application_name, application_version)}
|
||||
|
||||
# get machine name from the definition container
|
||||
machine_name = self._container.definition.getName()
|
||||
|
||||
# If it is not None, then we compare between the checked_version and the current_version
|
||||
machine_id = self._lookups.getMachineId()
|
||||
if machine_id is not None:
|
||||
Logger.log("i", "You have a(n) {0} in the printer list. Let's check the firmware!".format(machine_name))
|
||||
Logger.log("i", "You have a(n) {0} in the printer list. Do firmware-check.".format(self._machine_name))
|
||||
|
||||
current_version = self.getCurrentVersion()
|
||||
|
||||
@ -105,18 +97,20 @@ class FirmwareUpdateCheckerJob(Job):
|
||||
# If the checked_version is "", it's because is the first time we check firmware and in this case
|
||||
# we will not show the notification, but we will store it for the next time
|
||||
Application.getInstance().getPreferences().setValue(setting_key_str, current_version)
|
||||
Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s", machine_name, checked_version, current_version)
|
||||
Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s",
|
||||
self._machine_name, checked_version, current_version)
|
||||
|
||||
# The first time we want to store the current version, the notification will not be shown,
|
||||
# because the new version of Cura will be release before the firmware and we don't want to
|
||||
# notify the user when no new firmware version is available.
|
||||
if (checked_version != "") and (checked_version != current_version):
|
||||
Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE")
|
||||
message = FirmwareUpdateCheckerMessage(machine_id, machine_name, self._lookups.getRedirectUserUrl())
|
||||
message = FirmwareUpdateCheckerMessage(machine_id, self._machine_name,
|
||||
self._lookups.getRedirectUserUrl())
|
||||
message.actionTriggered.connect(self._callback)
|
||||
message.show()
|
||||
else:
|
||||
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(machine_name))
|
||||
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(self._machine_name))
|
||||
|
||||
except Exception as e:
|
||||
Logger.log("w", "Failed to check for new version: %s", e)
|
||||
|
@ -18,7 +18,7 @@ class FirmwareUpdateCheckerLookup:
|
||||
self._machine_id = machine_json.get("id")
|
||||
self._machine_name = machine_name.lower() # Lower in-case upper-case chars are added to the original json.
|
||||
self._check_urls = [] # type:List[str]
|
||||
for check_url in machine_json.get("check_urls"):
|
||||
for check_url in machine_json.get("check_urls", []):
|
||||
self._check_urls.append(check_url)
|
||||
self._redirect_user = machine_json.get("update_url")
|
||||
|
||||
|
@ -0,0 +1,62 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import pytest
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from UM.Version import Version
|
||||
|
||||
import FirmwareUpdateChecker
|
||||
|
||||
json_data = \
|
||||
{
|
||||
"ned":
|
||||
{
|
||||
"id": 1,
|
||||
"name": "ned",
|
||||
"check_urls": [""],
|
||||
"update_url": "https://ultimaker.com/en/resources/20500-upgrade-firmware",
|
||||
"version_parser": "default"
|
||||
},
|
||||
"olivia":
|
||||
{
|
||||
"id": 3,
|
||||
"name": "olivia",
|
||||
"check_urls": [""],
|
||||
"update_url": "https://ultimaker.com/en/resources/20500-upgrade-firmware",
|
||||
"version_parser": "default"
|
||||
},
|
||||
"emmerson":
|
||||
{
|
||||
"id": 5,
|
||||
"name": "emmerson",
|
||||
"check_urls": [""],
|
||||
"update_url": "https://ultimaker.com/en/resources/20500-upgrade-firmware",
|
||||
"version_parser": "default"
|
||||
}
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize("name, id", [
|
||||
("ned" , 1),
|
||||
("olivia" , 3),
|
||||
("emmerson", 5),
|
||||
])
|
||||
def test_FirmwareUpdateCheckerLookup(id, name):
|
||||
lookup = FirmwareUpdateChecker.FirmwareUpdateCheckerLookup.FirmwareUpdateCheckerLookup(name, json_data.get(name))
|
||||
|
||||
assert lookup.getMachineName() == name
|
||||
assert lookup.getMachineId() == id
|
||||
assert len(lookup.getCheckUrls()) >= 1
|
||||
assert lookup.getRedirectUserUrl() is not None
|
||||
|
||||
@pytest.mark.parametrize("name, version", [
|
||||
("ned" , Version("5.1.2.3")),
|
||||
("olivia" , Version("4.3.2.1")),
|
||||
("emmerson", Version("6.7.8.1")),
|
||||
])
|
||||
def test_FirmwareUpdateCheckerJob_getCurrentVersion(name, version):
|
||||
machine_data = json_data.get(name)
|
||||
job = FirmwareUpdateChecker.FirmwareUpdateCheckerJob.FirmwareUpdateCheckerJob(False, name, machine_data, MagicMock)
|
||||
job.getUrlResponse = MagicMock(return_value = str(version)) # Pretend like we got a good response from the server
|
||||
assert job.getCurrentVersion() == version
|
0
plugins/FirmwareUpdateChecker/tests/__init__.py
Normal file
0
plugins/FirmwareUpdateChecker/tests/__init__.py
Normal file
@ -57,7 +57,7 @@ class FirmwareUpdaterMachineAction(MachineAction):
|
||||
outputDeviceCanUpdateFirmwareChanged = pyqtSignal()
|
||||
@pyqtProperty(QObject, notify = outputDeviceCanUpdateFirmwareChanged)
|
||||
def firmwareUpdater(self) -> Optional["FirmwareUpdater"]:
|
||||
if self._active_output_device and self._active_output_device.activePrinter.getController().can_update_firmware:
|
||||
if self._active_output_device and self._active_output_device.activePrinter and self._active_output_device.activePrinter.getController().can_update_firmware:
|
||||
self._active_firmware_updater = self._active_output_device.getFirmwareUpdater()
|
||||
return self._active_firmware_updater
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
// Copyright (c) 2017 Ultimaker B.V.
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 1.4
|
||||
@ -7,31 +8,27 @@ import UM 1.3 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
|
||||
Item
|
||||
// We show a nice overlay on the 3D viewer when the current output device has no monitor view
|
||||
Rectangle
|
||||
{
|
||||
// We show a nice overlay on the 3D viewer when the current output device has no monitor view
|
||||
Rectangle
|
||||
id: viewportOverlay
|
||||
|
||||
color: UM.Theme.getColor("viewport_overlay")
|
||||
anchors.fill: parent
|
||||
|
||||
// This mouse area is to prevent mouse clicks to be passed onto the scene.
|
||||
MouseArea
|
||||
{
|
||||
id: viewportOverlay
|
||||
|
||||
color: UM.Theme.getColor("viewport_overlay")
|
||||
anchors.fill: parent
|
||||
|
||||
// This mouse area is to prevent mouse clicks to be passed onto the scene.
|
||||
MouseArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onWheel: wheel.accepted = true
|
||||
}
|
||||
|
||||
// Disable dropping files into Cura when the monitor page is active
|
||||
DropArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
}
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onWheel: wheel.accepted = true
|
||||
}
|
||||
|
||||
// Disable dropping files into Cura when the monitor page is active
|
||||
DropArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
}
|
||||
Loader
|
||||
{
|
||||
id: monitorViewComponent
|
||||
@ -45,4 +42,4 @@ Item
|
||||
|
||||
sourceComponent: Cura.MachineManager.printerOutputDevices.length > 0 ? Cura.MachineManager.printerOutputDevices[0].monitorItem : null
|
||||
}
|
||||
}
|
||||
}
|
@ -61,7 +61,7 @@ UM.Dialog
|
||||
anchors.leftMargin: base.textMargin
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: base.textMargin
|
||||
font: UM.Theme.getFont("large")
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
ListView
|
||||
@ -289,7 +289,7 @@ UM.Dialog
|
||||
|
||||
elide: Text.ElideRight
|
||||
height: 20 * screenScaleFactor
|
||||
font: UM.Theme.getFont("large")
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
@ -484,49 +484,15 @@ UM.Dialog
|
||||
onClicked: dialog.accept()
|
||||
}
|
||||
|
||||
Button
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
objectName: "postProcessingSaveAreaButton"
|
||||
visible: activeScriptsList.count > 0
|
||||
height: UM.Theme.getSize("save_button_save_to_button").height
|
||||
height: UM.Theme.getSize("action_button").height
|
||||
width: height
|
||||
tooltip: catalog.i18nc("@info:tooltip", "Change active post-processing scripts")
|
||||
onClicked: dialog.show()
|
||||
|
||||
style: ButtonStyle
|
||||
{
|
||||
background: Rectangle
|
||||
{
|
||||
id: deviceSelectionIcon
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
border.color: !control.enabled ? UM.Theme.getColor("action_button_disabled_border") :
|
||||
control.pressed ? UM.Theme.getColor("action_button_active_border") :
|
||||
control.hovered ? UM.Theme.getColor("action_button_hovered_border") : UM.Theme.getColor("action_button_border")
|
||||
color: !control.enabled ? UM.Theme.getColor("action_button_disabled") :
|
||||
control.pressed ? UM.Theme.getColor("action_button_active") :
|
||||
control.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button")
|
||||
Behavior on color { ColorAnimation { duration: 50; } }
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Math.round(UM.Theme.getSize("save_button_text_margin").width / 2)
|
||||
|
||||
width: parent.height
|
||||
height: parent.height
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: Math.round(parent.width / 2)
|
||||
height: Math.round(parent.height / 2)
|
||||
sourceSize.height: height
|
||||
color: !control.enabled ? UM.Theme.getColor("action_button_disabled_text") :
|
||||
control.pressed ? UM.Theme.getColor("action_button_active_text") :
|
||||
control.hovered ? UM.Theme.getColor("action_button_hovered_text") : UM.Theme.getColor("action_button_text")
|
||||
source: "postprocessing.svg"
|
||||
}
|
||||
}
|
||||
label: Label { }
|
||||
}
|
||||
iconSource: "postprocessing.svg"
|
||||
fixedWidthMode: true
|
||||
}
|
||||
}
|
@ -112,7 +112,7 @@ class ChangeAtZ(Script):
|
||||
"e1_Change_speed":
|
||||
{
|
||||
"label": "Change Speed",
|
||||
"description": "Select if total speed (print and travel) has to be cahnged",
|
||||
"description": "Select if total speed (print and travel) has to be changed",
|
||||
"type": "bool",
|
||||
"default_value": false
|
||||
},
|
||||
|
@ -163,9 +163,9 @@ Item
|
||||
id: rangleHandleLabel
|
||||
|
||||
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
|
||||
x: parent.x + parent.width + UM.Theme.getSize("default_margin").width
|
||||
x: parent.x - width - UM.Theme.getSize("default_margin").width
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
target: Qt.point(sliderRoot.width + width, y + height / 2)
|
||||
target: Qt.point(sliderRoot.width, y + height / 2)
|
||||
visible: sliderRoot.activeHandle == parent
|
||||
|
||||
// custom properties
|
||||
|
@ -50,7 +50,7 @@ catalog = i18nCatalog("cura")
|
||||
|
||||
## View used to display g-code paths.
|
||||
class SimulationView(CuraView):
|
||||
# Must match SimulationView.qml
|
||||
# Must match SimulationViewMenuComponent.qml
|
||||
LAYER_VIEW_TYPE_MATERIAL_TYPE = 0
|
||||
LAYER_VIEW_TYPE_LINE_TYPE = 1
|
||||
LAYER_VIEW_TYPE_FEEDRATE = 2
|
||||
|
@ -71,6 +71,11 @@ Item
|
||||
target: UM.Preferences
|
||||
onPreferenceChanged:
|
||||
{
|
||||
if (preference !== "view/only_show_top_layers" && preference !== "view/top_layer_count" && ! preference.match("layerview/"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
playButton.pauseSimulation()
|
||||
}
|
||||
}
|
||||
|
@ -22,6 +22,11 @@ Cura.ExpandableComponent
|
||||
target: UM.Preferences
|
||||
onPreferenceChanged:
|
||||
{
|
||||
if (preference !== "view/only_show_top_layers" && preference !== "view/top_layer_count" && ! preference.match("layerview/"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
layerTypeCombobox.currentIndex = UM.SimulationView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type")
|
||||
layerTypeCombobox.updateLegends(layerTypeCombobox.currentIndex)
|
||||
viewSettings.extruder_opacities = UM.Preferences.getValue("layerview/extruder_opacities").split("|")
|
||||
@ -43,7 +48,7 @@ Cura.ExpandableComponent
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
height: parent.height
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("default")
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
@ -60,7 +65,7 @@ Cura.ExpandableComponent
|
||||
}
|
||||
height: parent.height
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("default")
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
@ -49,12 +49,13 @@ fragment =
|
||||
// discard movements
|
||||
discard;
|
||||
}
|
||||
// support: 4, 5, 7, 10
|
||||
// support: 4, 5, 7, 10, 11 (prime tower)
|
||||
if ((u_show_helpers == 0) && (
|
||||
((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
|
||||
((v_line_type >= 4.5) && (v_line_type <= 5.5)) ||
|
||||
((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
|
||||
((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
|
||||
((v_line_type >= 4.5) && (v_line_type <= 5.5))
|
||||
((v_line_type >= 10.5) && (v_line_type <= 11.5))
|
||||
)) {
|
||||
discard;
|
||||
}
|
||||
|
@ -154,7 +154,7 @@ geometry41core =
|
||||
if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9))) {
|
||||
return;
|
||||
}
|
||||
if ((u_show_helpers == 0) && ((v_line_type[0] == 4) || (v_line_type[0] == 5) || (v_line_type[0] == 7) || (v_line_type[0] == 10))) {
|
||||
if ((u_show_helpers == 0) && ((v_line_type[0] == 4) || (v_line_type[0] == 5) || (v_line_type[0] == 7) || (v_line_type[0] == 10) || v_line_type[0] == 11)) {
|
||||
return;
|
||||
}
|
||||
if ((u_show_skin == 0) && ((v_line_type[0] == 1) || (v_line_type[0] == 2) || (v_line_type[0] == 3))) {
|
||||
|
@ -45,19 +45,23 @@ fragment =
|
||||
|
||||
void main()
|
||||
{
|
||||
if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9
|
||||
if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5))
|
||||
{ // actually, 8 and 9
|
||||
// discard movements
|
||||
discard;
|
||||
}
|
||||
// support: 4, 5, 7, 10
|
||||
// support: 4, 5, 7, 10, 11
|
||||
if ((u_show_helpers == 0) && (
|
||||
((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
|
||||
((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
|
||||
((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
|
||||
((v_line_type >= 4.5) && (v_line_type <= 5.5))
|
||||
)) {
|
||||
((v_line_type >= 4.5) && (v_line_type <= 5.5)) ||
|
||||
((v_line_type >= 10.5) && (v_line_type <= 11.5))
|
||||
))
|
||||
{
|
||||
discard;
|
||||
}
|
||||
|
||||
// skin: 1, 2, 3
|
||||
if ((u_show_skin == 0) && (
|
||||
(v_line_type >= 0.5) && (v_line_type <= 3.5)
|
||||
@ -65,7 +69,8 @@ fragment =
|
||||
discard;
|
||||
}
|
||||
// infill:
|
||||
if ((u_show_infill == 0) && (v_line_type >= 5.5) && (v_line_type <= 6.5)) {
|
||||
if ((u_show_infill == 0) && (v_line_type >= 5.5) && (v_line_type <= 6.5))
|
||||
{
|
||||
// discard movements
|
||||
discard;
|
||||
}
|
||||
@ -117,12 +122,13 @@ fragment41core =
|
||||
// discard movements
|
||||
discard;
|
||||
}
|
||||
// helpers: 4, 5, 7, 10
|
||||
// helpers: 4, 5, 7, 10, 11
|
||||
if ((u_show_helpers == 0) && (
|
||||
((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
|
||||
((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
|
||||
((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
|
||||
((v_line_type >= 4.5) && (v_line_type <= 5.5))
|
||||
((v_line_type >= 4.5) && (v_line_type <= 5.5)) ||
|
||||
((v_line_type >= 10.5) && (v_line_type <= 11.5))
|
||||
)) {
|
||||
discard;
|
||||
}
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 91 KiB |
@ -1 +0,0 @@
|
||||
<svg width="200px" height="200px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" preserveAspectRatio="xMidYMid" class="lds-rolling" style="animation-play-state: running; animation-delay: 0s; background-image: none; background-position: initial initial; background-repeat: initial initial;"><circle cx="50" cy="50" fill="none" ng-attr-stroke="{{config.color}}" ng-attr-stroke-width="{{config.width}}" ng-attr-r="{{config.radius}}" ng-attr-stroke-dasharray="{{config.dasharray}}" stroke="#0CA9E3" stroke-width="13" r="43" stroke-dasharray="202.63272615654165 69.54424205218055" style="animation-play-state: running; animation-delay: 0s;"><animateTransform attributeName="transform" type="rotate" calcMode="linear" values="0 50 50;360 50 50" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite" style="animation-play-state: running; animation-delay: 0s;"></animateTransform></circle></svg>
|
Before Width: | Height: | Size: 903 B |
@ -30,7 +30,7 @@ Row
|
||||
width: contentWidth
|
||||
anchors.verticalCenter: starIcon.verticalCenter
|
||||
color: starIcon.color
|
||||
font: UM.Theme.getFont("small")
|
||||
font: UM.Theme.getFont("default")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
@ -38,7 +38,7 @@ Window
|
||||
{
|
||||
id: mainView
|
||||
width: parent.width
|
||||
z: -1
|
||||
z: parent.z - 1
|
||||
anchors
|
||||
{
|
||||
top: header.bottom
|
||||
|
@ -55,7 +55,7 @@ Item
|
||||
bottomMargin: UM.Theme.getSize("default_margin").height
|
||||
}
|
||||
text: details.name || ""
|
||||
font: UM.Theme.getFont("large")
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("toolbox_property_label").height
|
||||
|
@ -61,8 +61,13 @@ Item
|
||||
id: labelStyle
|
||||
text: control.text
|
||||
color: control.enabled ? (control.hovered ? UM.Theme.getColor("primary") : UM.Theme.getColor("text")) : UM.Theme.getColor("text_inactive")
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
horizontalAlignment: Text.AlignRight
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
anchors
|
||||
{
|
||||
left: parent.left
|
||||
leftMargin: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
width: control.width
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ Item
|
||||
leftMargin: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
text: details === null ? "" : (details.name || "")
|
||||
font: UM.Theme.getFont("large")
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
width: contentWidth
|
||||
height: contentHeight
|
||||
|
@ -91,5 +91,10 @@ Column
|
||||
target: toolbox
|
||||
onInstallChanged: installed = toolbox.isInstalled(model.id)
|
||||
onMetadataChanged: canUpdate = toolbox.canUpdate(model.id)
|
||||
onFilterChanged:
|
||||
{
|
||||
installed = toolbox.isInstalled(model.id)
|
||||
canUpdate = toolbox.canUpdate(model.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ Column
|
||||
text: gridArea.heading
|
||||
width: parent.width
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
font: UM.Theme.getFont("medium")
|
||||
font: UM.Theme.getFont("large")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
Grid
|
||||
|
@ -112,7 +112,7 @@ Item
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("default")
|
||||
anchors.top: name.bottom
|
||||
anchors.bottom: rating.top
|
||||
|
@ -23,7 +23,7 @@ Rectangle
|
||||
text: catalog.i18nc("@label", "Featured")
|
||||
width: parent.width
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
font: UM.Theme.getFont("medium")
|
||||
font: UM.Theme.getFont("large")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
Grid
|
||||
|
@ -37,7 +37,7 @@ ScrollView
|
||||
width: page.width
|
||||
text: catalog.i18nc("@title:tab", "Plugins")
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
font: UM.Theme.getFont("medium")
|
||||
font: UM.Theme.getFont("large")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
Rectangle
|
||||
|
@ -30,6 +30,7 @@ Item
|
||||
CheckBox
|
||||
{
|
||||
id: disableButton
|
||||
anchors.verticalCenter: pluginInfo.verticalCenter
|
||||
checked: isEnabled
|
||||
visible: model.type == "plugin"
|
||||
width: visible ? UM.Theme.getSize("checkbox").width : 0
|
||||
@ -49,13 +50,14 @@ Item
|
||||
width: parent.width
|
||||
height: Math.floor(UM.Theme.getSize("toolbox_property_label").height)
|
||||
wrapMode: Text.WordWrap
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
color: pluginInfo.color
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: model.description
|
||||
font: UM.Theme.getFont("default")
|
||||
maximumLineCount: 3
|
||||
elide: Text.ElideRight
|
||||
width: parent.width
|
||||
@ -82,6 +84,7 @@ Item
|
||||
return model.author_name
|
||||
}
|
||||
}
|
||||
font: UM.Theme.getFont("medium")
|
||||
width: parent.width
|
||||
height: Math.floor(UM.Theme.getSize("toolbox_property_label").height)
|
||||
wrapMode: Text.WordWrap
|
||||
@ -96,6 +99,7 @@ Item
|
||||
Label
|
||||
{
|
||||
text: model.version
|
||||
font: UM.Theme.getFont("default")
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("toolbox_property_label").height
|
||||
color: UM.Theme.getColor("text")
|
||||
|
@ -62,17 +62,6 @@ Item
|
||||
readyAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
AnimatedImage
|
||||
{
|
||||
id: loader
|
||||
visible: active
|
||||
source: visible ? "../images/loading.gif" : ""
|
||||
width: UM.Theme.getSize("toolbox_loader").width
|
||||
height: UM.Theme.getSize("toolbox_loader").height
|
||||
anchors.right: button.left
|
||||
anchors.rightMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.verticalCenter: button.verticalCenter
|
||||
busy: active
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,8 @@ from UM.Extension import Extension
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Version import Version
|
||||
|
||||
import cura
|
||||
from cura import ApplicationMetadata
|
||||
from cura import UltimakerCloudAuthentication
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from .AuthorsModel import AuthorsModel
|
||||
@ -30,17 +31,14 @@ i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
## The Toolbox class is responsible of communicating with the server through the API
|
||||
class Toolbox(QObject, Extension):
|
||||
DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str
|
||||
DEFAULT_CLOUD_API_VERSION = 1 # type: int
|
||||
|
||||
def __init__(self, application: CuraApplication) -> None:
|
||||
super().__init__()
|
||||
|
||||
self._application = application # type: CuraApplication
|
||||
|
||||
self._sdk_version = None # type: Optional[Union[str, int]]
|
||||
self._cloud_api_version = None # type: Optional[int]
|
||||
self._cloud_api_root = None # type: Optional[str]
|
||||
self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
|
||||
self._cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion # type: int
|
||||
self._cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot # type: str
|
||||
self._api_url = None # type: Optional[str]
|
||||
|
||||
# Network:
|
||||
@ -182,9 +180,6 @@ class Toolbox(QObject, Extension):
|
||||
def _onAppInitialized(self) -> None:
|
||||
self._plugin_registry = self._application.getPluginRegistry()
|
||||
self._package_manager = self._application.getPackageManager()
|
||||
self._sdk_version = self._getSDKVersion()
|
||||
self._cloud_api_version = self._getCloudAPIVersion()
|
||||
self._cloud_api_root = self._getCloudAPIRoot()
|
||||
self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
|
||||
cloud_api_root = self._cloud_api_root,
|
||||
cloud_api_version = self._cloud_api_version,
|
||||
@ -195,36 +190,6 @@ class Toolbox(QObject, Extension):
|
||||
"packages": QUrl("{base_url}/packages".format(base_url = self._api_url))
|
||||
}
|
||||
|
||||
# Get the API root for the packages API depending on Cura version settings.
|
||||
def _getCloudAPIRoot(self) -> str:
|
||||
if not hasattr(cura, "CuraVersion"):
|
||||
return self.DEFAULT_CLOUD_API_ROOT
|
||||
if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): # type: ignore
|
||||
return self.DEFAULT_CLOUD_API_ROOT
|
||||
if not cura.CuraVersion.CuraCloudAPIRoot: # type: ignore
|
||||
return self.DEFAULT_CLOUD_API_ROOT
|
||||
return cura.CuraVersion.CuraCloudAPIRoot # type: ignore
|
||||
|
||||
# Get the cloud API version from CuraVersion
|
||||
def _getCloudAPIVersion(self) -> int:
|
||||
if not hasattr(cura, "CuraVersion"):
|
||||
return self.DEFAULT_CLOUD_API_VERSION
|
||||
if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): # type: ignore
|
||||
return self.DEFAULT_CLOUD_API_VERSION
|
||||
if not cura.CuraVersion.CuraCloudAPIVersion: # type: ignore
|
||||
return self.DEFAULT_CLOUD_API_VERSION
|
||||
return cura.CuraVersion.CuraCloudAPIVersion # type: ignore
|
||||
|
||||
# Get the packages version depending on Cura version settings.
|
||||
def _getSDKVersion(self) -> Union[int, str]:
|
||||
if not hasattr(cura, "CuraVersion"):
|
||||
return self._application.getAPIVersion().getMajor()
|
||||
if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
|
||||
return self._application.getAPIVersion().getMajor()
|
||||
if not cura.CuraVersion.CuraSDKVersion: # type: ignore
|
||||
return self._application.getAPIVersion().getMajor()
|
||||
return cura.CuraVersion.CuraSDKVersion # type: ignore
|
||||
|
||||
@pyqtSlot()
|
||||
def browsePackages(self) -> None:
|
||||
# Create the network manager:
|
||||
@ -270,12 +235,17 @@ class Toolbox(QObject, Extension):
|
||||
|
||||
def _convertPluginMetadata(self, plugin_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
try:
|
||||
highest_sdk_version_supported = Version(0)
|
||||
for supported_version in plugin_data["plugin"]["supported_sdk_versions"]:
|
||||
if supported_version > highest_sdk_version_supported:
|
||||
highest_sdk_version_supported = supported_version
|
||||
|
||||
formatted = {
|
||||
"package_id": plugin_data["id"],
|
||||
"package_type": "plugin",
|
||||
"display_name": plugin_data["plugin"]["name"],
|
||||
"package_version": plugin_data["plugin"]["version"],
|
||||
"sdk_version": plugin_data["plugin"]["api"],
|
||||
"sdk_version": highest_sdk_version_supported,
|
||||
"author": {
|
||||
"author_id": plugin_data["plugin"]["author"],
|
||||
"display_name": plugin_data["plugin"]["author"]
|
||||
@ -679,6 +649,7 @@ class Toolbox(QObject, Extension):
|
||||
Logger.log("w", "Received invalid JSON for %s.", response_type)
|
||||
break
|
||||
else:
|
||||
Logger.log("w", "Unable to connect with the server, we got a response code %s while trying to connect to %s", reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
|
||||
self.setViewPage("errored")
|
||||
self.resetDownload()
|
||||
elif reply.operation() == QNetworkAccessManager.PutOperation:
|
||||
|
@ -1,11 +1,15 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from .src import DiscoverUM3Action
|
||||
from .src import UM3OutputDevicePlugin
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
||||
|
||||
def register(app):
|
||||
return { "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}
|
||||
return {
|
||||
"output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(),
|
||||
"machine_action": DiscoverUM3Action.DiscoverUM3Action()
|
||||
}
|
||||
|
@ -223,7 +223,7 @@ Cura.MachineAction
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
text: base.selectedDevice ? base.selectedDevice.name : ""
|
||||
font: UM.Theme.getFont("large")
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
elide: Text.ElideRight
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
@ -15,6 +15,7 @@ Item
|
||||
id: base
|
||||
|
||||
property bool expanded: false
|
||||
property bool enabled: true
|
||||
property var borderWidth: 1
|
||||
property color borderColor: "#CCCCCC"
|
||||
property color headerBackgroundColor: "white"
|
||||
@ -34,7 +35,7 @@ Item
|
||||
color: borderColor
|
||||
width: borderWidth
|
||||
}
|
||||
color: headerMouseArea.containsMouse ? headerHoverColor : headerBackgroundColor
|
||||
color: base.enabled && headerMouseArea.containsMouse ? headerHoverColor : headerBackgroundColor
|
||||
height: childrenRect.height
|
||||
width: parent.width
|
||||
Behavior on color
|
||||
@ -50,8 +51,12 @@ Item
|
||||
{
|
||||
id: headerMouseArea
|
||||
anchors.fill: header
|
||||
onClicked: base.expanded = !base.expanded
|
||||
hoverEnabled: true
|
||||
onClicked:
|
||||
{
|
||||
if (!base.enabled) return
|
||||
base.expanded = !base.expanded
|
||||
}
|
||||
hoverEnabled: base.enabled
|
||||
}
|
||||
|
||||
Rectangle
|
||||
|
@ -18,7 +18,7 @@ import UM 1.3 as UM
|
||||
Item
|
||||
{
|
||||
// The buildplate name
|
||||
property alias buildplate: buildplateLabel.text
|
||||
property var buildplate: null
|
||||
|
||||
// Height is one 18px label/icon
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
@ -34,7 +34,16 @@ Item
|
||||
Item
|
||||
{
|
||||
height: parent.height
|
||||
width: 32 * screenScaleFactor // TODO: Theme! (Should be same as extruder icon width)
|
||||
width: 32 * screenScaleFactor // Ensure the icon is centered under the extruder icon (same width)
|
||||
|
||||
Rectangle
|
||||
{
|
||||
anchors.centerIn: parent
|
||||
height: parent.height
|
||||
width: height
|
||||
color: buildplateIcon.visible > 0 ? "transparent" : "#eeeeee" // TODO: Theme!
|
||||
radius: Math.floor(height / 2)
|
||||
}
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
@ -44,6 +53,7 @@ Item
|
||||
height: parent.height
|
||||
source: "../svg/icons/buildplate.svg"
|
||||
width: height
|
||||
visible: buildplate
|
||||
}
|
||||
}
|
||||
|
||||
@ -53,7 +63,8 @@ Item
|
||||
color: "#191919" // TODO: Theme!
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("default") // 12pt, regular
|
||||
text: ""
|
||||
text: buildplate ? buildplate : ""
|
||||
visible: text !== ""
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
|
@ -14,7 +14,12 @@ Item
|
||||
property var tileWidth: 834 * screenScaleFactor // TODO: Theme!
|
||||
property var tileHeight: 216 * screenScaleFactor // TODO: Theme!
|
||||
property var tileSpacing: 60 * screenScaleFactor // TODO: Theme!
|
||||
property var maxOffset: (OutputDevice.printers.length - 1) * (tileWidth + tileSpacing)
|
||||
|
||||
// Array/model of printers to populate the carousel with
|
||||
property var printers: []
|
||||
|
||||
// Maximum distance the carousel can be shifted
|
||||
property var maxOffset: (printers.length - 1) * (tileWidth + tileSpacing)
|
||||
|
||||
height: centerSection.height
|
||||
width: maximumWidth
|
||||
@ -129,7 +134,7 @@ Item
|
||||
|
||||
Repeater
|
||||
{
|
||||
model: OutputDevice.printers
|
||||
model: printers
|
||||
MonitorPrinterCard
|
||||
{
|
||||
printer: modelData
|
||||
@ -151,7 +156,7 @@ Item
|
||||
width: 36 * screenScaleFactor // TODO: Theme!
|
||||
height: 72 * screenScaleFactor // TODO: Theme!
|
||||
z: 10
|
||||
visible: currentIndex < OutputDevice.printers.length - 1
|
||||
visible: currentIndex < printers.length - 1
|
||||
onClicked: navigateTo(currentIndex + 1)
|
||||
hoverEnabled: true
|
||||
background: Rectangle
|
||||
@ -225,9 +230,10 @@ Item
|
||||
topMargin: 36 * screenScaleFactor // TODO: Theme!
|
||||
}
|
||||
spacing: 8 * screenScaleFactor // TODO: Theme!
|
||||
visible: printers.length > 1
|
||||
Repeater
|
||||
{
|
||||
model: OutputDevice.printers
|
||||
model: printers
|
||||
Button
|
||||
{
|
||||
background: Rectangle
|
||||
@ -243,7 +249,7 @@ Item
|
||||
}
|
||||
|
||||
function navigateTo( i ) {
|
||||
if (i >= 0 && i < OutputDevice.printers.length)
|
||||
if (i >= 0 && i < printers.length)
|
||||
{
|
||||
tiles.x = -1 * i * (tileWidth + tileSpacing)
|
||||
currentIndex = i
|
||||
|
@ -54,7 +54,7 @@ UM.Dialog
|
||||
wrapMode: Text.WordWrap
|
||||
text:
|
||||
{
|
||||
if (!printer.activePrintJob)
|
||||
if (!printer || !printer.activePrintJob)
|
||||
{
|
||||
return ""
|
||||
}
|
||||
|
@ -39,38 +39,62 @@ Item
|
||||
color: "#eeeeee" // TODO: Theme!
|
||||
position: 0
|
||||
}
|
||||
Label
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: materialLabel
|
||||
id: materialLabelWrapper
|
||||
anchors
|
||||
{
|
||||
left: extruderIcon.right
|
||||
leftMargin: 12 * screenScaleFactor // TODO: Theme!
|
||||
}
|
||||
color: "#191919" // TODO: Theme!
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("default") // 12pt, regular
|
||||
text: ""
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
color: materialLabel.visible > 0 ? "transparent" : "#eeeeee" // TODO: Theme!
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: Math.max(materialLabel.contentWidth, 60 * screenScaleFactor) // TODO: Theme!
|
||||
radius: 2 * screenScaleFactor // TODO: Theme!
|
||||
|
||||
Label
|
||||
{
|
||||
id: materialLabel
|
||||
|
||||
color: "#191919" // TODO: Theme!
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("default") // 12pt, regular
|
||||
text: ""
|
||||
visible: text !== ""
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
Label
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: printCoreLabel
|
||||
id: printCoreLabelWrapper
|
||||
anchors
|
||||
{
|
||||
left: materialLabel.left
|
||||
left: materialLabelWrapper.left
|
||||
bottom: parent.bottom
|
||||
}
|
||||
color: "#191919" // TODO: Theme!
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("default_bold") // 12pt, bold
|
||||
text: ""
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
color: printCoreLabel.visible > 0 ? "transparent" : "#eeeeee" // TODO: Theme!
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: Math.max(printCoreLabel.contentWidth, 36 * screenScaleFactor) // TODO: Theme!
|
||||
radius: 2 * screenScaleFactor // TODO: Theme!
|
||||
|
||||
Label
|
||||
{
|
||||
id: printCoreLabel
|
||||
|
||||
color: "#191919" // TODO: Theme!
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("default_bold") // 12pt, bold
|
||||
text: ""
|
||||
visible: text !== ""
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
@ -19,7 +19,7 @@ Item
|
||||
property int position: 0
|
||||
|
||||
// The extruder icon size; NOTE: This shouldn't need to be changed
|
||||
property int size: 32 // TODO: Theme!
|
||||
property int size: 32 * screenScaleFactor // TODO: Theme!
|
||||
|
||||
// THe extruder icon source; NOTE: This shouldn't need to be changed
|
||||
property string iconSource: "../svg/icons/extruder.svg"
|
||||
@ -35,26 +35,17 @@ Item
|
||||
width: size
|
||||
}
|
||||
|
||||
/*
|
||||
* The label uses some "fancy" math to ensure that if you change the overall
|
||||
* icon size, the number scales with it. That is to say, the font properties
|
||||
* are linked to the icon size, NOT the theme. And that's intentional.
|
||||
*/
|
||||
Label
|
||||
{
|
||||
id: positionLabel
|
||||
font
|
||||
{
|
||||
pointSize: Math.round(size * 0.3125)
|
||||
weight: Font.Bold
|
||||
}
|
||||
height: Math.round(size / 2) * screenScaleFactor
|
||||
font: UM.Theme.getFont("small")
|
||||
height: Math.round(size / 2)
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: position + 1
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: Math.round(size / 2) * screenScaleFactor
|
||||
x: Math.round(size * 0.25) * screenScaleFactor
|
||||
y: Math.round(size * 0.15625) * screenScaleFactor
|
||||
// TODO: Once 'size' is themed, screenScaleFactor won't be needed
|
||||
width: Math.round(size / 2)
|
||||
x: Math.round(size * 0.25)
|
||||
y: Math.round(size * 0.15625)
|
||||
visible: position >= 0
|
||||
}
|
||||
}
|
@ -26,6 +26,7 @@ Item
|
||||
|
||||
ExpandableCard
|
||||
{
|
||||
enabled: printJob != null
|
||||
borderColor: printJob.configurationChanges.length !== 0 ? "#f5a623" : "#CCCCCC" // TODO: Theme!
|
||||
headerItem: Row
|
||||
{
|
||||
@ -41,32 +42,56 @@ Item
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Label
|
||||
Item
|
||||
{
|
||||
text: printJob && printJob.name ? printJob.name : ""
|
||||
color: "#374355"
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("medium") // 14pt, regular
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 216 * screenScaleFactor // TODO: Theme! (Should match column size)
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: 216 * screenScaleFactor // TODO: Theme! (Should match column size)
|
||||
Rectangle
|
||||
{
|
||||
color: "#eeeeee"
|
||||
width: Math.round(parent.width / 2)
|
||||
height: parent.height
|
||||
visible: !printJob
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: printJob && printJob.name ? printJob.name : ""
|
||||
color: "#374355"
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("medium") // 14pt, regular
|
||||
visible: printJob
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
text: printJob ? OutputDevice.formatDuration(printJob.timeTotal) : ""
|
||||
color: "#374355"
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("medium") // 14pt, regular
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 216 * screenScaleFactor // TODO: Theme! (Should match column size)
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
Item
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: 216 * screenScaleFactor // TODO: Theme! (Should match column size)
|
||||
Rectangle
|
||||
{
|
||||
color: "#eeeeee"
|
||||
width: Math.round(parent.width / 3)
|
||||
height: parent.height
|
||||
visible: !printJob
|
||||
}
|
||||
Label
|
||||
{
|
||||
text: printJob ? OutputDevice.formatDuration(printJob.timeTotal) : ""
|
||||
color: "#374355"
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("medium") // 14pt, regular
|
||||
visible: printJob
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Item
|
||||
@ -75,6 +100,14 @@ Item
|
||||
height: 18 * screenScaleFactor // TODO: This should be childrenRect.height but QML throws warnings
|
||||
width: childrenRect.width
|
||||
|
||||
Rectangle
|
||||
{
|
||||
color: "#eeeeee"
|
||||
width: 72 * screenScaleFactor // TODO: Theme!
|
||||
height: parent.height
|
||||
visible: !printJob
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: printerAssignmentLabel
|
||||
@ -100,7 +133,7 @@ Item
|
||||
width: 120 * screenScaleFactor // TODO: Theme!
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
@ -115,6 +148,7 @@ Item
|
||||
}
|
||||
height: childrenRect.height
|
||||
spacing: 6 // TODO: Theme!
|
||||
visible: printJob
|
||||
|
||||
Repeater
|
||||
{
|
||||
|
@ -16,23 +16,28 @@ Item
|
||||
width: size
|
||||
height: size
|
||||
|
||||
// Actual content
|
||||
Image
|
||||
Rectangle
|
||||
{
|
||||
id: previewImage
|
||||
anchors.fill: parent
|
||||
opacity:
|
||||
color: printJob ? "transparent" : "#eeeeee" // TODO: Theme!
|
||||
radius: 8 // TODO: Theme!
|
||||
Image
|
||||
{
|
||||
if (printJob && (printJob.state == "error" || printJob.configurationChanges.length > 0 || !printJob.isActive))
|
||||
id: previewImage
|
||||
anchors.fill: parent
|
||||
opacity:
|
||||
{
|
||||
return 0.5
|
||||
if (printJob && (printJob.state == "error" || printJob.configurationChanges.length > 0 || !printJob.isActive))
|
||||
{
|
||||
return 0.5
|
||||
}
|
||||
return 1.0
|
||||
}
|
||||
return 1.0
|
||||
source: printJob ? printJob.previewImageUrl : ""
|
||||
}
|
||||
source: printJob ? printJob.previewImageUrl : ""
|
||||
visible: printJob
|
||||
}
|
||||
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: ultiBotImage
|
||||
|
@ -34,16 +34,16 @@ Item
|
||||
{
|
||||
background: Rectangle
|
||||
{
|
||||
color: printJob && printJob.isActive ? "#e4e4f2" : "#f3f3f9" // TODO: Theme!
|
||||
color: "#f5f5f5" // TODO: Theme!
|
||||
implicitHeight: visible ? 8 * screenScaleFactor : 0 // TODO: Theme!
|
||||
implicitWidth: 180 * screenScaleFactor // TODO: Theme!
|
||||
radius: 4 * screenScaleFactor // TODO: Theme!
|
||||
radius: 2 * screenScaleFactor // TODO: Theme!
|
||||
}
|
||||
progress: Rectangle
|
||||
{
|
||||
id: progressItem;
|
||||
color: printJob && printJob.isActive ? "#0a0850" : "#9392b2" // TODO: Theme!
|
||||
radius: 4 * screenScaleFactor // TODO: Theme!
|
||||
color: printJob && printJob.isActive ? "#3282ff" : "#CCCCCC" // TODO: Theme!
|
||||
radius: 2 * screenScaleFactor // TODO: Theme!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,16 +33,24 @@ Item
|
||||
width: 834 * screenScaleFactor // TODO: Theme!
|
||||
height: childrenRect.height
|
||||
|
||||
// Printer portion
|
||||
Rectangle
|
||||
{
|
||||
id: printerInfo
|
||||
id: background
|
||||
anchors.fill: parent
|
||||
color: "#FFFFFF" // TODO: Theme!
|
||||
border
|
||||
{
|
||||
color: "#CCCCCC" // TODO: Theme!
|
||||
width: borderSize // TODO: Remove once themed
|
||||
}
|
||||
color: "white" // TODO: Theme!
|
||||
radius: 2 * screenScaleFactor // TODO: Theme!
|
||||
}
|
||||
|
||||
// Printer portion
|
||||
Item
|
||||
{
|
||||
id: printerInfo
|
||||
|
||||
width: parent.width
|
||||
height: 144 * screenScaleFactor // TODO: Theme!
|
||||
|
||||
@ -56,15 +64,22 @@ Item
|
||||
}
|
||||
spacing: 18 * screenScaleFactor // TODO: Theme!
|
||||
|
||||
Image
|
||||
Rectangle
|
||||
{
|
||||
id: printerImage
|
||||
width: 108 * screenScaleFactor // TODO: Theme!
|
||||
height: 108 * screenScaleFactor // TODO: Theme!
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "../png/" + printer.type + ".png"
|
||||
mipmap: true
|
||||
color: printer ? "transparent" : "#eeeeee" // TODO: Theme!
|
||||
radius: 8 // TODO: Theme!
|
||||
Image
|
||||
{
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: printer ? "../png/" + printer.type + ".png" : ""
|
||||
mipmap: true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Item
|
||||
{
|
||||
@ -75,20 +90,38 @@ Item
|
||||
width: 180 * screenScaleFactor // TODO: Theme!
|
||||
height: printerNameLabel.height + printerFamilyPill.height + 6 * screenScaleFactor // TODO: Theme!
|
||||
|
||||
Label
|
||||
Rectangle
|
||||
{
|
||||
id: printerNameLabel
|
||||
text: printer && printer.name ? printer.name : ""
|
||||
color: "#414054" // TODO: Theme!
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("large") // 16pt, bold
|
||||
width: parent.width
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
// color: "#414054" // TODO: Theme!
|
||||
color: printer ? "transparent" : "#eeeeee" // TODO: Theme!
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
width: parent.width
|
||||
radius: 2 * screenScaleFactor // TODO: Theme!
|
||||
|
||||
Label
|
||||
{
|
||||
text: printer && printer.name ? printer.name : ""
|
||||
color: "#414054" // TODO: Theme!
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("large") // 16pt, bold
|
||||
width: parent.width
|
||||
visible: printer
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
color: "#eeeeee" // TODO: Theme!
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
radius: 2 * screenScaleFactor // TODO: Theme!
|
||||
visible: !printer
|
||||
width: 48 * screenScaleFactor // TODO: Theme!
|
||||
}
|
||||
MonitorPrinterPill
|
||||
{
|
||||
id: printerFamilyPill
|
||||
@ -98,7 +131,7 @@ Item
|
||||
topMargin: 6 * screenScaleFactor // TODO: Theme!
|
||||
left: printerNameLabel.left
|
||||
}
|
||||
text: printer.type
|
||||
text: printer ? printer.type : ""
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,16 +139,30 @@ Item
|
||||
{
|
||||
id: printerConfiguration
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
buildplate: "Glass"
|
||||
buildplate: printer ? "Glass" : null // 'Glass' as a default
|
||||
configurations:
|
||||
[
|
||||
base.printer.printerConfiguration.extruderConfigurations[0],
|
||||
base.printer.printerConfiguration.extruderConfigurations[1]
|
||||
]
|
||||
height: 72 * screenScaleFactor // TODO: Theme!
|
||||
{
|
||||
var configs = []
|
||||
if (printer)
|
||||
{
|
||||
configs.push(printer.printerConfiguration.extruderConfigurations[0])
|
||||
configs.push(printer.printerConfiguration.extruderConfigurations[1])
|
||||
}
|
||||
else
|
||||
{
|
||||
configs.push(null, null)
|
||||
}
|
||||
return configs
|
||||
}
|
||||
height: 72 * screenScaleFactor // TODO: Theme!te theRect's x property
|
||||
}
|
||||
|
||||
// TODO: Make this work.
|
||||
PropertyAnimation { target: printerConfiguration; property: "visible"; to: 0; loops: Animation.Infinite; duration: 500 }
|
||||
}
|
||||
|
||||
|
||||
|
||||
PrintJobContextMenu
|
||||
{
|
||||
id: contextButton
|
||||
@ -126,10 +173,11 @@ Item
|
||||
top: parent.top
|
||||
topMargin: 12 * screenScaleFactor // TODO: Theme!
|
||||
}
|
||||
printJob: printer.activePrintJob
|
||||
printJob: printer ? printer.activePrintJob : null
|
||||
width: 36 * screenScaleFactor // TODO: Theme!
|
||||
height: 36 * screenScaleFactor // TODO: Theme!
|
||||
enabled: base.enabled
|
||||
visible: printer
|
||||
}
|
||||
CameraButton
|
||||
{
|
||||
@ -143,10 +191,24 @@ Item
|
||||
}
|
||||
iconSource: "../svg/icons/camera.svg"
|
||||
enabled: base.enabled
|
||||
visible: printer
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Divider
|
||||
Rectangle
|
||||
{
|
||||
anchors
|
||||
{
|
||||
top: printJobInfo.top
|
||||
left: printJobInfo.left
|
||||
right: printJobInfo.right
|
||||
}
|
||||
height: borderSize // Remove once themed
|
||||
color: background.border.color
|
||||
}
|
||||
|
||||
// Print job portion
|
||||
Rectangle
|
||||
{
|
||||
@ -158,10 +220,10 @@ Item
|
||||
}
|
||||
border
|
||||
{
|
||||
color: printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 ? "#f5a623" : "#CCCCCC" // TODO: Theme!
|
||||
color: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 ? "#f5a623" : "transparent" // TODO: Theme!
|
||||
width: borderSize // TODO: Remove once themed
|
||||
}
|
||||
color: "white" // TODO: Theme!
|
||||
color: "transparent" // TODO: Theme!
|
||||
height: 84 * screenScaleFactor + borderSize // TODO: Remove once themed
|
||||
width: parent.width
|
||||
|
||||
@ -184,9 +246,12 @@ Item
|
||||
{
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
color: "#414054" // TODO: Theme!
|
||||
font: UM.Theme.getFont("large") // 16pt, bold
|
||||
color: printer ? "#414054" : "#aaaaaa" // TODO: Theme!
|
||||
font: UM.Theme.getFont("large_bold") // 16pt, bold
|
||||
text: {
|
||||
if (!printer) {
|
||||
return catalog.i18nc("@label:status", "Loading...")
|
||||
}
|
||||
if (printer && printer.state == "disabled")
|
||||
{
|
||||
return catalog.i18nc("@label:status", "Unavailable")
|
||||
@ -215,10 +280,10 @@ Item
|
||||
MonitorPrintJobPreview
|
||||
{
|
||||
anchors.centerIn: parent
|
||||
printJob: base.printer.activePrintJob
|
||||
printJob: printer ? printer.activePrintJob : null
|
||||
size: parent.height
|
||||
}
|
||||
visible: printer.activePrintJob
|
||||
visible: printer && printer.activePrintJob && !printerStatus.visible
|
||||
}
|
||||
|
||||
Item
|
||||
@ -229,15 +294,15 @@ Item
|
||||
}
|
||||
width: 180 * screenScaleFactor // TODO: Theme!
|
||||
height: printerNameLabel.height + printerFamilyPill.height + 6 * screenScaleFactor // TODO: Theme!
|
||||
visible: printer.activePrintJob
|
||||
visible: printer && printer.activePrintJob && !printerStatus.visible
|
||||
|
||||
Label
|
||||
{
|
||||
id: printerJobNameLabel
|
||||
color: printer.activePrintJob && printer.activePrintJob.isActive ? "#414054" : "#babac1" // TODO: Theme!
|
||||
color: printer && printer.activePrintJob && printer.activePrintJob.isActive ? "#414054" : "#babac1" // TODO: Theme!
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("large") // 16pt, bold
|
||||
text: base.printer.activePrintJob ? base.printer.activePrintJob.name : "Untitled" // TODO: I18N
|
||||
text: printer && printer.activePrintJob ? printer.activePrintJob.name : "Untitled" // TODO: I18N
|
||||
width: parent.width
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
@ -254,10 +319,10 @@ Item
|
||||
topMargin: 6 * screenScaleFactor // TODO: Theme!
|
||||
left: printerJobNameLabel.left
|
||||
}
|
||||
color: printer.activePrintJob && printer.activePrintJob.isActive ? "#53657d" : "#babac1" // TODO: Theme!
|
||||
color: printer && printer.activePrintJob && printer.activePrintJob.isActive ? "#53657d" : "#babac1" // TODO: Theme!
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("default") // 12pt, regular
|
||||
text: printer.activePrintJob ? printer.activePrintJob.owner : "Anonymous" // TODO: I18N
|
||||
text: printer && printer.activePrintJob ? printer.activePrintJob.owner : "Anonymous" // TODO: I18N
|
||||
width: parent.width
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
@ -272,8 +337,8 @@ Item
|
||||
{
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
printJob: printer.activePrintJob
|
||||
visible: printer.activePrintJob && printer.activePrintJob.configurationChanges.length === 0
|
||||
printJob: printer && printer.activePrintJob
|
||||
visible: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length === 0 && !printerStatus.visible
|
||||
}
|
||||
|
||||
Label
|
||||
@ -284,7 +349,7 @@ Item
|
||||
}
|
||||
font: UM.Theme.getFont("default")
|
||||
text: "Requires configuration changes"
|
||||
visible: printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0
|
||||
visible: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 && !printerStatus.visible
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
@ -326,7 +391,7 @@ Item
|
||||
}
|
||||
implicitHeight: 32 * screenScaleFactor // TODO: Theme!
|
||||
implicitWidth: 96 * screenScaleFactor // TODO: Theme!
|
||||
visible: printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0
|
||||
visible: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 && !printerStatus.visible
|
||||
onClicked: base.enabled ? overrideConfirmationDialog.open() : {}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ Item
|
||||
property alias buildplate: buildplateConfig.buildplate
|
||||
|
||||
// Array of extracted extruder configurations
|
||||
property var configurations: null
|
||||
property var configurations: [null,null]
|
||||
|
||||
// Default size, but should be stretched to fill parent
|
||||
height: 72 * parent.height
|
||||
@ -37,10 +37,10 @@ Item
|
||||
|
||||
MonitorExtruderConfiguration
|
||||
{
|
||||
color: modelData.activeMaterial ? modelData.activeMaterial.color : "#eeeeee" // TODO: Theme!
|
||||
material: modelData.activeMaterial ? modelData.activeMaterial.name : ""
|
||||
position: modelData.position
|
||||
printCore: modelData.hotendID
|
||||
color: modelData && modelData.activeMaterial ? modelData.activeMaterial.color : "#eeeeee" // TODO: Theme!
|
||||
material: modelData && modelData.activeMaterial ? modelData.activeMaterial.name : ""
|
||||
position: modelData && typeof(modelData.position) === "number" ? modelData.position : -1 // Use negative one to create empty extruder number
|
||||
printCore: modelData ? modelData.hotendID : ""
|
||||
|
||||
// Keep things responsive!
|
||||
width: Math.floor((base.width - (configurations.length - 1) * extruderConfigurationRow.spacing) / configurations.length)
|
||||
@ -53,6 +53,6 @@ Item
|
||||
{
|
||||
id: buildplateConfig
|
||||
anchors.bottom: parent.bottom
|
||||
buildplate: "Glass" // 'Glass' as a default
|
||||
buildplate: null
|
||||
}
|
||||
}
|
@ -27,12 +27,12 @@ Item
|
||||
}
|
||||
|
||||
implicitHeight: 18 * screenScaleFactor // TODO: Theme!
|
||||
implicitWidth: printerNameLabel.contentWidth + 12 // TODO: Theme!
|
||||
implicitWidth: Math.max(printerNameLabel.contentWidth + 12 * screenScaleFactor, 36 * screenScaleFactor) // TODO: Theme!
|
||||
|
||||
Rectangle {
|
||||
id: background
|
||||
anchors.fill: parent
|
||||
color: "#e4e4f2" // TODO: Theme!
|
||||
color: printerNameLabel.visible ? "#e4e4f2" : "#eeeeee"// TODO: Theme!
|
||||
radius: 2 * screenScaleFactor // TODO: Theme!
|
||||
}
|
||||
|
||||
@ -41,6 +41,7 @@ Item
|
||||
anchors.centerIn: parent
|
||||
color: "#535369" // TODO: Theme!
|
||||
text: tagText
|
||||
font.pointSize: 10
|
||||
font.pointSize: 10 // TODO: Theme!
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
@ -23,7 +23,7 @@ Item
|
||||
top: parent.top
|
||||
}
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("large_nonbold")
|
||||
font: UM.Theme.getFont("large")
|
||||
text: catalog.i18nc("@label", "Queued")
|
||||
}
|
||||
|
||||
@ -42,8 +42,8 @@ Item
|
||||
{
|
||||
id: externalLinkIcon
|
||||
anchors.verticalCenter: manageQueueLabel.verticalCenter
|
||||
color: UM.Theme.getColor("primary")
|
||||
source: "../svg/icons/external_link.svg"
|
||||
color: UM.Theme.getColor("text_link")
|
||||
source: UM.Theme.getIcon("external_link")
|
||||
width: 16 * screenScaleFactor // TODO: Theme! (Y U NO USE 18 LIKE ALL OTHER ICONS?!)
|
||||
height: 16 * screenScaleFactor // TODO: Theme! (Y U NO USE 18 LIKE ALL OTHER ICONS?!)
|
||||
}
|
||||
@ -56,10 +56,11 @@ Item
|
||||
leftMargin: 6 * screenScaleFactor // TODO: Theme!
|
||||
verticalCenter: externalLinkIcon.verticalCenter
|
||||
}
|
||||
color: UM.Theme.getColor("primary")
|
||||
color: UM.Theme.getColor("text_link")
|
||||
font: UM.Theme.getFont("default") // 12pt, regular
|
||||
linkColor: UM.Theme.getColor("primary")
|
||||
linkColor: UM.Theme.getColor("text_link")
|
||||
text: catalog.i18nc("@label link to connect manager", "Manage queue in Cura Connect")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
@ -144,7 +145,6 @@ Item
|
||||
topMargin: 12 * screenScaleFactor // TODO: Theme!
|
||||
}
|
||||
style: UM.Theme.styles.scrollview
|
||||
visible: OutputDevice.receivedPrintJobs
|
||||
width: parent.width
|
||||
|
||||
ListView
|
||||
@ -160,7 +160,7 @@ Item
|
||||
}
|
||||
printJob: modelData
|
||||
}
|
||||
model: OutputDevice.queuedPrintJobs
|
||||
model: OutputDevice.receivedPrintJobs ? OutputDevice.queuedPrintJobs : [null,null]
|
||||
spacing: 6 // TODO: Theme!
|
||||
}
|
||||
}
|
||||
|
@ -64,8 +64,10 @@ Component
|
||||
}
|
||||
width: parent.width
|
||||
height: 264 * screenScaleFactor // TODO: Theme!
|
||||
MonitorCarousel {
|
||||
MonitorCarousel
|
||||
{
|
||||
id: carousel
|
||||
printers: OutputDevice.receivedPrintJobs ? OutputDevice.printers : [null]
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,67 +90,4 @@ Item {
|
||||
source: "DiscoverUM3Action.qml";
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent;
|
||||
objectName: "networkPrinterConnectionInfo";
|
||||
spacing: UM.Theme.getSize("default_margin").width;
|
||||
visible: isUM3;
|
||||
|
||||
Button {
|
||||
onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication();
|
||||
text: catalog.i18nc("@action:button", "Request Access");
|
||||
tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer");
|
||||
visible: printerConnected && !printerAcceptsCommands && !authenticationRequested;
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors {
|
||||
left: parent.left;
|
||||
right: parent.right;
|
||||
}
|
||||
height: childrenRect.height;
|
||||
spacing: UM.Theme.getSize("default_margin").width;
|
||||
visible: printerConnected;
|
||||
|
||||
Column {
|
||||
Repeater {
|
||||
model: CuraApplication.getExtrudersModel()
|
||||
|
||||
Label {
|
||||
text: model.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Repeater {
|
||||
id: nozzleColumn;
|
||||
model: hotendIds
|
||||
|
||||
Label {
|
||||
text: nozzleColumn.model[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
Repeater {
|
||||
id: materialColumn;
|
||||
model: materialNames
|
||||
|
||||
Label {
|
||||
text: materialColumn.model[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button {
|
||||
onClicked: manager.loadConfigurationFromPrinter();
|
||||
text: catalog.i18nc("@action:button", "Activate Configuration");
|
||||
tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura");
|
||||
visible: false; // printerConnected && !isClusterPrinter()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
166
plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py
Normal file
166
plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py
Normal file
@ -0,0 +1,166 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import json
|
||||
from json import JSONDecodeError
|
||||
from time import time
|
||||
from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any, cast
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
|
||||
|
||||
from UM.Logger import Logger
|
||||
from cura import UltimakerCloudAuthentication
|
||||
from cura.API import Account
|
||||
from .ToolPathUploader import ToolPathUploader
|
||||
from ..Models import BaseModel
|
||||
from .Models.CloudClusterResponse import CloudClusterResponse
|
||||
from .Models.CloudError import CloudError
|
||||
from .Models.CloudClusterStatus import CloudClusterStatus
|
||||
from .Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest
|
||||
from .Models.CloudPrintResponse import CloudPrintResponse
|
||||
from .Models.CloudPrintJobResponse import CloudPrintJobResponse
|
||||
|
||||
|
||||
## The generic type variable used to document the methods below.
|
||||
CloudApiClientModel = TypeVar("CloudApiClientModel", bound = BaseModel)
|
||||
|
||||
|
||||
## The cloud API client is responsible for handling the requests and responses from the cloud.
|
||||
# Each method should only handle models instead of exposing Any HTTP details.
|
||||
class CloudApiClient:
|
||||
|
||||
# The cloud URL to use for this remote cluster.
|
||||
ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot
|
||||
CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH)
|
||||
CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH)
|
||||
|
||||
## Initializes a new cloud API client.
|
||||
# \param account: The user's account object
|
||||
# \param on_error: The callback to be called whenever we receive errors from the server.
|
||||
def __init__(self, account: Account, on_error: Callable[[List[CloudError]], None]) -> None:
|
||||
super().__init__()
|
||||
self._manager = QNetworkAccessManager()
|
||||
self._account = account
|
||||
self._on_error = on_error
|
||||
self._upload = None # type: Optional[ToolPathUploader]
|
||||
# In order to avoid garbage collection we keep the callbacks in this list.
|
||||
self._anti_gc_callbacks = [] # type: List[Callable[[], None]]
|
||||
|
||||
## Gets the account used for the API.
|
||||
@property
|
||||
def account(self) -> Account:
|
||||
return self._account
|
||||
|
||||
## Retrieves all the clusters for the user that is currently logged in.
|
||||
# \param on_finished: The function to be called after the result is parsed.
|
||||
def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any]) -> None:
|
||||
url = "{}/clusters".format(self.CLUSTER_API_ROOT)
|
||||
reply = self._manager.get(self._createEmptyRequest(url))
|
||||
self._addCallback(reply, on_finished, CloudClusterResponse)
|
||||
|
||||
## Retrieves the status of the given cluster.
|
||||
# \param cluster_id: The ID of the cluster.
|
||||
# \param on_finished: The function to be called after the result is parsed.
|
||||
def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None:
|
||||
url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id)
|
||||
reply = self._manager.get(self._createEmptyRequest(url))
|
||||
self._addCallback(reply, on_finished, CloudClusterStatus)
|
||||
|
||||
## Requests the cloud to register the upload of a print job mesh.
|
||||
# \param request: The request object.
|
||||
# \param on_finished: The function to be called after the result is parsed.
|
||||
def requestUpload(self, request: CloudPrintJobUploadRequest, on_finished: Callable[[CloudPrintJobResponse], Any]
|
||||
) -> None:
|
||||
url = "{}/jobs/upload".format(self.CURA_API_ROOT)
|
||||
body = json.dumps({"data": request.toDict()})
|
||||
reply = self._manager.put(self._createEmptyRequest(url), body.encode())
|
||||
self._addCallback(reply, on_finished, CloudPrintJobResponse)
|
||||
|
||||
## Uploads a print job tool path to the cloud.
|
||||
# \param print_job: The object received after requesting an upload with `self.requestUpload`.
|
||||
# \param mesh: The tool path data to be uploaded.
|
||||
# \param on_finished: The function to be called after the upload is successful.
|
||||
# \param on_progress: A function to be called during upload progress. It receives a percentage (0-100).
|
||||
# \param on_error: A function to be called if the upload fails.
|
||||
def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any],
|
||||
on_progress: Callable[[int], Any], on_error: Callable[[], Any]):
|
||||
self._upload = ToolPathUploader(self._manager, print_job, mesh, on_finished, on_progress, on_error)
|
||||
self._upload.start()
|
||||
|
||||
# Requests a cluster to print the given print job.
|
||||
# \param cluster_id: The ID of the cluster.
|
||||
# \param job_id: The ID of the print job.
|
||||
# \param on_finished: The function to be called after the result is parsed.
|
||||
def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any]) -> None:
|
||||
url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id)
|
||||
reply = self._manager.post(self._createEmptyRequest(url), b"")
|
||||
self._addCallback(reply, on_finished, CloudPrintResponse)
|
||||
|
||||
## We override _createEmptyRequest in order to add the user credentials.
|
||||
# \param url: The URL to request
|
||||
# \param content_type: The type of the body contents.
|
||||
def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest:
|
||||
request = QNetworkRequest(QUrl(path))
|
||||
if content_type:
|
||||
request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
|
||||
if self._account.isLoggedIn:
|
||||
request.setRawHeader(b"Authorization", "Bearer {}".format(self._account.accessToken).encode())
|
||||
return request
|
||||
|
||||
## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well.
|
||||
# \param reply: The reply from the server.
|
||||
# \return A tuple with a status code and a dictionary.
|
||||
@staticmethod
|
||||
def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]:
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
try:
|
||||
response = bytes(reply.readAll()).decode()
|
||||
return status_code, json.loads(response)
|
||||
except (UnicodeDecodeError, JSONDecodeError, ValueError) as err:
|
||||
error = CloudError(code=type(err).__name__, title=str(err), http_code=str(status_code),
|
||||
id=str(time()), http_status="500")
|
||||
Logger.logException("e", "Could not parse the stardust response: %s", error.toDict())
|
||||
return status_code, {"errors": [error.toDict()]}
|
||||
|
||||
## Parses the given models and calls the correct callback depending on the result.
|
||||
# \param response: The response from the server, after being converted to a dict.
|
||||
# \param on_finished: The callback in case the response is successful.
|
||||
# \param model_class: The type of the model to convert the response to. It may either be a single record or a list.
|
||||
def _parseModels(self, response: Dict[str, Any],
|
||||
on_finished: Union[Callable[[CloudApiClientModel], Any],
|
||||
Callable[[List[CloudApiClientModel]], Any]],
|
||||
model_class: Type[CloudApiClientModel]) -> None:
|
||||
if "data" in response:
|
||||
data = response["data"]
|
||||
if isinstance(data, list):
|
||||
results = [model_class(**c) for c in data] # type: List[CloudApiClientModel]
|
||||
on_finished_list = cast(Callable[[List[CloudApiClientModel]], Any], on_finished)
|
||||
on_finished_list(results)
|
||||
else:
|
||||
result = model_class(**data) # type: CloudApiClientModel
|
||||
on_finished_item = cast(Callable[[CloudApiClientModel], Any], on_finished)
|
||||
on_finished_item(result)
|
||||
elif "errors" in response:
|
||||
self._on_error([CloudError(**error) for error in response["errors"]])
|
||||
else:
|
||||
Logger.log("e", "Cannot find data or errors in the cloud response: %s", response)
|
||||
|
||||
## Creates a callback function so that it includes the parsing of the response into the correct model.
|
||||
# The callback is added to the 'finished' signal of the reply.
|
||||
# \param reply: The reply that should be listened to.
|
||||
# \param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either
|
||||
# a list or a single item.
|
||||
# \param model: The type of the model to convert the response to.
|
||||
def _addCallback(self,
|
||||
reply: QNetworkReply,
|
||||
on_finished: Union[Callable[[CloudApiClientModel], Any],
|
||||
Callable[[List[CloudApiClientModel]], Any]],
|
||||
model: Type[CloudApiClientModel],
|
||||
) -> None:
|
||||
def parse() -> None:
|
||||
status_code, response = self._parseReply(reply)
|
||||
self._anti_gc_callbacks.remove(parse)
|
||||
return self._parseModels(response, on_finished, model)
|
||||
|
||||
self._anti_gc_callbacks.append(parse)
|
||||
reply.finished.connect(parse)
|
@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from .CloudOutputDevice import CloudOutputDevice
|
||||
|
||||
|
||||
class CloudOutputController(PrinterOutputController):
|
||||
def __init__(self, output_device: "CloudOutputDevice") -> None:
|
||||
super().__init__(output_device)
|
||||
|
||||
# The cloud connection only supports fetching the printer and queue status and adding a job to the queue.
|
||||
# To let the UI know this we mark all features below as False.
|
||||
self.can_pause = False
|
||||
self.can_abort = False
|
||||
self.can_pre_heat_bed = False
|
||||
self.can_pre_heat_hotends = False
|
||||
self.can_send_raw_gcode = False
|
||||
self.can_control_manually = False
|
||||
self.can_update_firmware = False
|
424
plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py
Normal file
424
plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py
Normal file
@ -0,0 +1,424 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import os
|
||||
|
||||
from time import time
|
||||
from typing import Dict, List, Optional, Set, cast
|
||||
|
||||
from PyQt5.QtCore import QObject, QUrl, pyqtProperty, pyqtSignal, pyqtSlot
|
||||
|
||||
from UM import i18nCatalog
|
||||
from UM.Backend.Backend import BackendState
|
||||
from UM.FileHandler.FileHandler import FileHandler
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Qt.Duration import Duration, DurationFormat
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutputDevice import ConnectionType
|
||||
|
||||
from .CloudOutputController import CloudOutputController
|
||||
from ..MeshFormatHandler import MeshFormatHandler
|
||||
from ..UM3PrintJobOutputModel import UM3PrintJobOutputModel
|
||||
from .CloudProgressMessage import CloudProgressMessage
|
||||
from .CloudApiClient import CloudApiClient
|
||||
from .Models.CloudClusterResponse import CloudClusterResponse
|
||||
from .Models.CloudClusterStatus import CloudClusterStatus
|
||||
from .Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest
|
||||
from .Models.CloudPrintResponse import CloudPrintResponse
|
||||
from .Models.CloudPrintJobResponse import CloudPrintJobResponse
|
||||
from .Models.CloudClusterPrinterStatus import CloudClusterPrinterStatus
|
||||
from .Models.CloudClusterPrintJobStatus import CloudClusterPrintJobStatus
|
||||
from .Utils import findChanges, formatDateCompleted, formatTimeCompleted
|
||||
|
||||
|
||||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
|
||||
## The cloud output device is a network output device that works remotely but has limited functionality.
|
||||
# Currently it only supports viewing the printer and print job status and adding a new job to the queue.
|
||||
# As such, those methods have been implemented here.
|
||||
# Note that this device represents a single remote cluster, not a list of multiple clusters.
|
||||
class CloudOutputDevice(NetworkedPrinterOutputDevice):
|
||||
|
||||
# The interval with which the remote clusters are checked
|
||||
CHECK_CLUSTER_INTERVAL = 10.0 # seconds
|
||||
|
||||
# Signal triggered when the print jobs in the queue were changed.
|
||||
printJobsChanged = pyqtSignal()
|
||||
|
||||
# Signal triggered when the selected printer in the UI should be changed.
|
||||
activePrinterChanged = pyqtSignal()
|
||||
|
||||
# Notify can only use signals that are defined by the class that they are in, not inherited ones.
|
||||
# Therefore we create a private signal used to trigger the printersChanged signal.
|
||||
_clusterPrintersChanged = pyqtSignal()
|
||||
|
||||
## Creates a new cloud output device
|
||||
# \param api_client: The client that will run the API calls
|
||||
# \param cluster: The device response received from the cloud API.
|
||||
# \param parent: The optional parent of this output device.
|
||||
def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None:
|
||||
super().__init__(device_id = cluster.cluster_id, address = "",
|
||||
connection_type = ConnectionType.CloudConnection, properties = {}, parent = parent)
|
||||
self._api = api_client
|
||||
self._cluster = cluster
|
||||
|
||||
self._setInterfaceElements()
|
||||
|
||||
self._account = api_client.account
|
||||
|
||||
# We use the Cura Connect monitor tab to get most functionality right away.
|
||||
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
|
||||
"../../resources/qml/MonitorStage.qml")
|
||||
|
||||
# Trigger the printersChanged signal when the private signal is triggered.
|
||||
self.printersChanged.connect(self._clusterPrintersChanged)
|
||||
|
||||
# We keep track of which printer is visible in the monitor page.
|
||||
self._active_printer = None # type: Optional[PrinterOutputModel]
|
||||
|
||||
# Properties to populate later on with received cloud data.
|
||||
self._print_jobs = [] # type: List[UM3PrintJobOutputModel]
|
||||
self._number_of_extruders = 2 # All networked printers are dual-extrusion Ultimaker machines.
|
||||
|
||||
# We only allow a single upload at a time.
|
||||
self._progress = CloudProgressMessage()
|
||||
|
||||
# Keep server string of the last generated time to avoid updating models more than once for the same response
|
||||
self._received_printers = None # type: Optional[List[CloudClusterPrinterStatus]]
|
||||
self._received_print_jobs = None # type: Optional[List[CloudClusterPrintJobStatus]]
|
||||
|
||||
# A set of the user's job IDs that have finished
|
||||
self._finished_jobs = set() # type: Set[str]
|
||||
|
||||
# Reference to the uploaded print job / mesh
|
||||
self._tool_path = None # type: Optional[bytes]
|
||||
self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse]
|
||||
|
||||
## Connects this device.
|
||||
def connect(self) -> None:
|
||||
if self.isConnected():
|
||||
return
|
||||
super().connect()
|
||||
Logger.log("i", "Connected to cluster %s", self.key)
|
||||
CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange)
|
||||
|
||||
## Disconnects the device
|
||||
def disconnect(self) -> None:
|
||||
super().disconnect()
|
||||
Logger.log("i", "Disconnected from cluster %s", self.key)
|
||||
CuraApplication.getInstance().getBackend().backendStateChange.disconnect(self._onBackendStateChange)
|
||||
|
||||
## Resets the print job that was uploaded to force a new upload, runs whenever the user re-slices.
|
||||
def _onBackendStateChange(self, _: BackendState) -> None:
|
||||
self._tool_path = None
|
||||
self._uploaded_print_job = None
|
||||
|
||||
## Gets the cluster response from which this device was created.
|
||||
@property
|
||||
def clusterData(self) -> CloudClusterResponse:
|
||||
return self._cluster
|
||||
|
||||
## Updates the cluster data from the cloud.
|
||||
@clusterData.setter
|
||||
def clusterData(self, value: CloudClusterResponse) -> None:
|
||||
self._cluster = value
|
||||
|
||||
## Checks whether the given network key is found in the cloud's host name
|
||||
def matchesNetworkKey(self, network_key: str) -> bool:
|
||||
# A network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local."
|
||||
# the host name should then be "ultimakersystem-aabbccdd0011"
|
||||
return network_key.startswith(self.clusterData.host_name)
|
||||
|
||||
## Set all the interface elements and texts for this output device.
|
||||
def _setInterfaceElements(self) -> None:
|
||||
self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'
|
||||
self.setName(self._id)
|
||||
self.setShortDescription(I18N_CATALOG.i18nc("@action:button", "Print via Cloud"))
|
||||
self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud"))
|
||||
self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud"))
|
||||
|
||||
## Called when Cura requests an output device to receive a (G-code) file.
|
||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||
file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
||||
|
||||
# Show an error message if we're already sending a job.
|
||||
if self._progress.visible:
|
||||
message = Message(
|
||||
text = I18N_CATALOG.i18nc("@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job."),
|
||||
title = I18N_CATALOG.i18nc("@info:title", "Cloud error"),
|
||||
lifetime = 10
|
||||
)
|
||||
message.show()
|
||||
return
|
||||
|
||||
if self._uploaded_print_job:
|
||||
# The mesh didn't change, let's not upload it again
|
||||
self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintUploadCompleted)
|
||||
return
|
||||
|
||||
# Indicate we have started sending a job.
|
||||
self.writeStarted.emit(self)
|
||||
|
||||
mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion)
|
||||
if not mesh_format.is_valid:
|
||||
Logger.log("e", "Missing file or mesh writer!")
|
||||
return self._onUploadError(I18N_CATALOG.i18nc("@info:status", "Could not export print job."))
|
||||
|
||||
mesh = mesh_format.getBytes(nodes)
|
||||
|
||||
self._tool_path = mesh
|
||||
request = CloudPrintJobUploadRequest(
|
||||
job_name = file_name or mesh_format.file_extension,
|
||||
file_size = len(mesh),
|
||||
content_type = mesh_format.mime_type,
|
||||
)
|
||||
self._api.requestUpload(request, self._onPrintJobCreated)
|
||||
|
||||
## Called when the network data should be updated.
|
||||
def _update(self) -> None:
|
||||
super()._update()
|
||||
if self._last_request_time and time() - self._last_request_time < self.CHECK_CLUSTER_INTERVAL:
|
||||
return # Avoid calling the cloud too often
|
||||
|
||||
Logger.log("d", "Updating: %s - %s >= %s", time(), self._last_request_time, self.CHECK_CLUSTER_INTERVAL)
|
||||
if self._account.isLoggedIn:
|
||||
self.setAuthenticationState(AuthState.Authenticated)
|
||||
self._last_request_time = time()
|
||||
self._api.getClusterStatus(self.key, self._onStatusCallFinished)
|
||||
else:
|
||||
self.setAuthenticationState(AuthState.NotAuthenticated)
|
||||
|
||||
## Method called when HTTP request to status endpoint is finished.
|
||||
# Contains both printers and print jobs statuses in a single response.
|
||||
def _onStatusCallFinished(self, status: CloudClusterStatus) -> None:
|
||||
# Update all data from the cluster.
|
||||
self._last_response_time = time()
|
||||
if self._received_printers != status.printers:
|
||||
self._received_printers = status.printers
|
||||
self._updatePrinters(status.printers)
|
||||
|
||||
if status.print_jobs != self._received_print_jobs:
|
||||
self._received_print_jobs = status.print_jobs
|
||||
self._updatePrintJobs(status.print_jobs)
|
||||
|
||||
## Updates the local list of printers with the list received from the cloud.
|
||||
# \param jobs: The printers received from the cloud.
|
||||
def _updatePrinters(self, printers: List[CloudClusterPrinterStatus]) -> None:
|
||||
previous = {p.key: p for p in self._printers} # type: Dict[str, PrinterOutputModel]
|
||||
received = {p.uuid: p for p in printers} # type: Dict[str, CloudClusterPrinterStatus]
|
||||
|
||||
removed_printers, added_printers, updated_printers = findChanges(previous, received)
|
||||
|
||||
for removed_printer in removed_printers:
|
||||
if self._active_printer == removed_printer:
|
||||
self.setActivePrinter(None)
|
||||
self._printers.remove(removed_printer)
|
||||
|
||||
for added_printer in added_printers:
|
||||
self._printers.append(added_printer.createOutputModel(CloudOutputController(self)))
|
||||
|
||||
for model, printer in updated_printers:
|
||||
printer.updateOutputModel(model)
|
||||
|
||||
# Always have an active printer
|
||||
if self._printers and not self._active_printer:
|
||||
self.setActivePrinter(self._printers[0])
|
||||
|
||||
if added_printers or removed_printers:
|
||||
self.printersChanged.emit()
|
||||
|
||||
## Updates the local list of print jobs with the list received from the cloud.
|
||||
# \param jobs: The print jobs received from the cloud.
|
||||
def _updatePrintJobs(self, jobs: List[CloudClusterPrintJobStatus]) -> None:
|
||||
received = {j.uuid: j for j in jobs} # type: Dict[str, CloudClusterPrintJobStatus]
|
||||
previous = {j.key: j for j in self._print_jobs} # type: Dict[str, UM3PrintJobOutputModel]
|
||||
|
||||
removed_jobs, added_jobs, updated_jobs = findChanges(previous, received)
|
||||
|
||||
for removed_job in removed_jobs:
|
||||
if removed_job.assignedPrinter:
|
||||
removed_job.assignedPrinter.updateActivePrintJob(None)
|
||||
removed_job.stateChanged.disconnect(self._onPrintJobStateChanged)
|
||||
self._print_jobs.remove(removed_job)
|
||||
|
||||
for added_job in added_jobs:
|
||||
self._addPrintJob(added_job)
|
||||
|
||||
for model, job in updated_jobs:
|
||||
job.updateOutputModel(model)
|
||||
if job.printer_uuid:
|
||||
self._updateAssignedPrinter(model, job.printer_uuid)
|
||||
|
||||
# We only have to update when jobs are added or removed
|
||||
# updated jobs push their changes via their output model
|
||||
if added_jobs or removed_jobs:
|
||||
self.printJobsChanged.emit()
|
||||
|
||||
## Registers a new print job received via the cloud API.
|
||||
# \param job: The print job received.
|
||||
def _addPrintJob(self, job: CloudClusterPrintJobStatus) -> None:
|
||||
model = job.createOutputModel(CloudOutputController(self))
|
||||
model.stateChanged.connect(self._onPrintJobStateChanged)
|
||||
if job.printer_uuid:
|
||||
self._updateAssignedPrinter(model, job.printer_uuid)
|
||||
self._print_jobs.append(model)
|
||||
|
||||
## Handles the event of a change in a print job state
|
||||
def _onPrintJobStateChanged(self) -> None:
|
||||
user_name = self._getUserName()
|
||||
# TODO: confirm that notifications in Cura are still required
|
||||
for job in self._print_jobs:
|
||||
if job.state == "wait_cleanup" and job.key not in self._finished_jobs and job.owner == user_name:
|
||||
self._finished_jobs.add(job.key)
|
||||
Message(
|
||||
title = I18N_CATALOG.i18nc("@info:status", "Print finished"),
|
||||
text = (I18N_CATALOG.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.").format(
|
||||
printer_name = job.assignedPrinter.name,
|
||||
job_name = job.name
|
||||
) if job.assignedPrinter else
|
||||
I18N_CATALOG.i18nc("@info:status", "The print job '{job_name}' was finished.").format(
|
||||
job_name = job.name
|
||||
)),
|
||||
).show()
|
||||
|
||||
## Updates the printer assignment for the given print job model.
|
||||
def _updateAssignedPrinter(self, model: UM3PrintJobOutputModel, printer_uuid: str) -> None:
|
||||
printer = next((p for p in self._printers if printer_uuid == p.key), None)
|
||||
if not printer:
|
||||
Logger.log("w", "Missing printer %s for job %s in %s", model.assignedPrinter, model.key,
|
||||
[p.key for p in self._printers])
|
||||
return
|
||||
|
||||
printer.updateActivePrintJob(model)
|
||||
model.updateAssignedPrinter(printer)
|
||||
|
||||
## Uploads the mesh when the print job was registered with the cloud API.
|
||||
# \param job_response: The response received from the cloud API.
|
||||
def _onPrintJobCreated(self, job_response: CloudPrintJobResponse) -> None:
|
||||
self._progress.show()
|
||||
self._uploaded_print_job = job_response
|
||||
tool_path = cast(bytes, self._tool_path)
|
||||
self._api.uploadToolPath(job_response, tool_path, self._onPrintJobUploaded, self._progress.update, self._onUploadError)
|
||||
|
||||
## Requests the print to be sent to the printer when we finished uploading the mesh.
|
||||
def _onPrintJobUploaded(self) -> None:
|
||||
self._progress.update(100)
|
||||
print_job = cast(CloudPrintJobResponse, self._uploaded_print_job)
|
||||
self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted)
|
||||
|
||||
## Displays the given message if uploading the mesh has failed
|
||||
# \param message: The message to display.
|
||||
def _onUploadError(self, message: str = None) -> None:
|
||||
self._progress.hide()
|
||||
self._uploaded_print_job = None
|
||||
Message(
|
||||
text = message or I18N_CATALOG.i18nc("@info:text", "Could not upload the data to the printer."),
|
||||
title = I18N_CATALOG.i18nc("@info:title", "Cloud error"),
|
||||
lifetime = 10
|
||||
).show()
|
||||
self.writeError.emit()
|
||||
|
||||
## Shows a message when the upload has succeeded
|
||||
# \param response: The response from the cloud API.
|
||||
def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None:
|
||||
Logger.log("d", "The cluster will be printing this print job with the ID %s", response.cluster_job_id)
|
||||
self._progress.hide()
|
||||
Message(
|
||||
text = I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."),
|
||||
title = I18N_CATALOG.i18nc("@info:title", "Data Sent"),
|
||||
lifetime = 5
|
||||
).show()
|
||||
self.writeFinished.emit()
|
||||
|
||||
## Gets the remote printers.
|
||||
@pyqtProperty("QVariantList", notify=_clusterPrintersChanged)
|
||||
def printers(self) -> List[PrinterOutputModel]:
|
||||
return self._printers
|
||||
|
||||
## Get the active printer in the UI (monitor page).
|
||||
@pyqtProperty(QObject, notify = activePrinterChanged)
|
||||
def activePrinter(self) -> Optional[PrinterOutputModel]:
|
||||
return self._active_printer
|
||||
|
||||
## Set the active printer in the UI (monitor page).
|
||||
@pyqtSlot(QObject)
|
||||
def setActivePrinter(self, printer: Optional[PrinterOutputModel] = None) -> None:
|
||||
if printer != self._active_printer:
|
||||
self._active_printer = printer
|
||||
self.activePrinterChanged.emit()
|
||||
|
||||
@pyqtProperty(int, notify = _clusterPrintersChanged)
|
||||
def clusterSize(self) -> int:
|
||||
return len(self._printers)
|
||||
|
||||
## Get remote print jobs.
|
||||
@pyqtProperty("QVariantList", notify = printJobsChanged)
|
||||
def printJobs(self) -> List[UM3PrintJobOutputModel]:
|
||||
return self._print_jobs
|
||||
|
||||
## Get remote print jobs that are still in the print queue.
|
||||
@pyqtProperty("QVariantList", notify = printJobsChanged)
|
||||
def queuedPrintJobs(self) -> List[UM3PrintJobOutputModel]:
|
||||
return [print_job for print_job in self._print_jobs
|
||||
if print_job.state == "queued" or print_job.state == "error"]
|
||||
|
||||
## Get remote print jobs that are assigned to a printer.
|
||||
@pyqtProperty("QVariantList", notify = printJobsChanged)
|
||||
def activePrintJobs(self) -> List[UM3PrintJobOutputModel]:
|
||||
return [print_job for print_job in self._print_jobs if
|
||||
print_job.assignedPrinter is not None and print_job.state != "queued"]
|
||||
|
||||
@pyqtSlot(int, result = str)
|
||||
def formatDuration(self, seconds: int) -> str:
|
||||
return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
|
||||
|
||||
@pyqtSlot(int, result = str)
|
||||
def getTimeCompleted(self, time_remaining: int) -> str:
|
||||
return formatTimeCompleted(time_remaining)
|
||||
|
||||
@pyqtSlot(int, result = str)
|
||||
def getDateCompleted(self, time_remaining: int) -> str:
|
||||
return formatDateCompleted(time_remaining)
|
||||
|
||||
## TODO: The following methods are required by the monitor page QML, but are not actually available using cloud.
|
||||
# TODO: We fake the methods here to not break the monitor page.
|
||||
|
||||
@pyqtProperty(QUrl, notify = _clusterPrintersChanged)
|
||||
def activeCameraUrl(self) -> "QUrl":
|
||||
return QUrl()
|
||||
|
||||
@pyqtSlot(QUrl)
|
||||
def setActiveCameraUrl(self, camera_url: "QUrl") -> None:
|
||||
pass
|
||||
|
||||
@pyqtProperty(bool, notify = printJobsChanged)
|
||||
def receivedPrintJobs(self) -> bool:
|
||||
return bool(self._print_jobs)
|
||||
|
||||
@pyqtSlot()
|
||||
def openPrintJobControlPanel(self) -> None:
|
||||
pass
|
||||
|
||||
@pyqtSlot()
|
||||
def openPrinterControlPanel(self) -> None:
|
||||
pass
|
||||
|
||||
@pyqtSlot(str)
|
||||
def sendJobToTop(self, print_job_uuid: str) -> None:
|
||||
pass
|
||||
|
||||
@pyqtSlot(str)
|
||||
def deleteJobFromQueue(self, print_job_uuid: str) -> None:
|
||||
pass
|
||||
|
||||
@pyqtSlot(str)
|
||||
def forceSendJob(self, print_job_uuid: str) -> None:
|
||||
pass
|
||||
|
||||
@pyqtProperty("QVariantList", notify = _clusterPrintersChanged)
|
||||
def connectedPrintersTypeCount(self) -> List[Dict[str, str]]:
|
||||
return []
|
170
plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py
Normal file
170
plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py
Normal file
@ -0,0 +1,170 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Dict, List
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
||||
from UM import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from cura.API import Account
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from .CloudApiClient import CloudApiClient
|
||||
from .CloudOutputDevice import CloudOutputDevice
|
||||
from .Models.CloudClusterResponse import CloudClusterResponse
|
||||
from .Models.CloudError import CloudError
|
||||
from .Utils import findChanges
|
||||
|
||||
|
||||
## The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
|
||||
# Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code.
|
||||
#
|
||||
# API spec is available on https://api.ultimaker.com/docs/connect/spec/.
|
||||
#
|
||||
class CloudOutputDeviceManager:
|
||||
META_CLUSTER_ID = "um_cloud_cluster_id"
|
||||
|
||||
# The interval with which the remote clusters are checked
|
||||
CHECK_CLUSTER_INTERVAL = 30.0 # seconds
|
||||
|
||||
# The translation catalog for this device.
|
||||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
def __init__(self) -> None:
|
||||
# Persistent dict containing the remote clusters for the authenticated user.
|
||||
self._remote_clusters = {} # type: Dict[str, CloudOutputDevice]
|
||||
|
||||
application = CuraApplication.getInstance()
|
||||
self._output_device_manager = application.getOutputDeviceManager()
|
||||
|
||||
self._account = application.getCuraAPI().account # type: Account
|
||||
self._api = CloudApiClient(self._account, self._onApiError)
|
||||
|
||||
# Create a timer to update the remote cluster list
|
||||
self._update_timer = QTimer()
|
||||
self._update_timer.setInterval(int(self.CHECK_CLUSTER_INTERVAL * 1000))
|
||||
self._update_timer.setSingleShot(False)
|
||||
|
||||
self._running = False
|
||||
|
||||
# Called when the uses logs in or out
|
||||
def _onLoginStateChanged(self, is_logged_in: bool) -> None:
|
||||
Logger.log("d", "Log in state changed to %s", is_logged_in)
|
||||
if is_logged_in:
|
||||
if not self._update_timer.isActive():
|
||||
self._update_timer.start()
|
||||
self._getRemoteClusters()
|
||||
else:
|
||||
if self._update_timer.isActive():
|
||||
self._update_timer.stop()
|
||||
|
||||
# Notify that all clusters have disappeared
|
||||
self._onGetRemoteClustersFinished([])
|
||||
|
||||
## Gets all remote clusters from the API.
|
||||
def _getRemoteClusters(self) -> None:
|
||||
Logger.log("d", "Retrieving remote clusters")
|
||||
self._api.getClusters(self._onGetRemoteClustersFinished)
|
||||
|
||||
## Callback for when the request for getting the clusters. is finished.
|
||||
def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None:
|
||||
online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse]
|
||||
|
||||
removed_devices, added_clusters, updates = findChanges(self._remote_clusters, online_clusters)
|
||||
|
||||
Logger.log("d", "Parsed remote clusters to %s", [cluster.toDict() for cluster in online_clusters.values()])
|
||||
Logger.log("d", "Removed: %s, added: %s, updates: %s", len(removed_devices), len(added_clusters), len(updates))
|
||||
|
||||
# Remove output devices that are gone
|
||||
for removed_cluster in removed_devices:
|
||||
if removed_cluster.isConnected():
|
||||
removed_cluster.disconnect()
|
||||
removed_cluster.close()
|
||||
self._output_device_manager.removeOutputDevice(removed_cluster.key)
|
||||
del self._remote_clusters[removed_cluster.key]
|
||||
|
||||
# Add an output device for each new remote cluster.
|
||||
# We only add when is_online as we don't want the option in the drop down if the cluster is not online.
|
||||
for added_cluster in added_clusters:
|
||||
device = CloudOutputDevice(self._api, added_cluster)
|
||||
self._remote_clusters[added_cluster.cluster_id] = device
|
||||
|
||||
for device, cluster in updates:
|
||||
device.clusterData = cluster
|
||||
|
||||
self._connectToActiveMachine()
|
||||
|
||||
## Callback for when the active machine was changed by the user or a new remote cluster was found.
|
||||
def _connectToActiveMachine(self) -> None:
|
||||
active_machine = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not active_machine:
|
||||
return
|
||||
|
||||
# Remove all output devices that we have registered.
|
||||
# This is needed because when we switch machines we can only leave
|
||||
# output devices that are meant for that machine.
|
||||
for stored_cluster_id in self._remote_clusters:
|
||||
self._output_device_manager.removeOutputDevice(stored_cluster_id)
|
||||
|
||||
# Check if the stored cluster_id for the active machine is in our list of remote clusters.
|
||||
stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID)
|
||||
if stored_cluster_id in self._remote_clusters:
|
||||
device = self._remote_clusters[stored_cluster_id]
|
||||
self._connectToOutputDevice(device)
|
||||
Logger.log("d", "Device connected by metadata cluster ID %s", stored_cluster_id)
|
||||
else:
|
||||
self._connectByNetworkKey(active_machine)
|
||||
|
||||
## Tries to match the local network key to the cloud cluster host name.
|
||||
def _connectByNetworkKey(self, active_machine: GlobalStack) -> None:
|
||||
# Check if the active printer has a local network connection and match this key to the remote cluster.
|
||||
local_network_key = active_machine.getMetaDataEntry("um_network_key")
|
||||
if not local_network_key:
|
||||
return
|
||||
|
||||
device = next((c for c in self._remote_clusters.values() if c.matchesNetworkKey(local_network_key)), None)
|
||||
if not device:
|
||||
return
|
||||
|
||||
Logger.log("i", "Found cluster %s with network key %s", device, local_network_key)
|
||||
active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
|
||||
self._connectToOutputDevice(device)
|
||||
|
||||
## Connects to an output device and makes sure it is registered in the output device manager.
|
||||
def _connectToOutputDevice(self, device: CloudOutputDevice) -> None:
|
||||
device.connect()
|
||||
self._output_device_manager.addOutputDevice(device)
|
||||
|
||||
## Handles an API error received from the cloud.
|
||||
# \param errors: The errors received
|
||||
def _onApiError(self, errors: List[CloudError] = None) -> None:
|
||||
Logger.log("w", str(errors))
|
||||
message = Message(
|
||||
text = self.I18N_CATALOG.i18nc("@info:description", "There was an error connecting to the cloud."),
|
||||
title = self.I18N_CATALOG.i18nc("@info:title", "Error"),
|
||||
lifetime = 10
|
||||
)
|
||||
message.show()
|
||||
|
||||
## Starts running the cloud output device manager, thus periodically requesting cloud data.
|
||||
def start(self):
|
||||
if self._running:
|
||||
return
|
||||
application = CuraApplication.getInstance()
|
||||
self._account.loginStateChanged.connect(self._onLoginStateChanged)
|
||||
# When switching machines we check if we have to activate a remote cluster.
|
||||
application.globalContainerStackChanged.connect(self._connectToActiveMachine)
|
||||
self._update_timer.timeout.connect(self._getRemoteClusters)
|
||||
self._onLoginStateChanged(is_logged_in = self._account.isLoggedIn)
|
||||
|
||||
## Stops running the cloud output device manager.
|
||||
def stop(self):
|
||||
if not self._running:
|
||||
return
|
||||
application = CuraApplication.getInstance()
|
||||
self._account.loginStateChanged.disconnect(self._onLoginStateChanged)
|
||||
# When switching machines we check if we have to activate a remote cluster.
|
||||
application.globalContainerStackChanged.disconnect(self._connectToActiveMachine)
|
||||
self._update_timer.timeout.disconnect(self._getRemoteClusters)
|
||||
self._onLoginStateChanged(is_logged_in = False)
|
32
plugins/UM3NetworkPrinting/src/Cloud/CloudProgressMessage.py
Normal file
32
plugins/UM3NetworkPrinting/src/Cloud/CloudProgressMessage.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from UM import i18nCatalog
|
||||
from UM.Message import Message
|
||||
|
||||
|
||||
I18N_CATALOG = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Class responsible for showing a progress message while a mesh is being uploaded to the cloud.
|
||||
class CloudProgressMessage(Message):
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
text = I18N_CATALOG.i18nc("@info:status", "Sending data to remote cluster"),
|
||||
title = I18N_CATALOG.i18nc("@info:status", "Sending data to remote cluster"),
|
||||
progress = -1,
|
||||
lifetime = 0,
|
||||
dismissable = False,
|
||||
use_inactivity_timer = False
|
||||
)
|
||||
|
||||
## Shows the progress message.
|
||||
def show(self):
|
||||
self.setProgress(0)
|
||||
super().show()
|
||||
|
||||
## Updates the percentage of the uploaded.
|
||||
# \param percentage: The percentage amount (0-100).
|
||||
def update(self, percentage: int) -> None:
|
||||
if not self._visible:
|
||||
super().show()
|
||||
self.setProgress(percentage)
|
@ -0,0 +1,55 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from datetime import datetime, timezone
|
||||
from typing import Dict, Union, TypeVar, Type, List, Any
|
||||
|
||||
from ...Models import BaseModel
|
||||
|
||||
|
||||
## Base class for the models used in the interface with the Ultimaker cloud APIs.
|
||||
class BaseCloudModel(BaseModel):
|
||||
## Checks whether the two models are equal.
|
||||
# \param other: The other model.
|
||||
# \return True if they are equal, False if they are different.
|
||||
def __eq__(self, other):
|
||||
return type(self) == type(other) and self.toDict() == other.toDict()
|
||||
|
||||
## Checks whether the two models are different.
|
||||
# \param other: The other model.
|
||||
# \return True if they are different, False if they are the same.
|
||||
def __ne__(self, other) -> bool:
|
||||
return type(self) != type(other) or self.toDict() != other.toDict()
|
||||
|
||||
## Converts the model into a serializable dictionary
|
||||
def toDict(self) -> Dict[str, Any]:
|
||||
return self.__dict__
|
||||
|
||||
# Type variable used in the parse methods below, which should be a subclass of BaseModel.
|
||||
T = TypeVar("T", bound=BaseModel)
|
||||
|
||||
## Parses a single model.
|
||||
# \param model_class: The model class.
|
||||
# \param values: The value of the model, which is usually a dictionary, but may also be already parsed.
|
||||
# \return An instance of the model_class given.
|
||||
@staticmethod
|
||||
def parseModel(model_class: Type[T], values: Union[T, Dict[str, Any]]) -> T:
|
||||
if isinstance(values, dict):
|
||||
return model_class(**values)
|
||||
return values
|
||||
|
||||
## Parses a list of models.
|
||||
# \param model_class: The model class.
|
||||
# \param values: The value of the list. Each value is usually a dictionary, but may also be already parsed.
|
||||
# \return A list of instances of the model_class given.
|
||||
@classmethod
|
||||
def parseModels(cls, model_class: Type[T], values: List[Union[T, Dict[str, Any]]]) -> List[T]:
|
||||
return [cls.parseModel(model_class, value) for value in values]
|
||||
|
||||
## Parses the given date string.
|
||||
# \param date: The date to parse.
|
||||
# \return The parsed date.
|
||||
@staticmethod
|
||||
def parseDate(date: Union[str, datetime]) -> datetime:
|
||||
if isinstance(date, datetime):
|
||||
return date
|
||||
return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc)
|
@ -0,0 +1,13 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from .BaseCloudModel import BaseCloudModel
|
||||
|
||||
|
||||
## Class representing a cluster printer
|
||||
# Spec: https://api-staging.ultimaker.com/connect/v1/spec
|
||||
class CloudClusterBuildPlate(BaseCloudModel):
|
||||
## Create a new build plate
|
||||
# \param type: The type of buildplate glass or aluminium
|
||||
def __init__(self, type: str = "glass", **kwargs) -> None:
|
||||
self.type = type
|
||||
super().__init__(**kwargs)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user