mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-12-02 15:21:15 -07:00
Merge branch 'master' into infill_mesh_shell_defaults
This commit is contained in:
commit
53e1742d27
341 changed files with 77506 additions and 69425 deletions
|
|
@ -4,7 +4,8 @@
|
|||
from configparser import ConfigParser
|
||||
import zipfile
|
||||
import os
|
||||
from typing import cast, Dict, List, Optional, Tuple
|
||||
import json
|
||||
from typing import cast, Dict, List, Optional, Tuple, Any
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
|
@ -732,7 +733,25 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
|
||||
base_file_name = os.path.basename(file_name)
|
||||
self.setWorkspaceName(base_file_name)
|
||||
return nodes
|
||||
|
||||
return nodes, self._loadMetadata(file_name)
|
||||
|
||||
@staticmethod
|
||||
def _loadMetadata(file_name: str) -> Dict[str, Dict[str, Any]]:
|
||||
archive = zipfile.ZipFile(file_name, "r")
|
||||
|
||||
metadata_files = [name for name in archive.namelist() if name.endswith("plugin_metadata.json")]
|
||||
|
||||
result = dict()
|
||||
|
||||
for metadata_file in metadata_files:
|
||||
try:
|
||||
plugin_id = metadata_file.split("/")[0]
|
||||
result[plugin_id] = json.loads(archive.open("%s/plugin_metadata.json" % plugin_id).read().decode("utf-8"))
|
||||
except Exception:
|
||||
Logger.logException("w", "Unable to retrieve metadata for %s", metadata_file)
|
||||
|
||||
return result
|
||||
|
||||
def _processQualityChanges(self, global_stack):
|
||||
if self._machine_info.quality_changes_info is None:
|
||||
|
|
@ -1005,8 +1024,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
|
||||
# Set metadata fields that are missing from the global stack
|
||||
for key, value in self._machine_info.metadata_dict.items():
|
||||
if key not in global_stack.getMetaData():
|
||||
global_stack.setMetaDataEntry(key, value)
|
||||
global_stack.setMetaDataEntry(key, value)
|
||||
|
||||
def _updateActiveMachine(self, global_stack):
|
||||
# Actually change the active machine.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for reading 3MF files.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -73,11 +73,25 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
version_config_parser.write(version_file_string)
|
||||
archive.writestr(version_file, version_file_string.getvalue())
|
||||
|
||||
self._writePluginMetadataToArchive(archive)
|
||||
|
||||
# Close the archive & reset states.
|
||||
archive.close()
|
||||
mesh_writer.setStoreArchive(False)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _writePluginMetadataToArchive(archive: zipfile.ZipFile) -> None:
|
||||
file_name_template = "%s/plugin_metadata.json"
|
||||
|
||||
for plugin_id, metadata in Application.getInstance().getWorkspaceMetadataStorage().getAllData().items():
|
||||
file_name = file_name_template % plugin_id
|
||||
file_in_archive = zipfile.ZipInfo(file_name)
|
||||
# We have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
|
||||
file_in_archive.compress_type = zipfile.ZIP_DEFLATED
|
||||
import json
|
||||
archive.writestr(file_in_archive, json.dumps(metadata, separators = (", ", ": "), indent = 4, skipkeys = True))
|
||||
|
||||
## Helper function that writes ContainerStacks, InstanceContainers and DefinitionContainers to the archive.
|
||||
# \param container That follows the \type{ContainerInterface} to archive.
|
||||
# \param archive The archive to write to.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional
|
||||
|
||||
from UM.Mesh.MeshWriter import MeshWriter
|
||||
from UM.Math.Vector import Vector
|
||||
|
|
@ -40,7 +41,7 @@ class ThreeMFWriter(MeshWriter):
|
|||
}
|
||||
|
||||
self._unit_matrix_string = self._convertMatrixToString(Matrix())
|
||||
self._archive = None
|
||||
self._archive = None # type: Optional[zipfile.ZipFile]
|
||||
self._store_archive = False
|
||||
|
||||
def _convertMatrixToString(self, matrix):
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for writing 3MF files.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@
|
|||
"author": "fieldOfView",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for reading AMF files.",
|
||||
"api": "7.0.0"
|
||||
"api": "7.1.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"description": "Backup and restore your configuration.",
|
||||
"version": "1.2.0",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,10 +55,22 @@ class CuraEngineBackend(QObject, Backend):
|
|||
if Platform.isWindows():
|
||||
executable_name += ".exe"
|
||||
default_engine_location = executable_name
|
||||
if os.path.exists(os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name)):
|
||||
default_engine_location = os.path.join(CuraApplication.getInstallPrefix(), "bin", executable_name)
|
||||
if hasattr(sys, "frozen"):
|
||||
default_engine_location = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), executable_name)
|
||||
|
||||
search_path = [
|
||||
os.path.abspath(os.path.dirname(sys.executable)),
|
||||
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "bin")),
|
||||
os.path.abspath(os.path.join(os.path.dirname(sys.executable), "..")),
|
||||
|
||||
os.path.join(CuraApplication.getInstallPrefix(), "bin"),
|
||||
os.path.dirname(os.path.abspath(sys.executable)),
|
||||
]
|
||||
|
||||
for path in search_path:
|
||||
engine_path = os.path.join(path, executable_name)
|
||||
if os.path.isfile(engine_path):
|
||||
default_engine_location = engine_path
|
||||
break
|
||||
|
||||
if Platform.isLinux() and not default_engine_location:
|
||||
if not os.getenv("PATH"):
|
||||
raise OSError("There is something wrong with your Linux installation.")
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import numpy
|
||||
|
|
@ -153,7 +153,7 @@ class StartSliceJob(Job):
|
|||
self.setResult(StartJobResult.MaterialIncompatible)
|
||||
return
|
||||
|
||||
for position, extruder_stack in stack.extruders.items():
|
||||
for extruder_stack in stack.extruderList:
|
||||
material = extruder_stack.findContainer({"type": "material"})
|
||||
if not extruder_stack.isEnabled:
|
||||
continue
|
||||
|
|
@ -162,7 +162,6 @@ class StartSliceJob(Job):
|
|||
self.setResult(StartJobResult.MaterialIncompatible)
|
||||
return
|
||||
|
||||
|
||||
# Don't slice if there is a per object setting with an error value.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if not isinstance(node, CuraSceneNode) or not node.isSelectable():
|
||||
|
|
@ -172,146 +171,145 @@ class StartSliceJob(Job):
|
|||
self.setResult(StartJobResult.ObjectSettingError)
|
||||
return
|
||||
|
||||
with self._scene.getSceneLock():
|
||||
# Remove old layer data.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
|
||||
# Singe we walk through all nodes in the scene, they always have a parent.
|
||||
cast(SceneNode, node.getParent()).removeChild(node)
|
||||
break
|
||||
# Remove old layer data.
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if node.callDecoration("getLayerData") and node.callDecoration("getBuildPlateNumber") == self._build_plate_number:
|
||||
# Singe we walk through all nodes in the scene, they always have a parent.
|
||||
cast(SceneNode, node.getParent()).removeChild(node)
|
||||
break
|
||||
|
||||
# Get the objects in their groups to print.
|
||||
object_groups = []
|
||||
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||
for node in OneAtATimeIterator(self._scene.getRoot()):
|
||||
temp_list = []
|
||||
|
||||
# Node can't be printed, so don't bother sending it.
|
||||
if getattr(node, "_outside_buildarea", False):
|
||||
continue
|
||||
|
||||
# Filter on current build plate
|
||||
build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||
if build_plate_number is not None and build_plate_number != self._build_plate_number:
|
||||
continue
|
||||
|
||||
children = node.getAllChildren()
|
||||
children.append(node)
|
||||
for child_node in children:
|
||||
mesh_data = child_node.getMeshData()
|
||||
if mesh_data and mesh_data.getVertices() is not None:
|
||||
temp_list.append(child_node)
|
||||
|
||||
if temp_list:
|
||||
object_groups.append(temp_list)
|
||||
Job.yieldThread()
|
||||
if len(object_groups) == 0:
|
||||
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
|
||||
else:
|
||||
# Get the objects in their groups to print.
|
||||
object_groups = []
|
||||
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||
for node in OneAtATimeIterator(self._scene.getRoot()):
|
||||
temp_list = []
|
||||
has_printing_mesh = False
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
mesh_data = node.getMeshData()
|
||||
if node.callDecoration("isSliceable") and mesh_data and mesh_data.getVertices() is not None:
|
||||
is_non_printing_mesh = bool(node.callDecoration("isNonPrintingMesh"))
|
||||
|
||||
# Find a reason not to add the node
|
||||
if node.callDecoration("getBuildPlateNumber") != self._build_plate_number:
|
||||
continue
|
||||
if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh:
|
||||
continue
|
||||
# Node can't be printed, so don't bother sending it.
|
||||
if getattr(node, "_outside_buildarea", False):
|
||||
continue
|
||||
|
||||
temp_list.append(node)
|
||||
if not is_non_printing_mesh:
|
||||
has_printing_mesh = True
|
||||
# Filter on current build plate
|
||||
build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||
if build_plate_number is not None and build_plate_number != self._build_plate_number:
|
||||
continue
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
# If the list doesn't have any model with suitable settings then clean the list
|
||||
# otherwise CuraEngine will crash
|
||||
if not has_printing_mesh:
|
||||
temp_list.clear()
|
||||
children = node.getAllChildren()
|
||||
children.append(node)
|
||||
for child_node in children:
|
||||
mesh_data = child_node.getMeshData()
|
||||
if mesh_data and mesh_data.getVertices() is not None:
|
||||
temp_list.append(child_node)
|
||||
|
||||
if temp_list:
|
||||
object_groups.append(temp_list)
|
||||
Job.yieldThread()
|
||||
if len(object_groups) == 0:
|
||||
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
|
||||
else:
|
||||
temp_list = []
|
||||
has_printing_mesh = False
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
mesh_data = node.getMeshData()
|
||||
if node.callDecoration("isSliceable") and mesh_data and mesh_data.getVertices() is not None:
|
||||
is_non_printing_mesh = bool(node.callDecoration("isNonPrintingMesh"))
|
||||
|
||||
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return
|
||||
extruders_enabled = {position: stack.isEnabled for position, stack in global_stack.extruders.items()}
|
||||
filtered_object_groups = []
|
||||
has_model_with_disabled_extruders = False
|
||||
associated_disabled_extruders = set()
|
||||
for group in object_groups:
|
||||
stack = global_stack
|
||||
skip_group = False
|
||||
for node in group:
|
||||
# Only check if the printing extruder is enabled for printing meshes
|
||||
is_non_printing_mesh = node.callDecoration("evaluateIsNonPrintingMesh")
|
||||
extruder_position = node.callDecoration("getActiveExtruderPosition")
|
||||
if not is_non_printing_mesh and not extruders_enabled[extruder_position]:
|
||||
skip_group = True
|
||||
has_model_with_disabled_extruders = True
|
||||
associated_disabled_extruders.add(extruder_position)
|
||||
if not skip_group:
|
||||
filtered_object_groups.append(group)
|
||||
|
||||
if has_model_with_disabled_extruders:
|
||||
self.setResult(StartJobResult.ObjectsWithDisabledExtruder)
|
||||
associated_disabled_extruders = {str(c) for c in sorted([int(p) + 1 for p in associated_disabled_extruders])}
|
||||
self.setMessage(", ".join(associated_disabled_extruders))
|
||||
return
|
||||
|
||||
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
|
||||
# able to find a possible sequence or because there are no objects on the build plate (or they are outside
|
||||
# the build volume)
|
||||
if not filtered_object_groups:
|
||||
self.setResult(StartJobResult.NothingToSlice)
|
||||
return
|
||||
|
||||
self._buildGlobalSettingsMessage(stack)
|
||||
self._buildGlobalInheritsStackMessage(stack)
|
||||
|
||||
# Build messages for extruder stacks
|
||||
for extruder_stack in global_stack.extruderList:
|
||||
self._buildExtruderMessage(extruder_stack)
|
||||
|
||||
for group in filtered_object_groups:
|
||||
group_message = self._slice_message.addRepeatedMessage("object_lists")
|
||||
parent = group[0].getParent()
|
||||
if parent is not None and parent.callDecoration("isGroup"):
|
||||
self._handlePerObjectSettings(cast(CuraSceneNode, parent), group_message)
|
||||
|
||||
for object in group:
|
||||
mesh_data = object.getMeshData()
|
||||
if mesh_data is None:
|
||||
# Find a reason not to add the node
|
||||
if node.callDecoration("getBuildPlateNumber") != self._build_plate_number:
|
||||
continue
|
||||
if getattr(node, "_outside_buildarea", False) and not is_non_printing_mesh:
|
||||
continue
|
||||
rot_scale = object.getWorldTransformation().getTransposed().getData()[0:3, 0:3]
|
||||
translate = object.getWorldTransformation().getData()[:3, 3]
|
||||
|
||||
# This effectively performs a limited form of MeshData.getTransformed that ignores normals.
|
||||
verts = mesh_data.getVertices()
|
||||
verts = verts.dot(rot_scale)
|
||||
verts += translate
|
||||
temp_list.append(node)
|
||||
if not is_non_printing_mesh:
|
||||
has_printing_mesh = True
|
||||
|
||||
# Convert from Y up axes to Z up axes. Equals a 90 degree rotation.
|
||||
verts[:, [1, 2]] = verts[:, [2, 1]]
|
||||
verts[:, 1] *= -1
|
||||
Job.yieldThread()
|
||||
|
||||
obj = group_message.addRepeatedMessage("objects")
|
||||
obj.id = id(object)
|
||||
obj.name = object.getName()
|
||||
indices = mesh_data.getIndices()
|
||||
if indices is not None:
|
||||
flat_verts = numpy.take(verts, indices.flatten(), axis=0)
|
||||
else:
|
||||
flat_verts = numpy.array(verts)
|
||||
# If the list doesn't have any model with suitable settings then clean the list
|
||||
# otherwise CuraEngine will crash
|
||||
if not has_printing_mesh:
|
||||
temp_list.clear()
|
||||
|
||||
obj.vertices = flat_verts
|
||||
if temp_list:
|
||||
object_groups.append(temp_list)
|
||||
|
||||
self._handlePerObjectSettings(cast(CuraSceneNode, object), obj)
|
||||
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return
|
||||
extruders_enabled = {position: stack.isEnabled for position, stack in global_stack.extruders.items()}
|
||||
filtered_object_groups = []
|
||||
has_model_with_disabled_extruders = False
|
||||
associated_disabled_extruders = set()
|
||||
for group in object_groups:
|
||||
stack = global_stack
|
||||
skip_group = False
|
||||
for node in group:
|
||||
# Only check if the printing extruder is enabled for printing meshes
|
||||
is_non_printing_mesh = node.callDecoration("evaluateIsNonPrintingMesh")
|
||||
extruder_position = node.callDecoration("getActiveExtruderPosition")
|
||||
if not is_non_printing_mesh and not extruders_enabled[extruder_position]:
|
||||
skip_group = True
|
||||
has_model_with_disabled_extruders = True
|
||||
associated_disabled_extruders.add(extruder_position)
|
||||
if not skip_group:
|
||||
filtered_object_groups.append(group)
|
||||
|
||||
Job.yieldThread()
|
||||
if has_model_with_disabled_extruders:
|
||||
self.setResult(StartJobResult.ObjectsWithDisabledExtruder)
|
||||
associated_disabled_extruders = {str(c) for c in sorted([int(p) + 1 for p in associated_disabled_extruders])}
|
||||
self.setMessage(", ".join(associated_disabled_extruders))
|
||||
return
|
||||
|
||||
# There are cases when there is nothing to slice. This can happen due to one at a time slicing not being
|
||||
# able to find a possible sequence or because there are no objects on the build plate (or they are outside
|
||||
# the build volume)
|
||||
if not filtered_object_groups:
|
||||
self.setResult(StartJobResult.NothingToSlice)
|
||||
return
|
||||
|
||||
self._buildGlobalSettingsMessage(stack)
|
||||
self._buildGlobalInheritsStackMessage(stack)
|
||||
|
||||
# Build messages for extruder stacks
|
||||
for extruder_stack in global_stack.extruderList:
|
||||
self._buildExtruderMessage(extruder_stack)
|
||||
|
||||
for group in filtered_object_groups:
|
||||
group_message = self._slice_message.addRepeatedMessage("object_lists")
|
||||
parent = group[0].getParent()
|
||||
if parent is not None and parent.callDecoration("isGroup"):
|
||||
self._handlePerObjectSettings(cast(CuraSceneNode, parent), group_message)
|
||||
|
||||
for object in group:
|
||||
mesh_data = object.getMeshData()
|
||||
if mesh_data is None:
|
||||
continue
|
||||
rot_scale = object.getWorldTransformation().getTransposed().getData()[0:3, 0:3]
|
||||
translate = object.getWorldTransformation().getData()[:3, 3]
|
||||
|
||||
# This effectively performs a limited form of MeshData.getTransformed that ignores normals.
|
||||
verts = mesh_data.getVertices()
|
||||
verts = verts.dot(rot_scale)
|
||||
verts += translate
|
||||
|
||||
# Convert from Y up axes to Z up axes. Equals a 90 degree rotation.
|
||||
verts[:, [1, 2]] = verts[:, [2, 1]]
|
||||
verts[:, 1] *= -1
|
||||
|
||||
obj = group_message.addRepeatedMessage("objects")
|
||||
obj.id = id(object)
|
||||
obj.name = object.getName()
|
||||
indices = mesh_data.getIndices()
|
||||
if indices is not None:
|
||||
flat_verts = numpy.take(verts, indices.flatten(), axis=0)
|
||||
else:
|
||||
flat_verts = numpy.array(verts)
|
||||
|
||||
obj.vertices = flat_verts
|
||||
|
||||
self._handlePerObjectSettings(cast(CuraSceneNode, object), obj)
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
self.setResult(StartJobResult.Finished)
|
||||
|
||||
|
|
@ -345,10 +343,7 @@ class StartSliceJob(Job):
|
|||
result["time"] = time.strftime("%H:%M:%S") #Some extra settings.
|
||||
result["date"] = time.strftime("%d-%m-%Y")
|
||||
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
|
||||
|
||||
initial_extruder_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
|
||||
initial_extruder_nr = initial_extruder_stack.getProperty("extruder_nr", "value")
|
||||
result["initial_extruder_nr"] = initial_extruder_nr
|
||||
result["initial_extruder_nr"] = CuraApplication.getInstance().getExtruderManager().getInitialExtruderNr()
|
||||
|
||||
return result
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "CuraEngine Backend",
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": "Provides the link to the CuraEngine slicing backend.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"version": "1.0.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for importing Cura profiles.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for exporting Cura profiles.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog":"cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Checks for firmware updates.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a machine actions for updating firmware.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Reads g-code from a compressed archive.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Writes g-code to a compressed archive.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for importing profiles from g-code files.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import math
|
||||
|
|
@ -258,16 +258,19 @@ class FlavorParser:
|
|||
continue
|
||||
if item.startswith(";"):
|
||||
continue
|
||||
if item[0] == "X":
|
||||
x = float(item[1:])
|
||||
if item[0] == "Y":
|
||||
y = float(item[1:])
|
||||
if item[0] == "Z":
|
||||
z = float(item[1:])
|
||||
if item[0] == "F":
|
||||
f = float(item[1:]) / 60
|
||||
if item[0] == "E":
|
||||
e = float(item[1:])
|
||||
try:
|
||||
if item[0] == "X":
|
||||
x = float(item[1:])
|
||||
if item[0] == "Y":
|
||||
y = float(item[1:])
|
||||
if item[0] == "Z":
|
||||
z = float(item[1:])
|
||||
if item[0] == "F":
|
||||
f = float(item[1:]) / 60
|
||||
if item[0] == "E":
|
||||
e = float(item[1:])
|
||||
except ValueError: # Improperly formatted g-code: Coordinates are not floats.
|
||||
continue # Skip the command then.
|
||||
params = PositionOptional(x, y, z, f, e)
|
||||
return func(position, params, path)
|
||||
return position
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "G-code Reader",
|
||||
"author": "Victor Larchenko, Ultimaker",
|
||||
"author": "Victor Larchenko, Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Allows loading and displaying G-code files.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Writes g-code to a file.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Enables ability to generate printable geometry from 2D image files.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for importing profiles from legacy Cura versions.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "Machine Settings action",
|
||||
"author": "fieldOfView",
|
||||
"name": "Machine Settings Action",
|
||||
"author": "fieldOfView, Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "Model Checker",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"description": "Checks models and print configuration for possible printing issues and give suggestions.",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a monitor stage in Cura.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides the Per Model Settings.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
# There can be duplicates, which will be executed in sequence.
|
||||
self._script_list = [] # type: List[Script]
|
||||
self._selected_script_index = -1
|
||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.metaDataChanged.connect(self._restoreScriptInforFromMetadata)
|
||||
|
||||
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute)
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) # When the current printer changes, update the list of scripts.
|
||||
|
|
@ -209,33 +212,34 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
self.scriptListChanged.emit()
|
||||
self._propertyChanged()
|
||||
|
||||
## When the global container stack is changed, swap out the list of active
|
||||
# scripts.
|
||||
def _onGlobalContainerStackChanged(self) -> None:
|
||||
def _restoreScriptInforFromMetadata(self):
|
||||
self.loadAllScripts()
|
||||
new_stack = Application.getInstance().getGlobalContainerStack()
|
||||
new_stack = self._global_container_stack
|
||||
if new_stack is None:
|
||||
return
|
||||
self._script_list.clear()
|
||||
if not new_stack.getMetaDataEntry("post_processing_scripts"): # Missing or empty.
|
||||
self.scriptListChanged.emit() # Even emit this if it didn't change. We want it to write the empty list to the stack's metadata.
|
||||
if not new_stack.getMetaDataEntry("post_processing_scripts"): # Missing or empty.
|
||||
self.scriptListChanged.emit() # Even emit this if it didn't change. We want it to write the empty list to the stack's metadata.
|
||||
self.setSelectedScriptIndex(-1)
|
||||
return
|
||||
|
||||
self._script_list.clear()
|
||||
scripts_list_strs = new_stack.getMetaDataEntry("post_processing_scripts")
|
||||
for script_str in scripts_list_strs.split("\n"): # Encoded config files should never contain three newlines in a row. At most 2, just before section headers.
|
||||
for script_str in scripts_list_strs.split(
|
||||
"\n"): # Encoded config files should never contain three newlines in a row. At most 2, just before section headers.
|
||||
if not script_str: # There were no scripts in this one (or a corrupt file caused more than 3 consecutive newlines here).
|
||||
continue
|
||||
script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") # Unescape escape sequences.
|
||||
script_parser = configparser.ConfigParser(interpolation = None)
|
||||
script_parser = configparser.ConfigParser(interpolation=None)
|
||||
script_parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive.
|
||||
script_parser.read_string(script_str)
|
||||
for script_name, settings in script_parser.items(): # There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script.
|
||||
if script_name == "DEFAULT": # ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one.
|
||||
continue
|
||||
if script_name not in self._loaded_scripts: # Don't know this post-processing plug-in.
|
||||
Logger.log("e", "Unknown post-processing script {script_name} was encountered in this global stack.".format(script_name = script_name))
|
||||
if script_name not in self._loaded_scripts: # Don't know this post-processing plug-in.
|
||||
Logger.log("e",
|
||||
"Unknown post-processing script {script_name} was encountered in this global stack.".format(
|
||||
script_name=script_name))
|
||||
continue
|
||||
new_script = self._loaded_scripts[script_name]()
|
||||
new_script.initialize()
|
||||
|
|
@ -245,7 +249,22 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
self._script_list.append(new_script)
|
||||
|
||||
self.setSelectedScriptIndex(0)
|
||||
# Ensure that we always force an update (otherwise the fields don't update correctly!)
|
||||
self.selectedIndexChanged.emit()
|
||||
self.scriptListChanged.emit()
|
||||
self._propertyChanged()
|
||||
|
||||
## When the global container stack is changed, swap out the list of active
|
||||
# scripts.
|
||||
def _onGlobalContainerStackChanged(self) -> None:
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.metaDataChanged.disconnect(self._restoreScriptInforFromMetadata)
|
||||
|
||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.metaDataChanged.connect(self._restoreScriptInforFromMetadata)
|
||||
self._restoreScriptInforFromMetadata()
|
||||
|
||||
@pyqtSlot()
|
||||
def writeScriptsToStack(self) -> None:
|
||||
|
|
@ -267,14 +286,18 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
|
||||
script_list_string = "\n".join(script_list_strs) # ConfigParser should never output three newlines in a row when serialised, so it's a safe delimiter.
|
||||
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
if self._global_container_stack is None:
|
||||
return
|
||||
|
||||
if "post_processing_scripts" not in global_stack.getMetaData():
|
||||
global_stack.setMetaDataEntry("post_processing_scripts", "")
|
||||
# Ensure we don't get triggered by our own write.
|
||||
self._global_container_stack.metaDataChanged.disconnect(self._restoreScriptInforFromMetadata)
|
||||
|
||||
global_stack.setMetaDataEntry("post_processing_scripts", script_list_string)
|
||||
if "post_processing_scripts" not in self._global_container_stack.getMetaData():
|
||||
self._global_container_stack.setMetaDataEntry("post_processing_scripts", "")
|
||||
|
||||
self._global_container_stack.setMetaDataEntry("post_processing_scripts", script_list_string)
|
||||
# We do want to listen to other events.
|
||||
self._global_container_stack.metaDataChanged.connect(self._restoreScriptInforFromMetadata)
|
||||
|
||||
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
|
||||
def _createView(self) -> None:
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "Post Processing",
|
||||
"author": "Ultimaker",
|
||||
"version": "2.2.1",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"description": "Extension that allows for user created scripts for post processing",
|
||||
"catalog": "cura"
|
||||
}
|
||||
|
|
@ -72,18 +72,25 @@ class DisplayFilenameAndLayerOnLCD(Script):
|
|||
lcd_text = "M117 Printing " + name + " - Layer "
|
||||
i = self.getSettingValueByKey("startNum")
|
||||
for layer in data:
|
||||
display_text = lcd_text + str(i) + " " + name
|
||||
display_text = lcd_text + str(i)
|
||||
layer_index = data.index(layer)
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if line.startswith(";LAYER_COUNT:"):
|
||||
max_layer = line
|
||||
max_layer = max_layer.split(":")[1]
|
||||
if self.getSettingValueByKey("startNum") == 0:
|
||||
max_layer = str(int(max_layer) - 1)
|
||||
if line.startswith(";LAYER:"):
|
||||
if self.getSettingValueByKey("maxlayer"):
|
||||
display_text = display_text + " of " + max_layer
|
||||
if not self.getSettingValueByKey("scroll"):
|
||||
display_text = display_text + " " + name
|
||||
else:
|
||||
display_text = display_text + "!"
|
||||
if not self.getSettingValueByKey("scroll"):
|
||||
display_text = display_text + " " + name + "!"
|
||||
else:
|
||||
display_text = display_text + "!"
|
||||
line_index = lines.index(line)
|
||||
lines.insert(line_index + 1, display_text)
|
||||
i += 1
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class GCodeStep():
|
|||
Class to store the current value of each G_Code parameter
|
||||
for any G-Code step
|
||||
"""
|
||||
def __init__(self, step, in_relative_movement: bool = False):
|
||||
def __init__(self, step, in_relative_movement: bool = False) -> None:
|
||||
self.step = step
|
||||
self.step_x = 0
|
||||
self.step_y = 0
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
from ..Script import Script
|
||||
|
||||
|
||||
class TimeLapse(Script):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
|
@ -75,21 +76,29 @@ class TimeLapse(Script):
|
|||
trigger_command = self.getSettingValueByKey("trigger_command")
|
||||
pause_length = self.getSettingValueByKey("pause_length")
|
||||
gcode_to_append = ";TimeLapse Begin\n"
|
||||
last_x = 0
|
||||
last_y = 0
|
||||
|
||||
if park_print_head:
|
||||
gcode_to_append += self.putValue(G = 1, F = feed_rate, X = x_park, Y = y_park) + " ;Park print head\n"
|
||||
gcode_to_append += self.putValue(M = 400) + " ;Wait for moves to finish\n"
|
||||
gcode_to_append += self.putValue(G=1, F=feed_rate,
|
||||
X=x_park, Y=y_park) + " ;Park print head\n"
|
||||
gcode_to_append += self.putValue(M=400) + " ;Wait for moves to finish\n"
|
||||
gcode_to_append += trigger_command + " ;Snap Photo\n"
|
||||
gcode_to_append += self.putValue(G = 4, P = pause_length) + " ;Wait for camera\n"
|
||||
gcode_to_append += ";TimeLapse End\n"
|
||||
for layer in data:
|
||||
gcode_to_append += self.putValue(G=4, P=pause_length) + " ;Wait for camera\n"
|
||||
|
||||
for idx, layer in enumerate(data):
|
||||
for line in layer.split("\n"):
|
||||
if self.getValue(line, "G") in {0, 1}: # Track X,Y location.
|
||||
last_x = self.getValue(line, "X", last_x)
|
||||
last_y = self.getValue(line, "Y", last_y)
|
||||
# Check that a layer is being printed
|
||||
lines = layer.split("\n")
|
||||
for line in lines:
|
||||
if ";LAYER:" in line:
|
||||
index = data.index(layer)
|
||||
layer += gcode_to_append
|
||||
|
||||
data[index] = layer
|
||||
layer += "G0 X%s Y%s\n" % (last_x, last_y)
|
||||
|
||||
data[idx] = layer
|
||||
break
|
||||
return data
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a prepare stage in Cura.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a preview stage in Cura.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"description": "Provides removable drive hotplugging and writing support.",
|
||||
"version": "1.0.1",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,10 @@
|
|||
|
||||
from UM.Logger import LogOutput
|
||||
from typing import Set
|
||||
from sentry_sdk import add_breadcrumb
|
||||
try:
|
||||
from sentry_sdk import add_breadcrumb
|
||||
except ImportError:
|
||||
pass
|
||||
from typing import Optional
|
||||
import os
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import TYPE_CHECKING, Dict, Any
|
||||
try:
|
||||
import sentry_sdk
|
||||
has_sentry = True
|
||||
except ImportError:
|
||||
has_sentry = False
|
||||
|
||||
from . import SentryLogger
|
||||
|
||||
|
|
@ -13,4 +18,6 @@ def getMetaData() -> Dict[str, Any]:
|
|||
|
||||
|
||||
def register(app: "Application") -> Dict[str, Any]:
|
||||
if not has_sentry:
|
||||
return {} # Nothing to do here!
|
||||
return {"logger": SentryLogger.SentryLogger()}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Logs certain events so that they can be used by the crash reporter",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,7 +120,10 @@ class SimulationPass(RenderPass):
|
|||
end = self._layer_view.end_elements_index
|
||||
index = self._layer_view._current_path_num
|
||||
offset = 0
|
||||
for polygon in layer_data.getLayer(self._layer_view._current_layer_num).polygons:
|
||||
layer = layer_data.getLayer(self._layer_view._current_layer_num)
|
||||
if layer is None:
|
||||
continue
|
||||
for polygon in layer.polygons:
|
||||
# The size indicates all values in the two-dimension array, and the second dimension is
|
||||
# always size 3 because we have 3D points.
|
||||
if index >= polygon.data.size // 3 - offset:
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import sys
|
||||
|
|
@ -116,8 +116,9 @@ class SimulationView(CuraView):
|
|||
self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers"))
|
||||
self._compatibility_mode = self._evaluateCompatibilityMode()
|
||||
|
||||
self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"),
|
||||
self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled."),
|
||||
title = catalog.i18nc("@info:title", "Simulation View"))
|
||||
self._slice_first_warning_message = Message(catalog.i18nc("@info:status", "Nothing is shown because you need to slice first."), title = catalog.i18nc("@info:title", "No layers to show"))
|
||||
|
||||
QtApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||
|
||||
|
|
@ -149,6 +150,7 @@ class SimulationView(CuraView):
|
|||
if self._activity == activity:
|
||||
return
|
||||
self._activity = activity
|
||||
self._updateSliceWarningVisibility()
|
||||
self.activityChanged.emit()
|
||||
|
||||
def getSimulationPass(self) -> SimulationPass:
|
||||
|
|
@ -543,11 +545,13 @@ class SimulationView(CuraView):
|
|||
self._composite_pass.getLayerBindings().append("simulationview")
|
||||
self._old_composite_shader = self._composite_pass.getCompositeShader()
|
||||
self._composite_pass.setCompositeShader(self._simulationview_composite_shader)
|
||||
self._updateSliceWarningVisibility()
|
||||
|
||||
elif event.type == Event.ViewDeactivateEvent:
|
||||
self._controller.getScene().getRoot().childrenChanged.disconnect(self._onSceneChanged)
|
||||
Application.getInstance().getPreferences().preferenceChanged.disconnect(self._onPreferencesChanged)
|
||||
self._wireprint_warning_message.hide()
|
||||
self._slice_first_warning_message.hide()
|
||||
Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged)
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
|
||||
|
|
@ -661,6 +665,12 @@ class SimulationView(CuraView):
|
|||
|
||||
self._updateWithPreferences()
|
||||
|
||||
def _updateSliceWarningVisibility(self):
|
||||
if not self.getActivity():
|
||||
self._slice_first_warning_message.show()
|
||||
else:
|
||||
self._slice_first_warning_message.hide()
|
||||
|
||||
|
||||
class _CreateTopLayersJob(Job):
|
||||
def __init__(self, scene: "Scene", layer_number: int, solid_layers: int) -> None:
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ Cura.ExpandableComponent
|
|||
type_id: 1
|
||||
})
|
||||
layerViewTypes.append({
|
||||
text: catalog.i18nc("@label:listbox", "Feedrate"),
|
||||
text: catalog.i18nc("@label:listbox", "Speed"),
|
||||
type_id: 2
|
||||
})
|
||||
layerViewTypes.append({
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class SimulationViewProxy(QObject):
|
||||
def __init__(self, simulation_view: "SimulationView", parent=None):
|
||||
def __init__(self, simulation_view: "SimulationView", parent=None) -> None:
|
||||
super().__init__(parent)
|
||||
self._simulation_view = simulation_view
|
||||
self._current_layer = 0
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ vertex41core =
|
|||
case 1: // "Line type"
|
||||
v_color = a_color;
|
||||
break;
|
||||
case 2: // "Feedrate"
|
||||
case 2: // "Speed", or technically 'Feedrate'
|
||||
v_color = feedrateGradientColor(a_feedrate, u_min_feedrate, u_max_feedrate);
|
||||
break;
|
||||
case 3: // "Layer thickness"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides the Simulation view.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,6 +72,7 @@ Window
|
|||
right: parent.right
|
||||
}
|
||||
text: catalog.i18nc("@text:window", "Ultimaker Cura collects anonymous data in order to improve the print quality and user experience. Below is an example of all the data that is shared:")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Text.WordWrap
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
|
@ -88,7 +89,7 @@ Window
|
|||
right: parent.right
|
||||
}
|
||||
|
||||
textArea.text: manager.getExampleData()
|
||||
textArea.text: (manager === null) ? "" : manager.getExampleData()
|
||||
textArea.textFormat: Text.RichText
|
||||
textArea.wrapMode: Text.Wrap
|
||||
textArea.readOnly: true
|
||||
|
|
|
|||
|
|
@ -5,14 +5,13 @@ import json
|
|||
import os
|
||||
import platform
|
||||
import time
|
||||
from typing import cast, Optional, Set
|
||||
from typing import cast, Optional, Set, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QObject
|
||||
from PyQt5.QtNetwork import QNetworkRequest
|
||||
|
||||
from UM.Extension import Extension
|
||||
from UM.Application import Application
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Message import Message
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
|
|
@ -20,7 +19,8 @@ from UM.Qt.Duration import DurationFormat
|
|||
|
||||
from cura import ApplicationMetadata
|
||||
|
||||
from .SliceInfoJob import SliceInfoJob
|
||||
if TYPE_CHECKING:
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
|
@ -36,7 +36,8 @@ class SliceInfo(QObject, Extension):
|
|||
QObject.__init__(self, parent)
|
||||
Extension.__init__(self)
|
||||
|
||||
self._application = Application.getInstance()
|
||||
from cura.CuraApplication import CuraApplication
|
||||
self._application = CuraApplication.getInstance()
|
||||
|
||||
self._application.getOutputDeviceManager().writeStarted.connect(self._onWriteStarted)
|
||||
self._application.getPreferences().addPreference("info/send_slice_info", True)
|
||||
|
|
@ -56,7 +57,7 @@ class SliceInfo(QObject, Extension):
|
|||
## Perform action based on user input.
|
||||
# Note that clicking "Disable" won't actually disable the data sending, but rather take the user to preferences where they can disable it.
|
||||
def messageActionTriggered(self, message_id, action_id):
|
||||
Application.getInstance().getPreferences().setValue("info/asked_send_slice_info", True)
|
||||
self._application.getPreferences().setValue("info/asked_send_slice_info", True)
|
||||
if action_id == "MoreInfo":
|
||||
self.showMoreInfoDialog()
|
||||
self.send_slice_info_message.hide()
|
||||
|
|
@ -69,7 +70,7 @@ class SliceInfo(QObject, Extension):
|
|||
def _createDialog(self, qml_name):
|
||||
Logger.log("d", "Creating dialog [%s]", qml_name)
|
||||
file_path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), qml_name)
|
||||
dialog = Application.getInstance().createQmlComponent(file_path, {"manager": self})
|
||||
dialog = self._application.createQmlComponent(file_path, {"manager": self})
|
||||
return dialog
|
||||
|
||||
@pyqtSlot(result = str)
|
||||
|
|
@ -87,12 +88,10 @@ class SliceInfo(QObject, Extension):
|
|||
|
||||
@pyqtSlot(bool)
|
||||
def setSendSliceInfo(self, enabled: bool):
|
||||
Application.getInstance().getPreferences().setValue("info/send_slice_info", enabled)
|
||||
self._application.getPreferences().setValue("info/send_slice_info", enabled)
|
||||
|
||||
def _getUserModifiedSettingKeys(self) -> list:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
application = cast(CuraApplication, Application.getInstance())
|
||||
machine_manager = application.getMachineManager()
|
||||
machine_manager = self._application.getMachineManager()
|
||||
global_stack = machine_manager.activeMachine
|
||||
|
||||
user_modified_setting_keys = set() # type: Set[str]
|
||||
|
|
@ -106,30 +105,28 @@ class SliceInfo(QObject, Extension):
|
|||
|
||||
def _onWriteStarted(self, output_device):
|
||||
try:
|
||||
if not Application.getInstance().getPreferences().getValue("info/send_slice_info"):
|
||||
if not self._application.getPreferences().getValue("info/send_slice_info"):
|
||||
Logger.log("d", "'info/send_slice_info' is turned off.")
|
||||
return # Do nothing, user does not want to send data
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
application = cast(CuraApplication, Application.getInstance())
|
||||
machine_manager = application.getMachineManager()
|
||||
print_information = application.getPrintInformation()
|
||||
machine_manager = self._application.getMachineManager()
|
||||
print_information = self._application.getPrintInformation()
|
||||
|
||||
global_stack = machine_manager.activeMachine
|
||||
|
||||
data = dict() # The data that we're going to submit.
|
||||
data["time_stamp"] = time.time()
|
||||
data["schema_version"] = 0
|
||||
data["cura_version"] = application.getVersion()
|
||||
data["cura_version"] = self._application.getVersion()
|
||||
data["cura_build_type"] = ApplicationMetadata.CuraBuildType
|
||||
|
||||
active_mode = Application.getInstance().getPreferences().getValue("cura/active_mode")
|
||||
active_mode = self._application.getPreferences().getValue("cura/active_mode")
|
||||
if active_mode == 0:
|
||||
data["active_mode"] = "recommended"
|
||||
else:
|
||||
data["active_mode"] = "custom"
|
||||
|
||||
data["camera_view"] = application.getPreferences().getValue("general/camera_perspective_mode")
|
||||
data["camera_view"] = self._application.getPreferences().getValue("general/camera_perspective_mode")
|
||||
if data["camera_view"] == "orthographic":
|
||||
data["camera_view"] = "orthogonal" #The database still only recognises the old name "orthogonal".
|
||||
|
||||
|
|
@ -142,7 +139,7 @@ class SliceInfo(QObject, Extension):
|
|||
machine_settings_changed_by_user = True
|
||||
|
||||
data["machine_settings_changed_by_user"] = machine_settings_changed_by_user
|
||||
data["language"] = Application.getInstance().getPreferences().getValue("general/language")
|
||||
data["language"] = self._application.getPreferences().getValue("general/language")
|
||||
data["os"] = {"type": platform.system(), "version": platform.version()}
|
||||
|
||||
data["active_machine"] = {"definition_id": global_stack.definition.getId(),
|
||||
|
|
@ -184,7 +181,7 @@ class SliceInfo(QObject, Extension):
|
|||
|
||||
data["models"] = []
|
||||
# Listing all files placed on the build plate
|
||||
for node in DepthFirstIterator(application.getController().getScene().getRoot()):
|
||||
for node in DepthFirstIterator(self._application.getController().getScene().getRoot()):
|
||||
if node.callDecoration("isSliceable"):
|
||||
model = dict()
|
||||
model["hash"] = node.getMeshData().getHash()
|
||||
|
|
@ -263,10 +260,23 @@ class SliceInfo(QObject, Extension):
|
|||
# Convert data to bytes
|
||||
binary_data = json.dumps(data).encode("utf-8")
|
||||
|
||||
# Sending slice info non-blocking
|
||||
reportJob = SliceInfoJob(self.info_url, binary_data)
|
||||
reportJob.start()
|
||||
# Send slice info non-blocking
|
||||
network_manager = self._application.getHttpRequestManager()
|
||||
network_manager.post(self.info_url, data = binary_data,
|
||||
callback = self._onRequestFinished, error_callback = self._onRequestError)
|
||||
except Exception:
|
||||
# We really can't afford to have a mistake here, as this would break the sending of g-code to a device
|
||||
# (Either saving or directly to a printer). The functionality of the slice data is not *that* important.
|
||||
Logger.logException("e", "Exception raised while sending slice info.") # But we should be notified about these problems of course.
|
||||
|
||||
def _onRequestFinished(self, reply: "QNetworkReply") -> None:
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if status_code == 200:
|
||||
Logger.log("i", "SliceInfo sent successfully")
|
||||
return
|
||||
|
||||
data = reply.readAll().data().decode("utf-8")
|
||||
Logger.log("e", "SliceInfo request failed, status code %s, data: %s", status_code, data)
|
||||
|
||||
def _onRequestError(self, reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None:
|
||||
Logger.log("e", "Got error for SliceInfo request: %s", reply.errorString())
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
from UM.Platform import Platform
|
||||
|
||||
import ssl
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
import certifi
|
||||
|
||||
|
||||
class SliceInfoJob(Job):
|
||||
def __init__(self, url, data):
|
||||
super().__init__()
|
||||
self._url = url
|
||||
self._data = data
|
||||
|
||||
def run(self):
|
||||
if not self._url or not self._data:
|
||||
Logger.log("e", "URL or DATA for sending slice info was not set!")
|
||||
return
|
||||
|
||||
# CURA-6698 Create an SSL context and use certifi CA certificates for verification.
|
||||
context = ssl.SSLContext(protocol = ssl.PROTOCOL_TLSv1_2)
|
||||
context.load_verify_locations(cafile = certifi.where())
|
||||
|
||||
# Submit data
|
||||
kwoptions = {"data": self._data,
|
||||
"timeout": 5,
|
||||
"context": context}
|
||||
|
||||
Logger.log("i", "Sending anonymous slice info to [%s]...", self._url)
|
||||
|
||||
try:
|
||||
f = urllib.request.urlopen(self._url, **kwoptions)
|
||||
Logger.log("i", "Sent anonymous slice info.")
|
||||
f.close()
|
||||
except urllib.error.HTTPError:
|
||||
Logger.logException("e", "An HTTP error occurred while trying to send slice information")
|
||||
except Exception: # We don't want any exception to cause problems
|
||||
Logger.logException("e", "An exception occurred while trying to send slice information")
|
||||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Submits anonymous slice info. Can be disabled through preferences.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a normal solid mesh view.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Creates an eraser mesh to block the printing of support in certain places",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
# Toolbox is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from .src import Toolbox
|
||||
from .src.CloudSync.SyncOrchestrator import SyncOrchestrator
|
||||
|
||||
|
||||
def getMetaData():
|
||||
|
|
@ -9,4 +10,6 @@ def getMetaData():
|
|||
|
||||
|
||||
def register(app):
|
||||
return {"extension": Toolbox.Toolbox(app)}
|
||||
return {
|
||||
"extension": [Toolbox.Toolbox(app), SyncOrchestrator(app)]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@
|
|||
"name": "Toolbox",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"description": "Find, manage and install new Cura packages."
|
||||
}
|
||||
|
|
|
|||
3
plugins/Toolbox/resources/images/shop.svg
Normal file
3
plugins/Toolbox/resources/images/shop.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24">
|
||||
<path d="M19,3H5A2.9,2.9,0,0,0,2,6V9a3.9,3.9,0,0,0,2,3.4V22H20V12.4A3.9,3.9,0,0,0,22,9V6A2.9,2.9,0,0,0,19,3ZM10,5h4V9a2,2,0,0,1-4,0ZM4,9V5H8V9A2,2,0,0,1,4,9ZM18,20H14V15H10v5H6V13a3.7,3.7,0,0,0,3-1.4A3.7,3.7,0,0,0,12,13a3.7,3.7,0,0,0,3-1.4A3.7,3.7,0,0,0,18,13ZM20,9a2,2,0,0,1-4,0V5h4Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 364 B |
|
|
@ -96,17 +96,12 @@ Window
|
|||
visible: toolbox.restartRequired
|
||||
height: visible ? UM.Theme.getSize("toolbox_footer").height : 0
|
||||
}
|
||||
// TODO: Clean this up:
|
||||
|
||||
Connections
|
||||
{
|
||||
target: toolbox
|
||||
onShowLicenseDialog:
|
||||
{
|
||||
licenseDialog.pluginName = toolbox.getLicenseDialogPluginName();
|
||||
licenseDialog.licenseContent = toolbox.getLicenseDialogLicenseContent();
|
||||
licenseDialog.pluginFileLocation = toolbox.getLicenseDialogPluginFileLocation();
|
||||
licenseDialog.show();
|
||||
}
|
||||
onShowLicenseDialog: { licenseDialog.show() }
|
||||
onCloseLicenseDialog: { licenseDialog.close() }
|
||||
}
|
||||
|
||||
ToolboxLicenseDialog
|
||||
|
|
|
|||
|
|
@ -14,17 +14,44 @@ Rectangle
|
|||
Column
|
||||
{
|
||||
height: childrenRect.height + 2 * padding
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
width: parent.width
|
||||
padding: UM.Theme.getSize("wide_margin").height
|
||||
Label
|
||||
Item
|
||||
{
|
||||
id: heading
|
||||
text: catalog.i18nc("@label", "Featured")
|
||||
width: parent.width
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
font: UM.Theme.getFont("large")
|
||||
renderType: Text.NativeRendering
|
||||
width: parent.width - parent.padding * 2
|
||||
height: childrenRect.height
|
||||
Label
|
||||
{
|
||||
id: heading
|
||||
text: catalog.i18nc("@label", "Featured")
|
||||
width: contentWidth
|
||||
height: contentHeight
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
font: UM.Theme.getFont("large")
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
UM.TooltipArea
|
||||
{
|
||||
width: childrenRect.width
|
||||
height: childrenRect.height
|
||||
anchors.right: parent.right
|
||||
text: catalog.i18nc("@info:tooltip", "Go to Web Marketplace")
|
||||
Label
|
||||
{
|
||||
text: "<a href='%2'>".arg(toolbox.getWebMarketplaceUrl("materials")) + catalog.i18nc("@label", "Search materials") + "</a>"
|
||||
width: contentWidth
|
||||
height: contentHeight
|
||||
horizontalAlignment: Text.AlignRight
|
||||
font: UM.Theme.getFont("default")
|
||||
renderType: Text.NativeRendering
|
||||
|
||||
linkColor: UM.Theme.getColor("text_link")
|
||||
onLinkActivated: Qt.openUrlExternally(link)
|
||||
|
||||
visible: toolbox.viewCategory === "material"
|
||||
}
|
||||
}
|
||||
}
|
||||
Grid
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Copyright (c) 2020 Ultimaker B.V.
|
||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.10
|
||||
|
|
@ -51,32 +51,25 @@ Item
|
|||
toolbox.viewPage = "overview"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ToolboxTabButton
|
||||
{
|
||||
id: installedTabButton
|
||||
text: catalog.i18nc("@title:tab", "Installed")
|
||||
active: toolbox.viewCategory == "installed"
|
||||
enabled: !toolbox.isDownloading
|
||||
anchors
|
||||
ToolboxTabButton
|
||||
{
|
||||
right: parent.right
|
||||
rightMargin: UM.Theme.getSize("default_margin").width
|
||||
id: installedTabButton
|
||||
text: catalog.i18nc("@title:tab", "Installed")
|
||||
active: toolbox.viewCategory == "installed"
|
||||
enabled: !toolbox.isDownloading
|
||||
onClicked: toolbox.viewCategory = "installed"
|
||||
width: UM.Theme.getSize("toolbox_header_tab").width + marketplaceNotificationIcon.width - UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
onClicked: toolbox.viewCategory = "installed"
|
||||
width: UM.Theme.getSize("toolbox_header_tab").width + marketplaceNotificationIcon.width - UM.Theme.getSize("default_margin").width
|
||||
|
||||
|
||||
}
|
||||
|
||||
Cura.NotificationIcon
|
||||
{
|
||||
id: marketplaceNotificationIcon
|
||||
|
||||
visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0
|
||||
|
||||
anchors.right: installedTabButton.right
|
||||
anchors.verticalCenter: installedTabButton.verticalCenter
|
||||
|
||||
anchors.right: bar.right
|
||||
labelText:
|
||||
{
|
||||
const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length
|
||||
|
|
@ -84,6 +77,33 @@ Item
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
UM.TooltipArea
|
||||
{
|
||||
id: webMarketplaceButtonTooltipArea
|
||||
width: childrenRect.width
|
||||
height: parent.height
|
||||
text: catalog.i18nc("@info:tooltip", "Go to Web Marketplace")
|
||||
anchors
|
||||
{
|
||||
right: parent.right
|
||||
rightMargin: UM.Theme.getSize("default_margin").width
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
onClicked: Qt.openUrlExternally(toolbox.getWebMarketplaceUrl("plugins"))
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: cloudMarketplaceButton
|
||||
source: "../../images/shop.svg"
|
||||
color: UM.Theme.getColor(webMarketplaceButtonTooltipArea.containsMouse ? "primary" : "text")
|
||||
height: parent.height / 2
|
||||
width: height
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
}
|
||||
}
|
||||
|
||||
ToolboxShadow
|
||||
{
|
||||
anchors.top: bar.bottom
|
||||
|
|
|
|||
|
|
@ -20,6 +20,8 @@ UM.Dialog{
|
|||
maximumHeight: minimumHeight
|
||||
margin: 0
|
||||
|
||||
property string actionButtonText: subscribedPackagesModel.hasIncompatiblePackages && !subscribedPackagesModel.hasCompatiblePackages ? catalog.i18nc("@button", "Dismiss") : catalog.i18nc("@button", "Next")
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: root
|
||||
|
|
@ -48,19 +50,20 @@ UM.Dialog{
|
|||
{
|
||||
font: UM.Theme.getFont("default")
|
||||
text: catalog.i18nc("@label", "The following packages will be added:")
|
||||
visible: subscribedPackagesModel.hasCompatiblePackages
|
||||
color: UM.Theme.getColor("text")
|
||||
height: contentHeight + UM.Theme.getSize("default_margin").height
|
||||
}
|
||||
Repeater
|
||||
{
|
||||
model: toolbox.subscribedPackagesModel
|
||||
model: subscribedPackagesModel
|
||||
Component
|
||||
{
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
property var lineHeight: 60
|
||||
visible: model.is_compatible == "True" ? true : false
|
||||
property int lineHeight: 60
|
||||
visible: model.is_compatible
|
||||
height: visible ? (lineHeight + UM.Theme.getSize("default_margin").height) : 0 // We only show the compatible packages here
|
||||
Image
|
||||
{
|
||||
|
|
@ -73,7 +76,7 @@ UM.Dialog{
|
|||
}
|
||||
Label
|
||||
{
|
||||
text: model.name
|
||||
text: model.display_name
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
anchors.left: packageIcon.right
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||
|
|
@ -89,20 +92,21 @@ UM.Dialog{
|
|||
Label
|
||||
{
|
||||
font: UM.Theme.getFont("default")
|
||||
text: catalog.i18nc("@label", "The following packages can not be installed because of incompatible Cura version:")
|
||||
text: catalog.i18nc("@label", "The following packages can not be installed because of an incompatible Cura version:")
|
||||
visible: subscribedPackagesModel.hasIncompatiblePackages
|
||||
color: UM.Theme.getColor("text")
|
||||
height: contentHeight + UM.Theme.getSize("default_margin").height
|
||||
}
|
||||
Repeater
|
||||
{
|
||||
model: toolbox.subscribedPackagesModel
|
||||
model: subscribedPackagesModel
|
||||
Component
|
||||
{
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
property var lineHeight: 60
|
||||
visible: model.is_compatible == "True" ? false : true
|
||||
property int lineHeight: 60
|
||||
visible: !model.is_compatible && !model.is_dismissed
|
||||
height: visible ? (lineHeight + UM.Theme.getSize("default_margin").height) : 0 // We only show the incompatible packages here
|
||||
Image
|
||||
{
|
||||
|
|
@ -115,7 +119,7 @@ UM.Dialog{
|
|||
}
|
||||
Label
|
||||
{
|
||||
text: model.name
|
||||
text: model.display_name
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
anchors.left: packageIcon.right
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||
|
|
@ -130,13 +134,16 @@ UM.Dialog{
|
|||
|
||||
} // End of ScrollView
|
||||
|
||||
Cura.ActionButton
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: nextButton
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.right: parent.right
|
||||
anchors.margins: UM.Theme.getSize("default_margin").height
|
||||
text: catalog.i18nc("@button", "Next")
|
||||
text: actionButtonText
|
||||
onClicked: accept()
|
||||
leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width
|
||||
rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,70 +4,106 @@
|
|||
import QtQuick 2.10
|
||||
import QtQuick.Dialogs 1.1
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 1.4
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Controls.Styles 1.4
|
||||
|
||||
// TODO: Switch to QtQuick.Controls 2.x and remove QtQuick.Controls.Styles
|
||||
|
||||
import UM 1.1 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
UM.Dialog
|
||||
{
|
||||
title: catalog.i18nc("@title:window", "Plugin License Agreement")
|
||||
id: licenseDialog
|
||||
title: licenseModel.dialogTitle
|
||||
minimumWidth: UM.Theme.getSize("license_window_minimum").width
|
||||
minimumHeight: UM.Theme.getSize("license_window_minimum").height
|
||||
width: minimumWidth
|
||||
height: minimumHeight
|
||||
property var pluginName;
|
||||
property var licenseContent;
|
||||
property var pluginFileLocation;
|
||||
Item
|
||||
backgroundColor: UM.Theme.getColor("main_background")
|
||||
margin: screenScaleFactor * 10
|
||||
|
||||
ColumnLayout
|
||||
{
|
||||
anchors.fill: parent
|
||||
spacing: UM.Theme.getSize("thick_margin").height
|
||||
|
||||
UM.I18nCatalog{id: catalog; name: "cura"}
|
||||
|
||||
Label
|
||||
{
|
||||
id: licenseTitle
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
text: licenseDialog.pluginName + ": " + catalog.i18nc("@label", "This plugin contains a license.\nYou need to accept this license to install this plugin.\nDo you agree with the terms below?")
|
||||
id: licenseHeader
|
||||
Layout.fillWidth: true
|
||||
text: catalog.i18nc("@label", "You need to accept the license to install the package")
|
||||
color: UM.Theme.getColor("text")
|
||||
wrapMode: Text.Wrap
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
TextArea
|
||||
{
|
||||
id: licenseText
|
||||
anchors.top: licenseTitle.bottom
|
||||
anchors.bottom: parent.bottom
|
||||
|
||||
Row {
|
||||
id: packageRow
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
readOnly: true
|
||||
text: licenseDialog.licenseContent || ""
|
||||
height: childrenRect.height
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
leftPadding: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
Image
|
||||
{
|
||||
id: icon
|
||||
width: 30 * screenScaleFactor
|
||||
height: width
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: licenseModel.iconUrl || "../../images/logobot.svg"
|
||||
mipmap: true
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: packageName
|
||||
text: licenseModel.packageName
|
||||
color: UM.Theme.getColor("text")
|
||||
font.bold: true
|
||||
anchors.verticalCenter: icon.verticalCenter
|
||||
height: contentHeight
|
||||
wrapMode: Text.Wrap
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
Cura.ScrollableTextArea
|
||||
{
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
|
||||
textArea.text: licenseModel.licenseText
|
||||
textArea.readOnly: true
|
||||
}
|
||||
|
||||
}
|
||||
rightButtons:
|
||||
[
|
||||
Button
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
id: acceptButton
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
text: catalog.i18nc("@action:button", "Accept")
|
||||
onClicked:
|
||||
{
|
||||
licenseDialog.close();
|
||||
toolbox.install(licenseDialog.pluginFileLocation);
|
||||
}
|
||||
},
|
||||
Button
|
||||
leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width
|
||||
rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width
|
||||
|
||||
text: licenseModel.acceptButtonText
|
||||
onClicked: { handler.onLicenseAccepted() }
|
||||
}
|
||||
]
|
||||
|
||||
leftButtons:
|
||||
[
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: declineButton
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
text: catalog.i18nc("@action:button", "Decline")
|
||||
onClicked:
|
||||
{
|
||||
licenseDialog.close();
|
||||
}
|
||||
text: licenseModel.declineButtonText
|
||||
onClicked: { handler.onLicenseDeclined() }
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import re
|
||||
from typing import Dict, List, Optional, Union
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtProperty, pyqtSignal
|
||||
from PyQt5.QtCore import Qt, pyqtProperty
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
|
|
|
|||
28
plugins/Toolbox/src/CloudApiModel.py
Normal file
28
plugins/Toolbox/src/CloudApiModel.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from typing import Union
|
||||
|
||||
from cura import ApplicationMetadata, UltimakerCloudAuthentication
|
||||
|
||||
|
||||
class CloudApiModel:
|
||||
sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
|
||||
cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion # type: str
|
||||
cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot # type: str
|
||||
api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
|
||||
cloud_api_root = cloud_api_root,
|
||||
cloud_api_version = cloud_api_version,
|
||||
sdk_version = sdk_version
|
||||
) # type: str
|
||||
|
||||
# https://api.ultimaker.com/cura-packages/v1/user/packages
|
||||
api_url_user_packages = "{cloud_api_root}/cura-packages/v{cloud_api_version}/user/packages".format(
|
||||
cloud_api_root=cloud_api_root,
|
||||
cloud_api_version=cloud_api_version,
|
||||
)
|
||||
|
||||
## https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id}
|
||||
@classmethod
|
||||
def userPackageUrl(cls, package_id: str) -> str:
|
||||
|
||||
return (CloudApiModel.api_url_user_packages + "/{package_id}").format(
|
||||
package_id=package_id
|
||||
)
|
||||
105
plugins/Toolbox/src/CloudSync/CloudPackageChecker.py
Normal file
105
plugins/Toolbox/src/CloudSync/CloudPackageChecker.py
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
import json
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import QObject
|
||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
||||
|
||||
from UM import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal
|
||||
from cura.CuraApplication import CuraApplication, ApplicationMetadata
|
||||
from ..CloudApiModel import CloudApiModel
|
||||
from .SubscribedPackagesModel import SubscribedPackagesModel
|
||||
from ..UltimakerCloudScope import UltimakerCloudScope
|
||||
|
||||
from typing import List, Dict, Any
|
||||
|
||||
class CloudPackageChecker(QObject):
|
||||
def __init__(self, application: CuraApplication) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.discrepancies = Signal() # Emits SubscribedPackagesModel
|
||||
self._application = application # type: CuraApplication
|
||||
self._scope = UltimakerCloudScope(application)
|
||||
self._model = SubscribedPackagesModel()
|
||||
|
||||
self._application.initializationFinished.connect(self._onAppInitialized)
|
||||
self._i18n_catalog = i18nCatalog("cura")
|
||||
self._sdk_version = ApplicationMetadata.CuraSDKVersion
|
||||
|
||||
# This is a plugin, so most of the components required are not ready when
|
||||
# this is initialized. Therefore, we wait until the application is ready.
|
||||
def _onAppInitialized(self) -> None:
|
||||
self._package_manager = self._application.getPackageManager()
|
||||
# initial check
|
||||
self._fetchUserSubscribedPackages()
|
||||
# check again whenever the login state changes
|
||||
self._application.getCuraAPI().account.loginStateChanged.connect(self._fetchUserSubscribedPackages)
|
||||
|
||||
def _fetchUserSubscribedPackages(self) -> None:
|
||||
if self._application.getCuraAPI().account.isLoggedIn:
|
||||
self._getUserSubscribedPackages()
|
||||
|
||||
def _getUserSubscribedPackages(self) -> None:
|
||||
Logger.debug("Requesting subscribed packages metadata from server.")
|
||||
url = CloudApiModel.api_url_user_packages
|
||||
self._application.getHttpRequestManager().get(url,
|
||||
callback = self._onUserPackagesRequestFinished,
|
||||
error_callback = self._onUserPackagesRequestFinished,
|
||||
scope = self._scope)
|
||||
|
||||
def _onUserPackagesRequestFinished(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None:
|
||||
if error is not None or reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
|
||||
Logger.log("w",
|
||||
"Requesting user packages failed, response code %s while trying to connect to %s",
|
||||
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
|
||||
return
|
||||
|
||||
try:
|
||||
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
# Check for errors:
|
||||
if "errors" in json_data:
|
||||
for error in json_data["errors"]:
|
||||
Logger.log("e", "%s", error["title"])
|
||||
return
|
||||
self._handleCompatibilityData(json_data["data"])
|
||||
except json.decoder.JSONDecodeError:
|
||||
Logger.log("w", "Received invalid JSON for user subscribed packages from the Web Marketplace")
|
||||
|
||||
def _handleCompatibilityData(self, subscribed_packages_payload: List[Dict[str, Any]]) -> None:
|
||||
user_subscribed_packages = [plugin["package_id"] for plugin in subscribed_packages_payload]
|
||||
user_installed_packages = self._package_manager.getUserInstalledPackages()
|
||||
|
||||
# We need to re-evaluate the dismissed packages
|
||||
# (i.e. some package might got updated to the correct SDK version in the meantime,
|
||||
# hence remove them from the Dismissed Incompatible list)
|
||||
self._package_manager.reEvaluateDismissedPackages(subscribed_packages_payload, self._sdk_version)
|
||||
user_dismissed_packages = self._package_manager.getDismissedPackages()
|
||||
if user_dismissed_packages:
|
||||
user_installed_packages += user_dismissed_packages
|
||||
|
||||
# We check if there are packages installed in Web Marketplace but not in Cura marketplace
|
||||
package_discrepancy = list(set(user_subscribed_packages).difference(user_installed_packages))
|
||||
if package_discrepancy:
|
||||
self._model.addDiscrepancies(package_discrepancy)
|
||||
self._model.initialize(subscribed_packages_payload)
|
||||
self._handlePackageDiscrepancies()
|
||||
|
||||
def _handlePackageDiscrepancies(self) -> None:
|
||||
Logger.log("d", "Discrepancy found between Cloud subscribed packages and Cura installed packages")
|
||||
sync_message = Message(self._i18n_catalog.i18nc(
|
||||
"@info:generic",
|
||||
"\nDo you want to sync material and software packages with your account?"),
|
||||
title=self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
|
||||
sync_message.addAction("sync",
|
||||
name=self._i18n_catalog.i18nc("@action:button", "Sync"),
|
||||
icon="",
|
||||
description="Sync your Cloud subscribed packages to your local environment.",
|
||||
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)
|
||||
sync_message.actionTriggered.connect(self._onSyncButtonClicked)
|
||||
sync_message.show()
|
||||
|
||||
def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None:
|
||||
sync_message.hide()
|
||||
self.discrepancies.emit(self._model)
|
||||
51
plugins/Toolbox/src/CloudSync/CloudPackageManager.py
Normal file
51
plugins/Toolbox/src/CloudSync/CloudPackageManager.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
from UM.Logger import Logger
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from ..CloudApiModel import CloudApiModel
|
||||
from ..UltimakerCloudScope import UltimakerCloudScope
|
||||
|
||||
|
||||
class CloudPackageManager:
|
||||
"""Manages Cloud subscriptions
|
||||
|
||||
When a package is added to a user's account, the user is 'subscribed' to that package.
|
||||
Whenever the user logs in on another instance of Cura, these subscriptions can be used to sync the user's plugins
|
||||
|
||||
Singleton: use CloudPackageManager.getInstance() instead of CloudPackageManager()
|
||||
"""
|
||||
|
||||
__instance = None
|
||||
|
||||
@classmethod
|
||||
def getInstance(cls, app: CuraApplication):
|
||||
if not cls.__instance:
|
||||
cls.__instance = CloudPackageManager(app)
|
||||
return cls.__instance
|
||||
|
||||
def __init__(self, app: CuraApplication) -> None:
|
||||
if self.__instance is not None:
|
||||
raise RuntimeError("This is a Singleton. use getInstance()")
|
||||
|
||||
self._scope = UltimakerCloudScope(app) # type: UltimakerCloudScope
|
||||
|
||||
app.getPackageManager().packageInstalled.connect(self._onPackageInstalled)
|
||||
|
||||
def unsubscribe(self, package_id: str) -> None:
|
||||
url = CloudApiModel.userPackageUrl(package_id)
|
||||
HttpRequestManager.getInstance().delete(url = url, scope = self._scope)
|
||||
|
||||
def _subscribe(self, package_id: str) -> None:
|
||||
"""You probably don't want to use this directly. All installed packages will be automatically subscribed."""
|
||||
|
||||
Logger.debug("Subscribing to {}", package_id)
|
||||
data = "{\"data\": {\"package_id\": \"%s\", \"sdk_version\": \"%s\"}}" % (package_id, CloudApiModel.sdk_version)
|
||||
HttpRequestManager.getInstance().put(
|
||||
url = CloudApiModel.api_url_user_packages,
|
||||
data = data.encode(),
|
||||
scope = self._scope
|
||||
)
|
||||
|
||||
def _onPackageInstalled(self, package_id: str):
|
||||
if CuraApplication.getInstance().getCuraAPI().account.isLoggedIn:
|
||||
# We might already be subscribed, but checking would take one extra request. Instead, simply subscribe
|
||||
self._subscribe(package_id)
|
||||
39
plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py
Normal file
39
plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import os
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from UM.Signal import Signal
|
||||
from .SubscribedPackagesModel import SubscribedPackagesModel
|
||||
|
||||
|
||||
## Shows a list of packages to be added or removed. The user can select which packages to (un)install. The user's
|
||||
# choices are emitted on the `packageMutations` Signal.
|
||||
class DiscrepanciesPresenter(QObject):
|
||||
|
||||
def __init__(self, app: QtApplication) -> None:
|
||||
super().__init__(app)
|
||||
|
||||
self.packageMutations = Signal() # Emits SubscribedPackagesModel
|
||||
|
||||
self._app = app
|
||||
self._package_manager = app.getPackageManager()
|
||||
self._dialog = None # type: Optional[QObject]
|
||||
self._compatibility_dialog_path = "resources/qml/dialogs/CompatibilityDialog.qml"
|
||||
|
||||
def present(self, plugin_path: str, model: SubscribedPackagesModel) -> None:
|
||||
path = os.path.join(plugin_path, self._compatibility_dialog_path)
|
||||
self._dialog = self._app.createQmlComponent(path, {"subscribedPackagesModel": model, "handler": self})
|
||||
assert self._dialog
|
||||
self._dialog.accepted.connect(lambda: self._onConfirmClicked(model))
|
||||
|
||||
def _onConfirmClicked(self, model: SubscribedPackagesModel) -> None:
|
||||
# If there are incompatible packages - automatically dismiss them
|
||||
if model.getIncompatiblePackages():
|
||||
self._package_manager.dismissAllIncompatiblePackages(model.getIncompatiblePackages())
|
||||
# For now, all compatible packages presented to the user should be installed.
|
||||
# Later, we might remove items for which the user unselected the package
|
||||
if model.getCompatiblePackages():
|
||||
model.setItems(model.getCompatiblePackages())
|
||||
self.packageMutations.emit(model)
|
||||
144
plugins/Toolbox/src/CloudSync/DownloadPresenter.py
Normal file
144
plugins/Toolbox/src/CloudSync/DownloadPresenter.py
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
import tempfile
|
||||
from typing import Dict, List, Any
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
|
||||
from UM import i18n_catalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from .SubscribedPackagesModel import SubscribedPackagesModel
|
||||
from ..UltimakerCloudScope import UltimakerCloudScope
|
||||
|
||||
|
||||
## Downloads a set of packages from the Ultimaker Cloud Marketplace
|
||||
# use download() exactly once: should not be used for multiple sets of downloads since this class contains state
|
||||
class DownloadPresenter:
|
||||
|
||||
DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB
|
||||
|
||||
def __init__(self, app: CuraApplication) -> None:
|
||||
# Emits (Dict[str, str], List[str]) # (success_items, error_items)
|
||||
# Dict{success_package_id, temp_file_path}
|
||||
# List[errored_package_id]
|
||||
self.done = Signal()
|
||||
|
||||
self._app = app
|
||||
self._scope = UltimakerCloudScope(app)
|
||||
|
||||
self._started = False
|
||||
self._progress_message = self._createProgressMessage()
|
||||
self._progress = {} # type: Dict[str, Dict[str, Any]] # package_id, Dict
|
||||
self._error = [] # type: List[str] # package_id
|
||||
|
||||
def download(self, model: SubscribedPackagesModel) -> None:
|
||||
if self._started:
|
||||
Logger.error("Download already started. Create a new %s instead", self.__class__.__name__)
|
||||
return
|
||||
|
||||
manager = HttpRequestManager.getInstance()
|
||||
for item in model.items:
|
||||
package_id = item["package_id"]
|
||||
|
||||
def finishedCallback(reply: QNetworkReply, pid = package_id) -> None:
|
||||
self._onFinished(pid, reply)
|
||||
|
||||
def progressCallback(rx: int, rt: int, pid = package_id) -> None:
|
||||
self._onProgress(pid, rx, rt)
|
||||
|
||||
def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, pid = package_id) -> None:
|
||||
self._onError(pid)
|
||||
|
||||
request_data = manager.get(
|
||||
item["download_url"],
|
||||
callback = finishedCallback,
|
||||
download_progress_callback = progressCallback,
|
||||
error_callback = errorCallback,
|
||||
scope = self._scope)
|
||||
|
||||
self._progress[package_id] = {
|
||||
"received": 0,
|
||||
"total": 1, # make sure this is not considered done yet. Also divByZero-safe
|
||||
"file_written": None,
|
||||
"request_data": request_data,
|
||||
"package_model": item
|
||||
}
|
||||
|
||||
self._started = True
|
||||
self._progress_message.show()
|
||||
|
||||
def abort(self) -> None:
|
||||
manager = HttpRequestManager.getInstance()
|
||||
for item in self._progress.values():
|
||||
manager.abortRequest(item["request_data"])
|
||||
|
||||
# Aborts all current operations and returns a copy with the same settings such as app and scope
|
||||
def resetCopy(self) -> "DownloadPresenter":
|
||||
self.abort()
|
||||
self.done.disconnectAll()
|
||||
return DownloadPresenter(self._app)
|
||||
|
||||
def _createProgressMessage(self) -> Message:
|
||||
return Message(i18n_catalog.i18nc(
|
||||
"@info:generic",
|
||||
"\nSyncing..."),
|
||||
lifetime = 0,
|
||||
use_inactivity_timer=False,
|
||||
progress = 0.0,
|
||||
title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
|
||||
|
||||
def _onFinished(self, package_id: str, reply: QNetworkReply) -> None:
|
||||
self._progress[package_id]["received"] = self._progress[package_id]["total"]
|
||||
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode ="wb+", suffix =".curapackage", delete = False) as temp_file:
|
||||
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
|
||||
while bytes_read:
|
||||
temp_file.write(bytes_read)
|
||||
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
|
||||
self._app.processEvents()
|
||||
self._progress[package_id]["file_written"] = temp_file.name
|
||||
except IOError as e:
|
||||
Logger.logException("e", "Failed to write downloaded package to temp file", e)
|
||||
self._onError(package_id)
|
||||
temp_file.close()
|
||||
|
||||
self._checkDone()
|
||||
|
||||
def _onProgress(self, package_id: str, rx: int, rt: int) -> None:
|
||||
self._progress[package_id]["received"] = rx
|
||||
self._progress[package_id]["total"] = rt
|
||||
|
||||
received = 0
|
||||
total = 0
|
||||
for item in self._progress.values():
|
||||
received += item["received"]
|
||||
total += item["total"]
|
||||
|
||||
self._progress_message.setProgress(100.0 * (received / total)) # [0 .. 100] %
|
||||
|
||||
def _onError(self, package_id: str) -> None:
|
||||
self._progress.pop(package_id)
|
||||
self._error.append(package_id)
|
||||
self._checkDone()
|
||||
|
||||
def _checkDone(self) -> bool:
|
||||
for item in self._progress.values():
|
||||
if not item["file_written"]:
|
||||
return False
|
||||
|
||||
success_items = {
|
||||
package_id:
|
||||
{
|
||||
"package_path": value["file_written"],
|
||||
"icon_url": value["package_model"]["icon_url"]
|
||||
}
|
||||
for package_id, value in self._progress.items()
|
||||
}
|
||||
error_items = [package_id for package_id in self._error]
|
||||
|
||||
self._progress_message.hide()
|
||||
self.done.emit(success_items, error_items)
|
||||
return True
|
||||
77
plugins/Toolbox/src/CloudSync/LicenseModel.py
Normal file
77
plugins/Toolbox/src/CloudSync/LicenseModel.py
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
# Model for the ToolboxLicenseDialog
|
||||
class LicenseModel(QObject):
|
||||
DEFAULT_DECLINE_BUTTON_TEXT = catalog.i18nc("@button", "Decline")
|
||||
ACCEPT_BUTTON_TEXT = catalog.i18nc("@button", "Agree")
|
||||
|
||||
dialogTitleChanged = pyqtSignal()
|
||||
packageNameChanged = pyqtSignal()
|
||||
licenseTextChanged = pyqtSignal()
|
||||
iconChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, decline_button_text: str = DEFAULT_DECLINE_BUTTON_TEXT) -> None:
|
||||
super().__init__()
|
||||
|
||||
self._current_page_idx = 0
|
||||
self._page_count = 1
|
||||
self._dialogTitle = ""
|
||||
self._license_text = ""
|
||||
self._package_name = ""
|
||||
self._icon_url = ""
|
||||
self._decline_button_text = decline_button_text
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def acceptButtonText(self):
|
||||
return self.ACCEPT_BUTTON_TEXT
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def declineButtonText(self):
|
||||
return self._decline_button_text
|
||||
|
||||
@pyqtProperty(str, notify=dialogTitleChanged)
|
||||
def dialogTitle(self) -> str:
|
||||
return self._dialogTitle
|
||||
|
||||
@pyqtProperty(str, notify=packageNameChanged)
|
||||
def packageName(self) -> str:
|
||||
return self._package_name
|
||||
|
||||
def setPackageName(self, name: str) -> None:
|
||||
self._package_name = name
|
||||
self.packageNameChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=iconChanged)
|
||||
def iconUrl(self) -> str:
|
||||
return self._icon_url
|
||||
|
||||
def setIconUrl(self, url: str):
|
||||
self._icon_url = url
|
||||
self.iconChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=licenseTextChanged)
|
||||
def licenseText(self) -> str:
|
||||
return self._license_text
|
||||
|
||||
def setLicenseText(self, license_text: str) -> None:
|
||||
if self._license_text != license_text:
|
||||
self._license_text = license_text
|
||||
self.licenseTextChanged.emit()
|
||||
|
||||
def setCurrentPageIdx(self, idx: int) -> None:
|
||||
self._current_page_idx = idx
|
||||
self._updateDialogTitle()
|
||||
|
||||
def setPageCount(self, count: int) -> None:
|
||||
self._page_count = count
|
||||
self._updateDialogTitle()
|
||||
|
||||
def _updateDialogTitle(self):
|
||||
self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement")
|
||||
if self._page_count > 1:
|
||||
self._dialogTitle = self._dialogTitle + " ({}/{})".format(self._current_page_idx + 1, self._page_count)
|
||||
self.dialogTitleChanged.emit()
|
||||
113
plugins/Toolbox/src/CloudSync/LicensePresenter.py
Normal file
113
plugins/Toolbox/src/CloudSync/LicensePresenter.py
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import os
|
||||
from collections import OrderedDict
|
||||
from typing import Dict, Optional, List, Any
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
from UM.PackageManager import PackageManager
|
||||
from UM.Signal import Signal
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
from .LicenseModel import LicenseModel
|
||||
|
||||
|
||||
## Call present() to show a licenseDialog for a set of packages
|
||||
# licenseAnswers emits a list of Dicts containing answers when the user has made a choice for all provided packages
|
||||
class LicensePresenter(QObject):
|
||||
|
||||
def __init__(self, app: CuraApplication) -> None:
|
||||
super().__init__()
|
||||
self._catalog = i18nCatalog("cura")
|
||||
self._dialog = None # type: Optional[QObject]
|
||||
self._package_manager = app.getPackageManager() # type: PackageManager
|
||||
# Emits List[Dict[str, [Any]] containing for example
|
||||
# [{ "package_id": "BarbarianPlugin", "package_path" : "/tmp/dg345as", "accepted" : True }]
|
||||
self.licenseAnswers = Signal()
|
||||
|
||||
self._current_package_idx = 0
|
||||
self._package_models = [] # type: List[Dict]
|
||||
decline_button_text = self._catalog.i18nc("@button", "Decline and remove from account")
|
||||
self._license_model = LicenseModel(decline_button_text=decline_button_text) # type: LicenseModel
|
||||
self._page_count = 0
|
||||
|
||||
self._app = app
|
||||
|
||||
self._compatibility_dialog_path = "resources/qml/dialogs/ToolboxLicenseDialog.qml"
|
||||
|
||||
## Show a license dialog for multiple packages where users can read a license and accept or decline them
|
||||
# \param plugin_path: Root directory of the Toolbox plugin
|
||||
# \param packages: Dict[package id, file path]
|
||||
def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None:
|
||||
path = os.path.join(plugin_path, self._compatibility_dialog_path)
|
||||
|
||||
self._initState(packages)
|
||||
|
||||
if self._page_count == 0:
|
||||
self.licenseAnswers.emit(self._package_models)
|
||||
return
|
||||
|
||||
if self._dialog is None:
|
||||
|
||||
context_properties = {
|
||||
"catalog": self._catalog,
|
||||
"licenseModel": self._license_model,
|
||||
"handler": self
|
||||
}
|
||||
self._dialog = self._app.createQmlComponent(path, context_properties)
|
||||
self._presentCurrentPackage()
|
||||
|
||||
@pyqtSlot()
|
||||
def onLicenseAccepted(self) -> None:
|
||||
self._package_models[self._current_package_idx]["accepted"] = True
|
||||
self._checkNextPage()
|
||||
|
||||
@pyqtSlot()
|
||||
def onLicenseDeclined(self) -> None:
|
||||
self._package_models[self._current_package_idx]["accepted"] = False
|
||||
self._checkNextPage()
|
||||
|
||||
def _initState(self, packages: Dict[str, Dict[str, Any]]) -> None:
|
||||
|
||||
implicitly_accepted_count = 0
|
||||
|
||||
for package_id, item in packages.items():
|
||||
item["package_id"] = package_id
|
||||
item["licence_content"] = self._package_manager.getPackageLicense(item["package_path"])
|
||||
if item["licence_content"] is None:
|
||||
# Implicitly accept when there is no license
|
||||
item["accepted"] = True
|
||||
implicitly_accepted_count = implicitly_accepted_count + 1
|
||||
self._package_models.append(item)
|
||||
else:
|
||||
item["accepted"] = None #: None: no answer yet
|
||||
# When presenting the packages, we want to show packages which have a license first.
|
||||
# In fact, we don't want to show the others at all because they are implicitly accepted
|
||||
self._package_models.insert(0, item)
|
||||
CuraApplication.getInstance().processEvents()
|
||||
self._page_count = len(self._package_models) - implicitly_accepted_count
|
||||
self._license_model.setPageCount(self._page_count)
|
||||
|
||||
|
||||
def _presentCurrentPackage(self) -> None:
|
||||
package_model = self._package_models[self._current_package_idx]
|
||||
package_info = self._package_manager.getPackageInfo(package_model["package_path"])
|
||||
|
||||
self._license_model.setCurrentPageIdx(self._current_package_idx)
|
||||
self._license_model.setPackageName(package_info["display_name"])
|
||||
self._license_model.setIconUrl(package_model["icon_url"])
|
||||
self._license_model.setLicenseText(package_model["licence_content"])
|
||||
if self._dialog:
|
||||
self._dialog.open() # Does nothing if already open
|
||||
|
||||
def _checkNextPage(self) -> None:
|
||||
if self._current_package_idx + 1 < self._page_count:
|
||||
self._current_package_idx += 1
|
||||
self._presentCurrentPackage()
|
||||
else:
|
||||
if self._dialog:
|
||||
self._dialog.close()
|
||||
self.licenseAnswers.emit(self._package_models)
|
||||
|
||||
|
||||
|
||||
31
plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py
Normal file
31
plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
from UM import i18nCatalog
|
||||
from UM.Message import Message
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## Presents a dialog telling the user that a restart is required to apply changes
|
||||
# Since we cannot restart Cura, the app is closed instead when the button is clicked
|
||||
class RestartApplicationPresenter:
|
||||
def __init__(self, app: CuraApplication) -> None:
|
||||
self._app = app
|
||||
self._i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
def present(self) -> None:
|
||||
app_name = self._app.getApplicationDisplayName()
|
||||
|
||||
message = Message(self._i18n_catalog.i18nc(
|
||||
"@info:generic",
|
||||
"You need to quit and restart {} before changes have effect.", app_name
|
||||
))
|
||||
|
||||
message.addAction("quit",
|
||||
name="Quit " + app_name,
|
||||
icon = "",
|
||||
description="Close the application",
|
||||
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)
|
||||
|
||||
message.actionTriggered.connect(self._quitClicked)
|
||||
message.show()
|
||||
|
||||
def _quitClicked(self, *_):
|
||||
self._app.windowClosed()
|
||||
73
plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py
Normal file
73
plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtProperty, pyqtSlot
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from cura import ApplicationMetadata
|
||||
from UM.Logger import Logger
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
class SubscribedPackagesModel(ListModel):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._items = []
|
||||
self._metadata = None
|
||||
self._discrepancies = None
|
||||
self._sdk_version = ApplicationMetadata.CuraSDKVersion
|
||||
|
||||
self.addRoleName(Qt.UserRole + 1, "package_id")
|
||||
self.addRoleName(Qt.UserRole + 2, "display_name")
|
||||
self.addRoleName(Qt.UserRole + 3, "icon_url")
|
||||
self.addRoleName(Qt.UserRole + 4, "is_compatible")
|
||||
self.addRoleName(Qt.UserRole + 5, "is_dismissed")
|
||||
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def hasCompatiblePackages(self) -> bool:
|
||||
for item in self._items:
|
||||
if item['is_compatible']:
|
||||
return True
|
||||
return False
|
||||
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def hasIncompatiblePackages(self) -> bool:
|
||||
for item in self._items:
|
||||
if not item['is_compatible']:
|
||||
return True
|
||||
return False
|
||||
|
||||
def addDiscrepancies(self, discrepancy: List[str]) -> None:
|
||||
self._discrepancies = discrepancy
|
||||
|
||||
def getCompatiblePackages(self) -> List[Dict[str, Any]]:
|
||||
return [package for package in self._items if package["is_compatible"]]
|
||||
|
||||
def getIncompatiblePackages(self) -> List[str]:
|
||||
return [package["package_id"] for package in self._items if not package["is_compatible"]]
|
||||
|
||||
def initialize(self, subscribed_packages_payload: List[Dict[str, Any]]) -> None:
|
||||
self._items.clear()
|
||||
for item in subscribed_packages_payload:
|
||||
if item["package_id"] not in self._discrepancies:
|
||||
continue
|
||||
package = {
|
||||
"package_id": item["package_id"],
|
||||
"display_name": item["display_name"],
|
||||
"sdk_versions": item["sdk_versions"],
|
||||
"download_url": item["download_url"],
|
||||
"md5_hash": item["md5_hash"],
|
||||
"is_dismissed": False,
|
||||
}
|
||||
if self._sdk_version not in item["sdk_versions"]:
|
||||
package.update({"is_compatible": False})
|
||||
else:
|
||||
package.update({"is_compatible": True})
|
||||
try:
|
||||
package.update({"icon_url": item["icon_url"]})
|
||||
except KeyError: # There is no 'icon_url" in the response payload for this package
|
||||
package.update({"icon_url": ""})
|
||||
self._items.append(package)
|
||||
self.setItems(self._items)
|
||||
|
||||
|
||||
100
plugins/Toolbox/src/CloudSync/SyncOrchestrator.py
Normal file
100
plugins/Toolbox/src/CloudSync/SyncOrchestrator.py
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import os
|
||||
from typing import List, Dict, Any, cast
|
||||
|
||||
from UM import i18n_catalog
|
||||
from UM.Extension import Extension
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from .CloudPackageChecker import CloudPackageChecker
|
||||
from .CloudPackageManager import CloudPackageManager
|
||||
from .DiscrepanciesPresenter import DiscrepanciesPresenter
|
||||
from .DownloadPresenter import DownloadPresenter
|
||||
from .LicensePresenter import LicensePresenter
|
||||
from .RestartApplicationPresenter import RestartApplicationPresenter
|
||||
from .SubscribedPackagesModel import SubscribedPackagesModel
|
||||
|
||||
|
||||
## Orchestrates the synchronizing of packages from the user account to the installed packages
|
||||
# Example flow:
|
||||
# - CloudPackageChecker compares a list of packages the user `subscribed` to in their account
|
||||
# If there are `discrepancies` between the account and locally installed packages, they are emitted
|
||||
# - DiscrepanciesPresenter shows a list of packages to be added or removed to the user. It emits the `packageMutations`
|
||||
# the user selected to be performed
|
||||
# - The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed
|
||||
# - The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads
|
||||
# - The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to
|
||||
# be installed. It emits the `licenseAnswers` signal for accept or declines
|
||||
# - The CloudPackageManager removes the declined packages from the account
|
||||
# - The SyncOrchestrator uses PackageManager to install the downloaded packages and delete temp files.
|
||||
# - The RestartApplicationPresenter notifies the user that a restart is required for changes to take effect
|
||||
class SyncOrchestrator(Extension):
|
||||
|
||||
def __init__(self, app: CuraApplication) -> None:
|
||||
super().__init__()
|
||||
# Differentiate This PluginObject from the Toolbox. self.getId() includes _name.
|
||||
# getPluginId() will return the same value for The toolbox extension and this one
|
||||
self._name = "SyncOrchestrator"
|
||||
|
||||
self._package_manager = app.getPackageManager()
|
||||
# Keep a reference to the CloudPackageManager. it watches for installed packages and subscribes to them
|
||||
self._cloud_package_manager = CloudPackageManager.getInstance(app) # type: CloudPackageManager
|
||||
|
||||
self._checker = CloudPackageChecker(app) # type: CloudPackageChecker
|
||||
self._checker.discrepancies.connect(self._onDiscrepancies)
|
||||
|
||||
self._discrepancies_presenter = DiscrepanciesPresenter(app) # type: DiscrepanciesPresenter
|
||||
self._discrepancies_presenter.packageMutations.connect(self._onPackageMutations)
|
||||
|
||||
self._download_presenter = DownloadPresenter(app) # type: DownloadPresenter
|
||||
|
||||
self._license_presenter = LicensePresenter(app) # type: LicensePresenter
|
||||
self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers)
|
||||
|
||||
self._restart_presenter = RestartApplicationPresenter(app)
|
||||
|
||||
def _onDiscrepancies(self, model: SubscribedPackagesModel) -> None:
|
||||
plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId()))
|
||||
self._discrepancies_presenter.present(plugin_path, model)
|
||||
|
||||
def _onPackageMutations(self, mutations: SubscribedPackagesModel) -> None:
|
||||
self._download_presenter = self._download_presenter.resetCopy()
|
||||
self._download_presenter.done.connect(self._onDownloadFinished)
|
||||
self._download_presenter.download(mutations)
|
||||
|
||||
## Called when a set of packages have finished downloading
|
||||
# \param success_items: Dict[package_id, Dict[str, str]]
|
||||
# \param error_items: List[package_id]
|
||||
def _onDownloadFinished(self, success_items: Dict[str, Dict[str, str]], error_items: List[str]) -> None:
|
||||
if error_items:
|
||||
message = i18n_catalog.i18nc("@info:generic", "{} plugins failed to download".format(len(error_items)))
|
||||
self._showErrorMessage(message)
|
||||
|
||||
plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId()))
|
||||
self._license_presenter.present(plugin_path, success_items)
|
||||
|
||||
# Called when user has accepted / declined all licenses for the downloaded packages
|
||||
def _onLicenseAnswers(self, answers: List[Dict[str, Any]]) -> None:
|
||||
has_changes = False # True when at least one package is installed
|
||||
|
||||
for item in answers:
|
||||
if item["accepted"]:
|
||||
# install and subscribe packages
|
||||
if not self._package_manager.installPackage(item["package_path"]):
|
||||
message = "Could not install {}".format(item["package_id"])
|
||||
self._showErrorMessage(message)
|
||||
continue
|
||||
has_changes = True
|
||||
else:
|
||||
self._cloud_package_manager.unsubscribe(item["package_id"])
|
||||
# delete temp file
|
||||
os.remove(item["package_path"])
|
||||
|
||||
if has_changes:
|
||||
self._restart_presenter.present()
|
||||
|
||||
## Logs an error and shows it to the user
|
||||
def _showErrorMessage(self, text: str):
|
||||
Logger.error(text)
|
||||
Message(text, lifetime=0).show()
|
||||
0
plugins/Toolbox/src/CloudSync/__init__.py
Normal file
0
plugins/Toolbox/src/CloudSync/__init__.py
Normal file
|
|
@ -1,7 +1,8 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtProperty
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from cura import ApplicationMetadata
|
||||
|
||||
|
||||
class SubscribedPackagesModel(ListModel):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self._metadata = None
|
||||
self._discrepancies = None
|
||||
self._sdk_version = ApplicationMetadata.CuraSDKVersion
|
||||
|
||||
self.addRoleName(Qt.UserRole + 1, "name")
|
||||
self.addRoleName(Qt.UserRole + 2, "icon_url")
|
||||
self.addRoleName(Qt.UserRole + 3, "is_compatible")
|
||||
|
||||
def setMetadata(self, data):
|
||||
if self._metadata != data:
|
||||
self._metadata = data
|
||||
|
||||
def addValue(self, discrepancy):
|
||||
if self._discrepancies != discrepancy:
|
||||
self._discrepancies = discrepancy
|
||||
|
||||
def update(self):
|
||||
items = []
|
||||
|
||||
for item in self._metadata:
|
||||
if item["package_id"] not in self._discrepancies:
|
||||
continue
|
||||
package = {"name": item["display_name"], "sdk_versions": item["sdk_versions"]}
|
||||
if self._sdk_version not in item["sdk_versions"]:
|
||||
package.update({"is_compatible": "False"})
|
||||
else:
|
||||
package.update({"is_compatible": "True"})
|
||||
try:
|
||||
package.update({"icon_url": item["icon_url"]})
|
||||
except KeyError: # There is no 'icon_url" in the response payload for this package
|
||||
package.update({"icon_url": ""})
|
||||
|
||||
items.append(package)
|
||||
self.setItems(items)
|
||||
|
|
@ -4,10 +4,9 @@
|
|||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import platform
|
||||
from typing import cast, Any, Dict, List, Set, TYPE_CHECKING, Tuple, Optional, Union
|
||||
|
||||
from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
|
@ -15,24 +14,34 @@ from UM.PluginRegistry import PluginRegistry
|
|||
from UM.Extension import Extension
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Version import Version
|
||||
from UM.Message import Message
|
||||
|
||||
from cura import ApplicationMetadata
|
||||
from cura import UltimakerCloudAuthentication
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
|
||||
from .CloudApiModel import CloudApiModel
|
||||
from .AuthorsModel import AuthorsModel
|
||||
from .CloudSync.LicenseModel import LicenseModel
|
||||
from .PackagesModel import PackagesModel
|
||||
from .SubscribedPackagesModel import SubscribedPackagesModel
|
||||
from .UltimakerCloudScope import UltimakerCloudScope
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.TaskManagement.HttpRequestData import HttpRequestData
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
DEFAULT_MARKETPLACE_ROOT = "https://marketplace.ultimaker.com" # type: str
|
||||
|
||||
## The Toolbox class is responsible of communicating with the server through the API
|
||||
try:
|
||||
from cura.CuraVersion import CuraMarketplaceRoot
|
||||
except ImportError:
|
||||
CuraMarketplaceRoot = DEFAULT_MARKETPLACE_ROOT
|
||||
|
||||
# todo Remove license and download dialog, use SyncOrchestrator instead
|
||||
|
||||
## Provides a marketplace for users to download plugins an materials
|
||||
class Toolbox(QObject, Extension):
|
||||
def __init__(self, application: CuraApplication) -> None:
|
||||
super().__init__()
|
||||
|
|
@ -40,20 +49,14 @@ class Toolbox(QObject, Extension):
|
|||
self._application = application # type: CuraApplication
|
||||
|
||||
self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
|
||||
self._cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion # type: str
|
||||
self._cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot # type: str
|
||||
self._api_url = None # type: Optional[str]
|
||||
|
||||
# Network:
|
||||
self._download_request = None # type: Optional[QNetworkRequest]
|
||||
self._download_reply = None # type: Optional[QNetworkReply]
|
||||
self._download_request_data = None # type: Optional[HttpRequestData]
|
||||
self._download_progress = 0 # type: float
|
||||
self._is_downloading = False # type: bool
|
||||
self._network_manager = None # type: Optional[QNetworkAccessManager]
|
||||
self._request_headers = [] # type: List[Tuple[bytes, bytes]]
|
||||
self._updateRequestHeader()
|
||||
self._scope = UltimakerCloudScope(application) # type: UltimakerCloudScope
|
||||
|
||||
self._request_urls = {} # type: Dict[str, QUrl]
|
||||
self._request_urls = {} # type: Dict[str, str]
|
||||
self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated
|
||||
self._old_plugin_ids = set() # type: Set[str]
|
||||
self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]]
|
||||
|
|
@ -62,17 +65,15 @@ class Toolbox(QObject, Extension):
|
|||
self._server_response_data = {
|
||||
"authors": [],
|
||||
"packages": [],
|
||||
"updates": [],
|
||||
"subscribed_packages": [],
|
||||
"updates": []
|
||||
} # type: Dict[str, List[Any]]
|
||||
|
||||
# Models:
|
||||
self._models = {
|
||||
"authors": AuthorsModel(self),
|
||||
"packages": PackagesModel(self),
|
||||
"updates": PackagesModel(self),
|
||||
"subscribed_packages": SubscribedPackagesModel(self),
|
||||
} # type: Dict[str, Union[AuthorsModel, PackagesModel, SubscribedPackagesModel]]
|
||||
"updates": PackagesModel(self)
|
||||
} # type: Dict[str, Union[AuthorsModel, PackagesModel]]
|
||||
|
||||
self._plugins_showcase_model = PackagesModel(self)
|
||||
self._plugins_available_model = PackagesModel(self)
|
||||
|
|
@ -83,6 +84,8 @@ class Toolbox(QObject, Extension):
|
|||
self._materials_installed_model = PackagesModel(self)
|
||||
self._materials_generic_model = PackagesModel(self)
|
||||
|
||||
self._license_model = LicenseModel()
|
||||
|
||||
# These properties are for keeping track of the UI state:
|
||||
# ----------------------------------------------------------------------
|
||||
# View category defines which filter to use, and therefore effectively
|
||||
|
|
@ -105,13 +108,9 @@ class Toolbox(QObject, Extension):
|
|||
self._restart_required = False # type: bool
|
||||
|
||||
# variables for the license agreement dialog
|
||||
self._license_dialog_plugin_name = "" # type: str
|
||||
self._license_dialog_license_content = "" # type: str
|
||||
self._license_dialog_plugin_file_location = "" # type: str
|
||||
self._restart_dialog_message = "" # type: str
|
||||
|
||||
self._application.initializationFinished.connect(self._onAppInitialized)
|
||||
self._application.getCuraAPI().account.accessTokenChanged.connect(self._updateRequestHeader)
|
||||
|
||||
# Signals:
|
||||
# --------------------------------------------------------------------------
|
||||
|
|
@ -129,11 +128,11 @@ class Toolbox(QObject, Extension):
|
|||
filterChanged = pyqtSignal()
|
||||
metadataChanged = pyqtSignal()
|
||||
showLicenseDialog = pyqtSignal()
|
||||
closeLicenseDialog = pyqtSignal()
|
||||
uninstallVariablesChanged = pyqtSignal()
|
||||
|
||||
## Go back to the start state (welcome screen or loading if no login required)
|
||||
def _restart(self):
|
||||
self._updateRequestHeader()
|
||||
# For an Essentials build, login is mandatory
|
||||
if not self._application.getCuraAPI().account.isLoggedIn and ApplicationMetadata.IsEnterpriseVersion:
|
||||
self.setViewPage("welcome")
|
||||
|
|
@ -141,22 +140,6 @@ class Toolbox(QObject, Extension):
|
|||
self.setViewPage("loading")
|
||||
self._fetchPackageData()
|
||||
|
||||
def _updateRequestHeader(self):
|
||||
self._request_headers = [
|
||||
(b"User-Agent",
|
||||
str.encode(
|
||||
"%s/%s (%s %s)" % (
|
||||
self._application.getApplicationName(),
|
||||
self._application.getVersion(),
|
||||
platform.system(),
|
||||
platform.machine(),
|
||||
)
|
||||
))
|
||||
]
|
||||
access_token = self._application.getCuraAPI().account.accessToken
|
||||
if access_token:
|
||||
self._request_headers.append((b"Authorization", "Bearer {}".format(access_token).encode()))
|
||||
|
||||
def _resetUninstallVariables(self) -> None:
|
||||
self._package_id_to_uninstall = None # type: Optional[str]
|
||||
self._package_name_to_uninstall = ""
|
||||
|
|
@ -165,29 +148,22 @@ class Toolbox(QObject, Extension):
|
|||
|
||||
@pyqtSlot(str, int)
|
||||
def ratePackage(self, package_id: str, rating: int) -> None:
|
||||
url = QUrl("{base_url}/packages/{package_id}/ratings".format(base_url = self._api_url, package_id = package_id))
|
||||
|
||||
self._rate_request = QNetworkRequest(url)
|
||||
for header_name, header_value in self._request_headers:
|
||||
cast(QNetworkRequest, self._rate_request).setRawHeader(header_name, header_value)
|
||||
url = "{base_url}/packages/{package_id}/ratings".format(base_url = CloudApiModel.api_url, package_id = package_id)
|
||||
data = "{\"data\": {\"cura_version\": \"%s\", \"rating\": %i}}" % (Version(self._application.getVersion()), rating)
|
||||
self._rate_reply = cast(QNetworkAccessManager, self._network_manager).put(self._rate_request, data.encode())
|
||||
|
||||
@pyqtSlot(result = str)
|
||||
def getLicenseDialogPluginName(self) -> str:
|
||||
return self._license_dialog_plugin_name
|
||||
self._application.getHttpRequestManager().put(url, data = data.encode(), scope = self._scope)
|
||||
|
||||
@pyqtSlot(result = str)
|
||||
def getLicenseDialogPluginFileLocation(self) -> str:
|
||||
return self._license_dialog_plugin_file_location
|
||||
|
||||
@pyqtSlot(result = str)
|
||||
def getLicenseDialogLicenseContent(self) -> str:
|
||||
return self._license_dialog_license_content
|
||||
def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str, icon_url: str) -> None:
|
||||
# Set page 1/1 when opening the dialog for a single package
|
||||
self._license_model.setCurrentPageIdx(0)
|
||||
self._license_model.setPageCount(1)
|
||||
self._license_model.setIconUrl(icon_url)
|
||||
|
||||
def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str) -> None:
|
||||
self._license_dialog_plugin_name = plugin_name
|
||||
self._license_dialog_license_content = license_content
|
||||
self._license_model.setPackageName(plugin_name)
|
||||
self._license_model.setLicenseText(license_content)
|
||||
self._license_dialog_plugin_file_location = plugin_file_location
|
||||
self.showLicenseDialog.emit()
|
||||
|
||||
|
|
@ -196,16 +172,6 @@ class Toolbox(QObject, Extension):
|
|||
def _onAppInitialized(self) -> None:
|
||||
self._plugin_registry = self._application.getPluginRegistry()
|
||||
self._package_manager = self._application.getPackageManager()
|
||||
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,
|
||||
sdk_version = self._sdk_version
|
||||
)
|
||||
# https://api.ultimaker.com/cura-packages/v1/user/packages
|
||||
self._api_url_user_packages = "{cloud_api_root}/cura-packages/v{cloud_api_version}/user/packages".format(
|
||||
cloud_api_root = self._cloud_api_root,
|
||||
cloud_api_version = self._cloud_api_version,
|
||||
)
|
||||
|
||||
# We need to construct a query like installed_packages=ID:VERSION&installed_packages=ID:VERSION, etc.
|
||||
installed_package_ids_with_versions = [":".join(items) for items in
|
||||
|
|
@ -213,51 +179,28 @@ class Toolbox(QObject, Extension):
|
|||
installed_packages_query = "&installed_packages=".join(installed_package_ids_with_versions)
|
||||
|
||||
self._request_urls = {
|
||||
"authors": QUrl("{base_url}/authors".format(base_url = self._api_url)),
|
||||
"packages": QUrl("{base_url}/packages".format(base_url = self._api_url)),
|
||||
"updates": QUrl("{base_url}/packages/package-updates?installed_packages={query}".format(
|
||||
base_url = self._api_url, query = installed_packages_query)),
|
||||
"subscribed_packages": QUrl(self._api_url_user_packages)
|
||||
"authors": "{base_url}/authors".format(base_url = CloudApiModel.api_url),
|
||||
"packages": "{base_url}/packages".format(base_url = CloudApiModel.api_url),
|
||||
"updates": "{base_url}/packages/package-updates?installed_packages={query}".format(
|
||||
base_url = CloudApiModel.api_url, query = installed_packages_query)
|
||||
}
|
||||
|
||||
self._application.getCuraAPI().account.loginStateChanged.connect(self._restart)
|
||||
self._application.getCuraAPI().account.loginStateChanged.connect(self._fetchUserSubscribedPackages)
|
||||
|
||||
# On boot we check which packages have updates.
|
||||
if CuraApplication.getInstance().getPreferences().getValue("info/automatic_update_check") and len(installed_package_ids_with_versions) > 0:
|
||||
# Request the latest and greatest!
|
||||
self._fetchPackageUpdates()
|
||||
self._fetchUserSubscribedPackages()
|
||||
self._makeRequestByType("updates")
|
||||
|
||||
def _prepareNetworkManager(self):
|
||||
if self._network_manager is not None:
|
||||
self._network_manager.finished.disconnect(self._onRequestFinished)
|
||||
self._network_manager.networkAccessibleChanged.disconnect(self._onNetworkAccessibleChanged)
|
||||
self._network_manager = QNetworkAccessManager()
|
||||
self._network_manager.finished.connect(self._onRequestFinished)
|
||||
self._network_manager.networkAccessibleChanged.connect(self._onNetworkAccessibleChanged)
|
||||
|
||||
def _fetchPackageUpdates(self):
|
||||
self._prepareNetworkManager()
|
||||
self._makeRequestByType("updates")
|
||||
|
||||
def _fetchPackageData(self):
|
||||
self._prepareNetworkManager()
|
||||
# Make remote requests:
|
||||
def _fetchPackageData(self) -> None:
|
||||
self._makeRequestByType("packages")
|
||||
self._makeRequestByType("authors")
|
||||
# Gather installed packages:
|
||||
self._updateInstalledModels()
|
||||
|
||||
def _fetchUserSubscribedPackages(self):
|
||||
if self._application.getCuraAPI().account.isLoggedIn:
|
||||
self._prepareNetworkManager()
|
||||
self._makeRequestByType("subscribed_packages")
|
||||
|
||||
# Displays the toolbox
|
||||
@pyqtSlot()
|
||||
def launch(self) -> None:
|
||||
|
||||
if not self._dialog:
|
||||
self._dialog = self._createDialog("Toolbox.qml")
|
||||
|
||||
|
|
@ -268,7 +211,6 @@ class Toolbox(QObject, Extension):
|
|||
self._restart()
|
||||
|
||||
self._dialog.show()
|
||||
|
||||
# Apply enabled/disabled state to installed plugins
|
||||
self.enabledChanged.emit()
|
||||
|
||||
|
|
@ -279,7 +221,11 @@ class Toolbox(QObject, Extension):
|
|||
return None
|
||||
path = os.path.join(plugin_path, "resources", "qml", qml_name)
|
||||
|
||||
dialog = self._application.createQmlComponent(path, {"toolbox": self})
|
||||
dialog = self._application.createQmlComponent(path, {
|
||||
"toolbox": self,
|
||||
"handler": self,
|
||||
"licenseModel": self._license_model
|
||||
})
|
||||
if not dialog:
|
||||
raise Exception("Failed to create Marketplace dialog")
|
||||
return dialog
|
||||
|
|
@ -350,13 +296,14 @@ class Toolbox(QObject, Extension):
|
|||
self.metadataChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def install(self, file_path: str) -> None:
|
||||
self._package_manager.installPackage(file_path)
|
||||
def install(self, file_path: str) -> Optional[str]:
|
||||
package_id = self._package_manager.installPackage(file_path)
|
||||
self.installChanged.emit()
|
||||
self._updateInstalledModels()
|
||||
self.metadataChanged.emit()
|
||||
self._restart_required = True
|
||||
self.restartRequiredChanged.emit()
|
||||
return package_id
|
||||
|
||||
## Check package usage and uninstall
|
||||
# If the package is in use, you'll get a confirmation dialog to set everything to default
|
||||
|
|
@ -428,6 +375,16 @@ class Toolbox(QObject, Extension):
|
|||
self._resetUninstallVariables()
|
||||
self.closeConfirmResetDialog()
|
||||
|
||||
@pyqtSlot()
|
||||
def onLicenseAccepted(self):
|
||||
self.closeLicenseDialog.emit()
|
||||
package_id = self.install(self.getLicenseDialogPluginFileLocation())
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def onLicenseDeclined(self):
|
||||
self.closeLicenseDialog.emit()
|
||||
|
||||
def _markPackageMaterialsAsToBeUninstalled(self, package_id: str) -> None:
|
||||
container_registry = self._application.getContainerRegistry()
|
||||
|
||||
|
|
@ -576,184 +533,136 @@ class Toolbox(QObject, Extension):
|
|||
# Make API Calls
|
||||
# --------------------------------------------------------------------------
|
||||
def _makeRequestByType(self, request_type: str) -> None:
|
||||
Logger.log("d", "Requesting '%s' metadata from server.", request_type)
|
||||
request = QNetworkRequest(self._request_urls[request_type])
|
||||
for header_name, header_value in self._request_headers:
|
||||
request.setRawHeader(header_name, header_value)
|
||||
self._updateRequestHeader()
|
||||
if self._network_manager:
|
||||
self._network_manager.get(request)
|
||||
Logger.log("d", "Requesting [%s] metadata from server.", request_type)
|
||||
url = self._request_urls[request_type]
|
||||
|
||||
callback = lambda r, rt = request_type: self._onDataRequestFinished(rt, r)
|
||||
error_callback = lambda r, e, rt = request_type: self._onDataRequestError(rt, r, e)
|
||||
self._application.getHttpRequestManager().get(url,
|
||||
callback = callback,
|
||||
error_callback = error_callback,
|
||||
scope=self._scope)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def startDownload(self, url: str) -> None:
|
||||
Logger.log("i", "Attempting to download & install package from %s.", url)
|
||||
url = QUrl(url)
|
||||
self._download_request = QNetworkRequest(url)
|
||||
if hasattr(QNetworkRequest, "FollowRedirectsAttribute"):
|
||||
# Patch for Qt 5.6-5.8
|
||||
cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.FollowRedirectsAttribute, True)
|
||||
if hasattr(QNetworkRequest, "RedirectPolicyAttribute"):
|
||||
# Patch for Qt 5.9+
|
||||
cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.RedirectPolicyAttribute, True)
|
||||
for header_name, header_value in self._request_headers:
|
||||
cast(QNetworkRequest, self._download_request).setRawHeader(header_name, header_value)
|
||||
self._download_reply = cast(QNetworkAccessManager, self._network_manager).get(self._download_request)
|
||||
|
||||
callback = lambda r: self._onDownloadFinished(r)
|
||||
error_callback = lambda r, e: self._onDownloadFailed(r, e)
|
||||
download_progress_callback = self._onDownloadProgress
|
||||
request_data = self._application.getHttpRequestManager().get(url,
|
||||
callback = callback,
|
||||
error_callback = error_callback,
|
||||
download_progress_callback = download_progress_callback,
|
||||
scope=self._scope
|
||||
)
|
||||
|
||||
self._download_request_data = request_data
|
||||
self.setDownloadProgress(0)
|
||||
self.setIsDownloading(True)
|
||||
cast(QNetworkReply, self._download_reply).downloadProgress.connect(self._onDownloadProgress)
|
||||
|
||||
@pyqtSlot()
|
||||
def cancelDownload(self) -> None:
|
||||
Logger.log("i", "User cancelled the download of a package.")
|
||||
Logger.log("i", "User cancelled the download of a package. request %s", self._download_request_data)
|
||||
if self._download_request_data is not None:
|
||||
self._application.getHttpRequestManager().abortRequest(self._download_request_data)
|
||||
self._download_request_data = None
|
||||
self.resetDownload()
|
||||
|
||||
def resetDownload(self) -> None:
|
||||
if self._download_reply:
|
||||
try:
|
||||
self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
|
||||
except (TypeError, RuntimeError): # Raised when the method is not connected to the signal yet.
|
||||
pass # Don't need to disconnect.
|
||||
try:
|
||||
self._download_reply.abort()
|
||||
except RuntimeError:
|
||||
# In some cases the garbage collector is a bit to agressive, which causes the dowload_reply
|
||||
# to be deleted (especially if the machine has been put to sleep). As we don't know what exactly causes
|
||||
# this (The issue probably lives in the bowels of (py)Qt somewhere), we can only catch and ignore it.
|
||||
pass
|
||||
self._download_reply = None
|
||||
self._download_request = None
|
||||
self.setDownloadProgress(0)
|
||||
self.setIsDownloading(False)
|
||||
|
||||
# Handlers for Network Events
|
||||
# --------------------------------------------------------------------------
|
||||
def _onNetworkAccessibleChanged(self, network_accessibility: QNetworkAccessManager.NetworkAccessibility) -> None:
|
||||
if network_accessibility == QNetworkAccessManager.NotAccessible:
|
||||
self.resetDownload()
|
||||
def _onDataRequestError(self, request_type: str, reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None:
|
||||
Logger.log("e", "Request [%s] failed due to error [%s]: %s", request_type, error, reply.errorString())
|
||||
self.setViewPage("errored")
|
||||
|
||||
def _onRequestFinished(self, reply: QNetworkReply) -> None:
|
||||
if reply.error() == QNetworkReply.TimeoutError:
|
||||
Logger.log("w", "Got a timeout.")
|
||||
self.setViewPage("errored")
|
||||
self.resetDownload()
|
||||
def _onDataRequestFinished(self, request_type: str, reply: "QNetworkReply") -> None:
|
||||
if reply.operation() != QNetworkAccessManager.GetOperation:
|
||||
Logger.log("e", "_onDataRequestFinished() only handles GET requests but got [%s] instead", reply.operation())
|
||||
return
|
||||
|
||||
if reply.error() == QNetworkReply.HostNotFoundError:
|
||||
Logger.log("w", "Unable to reach server.")
|
||||
http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
if http_status_code != 200:
|
||||
Logger.log("e", "Request type [%s] got non-200 HTTP response: [%s]", http_status_code)
|
||||
self.setViewPage("errored")
|
||||
self.resetDownload()
|
||||
return
|
||||
|
||||
if reply.operation() == QNetworkAccessManager.GetOperation:
|
||||
for response_type, url in self._request_urls.items():
|
||||
if reply.url() == url:
|
||||
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200:
|
||||
try:
|
||||
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
|
||||
data = bytes(reply.readAll())
|
||||
try:
|
||||
json_data = json.loads(data.decode("utf-8"))
|
||||
except json.decoder.JSONDecodeError:
|
||||
Logger.log("e", "Failed to decode response data as JSON for request type [%s], response data [%s]",
|
||||
request_type, data)
|
||||
self.setViewPage("errored")
|
||||
return
|
||||
|
||||
# Check for errors:
|
||||
if "errors" in json_data:
|
||||
for error in json_data["errors"]:
|
||||
Logger.log("e", "%s", error["title"])
|
||||
return
|
||||
# Check for errors:
|
||||
if "errors" in json_data:
|
||||
for error in json_data["errors"]:
|
||||
Logger.log("e", "Request type [%s] got response showing error: %s", error["title"])
|
||||
self.setViewPage("errored")
|
||||
return
|
||||
|
||||
# Create model and apply metadata:
|
||||
if not self._models[response_type]:
|
||||
Logger.log("e", "Could not find the %s model.", response_type)
|
||||
break
|
||||
# Create model and apply metadata:
|
||||
if not self._models[request_type]:
|
||||
Logger.log("e", "Could not find the model for request type [%s].", request_type)
|
||||
self.setViewPage("errored")
|
||||
return
|
||||
|
||||
self._server_response_data[response_type] = json_data["data"]
|
||||
self._models[response_type].setMetadata(self._server_response_data[response_type])
|
||||
self._server_response_data[request_type] = json_data["data"]
|
||||
self._models[request_type].setMetadata(self._server_response_data[request_type])
|
||||
|
||||
if response_type == "packages":
|
||||
self._models[response_type].setFilter({"type": "plugin"})
|
||||
self.reBuildMaterialsModels()
|
||||
self.reBuildPluginsModels()
|
||||
self._notifyPackageManager()
|
||||
elif response_type == "authors":
|
||||
self._models[response_type].setFilter({"package_types": "material"})
|
||||
self._models[response_type].setFilter({"tags": "generic"})
|
||||
elif response_type == "updates":
|
||||
# Tell the package manager that there's a new set of updates available.
|
||||
packages = set([pkg["package_id"] for pkg in self._server_response_data[response_type]])
|
||||
self._package_manager.setPackagesWithUpdate(packages)
|
||||
elif response_type == "subscribed_packages":
|
||||
self._checkCompatibilities(json_data["data"])
|
||||
if request_type == "packages":
|
||||
self._models[request_type].setFilter({"type": "plugin"})
|
||||
self.reBuildMaterialsModels()
|
||||
self.reBuildPluginsModels()
|
||||
self._notifyPackageManager()
|
||||
elif request_type == "authors":
|
||||
self._models[request_type].setFilter({"package_types": "material"})
|
||||
self._models[request_type].setFilter({"tags": "generic"})
|
||||
elif request_type == "updates":
|
||||
# Tell the package manager that there's a new set of updates available.
|
||||
packages = set([pkg["package_id"] for pkg in self._server_response_data[request_type]])
|
||||
self._package_manager.setPackagesWithUpdate(packages)
|
||||
|
||||
self.metadataChanged.emit()
|
||||
self.metadataChanged.emit()
|
||||
|
||||
if self.isLoadingComplete():
|
||||
self.setViewPage("overview")
|
||||
|
||||
except json.decoder.JSONDecodeError:
|
||||
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()
|
||||
|
||||
def _checkCompatibilities(self, json_data) -> None:
|
||||
user_subscribed_packages = [plugin["package_id"] for plugin in json_data]
|
||||
user_installed_packages = self._package_manager.getUserInstalledPackages()
|
||||
|
||||
# We check if there are packages installed in Cloud Marketplace but not in Cura marketplace (discrepancy)
|
||||
package_discrepancy = list(set(user_subscribed_packages).difference(user_installed_packages))
|
||||
if package_discrepancy:
|
||||
self._models["subscribed_packages"].addValue(package_discrepancy)
|
||||
self._models["subscribed_packages"].update()
|
||||
Logger.log("d", "Discrepancy found between Cloud subscribed packages and Cura installed packages")
|
||||
sync_message = Message(i18n_catalog.i18nc(
|
||||
"@info:generic",
|
||||
"\nDo you want to sync material and software packages with your account?"),
|
||||
lifetime=0,
|
||||
title=i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
|
||||
sync_message.addAction("sync",
|
||||
name=i18n_catalog.i18nc("@action:button", "Sync"),
|
||||
icon="",
|
||||
description="Sync your Cloud subscribed packages to your local environment.",
|
||||
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)
|
||||
|
||||
sync_message.actionTriggered.connect(self._onSyncButtonClicked)
|
||||
sync_message.show()
|
||||
|
||||
def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None:
|
||||
sync_message.hide()
|
||||
compatibility_dialog_path = "resources/qml/dialogs/CompatibilityDialog.qml"
|
||||
plugin_path_prefix = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||
if plugin_path_prefix:
|
||||
path = os.path.join(plugin_path_prefix, compatibility_dialog_path)
|
||||
self.compatibility_dialog_view = self._application.getInstance().createQmlComponent(path, {"toolbox": self})
|
||||
if self.isLoadingComplete():
|
||||
self.setViewPage("overview")
|
||||
|
||||
# This function goes through all known remote versions of a package and notifies the package manager of this change
|
||||
def _notifyPackageManager(self):
|
||||
for package in self._server_response_data["packages"]:
|
||||
self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"]))
|
||||
|
||||
def _onDownloadFinished(self, reply: "QNetworkReply") -> None:
|
||||
self.resetDownload()
|
||||
|
||||
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
|
||||
Logger.log("w", "Failed to download package. The following error was returned: %s",
|
||||
json.loads(reply.readAll().data().decode("utf-8")))
|
||||
return
|
||||
# Must not delete the temporary file on Windows
|
||||
self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False)
|
||||
file_path = self._temp_plugin_file.name
|
||||
# Write first and close, otherwise on Windows, it cannot read the file
|
||||
self._temp_plugin_file.write(reply.readAll())
|
||||
self._temp_plugin_file.close()
|
||||
self._onDownloadComplete(file_path)
|
||||
|
||||
def _onDownloadFailed(self, reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None:
|
||||
Logger.log("w", "Failed to download package. The following error was returned: %s", error)
|
||||
|
||||
self.resetDownload()
|
||||
|
||||
def _onDownloadProgress(self, bytes_sent: int, bytes_total: int) -> None:
|
||||
if bytes_total > 0:
|
||||
new_progress = bytes_sent / bytes_total * 100
|
||||
self.setDownloadProgress(new_progress)
|
||||
if bytes_sent == bytes_total:
|
||||
self.setIsDownloading(False)
|
||||
self._download_reply = cast(QNetworkReply, self._download_reply)
|
||||
self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
|
||||
|
||||
# Check if the download was sucessfull
|
||||
if self._download_reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
|
||||
try:
|
||||
Logger.log("w", "Failed to download package. The following error was returned: %s", json.loads(bytes(self._download_reply.readAll()).decode("utf-8")))
|
||||
except json.decoder.JSONDecodeError:
|
||||
Logger.logException("w", "Failed to download package and failed to parse a response from it")
|
||||
finally:
|
||||
return
|
||||
# Must not delete the temporary file on Windows
|
||||
self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False)
|
||||
file_path = self._temp_plugin_file.name
|
||||
# Write first and close, otherwise on Windows, it cannot read the file
|
||||
self._temp_plugin_file.write(cast(QNetworkReply, self._download_reply).readAll())
|
||||
self._temp_plugin_file.close()
|
||||
self._onDownloadComplete(file_path)
|
||||
Logger.log("d", "new download progress %s / %s : %s%%", bytes_sent, bytes_total, new_progress)
|
||||
|
||||
def _onDownloadComplete(self, file_path: str) -> None:
|
||||
Logger.log("i", "Download complete.")
|
||||
|
|
@ -763,11 +672,16 @@ class Toolbox(QObject, Extension):
|
|||
return
|
||||
|
||||
license_content = self._package_manager.getPackageLicense(file_path)
|
||||
package_id = package_info["package_id"]
|
||||
if license_content is not None:
|
||||
self.openLicenseDialog(package_info["package_id"], license_content, file_path)
|
||||
# get the icon url for package_id, make sure the result is a string, never None
|
||||
icon_url = next((x["icon_url"] for x in self.packagesModel.items if x["id"] == package_id), None) or ""
|
||||
self.openLicenseDialog(package_info["display_name"], license_content, file_path, icon_url)
|
||||
return
|
||||
|
||||
self.install(file_path)
|
||||
installed_id = self.install(file_path)
|
||||
if installed_id != package_id:
|
||||
Logger.error("Installed package {} does not match {}".format(installed_id, package_id))
|
||||
|
||||
# Getter & Setters for Properties:
|
||||
# --------------------------------------------------------------------------
|
||||
|
|
@ -789,14 +703,14 @@ class Toolbox(QObject, Extension):
|
|||
def isDownloading(self) -> bool:
|
||||
return self._is_downloading
|
||||
|
||||
def setActivePackage(self, package: Dict[str, Any]) -> None:
|
||||
def setActivePackage(self, package: QObject) -> None:
|
||||
if self._active_package != package:
|
||||
self._active_package = package
|
||||
self.activePackageChanged.emit()
|
||||
|
||||
## The active package is the package that is currently being downloaded
|
||||
@pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged)
|
||||
def activePackage(self) -> Optional[Dict[str, Any]]:
|
||||
def activePackage(self) -> Optional[QObject]:
|
||||
return self._active_package
|
||||
|
||||
def setViewCategory(self, category: str = "plugin") -> None:
|
||||
|
|
@ -804,6 +718,11 @@ class Toolbox(QObject, Extension):
|
|||
self._view_category = category
|
||||
self.viewChanged.emit()
|
||||
|
||||
## Function explicitly defined so that it can be called through the callExtensionsMethod
|
||||
# which cannot receive arguments.
|
||||
def setViewCategoryToMaterials(self) -> None:
|
||||
self.setViewCategory("material")
|
||||
|
||||
@pyqtProperty(str, fset = setViewCategory, notify = viewChanged)
|
||||
def viewCategory(self) -> str:
|
||||
return self._view_category
|
||||
|
|
@ -823,10 +742,6 @@ class Toolbox(QObject, Extension):
|
|||
def authorsModel(self) -> AuthorsModel:
|
||||
return cast(AuthorsModel, self._models["authors"])
|
||||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
def subscribedPackagesModel(self) -> SubscribedPackagesModel:
|
||||
return cast(SubscribedPackagesModel, self._models["subscribed_packages"])
|
||||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
def packagesModel(self) -> PackagesModel:
|
||||
return cast(PackagesModel, self._models["packages"])
|
||||
|
|
@ -859,6 +774,13 @@ class Toolbox(QObject, Extension):
|
|||
def materialsGenericModel(self) -> PackagesModel:
|
||||
return self._materials_generic_model
|
||||
|
||||
@pyqtSlot(str, result = str)
|
||||
def getWebMarketplaceUrl(self, page: str) -> str:
|
||||
root = CuraMarketplaceRoot
|
||||
if root == "":
|
||||
root = DEFAULT_MARKETPLACE_ROOT
|
||||
return root + "/app/cura/" + page
|
||||
|
||||
# Filter Models:
|
||||
# --------------------------------------------------------------------------
|
||||
@pyqtSlot(str, str, str)
|
||||
|
|
|
|||
28
plugins/Toolbox/src/UltimakerCloudScope.py
Normal file
28
plugins/Toolbox/src/UltimakerCloudScope.py
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
from PyQt5.QtNetwork import QNetworkRequest
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.TaskManagement.HttpRequestScope import DefaultUserAgentScope
|
||||
from cura.API import Account
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## Add a Authorization header to the request for Ultimaker Cloud Api requests.
|
||||
# When the user is not logged in or a token is not available, a warning will be logged
|
||||
# Also add the user agent headers (see DefaultUserAgentScope)
|
||||
class UltimakerCloudScope(DefaultUserAgentScope):
|
||||
def __init__(self, application: CuraApplication):
|
||||
super().__init__(application)
|
||||
api = application.getCuraAPI()
|
||||
self._account = api.account # type: Account
|
||||
|
||||
def request_hook(self, request: QNetworkRequest):
|
||||
super().request_hook(request)
|
||||
token = self._account.accessToken
|
||||
if not self._account.isLoggedIn or token is None:
|
||||
Logger.warning("Cannot add authorization to Cloud Api request")
|
||||
return
|
||||
|
||||
header_dict = {
|
||||
"Authorization": "Bearer {}".format(token)
|
||||
}
|
||||
self.add_headers(request, header_dict)
|
||||
|
|
@ -3,5 +3,5 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for reading model files.",
|
||||
"api": "7.0.0"
|
||||
"api": "7.1.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for reading Ultimaker Format Packages.",
|
||||
"supported_sdk_versions": ["7.0.0"],
|
||||
"supported_sdk_versions": ["7.1.0"],
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for writing Ultimaker Format Packages.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"description": "Manages network connections to Ultimaker networked printers.",
|
||||
"version": "2.0.0",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ class ClusterPrinterMaterialStationSlot(ClusterPrintCoreConfiguration):
|
|||
# \param material_remaining: How much material is remaining on the spool (between 0 and 1, or -1 for missing data).
|
||||
# \param material_empty: Whether the material spool is too empty to be used.
|
||||
def __init__(self, slot_index: int, compatible: bool, material_remaining: float,
|
||||
material_empty: Optional[bool] = False, **kwargs):
|
||||
material_empty: Optional[bool] = False, **kwargs) -> None:
|
||||
self.slot_index = slot_index
|
||||
self.compatible = compatible
|
||||
self.material_remaining = material_remaining
|
||||
|
|
|
|||
|
|
@ -88,7 +88,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
|
|||
self._firmware_name_requested = False
|
||||
self._firmware_updater = AvrFirmwareUpdater(self)
|
||||
|
||||
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml")
|
||||
plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath("USBPrinting"))
|
||||
self._monitor_view_qml_path = os.path.join(plugin_path, "MonitorItem.qml")
|
||||
|
||||
CuraApplication.getInstance().getOnExitCallbackManager().addCallback(self._checkActivePrintingUponAppExit)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
"name": "USB printing",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.2",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"description": "Accepts G-Code and sends them to a printer. Plugin can also update firmware.",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Upgrades configurations from Cura 2.1 to Cura 2.2.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Upgrades configurations from Cura 2.2 to Cura 2.4.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Upgrades configurations from Cura 2.5 to Cura 2.6.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Upgrades configurations from Cura 2.6 to Cura 2.7.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Upgrades configurations from Cura 2.7 to Cura 3.0.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Upgrades configurations from Cura 3.0 to Cura 3.1.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Upgrades configurations from Cura 3.2 to Cura 3.3.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Upgrades configurations from Cura 3.3 to Cura 3.4.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Upgrades configurations from Cura 3.4 to Cura 3.5.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Upgrades configurations from Cura 3.5 to Cura 4.0.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Upgrades configurations from Cura 4.0 to Cura 4.1.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Upgrades configurations from Cura 4.1 to Cura 4.2.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Upgrades configurations from Cura 4.2 to Cura 4.3.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Upgrades configurations from Cura 4.3 to Cura 4.4.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,16 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import configparser
|
||||
from typing import Tuple, List
|
||||
import fnmatch # To filter files that we need to delete.
|
||||
import io
|
||||
import os # To get the path to check for hidden stacks to delete.
|
||||
import urllib.parse # To get the container IDs from file names.
|
||||
import re # To filter directories to search for hidden stacks to delete.
|
||||
from UM.Logger import Logger
|
||||
from UM.Resources import Resources # To get the path to check for hidden stacks to delete.
|
||||
from UM.Version import Version # To sort folders by version number.
|
||||
from UM.VersionUpgrade import VersionUpgrade
|
||||
|
||||
# Settings that were merged into one. Each one is a pair of settings. If both
|
||||
|
|
@ -16,6 +26,102 @@ _removed_settings = {
|
|||
}
|
||||
|
||||
class VersionUpgrade44to45(VersionUpgrade):
|
||||
def __init__(self) -> None:
|
||||
"""
|
||||
Creates the version upgrade plug-in from 4.4 to 4.5.
|
||||
|
||||
In this case the plug-in will also check for stacks that need to be
|
||||
deleted.
|
||||
"""
|
||||
|
||||
# Only delete hidden stacks when upgrading from version 4.4. Not 4.3 or 4.5, just when you're starting out from 4.4.
|
||||
# If you're starting from an earlier version, you can't have had the bug that produces too many hidden stacks (https://github.com/Ultimaker/Cura/issues/6731).
|
||||
# If you're starting from a later version, the bug was already fixed.
|
||||
data_storage_root = os.path.dirname(Resources.getDataStoragePath())
|
||||
folders = set(os.listdir(data_storage_root)) # All version folders.
|
||||
folders = set(filter(lambda p: re.fullmatch(r"\d+\.\d+", p), folders)) # Only folders with a correct version number as name.
|
||||
folders.difference_update({os.path.basename(Resources.getDataStoragePath())}) # Remove current version from candidates (since the folder was just copied).
|
||||
if folders:
|
||||
latest_version = max(folders, key = Version) # Sort them by semantic version numbering.
|
||||
if latest_version == "4.4":
|
||||
self.removeHiddenStacks()
|
||||
|
||||
def removeHiddenStacks(self) -> None:
|
||||
"""
|
||||
If starting the upgrade from 4.4, this will remove any hidden printer
|
||||
stacks from the configuration folder as well as all of the user profiles
|
||||
and definition changes profiles.
|
||||
|
||||
This will ONLY run when upgrading from 4.4, not when e.g. upgrading from
|
||||
4.3 to 4.6 (through 4.4). This is because it's to fix a bug
|
||||
(https://github.com/Ultimaker/Cura/issues/6731) that occurred in 4.4
|
||||
only, so only there will it have hidden stacks that need to be deleted.
|
||||
If people upgrade from 4.3 they don't need to be deleted. If people
|
||||
upgrade from 4.5 they have already been deleted previously or never got
|
||||
the broken hidden stacks.
|
||||
"""
|
||||
Logger.log("d", "Removing all hidden container stacks.")
|
||||
hidden_global_stacks = set() # Which global stacks have been found? We'll delete anything referred to by these. Set of stack IDs.
|
||||
hidden_extruder_stacks = set() # Which extruder stacks refer to the hidden global profiles?
|
||||
hidden_instance_containers = set() # Which instance containers are referred to by the hidden stacks?
|
||||
exclude_directories = {"plugins"}
|
||||
|
||||
# First find all of the hidden container stacks.
|
||||
data_storage = Resources.getDataStoragePath()
|
||||
for root, dirs, files in os.walk(data_storage):
|
||||
dirs[:] = [dir for dir in dirs if dir not in exclude_directories]
|
||||
for filename in fnmatch.filter(files, "*.global.cfg"):
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
try:
|
||||
parser.read(os.path.join(root, filename))
|
||||
except OSError: # File not found or insufficient rights.
|
||||
continue
|
||||
except configparser.Error: # Invalid file format.
|
||||
continue
|
||||
if "metadata" in parser and "hidden" in parser["metadata"] and parser["metadata"]["hidden"] == "True":
|
||||
stack_id = urllib.parse.unquote_plus(os.path.basename(filename).split(".")[0])
|
||||
hidden_global_stacks.add(stack_id)
|
||||
# The user container and definition changes container are specific to this stack. We need to delete those too.
|
||||
if "containers" in parser:
|
||||
if "0" in parser["containers"]: # User container.
|
||||
hidden_instance_containers.add(parser["containers"]["0"])
|
||||
if "6" in parser["containers"]: # Definition changes container.
|
||||
hidden_instance_containers.add(parser["containers"]["6"])
|
||||
os.remove(os.path.join(root, filename))
|
||||
|
||||
# Walk a second time to find all extruder stacks referring to these hidden container stacks.
|
||||
for root, dirs, files in os.walk(data_storage):
|
||||
dirs[:] = [dir for dir in dirs if dir not in exclude_directories]
|
||||
for filename in fnmatch.filter(files, "*.extruder.cfg"):
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
try:
|
||||
parser.read(os.path.join(root, filename))
|
||||
except OSError: # File not found or insufficient rights.
|
||||
continue
|
||||
except configparser.Error: # Invalid file format.
|
||||
continue
|
||||
if "metadata" in parser and "machine" in parser["metadata"] and parser["metadata"]["machine"] in hidden_global_stacks:
|
||||
stack_id = urllib.parse.unquote_plus(os.path.basename(filename).split(".")[0])
|
||||
hidden_extruder_stacks.add(stack_id)
|
||||
# The user container and definition changes container are specific to this stack. We need to delete those too.
|
||||
if "containers" in parser:
|
||||
if "0" in parser["containers"]: # User container.
|
||||
hidden_instance_containers.add(parser["containers"]["0"])
|
||||
if "6" in parser["containers"]: # Definition changes container.
|
||||
hidden_instance_containers.add(parser["containers"]["6"])
|
||||
os.remove(os.path.join(root, filename))
|
||||
|
||||
# Walk a third time to remove all instance containers that are referred to by either of those.
|
||||
for root, dirs, files in os.walk(data_storage):
|
||||
dirs[:] = [dir for dir in dirs if dir not in exclude_directories]
|
||||
for filename in fnmatch.filter(files, "*.inst.cfg"):
|
||||
container_id = urllib.parse.unquote_plus(os.path.basename(filename).split(".")[0])
|
||||
if container_id in hidden_instance_containers:
|
||||
try:
|
||||
os.remove(os.path.join(root, filename))
|
||||
except OSError: # Is a directory, file not found, or insufficient rights.
|
||||
continue
|
||||
|
||||
def getCfgVersion(self, serialised: str) -> int:
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
parser.read_string(serialised)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"description": "Upgrades configurations from Cura 4.4 to Cura 4.5.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"name": "X3D Reader",
|
||||
"author": "Seva Alekseyev",
|
||||
"author": "Seva Alekseyev, Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for reading X3D files.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides the X-Ray view.",
|
||||
"api": "7.0",
|
||||
"api": "7.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
15
plugins/XmlMaterialProfile/PluginInfo.py
Normal file
15
plugins/XmlMaterialProfile/PluginInfo.py
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
from UM.PluginObject import PluginObject
|
||||
|
||||
|
||||
class PluginInfo(PluginObject):
|
||||
__instance = None # type: PluginInfo
|
||||
|
||||
def __init__(self, *args, **kwags):
|
||||
super().__init__(*args, **kwags)
|
||||
if PluginInfo.__instance is not None:
|
||||
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
|
||||
PluginInfo.__instance = self
|
||||
|
||||
@classmethod
|
||||
def getInstance(cls, *args, **kwargs) -> "PluginInfo":
|
||||
return cls.__instance
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue