mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-10 08:17:49 -06:00
Merge branch 'master' into ppscript_pre_secure
This commit is contained in:
commit
7f89c7e740
484 changed files with 98147 additions and 70366 deletions
|
@ -86,7 +86,7 @@ class ThreeMFReader(MeshReader):
|
|||
|
||||
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
|
||||
# \returns Scene node.
|
||||
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode) -> Optional[SceneNode]:
|
||||
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode, file_name: str = "") -> Optional[SceneNode]:
|
||||
self._object_count += 1
|
||||
node_name = "Object %s" % self._object_count
|
||||
|
||||
|
@ -104,6 +104,10 @@ class ThreeMFReader(MeshReader):
|
|||
vertices = numpy.resize(data, (int(data.size / 3), 3))
|
||||
mesh_builder.setVertices(vertices)
|
||||
mesh_builder.calculateNormals(fast=True)
|
||||
if file_name:
|
||||
# The filename is used to give the user the option to reload the file if it is changed on disk
|
||||
# It is only set for the root node of the 3mf file
|
||||
mesh_builder.setFileName(file_name)
|
||||
mesh_data = mesh_builder.build()
|
||||
|
||||
if len(mesh_data.getVertices()):
|
||||
|
@ -171,7 +175,7 @@ class ThreeMFReader(MeshReader):
|
|||
scene_3mf = parser.parse(archive.open("3D/3dmodel.model").read())
|
||||
self._unit = scene_3mf.getUnit()
|
||||
for node in scene_3mf.getSceneNodes():
|
||||
um_node = self._convertSavitarNodeToUMNode(node)
|
||||
um_node = self._convertSavitarNodeToUMNode(node, file_name)
|
||||
if um_node is None:
|
||||
continue
|
||||
# compensate for original center position, if object(s) is/are not around its zero position
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -118,7 +118,7 @@ class AMFReader(MeshReader):
|
|||
mesh.merge_vertices()
|
||||
mesh.remove_unreferenced_vertices()
|
||||
mesh.fix_normals()
|
||||
mesh_data = self._toMeshData(mesh)
|
||||
mesh_data = self._toMeshData(mesh, file_name)
|
||||
|
||||
new_node = CuraSceneNode()
|
||||
new_node.setSelectable(True)
|
||||
|
@ -147,7 +147,13 @@ class AMFReader(MeshReader):
|
|||
|
||||
return group_node
|
||||
|
||||
def _toMeshData(self, tri_node: trimesh.base.Trimesh) -> MeshData:
|
||||
## Converts a Trimesh to Uranium's MeshData.
|
||||
# \param tri_node A Trimesh containing the contents of a file that was
|
||||
# just read.
|
||||
# \param file_name The full original filename used to watch for changes
|
||||
# \return Mesh data from the Trimesh in a way that Uranium can understand
|
||||
# it.
|
||||
def _toMeshData(self, tri_node: trimesh.base.Trimesh, file_name: str = "") -> MeshData:
|
||||
tri_faces = tri_node.faces
|
||||
tri_vertices = tri_node.vertices
|
||||
|
||||
|
@ -169,5 +175,5 @@ class AMFReader(MeshReader):
|
|||
indices = numpy.asarray(indices, dtype = numpy.int32)
|
||||
normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count)
|
||||
|
||||
mesh_data = MeshData(vertices = vertices, indices = indices, normals = normals)
|
||||
mesh_data = MeshData(vertices = vertices, indices = indices, normals = normals,file_name = file_name)
|
||||
return mesh_data
|
||||
|
|
|
@ -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.")
|
||||
|
@ -409,7 +421,10 @@ class CuraEngineBackend(QObject, Backend):
|
|||
|
||||
if job.getResult() == StartJobResult.NothingToSlice:
|
||||
if self._application.platformActivity:
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume or are assigned to a disabled extruder. Please scale or rotate models to fit, or enable an extruder."),
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Please review settings and check if your models:"
|
||||
"\n- Fit within the build volume"
|
||||
"\n- Are assigned to an enabled extruder"
|
||||
"\n- Are not all set as modifier meshes"),
|
||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||
self._error_message.show()
|
||||
self.setState(BackendState.Error)
|
||||
|
|
|
@ -422,13 +422,14 @@ class StartSliceJob(Job):
|
|||
|
||||
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
|
||||
start_gcode = settings["machine_start_gcode"]
|
||||
# Remove all the comments from the start g-code
|
||||
start_gcode = re.sub(r";.+?(\n|$)", "\n", start_gcode)
|
||||
bed_temperature_settings = ["material_bed_temperature", "material_bed_temperature_layer_0"]
|
||||
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr}
|
||||
settings["material_bed_temp_prepend"] = re.search(pattern, start_gcode) == None
|
||||
print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"]
|
||||
pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr}
|
||||
settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) == None
|
||||
|
||||
# Replace the setting tokens in start and end g-code.
|
||||
# Use values from the first used extruder by default so we get the expected temperatures
|
||||
initial_extruder_stack = CuraApplication.getInstance().getExtruderManager().getUsedExtruderStacks()[0]
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ class FirmwareUpdateCheckerJob(Job):
|
|||
try:
|
||||
# CURA-6698 Create an SSL context and use certifi CA certificates for verification.
|
||||
context = ssl.SSLContext(protocol = ssl.PROTOCOL_TLSv1_2)
|
||||
context.verify_mode = ssl.CERT_REQUIRED
|
||||
context.load_verify_locations(cafile = certifi.where())
|
||||
|
||||
request = urllib.request.Request(url, headers = self._headers)
|
||||
|
|
|
@ -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
|
||||
|
@ -169,6 +169,9 @@ class FlavorParser:
|
|||
# A threshold is set to avoid weird paths in the GCode
|
||||
if line_width > 1.2:
|
||||
return 0.35
|
||||
# Prevent showing infinitely wide lines
|
||||
if line_width < 0.0:
|
||||
return 0.0
|
||||
return line_width
|
||||
|
||||
def _gCode0(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
|
||||
|
@ -235,7 +238,7 @@ class FlavorParser:
|
|||
def _gCode92(self, position: Position, params: PositionOptional, path: List[List[Union[float, int]]]) -> Position:
|
||||
if params.e is not None:
|
||||
# Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
|
||||
self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
|
||||
self._extrusion_length_offset[self._extruder_number] = position.e[self._extruder_number] - params.e
|
||||
position.e[self._extruder_number] = params.e
|
||||
self._previous_extrusion_value = params.e
|
||||
else:
|
||||
|
@ -258,16 +261,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:])
|
||||
elif item[0] == "Y":
|
||||
y = float(item[1:])
|
||||
elif item[0] == "Z":
|
||||
z = float(item[1:])
|
||||
elif item[0] == "F":
|
||||
f = float(item[1:]) / 60
|
||||
elif 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"
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import numpy
|
||||
|
@ -96,7 +96,7 @@ class ImageReader(MeshReader):
|
|||
texel_width = 1.0 / (width_minus_one) * scale_vector.x
|
||||
texel_height = 1.0 / (height_minus_one) * scale_vector.z
|
||||
|
||||
height_data = numpy.zeros((height, width), dtype=numpy.float32)
|
||||
height_data = numpy.zeros((height, width), dtype = numpy.float32)
|
||||
|
||||
for x in range(0, width):
|
||||
for y in range(0, height):
|
||||
|
@ -112,7 +112,7 @@ class ImageReader(MeshReader):
|
|||
height_data = 1 - height_data
|
||||
|
||||
for _ in range(0, blur_iterations):
|
||||
copy = numpy.pad(height_data, ((1, 1), (1, 1)), mode= "edge")
|
||||
copy = numpy.pad(height_data, ((1, 1), (1, 1)), mode = "edge")
|
||||
|
||||
height_data += copy[1:-1, 2:]
|
||||
height_data += copy[1:-1, :-2]
|
||||
|
@ -165,7 +165,7 @@ class ImageReader(MeshReader):
|
|||
offsetsz = numpy.array(offsetsz, numpy.float32).reshape(-1, 1) * texel_height
|
||||
|
||||
# offsets for each texel quad
|
||||
heightmap_vertex_offsets = numpy.concatenate([offsetsx, numpy.zeros((offsetsx.shape[0], offsetsx.shape[1]), dtype=numpy.float32), offsetsz], 1)
|
||||
heightmap_vertex_offsets = numpy.concatenate([offsetsx, numpy.zeros((offsetsx.shape[0], offsetsx.shape[1]), dtype = numpy.float32), offsetsz], 1)
|
||||
heightmap_vertices += heightmap_vertex_offsets.repeat(6, 0).reshape(-1, 6, 3)
|
||||
|
||||
# apply height data to y values
|
||||
|
@ -174,7 +174,7 @@ class ImageReader(MeshReader):
|
|||
heightmap_vertices[:, 2, 1] = heightmap_vertices[:, 3, 1] = height_data[1:, 1:].reshape(-1)
|
||||
heightmap_vertices[:, 4, 1] = height_data[:-1, 1:].reshape(-1)
|
||||
|
||||
heightmap_indices = numpy.array(numpy.mgrid[0:heightmap_face_count * 3], dtype=numpy.int32).reshape(-1, 3)
|
||||
heightmap_indices = numpy.array(numpy.mgrid[0:heightmap_face_count * 3], dtype = numpy.int32).reshape(-1, 3)
|
||||
|
||||
mesh._vertices[0:(heightmap_vertices.size // 3), :] = heightmap_vertices.reshape(-1, 3)
|
||||
mesh._indices[0:(heightmap_indices.size // 3), :] = heightmap_indices
|
||||
|
@ -223,7 +223,7 @@ class ImageReader(MeshReader):
|
|||
mesh.addFaceByPoints(geo_width, 0, y, geo_width, 0, ny, geo_width, he1, ny)
|
||||
mesh.addFaceByPoints(geo_width, he1, ny, geo_width, he0, y, geo_width, 0, y)
|
||||
|
||||
mesh.calculateNormals(fast=True)
|
||||
mesh.calculateNormals(fast = True)
|
||||
|
||||
scene_node.setMeshData(mesh.build())
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
|
@ -33,9 +33,9 @@ class ImageReaderUI(QObject):
|
|||
self.base_height = 0.4
|
||||
self.peak_height = 2.5
|
||||
self.smoothing = 1
|
||||
self.lighter_is_higher = False;
|
||||
self.use_transparency_model = True;
|
||||
self.transmittance_1mm = 50.0; # based on pearl PLA
|
||||
self.lighter_is_higher = False
|
||||
self.use_transparency_model = True
|
||||
self.transmittance_1mm = 50.0 # based on pearl PLA
|
||||
|
||||
self._ui_lock = threading.Lock()
|
||||
self._cancelled = False
|
||||
|
@ -85,7 +85,7 @@ class ImageReaderUI(QObject):
|
|||
Logger.log("d", "Creating ImageReader config UI")
|
||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml")
|
||||
self._ui_view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint);
|
||||
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint)
|
||||
self._disable_size_callbacks = False
|
||||
|
||||
@pyqtSlot()
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -73,6 +73,8 @@ class ModelChecker(QObject, Extension):
|
|||
# Check node material shrinkage and bounding box size
|
||||
for node in self.sliceableNodes():
|
||||
node_extruder_position = node.callDecoration("getActiveExtruderPosition")
|
||||
if node_extruder_position is None:
|
||||
continue
|
||||
|
||||
# This function can be triggered in the middle of a machine change, so do not proceed if the machine change
|
||||
# has not done yet.
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.SettingInstance import SettingInstance
|
||||
from UM.Logger import Logger
|
||||
|
@ -24,6 +25,8 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
|
|||
self._node = None
|
||||
self._stack = None
|
||||
|
||||
PluginRegistry.getInstance().getPluginObject("PerObjectSettingsTool").visibility_handler = self
|
||||
|
||||
# this is a set of settings that will be skipped if the user chooses to reset.
|
||||
self._skip_reset_setting_set = set()
|
||||
|
||||
|
@ -68,7 +71,7 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
|
|||
|
||||
# Add all instances that are not added, but are in visibility list
|
||||
for item in visible:
|
||||
if not settings.getInstance(item): # Setting was not added already.
|
||||
if settings.getInstance(item) is None: # Setting was not added already.
|
||||
definition = self._stack.getSettingDefinition(item)
|
||||
if definition:
|
||||
new_instance = SettingInstance(definition, settings)
|
||||
|
|
|
@ -49,18 +49,6 @@ Item
|
|||
visibility_handler.addSkipResetSetting(currentMeshType)
|
||||
}
|
||||
|
||||
function setOverhangsMeshType()
|
||||
{
|
||||
if (infillOnlyCheckbox.checked)
|
||||
{
|
||||
setMeshType(infillMeshType)
|
||||
}
|
||||
else
|
||||
{
|
||||
setMeshType(cuttingMeshType)
|
||||
}
|
||||
}
|
||||
|
||||
function setMeshType(type)
|
||||
{
|
||||
UM.ActiveTool.setProperty("MeshType", type)
|
||||
|
@ -140,26 +128,43 @@ Item
|
|||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
CheckBox
|
||||
|
||||
ComboBox
|
||||
{
|
||||
id: infillOnlyCheckbox
|
||||
id: infillOnlyComboBox
|
||||
width: parent.width / 2 - UM.Theme.getSize("default_margin").width
|
||||
|
||||
text: catalog.i18nc("@action:checkbox", "Infill only");
|
||||
model: ListModel
|
||||
{
|
||||
id: infillOnlyComboBoxModel
|
||||
|
||||
style: UM.Theme.styles.checkbox;
|
||||
Component.onCompleted: {
|
||||
append({ text: catalog.i18nc("@item:inlistbox", "Infill mesh only") })
|
||||
append({ text: catalog.i18nc("@item:inlistbox", "Cutting mesh") })
|
||||
}
|
||||
}
|
||||
|
||||
visible: currentMeshType === infillMeshType || currentMeshType === cuttingMeshType
|
||||
onClicked: setOverhangsMeshType()
|
||||
|
||||
|
||||
onActivated:
|
||||
{
|
||||
if (index == 0){
|
||||
setMeshType(infillMeshType)
|
||||
} else {
|
||||
setMeshType(cuttingMeshType)
|
||||
}
|
||||
}
|
||||
|
||||
Binding
|
||||
{
|
||||
target: infillOnlyCheckbox
|
||||
property: "checked"
|
||||
value: currentMeshType === infillMeshType
|
||||
target: infillOnlyComboBox
|
||||
property: "currentIndex"
|
||||
value: currentMeshType === infillMeshType ? 0 : 1
|
||||
}
|
||||
}
|
||||
|
||||
Column // Settings Dialog
|
||||
Column // List of selected Settings to override for the selected object
|
||||
{
|
||||
// This is to ensure that the panel is first increasing in size up to 200 and then shows a scrollbar.
|
||||
// It kinda looks ugly otherwise (big panel, no content on it)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Tool import Tool
|
||||
from UM.Scene.Selection import Selection
|
||||
|
@ -22,14 +23,13 @@ class PerObjectSettingsTool(Tool):
|
|||
|
||||
self._multi_extrusion = False
|
||||
self._single_model_selected = False
|
||||
self.visibility_handler = None
|
||||
|
||||
Selection.selectionChanged.connect(self.propertyChanged)
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._onGlobalContainerChanged()
|
||||
Selection.selectionChanged.connect(self._updateEnabled)
|
||||
|
||||
|
||||
def event(self, event):
|
||||
super().event(event)
|
||||
if event.type == Event.MousePressEvent and self._controller.getToolsEnabled():
|
||||
|
@ -68,7 +68,8 @@ class PerObjectSettingsTool(Tool):
|
|||
|
||||
## Returns True when the mesh_type was changed, False when current mesh_type == mesh_type
|
||||
def setMeshType(self, mesh_type: str) -> bool:
|
||||
if self.getMeshType() == mesh_type:
|
||||
old_mesh_type = self.getMeshType()
|
||||
if old_mesh_type == mesh_type:
|
||||
return False
|
||||
|
||||
selected_object = Selection.getSelectedObject(0)
|
||||
|
@ -81,6 +82,7 @@ class PerObjectSettingsTool(Tool):
|
|||
selected_object.addDecorator(SettingOverrideDecorator())
|
||||
stack = selected_object.callDecoration("getStack")
|
||||
|
||||
settings_visibility_changed = False
|
||||
settings = stack.getTop()
|
||||
for property_key in ["infill_mesh", "cutting_mesh", "support_mesh", "anti_overhang_mesh"]:
|
||||
if property_key != mesh_type:
|
||||
|
@ -94,6 +96,23 @@ class PerObjectSettingsTool(Tool):
|
|||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||
settings.addInstance(new_instance)
|
||||
|
||||
for property_key in ["top_bottom_thickness", "wall_thickness"]:
|
||||
if mesh_type == "infill_mesh":
|
||||
if settings.getInstance(property_key) is None:
|
||||
definition = stack.getSettingDefinition(property_key)
|
||||
new_instance = SettingInstance(definition, settings)
|
||||
new_instance.setProperty("value", 0)
|
||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||
settings.addInstance(new_instance)
|
||||
settings_visibility_changed = True
|
||||
|
||||
elif old_mesh_type == "infill_mesh" and settings.getInstance(property_key) and settings.getProperty(property_key, "value") == 0:
|
||||
settings.removeInstance(property_key)
|
||||
settings_visibility_changed = True
|
||||
|
||||
if settings_visibility_changed:
|
||||
self.visibility_handler.forceVisibilityChanged()
|
||||
|
||||
self.propertyChanged.emit()
|
||||
return True
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -484,15 +484,53 @@ UM.Dialog
|
|||
onClicked: dialog.accept()
|
||||
}
|
||||
|
||||
Cura.SecondaryButton
|
||||
Item
|
||||
{
|
||||
objectName: "postProcessingSaveAreaButton"
|
||||
visible: activeScriptsList.count > 0
|
||||
height: UM.Theme.getSize("action_button").height
|
||||
width: height
|
||||
tooltip: catalog.i18nc("@info:tooltip", "Change active post-processing scripts")
|
||||
onClicked: dialog.show()
|
||||
iconSource: "postprocessing.svg"
|
||||
fixedWidthMode: true
|
||||
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
height: UM.Theme.getSize("action_button").height
|
||||
tooltip:
|
||||
{
|
||||
var tipText = catalog.i18nc("@info:tooltip", "Change active post-processing scripts.");
|
||||
if (activeScriptsList.count > 0)
|
||||
{
|
||||
tipText += "<br><br>" + catalog.i18ncp("@info:tooltip",
|
||||
"The following script is active:",
|
||||
"The following scripts are active:",
|
||||
activeScriptsList.count
|
||||
) + "<ul>";
|
||||
for(var i = 0; i < activeScriptsList.count; i++)
|
||||
{
|
||||
tipText += "<li>" + manager.getScriptLabelByKey(manager.scriptList[i]) + "</li>";
|
||||
}
|
||||
tipText += "</ul>";
|
||||
}
|
||||
return tipText
|
||||
}
|
||||
toolTipContentAlignment: Cura.ToolTip.ContentAlignment.AlignLeft
|
||||
onClicked: dialog.show()
|
||||
iconSource: "postprocessing.svg"
|
||||
fixedWidthMode: false
|
||||
}
|
||||
|
||||
Cura.NotificationIcon
|
||||
{
|
||||
id: activeScriptCountIcon
|
||||
visible: activeScriptsList.count > 0
|
||||
anchors
|
||||
{
|
||||
top: parent.top
|
||||
right: parent.right
|
||||
rightMargin: (-0.5 * width) | 0
|
||||
topMargin: (-0.5 * height) | 0
|
||||
}
|
||||
|
||||
labelText: activeScriptsList.count
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -6,14 +6,22 @@
|
|||
#Authors of the 2-1 ColorMix plug-in / script:
|
||||
# Written by John Hryb - john.hryb.4@gmail.com
|
||||
|
||||
##history / change-log:
|
||||
##V1.0.0
|
||||
|
||||
## Uses -
|
||||
## M163 - Set Mix Factor
|
||||
## M164 - Save Mix - saves to T3 as a unique mix
|
||||
|
||||
import re #To perform the search and replace.
|
||||
#history / change-log:
|
||||
#V1.0.0 - Initial
|
||||
#V1.1.0 -
|
||||
# additions:
|
||||
#Object number - To select individual models or all when using "one at a time" print sequence
|
||||
#V1.2.0
|
||||
# fixed layer heights Cura starts at 1 while G-code starts at 0
|
||||
# removed notes
|
||||
# changed Units of measurement to Units
|
||||
#V1.2.1
|
||||
# Fixed mm bug when not in multiples of layer height
|
||||
# Uses -
|
||||
# M163 - Set Mix Factor
|
||||
# M164 - Save Mix - saves to T2 as a unique mix
|
||||
|
||||
import re #To perform the search and replace.
|
||||
from ..Script import Script
|
||||
|
||||
class ColorMix(Script):
|
||||
|
@ -22,20 +30,28 @@ class ColorMix(Script):
|
|||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name":"ColorMix 2-1",
|
||||
"name":"ColorMix 2-1 V1.2.1",
|
||||
"key":"ColorMix 2-1",
|
||||
"metadata": {},
|
||||
"version": 2,
|
||||
"settings":
|
||||
{
|
||||
"measurement_units":
|
||||
"units_of_measurement":
|
||||
{
|
||||
"label": "Units of measurement",
|
||||
"label": "Units",
|
||||
"description": "Input value as mm or layer number.",
|
||||
"type": "enum",
|
||||
"options": {"mm":"mm","layer":"Layer"},
|
||||
"default_value": "layer"
|
||||
},
|
||||
"object_number":
|
||||
{
|
||||
"label": "Object Number",
|
||||
"description": "Select model to apply to for print one at a time print sequence. 0 = everything",
|
||||
"type": "int",
|
||||
"default_value": 0,
|
||||
"minimum_value": "0"
|
||||
},
|
||||
"start_height":
|
||||
{
|
||||
"label": "Start Height",
|
||||
|
@ -59,10 +75,10 @@ class ColorMix(Script):
|
|||
"type": "float",
|
||||
"default_value": 0,
|
||||
"minimum_value": "0",
|
||||
"minimum_value_warning": "0.1",
|
||||
"enabled": "c_behavior == 'blend_value'"
|
||||
"minimum_value_warning": "start_height",
|
||||
"enabled": "behavior == 'blend_value'"
|
||||
},
|
||||
"mix_start_ratio":
|
||||
"mix_start":
|
||||
{
|
||||
"label": "Start mix ratio",
|
||||
"description": "First extruder percentage 0-100",
|
||||
|
@ -72,7 +88,7 @@ class ColorMix(Script):
|
|||
"minimum_value_warning": "0",
|
||||
"maximum_value_warning": "100"
|
||||
},
|
||||
"mix_finish_ratio":
|
||||
"mix_finish":
|
||||
{
|
||||
"label": "End mix ratio",
|
||||
"description": "First extruder percentage 0-100 to finish blend",
|
||||
|
@ -81,14 +97,7 @@ class ColorMix(Script):
|
|||
"minimum_value": "0",
|
||||
"minimum_value_warning": "0",
|
||||
"maximum_value_warning": "100",
|
||||
"enabled": "c_behavior == 'blend_value'"
|
||||
},
|
||||
"notes":
|
||||
{
|
||||
"label": "Notes",
|
||||
"description": "A spot to put a note",
|
||||
"type": "str",
|
||||
"default_value": ""
|
||||
"enabled": "behavior == 'blend_value'"
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
@ -112,52 +121,53 @@ class ColorMix(Script):
|
|||
return default
|
||||
|
||||
def execute(self, data):
|
||||
#get user variables
|
||||
firstHeight = 0.0
|
||||
secondHeight = 0.0
|
||||
firstMix = 0.0
|
||||
SecondMix = 0.0
|
||||
|
||||
firstHeight = self.getSettingValueByKey("start_height")
|
||||
secondHeight = self.getSettingValueByKey("finish_height")
|
||||
firstMix = self.getSettingValueByKey("mix_start_ratio")
|
||||
SecondMix = self.getSettingValueByKey("mix_finish_ratio")
|
||||
|
||||
#locals
|
||||
layer = 0
|
||||
|
||||
firstMix = self.getSettingValueByKey("mix_start")
|
||||
secondMix = self.getSettingValueByKey("mix_finish")
|
||||
modelOfInterest = self.getSettingValueByKey("object_number")
|
||||
|
||||
#get layer height
|
||||
layerHeight = .2
|
||||
layerHeight = 0
|
||||
for active_layer in data:
|
||||
lines = active_layer.split("\n")
|
||||
for line in lines:
|
||||
if ";Layer height: " in line:
|
||||
layerHeight = self.getValue(line, ";Layer height: ", layerHeight)
|
||||
break
|
||||
if layerHeight != 0:
|
||||
break
|
||||
|
||||
#default layerHeight if not found
|
||||
if layerHeight == 0:
|
||||
layerHeight = .2
|
||||
|
||||
#get layers to use
|
||||
startLayer = 0
|
||||
endLayer = 0
|
||||
if self.getSettingValueByKey("measurement_units") == "mm":
|
||||
if firstHeight == 0:
|
||||
startLayer = 0
|
||||
else:
|
||||
startLayer = firstHeight / layerHeight
|
||||
if secondHeight == 0:
|
||||
endLayer = 0
|
||||
else:
|
||||
endLayer = secondHeight / layerHeight
|
||||
else: #layer height
|
||||
startLayer = firstHeight
|
||||
endLayer = secondHeight
|
||||
if self.getSettingValueByKey("units_of_measurement") == "mm":
|
||||
startLayer = round(firstHeight / layerHeight)
|
||||
endLayer = round(secondHeight / layerHeight)
|
||||
else: #layer height shifts down by one for g-code
|
||||
if firstHeight <= 0:
|
||||
firstHeight = 1
|
||||
if secondHeight <= 0:
|
||||
secondHeight = 1
|
||||
startLayer = firstHeight - 1
|
||||
endLayer = secondHeight - 1
|
||||
#see if one-shot
|
||||
if self.getSettingValueByKey("behavior") == "fixed_value":
|
||||
endLayer = startLayer
|
||||
firstExtruderIncrements = 0
|
||||
else: #blend
|
||||
firstExtruderIncrements = (SecondMix - firstMix) / (endLayer - startLayer)
|
||||
firstExtruderIncrements = (secondMix - firstMix) / (endLayer - startLayer)
|
||||
firstExtruderValue = 0
|
||||
index = 0
|
||||
|
||||
#start scanning
|
||||
layer = -1
|
||||
modelNumber = 0
|
||||
for active_layer in data:
|
||||
modified_gcode = ""
|
||||
lineIndex = 0;
|
||||
|
@ -169,22 +179,30 @@ class ColorMix(Script):
|
|||
# find current layer
|
||||
if ";LAYER:" in line:
|
||||
layer = self.getValue(line, ";LAYER:", layer)
|
||||
if (layer >= startLayer) and (layer <= endLayer): #find layers of interest
|
||||
if lines[lineIndex + 4] == "T2": #check if needing to delete old data
|
||||
del lines[(lineIndex + 1):(lineIndex + 5)]
|
||||
firstExtruderValue = int(((layer - startLayer) * firstExtruderIncrements) + firstMix)
|
||||
if firstExtruderValue == 100:
|
||||
modified_gcode += "M163 S0 P1\n"
|
||||
modified_gcode += "M163 S1 P0\n"
|
||||
elif firstExtruderValue == 0:
|
||||
modified_gcode += "M163 S0 P0\n"
|
||||
modified_gcode += "M163 S1 P1\n"
|
||||
else:
|
||||
modified_gcode += "M163 S0 P0.{:02d}\n".format(firstExtruderValue)
|
||||
modified_gcode += "M163 S1 P0.{:02d}\n".format(100 - firstExtruderValue)
|
||||
modified_gcode += "M164 S2\n"
|
||||
modified_gcode += "T2\n"
|
||||
#get model number by layer 0 repeats
|
||||
if layer == 0:
|
||||
modelNumber = modelNumber + 1
|
||||
#search for layers to manipulate
|
||||
if (layer >= startLayer) and (layer <= endLayer):
|
||||
#make sure correct model is selected
|
||||
if (modelOfInterest == 0) or (modelOfInterest == modelNumber):
|
||||
#Delete old data if required
|
||||
if lines[lineIndex + 4] == "T2":
|
||||
del lines[(lineIndex + 1):(lineIndex + 5)]
|
||||
#add mixing commands
|
||||
firstExtruderValue = int(((layer - startLayer) * firstExtruderIncrements) + firstMix)
|
||||
if firstExtruderValue == 100:
|
||||
modified_gcode += "M163 S0 P1\n"
|
||||
modified_gcode += "M163 S1 P0\n"
|
||||
elif firstExtruderValue == 0:
|
||||
modified_gcode += "M163 S0 P0\n"
|
||||
modified_gcode += "M163 S1 P1\n"
|
||||
else:
|
||||
modified_gcode += "M163 S0 P0.{:02d}\n".format(firstExtruderValue)
|
||||
modified_gcode += "M163 S1 P0.{:02d}\n".format(100 - firstExtruderValue)
|
||||
modified_gcode += "M164 S2\n"
|
||||
modified_gcode += "T2\n"
|
||||
lineIndex += 1 #for deleting index
|
||||
data[index] = modified_gcode
|
||||
index += 1
|
||||
return data
|
||||
return data
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
}
|
|
@ -47,7 +47,10 @@ class WindowsRemovableDrivePlugin(RemovableDrivePlugin.RemovableDrivePlugin):
|
|||
def checkRemovableDrives(self):
|
||||
drives = {}
|
||||
|
||||
# The currently available disk drives, e.g.: bitmask = ...1100 <-- ...DCBA
|
||||
bitmask = ctypes.windll.kernel32.GetLogicalDrives()
|
||||
# Since we are ignoring drives A and B, the bitmask has has to shift twice to the right
|
||||
bitmask >>= 2
|
||||
# Check possible drive letters, from C to Z
|
||||
# Note: using ascii_uppercase because we do not want this to change with locale!
|
||||
# Skip A and B, since those drives are typically reserved for floppy disks.
|
||||
|
|
|
@ -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,6 +3,9 @@
|
|||
|
||||
from UM.Logger import LogOutput
|
||||
from typing import Set
|
||||
|
||||
from cura.CrashHandler import CrashHandler
|
||||
|
||||
try:
|
||||
from sentry_sdk import add_breadcrumb
|
||||
except ImportError:
|
||||
|
@ -10,8 +13,6 @@ except ImportError:
|
|||
from typing import Optional
|
||||
import os
|
||||
|
||||
home_dir = os.path.expanduser("~")
|
||||
|
||||
|
||||
class SentryLogger(LogOutput):
|
||||
# Sentry (https://sentry.io) is the service that Cura uses for logging crashes. This logger ensures that the
|
||||
|
@ -37,7 +38,7 @@ class SentryLogger(LogOutput):
|
|||
# \param message String containing message to be logged
|
||||
def log(self, log_type: str, message: str) -> None:
|
||||
level = self._translateLogType(log_type)
|
||||
message = self._pruneSensitiveData(message)
|
||||
message = CrashHandler.pruneSensitiveData(message)
|
||||
if level is None:
|
||||
if message not in self._show_once:
|
||||
level = self._translateLogType(log_type[0])
|
||||
|
@ -47,12 +48,6 @@ class SentryLogger(LogOutput):
|
|||
else:
|
||||
add_breadcrumb(level = level, message = message)
|
||||
|
||||
@staticmethod
|
||||
def _pruneSensitiveData(message):
|
||||
if home_dir in message:
|
||||
message = message.replace(home_dir, "<user_home>")
|
||||
return message
|
||||
|
||||
@staticmethod
|
||||
def _translateLogType(log_type: str) -> Optional[str]:
|
||||
return SentryLogger._levels.get(log_type)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -46,10 +46,19 @@ class SimulationPass(RenderPass):
|
|||
self._layer_view = layerview
|
||||
self._compatibility_mode = layerview.getCompatibilityMode()
|
||||
|
||||
def _updateLayerShaderValues(self):
|
||||
def render(self):
|
||||
if not self._layer_shader:
|
||||
if self._compatibility_mode:
|
||||
shader_filename = "layers.shader"
|
||||
shadow_shader_filename = "layers_shadow.shader"
|
||||
else:
|
||||
shader_filename = "layers3d.shader"
|
||||
shadow_shader_filename = "layers3d_shadow.shader"
|
||||
self._layer_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), shader_filename))
|
||||
self._layer_shadow_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), shadow_shader_filename))
|
||||
self._current_shader = self._layer_shader
|
||||
# Use extruder 0 if the extruder manager reports extruder index -1 (for single extrusion printers)
|
||||
self._layer_shader.setUniformValue("u_active_extruder",
|
||||
float(max(0, self._extruder_manager.activeExtruderIndex)))
|
||||
self._layer_shader.setUniformValue("u_active_extruder", float(max(0, self._extruder_manager.activeExtruderIndex)))
|
||||
if self._layer_view:
|
||||
self._layer_shader.setUniformValue("u_max_feedrate", self._layer_view.getMaxFeedrate())
|
||||
self._layer_shader.setUniformValue("u_min_feedrate", self._layer_view.getMinFeedrate())
|
||||
|
@ -62,7 +71,7 @@ class SimulationPass(RenderPass):
|
|||
self._layer_shader.setUniformValue("u_show_skin", self._layer_view.getShowSkin())
|
||||
self._layer_shader.setUniformValue("u_show_infill", self._layer_view.getShowInfill())
|
||||
else:
|
||||
# defaults
|
||||
#defaults
|
||||
self._layer_shader.setUniformValue("u_max_feedrate", 1)
|
||||
self._layer_shader.setUniformValue("u_min_feedrate", 0)
|
||||
self._layer_shader.setUniformValue("u_max_thickness", 1)
|
||||
|
@ -74,20 +83,6 @@ class SimulationPass(RenderPass):
|
|||
self._layer_shader.setUniformValue("u_show_skin", 1)
|
||||
self._layer_shader.setUniformValue("u_show_infill", 1)
|
||||
|
||||
def render(self):
|
||||
if not self._layer_shader:
|
||||
if self._compatibility_mode:
|
||||
shader_filename = "layers.shader"
|
||||
shadow_shader_filename = "layers_shadow.shader"
|
||||
else:
|
||||
shader_filename = "layers3d.shader"
|
||||
shadow_shader_filename = "layers3d_shadow.shader"
|
||||
self._layer_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), shader_filename))
|
||||
self._layer_shadow_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), shadow_shader_filename))
|
||||
self._current_shader = self._layer_shader
|
||||
|
||||
self._updateLayerShaderValues()
|
||||
|
||||
if not self._tool_handle_shader:
|
||||
self._tool_handle_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "toolhandle.shader"))
|
||||
|
||||
|
@ -102,6 +97,7 @@ class SimulationPass(RenderPass):
|
|||
nozzle_node = None
|
||||
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
|
||||
if isinstance(node, ToolHandle):
|
||||
tool_handle_batch.addItem(node.getWorldTransformation(), mesh = node.getSolidMesh())
|
||||
|
||||
|
@ -116,24 +112,29 @@ class SimulationPass(RenderPass):
|
|||
|
||||
# Render all layers below a certain number as line mesh instead of vertices.
|
||||
if self._layer_view._current_layer_num > -1 and ((not self._layer_view._only_show_top_layers) or (not self._layer_view.getCompatibilityMode())):
|
||||
start = self._layer_view.start_elements_index
|
||||
end = self._layer_view.end_elements_index
|
||||
index = self._layer_view._current_path_num
|
||||
offset = 0
|
||||
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:
|
||||
index -= polygon.data.size // 3 - offset
|
||||
offset = 1 # This is to avoid the first point when there is more than one polygon, since has the same value as the last point in the previous polygon
|
||||
continue
|
||||
# The head position is calculated and translated
|
||||
head_position = Vector(polygon.data[index + offset][0], polygon.data[index + offset][1],
|
||||
polygon.data[index + offset][2]) + node.getWorldPosition()
|
||||
break
|
||||
start = 0
|
||||
end = 0
|
||||
element_counts = layer_data.getElementCounts()
|
||||
for layer in sorted(element_counts.keys()):
|
||||
# In the current layer, we show just the indicated paths
|
||||
if layer == self._layer_view._current_layer_num:
|
||||
# We look for the position of the head, searching the point of the current path
|
||||
index = self._layer_view._current_path_num
|
||||
offset = 0
|
||||
for polygon in layer_data.getLayer(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:
|
||||
index -= polygon.data.size // 3 - offset
|
||||
offset = 1 # This is to avoid the first point when there is more than one polygon, since has the same value as the last point in the previous polygon
|
||||
continue
|
||||
# The head position is calculated and translated
|
||||
head_position = Vector(polygon.data[index+offset][0], polygon.data[index+offset][1], polygon.data[index+offset][2]) + node.getWorldPosition()
|
||||
break
|
||||
break
|
||||
if self._layer_view._minimum_layer_num > layer:
|
||||
start += element_counts[layer]
|
||||
end += element_counts[layer]
|
||||
|
||||
# Calculate the range of paths in the last layer
|
||||
current_layer_start = end
|
||||
|
|
|
@ -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
|
||||
|
@ -56,6 +56,8 @@ class SimulationView(CuraView):
|
|||
LAYER_VIEW_TYPE_FEEDRATE = 2
|
||||
LAYER_VIEW_TYPE_THICKNESS = 3
|
||||
|
||||
_no_layers_warning_preference = "view/no_layers_warning"
|
||||
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
|
@ -71,8 +73,6 @@ class SimulationView(CuraView):
|
|||
self._max_paths = 0
|
||||
self._current_path_num = 0
|
||||
self._minimum_path_num = 0
|
||||
self.start_elements_index = 0
|
||||
self.end_elements_index = 0
|
||||
self.currentLayerNumChanged.connect(self._onCurrentLayerNumChanged)
|
||||
|
||||
self._busy = False
|
||||
|
@ -116,8 +116,12 @@ 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"),
|
||||
option_text = catalog.i18nc("@info:option_text", "Do not show this message again"), option_state = False)
|
||||
self._slice_first_warning_message.optionToggled.connect(self._onDontAskMeAgain)
|
||||
CuraApplication.getInstance().getPreferences().addPreference(self._no_layers_warning_preference, True)
|
||||
|
||||
QtApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||
|
||||
|
@ -149,6 +153,7 @@ class SimulationView(CuraView):
|
|||
if self._activity == activity:
|
||||
return
|
||||
self._activity = activity
|
||||
self._updateSliceWarningVisibility()
|
||||
self.activityChanged.emit()
|
||||
|
||||
def getSimulationPass(self) -> SimulationPass:
|
||||
|
@ -245,7 +250,6 @@ class SimulationView(CuraView):
|
|||
self._minimum_layer_num = self._current_layer_num
|
||||
|
||||
self._startUpdateTopLayers()
|
||||
self.recalculateStartEndElements()
|
||||
|
||||
self.currentLayerNumChanged.emit()
|
||||
|
||||
|
@ -260,7 +264,7 @@ class SimulationView(CuraView):
|
|||
self._current_layer_num = self._minimum_layer_num
|
||||
|
||||
self._startUpdateTopLayers()
|
||||
self.recalculateStartEndElements()
|
||||
|
||||
self.currentLayerNumChanged.emit()
|
||||
|
||||
def setPath(self, value: int) -> None:
|
||||
|
@ -274,7 +278,6 @@ class SimulationView(CuraView):
|
|||
self._minimum_path_num = self._current_path_num
|
||||
|
||||
self._startUpdateTopLayers()
|
||||
self.recalculateStartEndElements()
|
||||
self.currentPathNumChanged.emit()
|
||||
|
||||
def setMinimumPath(self, value: int) -> None:
|
||||
|
@ -362,24 +365,6 @@ class SimulationView(CuraView):
|
|||
return 0.0 # If it's still max-float, there are no measurements. Use 0 then.
|
||||
return self._min_thickness
|
||||
|
||||
def recalculateStartEndElements(self):
|
||||
self.start_elements_index = 0
|
||||
self.end_elements_index = 0
|
||||
scene = self.getController().getScene()
|
||||
for node in DepthFirstIterator(scene.getRoot()): # type: ignore
|
||||
layer_data = node.callDecoration("getLayerData")
|
||||
if not layer_data:
|
||||
continue
|
||||
|
||||
# Found a the layer data!
|
||||
element_counts = layer_data.getElementCounts()
|
||||
for layer in sorted(element_counts.keys()):
|
||||
if layer == self._current_layer_num:
|
||||
break
|
||||
if self._minimum_layer_num > layer:
|
||||
self.start_elements_index += element_counts[layer]
|
||||
self.end_elements_index += element_counts[layer]
|
||||
|
||||
def getMaxThickness(self) -> float:
|
||||
return self._max_thickness
|
||||
|
||||
|
@ -543,11 +528,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)
|
||||
|
@ -599,7 +586,6 @@ class SimulationView(CuraView):
|
|||
def _startUpdateTopLayers(self) -> None:
|
||||
if not self._compatibility_mode:
|
||||
return
|
||||
self.recalculateStartEndElements()
|
||||
if self._top_layers_job:
|
||||
self._top_layers_job.finished.disconnect(self._updateCurrentLayerMesh)
|
||||
self._top_layers_job.cancel()
|
||||
|
@ -661,6 +647,16 @@ class SimulationView(CuraView):
|
|||
|
||||
self._updateWithPreferences()
|
||||
|
||||
def _updateSliceWarningVisibility(self):
|
||||
if not self.getActivity()\
|
||||
and not CuraApplication.getInstance().getPreferences().getValue("general/auto_slice")\
|
||||
and CuraApplication.getInstance().getPreferences().getValue(self._no_layers_warning_preference):
|
||||
self._slice_first_warning_message.show()
|
||||
else:
|
||||
self._slice_first_warning_message.hide()
|
||||
|
||||
def _onDontAskMeAgain(self, checked: bool) -> None:
|
||||
CuraApplication.getInstance().getPreferences().setValue(self._no_layers_warning_preference, not checked)
|
||||
|
||||
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({
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,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 |
|
@ -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
|
||||
{
|
||||
|
|
|
@ -42,7 +42,7 @@ Item
|
|||
rightMargin: UM.Theme.getSize("wide_margin").width
|
||||
}
|
||||
height: UM.Theme.getSize("toolbox_footer_button").height
|
||||
text: catalog.i18nc("@info:button", "Quit Cura")
|
||||
text: catalog.i18nc("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName)
|
||||
onClicked: toolbox.restart()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
@ -90,7 +92,7 @@ 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
|
||||
|
@ -125,26 +127,6 @@ UM.Dialog{
|
|||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
UM.TooltipArea
|
||||
{
|
||||
width: childrenRect.width;
|
||||
height: childrenRect.height;
|
||||
text: catalog.i18nc("@info:tooltip", "Dismisses the package and won't be shown in this dialog anymore")
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: packageIcon.verticalCenter
|
||||
Label
|
||||
{
|
||||
text: "(Dismiss)"
|
||||
font: UM.Theme.getFont("small")
|
||||
color: UM.Theme.getColor("text")
|
||||
MouseArea
|
||||
{
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
anchors.fill: parent
|
||||
onClicked: handler.dismissIncompatiblePackage(subscribedPackagesModel, model.package_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -152,14 +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,12 +4,12 @@
|
|||
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
|
||||
{
|
||||
|
@ -19,50 +19,90 @@ UM.Dialog
|
|||
minimumHeight: UM.Theme.getSize("license_window_minimum").height
|
||||
width: minimumWidth
|
||||
height: minimumHeight
|
||||
backgroundColor: UM.Theme.getColor("main_background")
|
||||
margin: screenScaleFactor * 10
|
||||
|
||||
Item
|
||||
ColumnLayout
|
||||
{
|
||||
anchors.fill: parent
|
||||
spacing: UM.Theme.getSize("thick_margin").height
|
||||
|
||||
UM.I18nCatalog{id: catalog; name: "cura"}
|
||||
|
||||
|
||||
Label
|
||||
{
|
||||
id: licenseHeader
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
text: licenseModel.headerText
|
||||
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: licenseHeader.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: licenseModel.licenseText
|
||||
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")
|
||||
leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width
|
||||
rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width
|
||||
|
||||
text: licenseModel.acceptButtonText
|
||||
onClicked: { handler.onLicenseAccepted() }
|
||||
},
|
||||
Button
|
||||
}
|
||||
]
|
||||
|
||||
leftButtons:
|
||||
[
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
id: declineButton
|
||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||
text: catalog.i18nc("@action:button", "Decline")
|
||||
text: licenseModel.declineButtonText
|
||||
onClicked: { handler.onLicenseDeclined() }
|
||||
}
|
||||
]
|
||||
|
|
|
@ -18,3 +18,11 @@ class CloudApiModel:
|
|||
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
|
||||
)
|
||||
|
|
51
plugins/Toolbox/src/CloudSync/CloudApiClient.py
Normal file
51
plugins/Toolbox/src/CloudSync/CloudApiClient.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 CloudApiClient:
|
||||
"""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 CloudApiClient.getInstance() instead of CloudApiClient()
|
||||
"""
|
||||
|
||||
__instance = None
|
||||
|
||||
@classmethod
|
||||
def getInstance(cls, app: CuraApplication):
|
||||
if not cls.__instance:
|
||||
cls.__instance = CloudApiClient(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)
|
|
@ -1,3 +1,6 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
|
@ -8,11 +11,12 @@ from UM import i18nCatalog
|
|||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal
|
||||
from cura.CuraApplication import CuraApplication
|
||||
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:
|
||||
|
@ -22,74 +26,37 @@ class CloudPackageChecker(QObject):
|
|||
self._application = application # type: CuraApplication
|
||||
self._scope = UltimakerCloudScope(application)
|
||||
self._model = SubscribedPackagesModel()
|
||||
self._message = None # type: Optional[Message]
|
||||
|
||||
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()
|
||||
self._onLoginStateChanged()
|
||||
# check again whenever the login state changes
|
||||
self._application.getCuraAPI().account.loginStateChanged.connect(self._fetchUserSubscribedPackages)
|
||||
self._application.getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged)
|
||||
|
||||
def _fetchUserSubscribedPackages(self) -> None:
|
||||
def _onLoginStateChanged(self) -> None:
|
||||
if self._application.getCuraAPI().account.isLoggedIn:
|
||||
self._getUserPackages()
|
||||
self._getUserSubscribedPackages()
|
||||
elif self._message is not None:
|
||||
self._message.hide()
|
||||
self._message = None
|
||||
|
||||
def _handleCompatibilityData(self, json_data) -> None:
|
||||
user_subscribed_packages = [plugin["package_id"] for plugin in json_data]
|
||||
user_installed_packages = self._package_manager.getUserInstalledPackages()
|
||||
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 Cloud Marketplace but not in Cura marketplace
|
||||
package_discrepancy = list(set(user_subscribed_packages).difference(user_installed_packages))
|
||||
|
||||
self._model.setMetadata(json_data)
|
||||
self._model.addDiscrepancies(package_discrepancy)
|
||||
self._model.initialize()
|
||||
|
||||
if not self._model.hasCompatiblePackages:
|
||||
return None
|
||||
|
||||
if package_discrepancy:
|
||||
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?"),
|
||||
lifetime=0,
|
||||
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)
|
||||
|
||||
def _getUserPackages(self) -> None:
|
||||
Logger.log("d", "Requesting subscribed packages metadata from server.")
|
||||
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:
|
||||
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",
|
||||
|
@ -98,13 +65,50 @@ class CloudPackageChecker(QObject):
|
|||
|
||||
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 packages")
|
||||
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(self._package_manager, 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",
|
||||
"Do you want to sync material and software packages with your account?"),
|
||||
title = self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ),
|
||||
lifetime = 0)
|
||||
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()
|
||||
self._message = sync_message
|
||||
|
||||
def _onSyncButtonClicked(self, sync_message: Message, sync_message_action: str) -> None:
|
||||
sync_message.hide()
|
||||
self.discrepancies.emit(self._model)
|
|
@ -1,18 +0,0 @@
|
|||
from cura.CuraApplication import CuraApplication
|
||||
from ..CloudApiModel import CloudApiModel
|
||||
from ..UltimakerCloudScope import UltimakerCloudScope
|
||||
|
||||
|
||||
## 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
|
||||
class CloudPackageManager:
|
||||
def __init__(self, app: CuraApplication) -> None:
|
||||
self._request_manager = app.getHttpRequestManager()
|
||||
self._scope = UltimakerCloudScope(app)
|
||||
|
||||
def subscribe(self, package_id: str) -> None:
|
||||
data = "{\"data\": {\"package_id\": \"%s\", \"sdk_version\": \"%s\"}}" % (package_id, CloudApiModel.sdk_version)
|
||||
self._request_manager.put(url=CloudApiModel.api_url_user_packages,
|
||||
data=data.encode(),
|
||||
scope=self._scope
|
||||
)
|
|
@ -28,13 +28,12 @@ class DiscrepanciesPresenter(QObject):
|
|||
assert self._dialog
|
||||
self._dialog.accepted.connect(lambda: self._onConfirmClicked(model))
|
||||
|
||||
@pyqtSlot("QVariant", str)
|
||||
def dismissIncompatiblePackage(self, model: SubscribedPackagesModel, package_id: str) -> None:
|
||||
model.dismissPackage(package_id) # update the model to update the view
|
||||
self._package_manager.dismissPackage(package_id) # adds this package_id as dismissed in the user config file
|
||||
|
||||
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
|
||||
model.setItems(model.getCompatiblePackages())
|
||||
self.packageMutations.emit(model)
|
||||
if model.getCompatiblePackages():
|
||||
model.setItems(model.getCompatiblePackages())
|
||||
self.packageMutations.emit(model)
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import tempfile
|
||||
from typing import Dict, List, Any
|
||||
|
||||
|
@ -62,7 +65,8 @@ class DownloadPresenter:
|
|||
"received": 0,
|
||||
"total": 1, # make sure this is not considered done yet. Also divByZero-safe
|
||||
"file_written": None,
|
||||
"request_data": request_data
|
||||
"request_data": request_data,
|
||||
"package_model": item
|
||||
}
|
||||
|
||||
self._started = True
|
||||
|
@ -80,11 +84,9 @@ class DownloadPresenter:
|
|||
return DownloadPresenter(self._app)
|
||||
|
||||
def _createProgressMessage(self) -> Message:
|
||||
return Message(i18n_catalog.i18nc(
|
||||
"@info:generic",
|
||||
"\nSyncing..."),
|
||||
return Message(i18n_catalog.i18nc("@info:generic", "Syncing..."),
|
||||
lifetime = 0,
|
||||
use_inactivity_timer=False,
|
||||
use_inactivity_timer = False,
|
||||
progress = 0.0,
|
||||
title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
|
||||
|
||||
|
@ -92,7 +94,7 @@ class DownloadPresenter:
|
|||
self._progress[package_id]["received"] = self._progress[package_id]["total"]
|
||||
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode ="wb+", suffix =".curapackage", delete = False) as temp_file:
|
||||
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)
|
||||
|
@ -128,7 +130,14 @@ class DownloadPresenter:
|
|||
if not item["file_written"]:
|
||||
return False
|
||||
|
||||
success_items = {package_id : value["file_written"] for package_id, value in self._progress.items()}
|
||||
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()
|
||||
|
|
|
@ -6,31 +6,52 @@ catalog = i18nCatalog("cura")
|
|||
|
||||
# Model for the ToolboxLicenseDialog
|
||||
class LicenseModel(QObject):
|
||||
dialogTitleChanged = pyqtSignal()
|
||||
headerChanged = pyqtSignal()
|
||||
licenseTextChanged = pyqtSignal()
|
||||
DEFAULT_DECLINE_BUTTON_TEXT = catalog.i18nc("@button", "Decline")
|
||||
ACCEPT_BUTTON_TEXT = catalog.i18nc("@button", "Agree")
|
||||
|
||||
def __init__(self) -> None:
|
||||
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._header_text = ""
|
||||
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=headerChanged)
|
||||
def headerText(self) -> str:
|
||||
return self._header_text
|
||||
@pyqtProperty(str, notify=packageNameChanged)
|
||||
def packageName(self) -> str:
|
||||
return self._package_name
|
||||
|
||||
def setPackageName(self, name: str) -> None:
|
||||
self._header_text = name + ": " + 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?")
|
||||
self.headerChanged.emit()
|
||||
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:
|
||||
|
@ -50,6 +71,7 @@ class LicenseModel(QObject):
|
|||
self._updateDialogTitle()
|
||||
|
||||
def _updateDialogTitle(self):
|
||||
self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement ({}/{})"
|
||||
.format(self._current_page_idx + 1, self._page_count))
|
||||
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()
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import os
|
||||
from typing import Dict, Optional, List
|
||||
from collections import OrderedDict
|
||||
from typing import Dict, Optional, List, Any
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.PackageManager import PackageManager
|
||||
from UM.Signal import Signal
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
@ -11,12 +13,20 @@ 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):
|
||||
"""Presents licenses for a set of packages for the user to accept or reject.
|
||||
|
||||
Call present() exactly once to show a licenseDialog for a set of packages
|
||||
Before presenting another set of licenses, create a new instance using resetCopy().
|
||||
|
||||
licenseAnswers emits a list of Dicts containing answers when the user has made a choice for all provided packages.
|
||||
"""
|
||||
|
||||
def __init__(self, app: CuraApplication) -> None:
|
||||
super().__init__()
|
||||
self._presented = False
|
||||
"""Whether present() has been called and state is expected to be initialized"""
|
||||
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
|
||||
|
@ -25,7 +35,9 @@ class LicensePresenter(QObject):
|
|||
|
||||
self._current_package_idx = 0
|
||||
self._package_models = [] # type: List[Dict]
|
||||
self._license_model = LicenseModel() # type: LicenseModel
|
||||
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
|
||||
|
||||
|
@ -34,21 +46,36 @@ class LicensePresenter(QObject):
|
|||
## 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, str]) -> None:
|
||||
def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None:
|
||||
if self._presented:
|
||||
Logger.error("{clazz} is single-use. Create a new {clazz} instead", clazz=self.__class__.__name__)
|
||||
return
|
||||
|
||||
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": i18nCatalog("cura"),
|
||||
"catalog": self._catalog,
|
||||
"licenseModel": self._license_model,
|
||||
"handler": self
|
||||
}
|
||||
self._dialog = self._app.createQmlComponent(path, context_properties)
|
||||
self._license_model.setPageCount(len(self._package_models))
|
||||
self._presentCurrentPackage()
|
||||
self._presented = True
|
||||
|
||||
def resetCopy(self) -> "LicensePresenter":
|
||||
"""Clean up and return a new copy with the same settings such as app"""
|
||||
if self._dialog:
|
||||
self._dialog.close()
|
||||
self.licenseAnswers.disconnectAll()
|
||||
return LicensePresenter(self._app)
|
||||
|
||||
@pyqtSlot()
|
||||
def onLicenseAccepted(self) -> None:
|
||||
|
@ -60,32 +87,41 @@ class LicensePresenter(QObject):
|
|||
self._package_models[self._current_package_idx]["accepted"] = False
|
||||
self._checkNextPage()
|
||||
|
||||
def _initState(self, packages: Dict[str, str]) -> None:
|
||||
self._package_models = [
|
||||
{
|
||||
"package_id" : package_id,
|
||||
"package_path" : package_path,
|
||||
"accepted" : None #: None: no answer yet
|
||||
}
|
||||
for package_id, package_path in packages.items()
|
||||
]
|
||||
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]
|
||||
license_content = self._package_manager.getPackageLicense(package_model["package_path"])
|
||||
if license_content is None:
|
||||
# Implicitly accept when there is no license
|
||||
self.onLicenseAccepted()
|
||||
return
|
||||
package_info = self._package_manager.getPackageInfo(package_model["package_path"])
|
||||
|
||||
self._license_model.setCurrentPageIdx(self._current_package_idx)
|
||||
self._license_model.setPackageName(package_model["package_id"])
|
||||
self._license_model.setLicenseText(license_content)
|
||||
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 < len(self._package_models):
|
||||
if self._current_package_idx + 1 < self._page_count:
|
||||
self._current_package_idx += 1
|
||||
self._presentCurrentPackage()
|
||||
else:
|
||||
|
|
|
@ -2,9 +2,12 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtProperty, pyqtSlot
|
||||
|
||||
from UM.PackageManager import PackageManager
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Version import Version
|
||||
|
||||
from cura import ApplicationMetadata
|
||||
from UM.Logger import Logger
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
|
@ -37,27 +40,18 @@ class SubscribedPackagesModel(ListModel):
|
|||
return True
|
||||
return False
|
||||
|
||||
# Sets the "is_compatible" to True for the given package, in memory
|
||||
|
||||
@pyqtSlot()
|
||||
def dismissPackage(self, package_id: str) -> None:
|
||||
package = self.find(key="package_id", value=package_id)
|
||||
if package != -1:
|
||||
self.setProperty(package, property="is_dismissed", value=True)
|
||||
Logger.debug("Package {} has been dismissed".format(package_id))
|
||||
|
||||
def setMetadata(self, data: List[Dict[str, List[Any]]]) -> None:
|
||||
self._metadata = data
|
||||
|
||||
def addDiscrepancies(self, discrepancy: List[str]) -> None:
|
||||
self._discrepancies = discrepancy
|
||||
|
||||
def getCompatiblePackages(self):
|
||||
return [x for x in self._items if x["is_compatible"]]
|
||||
def getCompatiblePackages(self) -> List[Dict[str, Any]]:
|
||||
return [package for package in self._items if package["is_compatible"]]
|
||||
|
||||
def initialize(self) -> None:
|
||||
def getIncompatiblePackages(self) -> List[str]:
|
||||
return [package["package_id"] for package in self._items if not package["is_compatible"]]
|
||||
|
||||
def initialize(self, package_manager: PackageManager, subscribed_packages_payload: List[Dict[str, Any]]) -> None:
|
||||
self._items.clear()
|
||||
for item in self._metadata:
|
||||
for item in subscribed_packages_payload:
|
||||
if item["package_id"] not in self._discrepancies:
|
||||
continue
|
||||
package = {
|
||||
|
@ -68,15 +62,13 @@ class SubscribedPackagesModel(ListModel):
|
|||
"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})
|
||||
|
||||
compatible = any(package_manager.isPackageCompatible(Version(version)) for version in item["sdk_versions"])
|
||||
package.update({"is_compatible": compatible})
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
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 .CloudApiClient import CloudApiClient
|
||||
from .DiscrepanciesPresenter import DiscrepanciesPresenter
|
||||
from .DownloadPresenter import DownloadPresenter
|
||||
from .LicensePresenter import LicensePresenter
|
||||
|
@ -24,7 +26,7 @@ from .SubscribedPackagesModel import SubscribedPackagesModel
|
|||
# - 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 CloudApiClient 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):
|
||||
|
@ -36,7 +38,8 @@ class SyncOrchestrator(Extension):
|
|||
self._name = "SyncOrchestrator"
|
||||
|
||||
self._package_manager = app.getPackageManager()
|
||||
self._cloud_package_manager = CloudPackageManager(app)
|
||||
# Keep a reference to the CloudApiClient. it watches for installed packages and subscribes to them
|
||||
self._cloud_api = CloudApiClient.getInstance(app) # type: CloudApiClient
|
||||
|
||||
self._checker = CloudPackageChecker(app) # type: CloudPackageChecker
|
||||
self._checker.discrepancies.connect(self._onDiscrepancies)
|
||||
|
@ -61,32 +64,39 @@ class SyncOrchestrator(Extension):
|
|||
self._download_presenter.download(mutations)
|
||||
|
||||
## Called when a set of packages have finished downloading
|
||||
# \param success_items: Dict[package_id, file_path]
|
||||
# \param success_items: Dict[package_id, Dict[str, str]]
|
||||
# \param error_items: List[package_id]
|
||||
def _onDownloadFinished(self, success_items: Dict[str, str], error_items: List[str]) -> None:
|
||||
# todo handle error items
|
||||
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 = self._license_presenter.resetCopy()
|
||||
self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers)
|
||||
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:
|
||||
Logger.debug("Got license answers: {}", answers)
|
||||
|
||||
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"]):
|
||||
Logger.error("could not install {}".format(item["package_id"]))
|
||||
message = "Could not install {}".format(item["package_id"])
|
||||
self._showErrorMessage(message)
|
||||
continue
|
||||
self._cloud_package_manager.subscribe(item["package_id"])
|
||||
has_changes = True
|
||||
else:
|
||||
# todo unsubscribe declined packages
|
||||
pass
|
||||
self._cloud_api.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()
|
||||
|
|
|
@ -67,17 +67,22 @@ class PackagesModel(ListModel):
|
|||
|
||||
links_dict = {}
|
||||
if "data" in package:
|
||||
# Links is a list of dictionaries with "title" and "url". Convert this list into a dict so it's easier
|
||||
# to process.
|
||||
link_list = package["data"]["links"] if "links" in package["data"] else []
|
||||
links_dict = {d["title"]: d["url"] for d in link_list}
|
||||
|
||||
# This code never gets executed because the API response does not contain "supported_configs" in it
|
||||
# It is so because 2y ago when this was created - it did contain it. But it was a prototype only
|
||||
# and never got to production. As agreed with the team, it'll stay here for now, in case we decide to rework and use it
|
||||
# The response payload has been changed. Please see:
|
||||
# https://github.com/Ultimaker/Cura/compare/CURA-7072-temp?expand=1
|
||||
if "supported_configs" in package["data"]:
|
||||
if len(package["data"]["supported_configs"]) > 0:
|
||||
has_configs = True
|
||||
configs_model = ConfigsModel()
|
||||
configs_model.setConfigs(package["data"]["supported_configs"])
|
||||
|
||||
# Links is a list of dictionaries with "title" and "url". Convert this list into a dict so it's easier
|
||||
# to process.
|
||||
link_list = package["data"]["links"] if "links" in package["data"] else []
|
||||
links_dict = {d["title"]: d["url"] for d in link_list}
|
||||
|
||||
if "author_id" not in package["author"] or "display_name" not in package["author"]:
|
||||
package["author"]["author_id"] = ""
|
||||
package["author"]["display_name"] = ""
|
||||
|
|
|
@ -16,12 +16,12 @@ from UM.i18n import i18nCatalog
|
|||
from UM.Version import Version
|
||||
|
||||
from cura import ApplicationMetadata
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
|
||||
from .CloudApiModel import CloudApiModel
|
||||
from .AuthorsModel import AuthorsModel
|
||||
from .CloudSync.CloudPackageManager import CloudPackageManager
|
||||
from .CloudSync.LicenseModel import LicenseModel
|
||||
from .PackagesModel import PackagesModel
|
||||
from .UltimakerCloudScope import UltimakerCloudScope
|
||||
|
@ -32,6 +32,13 @@ if TYPE_CHECKING:
|
|||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
DEFAULT_MARKETPLACE_ROOT = "https://marketplace.ultimaker.com" # type: str
|
||||
|
||||
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
|
||||
|
@ -44,7 +51,6 @@ class Toolbox(QObject, Extension):
|
|||
self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
|
||||
|
||||
# Network:
|
||||
self._cloud_package_manager = CloudPackageManager(application) # type: CloudPackageManager
|
||||
self._download_request_data = None # type: Optional[HttpRequestData]
|
||||
self._download_progress = 0 # type: float
|
||||
self._is_downloading = False # type: bool
|
||||
|
@ -147,17 +153,14 @@ class Toolbox(QObject, Extension):
|
|||
|
||||
self._application.getHttpRequestManager().put(url, data = data.encode(), scope = self._scope)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def subscribe(self, package_id: str) -> None:
|
||||
self._cloud_package_manager.subscribe(package_id)
|
||||
|
||||
def getLicenseDialogPluginFileLocation(self) -> str:
|
||||
return self._license_dialog_plugin_file_location
|
||||
|
||||
def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str) -> None:
|
||||
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)
|
||||
|
||||
self._license_model.setPackageName(plugin_name)
|
||||
self._license_model.setLicenseText(license_content)
|
||||
|
@ -376,7 +379,6 @@ class Toolbox(QObject, Extension):
|
|||
def onLicenseAccepted(self):
|
||||
self.closeLicenseDialog.emit()
|
||||
package_id = self.install(self.getLicenseDialogPluginFileLocation())
|
||||
self.subscribe(package_id)
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
|
@ -670,14 +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
|
||||
|
||||
package_id = self.install(file_path)
|
||||
if package_id != package_info["package_id"]:
|
||||
Logger.error("Installed package {} does not match {}".format(package_id, package_info["package_id"]))
|
||||
self.subscribe(package_id)
|
||||
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:
|
||||
# --------------------------------------------------------------------------
|
||||
|
@ -699,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:
|
||||
|
@ -770,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)
|
||||
|
|
|
@ -6,6 +6,9 @@ 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)
|
||||
|
|
|
@ -108,7 +108,7 @@ class TrimeshReader(MeshReader):
|
|||
mesh.merge_vertices()
|
||||
mesh.remove_unreferenced_vertices()
|
||||
mesh.fix_normals()
|
||||
mesh_data = self._toMeshData(mesh)
|
||||
mesh_data = self._toMeshData(mesh, file_name)
|
||||
|
||||
file_base_name = os.path.basename(file_name)
|
||||
new_node = CuraSceneNode()
|
||||
|
@ -133,9 +133,10 @@ class TrimeshReader(MeshReader):
|
|||
## Converts a Trimesh to Uranium's MeshData.
|
||||
# \param tri_node A Trimesh containing the contents of a file that was
|
||||
# just read.
|
||||
# \param file_name The full original filename used to watch for changes
|
||||
# \return Mesh data from the Trimesh in a way that Uranium can understand
|
||||
# it.
|
||||
def _toMeshData(self, tri_node: trimesh.base.Trimesh) -> MeshData:
|
||||
def _toMeshData(self, tri_node: trimesh.base.Trimesh, file_name: str = "") -> MeshData:
|
||||
tri_faces = tri_node.faces
|
||||
tri_vertices = tri_node.vertices
|
||||
|
||||
|
@ -157,5 +158,5 @@ class TrimeshReader(MeshReader):
|
|||
indices = numpy.asarray(indices, dtype = numpy.int32)
|
||||
normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count)
|
||||
|
||||
mesh_data = MeshData(vertices = vertices, indices = indices, normals = normals)
|
||||
mesh_data = MeshData(vertices = vertices, indices = indices, normals = normals, file_name = file_name)
|
||||
return mesh_data
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -30,22 +30,22 @@ Item
|
|||
borderColor: printJob && printJob.configurationChanges.length !== 0 ? UM.Theme.getColor("warning") : UM.Theme.getColor("monitor_card_border")
|
||||
headerItem: Row
|
||||
{
|
||||
height: 48 * screenScaleFactor // TODO: Theme!
|
||||
height: Math.round(48 * screenScaleFactor) // TODO: Theme!
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 24 * screenScaleFactor // TODO: Theme!
|
||||
spacing: 18 * screenScaleFactor // TODO: Theme!
|
||||
anchors.leftMargin: Math.round(24 * screenScaleFactor) // TODO: Theme!
|
||||
spacing: Math.round(18 * screenScaleFactor) // TODO: Theme!
|
||||
|
||||
MonitorPrintJobPreview
|
||||
{
|
||||
printJob: base.printJob
|
||||
size: 32 * screenScaleFactor // TODO: Theme!
|
||||
size: Math.round(32 * screenScaleFactor) // TODO: Theme!
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
height: Math.round(18 * screenScaleFactor) // TODO: Theme!
|
||||
width: UM.Theme.getSize("monitor_column").width
|
||||
Rectangle
|
||||
{
|
||||
|
@ -74,7 +74,7 @@ Item
|
|||
Item
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
height: Math.round(18 * screenScaleFactor) // TODO: Theme!
|
||||
width: UM.Theme.getSize("monitor_column").width
|
||||
|
||||
Rectangle
|
||||
|
@ -95,7 +95,7 @@ Item
|
|||
visible: printJob
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
height: Math.round(18 * screenScaleFactor) // TODO: Theme!
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
@ -104,13 +104,13 @@ Item
|
|||
Item
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: 18 * screenScaleFactor // TODO: This should be childrenRect.height but QML throws warnings
|
||||
height: Math.round(18 * screenScaleFactor) // TODO: This should be childrenRect.height but QML throws warnings
|
||||
width: childrenRect.width
|
||||
|
||||
Rectangle
|
||||
{
|
||||
color: UM.Theme.getColor("monitor_skeleton_loading")
|
||||
width: 72 * screenScaleFactor // TODO: Theme!
|
||||
width: Math.round(72 * screenScaleFactor) // TODO: Theme!
|
||||
height: parent.height
|
||||
visible: !printJob
|
||||
radius: 2 * screenScaleFactor // TODO: Theme!
|
||||
|
@ -124,21 +124,22 @@ Item
|
|||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("medium") // 14pt, regular
|
||||
text: {
|
||||
if (printJob !== null) {
|
||||
if (printJob !== null)
|
||||
{
|
||||
if (printJob.assignedPrinter == null)
|
||||
{
|
||||
if (printJob.state == "error")
|
||||
{
|
||||
return catalog.i18nc("@label", "Unavailable printer")
|
||||
return catalog.i18nc("@label", "Unavailable printer");
|
||||
}
|
||||
return catalog.i18nc("@label", "First available")
|
||||
return catalog.i18nc("@label", "First available");
|
||||
}
|
||||
return printJob.assignedPrinter.name
|
||||
return printJob.assignedPrinter.name;
|
||||
}
|
||||
return ""
|
||||
return "";
|
||||
}
|
||||
visible: printJob
|
||||
width: 120 * screenScaleFactor // TODO: Theme!
|
||||
width: Math.round(120 * screenScaleFactor) // TODO: Theme!
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
height: parent.height
|
||||
|
@ -152,11 +153,11 @@ Item
|
|||
anchors
|
||||
{
|
||||
left: printerAssignmentLabel.right;
|
||||
leftMargin: 12 // TODO: Theme!
|
||||
leftMargin: Math.round(12 * screenScaleFactor) // TODO: Theme!
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
height: childrenRect.height
|
||||
spacing: 6 // TODO: Theme!
|
||||
spacing: Math.round(6 * screenScaleFactor) // TODO: Theme!
|
||||
visible: printJob
|
||||
|
||||
MonitorPrinterPill
|
||||
|
@ -171,10 +172,10 @@ Item
|
|||
anchors
|
||||
{
|
||||
left: parent.left
|
||||
leftMargin: 74 * screenScaleFactor // TODO: Theme!
|
||||
leftMargin: Math.round(74 * screenScaleFactor) // TODO: Theme!
|
||||
}
|
||||
height: 108 * screenScaleFactor // TODO: Theme!
|
||||
spacing: 18 * screenScaleFactor // TODO: Theme!
|
||||
height: Math.round(108 * screenScaleFactor) // TODO: Theme!
|
||||
spacing: Math.round(18 * screenScaleFactor) // TODO: Theme!
|
||||
|
||||
MonitorPrinterConfiguration
|
||||
{
|
||||
|
@ -182,7 +183,7 @@ Item
|
|||
anchors.verticalCenter: parent.verticalCenter
|
||||
buildplate: catalog.i18nc("@label", "Glass")
|
||||
configurations: base.printJob.configuration.extruderConfigurations
|
||||
height: 72 * screenScaleFactor // TODO: Theme!
|
||||
height: Math.round(72 * screenScaleFactor) // TODO: Theme!
|
||||
}
|
||||
|
||||
Label {
|
||||
|
@ -193,7 +194,7 @@ Item
|
|||
anchors.top: printerConfiguration.top
|
||||
|
||||
// FIXED-LINE-HEIGHT:
|
||||
height: 18 * screenScaleFactor // TODO: Theme!
|
||||
height: Math.round(18 * screenScaleFactor) // TODO: Theme!
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
|
@ -206,21 +207,22 @@ Item
|
|||
anchors
|
||||
{
|
||||
right: parent.right
|
||||
rightMargin: 8 * screenScaleFactor // TODO: Theme!
|
||||
rightMargin: Math.round(8 * screenScaleFactor) // TODO: Theme!
|
||||
top: parent.top
|
||||
topMargin: 8 * screenScaleFactor // TODO: Theme!
|
||||
topMargin: Math.round(8 * screenScaleFactor) // TODO: Theme!
|
||||
}
|
||||
width: 32 * screenScaleFactor // TODO: Theme!
|
||||
height: 32 * screenScaleFactor // TODO: Theme!
|
||||
width: Math.round(32 * screenScaleFactor) // TODO: Theme!
|
||||
height: Math.round(32 * screenScaleFactor) // TODO: Theme!
|
||||
enabled: OutputDevice.supportsPrintJobActions
|
||||
onClicked: enabled ? contextMenu.switchPopupState() : {}
|
||||
visible:
|
||||
{
|
||||
if (!printJob) {
|
||||
return false
|
||||
if (!printJob)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
var states = ["queued", "error", "sent_to_printer", "pre_print", "printing", "pausing", "paused", "resuming"]
|
||||
return states.indexOf(printJob.state) !== -1
|
||||
var states = ["queued", "error", "sent_to_printer", "pre_print", "printing", "pausing", "paused", "resuming"];
|
||||
return states.indexOf(printJob.state) !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from time import time
|
||||
import os
|
||||
from typing import List, Optional, cast
|
||||
|
||||
from PyQt5.QtCore import QObject, QUrl, pyqtProperty, pyqtSignal, pyqtSlot
|
||||
|
@ -191,8 +192,9 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
|
|||
def _onPrintJobCreated(self, job: ExportFileJob) -> None:
|
||||
output = job.getOutput()
|
||||
self._tool_path = output # store the tool path to prevent re-uploading when printing the same file again
|
||||
file_name = job.getFileName()
|
||||
request = CloudPrintJobUploadRequest(
|
||||
job_name=job.getFileName(),
|
||||
job_name=os.path.splitext(file_name)[0],
|
||||
file_size=len(output),
|
||||
content_type=job.getMimeType(),
|
||||
)
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
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