Merge branch 'master' into refactor_singleton_settingsbase

This commit is contained in:
Ghostkeeper 2018-09-13 11:52:13 +02:00
commit cc77632357
No known key found for this signature in database
GPG key ID: 5252B696FB5E7C7A
151 changed files with 62703 additions and 2111 deletions

1
.gitignore vendored
View file

@ -26,6 +26,7 @@ LC_MESSAGES
*.lprof *.lprof
*~ *~
*.qm *.qm
.directory
.idea .idea
cura.desktop cura.desktop

13
Jenkinsfile vendored
View file

@ -12,6 +12,19 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
// If any error occurs during building, we want to catch it and continue with the "finale" stage. // If any error occurs during building, we want to catch it and continue with the "finale" stage.
catchError { catchError {
stage('Pre Checks') {
if (isUnix()) {
try {
sh """
echo 'Check for duplicate shortcut keys in all translation files.'
${env.CURA_ENVIRONMENT_PATH}/master/bin/python3 scripts/check_shortcut_keys.py
"""
} catch(e) {
currentBuild.result = "UNSTABLE"
}
}
}
// Building and testing should happen in a subdirectory. // Building and testing should happen in a subdirectory.
dir('build') { dir('build') {
// Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup. // Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup.

View file

@ -528,7 +528,7 @@ class BuildVolume(SceneNode):
def _onStackChanged(self): def _onStackChanged(self):
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged) self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()) extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
for extruder in extruders: for extruder in extruders:
extruder.propertyChanged.disconnect(self._onSettingPropertyChanged) extruder.propertyChanged.disconnect(self._onSettingPropertyChanged)
@ -536,7 +536,7 @@ class BuildVolume(SceneNode):
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged) self._global_container_stack.propertyChanged.connect(self._onSettingPropertyChanged)
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()) extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
for extruder in extruders: for extruder in extruders:
extruder.propertyChanged.connect(self._onSettingPropertyChanged) extruder.propertyChanged.connect(self._onSettingPropertyChanged)

View file

@ -19,5 +19,11 @@ class CameraImageProvider(QQuickImageProvider):
return image, QSize(15, 15) return image, QSize(15, 15)
except AttributeError: except AttributeError:
pass try:
image = output_device.activeCamera.getImage()
return image, QSize(15, 15)
except AttributeError:
pass
return QImage(), QSize(15, 15) return QImage(), QSize(15, 15)

View file

@ -93,6 +93,7 @@ from . import CuraActions
from cura.Scene import ZOffsetDecorator from cura.Scene import ZOffsetDecorator
from . import CuraSplashScreen from . import CuraSplashScreen
from . import CameraImageProvider from . import CameraImageProvider
from . import PrintJobPreviewImageProvider
from . import MachineActionManager from . import MachineActionManager
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
@ -113,6 +114,9 @@ from UM.FlameProfiler import pyqtSlot
if TYPE_CHECKING: if TYPE_CHECKING:
from plugins.SliceInfoPlugin.SliceInfo import SliceInfo from plugins.SliceInfoPlugin.SliceInfo import SliceInfo
from cura.Machines.MaterialManager import MaterialManager
from cura.Machines.QualityManager import QualityManager
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
numpy.seterr(all = "ignore") numpy.seterr(all = "ignore")
@ -174,12 +178,12 @@ class CuraApplication(QtApplication):
self._machine_action_manager = None self._machine_action_manager = None
self.empty_container = None self.empty_container = None # type: EmptyInstanceContainer
self.empty_definition_changes_container = None self.empty_definition_changes_container = None # type: EmptyInstanceContainer
self.empty_variant_container = None self.empty_variant_container = None # type: EmptyInstanceContainer
self.empty_material_container = None self.empty_material_container = None # type: EmptyInstanceContainer
self.empty_quality_container = None self.empty_quality_container = None # type: EmptyInstanceContainer
self.empty_quality_changes_container = None self.empty_quality_changes_container = None # type: EmptyInstanceContainer
self._variant_manager = None self._variant_manager = None
self._material_manager = None self._material_manager = None
@ -367,7 +371,7 @@ class CuraApplication(QtApplication):
# Add empty variant, material and quality containers. # Add empty variant, material and quality containers.
# Since they are empty, they should never be serialized and instead just programmatically created. # Since they are empty, they should never be serialized and instead just programmatically created.
# We need them to simplify the switching between materials. # We need them to simplify the switching between materials.
self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container # type: EmptyInstanceContainer
self._container_registry.addContainer( self._container_registry.addContainer(
cura.Settings.cura_empty_instance_containers.empty_definition_changes_container) cura.Settings.cura_empty_instance_containers.empty_definition_changes_container)
@ -428,6 +432,7 @@ class CuraApplication(QtApplication):
# Readers & Writers: # Readers & Writers:
"GCodeWriter", "GCodeWriter",
"STLReader", "STLReader",
"3MFWriter",
# Tools: # Tools:
"CameraTool", "CameraTool",
@ -502,6 +507,7 @@ class CuraApplication(QtApplication):
def _onEngineCreated(self): def _onEngineCreated(self):
self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider()) self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider())
@pyqtProperty(bool) @pyqtProperty(bool)
def needToShowUserAgreement(self): def needToShowUserAgreement(self):
@ -804,20 +810,20 @@ class CuraApplication(QtApplication):
self._machine_manager = MachineManager(self) self._machine_manager = MachineManager(self)
return self._machine_manager return self._machine_manager
def getExtruderManager(self, *args): def getExtruderManager(self, *args) -> ExtruderManager:
if self._extruder_manager is None: if self._extruder_manager is None:
self._extruder_manager = ExtruderManager() self._extruder_manager = ExtruderManager()
return self._extruder_manager return self._extruder_manager
def getVariantManager(self, *args): def getVariantManager(self, *args) -> VariantManager:
return self._variant_manager return self._variant_manager
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getMaterialManager(self, *args): def getMaterialManager(self, *args) -> "MaterialManager":
return self._material_manager return self._material_manager
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getQualityManager(self, *args): def getQualityManager(self, *args) -> "QualityManager":
return self._quality_manager return self._quality_manager
def getObjectsModel(self, *args): def getObjectsModel(self, *args):
@ -826,23 +832,23 @@ class CuraApplication(QtApplication):
return self._object_manager return self._object_manager
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getMultiBuildPlateModel(self, *args): def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel:
if self._multi_build_plate_model is None: if self._multi_build_plate_model is None:
self._multi_build_plate_model = MultiBuildPlateModel(self) self._multi_build_plate_model = MultiBuildPlateModel(self)
return self._multi_build_plate_model return self._multi_build_plate_model
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getBuildPlateModel(self, *args): def getBuildPlateModel(self, *args) -> BuildPlateModel:
if self._build_plate_model is None: if self._build_plate_model is None:
self._build_plate_model = BuildPlateModel(self) self._build_plate_model = BuildPlateModel(self)
return self._build_plate_model return self._build_plate_model
def getCuraSceneController(self, *args): def getCuraSceneController(self, *args) -> CuraSceneController:
if self._cura_scene_controller is None: if self._cura_scene_controller is None:
self._cura_scene_controller = CuraSceneController.createCuraSceneController() self._cura_scene_controller = CuraSceneController.createCuraSceneController()
return self._cura_scene_controller return self._cura_scene_controller
def getSettingInheritanceManager(self, *args): def getSettingInheritanceManager(self, *args) -> SettingInheritanceManager:
if self._setting_inheritance_manager is None: if self._setting_inheritance_manager is None:
self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager() self._setting_inheritance_manager = SettingInheritanceManager.createSettingInheritanceManager()
return self._setting_inheritance_manager return self._setting_inheritance_manager

View file

@ -24,29 +24,34 @@ if TYPE_CHECKING:
# This is used in Variant, Material, and Quality Managers. # This is used in Variant, Material, and Quality Managers.
# #
class ContainerNode: class ContainerNode:
__slots__ = ("metadata", "container", "children_map") __slots__ = ("_metadata", "container", "children_map")
def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None: def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
self.metadata = metadata self._metadata = metadata
self.container = None self.container = None
self.children_map = OrderedDict() #type: OrderedDict[str, Union[QualityGroup, ContainerNode]] self.children_map = OrderedDict() # type: ignore # This is because it's children are supposed to override it.
## Get an entry value from the metadata ## Get an entry value from the metadata
def getMetaDataEntry(self, entry: str, default: Any = None) -> Any: def getMetaDataEntry(self, entry: str, default: Any = None) -> Any:
if self.metadata is None: if self._metadata is None:
return default return default
return self.metadata.get(entry, default) return self._metadata.get(entry, default)
def getMetadata(self) -> Dict[str, Any]:
if self._metadata is None:
return {}
return self._metadata
def getChildNode(self, child_key: str) -> Optional["ContainerNode"]: def getChildNode(self, child_key: str) -> Optional["ContainerNode"]:
return self.children_map.get(child_key) return self.children_map.get(child_key)
def getContainer(self) -> Optional["InstanceContainer"]: def getContainer(self) -> Optional["InstanceContainer"]:
if self.metadata is None: if self._metadata is None:
Logger.log("e", "Cannot get container for a ContainerNode without metadata.") Logger.log("e", "Cannot get container for a ContainerNode without metadata.")
return None return None
if self.container is None: if self.container is None:
container_id = self.metadata["id"] container_id = self._metadata["id"]
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
container_list = ContainerRegistry.getInstance().findInstanceContainers(id = container_id) container_list = ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
if not container_list: if not container_list:

View file

@ -50,7 +50,7 @@ class MachineErrorChecker(QObject):
self._error_check_timer.setInterval(100) self._error_check_timer.setInterval(100)
self._error_check_timer.setSingleShot(True) self._error_check_timer.setSingleShot(True)
def initialize(self): def initialize(self) -> None:
self._error_check_timer.timeout.connect(self._rescheduleCheck) self._error_check_timer.timeout.connect(self._rescheduleCheck)
# Reconnect all signals when the active machine gets changed. # Reconnect all signals when the active machine gets changed.
@ -62,7 +62,7 @@ class MachineErrorChecker(QObject):
self._onMachineChanged() self._onMachineChanged()
def _onMachineChanged(self): def _onMachineChanged(self) -> None:
if self._global_stack: if self._global_stack:
self._global_stack.propertyChanged.disconnect(self.startErrorCheck) self._global_stack.propertyChanged.disconnect(self.startErrorCheck)
self._global_stack.containersChanged.disconnect(self.startErrorCheck) self._global_stack.containersChanged.disconnect(self.startErrorCheck)
@ -94,7 +94,7 @@ class MachineErrorChecker(QObject):
return self._need_to_check or self._check_in_progress return self._need_to_check or self._check_in_progress
# Starts the error check timer to schedule a new error check. # Starts the error check timer to schedule a new error check.
def startErrorCheck(self, *args): def startErrorCheck(self, *args) -> None:
if not self._check_in_progress: if not self._check_in_progress:
self._need_to_check = True self._need_to_check = True
self.needToWaitForResultChanged.emit() self.needToWaitForResultChanged.emit()
@ -103,7 +103,7 @@ class MachineErrorChecker(QObject):
# This function is called by the timer to reschedule a new error check. # This function is called by the timer to reschedule a new error check.
# If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag # If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag
# to notify the current check to stop and start a new one. # to notify the current check to stop and start a new one.
def _rescheduleCheck(self): def _rescheduleCheck(self) -> None:
if self._check_in_progress and not self._need_to_check: if self._check_in_progress and not self._need_to_check:
self._need_to_check = True self._need_to_check = True
self.needToWaitForResultChanged.emit() self.needToWaitForResultChanged.emit()
@ -128,7 +128,7 @@ class MachineErrorChecker(QObject):
self._start_time = time.time() self._start_time = time.time()
Logger.log("d", "New error check scheduled.") Logger.log("d", "New error check scheduled.")
def _checkStack(self): def _checkStack(self) -> None:
if self._need_to_check: if self._need_to_check:
Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.") Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.")
self._check_in_progress = False self._check_in_progress = False
@ -169,7 +169,7 @@ class MachineErrorChecker(QObject):
# Schedule the check for the next key # Schedule the check for the next key
self._application.callLater(self._checkStack) self._application.callLater(self._checkStack)
def _setResult(self, result: bool): def _setResult(self, result: bool) -> None:
if result != self._has_errors: if result != self._has_errors:
self._has_errors = result self._has_errors = result
self.hasErrorUpdated.emit() self.hasErrorUpdated.emit()

View file

@ -24,8 +24,8 @@ class MaterialGroup:
def __init__(self, name: str, root_material_node: "MaterialNode") -> None: def __init__(self, name: str, root_material_node: "MaterialNode") -> None:
self.name = name self.name = name
self.is_read_only = False self.is_read_only = False
self.root_material_node = root_material_node # type: MaterialNode self.root_material_node = root_material_node # type: MaterialNode
self.derived_material_node_list = [] # type: List[MaterialNode] self.derived_material_node_list = [] # type: List[MaterialNode]
def __str__(self) -> str: def __str__(self) -> str:
return "%s[%s]" % (self.__class__.__name__, self.name) return "%s[%s]" % (self.__class__.__name__, self.name)

View file

@ -4,8 +4,7 @@
from collections import defaultdict, OrderedDict from collections import defaultdict, OrderedDict
import copy import copy
import uuid import uuid
import json from typing import Dict, Optional, TYPE_CHECKING, Any, Set, List, cast, Tuple
from typing import Dict, Optional, TYPE_CHECKING
from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot from PyQt5.Qt import QTimer, QObject, pyqtSignal, pyqtSlot
@ -39,26 +38,35 @@ if TYPE_CHECKING:
# #
class MaterialManager(QObject): class MaterialManager(QObject):
materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated. materialsUpdated = pyqtSignal() # Emitted whenever the material lookup tables are updated.
favoritesUpdated = pyqtSignal() # Emitted whenever the favorites are changed favoritesUpdated = pyqtSignal() # Emitted whenever the favorites are changed
def __init__(self, container_registry, parent = None): def __init__(self, container_registry, parent = None):
super().__init__(parent) super().__init__(parent)
self._application = Application.getInstance() self._application = Application.getInstance()
self._container_registry = container_registry # type: ContainerRegistry self._container_registry = container_registry # type: ContainerRegistry
self._fallback_materials_map = dict() # material_type -> generic material metadata # Material_type -> generic material metadata
self._material_group_map = dict() # root_material_id -> MaterialGroup self._fallback_materials_map = dict() # type: Dict[str, Dict[str, Any]]
self._diameter_machine_nozzle_buildplate_material_map = dict() # approximate diameter str -> dict(machine_definition_id -> MaterialNode)
# Root_material_id -> MaterialGroup
self._material_group_map = dict() # type: Dict[str, MaterialGroup]
# Approximate diameter str
self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]]
# We're using these two maps to convert between the specific diameter material id and the generic material id # We're using these two maps to convert between the specific diameter material id and the generic material id
# because the generic material ids are used in qualities and definitions, while the specific diameter material is meant # because the generic material ids are used in qualities and definitions, while the specific diameter material is meant
# i.e. generic_pla -> generic_pla_175 # i.e. generic_pla -> generic_pla_175
self._material_diameter_map = defaultdict(dict) # root_material_id -> approximate diameter str -> root_material_id for that diameter # root_material_id -> approximate diameter str -> root_material_id for that diameter
self._diameter_material_map = dict() # material id including diameter (generic_pla_175) -> material root id (generic_pla) self._material_diameter_map = defaultdict(dict) # type: Dict[str, Dict[str, str]]
# Material id including diameter (generic_pla_175) -> material root id (generic_pla)
self._diameter_material_map = dict() # type: Dict[str, str]
# This is used in Legacy UM3 send material function and the material management page. # This is used in Legacy UM3 send material function and the material management page.
self._guid_material_groups_map = defaultdict(list) # GUID -> a list of material_groups # GUID -> a list of material_groups
self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]]
# The machine definition ID for the non-machine-specific materials. # The machine definition ID for the non-machine-specific materials.
# This is used as the last fallback option if the given machine-specific material(s) cannot be found. # This is used as the last fallback option if the given machine-specific material(s) cannot be found.
@ -77,15 +85,15 @@ class MaterialManager(QObject):
self._container_registry.containerAdded.connect(self._onContainerMetadataChanged) self._container_registry.containerAdded.connect(self._onContainerMetadataChanged)
self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged) self._container_registry.containerRemoved.connect(self._onContainerMetadataChanged)
self._favorites = set() self._favorites = set() # type: Set[str]
def initialize(self): def initialize(self) -> None:
# Find all materials and put them in a matrix for quick search. # Find all materials and put them in a matrix for quick search.
material_metadatas = {metadata["id"]: metadata for metadata in material_metadatas = {metadata["id"]: metadata for metadata in
self._container_registry.findContainersMetadata(type = "material") if self._container_registry.findContainersMetadata(type = "material") if
metadata.get("GUID")} metadata.get("GUID")} # type: Dict[str, Dict[str, Any]]
self._material_group_map = dict() self._material_group_map = dict() # type: Dict[str, MaterialGroup]
# Map #1 # Map #1
# root_material_id -> MaterialGroup # root_material_id -> MaterialGroup
@ -94,7 +102,7 @@ class MaterialManager(QObject):
if material_id == "empty_material": if material_id == "empty_material":
continue continue
root_material_id = material_metadata.get("base_file") root_material_id = material_metadata.get("base_file", "")
if root_material_id not in self._material_group_map: if root_material_id not in self._material_group_map:
self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id])) self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id) self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
@ -110,26 +118,26 @@ class MaterialManager(QObject):
# Map #1.5 # Map #1.5
# GUID -> material group list # GUID -> material group list
self._guid_material_groups_map = defaultdict(list) self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]]
for root_material_id, material_group in self._material_group_map.items(): for root_material_id, material_group in self._material_group_map.items():
guid = material_group.root_material_node.metadata["GUID"] guid = material_group.root_material_node.getMetaDataEntry("GUID", "")
self._guid_material_groups_map[guid].append(material_group) self._guid_material_groups_map[guid].append(material_group)
# Map #2 # Map #2
# Lookup table for material type -> fallback material metadata, only for read-only materials # Lookup table for material type -> fallback material metadata, only for read-only materials
grouped_by_type_dict = dict() grouped_by_type_dict = dict() # type: Dict[str, Any]
material_types_without_fallback = set() material_types_without_fallback = set()
for root_material_id, material_node in self._material_group_map.items(): for root_material_id, material_node in self._material_group_map.items():
material_type = material_node.root_material_node.metadata["material"] material_type = material_node.root_material_node.getMetaDataEntry("material", "")
if material_type not in grouped_by_type_dict: if material_type not in grouped_by_type_dict:
grouped_by_type_dict[material_type] = {"generic": None, grouped_by_type_dict[material_type] = {"generic": None,
"others": []} "others": []}
material_types_without_fallback.add(material_type) material_types_without_fallback.add(material_type)
brand = material_node.root_material_node.metadata["brand"] brand = material_node.root_material_node.getMetaDataEntry("brand", "")
if brand.lower() == "generic": if brand.lower() == "generic":
to_add = True to_add = True
if material_type in grouped_by_type_dict: if material_type in grouped_by_type_dict:
diameter = material_node.root_material_node.metadata.get("approximate_diameter") diameter = material_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
if diameter != self._default_approximate_diameter_for_quality_search: if diameter != self._default_approximate_diameter_for_quality_search:
to_add = False # don't add if it's not the default diameter to_add = False # don't add if it's not the default diameter
@ -138,7 +146,7 @@ class MaterialManager(QObject):
# - if it's in the list, it means that is a new material without fallback # - if it's in the list, it means that is a new material without fallback
# - if it is not, then it is a custom material with a fallback material (parent) # - if it is not, then it is a custom material with a fallback material (parent)
if material_type in material_types_without_fallback: if material_type in material_types_without_fallback:
grouped_by_type_dict[material_type] = material_node.root_material_node.metadata grouped_by_type_dict[material_type] = material_node.root_material_node._metadata
material_types_without_fallback.remove(material_type) material_types_without_fallback.remove(material_type)
# Remove the materials that have no fallback materials # Remove the materials that have no fallback materials
@ -155,15 +163,15 @@ class MaterialManager(QObject):
self._diameter_material_map = dict() self._diameter_material_map = dict()
# Group the material IDs by the same name, material, brand, and color but with different diameters. # Group the material IDs by the same name, material, brand, and color but with different diameters.
material_group_dict = dict() material_group_dict = dict() # type: Dict[Tuple[Any], Dict[str, str]]
keys_to_fetch = ("name", "material", "brand", "color") keys_to_fetch = ("name", "material", "brand", "color")
for root_material_id, machine_node in self._material_group_map.items(): for root_material_id, machine_node in self._material_group_map.items():
root_material_metadata = machine_node.root_material_node.metadata root_material_metadata = machine_node.root_material_node._metadata
key_data = [] key_data_list = [] # type: List[Any]
for key in keys_to_fetch: for key in keys_to_fetch:
key_data.append(root_material_metadata.get(key)) key_data_list.append(machine_node.root_material_node.getMetaDataEntry(key))
key_data = tuple(key_data) key_data = cast(Tuple[Any], tuple(key_data_list)) # type: Tuple[Any]
# If the key_data doesn't exist, it doesn't matter if the material is read only... # If the key_data doesn't exist, it doesn't matter if the material is read only...
if key_data not in material_group_dict: if key_data not in material_group_dict:
@ -172,8 +180,8 @@ class MaterialManager(QObject):
# ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it # ...but if key_data exists, we just overwrite it if the material is read only, otherwise we skip it
if not machine_node.is_read_only: if not machine_node.is_read_only:
continue continue
approximate_diameter = root_material_metadata.get("approximate_diameter") approximate_diameter = machine_node.root_material_node.getMetaDataEntry("approximate_diameter", "")
material_group_dict[key_data][approximate_diameter] = root_material_metadata["id"] material_group_dict[key_data][approximate_diameter] = machine_node.root_material_node.getMetaDataEntry("id", "")
# Map [root_material_id][diameter] -> root_material_id for this diameter # Map [root_material_id][diameter] -> root_material_id for this diameter
for data_dict in material_group_dict.values(): for data_dict in material_group_dict.values():
@ -192,7 +200,7 @@ class MaterialManager(QObject):
# Map #4 # Map #4
# "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer # "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer
self._diameter_machine_nozzle_buildplate_material_map = dict() self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]]
for material_metadata in material_metadatas.values(): for material_metadata in material_metadatas.values():
self.__addMaterialMetadataIntoLookupTree(material_metadata) self.__addMaterialMetadataIntoLookupTree(material_metadata)
@ -203,7 +211,7 @@ class MaterialManager(QObject):
self._favorites.add(item) self._favorites.add(item)
self.favoritesUpdated.emit() self.favoritesUpdated.emit()
def __addMaterialMetadataIntoLookupTree(self, material_metadata: dict) -> None: def __addMaterialMetadataIntoLookupTree(self, material_metadata: Dict[str, Any]) -> None:
material_id = material_metadata["id"] material_id = material_metadata["id"]
# We don't store empty material in the lookup tables # We don't store empty material in the lookup tables
@ -290,7 +298,7 @@ class MaterialManager(QObject):
return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id) return self._material_diameter_map.get(root_material_id, {}).get(approximate_diameter, root_material_id)
def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str: def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
return self._diameter_material_map.get(root_material_id) return self._diameter_material_map.get(root_material_id, "")
def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]: def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]:
return self._guid_material_groups_map.get(guid) return self._guid_material_groups_map.get(guid)
@ -351,7 +359,7 @@ class MaterialManager(QObject):
# A convenience function to get available materials for the given machine with the extruder position. # A convenience function to get available materials for the given machine with the extruder position.
# #
def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack", def getAvailableMaterialsForMachineExtruder(self, machine: "GlobalStack",
extruder_stack: "ExtruderStack") -> Optional[dict]: extruder_stack: "ExtruderStack") -> Optional[Dict[str, MaterialNode]]:
buildplate_name = machine.getBuildplateName() buildplate_name = machine.getBuildplateName()
nozzle_name = None nozzle_name = None
if extruder_stack.variant.getId() != "empty_variant": if extruder_stack.variant.getId() != "empty_variant":
@ -368,7 +376,7 @@ class MaterialManager(QObject):
# 2. cannot find any material InstanceContainers with the given settings. # 2. cannot find any material InstanceContainers with the given settings.
# #
def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str], def getMaterialNode(self, machine_definition_id: str, nozzle_name: Optional[str],
buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["InstanceContainer"]: buildplate_name: Optional[str], diameter: float, root_material_id: str) -> Optional["MaterialNode"]:
# round the diameter to get the approximate diameter # round the diameter to get the approximate diameter
rounded_diameter = str(round(diameter)) rounded_diameter = str(round(diameter))
if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map: if rounded_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
@ -377,7 +385,7 @@ class MaterialManager(QObject):
return None return None
# If there are nozzle materials, get the nozzle-specific material # If there are nozzle materials, get the nozzle-specific material
machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] machine_nozzle_buildplate_material_map = self._diameter_machine_nozzle_buildplate_material_map[rounded_diameter] # type: Dict[str, MaterialNode]
machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id) machine_node = machine_nozzle_buildplate_material_map.get(machine_definition_id)
nozzle_node = None nozzle_node = None
buildplate_node = None buildplate_node = None
@ -426,7 +434,7 @@ class MaterialManager(QObject):
# Look at the guid to material dictionary # Look at the guid to material dictionary
root_material_id = None root_material_id = None
for material_group in self._guid_material_groups_map[material_guid]: for material_group in self._guid_material_groups_map[material_guid]:
root_material_id = material_group.root_material_node.metadata["id"] root_material_id = cast(str, material_group.root_material_node.getMetaDataEntry("id", ""))
break break
if not root_material_id: if not root_material_id:
@ -502,7 +510,7 @@ class MaterialManager(QObject):
# Sets the new name for the given material. # Sets the new name for the given material.
# #
@pyqtSlot("QVariant", str) @pyqtSlot("QVariant", str)
def setMaterialName(self, material_node: "MaterialNode", name: str): def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
root_material_id = material_node.getMetaDataEntry("base_file") root_material_id = material_node.getMetaDataEntry("base_file")
if root_material_id is None: if root_material_id is None:
return return
@ -520,7 +528,7 @@ class MaterialManager(QObject):
# Removes the given material. # Removes the given material.
# #
@pyqtSlot("QVariant") @pyqtSlot("QVariant")
def removeMaterial(self, material_node: "MaterialNode"): def removeMaterial(self, material_node: "MaterialNode") -> None:
root_material_id = material_node.getMetaDataEntry("base_file") root_material_id = material_node.getMetaDataEntry("base_file")
if root_material_id is not None: if root_material_id is not None:
self.removeMaterialByRootId(root_material_id) self.removeMaterialByRootId(root_material_id)
@ -530,8 +538,8 @@ class MaterialManager(QObject):
# Returns the root material ID of the duplicated material if successful. # Returns the root material ID of the duplicated material if successful.
# #
@pyqtSlot("QVariant", result = str) @pyqtSlot("QVariant", result = str)
def duplicateMaterial(self, material_node, new_base_id = None, new_metadata = None) -> Optional[str]: def duplicateMaterial(self, material_node: MaterialNode, new_base_id: Optional[str] = None, new_metadata: Dict[str, Any] = None) -> Optional[str]:
root_material_id = material_node.metadata["base_file"] root_material_id = cast(str, material_node.getMetaDataEntry("base_file", ""))
material_group = self.getMaterialGroup(root_material_id) material_group = self.getMaterialGroup(root_material_id)
if not material_group: if not material_group:
@ -586,7 +594,7 @@ class MaterialManager(QObject):
# #
# Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID. # Create a new material by cloning Generic PLA for the current material diameter and generate a new GUID.
# # Returns the ID of the newly created material.
@pyqtSlot(result = str) @pyqtSlot(result = str)
def createMaterial(self) -> str: def createMaterial(self) -> str:
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
@ -619,7 +627,7 @@ class MaterialManager(QObject):
return new_id return new_id
@pyqtSlot(str) @pyqtSlot(str)
def addFavorite(self, root_material_id: str): def addFavorite(self, root_material_id: str) -> None:
self._favorites.add(root_material_id) self._favorites.add(root_material_id)
self.favoritesUpdated.emit() self.favoritesUpdated.emit()
@ -628,7 +636,7 @@ class MaterialManager(QObject):
self._application.saveSettings() self._application.saveSettings()
@pyqtSlot(str) @pyqtSlot(str)
def removeFavorite(self, root_material_id: str): def removeFavorite(self, root_material_id: str) -> None:
self._favorites.remove(root_material_id) self._favorites.remove(root_material_id)
self.favoritesUpdated.emit() self.favoritesUpdated.emit()

View file

@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Dict from typing import Optional, Dict, Any
from collections import OrderedDict
from .ContainerNode import ContainerNode from .ContainerNode import ContainerNode
@ -14,6 +14,12 @@ from .ContainerNode import ContainerNode
class MaterialNode(ContainerNode): class MaterialNode(ContainerNode):
__slots__ = ("material_map", "children_map") __slots__ = ("material_map", "children_map")
def __init__(self, metadata: Optional[dict] = None) -> None: def __init__(self, metadata: Optional[Dict[str, Any]] = None) -> None:
super().__init__(metadata = metadata) super().__init__(metadata = metadata)
self.material_map = {} # type: Dict[str, MaterialNode] # material_root_id -> material_node self.material_map = {} # type: Dict[str, MaterialNode] # material_root_id -> material_node
# We overide this as we want to indicate that MaterialNodes can only contain other material nodes.
self.children_map = OrderedDict() # type: OrderedDict[str, "MaterialNode"]
def getChildNode(self, child_key: str) -> Optional["MaterialNode"]:
return self.children_map.get(child_key)

View file

@ -113,7 +113,7 @@ class BaseMaterialsModel(ListModel):
## This is another convenience function which is shared by all material ## This is another convenience function which is shared by all material
# models so it's put here to avoid having so much duplicated code. # models so it's put here to avoid having so much duplicated code.
def _createMaterialItem(self, root_material_id, container_node): def _createMaterialItem(self, root_material_id, container_node):
metadata = container_node.metadata metadata = container_node.getMetadata()
item = { item = {
"root_material_id": root_material_id, "root_material_id": root_material_id,
"id": metadata["id"], "id": metadata["id"],
@ -124,7 +124,7 @@ class BaseMaterialsModel(ListModel):
"description": metadata["description"], "description": metadata["description"],
"material": metadata["material"], "material": metadata["material"],
"color_name": metadata["color_name"], "color_name": metadata["color_name"],
"color_code": metadata["color_code"], "color_code": metadata.get("color_code", ""),
"density": metadata.get("properties", {}).get("density", ""), "density": metadata.get("properties", {}).get("density", ""),
"diameter": metadata.get("properties", {}).get("diameter", ""), "diameter": metadata.get("properties", {}).get("diameter", ""),
"approximate_diameter": metadata["approximate_diameter"], "approximate_diameter": metadata["approximate_diameter"],

View file

@ -23,7 +23,7 @@ class FavoriteMaterialsModel(BaseMaterialsModel):
item_list = [] item_list = []
for root_material_id, container_node in self._available_materials.items(): for root_material_id, container_node in self._available_materials.items():
metadata = container_node.metadata metadata = container_node.getMetadata()
# Do not include the materials from a to-be-removed package # Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)): if bool(metadata.get("removed", False)):

View file

@ -23,7 +23,7 @@ class GenericMaterialsModel(BaseMaterialsModel):
item_list = [] item_list = []
for root_material_id, container_node in self._available_materials.items(): for root_material_id, container_node in self._available_materials.items():
metadata = container_node.metadata metadata = container_node.getMetadata()
# Do not include the materials from a to-be-removed package # Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)): if bool(metadata.get("removed", False)):

View file

@ -41,21 +41,19 @@ class MaterialBrandsModel(BaseMaterialsModel):
# Part 1: Generate the entire tree of brands -> material types -> spcific materials # Part 1: Generate the entire tree of brands -> material types -> spcific materials
for root_material_id, container_node in self._available_materials.items(): for root_material_id, container_node in self._available_materials.items():
metadata = container_node.metadata
# Do not include the materials from a to-be-removed package # Do not include the materials from a to-be-removed package
if bool(metadata.get("removed", False)): if bool(container_node.getMetaDataEntry("removed", False)):
continue continue
# Add brands we haven't seen yet to the dict, skipping generics # Add brands we haven't seen yet to the dict, skipping generics
brand = metadata["brand"] brand = container_node.getMetaDataEntry("brand", "")
if brand.lower() == "generic": if brand.lower() == "generic":
continue continue
if brand not in brand_group_dict: if brand not in brand_group_dict:
brand_group_dict[brand] = {} brand_group_dict[brand] = {}
# Add material types we haven't seen yet to the dict # Add material types we haven't seen yet to the dict
material_type = metadata["material"] material_type = container_node.getMetaDataEntry("material", "")
if material_type not in brand_group_dict[brand]: if material_type not in brand_group_dict[brand]:
brand_group_dict[brand][material_type] = [] brand_group_dict[brand][material_type] = []

View file

@ -6,10 +6,10 @@ from PyQt5.QtCore import Qt
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.Settings.SettingFunction import SettingFunction
from cura.Machines.QualityManager import QualityGroup from cura.Machines.QualityManager import QualityGroup
# #
# QML Model for all built-in quality profiles. This model is used for the drop-down quality menu. # QML Model for all built-in quality profiles. This model is used for the drop-down quality menu.
# #
@ -106,4 +106,8 @@ class QualityProfilesDropDownMenuModel(ListModel):
container = global_stack.definition container = global_stack.definition
if container and container.hasProperty("layer_height", "value"): if container and container.hasProperty("layer_height", "value"):
layer_height = container.getProperty("layer_height", "value") layer_height = container.getProperty("layer_height", "value")
if isinstance(layer_height, SettingFunction):
layer_height = layer_height(global_stack)
return float(layer_height) return float(layer_height)

View file

@ -17,7 +17,7 @@ class QualityChangesGroup(QualityGroup):
super().__init__(name, quality_type, parent) super().__init__(name, quality_type, parent)
self._container_registry = Application.getInstance().getContainerRegistry() self._container_registry = Application.getInstance().getContainerRegistry()
def addNode(self, node: "QualityNode"): def addNode(self, node: "QualityNode") -> None:
extruder_position = node.getMetaDataEntry("position") extruder_position = node.getMetaDataEntry("position")
if extruder_position is None and self.node_for_global is not None or extruder_position in self.nodes_for_extruders: #We would be overwriting another node. if extruder_position is None and self.node_for_global is not None or extruder_position in self.nodes_for_extruders: #We would be overwriting another node.

View file

@ -6,6 +6,7 @@ from typing import Dict, Optional, List, Set
from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtCore import QObject, pyqtSlot
from cura.Machines.ContainerNode import ContainerNode from cura.Machines.ContainerNode import ContainerNode
# #
# A QualityGroup represents a group of containers that must be applied to each ContainerStack when it's used. # A QualityGroup represents a group of containers that must be applied to each ContainerStack when it's used.
# Some concrete examples are Quality and QualityChanges: when we select quality type "normal", this quality type # Some concrete examples are Quality and QualityChanges: when we select quality type "normal", this quality type
@ -34,7 +35,7 @@ class QualityGroup(QObject):
return self.name return self.name
def getAllKeys(self) -> Set[str]: def getAllKeys(self) -> Set[str]:
result = set() #type: Set[str] result = set() # type: Set[str]
for node in [self.node_for_global] + list(self.nodes_for_extruders.values()): for node in [self.node_for_global] + list(self.nodes_for_extruders.values()):
if node is None: if node is None:
continue continue

View file

@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING, Optional, cast from typing import TYPE_CHECKING, Optional, cast, Dict, List
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
@ -20,6 +20,8 @@ if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from .QualityChangesGroup import QualityChangesGroup from .QualityChangesGroup import QualityChangesGroup
from cura.CuraApplication import CuraApplication
from UM.Settings.ContainerRegistry import ContainerRegistry
# #
@ -36,17 +38,20 @@ class QualityManager(QObject):
qualitiesUpdated = pyqtSignal() qualitiesUpdated = pyqtSignal()
def __init__(self, container_registry, parent = None): def __init__(self, container_registry: "ContainerRegistry", parent = None) -> None:
super().__init__(parent) super().__init__(parent)
self._application = Application.getInstance() self._application = Application.getInstance() # type: CuraApplication
self._material_manager = self._application.getMaterialManager() self._material_manager = self._application.getMaterialManager()
self._container_registry = container_registry self._container_registry = container_registry
self._empty_quality_container = self._application.empty_quality_container self._empty_quality_container = self._application.empty_quality_container
self._empty_quality_changes_container = self._application.empty_quality_changes_container self._empty_quality_changes_container = self._application.empty_quality_changes_container
self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # for quality lookup # For quality lookup
self._machine_quality_type_to_quality_changes_dict = {} # for quality_changes lookup self._machine_nozzle_buildplate_material_quality_type_to_quality_dict = {} # type: Dict[str, QualityNode]
# For quality_changes lookup
self._machine_quality_type_to_quality_changes_dict = {} # type: Dict[str, QualityNode]
self._default_machine_definition_id = "fdmprinter" self._default_machine_definition_id = "fdmprinter"
@ -62,7 +67,7 @@ class QualityManager(QObject):
self._update_timer.setSingleShot(True) self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._updateMaps) self._update_timer.timeout.connect(self._updateMaps)
def initialize(self): def initialize(self) -> None:
# Initialize the lookup tree for quality profiles with following structure: # Initialize the lookup tree for quality profiles with following structure:
# <machine> -> <nozzle> -> <buildplate> -> <material> # <machine> -> <nozzle> -> <buildplate> -> <material>
# <machine> -> <material> # <machine> -> <material>
@ -133,13 +138,13 @@ class QualityManager(QObject):
Logger.log("d", "Lookup tables updated.") Logger.log("d", "Lookup tables updated.")
self.qualitiesUpdated.emit() self.qualitiesUpdated.emit()
def _updateMaps(self): def _updateMaps(self) -> None:
self.initialize() self.initialize()
def _onContainerMetadataChanged(self, container): def _onContainerMetadataChanged(self, container: InstanceContainer) -> None:
self._onContainerChanged(container) self._onContainerChanged(container)
def _onContainerChanged(self, container): def _onContainerChanged(self, container: InstanceContainer) -> None:
container_type = container.getMetaDataEntry("type") container_type = container.getMetaDataEntry("type")
if container_type not in ("quality", "quality_changes"): if container_type not in ("quality", "quality_changes"):
return return
@ -148,7 +153,7 @@ class QualityManager(QObject):
self._update_timer.start() self._update_timer.start()
# Updates the given quality groups' availabilities according to which extruders are being used/ enabled. # Updates the given quality groups' availabilities according to which extruders are being used/ enabled.
def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list): def _updateQualityGroupsAvailability(self, machine: "GlobalStack", quality_group_list) -> None:
used_extruders = set() used_extruders = set()
for i in range(machine.getProperty("machine_extruder_count", "value")): for i in range(machine.getProperty("machine_extruder_count", "value")):
if str(i) in machine.extruders and machine.extruders[str(i)].isEnabled: if str(i) in machine.extruders and machine.extruders[str(i)].isEnabled:
@ -196,32 +201,42 @@ class QualityManager(QObject):
# Whether a QualityGroup is available can be unknown via the field QualityGroup.is_available. # Whether a QualityGroup is available can be unknown via the field QualityGroup.is_available.
# For more details, see QualityGroup. # For more details, see QualityGroup.
# #
def getQualityGroups(self, machine: "GlobalStack") -> dict: def getQualityGroups(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition) machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
# This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks # This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks
has_variant_materials = parseBool(machine.getMetaDataEntry("has_variant_materials", False)) has_machine_specific_qualities = machine.getHasMachineQuality()
# To find the quality container for the GlobalStack, check in the following fall-back manner: # To find the quality container for the GlobalStack, check in the following fall-back manner:
# (1) the machine-specific node # (1) the machine-specific node
# (2) the generic node # (2) the generic node
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id) machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
# Check if this machine has specific quality profiles for its extruders, if so, when looking up extruder
# qualities, we should not fall back to use the global qualities.
has_extruder_specific_qualities = False
if machine_node:
if machine_node.children_map:
has_extruder_specific_qualities = True
default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(self._default_machine_definition_id) default_machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(self._default_machine_definition_id)
nodes_to_check = [machine_node, default_machine_node]
nodes_to_check = [] # type: List[QualityNode]
if machine_node is not None:
nodes_to_check.append(machine_node)
if default_machine_node is not None:
nodes_to_check.append(default_machine_node)
# Iterate over all quality_types in the machine node # Iterate over all quality_types in the machine node
quality_group_dict = {} quality_group_dict = {}
for node in nodes_to_check: for node in nodes_to_check:
if node and node.quality_type_map: if node and node.quality_type_map:
# Only include global qualities quality_node = list(node.quality_type_map.values())[0]
if has_variant_materials: is_global_quality = parseBool(quality_node.getMetaDataEntry("global_quality", False))
quality_node = list(node.quality_type_map.values())[0] if not is_global_quality:
is_global_quality = parseBool(quality_node.metadata.get("global_quality", False)) continue
if not is_global_quality:
continue
for quality_type, quality_node in node.quality_type_map.items(): for quality_type, quality_node in node.quality_type_map.items():
quality_group = QualityGroup(quality_node.metadata["name"], quality_type) quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
quality_group.node_for_global = quality_node quality_group.node_for_global = quality_node
quality_group_dict[quality_type] = quality_group quality_group_dict[quality_type] = quality_group
break break
@ -268,13 +283,16 @@ class QualityManager(QObject):
# Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into # Each points above can be represented as a node in the lookup tree, so here we simply put those nodes into
# the list with priorities as the order. Later, we just need to loop over each node in this list and fetch # the list with priorities as the order. Later, we just need to loop over each node in this list and fetch
# qualities from there. # qualities from there.
node_info_list_0 = [nozzle_name, buildplate_name, root_material_id] node_info_list_0 = [nozzle_name, buildplate_name, root_material_id] # type: List[Optional[str]]
nodes_to_check = [] nodes_to_check = []
# This function tries to recursively find the deepest (the most specific) branch and add those nodes to # This function tries to recursively find the deepest (the most specific) branch and add those nodes to
# the search list in the order described above. So, by iterating over that search node list, we first look # the search list in the order described above. So, by iterating over that search node list, we first look
# in the more specific branches and then the less specific (generic) ones. # in the more specific branches and then the less specific (generic) ones.
def addNodesToCheck(node, nodes_to_check_list, node_info_list, node_info_idx): def addNodesToCheck(node: Optional[QualityNode], nodes_to_check_list: List[QualityNode], node_info_list, node_info_idx: int) -> None:
if node is None:
return
if node_info_idx < len(node_info_list): if node_info_idx < len(node_info_list):
node_name = node_info_list[node_info_idx] node_name = node_info_list[node_info_idx]
if node_name is not None: if node_name is not None:
@ -295,35 +313,42 @@ class QualityManager(QObject):
# The last fall back will be the global qualities (either from the machine-specific node or the generic # The last fall back will be the global qualities (either from the machine-specific node or the generic
# node), but we only use one. For details see the overview comments above. # node), but we only use one. For details see the overview comments above.
if machine_node.quality_type_map:
if machine_node is not None and machine_node.quality_type_map:
nodes_to_check += [machine_node] nodes_to_check += [machine_node]
else: elif default_machine_node is not None:
nodes_to_check += [default_machine_node] nodes_to_check += [default_machine_node]
for node in nodes_to_check: for node_idx, node in enumerate(nodes_to_check):
if node and node.quality_type_map: if node and node.quality_type_map:
if has_variant_materials: if has_extruder_specific_qualities:
# Only include variant qualities; skip non global qualities # Only include variant qualities; skip non global qualities
quality_node = list(node.quality_type_map.values())[0] quality_node = list(node.quality_type_map.values())[0]
is_global_quality = parseBool(quality_node.metadata.get("global_quality", False)) is_global_quality = parseBool(quality_node.getMetaDataEntry("global_quality", False))
if is_global_quality: if is_global_quality:
continue continue
for quality_type, quality_node in node.quality_type_map.items(): for quality_type, quality_node in node.quality_type_map.items():
if quality_type not in quality_group_dict: if quality_type not in quality_group_dict:
quality_group = QualityGroup(quality_node.metadata["name"], quality_type) quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
quality_group_dict[quality_type] = quality_group quality_group_dict[quality_type] = quality_group
quality_group = quality_group_dict[quality_type] quality_group = quality_group_dict[quality_type]
if position not in quality_group.nodes_for_extruders: if position not in quality_group.nodes_for_extruders:
quality_group.nodes_for_extruders[position] = quality_node quality_group.nodes_for_extruders[position] = quality_node
# If the machine has its own specific qualities, for extruders, it should skip the global qualities
# and use the material/variant specific qualities.
if has_extruder_specific_qualities:
if node_idx == len(nodes_to_check) - 1:
break
# Update availabilities for each quality group # Update availabilities for each quality group
self._updateQualityGroupsAvailability(machine, quality_group_dict.values()) self._updateQualityGroupsAvailability(machine, quality_group_dict.values())
return quality_group_dict return quality_group_dict
def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> dict: def getQualityGroupsForMachineDefinition(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition) machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
# To find the quality container for the GlobalStack, check in the following fall-back manner: # To find the quality container for the GlobalStack, check in the following fall-back manner:
@ -339,7 +364,7 @@ class QualityManager(QObject):
for node in nodes_to_check: for node in nodes_to_check:
if node and node.quality_type_map: if node and node.quality_type_map:
for quality_type, quality_node in node.quality_type_map.items(): for quality_type, quality_node in node.quality_type_map.items():
quality_group = QualityGroup(quality_node.metadata["name"], quality_type) quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
quality_group.node_for_global = quality_node quality_group.node_for_global = quality_node
quality_group_dict[quality_type] = quality_group quality_group_dict[quality_type] = quality_group
break break
@ -361,10 +386,21 @@ class QualityManager(QObject):
# Remove the given quality changes group. # Remove the given quality changes group.
# #
@pyqtSlot(QObject) @pyqtSlot(QObject)
def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup"): def removeQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
Logger.log("i", "Removing quality changes group [%s]", quality_changes_group.name) Logger.log("i", "Removing quality changes group [%s]", quality_changes_group.name)
removed_quality_changes_ids = set()
for node in quality_changes_group.getAllNodes(): for node in quality_changes_group.getAllNodes():
self._container_registry.removeContainer(node.getMetaDataEntry("id")) container_id = node.getMetaDataEntry("id")
self._container_registry.removeContainer(container_id)
removed_quality_changes_ids.add(container_id)
# Reset all machines that have activated this quality changes to empty.
for global_stack in self._container_registry.findContainerStacks(type = "machine"):
if global_stack.qualityChanges.getId() in removed_quality_changes_ids:
global_stack.qualityChanges = self._empty_quality_changes_container
for extruder_stack in self._container_registry.findContainerStacks(type = "extruder_train"):
if extruder_stack.qualityChanges.getId() in removed_quality_changes_ids:
extruder_stack.qualityChanges = self._empty_quality_changes_container
# #
# Rename a set of quality changes containers. Returns the new name. # Rename a set of quality changes containers. Returns the new name.
@ -393,7 +429,7 @@ class QualityManager(QObject):
# Duplicates the given quality. # Duplicates the given quality.
# #
@pyqtSlot(str, "QVariantMap") @pyqtSlot(str, "QVariantMap")
def duplicateQualityChanges(self, quality_changes_name, quality_model_item): def duplicateQualityChanges(self, quality_changes_name: str, quality_model_item) -> None:
global_stack = self._application.getGlobalContainerStack() global_stack = self._application.getGlobalContainerStack()
if not global_stack: if not global_stack:
Logger.log("i", "No active global stack, cannot duplicate quality changes.") Logger.log("i", "No active global stack, cannot duplicate quality changes.")
@ -421,7 +457,7 @@ class QualityManager(QObject):
# the user containers in each stack. These then replace the quality_changes containers in the # the user containers in each stack. These then replace the quality_changes containers in the
# stack and clear the user settings. # stack and clear the user settings.
@pyqtSlot(str) @pyqtSlot(str)
def createQualityChanges(self, base_name): def createQualityChanges(self, base_name: str) -> None:
machine_manager = Application.getInstance().getMachineManager() machine_manager = Application.getInstance().getMachineManager()
global_stack = machine_manager.activeMachine global_stack = machine_manager.activeMachine

View file

@ -16,6 +16,9 @@ class QualityNode(ContainerNode):
super().__init__(metadata = metadata) super().__init__(metadata = metadata)
self.quality_type_map = {} # type: Dict[str, QualityNode] # quality_type -> QualityNode for InstanceContainer self.quality_type_map = {} # type: Dict[str, QualityNode] # quality_type -> QualityNode for InstanceContainer
def getChildNode(self, child_key: str) -> Optional["QualityNode"]:
return self.children_map.get(child_key)
def addQualityMetadata(self, quality_type: str, metadata: dict): def addQualityMetadata(self, quality_type: str, metadata: dict):
if quality_type not in self.quality_type_map: if quality_type not in self.quality_type_map:
self.quality_type_map[quality_type] = QualityNode(metadata) self.quality_type_map[quality_type] = QualityNode(metadata)

View file

@ -2,7 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from collections import OrderedDict from collections import OrderedDict
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING, Dict
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Logger import Logger from UM.Logger import Logger
@ -36,11 +36,11 @@ if TYPE_CHECKING:
# #
class VariantManager: class VariantManager:
def __init__(self, container_registry): def __init__(self, container_registry: ContainerRegistry) -> None:
self._container_registry = container_registry # type: ContainerRegistry self._container_registry = container_registry
self._machine_to_variant_dict_map = dict() # <machine_type> -> <variant_dict> self._machine_to_variant_dict_map = dict() # type: Dict[str, Dict["VariantType", Dict[str, ContainerNode]]]
self._machine_to_buildplate_dict_map = dict() self._machine_to_buildplate_dict_map = dict() # type: Dict[str, Dict[str, ContainerNode]]
self._exclude_variant_id_list = ["empty_variant"] self._exclude_variant_id_list = ["empty_variant"]
@ -48,7 +48,7 @@ class VariantManager:
# Initializes the VariantManager including: # Initializes the VariantManager including:
# - initializing the variant lookup table based on the metadata in ContainerRegistry. # - initializing the variant lookup table based on the metadata in ContainerRegistry.
# #
def initialize(self): def initialize(self) -> None:
self._machine_to_variant_dict_map = OrderedDict() self._machine_to_variant_dict_map = OrderedDict()
self._machine_to_buildplate_dict_map = OrderedDict() self._machine_to_buildplate_dict_map = OrderedDict()
@ -106,10 +106,10 @@ class VariantManager:
variant_node = variant_dict[variant_name] variant_node = variant_dict[variant_name]
break break
return variant_node return variant_node
return self._machine_to_variant_dict_map[machine_definition_id].get(variant_type, {}).get(variant_name) return self._machine_to_variant_dict_map[machine_definition_id].get(variant_type, {}).get(variant_name)
def getVariantNodes(self, machine: "GlobalStack", def getVariantNodes(self, machine: "GlobalStack", variant_type: "VariantType") -> Dict[str, ContainerNode]:
variant_type: Optional["VariantType"] = None) -> dict:
machine_definition_id = machine.definition.getId() machine_definition_id = machine.definition.getId()
return self._machine_to_variant_dict_map.get(machine_definition_id, {}).get(variant_type, {}) return self._machine_to_variant_dict_map.get(machine_definition_id, {}).get(variant_type, {})

View file

@ -0,0 +1,27 @@
from PyQt5.QtGui import QImage
from PyQt5.QtQuick import QQuickImageProvider
from PyQt5.QtCore import QSize
from UM.Application import Application
class PrintJobPreviewImageProvider(QQuickImageProvider):
def __init__(self):
super().__init__(QQuickImageProvider.Image)
## Request a new image.
def requestImage(self, id: str, size: QSize) -> QImage:
# The id will have an uuid and an increment separated by a slash. As we don't care about the value of the
# increment, we need to strip that first.
uuid = id[id.find("/") + 1:]
for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
if not hasattr(output_device, "printJobs"):
continue
for print_job in output_device.printJobs:
if print_job.key == uuid:
if print_job.getPreviewImage():
return print_job.getPreviewImage(), QSize(15, 15)
else:
return QImage(), QSize(15, 15)
return QImage(), QSize(15,15)

View file

@ -27,7 +27,13 @@ class ConfigurationModel(QObject):
return self._printer_type return self._printer_type
def setExtruderConfigurations(self, extruder_configurations): def setExtruderConfigurations(self, extruder_configurations):
self._extruder_configurations = extruder_configurations if self._extruder_configurations != extruder_configurations:
self._extruder_configurations = extruder_configurations
for extruder_configuration in self._extruder_configurations:
extruder_configuration.extruderConfigurationChanged.connect(self.configurationChanged)
self.configurationChanged.emit()
@pyqtProperty("QVariantList", fset = setExtruderConfigurations, notify = configurationChanged) @pyqtProperty("QVariantList", fset = setExtruderConfigurations, notify = configurationChanged)
def extruderConfigurations(self): def extruderConfigurations(self):

View file

@ -1,56 +1,67 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
class ExtruderConfigurationModel(QObject): class ExtruderConfigurationModel(QObject):
extruderConfigurationChanged = pyqtSignal() extruderConfigurationChanged = pyqtSignal()
def __init__(self): def __init__(self, position: int = -1) -> None:
super().__init__() super().__init__()
self._position = -1 self._position = position # type: int
self._material = None self._material = None # type: Optional[MaterialOutputModel]
self._hotend_id = None self._hotend_id = None # type: Optional[str]
def setPosition(self, position): def setPosition(self, position: int) -> None:
self._position = position self._position = position
@pyqtProperty(int, fset = setPosition, notify = extruderConfigurationChanged) @pyqtProperty(int, fset = setPosition, notify = extruderConfigurationChanged)
def position(self): def position(self) -> int:
return self._position return self._position
def setMaterial(self, material): def setMaterial(self, material: Optional[MaterialOutputModel]) -> None:
self._material = material if self._hotend_id != material:
self._material = material
self.extruderConfigurationChanged.emit()
@pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged) @pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged)
def material(self): def activeMaterial(self) -> Optional[MaterialOutputModel]:
return self._material return self._material
def setHotendID(self, hotend_id): @pyqtProperty(QObject, fset=setMaterial, notify=extruderConfigurationChanged)
self._hotend_id = hotend_id def material(self) -> Optional[MaterialOutputModel]:
return self._material
def setHotendID(self, hotend_id: Optional[str]) -> None:
if self._hotend_id != hotend_id:
self._hotend_id = hotend_id
self.extruderConfigurationChanged.emit()
@pyqtProperty(str, fset = setHotendID, notify = extruderConfigurationChanged) @pyqtProperty(str, fset = setHotendID, notify = extruderConfigurationChanged)
def hotendID(self): def hotendID(self) -> Optional[str]:
return self._hotend_id return self._hotend_id
## This method is intended to indicate whether the configuration is valid or not. ## This method is intended to indicate whether the configuration is valid or not.
# The method checks if the mandatory fields are or not set # The method checks if the mandatory fields are or not set
# At this moment is always valid since we allow to have empty material and variants. # At this moment is always valid since we allow to have empty material and variants.
def isValid(self): def isValid(self) -> bool:
return True return True
def __str__(self): def __str__(self) -> str:
message_chunks = [] message_chunks = []
message_chunks.append("Position: " + str(self._position)) message_chunks.append("Position: " + str(self._position))
message_chunks.append("-") message_chunks.append("-")
message_chunks.append("Material: " + self.material.type if self.material else "empty") message_chunks.append("Material: " + self.activeMaterial.type if self.activeMaterial else "empty")
message_chunks.append("-") message_chunks.append("-")
message_chunks.append("HotendID: " + self.hotendID if self.hotendID else "empty") message_chunks.append("HotendID: " + self.hotendID if self.hotendID else "empty")
return " ".join(message_chunks) return " ".join(message_chunks)
def __eq__(self, other): def __eq__(self, other) -> bool:
return hash(self) == hash(other) return hash(self) == hash(other)
# Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is # Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is

View file

@ -12,64 +12,61 @@ if TYPE_CHECKING:
class ExtruderOutputModel(QObject): class ExtruderOutputModel(QObject):
hotendIDChanged = pyqtSignal()
targetHotendTemperatureChanged = pyqtSignal() targetHotendTemperatureChanged = pyqtSignal()
hotendTemperatureChanged = pyqtSignal() hotendTemperatureChanged = pyqtSignal()
activeMaterialChanged = pyqtSignal()
extruderConfigurationChanged = pyqtSignal() extruderConfigurationChanged = pyqtSignal()
isPreheatingChanged = pyqtSignal() isPreheatingChanged = pyqtSignal()
def __init__(self, printer: "PrinterOutputModel", position, parent=None) -> None: def __init__(self, printer: "PrinterOutputModel", position: int, parent=None) -> None:
super().__init__(parent) super().__init__(parent)
self._printer = printer self._printer = printer # type: PrinterOutputModel
self._position = position self._position = position
self._target_hotend_temperature = 0 # type: float self._target_hotend_temperature = 0.0 # type: float
self._hotend_temperature = 0 # type: float self._hotend_temperature = 0.0 # type: float
self._hotend_id = ""
self._active_material = None # type: Optional[MaterialOutputModel]
self._extruder_configuration = ExtruderConfigurationModel()
self._extruder_configuration.position = self._position
self._is_preheating = False self._is_preheating = False
def getPrinter(self): # The extruder output model wraps the configuration model. This way we can use the same config model for jobs
# and extruders alike.
self._extruder_configuration = ExtruderConfigurationModel()
self._extruder_configuration.position = self._position
self._extruder_configuration.extruderConfigurationChanged.connect(self.extruderConfigurationChanged)
def getPrinter(self) -> "PrinterOutputModel":
return self._printer return self._printer
def getPosition(self): def getPosition(self) -> int:
return self._position return self._position
# Does the printer support pre-heating the bed at all # Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def canPreHeatHotends(self): def canPreHeatHotends(self) -> bool:
if self._printer: if self._printer:
return self._printer.canPreHeatHotends return self._printer.canPreHeatHotends
return False return False
@pyqtProperty(QObject, notify = activeMaterialChanged) @pyqtProperty(QObject, notify = extruderConfigurationChanged)
def activeMaterial(self) -> Optional["MaterialOutputModel"]: def activeMaterial(self) -> Optional["MaterialOutputModel"]:
return self._active_material return self._extruder_configuration.activeMaterial
def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]): def updateActiveMaterial(self, material: Optional["MaterialOutputModel"]) -> None:
if self._active_material != material: self._extruder_configuration.setMaterial(material)
self._active_material = material
self._extruder_configuration.material = self._active_material
self.activeMaterialChanged.emit()
self.extruderConfigurationChanged.emit()
## Update the hotend temperature. This only changes it locally. ## Update the hotend temperature. This only changes it locally.
def updateHotendTemperature(self, temperature: float): def updateHotendTemperature(self, temperature: float) -> None:
if self._hotend_temperature != temperature: if self._hotend_temperature != temperature:
self._hotend_temperature = temperature self._hotend_temperature = temperature
self.hotendTemperatureChanged.emit() self.hotendTemperatureChanged.emit()
def updateTargetHotendTemperature(self, temperature: float): def updateTargetHotendTemperature(self, temperature: float) -> None:
if self._target_hotend_temperature != temperature: if self._target_hotend_temperature != temperature:
self._target_hotend_temperature = temperature self._target_hotend_temperature = temperature
self.targetHotendTemperatureChanged.emit() self.targetHotendTemperatureChanged.emit()
## Set the target hotend temperature. This ensures that it's actually sent to the remote. ## Set the target hotend temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(float) @pyqtSlot(float)
def setTargetHotendTemperature(self, temperature: float): def setTargetHotendTemperature(self, temperature: float) -> None:
self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature) self._printer.getController().setTargetHotendTemperature(self._printer, self, temperature)
self.updateTargetHotendTemperature(temperature) self.updateTargetHotendTemperature(temperature)
@ -81,30 +78,26 @@ class ExtruderOutputModel(QObject):
def hotendTemperature(self) -> float: def hotendTemperature(self) -> float:
return self._hotend_temperature return self._hotend_temperature
@pyqtProperty(str, notify = hotendIDChanged) @pyqtProperty(str, notify = extruderConfigurationChanged)
def hotendID(self) -> str: def hotendID(self) -> str:
return self._hotend_id return self._extruder_configuration.hotendID
def updateHotendID(self, id: str): def updateHotendID(self, hotend_id: str) -> None:
if self._hotend_id != id: self._extruder_configuration.setHotendID(hotend_id)
self._hotend_id = id
self._extruder_configuration.hotendID = self._hotend_id
self.hotendIDChanged.emit()
self.extruderConfigurationChanged.emit()
@pyqtProperty(QObject, notify = extruderConfigurationChanged) @pyqtProperty(QObject, notify = extruderConfigurationChanged)
def extruderConfiguration(self): def extruderConfiguration(self) -> Optional[ExtruderConfigurationModel]:
if self._extruder_configuration.isValid(): if self._extruder_configuration.isValid():
return self._extruder_configuration return self._extruder_configuration
return None return None
def updateIsPreheating(self, pre_heating): def updateIsPreheating(self, pre_heating: bool) -> None:
if self._is_preheating != pre_heating: if self._is_preheating != pre_heating:
self._is_preheating = pre_heating self._is_preheating = pre_heating
self.isPreheatingChanged.emit() self.isPreheatingChanged.emit()
@pyqtProperty(bool, notify=isPreheatingChanged) @pyqtProperty(bool, notify=isPreheatingChanged)
def isPreheating(self): def isPreheating(self) -> bool:
return self._is_preheating return self._is_preheating
## Pre-heats the extruder before printer. ## Pre-heats the extruder before printer.
@ -113,9 +106,9 @@ class ExtruderOutputModel(QObject):
# Celsius. # Celsius.
# \param duration How long the bed should stay warm, in seconds. # \param duration How long the bed should stay warm, in seconds.
@pyqtSlot(float, float) @pyqtSlot(float, float)
def preheatHotend(self, temperature, duration): def preheatHotend(self, temperature: float, duration: float) -> None:
self._printer._controller.preheatHotend(self, temperature, duration) self._printer._controller.preheatHotend(self, temperature, duration)
@pyqtSlot() @pyqtSlot()
def cancelPreheatHotend(self): def cancelPreheatHotend(self) -> None:
self._printer._controller.cancelPreheatHotend(self) self._printer._controller.cancelPreheatHotend(self)

View file

@ -188,40 +188,55 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
if reply in self._kept_alive_multiparts: if reply in self._kept_alive_multiparts:
del self._kept_alive_multiparts[reply] del self._kept_alive_multiparts[reply]
def put(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: def _validateManager(self) -> None:
if self._manager is None: if self._manager is None:
self._createNetworkManager() self._createNetworkManager()
assert (self._manager is not None) assert (self._manager is not None)
def put(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
self._validateManager()
request = self._createEmptyRequest(target) request = self._createEmptyRequest(target)
self._last_request_time = time() self._last_request_time = time()
reply = self._manager.put(request, data.encode()) if self._manager is not None:
self._registerOnFinishedCallback(reply, on_finished) reply = self._manager.put(request, data.encode())
self._registerOnFinishedCallback(reply, on_finished)
else:
Logger.log("e", "Could not find manager.")
def delete(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
self._validateManager()
request = self._createEmptyRequest(target)
self._last_request_time = time()
if self._manager is not None:
reply = self._manager.deleteResource(request)
self._registerOnFinishedCallback(reply, on_finished)
else:
Logger.log("e", "Could not find manager.")
def get(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: def get(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
if self._manager is None: self._validateManager()
self._createNetworkManager()
assert (self._manager is not None)
request = self._createEmptyRequest(target) request = self._createEmptyRequest(target)
self._last_request_time = time() self._last_request_time = time()
reply = self._manager.get(request) if self._manager is not None:
self._registerOnFinishedCallback(reply, on_finished) reply = self._manager.get(request)
self._registerOnFinishedCallback(reply, on_finished)
else:
Logger.log("e", "Could not find manager.")
def post(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None: def post(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None:
if self._manager is None: self._validateManager()
self._createNetworkManager()
assert (self._manager is not None)
request = self._createEmptyRequest(target) request = self._createEmptyRequest(target)
self._last_request_time = time() self._last_request_time = time()
reply = self._manager.post(request, data) if self._manager is not None:
if on_progress is not None: reply = self._manager.post(request, data)
reply.uploadProgress.connect(on_progress) if on_progress is not None:
self._registerOnFinishedCallback(reply, on_finished) reply.uploadProgress.connect(on_progress)
self._registerOnFinishedCallback(reply, on_finished)
else:
Logger.log("e", "Could not find manager.")
def postFormWithParts(self, target: str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> QNetworkReply: def postFormWithParts(self, target: str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> QNetworkReply:
self._validateManager()
if self._manager is None:
self._createNetworkManager()
assert (self._manager is not None)
request = self._createEmptyRequest(target, content_type=None) request = self._createEmptyRequest(target, content_type=None)
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType) multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
for part in parts: for part in parts:
@ -229,15 +244,18 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._last_request_time = time() self._last_request_time = time()
reply = self._manager.post(request, multi_post_part) if self._manager is not None:
reply = self._manager.post(request, multi_post_part)
self._kept_alive_multiparts[reply] = multi_post_part self._kept_alive_multiparts[reply] = multi_post_part
if on_progress is not None: if on_progress is not None:
reply.uploadProgress.connect(on_progress) reply.uploadProgress.connect(on_progress)
self._registerOnFinishedCallback(reply, on_finished) self._registerOnFinishedCallback(reply, on_finished)
return reply return reply
else:
Logger.log("e", "Could not find manager.")
def postForm(self, target: str, header_data: str, body_data: bytes, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None: def postForm(self, target: str, header_data: str, body_data: bytes, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None:
post_part = QHttpPart() post_part = QHttpPart()

View file

@ -2,11 +2,15 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING, List
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QImage
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
class PrintJobOutputModel(QObject): class PrintJobOutputModel(QObject):
@ -17,6 +21,9 @@ class PrintJobOutputModel(QObject):
keyChanged = pyqtSignal() keyChanged = pyqtSignal()
assignedPrinterChanged = pyqtSignal() assignedPrinterChanged = pyqtSignal()
ownerChanged = pyqtSignal() ownerChanged = pyqtSignal()
configurationChanged = pyqtSignal()
previewImageChanged = pyqtSignal()
compatibleMachineFamiliesChanged = pyqtSignal()
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None: def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None:
super().__init__(parent) super().__init__(parent)
@ -29,6 +36,48 @@ class PrintJobOutputModel(QObject):
self._assigned_printer = None # type: Optional[PrinterOutputModel] self._assigned_printer = None # type: Optional[PrinterOutputModel]
self._owner = "" # Who started/owns the print job? self._owner = "" # Who started/owns the print job?
self._configuration = None # type: Optional[ConfigurationModel]
self._compatible_machine_families = [] # type: List[str]
self._preview_image_id = 0
self._preview_image = None # type: Optional[QImage]
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
def compatibleMachineFamilies(self):
# Hack; Some versions of cluster will return a family more than once...
return set(self._compatible_machine_families)
def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None:
if self._compatible_machine_families != compatible_machine_families:
self._compatible_machine_families = compatible_machine_families
self.compatibleMachineFamiliesChanged.emit()
@pyqtProperty(QUrl, notify=previewImageChanged)
def previewImageUrl(self):
self._preview_image_id += 1
# There is an image provider that is called "camera". In order to ensure that the image qml object, that
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
# as new (instead of relying on cached version and thus forces an update.
temp = "image://print_job_preview/" + str(self._preview_image_id) + "/" + self._key
return QUrl(temp, QUrl.TolerantMode)
def getPreviewImage(self) -> Optional[QImage]:
return self._preview_image
def updatePreviewImage(self, preview_image: Optional[QImage]) -> None:
if self._preview_image != preview_image:
self._preview_image = preview_image
self.previewImageChanged.emit()
@pyqtProperty(QObject, notify=configurationChanged)
def configuration(self) -> Optional["ConfigurationModel"]:
return self._configuration
def updateConfiguration(self, configuration: Optional["ConfigurationModel"]) -> None:
if self._configuration != configuration:
self._configuration = configuration
self.configurationChanged.emit()
@pyqtProperty(str, notify=ownerChanged) @pyqtProperty(str, notify=ownerChanged)
def owner(self): def owner(self):
return self._owner return self._owner

View file

@ -35,7 +35,7 @@ class PrinterOutputModel(QObject):
self._key = "" # Unique identifier self._key = "" # Unique identifier
self._controller = output_controller self._controller = output_controller
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)] self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
self._head_position = Vector(0, 0, 0) self._head_position = Vector(0, 0, 0)
self._active_print_job = None # type: Optional[PrintJobOutputModel] self._active_print_job = None # type: Optional[PrintJobOutputModel]
self._firmware_version = firmware_version self._firmware_version = firmware_version
@ -43,9 +43,9 @@ class PrinterOutputModel(QObject):
self._is_preheating = False self._is_preheating = False
self._printer_type = "" self._printer_type = ""
self._buildplate_name = None self._buildplate_name = None
# Update the printer configuration every time any of the extruders changes its configuration
for extruder in self._extruders: self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
extruder.extruderConfigurationChanged.connect(self._updateExtruderConfiguration) self._extruders]
self._camera = None self._camera = None
@ -283,7 +283,3 @@ class PrinterOutputModel(QObject):
if self._printer_configuration.isValid(): if self._printer_configuration.isValid():
return self._printer_configuration return self._printer_configuration
return None return None
def _updateExtruderConfiguration(self):
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._extruders]
self.configurationChanged.emit()

View file

@ -304,7 +304,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
if self._global_stack: if self._global_stack:
self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged) self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged)
self._global_stack.containersChanged.disconnect(self._onChanged) self._global_stack.containersChanged.disconnect(self._onChanged)
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_stack.getId()) extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
for extruder in extruders: for extruder in extruders:
extruder.propertyChanged.disconnect(self._onSettingValueChanged) extruder.propertyChanged.disconnect(self._onSettingValueChanged)
@ -314,7 +314,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
self._global_stack.propertyChanged.connect(self._onSettingValueChanged) self._global_stack.propertyChanged.connect(self._onSettingValueChanged)
self._global_stack.containersChanged.connect(self._onChanged) self._global_stack.containersChanged.connect(self._onChanged)
extruders = ExtruderManager.getInstance().getMachineExtruders(self._global_stack.getId()) extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
for extruder in extruders: for extruder in extruders:
extruder.propertyChanged.connect(self._onSettingValueChanged) extruder.propertyChanged.connect(self._onSettingValueChanged)

View file

@ -4,12 +4,12 @@
import os import os
import urllib.parse import urllib.parse
import uuid import uuid
from typing import Any from typing import Dict, Union, Any, TYPE_CHECKING, List
from typing import Dict, Union, Optional
from PyQt5.QtCore import QObject, QUrl, QVariant from PyQt5.QtCore import QObject, QUrl
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Logger import Logger from UM.Logger import Logger
@ -21,6 +21,18 @@ from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
from cura.Machines.ContainerNode import ContainerNode
from cura.Machines.MaterialNode import MaterialNode
from cura.Machines.QualityChangesGroup import QualityChangesGroup
from UM.PluginRegistry import PluginRegistry
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.MachineManager import MachineManager
from cura.Machines.MaterialManager import MaterialManager
from cura.Machines.QualityManager import QualityManager
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -31,20 +43,20 @@ catalog = i18nCatalog("cura")
# when a certain action happens. This can be done through this class. # when a certain action happens. This can be done through this class.
class ContainerManager(QObject): class ContainerManager(QObject):
def __init__(self, application): def __init__(self, application: "CuraApplication") -> None:
if ContainerManager.__instance is not None: if ContainerManager.__instance is not None:
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
ContainerManager.__instance = self ContainerManager.__instance = self
super().__init__(parent = application) super().__init__(parent = application)
self._application = application self._application = application # type: CuraApplication
self._plugin_registry = self._application.getPluginRegistry() self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
self._container_registry = self._application.getContainerRegistry() self._container_registry = self._application.getContainerRegistry() # type: ContainerRegistry
self._machine_manager = self._application.getMachineManager() self._machine_manager = self._application.getMachineManager() # type: MachineManager
self._material_manager = self._application.getMaterialManager() self._material_manager = self._application.getMaterialManager() # type: MaterialManager
self._quality_manager = self._application.getQualityManager() self._quality_manager = self._application.getQualityManager() # type: QualityManager
self._container_name_filters = {} # type: Dict[str, Dict[str, Any]] self._container_name_filters = {} # type: Dict[str, Dict[str, Any]]
@pyqtSlot(str, str, result=str) @pyqtSlot(str, str, result=str)
def getContainerMetaDataEntry(self, container_id: str, entry_names: str) -> str: def getContainerMetaDataEntry(self, container_id: str, entry_names: str) -> str:
@ -69,21 +81,23 @@ class ContainerManager(QObject):
# by using "/" as a separator. For example, to change an entry "foo" in a # by using "/" as a separator. For example, to change an entry "foo" in a
# dictionary entry "bar", you can specify "bar/foo" as entry name. # dictionary entry "bar", you can specify "bar/foo" as entry name.
# #
# \param container_id \type{str} The ID of the container to change. # \param container_node \type{ContainerNode}
# \param entry_name \type{str} The name of the metadata entry to change. # \param entry_name \type{str} The name of the metadata entry to change.
# \param entry_value The new value of the entry. # \param entry_value The new value of the entry.
# #
# \return True if successful, False if not.
# TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this. # TODO: This is ONLY used by MaterialView for material containers. Maybe refactor this.
# Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want? # Update: In order for QML to use objects and sub objects, those (sub) objects must all be QObject. Is that what we want?
@pyqtSlot("QVariant", str, str) @pyqtSlot("QVariant", str, str)
def setContainerMetaDataEntry(self, container_node, entry_name, entry_value): def setContainerMetaDataEntry(self, container_node: "ContainerNode", entry_name: str, entry_value: str) -> bool:
root_material_id = container_node.metadata["base_file"] root_material_id = container_node.getMetaDataEntry("base_file", "")
if self._container_registry.isReadOnly(root_material_id): if self._container_registry.isReadOnly(root_material_id):
Logger.log("w", "Cannot set metadata of read-only container %s.", root_material_id) Logger.log("w", "Cannot set metadata of read-only container %s.", root_material_id)
return False return False
material_group = self._material_manager.getMaterialGroup(root_material_id) material_group = self._material_manager.getMaterialGroup(root_material_id)
if material_group is None:
Logger.log("w", "Unable to find material group for: %s.", root_material_id)
return False
entries = entry_name.split("/") entries = entry_name.split("/")
entry_name = entries.pop() entry_name = entries.pop()
@ -91,11 +105,11 @@ class ContainerManager(QObject):
sub_item_changed = False sub_item_changed = False
if entries: if entries:
root_name = entries.pop(0) root_name = entries.pop(0)
root = material_group.root_material_node.metadata.get(root_name) root = material_group.root_material_node.getMetaDataEntry(root_name)
item = root item = root
for _ in range(len(entries)): for _ in range(len(entries)):
item = item.get(entries.pop(0), { }) item = item.get(entries.pop(0), {})
if item[entry_name] != entry_value: if item[entry_name] != entry_value:
sub_item_changed = True sub_item_changed = True
@ -109,9 +123,10 @@ class ContainerManager(QObject):
container.setMetaDataEntry(entry_name, entry_value) container.setMetaDataEntry(entry_name, entry_value)
if sub_item_changed: #If it was only a sub-item that has changed then the setMetaDataEntry won't correctly notice that something changed, and we must manually signal that the metadata changed. if sub_item_changed: #If it was only a sub-item that has changed then the setMetaDataEntry won't correctly notice that something changed, and we must manually signal that the metadata changed.
container.metaDataChanged.emit(container) container.metaDataChanged.emit(container)
return True
@pyqtSlot(str, result = str) @pyqtSlot(str, result = str)
def makeUniqueName(self, original_name): def makeUniqueName(self, original_name: str) -> str:
return self._container_registry.uniqueName(original_name) return self._container_registry.uniqueName(original_name)
## Get a list of string that can be used as name filters for a Qt File Dialog ## Get a list of string that can be used as name filters for a Qt File Dialog
@ -125,7 +140,7 @@ class ContainerManager(QObject):
# #
# \return A string list with name filters. # \return A string list with name filters.
@pyqtSlot(str, result = "QStringList") @pyqtSlot(str, result = "QStringList")
def getContainerNameFilters(self, type_name): def getContainerNameFilters(self, type_name: str) -> List[str]:
if not self._container_name_filters: if not self._container_name_filters:
self._updateContainerNameFilters() self._updateContainerNameFilters()
@ -257,7 +272,7 @@ class ContainerManager(QObject):
# #
# \return \type{bool} True if successful, False if not. # \return \type{bool} True if successful, False if not.
@pyqtSlot(result = bool) @pyqtSlot(result = bool)
def updateQualityChanges(self): def updateQualityChanges(self) -> bool:
global_stack = self._machine_manager.activeMachine global_stack = self._machine_manager.activeMachine
if not global_stack: if not global_stack:
return False return False
@ -313,10 +328,10 @@ class ContainerManager(QObject):
# \param material_id \type{str} the id of the material for which to get the linked materials. # \param material_id \type{str} the id of the material for which to get the linked materials.
# \return \type{list} a list of names of materials with the same GUID # \return \type{list} a list of names of materials with the same GUID
@pyqtSlot("QVariant", bool, result = "QStringList") @pyqtSlot("QVariant", bool, result = "QStringList")
def getLinkedMaterials(self, material_node, exclude_self = False): def getLinkedMaterials(self, material_node: "MaterialNode", exclude_self: bool = False):
guid = material_node.metadata["GUID"] guid = material_node.getMetaDataEntry("GUID", "")
self_root_material_id = material_node.metadata["base_file"] self_root_material_id = material_node.getMetaDataEntry("base_file")
material_group_list = self._material_manager.getMaterialGroupListByGUID(guid) material_group_list = self._material_manager.getMaterialGroupListByGUID(guid)
linked_material_names = [] linked_material_names = []
@ -324,15 +339,19 @@ class ContainerManager(QObject):
for material_group in material_group_list: for material_group in material_group_list:
if exclude_self and material_group.name == self_root_material_id: if exclude_self and material_group.name == self_root_material_id:
continue continue
linked_material_names.append(material_group.root_material_node.metadata["name"]) linked_material_names.append(material_group.root_material_node.getMetaDataEntry("name", ""))
return linked_material_names return linked_material_names
## Unlink a material from all other materials by creating a new GUID ## Unlink a material from all other materials by creating a new GUID
# \param material_id \type{str} the id of the material to create a new GUID for. # \param material_id \type{str} the id of the material to create a new GUID for.
@pyqtSlot("QVariant") @pyqtSlot("QVariant")
def unlinkMaterial(self, material_node): def unlinkMaterial(self, material_node: "MaterialNode") -> None:
# Get the material group # Get the material group
material_group = self._material_manager.getMaterialGroup(material_node.metadata["base_file"]) material_group = self._material_manager.getMaterialGroup(material_node.getMetaDataEntry("base_file", ""))
if material_group is None:
Logger.log("w", "Unable to find material group for %s", material_node)
return
# Generate a new GUID # Generate a new GUID
new_guid = str(uuid.uuid4()) new_guid = str(uuid.uuid4())
@ -344,7 +363,7 @@ class ContainerManager(QObject):
if container is not None: if container is not None:
container.setMetaDataEntry("GUID", new_guid) container.setMetaDataEntry("GUID", new_guid)
def _performMerge(self, merge_into, merge, clear_settings = True): def _performMerge(self, merge_into: InstanceContainer, merge: InstanceContainer, clear_settings: bool = True) -> None:
if merge == merge_into: if merge == merge_into:
return return
@ -400,7 +419,7 @@ class ContainerManager(QObject):
## Import single profile, file_url does not have to end with curaprofile ## Import single profile, file_url does not have to end with curaprofile
@pyqtSlot(QUrl, result="QVariantMap") @pyqtSlot(QUrl, result="QVariantMap")
def importProfile(self, file_url): def importProfile(self, file_url: QUrl):
if not file_url.isValid(): if not file_url.isValid():
return return
path = file_url.toLocalFile() path = file_url.toLocalFile()
@ -409,7 +428,7 @@ class ContainerManager(QObject):
return self._container_registry.importProfile(path) return self._container_registry.importProfile(path)
@pyqtSlot(QObject, QUrl, str) @pyqtSlot(QObject, QUrl, str)
def exportQualityChangesGroup(self, quality_changes_group, file_url: QUrl, file_type: str): def exportQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup", file_url: QUrl, file_type: str) -> None:
if not file_url.isValid(): if not file_url.isValid():
return return
path = file_url.toLocalFile() path = file_url.toLocalFile()

View file

@ -4,7 +4,7 @@
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt. from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant # For communicating data and events to Qt.
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
import cura.CuraApplication #To get the global container stack to find the current machine. import cura.CuraApplication # To get the global container stack to find the current machine.
from UM.Logger import Logger from UM.Logger import Logger
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
@ -12,15 +12,15 @@ from UM.Scene.Selection import Selection
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID. from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
from UM.Settings.SettingFunction import SettingFunction from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.SettingInstance import SettingInstance
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from typing import Optional, List, TYPE_CHECKING, Union, Dict from typing import Optional, TYPE_CHECKING, Dict, List, Any, Union
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from UM.Scene.SceneNode import SceneNode
## Manages all existing extruder stacks. ## Manages all existing extruder stacks.
@ -38,9 +38,13 @@ class ExtruderManager(QObject):
self._application = cura.CuraApplication.CuraApplication.getInstance() self._application = cura.CuraApplication.CuraApplication.getInstance()
self._extruder_trains = {} # Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders. # Per machine, a dictionary of extruder container stack IDs. Only for separately defined extruders.
self._extruder_trains = {} # type: Dict[str, Dict[str, "ExtruderStack"]]
self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack self._active_extruder_index = -1 # Indicates the index of the active extruder stack. -1 means no active extruder stack
self._selected_object_extruders = []
# TODO; I have no idea why this is a union of ID's and extruder stacks. This needs to be fixed at some point.
self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]]
self._addCurrentMachineExtruders() self._addCurrentMachineExtruders()
Selection.selectionChanged.connect(self.resetSelectedObjectExtruders) Selection.selectionChanged.connect(self.resetSelectedObjectExtruders)
@ -68,7 +72,7 @@ class ExtruderManager(QObject):
## Return extruder count according to extruder trains. ## Return extruder count according to extruder trains.
@pyqtProperty(int, notify = extrudersChanged) @pyqtProperty(int, notify = extrudersChanged)
def extruderCount(self): def extruderCount(self) -> int:
if not self._application.getGlobalContainerStack(): if not self._application.getGlobalContainerStack():
return 0 # No active machine, so no extruders. return 0 # No active machine, so no extruders.
try: try:
@ -79,28 +83,14 @@ class ExtruderManager(QObject):
## Gets a dict with the extruder stack ids with the extruder number as the key. ## Gets a dict with the extruder stack ids with the extruder number as the key.
@pyqtProperty("QVariantMap", notify = extrudersChanged) @pyqtProperty("QVariantMap", notify = extrudersChanged)
def extruderIds(self) -> Dict[str, str]: def extruderIds(self) -> Dict[str, str]:
extruder_stack_ids = {} extruder_stack_ids = {} # type: Dict[str, str]
global_container_stack = self._application.getGlobalContainerStack() global_container_stack = self._application.getGlobalContainerStack()
if global_container_stack: if global_container_stack:
global_stack_id = global_container_stack.getId() extruder_stack_ids = {position: extruder.id for position, extruder in global_container_stack.extruders.items()}
if global_stack_id in self._extruder_trains:
for position in self._extruder_trains[global_stack_id]:
extruder_stack_ids[position] = self._extruder_trains[global_stack_id][position].getId()
return extruder_stack_ids return extruder_stack_ids
@pyqtSlot(str, result = str)
def getQualityChangesIdByExtruderStackId(self, extruder_stack_id: str) -> str:
global_container_stack = self._application.getGlobalContainerStack()
if global_container_stack is not None:
for position in self._extruder_trains[global_container_stack.getId()]:
extruder = self._extruder_trains[global_container_stack.getId()][position]
if extruder.getId() == extruder_stack_id:
return extruder.qualityChanges.getId()
return ""
## Changes the active extruder by index. ## Changes the active extruder by index.
# #
# \param index The index of the new active extruder. # \param index The index of the new active extruder.
@ -117,9 +107,9 @@ class ExtruderManager(QObject):
# #
# \param index The index of the extruder whose name to get. # \param index The index of the extruder whose name to get.
@pyqtSlot(int, result = str) @pyqtSlot(int, result = str)
def getExtruderName(self, index): def getExtruderName(self, index: int) -> str:
try: try:
return list(self.getActiveExtruderStacks())[index].getName() return self.getActiveExtruderStacks()[index].getName()
except IndexError: except IndexError:
return "" return ""
@ -128,12 +118,12 @@ class ExtruderManager(QObject):
## Provides a list of extruder IDs used by the current selected objects. ## Provides a list of extruder IDs used by the current selected objects.
@pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged) @pyqtProperty("QVariantList", notify = selectedObjectExtrudersChanged)
def selectedObjectExtruders(self) -> List[str]: def selectedObjectExtruders(self) -> List[Union[str, "ExtruderStack"]]:
if not self._selected_object_extruders: if not self._selected_object_extruders:
object_extruders = set() object_extruders = set()
# First, build a list of the actual selected objects (including children of groups, excluding group nodes) # First, build a list of the actual selected objects (including children of groups, excluding group nodes)
selected_nodes = [] selected_nodes = [] # type: List["SceneNode"]
for node in Selection.getAllSelectedObjects(): for node in Selection.getAllSelectedObjects():
if node.callDecoration("isGroup"): if node.callDecoration("isGroup"):
for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
@ -145,16 +135,15 @@ class ExtruderManager(QObject):
selected_nodes.append(node) selected_nodes.append(node)
# Then, figure out which nodes are used by those selected nodes. # Then, figure out which nodes are used by those selected nodes.
global_stack = self._application.getGlobalContainerStack() current_extruder_trains = self.getActiveExtruderStacks()
current_extruder_trains = self._extruder_trains.get(global_stack.getId())
for node in selected_nodes: for node in selected_nodes:
extruder = node.callDecoration("getActiveExtruder") extruder = node.callDecoration("getActiveExtruder")
if extruder: if extruder:
object_extruders.add(extruder) object_extruders.add(extruder)
elif current_extruder_trains: elif current_extruder_trains:
object_extruders.add(current_extruder_trains["0"].getId()) object_extruders.add(current_extruder_trains[0].getId())
self._selected_object_extruders = list(object_extruders) self._selected_object_extruders = list(object_extruders) # type: List[Union[str, "ExtruderStack"]]
return self._selected_object_extruders return self._selected_object_extruders
@ -163,19 +152,12 @@ class ExtruderManager(QObject):
# This will trigger a recalculation of the extruders used for the # This will trigger a recalculation of the extruders used for the
# selection. # selection.
def resetSelectedObjectExtruders(self) -> None: def resetSelectedObjectExtruders(self) -> None:
self._selected_object_extruders = [] self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]]
self.selectedObjectExtrudersChanged.emit() self.selectedObjectExtrudersChanged.emit()
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]: def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
global_container_stack = self._application.getGlobalContainerStack() return self.getExtruderStack(self._active_extruder_index)
if global_container_stack:
if global_container_stack.getId() in self._extruder_trains:
if str(self._active_extruder_index) in self._extruder_trains[global_container_stack.getId()]:
return self._extruder_trains[global_container_stack.getId()][str(self._active_extruder_index)]
return None
## Get an extruder stack by index ## Get an extruder stack by index
def getExtruderStack(self, index) -> Optional["ExtruderStack"]: def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
@ -186,16 +168,7 @@ class ExtruderManager(QObject):
return self._extruder_trains[global_container_stack.getId()][str(index)] return self._extruder_trains[global_container_stack.getId()][str(index)]
return None return None
## Get all extruder stacks def registerExtruder(self, extruder_train: "ExtruderStack", machine_id: str) -> None:
def getExtruderStacks(self) -> List["ExtruderStack"]:
result = []
for i in range(self.extruderCount):
stack = self.getExtruderStack(i)
if stack:
result.append(stack)
return result
def registerExtruder(self, extruder_train, machine_id):
changed = False changed = False
if machine_id not in self._extruder_trains: if machine_id not in self._extruder_trains:
@ -214,23 +187,20 @@ class ExtruderManager(QObject):
if changed: if changed:
self.extrudersChanged.emit(machine_id) self.extrudersChanged.emit(machine_id)
def getAllExtruderValues(self, setting_key):
return self.getAllExtruderSettings(setting_key, "value")
## Gets a property of a setting for all extruders. ## Gets a property of a setting for all extruders.
# #
# \param setting_key \type{str} The setting to get the property of. # \param setting_key \type{str} The setting to get the property of.
# \param property \type{str} The property to get. # \param property \type{str} The property to get.
# \return \type{List} the list of results # \return \type{List} the list of results
def getAllExtruderSettings(self, setting_key: str, prop: str): def getAllExtruderSettings(self, setting_key: str, prop: str) -> List:
result = [] result = []
for index in self.extruderIds:
extruder_stack_id = self.extruderIds[str(index)] for extruder_stack in self.getActiveExtruderStacks():
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = extruder_stack_id)[0]
result.append(extruder_stack.getProperty(setting_key, prop)) result.append(extruder_stack.getProperty(setting_key, prop))
return result return result
def extruderValueWithDefault(self, value): def extruderValueWithDefault(self, value: str) -> str:
machine_manager = self._application.getMachineManager() machine_manager = self._application.getMachineManager()
if value == "-1": if value == "-1":
return machine_manager.defaultExtruderPosition return machine_manager.defaultExtruderPosition
@ -321,7 +291,7 @@ class ExtruderManager(QObject):
## Removes the container stack and user profile for the extruders for a specific machine. ## Removes the container stack and user profile for the extruders for a specific machine.
# #
# \param machine_id The machine to remove the extruders for. # \param machine_id The machine to remove the extruders for.
def removeMachineExtruders(self, machine_id: str): def removeMachineExtruders(self, machine_id: str) -> None:
for extruder in self.getMachineExtruders(machine_id): for extruder in self.getMachineExtruders(machine_id):
ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId()) ContainerRegistry.getInstance().removeContainer(extruder.userChanges.getId())
ContainerRegistry.getInstance().removeContainer(extruder.getId()) ContainerRegistry.getInstance().removeContainer(extruder.getId())
@ -331,24 +301,11 @@ class ExtruderManager(QObject):
## Returns extruders for a specific machine. ## Returns extruders for a specific machine.
# #
# \param machine_id The machine to get the extruders of. # \param machine_id The machine to get the extruders of.
def getMachineExtruders(self, machine_id: str): def getMachineExtruders(self, machine_id: str) -> List["ExtruderStack"]:
if machine_id not in self._extruder_trains: if machine_id not in self._extruder_trains:
return [] return []
return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]] return [self._extruder_trains[machine_id][name] for name in self._extruder_trains[machine_id]]
## Returns a list containing the global stack and active extruder stacks.
#
# The first element is the global container stack, followed by any extruder stacks.
# \return \type{List[ContainerStack]}
def getActiveGlobalAndExtruderStacks(self) -> Optional[List[Union["ExtruderStack", "GlobalStack"]]]:
global_stack = self._application.getGlobalContainerStack()
if not global_stack:
return None
result = [global_stack]
result.extend(self.getActiveExtruderStacks())
return result
## Returns the list of active extruder stacks, taking into account the machine extruder count. ## Returns the list of active extruder stacks, taking into account the machine extruder count.
# #
# \return \type{List[ContainerStack]} a list of # \return \type{List[ContainerStack]} a list of
@ -357,10 +314,7 @@ class ExtruderManager(QObject):
if not global_stack: if not global_stack:
return [] return []
result = [] result = list(global_stack.extruders.values())
if global_stack.getId() in self._extruder_trains:
for extruder in sorted(self._extruder_trains[global_stack.getId()]):
result.append(self._extruder_trains[global_stack.getId()][extruder])
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value") machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
@ -406,7 +360,7 @@ class ExtruderManager(QObject):
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing # After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this. # "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
def _fixSingleExtrusionMachineExtruderDefinition(self, global_stack): def _fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"] expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
extruder_stack_0 = global_stack.extruders["0"] extruder_stack_0 = global_stack.extruders["0"]
if extruder_stack_0.definition.getId() != expected_extruder_definition_0_id: if extruder_stack_0.definition.getId() != expected_extruder_definition_0_id:
@ -425,11 +379,11 @@ class ExtruderManager(QObject):
# \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list. # \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list.
# If no extruder has the value, the list will contain the global value. # If no extruder has the value, the list will contain the global value.
@staticmethod @staticmethod
def getExtruderValues(key): def getExtruderValues(key: str) -> List[Any]:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
result = [] result = []
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()): for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
if not extruder.isEnabled: if not extruder.isEnabled:
continue continue
# only include values from extruders that are "active" for the current machine instance # only include values from extruders that are "active" for the current machine instance
@ -460,7 +414,7 @@ class ExtruderManager(QObject):
# \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list. # \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list.
# If no extruder has the value, the list will contain the global value. # If no extruder has the value, the list will contain the global value.
@staticmethod @staticmethod
def getDefaultExtruderValues(key): def getDefaultExtruderValues(key: str) -> List[Any]:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
context = PropertyEvaluationContext(global_stack) context = PropertyEvaluationContext(global_stack)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container context.context["evaluate_from_container_index"] = 1 # skip the user settings container
@ -471,7 +425,7 @@ class ExtruderManager(QObject):
} }
result = [] result = []
for extruder in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()): for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
# only include values from extruders that are "active" for the current machine instance # only include values from extruders that are "active" for the current machine instance
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value", context = context): if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value", context = context):
continue continue
@ -504,7 +458,7 @@ class ExtruderManager(QObject):
# #
# \return String representing the extruder values # \return String representing the extruder values
@pyqtSlot(str, result="QVariant") @pyqtSlot(str, result="QVariant")
def getInstanceExtruderValues(self, key): def getInstanceExtruderValues(self, key) -> List:
return ExtruderManager.getExtruderValues(key) return ExtruderManager.getExtruderValues(key)
## Get the value for a setting from a specific extruder. ## Get the value for a setting from a specific extruder.
@ -517,7 +471,7 @@ class ExtruderManager(QObject):
# \return The value of the setting for the specified extruder or for the # \return The value of the setting for the specified extruder or for the
# global stack if not found. # global stack if not found.
@staticmethod @staticmethod
def getExtruderValue(extruder_index, key): def getExtruderValue(extruder_index: int, key: str) -> Any:
if extruder_index == -1: if extruder_index == -1:
extruder_index = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition) extruder_index = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition)
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index) extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
@ -542,7 +496,7 @@ class ExtruderManager(QObject):
# \return The value of the setting for the specified extruder or for the # \return The value of the setting for the specified extruder or for the
# global stack if not found. # global stack if not found.
@staticmethod @staticmethod
def getDefaultExtruderValue(extruder_index, key): def getDefaultExtruderValue(extruder_index: int, key: str) -> Any:
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index) extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
context = PropertyEvaluationContext(extruder) context = PropertyEvaluationContext(extruder)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container context.context["evaluate_from_container_index"] = 1 # skip the user settings container
@ -569,7 +523,7 @@ class ExtruderManager(QObject):
# #
# \return The effective value # \return The effective value
@staticmethod @staticmethod
def getResolveOrValue(key): def getResolveOrValue(key: str) -> Any:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
resolved_value = global_stack.getProperty(key, "value") resolved_value = global_stack.getProperty(key, "value")
@ -583,7 +537,7 @@ class ExtruderManager(QObject):
# #
# \return The effective value # \return The effective value
@staticmethod @staticmethod
def getDefaultResolveOrValue(key): def getDefaultResolveOrValue(key: str) -> Any:
global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack() global_stack = cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()
context = PropertyEvaluationContext(global_stack) context = PropertyEvaluationContext(global_stack)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container context.context["evaluate_from_container_index"] = 1 # skip the user settings container

View file

@ -134,7 +134,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
# Link to new extruders # Link to new extruders
self._active_machine_extruders = [] self._active_machine_extruders = []
extruder_manager = Application.getInstance().getExtruderManager() extruder_manager = Application.getInstance().getExtruderManager()
for extruder in extruder_manager.getExtruderStacks(): for extruder in extruder_manager.getActiveExtruderStacks():
if extruder is None: #This extruder wasn't loaded yet. This happens asynchronously while this model is constructed from QML. if extruder is None: #This extruder wasn't loaded yet. This happens asynchronously while this model is constructed from QML.
continue continue
extruder.containersChanged.connect(self._onExtruderStackContainersChanged) extruder.containersChanged.connect(self._onExtruderStackContainersChanged)
@ -171,7 +171,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
# get machine extruder count for verification # get machine extruder count for verification
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value") machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
for extruder in Application.getInstance().getExtruderManager().getMachineExtruders(global_container_stack.getId()): for extruder in Application.getInstance().getExtruderManager().getActiveExtruderStacks():
position = extruder.getMetaDataEntry("position", default = "0") # Get the position position = extruder.getMetaDataEntry("position", default = "0") # Get the position
try: try:
position = int(position) position = int(position)

View file

@ -13,6 +13,8 @@ from UM.Settings.SettingInstance import InstanceState
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import PropertyEvaluationContext from UM.Settings.Interfaces import PropertyEvaluationContext
from UM.Logger import Logger from UM.Logger import Logger
from UM.Util import parseBool
import cura.CuraApplication import cura.CuraApplication
from . import Exceptions from . import Exceptions
@ -188,6 +190,15 @@ class GlobalStack(CuraContainerStack):
def getHeadAndFansCoordinates(self): def getHeadAndFansCoordinates(self):
return self.getProperty("machine_head_with_fans_polygon", "value") return self.getProperty("machine_head_with_fans_polygon", "value")
def getHasMaterials(self) -> bool:
return parseBool(self.getMetaDataEntry("has_materials", False))
def getHasVariants(self) -> bool:
return parseBool(self.getMetaDataEntry("has_variants", False))
def getHasMachineQuality(self) -> bool:
return parseBool(self.getMetaDataEntry("has_machine_quality", False))
## private: ## private:
global_stack_mime = MimeType( global_stack_mime = MimeType(

View file

@ -385,7 +385,9 @@ class MachineManager(QObject):
# \param definition_id \type{str} definition id that needs to look for # \param definition_id \type{str} definition id that needs to look for
# \param metadata_filter \type{dict} list of metadata keys and values used for filtering # \param metadata_filter \type{dict} list of metadata keys and values used for filtering
@staticmethod @staticmethod
def getMachine(definition_id: str, metadata_filter: Dict[str, str] = None) -> Optional["GlobalStack"]: def getMachine(definition_id: str, metadata_filter: Optional[Dict[str, str]] = None) -> Optional["GlobalStack"]:
if metadata_filter is None:
metadata_filter = {}
machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter) machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
for machine in machines: for machine in machines:
if machine.definition.getId() == definition_id: if machine.definition.getId() == definition_id:
@ -412,7 +414,7 @@ class MachineManager(QObject):
# Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are # Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
extruder_stacks = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()) extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
count = 1 # we start with the global stack count = 1 # we start with the global stack
for stack in extruder_stacks: for stack in extruder_stacks:
md = stack.getMetaData() md = stack.getMetaData()
@ -435,7 +437,7 @@ class MachineManager(QObject):
if self._global_container_stack.getTop().findInstances(): if self._global_container_stack.getTop().findInstances():
return True return True
stacks = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
for stack in stacks: for stack in stacks:
if stack.getTop().findInstances(): if stack.getTop().findInstances():
return True return True
@ -448,7 +450,7 @@ class MachineManager(QObject):
return 0 return 0
num_user_settings = 0 num_user_settings = 0
num_user_settings += len(self._global_container_stack.getTop().findInstances()) num_user_settings += len(self._global_container_stack.getTop().findInstances())
stacks = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
for stack in stacks: for stack in stacks:
num_user_settings += len(stack.getTop().findInstances()) num_user_settings += len(stack.getTop().findInstances())
return num_user_settings return num_user_settings
@ -473,7 +475,7 @@ class MachineManager(QObject):
stack = ExtruderManager.getInstance().getActiveExtruderStack() stack = ExtruderManager.getInstance().getActiveExtruderStack()
stacks = [stack] stacks = [stack]
else: else:
stacks = ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()) stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
for stack in stacks: for stack in stacks:
if stack is not None: if stack is not None:
@ -638,7 +640,7 @@ class MachineManager(QObject):
if self._active_container_stack is None or self._global_container_stack is None: if self._active_container_stack is None or self._global_container_stack is None:
return return
new_value = self._active_container_stack.getProperty(key, "value") new_value = self._active_container_stack.getProperty(key, "value")
extruder_stacks = [stack for stack in ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())] extruder_stacks = [stack for stack in ExtruderManager.getInstance().getActiveExtruderStacks()]
# check in which stack the value has to be replaced # check in which stack the value has to be replaced
for extruder_stack in extruder_stacks: for extruder_stack in extruder_stacks:
@ -890,7 +892,11 @@ class MachineManager(QObject):
extruder_nr = node.callDecoration("getActiveExtruderPosition") extruder_nr = node.callDecoration("getActiveExtruderPosition")
if extruder_nr is not None and int(extruder_nr) > extruder_count - 1: if extruder_nr is not None and int(extruder_nr) > extruder_count - 1:
node.callDecoration("setActiveExtruder", extruder_manager.getExtruderStack(extruder_count - 1).getId()) extruder = extruder_manager.getExtruderStack(extruder_count - 1)
if extruder is not None:
node.callDecoration("setActiveExtruder", extruder.getId())
else:
Logger.log("w", "Could not find extruder to set active.")
# Make sure one of the extruder stacks is active # Make sure one of the extruder stacks is active
extruder_manager.setActiveExtruderIndex(0) extruder_manager.setActiveExtruderIndex(0)

View file

@ -43,7 +43,9 @@ class UserChangesModel(ListModel):
global_stack = Application.getInstance().getGlobalContainerStack() global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack: if not global_stack:
return return
stacks = ExtruderManager.getInstance().getActiveGlobalAndExtruderStacks()
stacks = [global_stack]
stacks.extend(global_stack.extruders.values())
# Check if the definition container has a translation file and ensure it's loaded. # Check if the definition container has a translation file and ensure it's loaded.
definition = global_stack.getBottom() definition = global_stack.getBottom()

View file

@ -225,7 +225,7 @@ class ThreeMFReader(MeshReader):
except Exception: except Exception:
Logger.logException("e", "An exception occurred in 3mf reader.") Logger.logException("e", "An exception occurred in 3mf reader.")
return [] return None
return result return result

View file

@ -85,14 +85,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__() super().__init__()
MimeTypeDatabase.addMimeType(
MimeType(
name="application/x-curaproject+xml",
comment="Cura Project File",
suffixes=["curaproject.3mf"]
)
)
self._supported_extensions = [".3mf"] self._supported_extensions = [".3mf"]
self._dialog = WorkspaceDialog() self._dialog = WorkspaceDialog()
self._3mf_mesh_reader = None self._3mf_mesh_reader = None
@ -726,8 +718,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
nodes = [] nodes = []
base_file_name = os.path.basename(file_name) base_file_name = os.path.basename(file_name)
if base_file_name.endswith(".curaproject.3mf"):
base_file_name = base_file_name[:base_file_name.rfind(".curaproject.3mf")]
self.setWorkspaceName(base_file_name) self.setWorkspaceName(base_file_name)
return nodes return nodes
@ -944,7 +934,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
root_material_id) root_material_id)
if material_node is not None and material_node.getContainer() is not None: if material_node is not None and material_node.getContainer() is not None:
extruder_stack.material = material_node.getContainer() extruder_stack.material = material_node.getContainer() # type: InstanceContainer
def _applyChangesToMachine(self, global_stack, extruder_stack_dict): def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
# Clear all first # Clear all first

View file

@ -18,11 +18,7 @@ catalog = i18nCatalog("cura")
def getMetaData() -> Dict: def getMetaData() -> Dict:
# Workaround for osx not supporting double file extensions correctly. workspace_extension = "3mf"
if Platform.isOSX():
workspace_extension = "3mf"
else:
workspace_extension = "curaproject.3mf"
metaData = {} metaData = {}
if "3MFReader.ThreeMFReader" in sys.modules: if "3MFReader.ThreeMFReader" in sys.modules:

View file

@ -15,11 +15,7 @@ from UM.Platform import Platform
i18n_catalog = i18nCatalog("uranium") i18n_catalog = i18nCatalog("uranium")
def getMetaData(): def getMetaData():
# Workarround for osx not supporting double file extensions correctly. workspace_extension = "3mf"
if Platform.isOSX():
workspace_extension = "3mf"
else:
workspace_extension = "curaproject.3mf"
metaData = {} metaData = {}
@ -36,7 +32,7 @@ def getMetaData():
"output": [{ "output": [{
"extension": workspace_extension, "extension": workspace_extension,
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"), "description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
"mime_type": "application/x-curaproject+xml", "mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode "mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
}] }]
} }

View file

@ -342,7 +342,7 @@ class CuraEngineBackend(QObject, Backend):
if not self._global_container_stack: if not self._global_container_stack:
Logger.log("w", "Global container stack not assigned to CuraEngineBackend!") Logger.log("w", "Global container stack not assigned to CuraEngineBackend!")
return return
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId())) extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
error_keys = [] #type: List[str] error_keys = [] #type: List[str]
for extruder in extruders: for extruder in extruders:
error_keys.extend(extruder.getErrorKeys()) error_keys.extend(extruder.getErrorKeys())

View file

@ -178,7 +178,7 @@ class ProcessSlicedLayersJob(Job):
# Find out colors per extruder # Find out colors per extruder
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
manager = ExtruderManager.getInstance() manager = ExtruderManager.getInstance()
extruders = list(manager.getMachineExtruders(global_container_stack.getId())) extruders = manager.getActiveExtruderStacks()
if extruders: if extruders:
material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32) material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32)
for extruder in extruders: for extruder in extruders:

View file

@ -333,7 +333,7 @@ class StartSliceJob(Job):
"-1": self._buildReplacementTokens(global_stack) "-1": self._buildReplacementTokens(global_stack)
} }
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()): for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
extruder_nr = extruder_stack.getProperty("extruder_nr", "value") extruder_nr = extruder_stack.getProperty("extruder_nr", "value")
self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack) self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack)

View file

@ -275,7 +275,7 @@ class FlavorParser:
## For showing correct x, y offsets for each extruder ## For showing correct x, y offsets for each extruder
def _extruderOffsets(self) -> Dict[int, List[float]]: def _extruderOffsets(self) -> Dict[int, List[float]]:
result = {} result = {}
for extruder in ExtruderManager.getInstance().getExtruderStacks(): for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
result[int(extruder.getMetaData().get("position", "0"))] = [ result[int(extruder.getMetaData().get("position", "0"))] = [
extruder.getProperty("machine_nozzle_offset_x", "value"), extruder.getProperty("machine_nozzle_offset_x", "value"),
extruder.getProperty("machine_nozzle_offset_y", "value")] extruder.getProperty("machine_nozzle_offset_y", "value")]

View file

@ -16,6 +16,8 @@ Cura.MachineAction
property var extrudersModel: Cura.ExtrudersModel{} property var extrudersModel: Cura.ExtrudersModel{}
property int extruderTabsCount: 0 property int extruderTabsCount: 0
property var activeMachineId: Cura.MachineManager.activeMachine != null ? Cura.MachineManager.activeMachine.id : ""
Connections Connections
{ {
target: base.extrudersModel target: base.extrudersModel
@ -511,7 +513,7 @@ Cura.MachineAction
} }
return ""; return "";
} }
return Cura.MachineManager.activeMachineId; return base.activeMachineId
} }
key: settingKey key: settingKey
watchedProperties: [ "value", "description" ] watchedProperties: [ "value", "description" ]
@ -564,7 +566,7 @@ Cura.MachineAction
} }
return ""; return "";
} }
return Cura.MachineManager.activeMachineId; return base.activeMachineId
} }
key: settingKey key: settingKey
watchedProperties: [ "value", "description" ] watchedProperties: [ "value", "description" ]
@ -655,7 +657,7 @@ Cura.MachineAction
} }
return ""; return "";
} }
return Cura.MachineManager.activeMachineId; return base.activeMachineId
} }
key: settingKey key: settingKey
watchedProperties: [ "value", "options", "description" ] watchedProperties: [ "value", "options", "description" ]
@ -754,7 +756,7 @@ Cura.MachineAction
} }
return ""; return "";
} }
return Cura.MachineManager.activeMachineId; return base.activeMachineId
} }
key: settingKey key: settingKey
watchedProperties: [ "value", "description" ] watchedProperties: [ "value", "description" ]
@ -879,7 +881,7 @@ Cura.MachineAction
{ {
id: machineExtruderCountProvider id: machineExtruderCountProvider
containerStackId: Cura.MachineManager.activeMachineId containerStackId: base.activeMachineId
key: "machine_extruder_count" key: "machine_extruder_count"
watchedProperties: [ "value", "description" ] watchedProperties: [ "value", "description" ]
storeIndex: manager.containerIndex storeIndex: manager.containerIndex
@ -889,7 +891,7 @@ Cura.MachineAction
{ {
id: machineHeadPolygonProvider id: machineHeadPolygonProvider
containerStackId: Cura.MachineManager.activeMachineId containerStackId: base.acthiveMachineId
key: "machine_head_with_fans_polygon" key: "machine_head_with_fans_polygon"
watchedProperties: [ "value" ] watchedProperties: [ "value" ]
storeIndex: manager.containerIndex storeIndex: manager.containerIndex

View file

@ -17,7 +17,6 @@ Item {
width: childrenRect.width; width: childrenRect.width;
height: childrenRect.height; height: childrenRect.height;
property var all_categories_except_support: [ "machine_settings", "resolution", "shell", "infill", "material", "speed", property var all_categories_except_support: [ "machine_settings", "resolution", "shell", "infill", "material", "speed",
"travel", "cooling", "platform_adhesion", "dual", "meshfix", "blackmagic", "experimental"] "travel", "cooling", "platform_adhesion", "dual", "meshfix", "blackmagic", "experimental"]
@ -45,7 +44,7 @@ Item {
UM.SettingPropertyProvider UM.SettingPropertyProvider
{ {
id: meshTypePropertyProvider id: meshTypePropertyProvider
containerStackId: Cura.MachineManager.activeMachineId containerStack: Cura.MachineManager.activeMachine
watchedProperties: [ "enabled" ] watchedProperties: [ "enabled" ]
} }
@ -518,7 +517,7 @@ Item {
{ {
id: machineExtruderCount id: machineExtruderCount
containerStackId: Cura.MachineManager.activeMachineId containerStack: Cura.MachineManager.activeMachine
key: "machine_extruder_count" key: "machine_extruder_count"
watchedProperties: [ "value" ] watchedProperties: [ "value" ]
storeIndex: 0 storeIndex: 0
@ -528,7 +527,7 @@ Item {
{ {
id: printSequencePropertyProvider id: printSequencePropertyProvider
containerStackId: Cura.MachineManager.activeMachineId containerStack: Cura.MachineManager.activeMachine
key: "print_sequence" key: "print_sequence"
watchedProperties: [ "value" ] watchedProperties: [ "value" ]
storeIndex: 0 storeIndex: 0

View file

@ -384,7 +384,7 @@ UM.Dialog
UM.SettingPropertyProvider UM.SettingPropertyProvider
{ {
id: inheritStackProvider id: inheritStackProvider
containerStackId: Cura.MachineManager.activeMachineId containerStack: Cura.MachineManager.activeMachine
key: model.key ? model.key : "None" key: model.key ? model.key : "None"
watchedProperties: [ "limit_to_extruder" ] watchedProperties: [ "limit_to_extruder" ]
} }

View file

@ -44,12 +44,11 @@ UM.PointingRectangle {
id: valueLabel id: valueLabel
anchors { anchors {
left: parent.left
leftMargin: Math.round(UM.Theme.getSize("default_margin").width / 2)
verticalCenter: parent.verticalCenter verticalCenter: parent.verticalCenter
horizontalCenter: parent.horizontalCenter
} }
width: maximumValue.toString().length * 12 * screenScaleFactor width: (maximumValue.toString().length + 1) * 10 * screenScaleFactor
text: sliderLabelRoot.value + startFrom // the current handle value, add 1 because layers is an array text: sliderLabelRoot.value + startFrom // the current handle value, add 1 because layers is an array
horizontalAlignment: TextInput.AlignRight horizontalAlignment: TextInput.AlignRight

View file

@ -667,8 +667,11 @@ Item
{ {
target: UM.SimulationView target: UM.SimulationView
onMaxLayersChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer) onMaxLayersChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer)
onMinimumLayerChanged: layerSlider.setLowerValue(UM.SimulationView.minimumLayer) onCurrentLayerChanged:
onCurrentLayerChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer) {
playButton.pauseSimulation()
layerSlider.setUpperValue(UM.SimulationView.currentLayer)
}
} }
// make sure the slider handlers show the correct value after switching views // make sure the slider handlers show the correct value after switching views
@ -723,6 +726,7 @@ Item
UM.SimulationView.setSimulationRunning(true) UM.SimulationView.setSimulationRunning(true)
iconSource = "./resources/simulation_pause.svg" iconSource = "./resources/simulation_pause.svg"
simulationTimer.start() simulationTimer.start()
status = 1
} }
} }
@ -766,6 +770,7 @@ Item
{ {
UM.SimulationView.setCurrentLayer(currentLayer+1) UM.SimulationView.setCurrentLayer(currentLayer+1)
UM.SimulationView.setCurrentPath(0) UM.SimulationView.setCurrentPath(0)
playButton.resumeSimulation()
} }
} }
else else

View file

@ -82,9 +82,16 @@ Item
} }
spacing: Math.floor(UM.Theme.getSize("narrow_margin").height) spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
width: childrenRect.width width: childrenRect.width
Label Label
{ {
text: catalog.i18nc("@label", "Contact") + ":" text: catalog.i18nc("@label", "Website") + ":"
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text_medium")
}
Label
{
text: catalog.i18nc("@label", "Email") + ":"
font: UM.Theme.getFont("very_small") font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text_medium") color: UM.Theme.getColor("text_medium")
} }
@ -100,18 +107,32 @@ Item
topMargin: UM.Theme.getSize("default_margin").height topMargin: UM.Theme.getSize("default_margin").height
} }
spacing: Math.floor(UM.Theme.getSize("narrow_margin").height) spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
Label
{
text:
{
if (details.website)
{
return "<a href=\"" + details.website + "\">" + details.website + "</a>"
}
return ""
}
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
}
Label Label
{ {
text: text:
{ {
if (details.email) if (details.email)
{ {
return "<a href=\"mailto:"+details.email+"\">"+details.name+"</a>" return "<a href=\"mailto:" + details.email + "\">" + details.email + "</a>"
}
else
{
return "<a href=\""+details.website+"\">"+details.name+"</a>"
} }
return ""
} }
font: UM.Theme.getFont("very_small") font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")

View file

@ -1,14 +1,25 @@
// Copyright (c) 2018 Ultimaker B.V. // Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher. // Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2 import QtQuick 2.7
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM import UM 1.1 as UM
Item Item
{ {
id: base
property var packageData property var packageData
property var technicalDataSheetUrl: {
var link = undefined
if ("Technical Data Sheet" in packageData.links)
{
link = packageData.links["Technical Data Sheet"]
}
return link
}
anchors.topMargin: UM.Theme.getSize("default_margin").height anchors.topMargin: UM.Theme.getSize("default_margin").height
height: visible ? childrenRect.height : 0 height: visible ? childrenRect.height : 0
visible: packageData.type == "material" && packageData.has_configs visible: packageData.type == "material" && packageData.has_configs
@ -132,4 +143,25 @@ Item
width: Math.floor(table.width * 0.1) width: Math.floor(table.width * 0.1)
} }
} }
Label
{
id: technical_data_sheet
anchors.top: table.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height / 2
visible: base.technicalDataSheetUrl !== undefined
text:
{
if (base.technicalDataSheetUrl !== undefined)
{
return "<a href='%1'>%2</a>".arg(base.technicalDataSheetUrl).arg("Technical Data Sheet")
}
return ""
}
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
}
} }

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V. // Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher. // Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2 import QtQuick 2.7
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM import UM 1.1 as UM

View file

@ -32,7 +32,7 @@ Item
width: UM.Theme.getSize("toolbox_thumbnail_medium").width width: UM.Theme.getSize("toolbox_thumbnail_medium").width
height: UM.Theme.getSize("toolbox_thumbnail_medium").height height: UM.Theme.getSize("toolbox_thumbnail_medium").height
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
source: details.icon_url || "../images/logobot.svg" source: details === null ? "" : (details.icon_url || "../images/logobot.svg")
mipmap: true mipmap: true
anchors anchors
{ {
@ -55,7 +55,7 @@ Item
rightMargin: UM.Theme.getSize("wide_margin").width rightMargin: UM.Theme.getSize("wide_margin").width
bottomMargin: UM.Theme.getSize("default_margin").height bottomMargin: UM.Theme.getSize("default_margin").height
} }
text: details.name || "" text: details === null ? "" : (details.name || "")
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
@ -114,7 +114,7 @@ Item
height: childrenRect.height height: childrenRect.height
Label Label
{ {
text: details.version || catalog.i18nc("@label", "Unknown") text: details === null ? "" : (details.version || catalog.i18nc("@label", "Unknown"))
font: UM.Theme.getFont("very_small") font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
} }
@ -122,6 +122,10 @@ Item
{ {
text: text:
{ {
if (details === null)
{
return ""
}
var date = new Date(details.last_updated) var date = new Date(details.last_updated)
return date.toLocaleString(UM.Preferences.getValue("general/language")) return date.toLocaleString(UM.Preferences.getValue("general/language"))
} }
@ -132,6 +136,10 @@ Item
{ {
text: text:
{ {
if (details === null)
{
return ""
}
if (details.author_email) if (details.author_email)
{ {
return "<a href=\"mailto:" + details.author_email+"?Subject=Cura: " + details.name + "\">" + details.author_name + "</a>" return "<a href=\"mailto:" + details.author_email+"?Subject=Cura: " + details.name + "\">" + details.author_name + "</a>"
@ -148,7 +156,7 @@ Item
} }
Label Label
{ {
text: details.download_count || catalog.i18nc("@label", "Unknown") text: details === null ? "" : (details.download_count || catalog.i18nc("@label", "Unknown"))
font: UM.Theme.getFont("very_small") font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
} }

View file

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V. // Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher. // Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2 import QtQuick 2.7
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM import UM 1.1 as UM

View file

@ -9,7 +9,7 @@ import UM 1.1 as UM
Item Item
{ {
property int packageCount: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getTotalNumberOfPackagesByAuthor(model.id) : 1 property int packageCount: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getTotalNumberOfMaterialPackagesByAuthor(model.id) : 1
property int installedPackages: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0) property int installedPackages: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0)
height: childrenRect.height height: childrenRect.height
Layout.alignment: Qt.AlignTop | Qt.AlignLeft Layout.alignment: Qt.AlignTop | Qt.AlignLeft

View file

@ -12,7 +12,9 @@ ScrollView
width: parent.width width: parent.width
height: parent.height height: parent.height
style: UM.Theme.styles.scrollview style: UM.Theme.styles.scrollview
flickableItem.flickableDirection: Flickable.VerticalFlick flickableItem.flickableDirection: Flickable.VerticalFlick
Column Column
{ {
width: base.width width: base.width
@ -30,7 +32,7 @@ ScrollView
id: allPlugins id: allPlugins
width: parent.width width: parent.width
heading: toolbox.viewCategory == "material" ? catalog.i18nc("@label", "Community Contributions") : catalog.i18nc("@label", "Community Plugins") heading: toolbox.viewCategory == "material" ? catalog.i18nc("@label", "Community Contributions") : catalog.i18nc("@label", "Community Plugins")
model: toolbox.viewCategory == "material" ? toolbox.authorsModel : toolbox.packagesModel model: toolbox.viewCategory == "material" ? toolbox.materialsAvailableModel : toolbox.pluginsAvailableModel
} }
ToolboxDownloadsGrid ToolboxDownloadsGrid

View file

@ -9,7 +9,7 @@ import UM 1.1 as UM
Rectangle Rectangle
{ {
property int packageCount: toolbox.viewCategory == "material" ? toolbox.getTotalNumberOfPackagesByAuthor(model.id) : 1 property int packageCount: toolbox.viewCategory == "material" ? toolbox.getTotalNumberOfMaterialPackagesByAuthor(model.id) : 1
property int installedPackages: toolbox.viewCategory == "material" ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0) property int installedPackages: toolbox.viewCategory == "material" ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0)
id: tileBase id: tileBase
width: UM.Theme.getSize("toolbox_thumbnail_large").width + (2 * UM.Theme.getSize("default_lining").width) width: UM.Theme.getSize("toolbox_thumbnail_large").width + (2 * UM.Theme.getSize("default_lining").width)

View file

@ -6,6 +6,7 @@ import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2 import QtQuick.Window 2.2
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4 import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM import UM 1.1 as UM
ScrollView ScrollView
@ -16,6 +17,7 @@ ScrollView
height: parent.height height: parent.height
style: UM.Theme.styles.scrollview style: UM.Theme.styles.scrollview
flickableItem.flickableDirection: Flickable.VerticalFlick flickableItem.flickableDirection: Flickable.VerticalFlick
Column Column
{ {
spacing: UM.Theme.getSize("default_margin").height spacing: UM.Theme.getSize("default_margin").height

View file

@ -33,6 +33,9 @@ class AuthorsModel(ListModel):
def _update(self): def _update(self):
items = [] items = []
if not self._metadata:
self.setItems([])
return
for author in self._metadata: for author in self._metadata:
items.append({ items.append({

View file

@ -5,9 +5,13 @@ import re
from typing import Dict from typing import Dict
from PyQt5.QtCore import Qt, pyqtProperty from PyQt5.QtCore import Qt, pyqtProperty
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from .ConfigsModel import ConfigsModel from .ConfigsModel import ConfigsModel
## Model that holds cura packages. By setting the filter property the instances held by this model can be changed. ## Model that holds cura packages. By setting the filter property the instances held by this model can be changed.
class PackagesModel(ListModel): class PackagesModel(ListModel):
def __init__(self, parent = None): def __init__(self, parent = None):
@ -34,6 +38,8 @@ class PackagesModel(ListModel):
self.addRoleName(Qt.UserRole + 17, "supported_configs") self.addRoleName(Qt.UserRole + 17, "supported_configs")
self.addRoleName(Qt.UserRole + 18, "download_count") self.addRoleName(Qt.UserRole + 18, "download_count")
self.addRoleName(Qt.UserRole + 19, "tags") self.addRoleName(Qt.UserRole + 19, "tags")
self.addRoleName(Qt.UserRole + 20, "links")
self.addRoleName(Qt.UserRole + 21, "website")
# List of filters for queries. The result is the union of the each list of results. # List of filters for queries. The result is the union of the each list of results.
self._filter = {} # type: Dict[str, str] self._filter = {} # type: Dict[str, str]
@ -45,10 +51,16 @@ class PackagesModel(ListModel):
def _update(self): def _update(self):
items = [] items = []
for package in self._metadata: if self._metadata is None:
Logger.logException("w", "Failed to load packages for Toolbox")
self.setItems(items)
return
for package in self._metadata:
has_configs = False has_configs = False
configs_model = None configs_model = None
links_dict = {}
if "data" in package: if "data" in package:
if "supported_configs" in package["data"]: if "supported_configs" in package["data"]:
if len(package["data"]["supported_configs"]) > 0: if len(package["data"]["supported_configs"]) > 0:
@ -56,41 +68,47 @@ class PackagesModel(ListModel):
configs_model = ConfigsModel() configs_model = ConfigsModel()
configs_model.setConfigs(package["data"]["supported_configs"]) 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"]: if "author_id" not in package["author"] or "display_name" not in package["author"]:
package["author"]["author_id"] = "" package["author"]["author_id"] = ""
package["author"]["display_name"] = "" package["author"]["display_name"] = ""
# raise Exception("Detected a package with malformed author data.")
items.append({ items.append({
"id": package["package_id"], "id": package["package_id"],
"type": package["package_type"], "type": package["package_type"],
"name": package["display_name"], "name": package["display_name"],
"version": package["package_version"], "version": package["package_version"],
"author_id": package["author"]["author_id"], "author_id": package["author"]["author_id"],
"author_name": package["author"]["display_name"], "author_name": package["author"]["display_name"],
"author_email": package["author"]["email"] if "email" in package["author"] else None, "author_email": package["author"]["email"] if "email" in package["author"] else None,
"description": package["description"] if "description" in package else None, "description": package["description"] if "description" in package else None,
"icon_url": package["icon_url"] if "icon_url" in package else None, "icon_url": package["icon_url"] if "icon_url" in package else None,
"image_urls": package["image_urls"] if "image_urls" in package else None, "image_urls": package["image_urls"] if "image_urls" in package else None,
"download_url": package["download_url"] if "download_url" in package else None, "download_url": package["download_url"] if "download_url" in package else None,
"last_updated": package["last_updated"] if "last_updated" in package else None, "last_updated": package["last_updated"] if "last_updated" in package else None,
"is_bundled": package["is_bundled"] if "is_bundled" in package else False, "is_bundled": package["is_bundled"] if "is_bundled" in package else False,
"is_active": package["is_active"] if "is_active" in package else False, "is_active": package["is_active"] if "is_active" in package else False,
"is_installed": package["is_installed"] if "is_installed" in package else False, "is_installed": package["is_installed"] if "is_installed" in package else False,
"has_configs": has_configs, "has_configs": has_configs,
"supported_configs": configs_model, "supported_configs": configs_model,
"download_count": package["download_count"] if "download_count" in package else 0, "download_count": package["download_count"] if "download_count" in package else 0,
"tags": package["tags"] if "tags" in package else [] "tags": package["tags"] if "tags" in package else [],
"links": links_dict,
"website": package["website"] if "website" in package else None,
}) })
# Filter on all the key-word arguments. # Filter on all the key-word arguments.
for key, value in self._filter.items(): for key, value in self._filter.items():
if key is "tags": if key is "tags":
key_filter = lambda item, value = value: value in item["tags"] key_filter = lambda item, v = value: v in item["tags"]
elif "*" in value: elif "*" in value:
key_filter = lambda candidate, key = key, value = value: self._matchRegExp(candidate, key, value) key_filter = lambda candidate, k = key, v = value: self._matchRegExp(candidate, k, v)
else: else:
key_filter = lambda candidate, key = key, value = value: self._matchString(candidate, key, value) key_filter = lambda candidate, k = key, v = value: self._matchString(candidate, k, v)
items = filter(key_filter, items) items = filter(key_filter, items)
# Execute all filters. # Execute all filters.

View file

@ -6,7 +6,7 @@ import json
import os import os
import tempfile import tempfile
import platform import platform
from typing import cast, List from typing import cast, List, TYPE_CHECKING, Tuple, Optional
from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
@ -20,9 +20,13 @@ from UM.Version import Version
import cura import cura
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from .AuthorsModel import AuthorsModel from .AuthorsModel import AuthorsModel
from .PackagesModel import PackagesModel from .PackagesModel import PackagesModel
if TYPE_CHECKING:
from cura.Settings.GlobalStack import GlobalStack
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -34,19 +38,19 @@ class Toolbox(QObject, Extension):
def __init__(self, application: CuraApplication) -> None: def __init__(self, application: CuraApplication) -> None:
super().__init__() super().__init__()
self._application = application #type: CuraApplication self._application = application # type: CuraApplication
self._sdk_version = None # type: Optional[int] self._sdk_version = None # type: Optional[int]
self._cloud_api_version = None # type: Optional[int] self._cloud_api_version = None # type: Optional[int]
self._cloud_api_root = None # type: Optional[str] self._cloud_api_root = None # type: Optional[str]
self._api_url = None # type: Optional[str] self._api_url = None # type: Optional[str]
# Network: # Network:
self._download_request = None #type: Optional[QNetworkRequest] self._download_request = None # type: Optional[QNetworkRequest]
self._download_reply = None #type: Optional[QNetworkReply] self._download_reply = None # type: Optional[QNetworkReply]
self._download_progress = 0 #type: float self._download_progress = 0 # type: float
self._is_downloading = False #type: bool self._is_downloading = False # type: bool
self._network_manager = None #type: Optional[QNetworkAccessManager] self._network_manager = None # type: Optional[QNetworkAccessManager]
self._request_header = [ self._request_header = [
b"User-Agent", b"User-Agent",
str.encode( str.encode(
@ -58,9 +62,9 @@ class Toolbox(QObject, Extension):
) )
) )
] ]
self._request_urls = {} # type: Dict[str, QUrl] self._request_urls = {} # type: Dict[str, QUrl]
self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated
self._old_plugin_ids = [] # type: List[str] self._old_plugin_ids = [] # type: List[str]
# Data: # Data:
self._metadata = { self._metadata = {
@ -73,7 +77,7 @@ class Toolbox(QObject, Extension):
"materials_available": [], "materials_available": [],
"materials_installed": [], "materials_installed": [],
"materials_generic": [] "materials_generic": []
} # type: Dict[str, List[Any]] } # type: Dict[str, List[Any]]
# Models: # Models:
self._models = { self._models = {
@ -83,42 +87,40 @@ class Toolbox(QObject, Extension):
"plugins_available": PackagesModel(self), "plugins_available": PackagesModel(self),
"plugins_installed": PackagesModel(self), "plugins_installed": PackagesModel(self),
"materials_showcase": AuthorsModel(self), "materials_showcase": AuthorsModel(self),
"materials_available": PackagesModel(self), "materials_available": AuthorsModel(self),
"materials_installed": PackagesModel(self), "materials_installed": PackagesModel(self),
"materials_generic": PackagesModel(self) "materials_generic": PackagesModel(self)
} # type: Dict[str, ListModel] } # type: Dict[str, ListModel]
# These properties are for keeping track of the UI state: # These properties are for keeping track of the UI state:
# ---------------------------------------------------------------------- # ----------------------------------------------------------------------
# View category defines which filter to use, and therefore effectively # View category defines which filter to use, and therefore effectively
# which category is currently being displayed. For example, possible # which category is currently being displayed. For example, possible
# values include "plugin" or "material", but also "installed". # values include "plugin" or "material", but also "installed".
self._view_category = "plugin" #type: str self._view_category = "plugin" # type: str
# View page defines which type of page layout to use. For example, # View page defines which type of page layout to use. For example,
# possible values include "overview", "detail" or "author". # possible values include "overview", "detail" or "author".
self._view_page = "loading" #type: str self._view_page = "loading" # type: str
# Active package refers to which package is currently being downloaded, # Active package refers to which package is currently being downloaded,
# installed, or otherwise modified. # installed, or otherwise modified.
self._active_package = None # type: Optional[Dict[str, Any]] self._active_package = None # type: Optional[Dict[str, Any]]
self._dialog = None #type: Optional[QObject] self._dialog = None # type: Optional[QObject]
self._confirm_reset_dialog = None #type: Optional[QObject] self._confirm_reset_dialog = None # type: Optional[QObject]
self._resetUninstallVariables() self._resetUninstallVariables()
self._restart_required = False #type: bool self._restart_required = False # type: bool
# variables for the license agreement dialog # variables for the license agreement dialog
self._license_dialog_plugin_name = "" #type: str self._license_dialog_plugin_name = "" # type: str
self._license_dialog_license_content = "" #type: str self._license_dialog_license_content = "" # type: str
self._license_dialog_plugin_file_location = "" #type: str self._license_dialog_plugin_file_location = "" # type: str
self._restart_dialog_message = "" #type: str self._restart_dialog_message = "" # type: str
self._application.initializationFinished.connect(self._onAppInitialized) self._application.initializationFinished.connect(self._onAppInitialized)
# Signals: # Signals:
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# Downloading changes # Downloading changes
@ -137,11 +139,11 @@ class Toolbox(QObject, Extension):
showLicenseDialog = pyqtSignal() showLicenseDialog = pyqtSignal()
uninstallVariablesChanged = pyqtSignal() uninstallVariablesChanged = pyqtSignal()
def _resetUninstallVariables(self): def _resetUninstallVariables(self) -> None:
self._package_id_to_uninstall = None self._package_id_to_uninstall = None # type: Optional[str]
self._package_name_to_uninstall = "" self._package_name_to_uninstall = ""
self._package_used_materials = [] self._package_used_materials = [] # type: List[Tuple[GlobalStack, str, str]]
self._package_used_qualities = [] self._package_used_qualities = [] # type: List[Tuple[GlobalStack, str, str]]
@pyqtSlot(result = str) @pyqtSlot(result = str)
def getLicenseDialogPluginName(self) -> str: def getLicenseDialogPluginName(self) -> str:
@ -229,10 +231,12 @@ class Toolbox(QObject, Extension):
# Make remote requests: # Make remote requests:
self._makeRequestByType("packages") self._makeRequestByType("packages")
self._makeRequestByType("authors") self._makeRequestByType("authors")
self._makeRequestByType("plugins_showcase") # TODO: Uncomment in the future when the tag-filtered api calls work in the cloud server
self._makeRequestByType("materials_showcase") # self._makeRequestByType("plugins_showcase")
self._makeRequestByType("materials_available") # self._makeRequestByType("plugins_available")
self._makeRequestByType("materials_generic") # self._makeRequestByType("materials_showcase")
# self._makeRequestByType("materials_available")
# self._makeRequestByType("materials_generic")
# Gather installed packages: # Gather installed packages:
self._updateInstalledModels() self._updateInstalledModels()
@ -344,26 +348,26 @@ class Toolbox(QObject, Extension):
self.uninstall(package_id) self.uninstall(package_id)
@pyqtProperty(str, notify = uninstallVariablesChanged) @pyqtProperty(str, notify = uninstallVariablesChanged)
def pluginToUninstall(self): def pluginToUninstall(self) -> str:
return self._package_name_to_uninstall return self._package_name_to_uninstall
@pyqtProperty(str, notify = uninstallVariablesChanged) @pyqtProperty(str, notify = uninstallVariablesChanged)
def uninstallUsedMaterials(self): def uninstallUsedMaterials(self) -> str:
return "\n".join(["%s (%s)" % (str(global_stack.getName()), material) for global_stack, extruder_nr, material in self._package_used_materials]) return "\n".join(["%s (%s)" % (str(global_stack.getName()), material) for global_stack, extruder_nr, material in self._package_used_materials])
@pyqtProperty(str, notify = uninstallVariablesChanged) @pyqtProperty(str, notify = uninstallVariablesChanged)
def uninstallUsedQualities(self): def uninstallUsedQualities(self) -> str:
return "\n".join(["%s (%s)" % (str(global_stack.getName()), quality) for global_stack, extruder_nr, quality in self._package_used_qualities]) return "\n".join(["%s (%s)" % (str(global_stack.getName()), quality) for global_stack, extruder_nr, quality in self._package_used_qualities])
@pyqtSlot() @pyqtSlot()
def closeConfirmResetDialog(self): def closeConfirmResetDialog(self) -> None:
if self._confirm_reset_dialog is not None: if self._confirm_reset_dialog is not None:
self._confirm_reset_dialog.close() self._confirm_reset_dialog.close()
## Uses "uninstall variables" to reset qualities and materials, then uninstall ## Uses "uninstall variables" to reset qualities and materials, then uninstall
# It's used as an action on Confirm reset on Uninstall # It's used as an action on Confirm reset on Uninstall
@pyqtSlot() @pyqtSlot()
def resetMaterialsQualitiesAndUninstall(self): def resetMaterialsQualitiesAndUninstall(self) -> None:
application = CuraApplication.getInstance() application = CuraApplication.getInstance()
material_manager = application.getMaterialManager() material_manager = application.getMaterialManager()
quality_manager = application.getQualityManager() quality_manager = application.getQualityManager()
@ -376,9 +380,9 @@ class Toolbox(QObject, Extension):
default_quality_group = quality_manager.getDefaultQualityType(global_stack) default_quality_group = quality_manager.getDefaultQualityType(global_stack)
machine_manager.setQualityGroup(default_quality_group, global_stack = global_stack) machine_manager.setQualityGroup(default_quality_group, global_stack = global_stack)
self._markPackageMaterialsAsToBeUninstalled(self._package_id_to_uninstall) if self._package_id_to_uninstall is not None:
self._markPackageMaterialsAsToBeUninstalled(self._package_id_to_uninstall)
self.uninstall(self._package_id_to_uninstall) self.uninstall(self._package_id_to_uninstall)
self._resetUninstallVariables() self._resetUninstallVariables()
self.closeConfirmResetDialog() self.closeConfirmResetDialog()
@ -514,12 +518,14 @@ class Toolbox(QObject, Extension):
count += 1 count += 1
return count return count
# This slot is only used to get the number of material packages by author, not any other type of packages.
@pyqtSlot(str, result = int) @pyqtSlot(str, result = int)
def getTotalNumberOfPackagesByAuthor(self, author_id: str) -> int: def getTotalNumberOfMaterialPackagesByAuthor(self, author_id: str) -> int:
count = 0 count = 0
for package in self._metadata["materials_available"]: for package in self._metadata["packages"]:
if package["author"]["author_id"] == author_id: if package["package_type"] == "material":
count += 1 if package["author"]["author_id"] == author_id:
count += 1
return count return count
@pyqtSlot(str, result = bool) @pyqtSlot(str, result = bool)
@ -606,8 +612,22 @@ class Toolbox(QObject, Extension):
self.resetDownload() self.resetDownload()
return return
# HACK: These request are not handled independently at this moment, but together from the "packages" call
do_not_handle = [
"materials_available",
"materials_showcase",
"materials_generic",
"plugins_available",
"plugins_showcase",
]
if reply.operation() == QNetworkAccessManager.GetOperation: if reply.operation() == QNetworkAccessManager.GetOperation:
for type, url in self._request_urls.items(): for type, url in self._request_urls.items():
# HACK: Do nothing because we'll handle these from the "packages" call
if type in do_not_handle:
return
if reply.url() == url: if reply.url() == url:
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200:
try: try:
@ -624,24 +644,15 @@ class Toolbox(QObject, Extension):
Logger.log("e", "Could not find the %s model.", type) Logger.log("e", "Could not find the %s model.", type)
break break
# HACK: Eventually get rid of the code from here... self._metadata[type] = json_data["data"]
if type is "plugins_showcase" or type is "materials_showcase": self._models[type].setMetadata(self._metadata[type])
self._metadata["plugins_showcase"] = json_data["data"]["plugin"]["packages"]
self._models["plugins_showcase"].setMetadata(self._metadata["plugins_showcase"])
self._metadata["materials_showcase"] = json_data["data"]["material"]["authors"]
self._models["materials_showcase"].setMetadata(self._metadata["materials_showcase"])
else:
# ...until here.
# This hack arises for multiple reasons but the main
# one is because there are not separate API calls
# for different kinds of showcases.
self._metadata[type] = json_data["data"]
self._models[type].setMetadata(self._metadata[type])
# Do some auto filtering # Do some auto filtering
# TODO: Make multiple API calls in the future to handle this # TODO: Make multiple API calls in the future to handle this
if type is "packages": if type is "packages":
self._models[type].setFilter({"type": "plugin"}) self._models[type].setFilter({"type": "plugin"})
self.buildMaterialsModels()
self.buildPluginsModels()
if type is "authors": if type is "authors":
self._models[type].setFilter({"package_types": "material"}) self._models[type].setFilter({"package_types": "material"})
if type is "materials_generic": if type is "materials_generic":
@ -680,7 +691,7 @@ class Toolbox(QObject, Extension):
self._temp_plugin_file.close() self._temp_plugin_file.close()
self._onDownloadComplete(file_path) self._onDownloadComplete(file_path)
def _onDownloadComplete(self, file_path: str): def _onDownloadComplete(self, file_path: str) -> None:
Logger.log("i", "Toolbox: Download complete.") Logger.log("i", "Toolbox: Download complete.")
package_info = self._package_manager.getPackageInfo(file_path) package_info = self._package_manager.getPackageInfo(file_path)
if not package_info: if not package_info:
@ -739,9 +750,7 @@ class Toolbox(QObject, Extension):
def viewPage(self) -> str: def viewPage(self) -> str:
return self._view_page return self._view_page
# Exposed Models:
# Expose Models:
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@pyqtProperty(QObject, notify = metadataChanged) @pyqtProperty(QObject, notify = metadataChanged)
def authorsModel(self) -> AuthorsModel: def authorsModel(self) -> AuthorsModel:
@ -755,6 +764,10 @@ class Toolbox(QObject, Extension):
def pluginsShowcaseModel(self) -> PackagesModel: def pluginsShowcaseModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["plugins_showcase"]) return cast(PackagesModel, self._models["plugins_showcase"])
@pyqtProperty(QObject, notify = metadataChanged)
def pluginsAvailableModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["plugins_available"])
@pyqtProperty(QObject, notify = metadataChanged) @pyqtProperty(QObject, notify = metadataChanged)
def pluginsInstalledModel(self) -> PackagesModel: def pluginsInstalledModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["plugins_installed"]) return cast(PackagesModel, self._models["plugins_installed"])
@ -763,6 +776,10 @@ class Toolbox(QObject, Extension):
def materialsShowcaseModel(self) -> AuthorsModel: def materialsShowcaseModel(self) -> AuthorsModel:
return cast(AuthorsModel, self._models["materials_showcase"]) return cast(AuthorsModel, self._models["materials_showcase"])
@pyqtProperty(QObject, notify = metadataChanged)
def materialsAvailableModel(self) -> AuthorsModel:
return cast(AuthorsModel, self._models["materials_available"])
@pyqtProperty(QObject, notify = metadataChanged) @pyqtProperty(QObject, notify = metadataChanged)
def materialsInstalledModel(self) -> PackagesModel: def materialsInstalledModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["materials_installed"]) return cast(PackagesModel, self._models["materials_installed"])
@ -771,8 +788,6 @@ class Toolbox(QObject, Extension):
def materialsGenericModel(self) -> PackagesModel: def materialsGenericModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["materials_generic"]) return cast(PackagesModel, self._models["materials_generic"])
# Filter Models: # Filter Models:
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
@pyqtSlot(str, str, str) @pyqtSlot(str, str, str)
@ -798,3 +813,48 @@ class Toolbox(QObject, Extension):
return return
self._models[model_type].setFilter({}) self._models[model_type].setFilter({})
self.filterChanged.emit() self.filterChanged.emit()
# HACK(S):
# --------------------------------------------------------------------------
def buildMaterialsModels(self) -> None:
self._metadata["materials_showcase"] = []
self._metadata["materials_available"] = []
processed_authors = [] # type: List[str]
for item in self._metadata["packages"]:
if item["package_type"] == "material":
author = item["author"]
if author["author_id"] in processed_authors:
continue
# Generic materials to be in the same section
if "generic" in item["tags"]:
self._metadata["materials_generic"].append(item)
else:
if "showcase" in item["tags"]:
self._metadata["materials_showcase"].append(author)
else:
self._metadata["materials_available"].append(author)
processed_authors.append(author["author_id"])
self._models["materials_showcase"].setMetadata(self._metadata["materials_showcase"])
self._models["materials_available"].setMetadata(self._metadata["materials_available"])
self._models["materials_generic"].setMetadata(self._metadata["materials_generic"])
def buildPluginsModels(self) -> None:
self._metadata["plugins_showcase"] = []
self._metadata["plugins_available"] = []
for item in self._metadata["packages"]:
if item["package_type"] == "plugin":
if "showcase" in item["tags"]:
self._metadata["plugins_showcase"].append(item)
else:
self._metadata["plugins_available"].append(item)
self._models["plugins_showcase"].setMetadata(self._metadata["plugins_showcase"])
self._models["plugins_available"].setMetadata(self._metadata["plugins_available"])

View file

@ -13,6 +13,7 @@ from UM.PluginRegistry import PluginRegistry #To get the g-code writer.
from PyQt5.QtCore import QBuffer from PyQt5.QtCore import QBuffer
from cura.Snapshot import Snapshot from cura.Snapshot import Snapshot
from cura.Utils.Threading import call_on_qt_thread
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -29,6 +30,11 @@ class UFPWriter(MeshWriter):
Logger.log("d", "Creating thumbnail image...") Logger.log("d", "Creating thumbnail image...")
self._snapshot = Snapshot.snapshot(width = 300, height = 300) self._snapshot = Snapshot.snapshot(width = 300, height = 300)
# This needs to be called on the main thread (Qt thread) because the serialization of material containers can
# trigger loading other containers. Because those loaded containers are QtObjects, they must be created on the
# Qt thread. The File read/write operations right now are executed on separated threads because they are scheduled
# by the Job class.
@call_on_qt_thread
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode): def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
archive = VirtualFile() archive = VirtualFile()
archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly) archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly)
@ -60,5 +66,50 @@ class UFPWriter(MeshWriter):
else: else:
Logger.log("d", "Thumbnail not created, cannot save it") Logger.log("d", "Thumbnail not created, cannot save it")
# Store the material.
application = Application.getInstance()
machine_manager = application.getMachineManager()
material_manager = application.getMaterialManager()
global_stack = machine_manager.activeMachine
material_extension = "xml.fdm_material"
material_mime_type = "application/x-ultimaker-material-profile"
try:
archive.addContentType(extension = material_extension, mime_type = material_mime_type)
except:
Logger.log("w", "The material extension: %s was already added", material_extension)
added_materials = []
for extruder_stack in global_stack.extruders.values():
material = extruder_stack.material
material_file_name = material.getMetaData()["base_file"] + ".xml.fdm_material"
material_file_name = "/Materials/" + material_file_name
#Same material cannot be added
if material_file_name in added_materials:
continue
material_root_id = material.getMetaDataEntry("base_file")
material_group = material_manager.getMaterialGroup(material_root_id)
if material_group is None:
Logger.log("e", "Cannot find material container with root id [%s]", material_root_id)
return False
material_container = material_group.root_material_node.getContainer()
try:
serialized_material = material_container.serialize()
except NotImplementedError:
Logger.log("e", "Unable serialize material container with root id: %s", material_root_id)
return False
material_file = archive.getStream(material_file_name)
material_file.write(serialized_material.encode("UTF-8"))
archive.addRelation(virtual_path = material_file_name,
relation_type = "http://schemas.ultimaker.org/package/2018/relationships/material",
origin = "/3D/model.gcode")
added_materials.append(material_file_name)
archive.close() archive.close()
return True return True

View file

@ -1,241 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import UM 1.3 as UM
import Cura 1.0 as Cura
Component
{
Rectangle
{
id: base
property var manager: Cura.MachineManager.printerOutputDevices[0]
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
visible: manager != null
anchors.fill: parent
color: UM.Theme.getColor("viewport_background")
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Label
{
id: activePrintersLabel
font: UM.Theme.getFont("large")
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right:parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: Cura.MachineManager.printerOutputDevices[0].name
elide: Text.ElideRight
}
Rectangle
{
id: printJobArea
border.width: UM.Theme.getSize("default_lining").width
border.color: lineColor
anchors.top: activePrintersLabel.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin:UM.Theme.getSize("default_margin").width
radius: cornerRadius
height: childrenRect.height
Item
{
id: printJobTitleBar
width: parent.width
height: printJobTitleLabel.height + 2 * UM.Theme.getSize("default_margin").height
Label
{
id: printJobTitleLabel
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
text: catalog.i18nc("@title", "Print jobs")
font: UM.Theme.getFont("default")
opacity: 0.75
}
Rectangle
{
anchors.bottom: parent.bottom
height: UM.Theme.getSize("default_lining").width
color: lineColor
width: parent.width
}
}
Column
{
id: printJobColumn
anchors.top: printJobTitleBar.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
Item
{
width: parent.width
height: childrenRect.height
opacity: 0.65
Label
{
text: catalog.i18nc("@label", "Printing")
font: UM.Theme.getFont("very_small")
}
Label
{
text: manager.activePrintJobs.length
font: UM.Theme.getFont("small")
anchors.right: parent.right
}
}
Item
{
width: parent.width
height: childrenRect.height
opacity: 0.65
Label
{
text: catalog.i18nc("@label", "Queued")
font: UM.Theme.getFont("very_small")
}
Label
{
text: manager.queuedPrintJobs.length
font: UM.Theme.getFont("small")
anchors.right: parent.right
}
}
}
OpenPanelButton
{
anchors.top: printJobColumn.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").height
id: configButton
onClicked: base.manager.openPrintJobControlPanel()
text: catalog.i18nc("@action:button", "View print jobs")
}
Item
{
// spacer
anchors.top: configButton.bottom
width: UM.Theme.getSize("default_margin").width
height: UM.Theme.getSize("default_margin").height
}
}
Rectangle
{
id: printersArea
border.width: UM.Theme.getSize("default_lining").width
border.color: lineColor
anchors.top: printJobArea.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin:UM.Theme.getSize("default_margin").width
radius: cornerRadius
height: childrenRect.height
Item
{
id: printersTitleBar
width: parent.width
height: printJobTitleLabel.height + 2 * UM.Theme.getSize("default_margin").height
Label
{
id: printersTitleLabel
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
text: catalog.i18nc("@label:title", "Printers")
font: UM.Theme.getFont("default")
opacity: 0.75
}
Rectangle
{
anchors.bottom: parent.bottom
height: UM.Theme.getSize("default_lining").width
color: lineColor
width: parent.width
}
}
Column
{
id: printersColumn
anchors.top: printersTitleBar.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
Repeater
{
model: manager.connectedPrintersTypeCount
Item
{
width: parent.width
height: childrenRect.height
opacity: 0.65
Label
{
text: modelData.machine_type
font: UM.Theme.getFont("very_small")
}
Label
{
text: modelData.count
font: UM.Theme.getFont("small")
anchors.right: parent.right
}
}
}
}
OpenPanelButton
{
anchors.top: printersColumn.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").height
id: printerConfigButton
onClicked: base.manager.openPrinterControlPanel()
text: catalog.i18nc("@action:button", "View printers")
}
Item
{
// spacer
anchors.top: printerConfigButton.bottom
width: UM.Theme.getSize("default_margin").width
height: UM.Theme.getSize("default_margin").height
}
}
}
}

View file

@ -1,118 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.3 as UM
import Cura 1.0 as Cura
Component
{
Rectangle
{
id: monitorFrame
width: maximumWidth
height: maximumHeight
color: UM.Theme.getColor("viewport_background")
property var emphasisColor: UM.Theme.getColor("setting_control_border_highlight")
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Label
{
id: activePrintersLabel
font: UM.Theme.getFont("large")
anchors {
top: parent.top
topMargin: UM.Theme.getSize("default_margin").height * 2 // a bit more spacing to give it some breathing room
horizontalCenter: parent.horizontalCenter
}
text: OutputDevice.printers.length == 0 ? catalog.i18nc("@label: arg 1 is group name", "%1 is not set up to host a group of connected Ultimaker 3 printers").arg(Cura.MachineManager.printerOutputDevices[0].name) : ""
visible: OutputDevice.printers.length == 0
}
Item
{
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
width: Math.min(800 * screenScaleFactor, maximumWidth)
height: children.height
visible: OutputDevice.printers.length != 0
Label
{
id: addRemovePrintersLabel
anchors.right: parent.right
text: catalog.i18nc("@label link to connect manager", "Add/Remove printers")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
}
MouseArea
{
anchors.fill: addRemovePrintersLabel
hoverEnabled: true
onClicked: Cura.MachineManager.printerOutputDevices[0].openPrinterControlPanel()
onEntered: addRemovePrintersLabel.font.underline = true
onExited: addRemovePrintersLabel.font.underline = false
}
}
ScrollView
{
id: printerScrollView
anchors.margins: UM.Theme.getSize("default_margin").width
anchors.top: activePrintersLabel.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_lining").width // To ensure border can be drawn.
anchors.rightMargin: UM.Theme.getSize("default_lining").width
anchors.right: parent.right
ListView
{
anchors.fill: parent
spacing: -UM.Theme.getSize("default_lining").height
model: OutputDevice.printers
delegate: PrinterInfoBlock
{
printer: modelData
width: Math.min(800 * screenScaleFactor, maximumWidth)
height: 125 * screenScaleFactor
// Add a 1 pix margin, as the border is sometimes cut off otherwise.
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
PrinterVideoStream
{
visible: OutputDevice.activePrinter != null
anchors.fill:parent
}
onVisibleChanged:
{
if (!monitorFrame.visible)
{
// After switching the Tab ensure that active printer is Null, the video stream image
// might be active
OutputDevice.setActivePrinter(null)
}
}
}
}

View file

@ -1,71 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.1 as UM
Button {
objectName: "openPanelSaveAreaButton"
id: openPanelSaveAreaButton
UM.I18nCatalog { id: catalog; name: "cura"; }
height: UM.Theme.getSize("save_button_save_to_button").height
tooltip: catalog.i18nc("@info:tooltip", "Opens the print jobs page with your default web browser.")
text: catalog.i18nc("@action:button", "View print jobs")
// FIXME: This button style is copied and duplicated from SaveButton.qml
style: ButtonStyle {
background: Rectangle
{
border.width: UM.Theme.getSize("default_lining").width
border.color:
{
if(!control.enabled)
return UM.Theme.getColor("action_button_disabled_border");
else if(control.pressed)
return UM.Theme.getColor("print_button_ready_pressed_border");
else if(control.hovered)
return UM.Theme.getColor("print_button_ready_hovered_border");
else
return UM.Theme.getColor("print_button_ready_border");
}
color:
{
if(!control.enabled)
return UM.Theme.getColor("action_button_disabled");
else if(control.pressed)
return UM.Theme.getColor("print_button_ready_pressed");
else if(control.hovered)
return UM.Theme.getColor("print_button_ready_hovered");
else
return UM.Theme.getColor("print_button_ready");
}
Behavior on color { ColorAnimation { duration: 50; } }
implicitWidth: actualLabel.contentWidth + (UM.Theme.getSize("sidebar_margin").width * 2)
Label {
id: actualLabel
anchors.centerIn: parent
color:
{
if(!control.enabled)
return UM.Theme.getColor("action_button_disabled_text");
else if(control.pressed)
return UM.Theme.getColor("print_button_ready_text");
else if(control.hovered)
return UM.Theme.getColor("print_button_ready_text");
else
return UM.Theme.getColor("print_button_ready_text");
}
font: UM.Theme.getFont("action_button")
text: control.text;
}
}
label: Item { }
}
}

View file

@ -1,33 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.2 as UM
Item
{
id: extruderInfo
property var printCoreConfiguration
width: Math.round(parent.width / 2)
height: childrenRect.height
Label
{
id: materialLabel
text: printCoreConfiguration.activeMaterial != null ? printCoreConfiguration.activeMaterial.name : ""
elide: Text.ElideRight
width: parent.width
font: UM.Theme.getFont("very_small")
}
Label
{
id: printCoreLabel
text: printCoreConfiguration.hotendID
anchors.top: materialLabel.bottom
elide: Text.ElideRight
width: parent.width
font: UM.Theme.getFont("very_small")
opacity: 0.5
}
}

View file

@ -1,431 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.3 as UM
Rectangle
{
function strPadLeft(string, pad, length)
{
return (new Array(length + 1).join(pad) + string).slice(-length);
}
function getPrettyTime(time)
{
return OutputDevice.formatDuration(time)
}
function formatPrintJobPercent(printJob)
{
if (printJob == null)
{
return "";
}
if (printJob.timeTotal === 0)
{
return "";
}
return Math.min(100, Math.round(printJob.timeElapsed / printJob.timeTotal * 100)) + "%";
}
function printerStatusText(printer)
{
switch (printer.state)
{
case "pre_print":
return catalog.i18nc("@label:status", "Preparing to print")
case "printing":
return catalog.i18nc("@label:status", "Printing");
case "idle":
return catalog.i18nc("@label:status", "Available");
case "unreachable":
return catalog.i18nc("@label:status", "Lost connection with the printer");
case "maintenance":
return catalog.i18nc("@label:status", "Unavailable");
default:
return catalog.i18nc("@label:status", "Unknown");
}
}
id: printerDelegate
property var printer: null
property var printJob: printer != null ? printer.activePrintJob: null
border.width: UM.Theme.getSize("default_lining").width
border.color: mouse.containsMouse ? emphasisColor : lineColor
z: mouse.containsMouse ? 1 : 0 // Push this item up a bit on mouse over to ensure that the highlighted bottom border is visible.
MouseArea
{
id: mouse
anchors.fill:parent
onClicked: OutputDevice.setActivePrinter(printer)
hoverEnabled: true;
// Only clickable if no printer is selected
enabled: OutputDevice.activePrinter == null && printer.state !== "unreachable"
}
Row
{
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: UM.Theme.getSize("default_margin").width
Rectangle
{
width: Math.round(parent.width / 3)
height: parent.height
Label // Print job name
{
id: jobNameLabel
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: printJob != null ? printJob.name : ""
font: UM.Theme.getFont("default_bold")
elide: Text.ElideRight
}
Label
{
id: jobOwnerLabel
anchors.top: jobNameLabel.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: printJob != null ? printJob.owner : ""
opacity: 0.50
elide: Text.ElideRight
}
Label
{
id: totalTimeLabel
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: printJob != null ? getPrettyTime(printJob.timeTotal) : ""
opacity: 0.65
font: UM.Theme.getFont("default")
elide: Text.ElideRight
}
}
Rectangle
{
width: Math.round(parent.width / 3 * 2)
height: parent.height
Label // Friendly machine name
{
id: printerNameLabel
anchors.top: parent.top
anchors.left: parent.left
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width - showCameraIcon.width)
text: printer.name
font: UM.Theme.getFont("default_bold")
elide: Text.ElideRight
}
Label // Machine variant
{
id: printerTypeLabel
anchors.top: printerNameLabel.bottom
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
text: printer.type
anchors.left: parent.left
elide: Text.ElideRight
font: UM.Theme.getFont("very_small")
opacity: 0.50
}
Rectangle // Camera icon
{
id: showCameraIcon
width: 40 * screenScaleFactor
height: width
radius: width
anchors.right: printProgressArea.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
color: emphasisColor
opacity: printer != null && printer.state === "unreachable" ? 0.3 : 1
Image
{
width: parent.width
height: width
anchors.right: parent.right
anchors.rightMargin: parent.rightMargin
source: "camera-icon.svg"
}
}
Row // PrintCore config
{
id: extruderInfo
anchors.bottom: parent.bottom
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
PrintCoreConfiguration
{
id: leftExtruderInfo
width: Math.round((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.extruders[0]
}
Rectangle
{
id: extruderSeperator
width: UM.Theme.getSize("default_lining").width
height: parent.height
color: lineColor
}
PrintCoreConfiguration
{
id: rightExtruderInfo
width: Math.round((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.extruders[1]
}
}
Rectangle // Print progress
{
id: printProgressArea
anchors.right: parent.right
anchors.top: parent.top
height: showExtended ? parent.height: printProgressTitleBar.height
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
border.width: UM.Theme.getSize("default_lining").width
border.color: lineColor
radius: cornerRadius
property var showExtended: {
if(printJob != null)
{
var extendStates = ["sent_to_printer", "wait_for_configuration", "printing", "pre_print", "post_print", "wait_cleanup", "queued"];
return extendStates.indexOf(printJob.state) !== -1;
}
return printer.state == "disabled"
}
Item // Status and Percent
{
id: printProgressTitleBar
property var showPercent: {
return printJob != null && (["printing", "post_print", "pre_print", "sent_to_printer"].indexOf(printJob.state) !== -1);
}
width: parent.width
//TODO: hardcoded value
height: 40 * screenScaleFactor
anchors.left: parent.left
Label
{
id: statusText
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: progressText.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
text: {
if (printer.state == "disabled")
{
return catalog.i18nc("@label:status", "Disabled");
}
if (printer.state === "unreachable")
{
return printerStatusText(printer);
}
if (printJob != null)
{
switch (printJob.state)
{
case "printing":
case "post_print":
return catalog.i18nc("@label:status", "Printing")
case "wait_for_configuration":
return catalog.i18nc("@label:status", "Reserved")
case "wait_cleanup":
case "wait_user_action":
return catalog.i18nc("@label:status", "Finished")
case "pre_print":
case "sent_to_printer":
return catalog.i18nc("@label", "Preparing to print")
case "queued":
return catalog.i18nc("@label:status", "Action required");
case "pausing":
case "paused":
return catalog.i18nc("@label:status", "Paused");
case "resuming":
return catalog.i18nc("@label:status", "Resuming");
case "aborted":
return catalog.i18nc("@label:status", "Print aborted");
default:
// If print job has unknown status show printer.status
return printerStatusText(printer);
}
}
return printerStatusText(printer);
}
elide: Text.ElideRight
font: UM.Theme.getFont("small")
}
Label
{
id: progressText
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.top: statusText.top
text: formatPrintJobPercent(printJob)
visible: printProgressTitleBar.showPercent
//TODO: Hardcoded value
opacity: 0.65
font: UM.Theme.getFont("very_small")
}
Image
{
width: statusText.height
height: width
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.top: statusText.top
visible: !printProgressTitleBar.showPercent
source: {
if (printer.state == "disabled")
{
return "blocked-icon.svg";
}
if (printer.state === "unreachable")
{
return "";
}
if (printJob != null)
{
if(printJob.state === "queued")
{
return "action-required-icon.svg";
}
else if (printJob.state === "wait_cleanup")
{
return "checkmark-icon.svg";
}
}
return ""; // We're not going to show it, so it will not be resolved as a url.
}
}
Rectangle
{
//TODO: This will become a progress bar in the future
width: parent.width
height: UM.Theme.getSize("default_lining").height
anchors.bottom: parent.bottom
anchors.left: parent.left
visible: printProgressArea.showExtended
color: lineColor
}
}
Column
{
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.top: printProgressTitleBar.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width - 2 * UM.Theme.getSize("default_margin").width
visible: printProgressArea.showExtended
Label // Status detail
{
text:
{
if (printer.state == "disabled")
{
return catalog.i18nc("@label", "Not accepting print jobs");
}
if (printer.state === "unreachable")
{
return "";
}
if(printJob != null)
{
switch (printJob.state)
{
case "printing":
case "post_print":
return catalog.i18nc("@label", "Finishes at: ") + OutputDevice.getTimeCompleted(printJob.timeTotal - printJob.timeElapsed)
case "wait_cleanup":
return catalog.i18nc("@label", "Clear build plate")
case "sent_to_printer":
case "pre_print":
return catalog.i18nc("@label", "Preparing to print")
case "wait_for_configuration":
return catalog.i18nc("@label", "Not accepting print jobs")
case "queued":
return catalog.i18nc("@label", "Waiting for configuration change");
default:
return "";
}
}
return "";
}
anchors.left: parent.left
anchors.right: parent.right
elide: Text.ElideRight
wrapMode: Text.Wrap
font: UM.Theme.getFont("default")
}
Label // Status 2nd row
{
text: {
if(printJob != null)
{
if(printJob.state == "printing" || printJob.state == "post_print")
{
return OutputDevice.getDateCompleted(printJob.timeTotal - printJob.timeElapsed)
}
}
return "";
}
elide: Text.ElideRight
font: UM.Theme.getFont("default")
}
}
}
}
}
}

View file

@ -1,54 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.3 as UM
import Cura 1.0 as Cura
Rectangle
{
id: base
width: 250 * screenScaleFactor
height: 250 * screenScaleFactor
signal clicked()
MouseArea
{
anchors.fill:parent
onClicked: base.clicked()
}
Rectangle
{
// TODO: Actually add UM icon / picture
width: 100 * screenScaleFactor
height: 100 * screenScaleFactor
border.width: UM.Theme.getSize("default_lining").width
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
}
Label
{
id: nameLabel
anchors.bottom: ipLabel.top
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: modelData.friendly_name.toString()
font: UM.Theme.getFont("large")
elide: Text.ElideMiddle;
height: UM.Theme.getSize("section").height;
}
Label
{
id: ipLabel
text: modelData.ip_address.toString()
anchors.bottom: parent.bottom
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
font: UM.Theme.getFont("default")
height:10 * screenScaleFactor
anchors.horizontalCenter: parent.horizontalCenter
}
}

View file

@ -1,11 +1,11 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from . import DiscoverUM3Action from .src import DiscoverUM3Action
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
from . import UM3OutputDevicePlugin from .src import UM3OutputDevicePlugin
def getMetaData(): def getMetaData():
return {} return {}

View file

@ -0,0 +1,717 @@
import QtQuick 2.3
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls 2.0 as Controls2
import UM 1.3 as UM
import Cura 1.0 as Cura
Component
{
Rectangle
{
id: base
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
visible: OutputDevice != null
anchors.fill: parent
color: "white"
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Label
{
id: printingLabel
font: UM.Theme.getFont("large")
anchors
{
margins: 2 * UM.Theme.getSize("default_margin").width
leftMargin: 4 * UM.Theme.getSize("default_margin").width
top: parent.top
left: parent.left
right: parent.right
}
text: catalog.i18nc("@label", "Printing")
elide: Text.ElideRight
}
Label
{
id: managePrintersLabel
anchors.rightMargin: 4 * UM.Theme.getSize("default_margin").width
anchors.right: printerScrollView.right
anchors.bottom: printingLabel.bottom
text: catalog.i18nc("@label link to connect manager", "Manage printers")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("primary")
linkColor: UM.Theme.getColor("primary")
}
MouseArea
{
anchors.fill: managePrintersLabel
hoverEnabled: true
onClicked: Cura.MachineManager.printerOutputDevices[0].openPrinterControlPanel()
onEntered: managePrintersLabel.font.underline = true
onExited: managePrintersLabel.font.underline = false
}
ScrollView
{
id: printerScrollView
anchors
{
top: printingLabel.bottom
left: parent.left
right: parent.right
topMargin: UM.Theme.getSize("default_margin").height
bottom: parent.bottom
bottomMargin: UM.Theme.getSize("default_margin").height
}
style: UM.Theme.styles.scrollview
ListView
{
anchors
{
top: parent.top
bottom: parent.bottom
left: parent.left
right: parent.right
leftMargin: 2 * UM.Theme.getSize("default_margin").width
rightMargin: 2 * UM.Theme.getSize("default_margin").width
}
spacing: UM.Theme.getSize("default_margin").height -10
model: OutputDevice.printers
delegate: Item
{
width: parent.width
height: base.height + 2 * base.shadowRadius // To ensure that the shadow doesn't get cut off.
Rectangle
{
width: parent.width - 2 * shadowRadius
height: childrenRect.height + UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
id: base
property var shadowRadius: 5
property var collapsed: true
layer.enabled: true
layer.effect: DropShadow
{
radius: base.shadowRadius
verticalOffset: 2
color: "#3F000000" // 25% shadow
}
Item
{
id: printerInfo
height: machineIcon.height
anchors
{
top: parent.top
left: parent.left
right: parent.right
margins: UM.Theme.getSize("default_margin").width
}
MouseArea
{
anchors.fill: parent
onClicked: base.collapsed = !base.collapsed
}
Item
{
id: machineIcon
// Yeah, this is hardcoded now, but I can't think of a good way to fix this.
// The UI is going to get another update soon, so it's probably not worth the effort...
width: 58
height: 58
anchors.top: parent.top
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.left: parent.left
UM.RecolorImage
{
anchors.centerIn: parent
source:
{
switch(modelData.type)
{
case "Ultimaker 3":
return "../svg/UM3-icon.svg"
case "Ultimaker 3 Extended":
return "../svg/UM3x-icon.svg"
case "Ultimaker S5":
return "../svg/UMs5-icon.svg"
}
}
width: sourceSize.width
height: sourceSize.height
color:
{
if(modelData.state == "disabled")
{
return UM.Theme.getColor("setting_control_disabled")
}
if(modelData.activePrintJob != undefined)
{
return UM.Theme.getColor("primary")
}
return UM.Theme.getColor("setting_control_disabled")
}
}
}
Item
{
height: childrenRect.height
anchors
{
right: collapseIcon.left
rightMargin: UM.Theme.getSize("default_margin").width
left: machineIcon.right
leftMargin: UM.Theme.getSize("default_margin").width
verticalCenter: machineIcon.verticalCenter
}
Label
{
id: machineNameLabel
text: modelData.name
width: parent.width
elide: Text.ElideRight
font: UM.Theme.getFont("default_bold")
}
Label
{
id: activeJobLabel
text:
{
if (modelData.state == "disabled")
{
return catalog.i18nc("@label", "Not available")
} else if (modelData.state == "unreachable")
{
return catalog.i18nc("@label", "Unreachable")
}
if (modelData.activePrintJob != null)
{
return modelData.activePrintJob.name
}
return catalog.i18nc("@label", "Available")
}
anchors.top: machineNameLabel.bottom
width: parent.width
elide: Text.ElideRight
font: UM.Theme.getFont("default")
opacity: 0.6
}
}
UM.RecolorImage
{
id: collapseIcon
width: 15
height: 15
sourceSize.width: width
sourceSize.height: height
source: base.collapsed ? UM.Theme.getIcon("arrow_left") : UM.Theme.getIcon("arrow_bottom")
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
color: "black"
}
}
Item
{
id: detailedInfo
property var printJob: modelData.activePrintJob
visible: height == childrenRect.height
anchors.top: printerInfo.bottom
width: parent.width
height: !base.collapsed ? childrenRect.height : 0
opacity: visible ? 1 : 0
Behavior on height { NumberAnimation { duration: 100 } }
Behavior on opacity { NumberAnimation { duration: 100 } }
Rectangle
{
id: topSpacer
color: UM.Theme.getColor("viewport_background")
height: 2
anchors
{
left: parent.left
right: parent.right
margins: UM.Theme.getSize("default_margin").width
top: parent.top
topMargin: UM.Theme.getSize("default_margin").width
}
}
PrinterFamilyPill
{
id: printerFamilyPill
color: UM.Theme.getColor("viewport_background")
anchors.top: topSpacer.bottom
anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height
text: modelData.type
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
padding: 3
}
Row
{
id: extrudersInfo
anchors.top: printerFamilyPill.bottom
anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: 2 * UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin: 2 * UM.Theme.getSize("default_margin").width
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
PrintCoreConfiguration
{
id: leftExtruderInfo
width: Math.round(parent.width / 2)
printCoreConfiguration: modelData.printerConfiguration.extruderConfigurations[0]
}
PrintCoreConfiguration
{
id: rightExtruderInfo
width: Math.round(parent.width / 2)
printCoreConfiguration: modelData.printerConfiguration.extruderConfigurations[1]
}
}
Rectangle
{
id: jobSpacer
color: UM.Theme.getColor("viewport_background")
height: 2
anchors
{
left: parent.left
right: parent.right
margins: UM.Theme.getSize("default_margin").width
top: extrudersInfo.bottom
topMargin: 2 * UM.Theme.getSize("default_margin").height
}
}
Item
{
id: jobInfo
property var showJobInfo: modelData.activePrintJob != null && modelData.activePrintJob.state != "queued"
anchors.top: jobSpacer.bottom
anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").width
anchors.leftMargin: 2 * UM.Theme.getSize("default_margin").width
height: showJobInfo ? childrenRect.height + 2 * UM.Theme.getSize("default_margin").height: 0
visible: showJobInfo
Label
{
id: printJobName
text: modelData.activePrintJob != null ? modelData.activePrintJob.name : ""
font: UM.Theme.getFont("default_bold")
anchors.left: parent.left
anchors.right: contextButton.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
elide: Text.ElideRight
}
Label
{
id: ownerName
anchors.top: printJobName.bottom
text: modelData.activePrintJob != null ? modelData.activePrintJob.owner : ""
font: UM.Theme.getFont("default")
opacity: 0.6
width: parent.width
elide: Text.ElideRight
}
function switchPopupState()
{
if (popup.visible)
{
popup.close()
}
else
{
popup.open()
}
}
Controls2.Button
{
id: contextButton
text: "\u22EE" //Unicode; Three stacked points.
font.pixelSize: 25
width: 35
height: width
anchors
{
right: parent.right
top: parent.top
}
hoverEnabled: true
background: Rectangle
{
opacity: contextButton.down || contextButton.hovered ? 1 : 0
width: contextButton.width
height: contextButton.height
radius: 0.5 * width
color: UM.Theme.getColor("viewport_background")
}
onClicked: parent.switchPopupState()
}
Controls2.Popup
{
// TODO Change once updating to Qt5.10 - The 'opened' property is in 5.10 but the behavior is now implemented with the visible property
id: popup
clip: true
closePolicy: Controls2.Popup.CloseOnPressOutsideParent
x: parent.width - width
y: contextButton.height
width: 160
height: contentItem.height + 2 * padding
visible: false
transformOrigin: Controls2.Popup.Top
contentItem: Item
{
width: popup.width - 2 * popup.padding
height: childrenRect.height + 15
Controls2.Button
{
id: pauseButton
text: modelData.activePrintJob != null && modelData.activePrintJob.state == "paused" ? catalog.i18nc("@label", "Resume") : catalog.i18nc("@label", "Pause")
onClicked:
{
if(modelData.activePrintJob.state == "paused")
{
modelData.activePrintJob.setState("print")
}
else if(modelData.activePrintJob.state == "printing")
{
modelData.activePrintJob.setState("pause")
}
popup.close()
}
width: parent.width
enabled: modelData.activePrintJob != null && ["paused", "printing"].indexOf(modelData.activePrintJob.state) >= 0
anchors.top: parent.top
anchors.topMargin: 10
hoverEnabled: true
background: Rectangle
{
opacity: pauseButton.down || pauseButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background")
}
}
Controls2.Button
{
id: abortButton
text: catalog.i18nc("@label", "Abort")
onClicked:
{
modelData.activePrintJob.setState("abort")
popup.close()
}
width: parent.width
anchors.top: pauseButton.bottom
hoverEnabled: true
enabled: modelData.activePrintJob != null && ["paused", "printing", "pre_print"].indexOf(modelData.activePrintJob.state) >= 0
background: Rectangle
{
opacity: abortButton.down || abortButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background")
}
}
}
background: Item
{
width: popup.width
height: popup.height
DropShadow
{
anchors.fill: pointedRectangle
radius: 5
color: "#3F000000" // 25% shadow
source: pointedRectangle
transparentBorder: true
verticalOffset: 2
}
Item
{
id: pointedRectangle
width: parent.width -10
height: parent.height -10
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
Rectangle
{
id: point
height: 13
width: 13
color: UM.Theme.getColor("setting_control")
transform: Rotation { angle: 45}
anchors.right: bloop.right
y: 1
}
Rectangle
{
id: bloop
color: UM.Theme.getColor("setting_control")
width: parent.width
anchors.top: parent.top
anchors.topMargin: 10
anchors.bottom: parent.bottom
anchors.bottomMargin: 5
}
}
}
exit: Transition
{
// This applies a default NumberAnimation to any changes a state change makes to x or y properties
NumberAnimation { property: "visible"; duration: 75; }
}
enter: Transition
{
// This applies a default NumberAnimation to any changes a state change makes to x or y properties
NumberAnimation { property: "visible"; duration: 75; }
}
onClosed: visible = false
onOpened: visible = true
}
Image
{
id: printJobPreview
source: modelData.activePrintJob != null ? modelData.activePrintJob.previewImageUrl : ""
anchors.top: ownerName.bottom
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width / 2
height: width
opacity:
{
if(modelData.activePrintJob == null)
{
return 1.0
}
switch(modelData.activePrintJob.state)
{
case "wait_cleanup":
case "wait_user_action":
case "paused":
return 0.5
default:
return 1.0
}
}
}
UM.RecolorImage
{
id: statusImage
anchors.centerIn: printJobPreview
source:
{
if(modelData.activePrintJob == null)
{
return ""
}
switch(modelData.activePrintJob.state)
{
case "paused":
return "../svg/paused-icon.svg"
case "wait_cleanup":
if(modelData.activePrintJob.timeElapsed < modelData.activePrintJob.timeTotal)
{
return "../svg/aborted-icon.svg"
}
return "../svg/approved-icon.svg"
case "wait_user_action":
return "../svg/aborted-icon.svg"
default:
return ""
}
}
visible: source != ""
width: 0.5 * printJobPreview.width
height: 0.5 * printJobPreview.height
sourceSize.width: width
sourceSize.height: height
color: "black"
}
Rectangle
{
id: showCameraIcon
width: 35 * screenScaleFactor
height: width
radius: 0.5 * width
anchors.left: parent.left
anchors.bottom: printJobPreview.bottom
color: UM.Theme.getColor("setting_control_border_highlight")
Image
{
width: parent.width
height: width
anchors.right: parent.right
anchors.rightMargin: parent.rightMargin
source: "../svg/camera-icon.svg"
}
MouseArea
{
anchors.fill:parent
onClicked:
{
OutputDevice.setActiveCamera(modelData.camera)
}
}
}
}
}
ProgressBar
{
property var progress:
{
if(modelData.activePrintJob == null)
{
return 0
}
var result = modelData.activePrintJob.timeElapsed / modelData.activePrintJob.timeTotal
if(result > 1.0)
{
result = 1.0
}
return result
}
id: jobProgressBar
width: parent.width
value: progress
anchors.top: detailedInfo.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
visible: modelData.activePrintJob != null && modelData.activePrintJob != undefined
style: ProgressBarStyle
{
property var progressText:
{
if(modelData.activePrintJob == null)
{
return ""
}
switch(modelData.activePrintJob.state)
{
case "wait_cleanup":
if(modelData.activePrintJob.timeTotal > modelData.activePrintJob.timeElapsed)
{
return catalog.i18nc("@label:status", "Aborted")
}
return catalog.i18nc("@label:status", "Finished")
case "pre_print":
case "sent_to_printer":
return catalog.i18nc("@label:status", "Preparing")
case "aborted":
case "wait_user_action":
return catalog.i18nc("@label:status", "Aborted")
case "pausing":
return catalog.i18nc("@label:status", "Pausing")
case "paused":
return catalog.i18nc("@label:status", "Paused")
case "resuming":
return catalog.i18nc("@label:status", "Resuming")
case "queued":
return catalog.i18nc("@label:status", "Action required")
default:
OutputDevice.formatDuration(modelData.activePrintJob.timeTotal - modelData.activePrintJob.timeElapsed)
}
}
background: Rectangle
{
implicitWidth: 100
implicitHeight: visible ? 24 : 0
color: UM.Theme.getColor("viewport_background")
}
progress: Rectangle
{
color: UM.Theme.getColor("primary")
id: progressItem
function getTextOffset()
{
if(progressItem.width + progressLabel.width < control.width)
{
return progressItem.width + UM.Theme.getSize("default_margin").width
}
else
{
return progressItem.width - progressLabel.width - UM.Theme.getSize("default_margin").width
}
}
Label
{
id: progressLabel
anchors.left: parent.left
anchors.leftMargin: getTextOffset()
text: progressText
anchors.verticalCenter: parent.verticalCenter
color: progressItem.width + progressLabel.width < control.width ? "black" : "white"
width: contentWidth
font: UM.Theme.getFont("default")
}
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,108 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.3 as UM
import Cura 1.0 as Cura
Component
{
Rectangle
{
id: monitorFrame
width: maximumWidth
height: maximumHeight
color: UM.Theme.getColor("viewport_background")
property var emphasisColor: UM.Theme.getColor("setting_control_border_highlight")
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Label
{
id: manageQueueLabel
anchors.rightMargin: 4 * UM.Theme.getSize("default_margin").width
anchors.right: queuedPrintJobs.right
anchors.bottom: queuedLabel.bottom
text: catalog.i18nc("@label link to connect manager", "Manage queue")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("primary")
linkColor: UM.Theme.getColor("primary")
}
MouseArea
{
anchors.fill: manageQueueLabel
hoverEnabled: true
onClicked: Cura.MachineManager.printerOutputDevices[0].openPrintJobControlPanel()
onEntered: manageQueueLabel.font.underline = true
onExited: manageQueueLabel.font.underline = false
}
Label
{
id: queuedLabel
anchors.left: queuedPrintJobs.left
anchors.top: parent.top
anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height
anchors.leftMargin: 3 * UM.Theme.getSize("default_margin").width
text: catalog.i18nc("@label", "Queued")
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
}
ScrollView
{
id: queuedPrintJobs
anchors
{
top: queuedLabel.bottom
topMargin: UM.Theme.getSize("default_margin").height
horizontalCenter: parent.horizontalCenter
bottomMargin: 0
bottom: parent.bottom
}
style: UM.Theme.styles.scrollview
width: Math.min(800 * screenScaleFactor, maximumWidth)
ListView
{
anchors.fill: parent
//anchors.margins: UM.Theme.getSize("default_margin").height
spacing: UM.Theme.getSize("default_margin").height - 10 // 2x the shadow radius
model: OutputDevice.queuedPrintJobs
delegate: PrintJobInfoBlock
{
printJob: modelData
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").height
anchors.leftMargin: UM.Theme.getSize("default_margin").height
height: 175 * screenScaleFactor
}
}
}
PrinterVideoStream
{
visible: OutputDevice.activeCamera != null
anchors.fill: parent
camera: OutputDevice.activeCamera
}
onVisibleChanged:
{
if (monitorFrame != null && !monitorFrame.visible)
{
OutputDevice.setActiveCamera(null)
}
}
}
}

View file

@ -364,7 +364,6 @@ Cura.MachineAction
{ {
id: addressField id: addressField
width: parent.width width: parent.width
maximumLength: 40
validator: RegExpValidator validator: RegExpValidator
{ {
regExp: /[a-zA-Z0-9\.\-\_]*/ regExp: /[a-zA-Z0-9\.\-\_]*/

View file

@ -0,0 +1,93 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.2 as UM
Item
{
id: extruderInfo
property var printCoreConfiguration
width: Math.round(parent.width / 2)
height: childrenRect.height
Item
{
id: extruderCircle
width: 30
height: 30
anchors.verticalCenter: printAndMaterialLabel.verticalCenter
opacity:
{
if(printCoreConfiguration == null || printCoreConfiguration.activeMaterial == null || printCoreConfiguration.hotendID == null)
{
return 0.5
}
return 1
}
Rectangle
{
anchors.fill: parent
radius: Math.round(width / 2)
border.width: 2
border.color: "black"
}
Label
{
anchors.centerIn: parent
font: UM.Theme.getFont("default_bold")
text: printCoreConfiguration.position + 1
}
}
Item
{
id: printAndMaterialLabel
anchors
{
right: parent.right
left: extruderCircle.right
margins: UM.Theme.getSize("default_margin").width
}
height: childrenRect.height
Label
{
id: materialLabel
text:
{
if(printCoreConfiguration != undefined && printCoreConfiguration.activeMaterial != undefined)
{
return printCoreConfiguration.activeMaterial.name
}
return ""
}
font: UM.Theme.getFont("default_bold")
elide: Text.ElideRight
width: parent.width
}
Label
{
id: printCoreLabel
text:
{
if(printCoreConfiguration != undefined && printCoreConfiguration.hotendID != undefined)
{
return printCoreConfiguration.hotendID
}
return ""
}
anchors.top: materialLabel.bottom
elide: Text.ElideRight
width: parent.width
opacity: 0.6
font: UM.Theme.getFont("default")
}
}
}

View file

@ -0,0 +1,378 @@
import QtQuick 2.2
import QtQuick.Controls 2.0
import QtQuick.Controls.Styles 1.4
import QtGraphicalEffects 1.0
import UM 1.3 as UM
Item
{
id: base
property var printJob: null
property var shadowRadius: 5
function getPrettyTime(time)
{
return OutputDevice.formatDuration(time)
}
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Rectangle
{
id: background
anchors
{
top: parent.top
topMargin: 3
left: parent.left
leftMargin: base.shadowRadius
rightMargin: base.shadowRadius
right: parent.right
bottom: parent.bottom
bottomMargin: base.shadowRadius
}
layer.enabled: true
layer.effect: DropShadow
{
radius: base.shadowRadius
verticalOffset: 2
color: "#3F000000" // 25% shadow
}
Item
{
// Content on the left of the infobox
anchors
{
top: parent.top
bottom: parent.bottom
left: parent.left
right: parent.horizontalCenter
margins: 2 * UM.Theme.getSize("default_margin").width
rightMargin: UM.Theme.getSize("default_margin").width
}
Label
{
id: printJobName
text: printJob.name
font: UM.Theme.getFont("default_bold")
width: parent.width
elide: Text.ElideRight
}
Label
{
id: ownerName
anchors.top: printJobName.bottom
text: printJob.owner
font: UM.Theme.getFont("default")
opacity: 0.6
width: parent.width
elide: Text.ElideRight
}
Image
{
id: printJobPreview
source: printJob.previewImageUrl
anchors.top: ownerName.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: totalTimeLabel.bottom
width: height
opacity: printJob.state == "error" ? 0.5 : 1.0
}
UM.RecolorImage
{
id: statusImage
anchors.centerIn: printJobPreview
source: printJob.state == "error" ? "../svg/aborted-icon.svg" : ""
visible: source != ""
width: 0.5 * printJobPreview.width
height: 0.5 * printJobPreview.height
sourceSize.width: width
sourceSize.height: height
color: "black"
}
Label
{
id: totalTimeLabel
opacity: 0.6
anchors.bottom: parent.bottom
anchors.right: parent.right
font: UM.Theme.getFont("default")
text: printJob != null ? getPrettyTime(printJob.timeTotal) : ""
elide: Text.ElideRight
}
}
Item
{
// Content on the right side of the infobox.
anchors
{
top: parent.top
bottom: parent.bottom
left: parent.horizontalCenter
right: parent.right
margins: 2 * UM.Theme.getSize("default_margin").width
leftMargin: UM.Theme.getSize("default_margin").width
}
Label
{
id: targetPrinterLabel
elide: Text.ElideRight
font: UM.Theme.getFont("default_bold")
text:
{
if(printJob.assignedPrinter == null)
{
if(printJob.state == "error")
{
return catalog.i18nc("@label", "Waiting for: Unavailable printer")
}
return catalog.i18nc("@label", "Waiting for: First available")
}
else
{
return catalog.i18nc("@label", "Waiting for: ") + printJob.assignedPrinter.name
}
}
anchors
{
left: parent.left
right: contextButton.left
rightMargin: UM.Theme.getSize("default_margin").width
}
}
function switchPopupState()
{
popup.visible ? popup.close() : popup.open()
}
Button
{
id: contextButton
text: "\u22EE" //Unicode; Three stacked points.
font.pixelSize: 25
width: 35
height: width
anchors
{
right: parent.right
top: parent.top
}
hoverEnabled: true
background: Rectangle
{
opacity: contextButton.down || contextButton.hovered ? 1 : 0
width: contextButton.width
height: contextButton.height
radius: 0.5 * width
color: UM.Theme.getColor("viewport_background")
}
onClicked: parent.switchPopupState()
}
Popup
{
// TODO Change once updating to Qt5.10 - The 'opened' property is in 5.10 but the behavior is now implemented with the visible property
id: popup
clip: true
closePolicy: Popup.CloseOnPressOutsideParent
x: parent.width - width
y: contextButton.height
width: 160
height: contentItem.height + 2 * padding
visible: false
transformOrigin: Popup.Top
contentItem: Item
{
width: popup.width - 2 * popup.padding
height: childrenRect.height + 15
Button
{
id: sendToTopButton
text: catalog.i18nc("@label", "Move to top")
onClicked:
{
OutputDevice.sendJobToTop(printJob.key)
popup.close()
}
width: parent.width
enabled: OutputDevice.queuedPrintJobs[0].key != printJob.key
anchors.top: parent.top
anchors.topMargin: 10
hoverEnabled: true
background: Rectangle
{
opacity: sendToTopButton.down || sendToTopButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background")
}
}
Button
{
id: deleteButton
text: catalog.i18nc("@label", "Delete")
onClicked:
{
OutputDevice.deleteJobFromQueue(printJob.key)
popup.close()
}
width: parent.width
anchors.top: sendToTopButton.bottom
hoverEnabled: true
background: Rectangle
{
opacity: deleteButton.down || deleteButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background")
}
}
}
background: Item
{
width: popup.width
height: popup.height
DropShadow
{
anchors.fill: pointedRectangle
radius: 5
color: "#3F000000" // 25% shadow
source: pointedRectangle
transparentBorder: true
verticalOffset: 2
}
Item
{
id: pointedRectangle
width: parent.width -10
height: parent.height -10
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
Rectangle
{
id: point
height: 13
width: 13
color: UM.Theme.getColor("setting_control")
transform: Rotation { angle: 45}
anchors.right: bloop.right
y: 1
}
Rectangle
{
id: bloop
color: UM.Theme.getColor("setting_control")
width: parent.width
anchors.top: parent.top
anchors.topMargin: 10
anchors.bottom: parent.bottom
anchors.bottomMargin: 5
}
}
}
exit: Transition
{
// This applies a default NumberAnimation to any changes a state change makes to x or y properties
NumberAnimation { property: "visible"; duration: 75; }
}
enter: Transition
{
// This applies a default NumberAnimation to any changes a state change makes to x or y properties
NumberAnimation { property: "visible"; duration: 75; }
}
onClosed: visible = false
onOpened: visible = true
}
Row
{
id: printerFamilyPills
spacing: 0.5 * UM.Theme.getSize("default_margin").width
anchors
{
left: parent.left
right: parent.right
bottom: extrudersInfo.top
bottomMargin: UM.Theme.getSize("default_margin").height
}
height: childrenRect.height
Repeater
{
model: printJob.compatibleMachineFamilies
delegate: PrinterFamilyPill
{
text: modelData
color: UM.Theme.getColor("viewport_background")
padding: 3
}
}
}
// PrintCore && Material config
Row
{
id: extrudersInfo
anchors.bottom: parent.bottom
anchors
{
left: parent.left
right: parent.right
}
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
PrintCoreConfiguration
{
id: leftExtruderInfo
width: Math.round(parent.width / 2)
printCoreConfiguration: printJob.configuration.extruderConfigurations[0]
}
PrintCoreConfiguration
{
id: rightExtruderInfo
width: Math.round(parent.width / 2)
printCoreConfiguration: printJob.configuration.extruderConfigurations[1]
}
}
}
Rectangle
{
color: UM.Theme.getColor("viewport_background")
width: 2
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter
}
}
}

View file

@ -0,0 +1,28 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import UM 1.2 as UM
Item
{
property alias color: background.color
property alias text: familyNameLabel.text
property var padding: 0
implicitHeight: familyNameLabel.contentHeight + 2 * padding // Apply the padding to top and bottom.
implicitWidth: familyNameLabel.contentWidth + implicitHeight // The extra height is added to ensure the radius doesn't cut something off.
Rectangle
{
id: background
height: parent.height
width: parent.width
color: parent.color
anchors.right: parent.right
anchors.horizontalCenter: parent.horizontalCenter
radius: 0.5 * height
}
Label
{
id: familyNameLabel
anchors.centerIn: parent
text: ""
}
}

View file

@ -7,6 +7,8 @@ import UM 1.3 as UM
Item Item
{ {
property var camera: null
Rectangle Rectangle
{ {
anchors.fill:parent anchors.fill:parent
@ -17,7 +19,7 @@ Item
MouseArea MouseArea
{ {
anchors.fill: parent anchors.fill: parent
onClicked: OutputDevice.setActivePrinter(null) onClicked: OutputDevice.setActiveCamera(null)
z: 0 z: 0
} }
@ -32,7 +34,7 @@ Item
width: 20 * screenScaleFactor width: 20 * screenScaleFactor
height: 20 * screenScaleFactor height: 20 * screenScaleFactor
onClicked: OutputDevice.setActivePrinter(null) onClicked: OutputDevice.setActiveCamera(null)
style: ButtonStyle style: ButtonStyle
{ {
@ -65,23 +67,24 @@ Item
{ {
if(visible) if(visible)
{ {
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null) if(camera != null)
{ {
OutputDevice.activePrinter.camera.start() camera.start()
} }
} else } else
{ {
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null) if(camera != null)
{ {
OutputDevice.activePrinter.camera.stop() camera.stop()
} }
} }
} }
source: source:
{ {
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage) if(camera != null && camera.latestImage != null)
{ {
return OutputDevice.activePrinter.camera.latestImage; return camera.latestImage;
} }
return ""; return "";
} }
@ -92,7 +95,7 @@ Item
anchors.fill: cameraImage anchors.fill: cameraImage
onClicked: onClicked:
{ {
OutputDevice.setActivePrinter(null) OutputDevice.setActiveCamera(null)
} }
z: 1 z: 1
} }

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 46.16 48"><defs><style>.cls-1{fill:#9a9a9a;}</style></defs><title>UM3-icon</title><g id="Symbols"><g id="system_overview_inactive" data-name="system overview inactive"><path id="Shape" class="cls-1" d="M18.4,12.2h9.26c.1,0,.1,0,.2-.2l1.73-4.27a.22.22,0,0,0-.2-.2H16.67a.22.22,0,0,0-.2.2L18.2,12C18.3,12.2,18.3,12.2,18.4,12.2Z"/><path id="Shape-2" data-name="Shape" class="cls-1" d="M38.33,35.08H7.72a.48.48,0,0,0-.5.51V37a.48.48,0,0,0,.5.51H38.44a.48.48,0,0,0,.5-.51V35.59A.64.64,0,0,0,38.33,35.08Z"/><path id="Shape-3" data-name="Shape" class="cls-1" d="M0,0V48H3.76a2.86,2.86,0,0,1,2.13-1H40.27a2.86,2.86,0,0,1,2.13,1h3.76V0ZM41.28,37a2.83,2.83,0,0,1-2.84,2.84H7.72A2.84,2.84,0,0,1,4.88,37V5.49a.65.65,0,0,1,.61-.61H40.67a.65.65,0,0,1,.61.61Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 847 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 46.16 58"><defs><style>.cls-1{fill:#9a9a9a;}</style></defs><title>UM3x-icon</title><g id="Symbols"><g id="system_overview_inactive" data-name="system overview inactive"><path id="Shape" class="cls-1" d="M18.4,12.2h9.26c.1,0,.1,0,.2-.2l1.73-4.27a.22.22,0,0,0-.2-.2H16.67a.22.22,0,0,0-.2.2L18.2,12C18.3,12.2,18.3,12.2,18.4,12.2Z"/><path id="Shape-2" data-name="Shape" class="cls-1" d="M38.33,45.08H7.72a.48.48,0,0,0-.5.51V47a.48.48,0,0,0,.5.51H38.44a.48.48,0,0,0,.5-.51V45.59A.64.64,0,0,0,38.33,45.08Z"/><path id="Shape-3" data-name="Shape" class="cls-1" d="M0,0V58H3.76a2.86,2.86,0,0,1,2.13-1H40.27a2.86,2.86,0,0,1,2.13,1h3.76V0ZM41.28,35.32V47a2.83,2.83,0,0,1-2.84,2.84H7.72A2.84,2.84,0,0,1,4.88,47V5.49a.65.65,0,0,1,.61-.61H40.67a.65.65,0,0,1,.61.61Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 854 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58"><defs><style>.cls-1{fill:none;}.cls-2{fill:#9a9a9a;}</style></defs><title>UMs5-icon</title><path class="cls-1" d="M33.83,12.33c-.1.2-.1.2-.2.2H24.37c-.1,0-.1,0-.2-.2L22.44,8.06a.22.22,0,0,1,.2-.2H35.36a.22.22,0,0,1,.2.2Z"/><path class="cls-2" d="M35.36,7.86H22.64a.22.22,0,0,0-.2.2l1.73,4.27c.1.2.1.2.2.2h9.26c.1,0,.1,0,.2-.2l1.73-4.27A.22.22,0,0,0,35.36,7.86Z"/><path class="cls-2" d="M0,0V58H3.75a2.85,2.85,0,0,1,2.12-1H52.13a2.85,2.85,0,0,1,2.12,1H58V0ZM37.5,53.82a1.5,1.5,0,0,1-1.5,1.5H22a1.5,1.5,0,0,1-1.5-1.5v-4a1.5,1.5,0,0,1,1.5-1.5H36a1.5,1.5,0,0,1,1.5,1.5Zm15.63-18.5V47a2.83,2.83,0,0,1-2.83,2.84H38.5v0a2.5,2.5,0,0,0-2.5-2.5H22a2.5,2.5,0,0,0-2.5,2.5v0H7.7A2.83,2.83,0,0,1,4.87,47V5.49a.65.65,0,0,1,.6-.61H52.53a.65.65,0,0,1,.6.61Z"/></svg>

After

Width:  |  Height:  |  Size: 842 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><title>aborted-icon</title><path d="M16,0A16,16,0,1,0,32,16,16,16,0,0,0,16,0Zm1.69,28.89a13,13,0,1,1,11.2-11.2A13,13,0,0,1,17.69,28.89Z"/><polygon points="20.6 9.28 16 13.88 11.4 9.28 9.28 11.4 13.88 16 9.28 20.6 11.4 22.72 16 18.12 20.6 22.72 22.72 20.6 18.12 16 22.72 11.4 20.6 9.28"/></svg>

After

Width:  |  Height:  |  Size: 386 B

View file

Before

Width:  |  Height:  |  Size: 844 B

After

Width:  |  Height:  |  Size: 844 B

Before After
Before After

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><title>approved-icon</title><path d="M16,29A13,13,0,1,0,3,16,13,13,0,0,0,16,29ZM8,14.59l6,5.3L23.89,9l2.22,2L14.18,24.11,6,16.83Z" fill="none"/><path d="M16,32A16,16,0,1,0,0,16,16,16,0,0,0,16,32ZM16,3A13,13,0,1,1,3,16,13,13,0,0,1,16,3Z"/><polygon points="26.11 11.01 23.89 8.99 13.96 19.89 8 14.59 6 16.83 14.18 24.11 26.11 11.01"/></svg>

After

Width:  |  Height:  |  Size: 431 B

View file

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 311 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 438 B

After

Width:  |  Height:  |  Size: 438 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 194 B

After

Width:  |  Height:  |  Size: 194 B

Before After
Before After

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><title>paused-icon</title><path d="M16,0A16,16,0,1,0,32,16,16,16,0,0,0,16,0Zm0,29A13,13,0,1,1,29,16,13,13,0,0,1,16,29Z"/><rect x="11.5" y="9" width="3" height="14"/><rect x="17.5" y="9" width="3" height="14"/></svg>

After

Width:  |  Height:  |  Size: 308 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><title>warning-icon</title><path d="M18.09,1.31A2.35,2.35,0,0,0,16,0a2.31,2.31,0,0,0-2.09,1.31L.27,28.44A2.49,2.49,0,0,0,.11,30.3a2.38,2.38,0,0,0,1.16,1.42A2.33,2.33,0,0,0,2.36,32H29.64A2.4,2.4,0,0,0,32,29.57a2.55,2.55,0,0,0-.27-1.14ZM3.34,29,16,3.83,28.66,29Z"/><polygon points="13.94 25.19 13.94 25.19 13.94 25.19 13.94 25.19"/><polygon points="14.39 21.68 17.61 21.68 18.11 11.85 13.89 11.85 14.39 21.68"/><path d="M16.06,23.08a2.19,2.19,0,0,0-1.56,3.66,2.14,2.14,0,0,0,1.56.55,2.06,2.06,0,0,0,1.54-.55,2.1,2.1,0,0,0,.55-1.55,2.17,2.17,0,0,0-.53-1.55A2.05,2.05,0,0,0,16.06,23.08Z"/></svg>

After

Width:  |  Height:  |  Size: 684 B

View file

@ -4,19 +4,21 @@
from typing import Any, cast, Optional, Set, Tuple, Union from typing import Any, cast, Optional, Set, Tuple, Union
from UM.FileHandler.FileHandler import FileHandler from UM.FileHandler.FileHandler import FileHandler
from UM.FileHandler.FileWriter import FileWriter #To choose based on the output file mode (text vs. binary). from UM.FileHandler.FileWriter import FileWriter # To choose based on the output file mode (text vs. binary).
from UM.FileHandler.WriteFileJob import WriteFileJob #To call the file writer asynchronously. from UM.FileHandler.WriteFileJob import WriteFileJob # To call the file writer asynchronously.
from UM.Logger import Logger from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Mesh.MeshWriter import MeshWriter # For typing
from UM.Message import Message from UM.Message import Message
from UM.Qt.Duration import Duration, DurationFormat from UM.Qt.Duration import Duration, DurationFormat
from UM.OutputDevice import OutputDeviceError #To show that something went wrong when writing. from UM.OutputDevice import OutputDeviceError # To show that something went wrong when writing.
from UM.Scene.SceneNode import SceneNode #For typing. from UM.Scene.SceneNode import SceneNode # For typing.
from UM.Version import Version #To check against firmware versions for support. from UM.Version import Version # To check against firmware versions for support.
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
@ -27,14 +29,14 @@ from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController
from .SendMaterialJob import SendMaterialJob from .SendMaterialJob import SendMaterialJob
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices, QImage
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
from time import time from time import time
from datetime import datetime from datetime import datetime
from typing import Optional, Dict, List, Set from typing import Optional, Dict, List
import io #To create the correct buffers for sending data to the printer. import io # To create the correct buffers for sending data to the printer.
import json import json
import os import os
@ -44,6 +46,7 @@ i18n_catalog = i18nCatalog("cura")
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice): class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
printJobsChanged = pyqtSignal() printJobsChanged = pyqtSignal()
activePrinterChanged = pyqtSignal() activePrinterChanged = pyqtSignal()
activeCameraChanged = pyqtSignal()
# This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in. # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in.
# Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions. # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions.
@ -59,24 +62,24 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._print_jobs = [] # type: List[PrintJobOutputModel] self._print_jobs = [] # type: List[PrintJobOutputModel]
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml") self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/ClusterMonitorItem.qml")
self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml") self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/ClusterControlItem.qml")
# See comments about this hack with the clusterPrintersChanged signal # See comments about this hack with the clusterPrintersChanged signal
self.printersChanged.connect(self.clusterPrintersChanged) self.printersChanged.connect(self.clusterPrintersChanged)
self._accepts_commands = True #type: bool self._accepts_commands = True # type: bool
# Cluster does not have authentication, so default to authenticated # Cluster does not have authentication, so default to authenticated
self._authentication_state = AuthState.Authenticated self._authentication_state = AuthState.Authenticated
self._error_message = None #type: Optional[Message] self._error_message = None # type: Optional[Message]
self._write_job_progress_message = None #type: Optional[Message] self._write_job_progress_message = None # type: Optional[Message]
self._progress_message = None #type: Optional[Message] self._progress_message = None # type: Optional[Message]
self._active_printer = None # type: Optional[PrinterOutputModel] self._active_printer = None # type: Optional[PrinterOutputModel]
self._printer_selection_dialog = None #type: QObject self._printer_selection_dialog = None # type: QObject
self.setPriority(3) # Make sure the output device gets selected above local file output self.setPriority(3) # Make sure the output device gets selected above local file output
self.setName(self._id) self.setName(self._id)
@ -87,32 +90,35 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._printer_uuid_to_unique_name_mapping = {} # type: Dict[str, str] self._printer_uuid_to_unique_name_mapping = {} # type: Dict[str, str]
self._finished_jobs = [] # type: List[PrintJobOutputModel] self._finished_jobs = [] # type: List[PrintJobOutputModel]
self._cluster_size = int(properties.get(b"cluster_size", 0)) self._cluster_size = int(properties.get(b"cluster_size", 0)) # type: int
self._latest_reply_handler = None #type: Optional[QNetworkReply] self._latest_reply_handler = None # type: Optional[QNetworkReply]
self._sending_job = None
self._active_camera = None # type: Optional[NetworkCamera]
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
self.writeStarted.emit(self) self.writeStarted.emit(self)
self.sendMaterialProfiles() self.sendMaterialProfiles()
#Formats supported by this application (file types that we can actually write). # Formats supported by this application (file types that we can actually write).
if file_handler: if file_handler:
file_formats = file_handler.getSupportedFileTypesWrite() file_formats = file_handler.getSupportedFileTypesWrite()
else: else:
file_formats = CuraApplication.getInstance().getMeshFileHandler().getSupportedFileTypesWrite() file_formats = CuraApplication.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()
global_stack = CuraApplication.getInstance().getGlobalContainerStack() global_stack = CuraApplication.getInstance().getGlobalContainerStack()
#Create a list from the supported file formats string. # Create a list from the supported file formats string.
if not global_stack: if not global_stack:
Logger.log("e", "Missing global stack!") Logger.log("e", "Missing global stack!")
return return
machine_file_formats = global_stack.getMetaDataEntry("file_formats").split(";") machine_file_formats = global_stack.getMetaDataEntry("file_formats").split(";")
machine_file_formats = [file_type.strip() for file_type in machine_file_formats] machine_file_formats = [file_type.strip() for file_type in machine_file_formats]
#Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format. # Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
if "application/x-ufp" not in machine_file_formats and Version(self.firmwareVersion) >= Version("4.4"): if "application/x-ufp" not in machine_file_formats and Version(self.firmwareVersion) >= Version("4.4"):
machine_file_formats = ["application/x-ufp"] + machine_file_formats machine_file_formats = ["application/x-ufp"] + machine_file_formats
@ -125,7 +131,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
raise OutputDeviceError.WriteRequestFailedError(i18n_catalog.i18nc("@info:status", "There are no file formats available to write with!")) raise OutputDeviceError.WriteRequestFailedError(i18n_catalog.i18nc("@info:status", "There are no file formats available to write with!"))
preferred_format = file_formats[0] preferred_format = file_formats[0]
#Just take the first file format available. # Just take the first file format available.
if file_handler is not None: if file_handler is not None:
writer = file_handler.getWriterByMimeType(cast(str, preferred_format["mime_type"])) writer = file_handler.getWriterByMimeType(cast(str, preferred_format["mime_type"]))
else: else:
@ -135,29 +141,30 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
Logger.log("e", "Unexpected error when trying to get the FileWriter") Logger.log("e", "Unexpected error when trying to get the FileWriter")
return return
#This function pauses with the yield, waiting on instructions on which printer it needs to print with. # This function pauses with the yield, waiting on instructions on which printer it needs to print with.
if not writer: if not writer:
Logger.log("e", "Missing file or mesh writer!") Logger.log("e", "Missing file or mesh writer!")
return return
self._sending_job = self._sendPrintJob(writer, preferred_format, nodes) self._sending_job = self._sendPrintJob(writer, preferred_format, nodes)
self._sending_job.send(None) #Start the generator. if self._sending_job is not None:
self._sending_job.send(None) # Start the generator.
if len(self._printers) > 1: #We need to ask the user. if len(self._printers) > 1: # We need to ask the user.
self._spawnPrinterSelectionDialog() self._spawnPrinterSelectionDialog()
is_job_sent = True is_job_sent = True
else: #Just immediately continue. else: # Just immediately continue.
self._sending_job.send("") #No specifically selected printer. self._sending_job.send("") # No specifically selected printer.
is_job_sent = self._sending_job.send(None) is_job_sent = self._sending_job.send(None)
def _spawnPrinterSelectionDialog(self): def _spawnPrinterSelectionDialog(self):
if self._printer_selection_dialog is None: if self._printer_selection_dialog is None:
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PrintWindow.qml") path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/PrintWindow.qml")
self._printer_selection_dialog = CuraApplication.getInstance().createQmlComponent(path, {"OutputDevice": self}) self._printer_selection_dialog = CuraApplication.getInstance().createQmlComponent(path, {"OutputDevice": self})
if self._printer_selection_dialog is not None: if self._printer_selection_dialog is not None:
self._printer_selection_dialog.show() self._printer_selection_dialog.show()
@pyqtProperty(int, constant=True) @pyqtProperty(int, constant=True)
def clusterSize(self): def clusterSize(self) -> int:
return self._cluster_size return self._cluster_size
## Allows the user to choose a printer to print with from the printer ## Allows the user to choose a printer to print with from the printer
@ -165,7 +172,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
# \param target_printer The name of the printer to target. # \param target_printer The name of the printer to target.
@pyqtSlot(str) @pyqtSlot(str)
def selectPrinter(self, target_printer: str = "") -> None: def selectPrinter(self, target_printer: str = "") -> None:
self._sending_job.send(target_printer) if self._sending_job is not None:
self._sending_job.send(target_printer)
@pyqtSlot() @pyqtSlot()
def cancelPrintSelection(self) -> None: def cancelPrintSelection(self) -> None:
@ -214,8 +222,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
job.start() job.start()
yield True #Return that we had success! yield True # Return that we had success!
yield #To prevent having to catch the StopIteration exception. yield # To prevent having to catch the StopIteration exception.
def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None: def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None:
if self._write_job_progress_message: if self._write_job_progress_message:
@ -240,7 +248,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
file_name = CuraApplication.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"] file_name = CuraApplication.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"]
output = stream.getvalue() #Either str or bytes depending on the output mode. output = stream.getvalue() # Either str or bytes depending on the output mode.
if isinstance(stream, io.StringIO): if isinstance(stream, io.StringIO):
output = cast(str, output).encode("utf-8") output = cast(str, output).encode("utf-8")
output = cast(bytes, output) output = cast(bytes, output)
@ -253,6 +261,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
def activePrinter(self) -> Optional[PrinterOutputModel]: def activePrinter(self) -> Optional[PrinterOutputModel]:
return self._active_printer return self._active_printer
@pyqtProperty(QObject, notify=activeCameraChanged)
def activeCamera(self) -> Optional[NetworkCamera]:
return self._active_camera
@pyqtSlot(QObject) @pyqtSlot(QObject)
def setActivePrinter(self, printer: Optional[PrinterOutputModel]) -> None: def setActivePrinter(self, printer: Optional[PrinterOutputModel]) -> None:
if self._active_printer != printer: if self._active_printer != printer:
@ -261,6 +273,19 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._active_printer = printer self._active_printer = printer
self.activePrinterChanged.emit() self.activePrinterChanged.emit()
@pyqtSlot(QObject)
def setActiveCamera(self, camera: Optional[NetworkCamera]) -> None:
if self._active_camera != camera:
if self._active_camera:
self._active_camera.stop()
self._active_camera = camera
if self._active_camera:
self._active_camera.start()
self.activeCameraChanged.emit()
def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None: def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None:
if self._progress_message: if self._progress_message:
self._progress_message.hide() self._progress_message.hide()
@ -279,8 +304,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
# If successfully sent: # If successfully sent:
if bytes_sent == bytes_total: if bytes_sent == bytes_total:
# Show a confirmation to the user so they know the job was sucessful and provide the option to switch to the # Show a confirmation to the user so they know the job was sucessful and provide the option to switch to
# monitor tab. # the monitor tab.
self._success_message = Message( self._success_message = Message(
i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."), i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."),
lifetime=5, dismissable=True, lifetime=5, dismissable=True,
@ -329,7 +354,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
@pyqtProperty("QVariantList", notify = printJobsChanged) @pyqtProperty("QVariantList", notify = printJobsChanged)
def queuedPrintJobs(self) -> List[PrintJobOutputModel]: def queuedPrintJobs(self) -> List[PrintJobOutputModel]:
return [print_job for print_job in self._print_jobs if print_job.state == "queued"] return [print_job for print_job in self._print_jobs if print_job.state == "queued" or print_job.state == "error"]
@pyqtProperty("QVariantList", notify = printJobsChanged) @pyqtProperty("QVariantList", notify = printJobsChanged)
def activePrintJobs(self) -> List[PrintJobOutputModel]: def activePrintJobs(self) -> List[PrintJobOutputModel]:
@ -348,6 +373,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
result.append({"machine_type": machine_type, "count": str(printer_count[machine_type])}) result.append({"machine_type": machine_type, "count": str(printer_count[machine_type])})
return result return result
@pyqtProperty("QVariantList", notify=clusterPrintersChanged)
def printers(self):
return self._printers
@pyqtSlot(int, result = str) @pyqtSlot(int, result = str)
def formatDuration(self, seconds: int) -> str: def formatDuration(self, seconds: int) -> str:
return Duration(seconds).getDisplayString(DurationFormat.Format.Short) return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
@ -364,6 +393,19 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
datetime_completed = datetime.fromtimestamp(current_time + time_remaining) datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper() return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
@pyqtSlot(str)
def sendJobToTop(self, print_job_uuid: str) -> None:
# This function is part of the output device (and not of the printjob output model) as this type of operation
# is a modification of the cluster queue and not of the actual job.
data = "{\"to_position\": 0}"
self.put("print_jobs/{uuid}/move_to_position".format(uuid = print_job_uuid), data, on_finished=None)
@pyqtSlot(str)
def deleteJobFromQueue(self, print_job_uuid: str) -> None:
# This function is part of the output device (and not of the printjob output model) as this type of operation
# is a modification of the cluster queue and not of the actual job.
self.delete("print_jobs/{uuid}".format(uuid = print_job_uuid), on_finished=None)
def _printJobStateChanged(self) -> None: def _printJobStateChanged(self) -> None:
username = self._getUserName() username = self._getUserName()
@ -392,11 +434,26 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
super().connect() super().connect()
self.sendMaterialProfiles() self.sendMaterialProfiles()
def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None:
reply_url = reply.url().toString()
uuid = reply_url[reply_url.find("print_jobs/")+len("print_jobs/"):reply_url.rfind("/preview_image")]
print_job = findByKey(self._print_jobs, uuid)
if print_job:
image = QImage()
image.loadFromData(reply.readAll())
print_job.updatePreviewImage(image)
def _update(self) -> None: def _update(self) -> None:
super()._update() super()._update()
self.get("printers/", on_finished = self._onGetPrintersDataFinished) self.get("printers/", on_finished = self._onGetPrintersDataFinished)
self.get("print_jobs/", on_finished = self._onGetPrintJobsFinished) self.get("print_jobs/", on_finished = self._onGetPrintJobsFinished)
for print_job in self._print_jobs:
if print_job.getPreviewImage() is None:
self.get("print_jobs/{uuid}/preview_image".format(uuid=print_job.key), on_finished=self._onGetPreviewImageFinished)
def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None: def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None:
if not checkValidGetReply(reply): if not checkValidGetReply(reply):
return return
@ -407,16 +464,19 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
print_jobs_seen = [] print_jobs_seen = []
job_list_changed = False job_list_changed = False
for print_job_data in result: for idx, print_job_data in enumerate(result):
print_job = findByKey(self._print_jobs, print_job_data["uuid"]) print_job = findByKey(self._print_jobs, print_job_data["uuid"])
if print_job is None: if print_job is None:
print_job = self._createPrintJobModel(print_job_data) print_job = self._createPrintJobModel(print_job_data)
job_list_changed = True job_list_changed = True
elif not job_list_changed:
# Check if the order of the jobs has changed since the last check
if self._print_jobs.index(print_job) != idx:
job_list_changed = True
self._updatePrintJob(print_job, print_job_data) self._updatePrintJob(print_job, print_job_data)
if print_job.state != "queued": # Print job should be assigned to a printer. if print_job.state != "queued" and print_job.state != "error": # Print job should be assigned to a printer.
if print_job.state in ["failed", "finished", "aborted", "none"]: if print_job.state in ["failed", "finished", "aborted", "none"]:
# Print job was already completed, so don't attach it to a printer. # Print job was already completed, so don't attach it to a printer.
printer = None printer = None
@ -437,6 +497,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
job_list_changed = job_list_changed or self._removeJob(removed_job) job_list_changed = job_list_changed or self._removeJob(removed_job)
if job_list_changed: if job_list_changed:
# Override the old list with the new list (either because jobs were removed / added or order changed)
self._print_jobs = print_jobs_seen
self.printJobsChanged.emit() # Do a single emit for all print job changes. self.printJobsChanged.emit() # Do a single emit for all print job changes.
def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None: def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None:
@ -478,16 +540,59 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
def _createPrintJobModel(self, data: Dict[str, Any]) -> PrintJobOutputModel: def _createPrintJobModel(self, data: Dict[str, Any]) -> PrintJobOutputModel:
print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self), print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
key=data["uuid"], name= data["name"]) key=data["uuid"], name= data["name"])
configuration = ConfigurationModel()
extruders = [ExtruderConfigurationModel(position = idx) for idx in range(0, self._number_of_extruders)]
for index in range(0, self._number_of_extruders):
try:
extruder_data = data["configuration"][index]
except IndexError:
continue
extruder = extruders[int(data["configuration"][index]["extruder_index"])]
extruder.setHotendID(extruder_data.get("print_core_id", ""))
extruder.setMaterial(self._createMaterialOutputModel(extruder_data.get("material", {})))
configuration.setExtruderConfigurations(extruders)
print_job.updateConfiguration(configuration)
print_job.setCompatibleMachineFamilies(data.get("compatible_machine_families", []))
print_job.stateChanged.connect(self._printJobStateChanged) print_job.stateChanged.connect(self._printJobStateChanged)
self._print_jobs.append(print_job)
return print_job return print_job
def _updatePrintJob(self, print_job: PrintJobOutputModel, data: Dict[str, Any]) -> None: def _updatePrintJob(self, print_job: PrintJobOutputModel, data: Dict[str, Any]) -> None:
print_job.updateTimeTotal(data["time_total"]) print_job.updateTimeTotal(data["time_total"])
print_job.updateTimeElapsed(data["time_elapsed"]) print_job.updateTimeElapsed(data["time_elapsed"])
print_job.updateState(data["status"]) impediments_to_printing = data.get("impediments_to_printing", [])
print_job.updateOwner(data["owner"]) print_job.updateOwner(data["owner"])
status_set_by_impediment = False
for impediment in impediments_to_printing:
if impediment["severity"] == "UNFIXABLE":
status_set_by_impediment = True
print_job.updateState("error")
break
if not status_set_by_impediment:
print_job.updateState(data["status"])
def _createMaterialOutputModel(self, material_data) -> MaterialOutputModel:
containers = ContainerRegistry.getInstance().findInstanceContainers(type="material", GUID=material_data["guid"])
if containers:
color = containers[0].getMetaDataEntry("color_code")
brand = containers[0].getMetaDataEntry("brand")
material_type = containers[0].getMetaDataEntry("material")
name = containers[0].getName()
else:
Logger.log("w",
"Unable to find material with guid {guid}. Using data as provided by cluster".format(
guid=material_data["guid"]))
color = material_data["color"]
brand = material_data["brand"]
material_type = material_data["material"]
name = "Empty" if material_data["material"] == "empty" else "Unknown"
return MaterialOutputModel(guid=material_data["guid"], type=material_type,
brand=brand, color=color, name=name)
def _updatePrinter(self, printer: PrinterOutputModel, data: Dict[str, Any]) -> None: def _updatePrinter(self, printer: PrinterOutputModel, data: Dict[str, Any]) -> None:
# For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer. # For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
# Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping. # Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
@ -523,24 +628,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
material_data = extruder_data["material"] material_data = extruder_data["material"]
if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]: if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]:
containers = ContainerRegistry.getInstance().findInstanceContainers(type="material", material = self._createMaterialOutputModel(material_data)
GUID=material_data["guid"])
if containers:
color = containers[0].getMetaDataEntry("color_code")
brand = containers[0].getMetaDataEntry("brand")
material_type = containers[0].getMetaDataEntry("material")
name = containers[0].getName()
else:
Logger.log("w",
"Unable to find material with guid {guid}. Using data as provided by cluster".format(
guid=material_data["guid"]))
color = material_data["color"]
brand = material_data["brand"]
material_type = material_data["material"]
name = "Empty" if material_data["material"] == "empty" else "Unknown"
material = MaterialOutputModel(guid=material_data["guid"], type=material_type,
brand=brand, color=color, name=name)
extruder.updateActiveMaterial(material) extruder.updateActiveMaterial(material)
def _removeJob(self, job: PrintJobOutputModel) -> bool: def _removeJob(self, job: PrintJobOutputModel) -> bool:
@ -568,6 +656,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
job = SendMaterialJob(device = self) job = SendMaterialJob(device = self)
job.run() job.run()
def loadJsonFromReply(reply: QNetworkReply) -> Optional[List[Dict[str, Any]]]: def loadJsonFromReply(reply: QNetworkReply) -> Optional[List[Dict[str, Any]]]:
try: try:
result = json.loads(bytes(reply.readAll()).decode("utf-8")) result = json.loads(bytes(reply.readAll()).decode("utf-8"))
@ -586,8 +675,8 @@ def checkValidGetReply(reply: QNetworkReply) -> bool:
return True return True
def findByKey(list: List[Union[PrintJobOutputModel, PrinterOutputModel]], key: str) -> Optional[PrintJobOutputModel]: def findByKey(lst: List[Union[PrintJobOutputModel, PrinterOutputModel]], key: str) -> Optional[PrintJobOutputModel]:
for item in list: for item in lst:
if item.key == key: if item.key == key:
return item return item
return None return None

View file

@ -6,8 +6,6 @@ from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
MYPY = False MYPY = False
if MYPY: if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
class ClusterUM3PrinterOutputController(PrinterOutputController): class ClusterUM3PrinterOutputController(PrinterOutputController):
def __init__(self, output_device): def __init__(self, output_device):

View file

@ -24,7 +24,7 @@ class DiscoverUM3Action(MachineAction):
def __init__(self) -> None: def __init__(self) -> None:
super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network")) super().__init__("DiscoverUM3Action", catalog.i18nc("@action","Connect via Network"))
self._qml_url = "DiscoverUM3Action.qml" self._qml_url = "resources/qml/DiscoverUM3Action.qml"
self._network_plugin = None #type: Optional[UM3OutputDevicePlugin] self._network_plugin = None #type: Optional[UM3OutputDevicePlugin]
@ -174,7 +174,7 @@ class DiscoverUM3Action(MachineAction):
plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting") plugin_path = PluginRegistry.getInstance().getPluginPath("UM3NetworkPrinting")
if not plugin_path: if not plugin_path:
return return
path = os.path.join(plugin_path, "UM3InfoComponents.qml") path = os.path.join(plugin_path, "resources/qml/UM3InfoComponents.qml")
self.__additional_components_view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self}) self.__additional_components_view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
if not self.__additional_components_view: if not self.__additional_components_view:
Logger.log("w", "Could not create ui components for UM3.") Logger.log("w", "Could not create ui components for UM3.")

View file

@ -76,7 +76,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
self.setIconName("print") self.setIconName("print")
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml") self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/MonitorItem.qml")
self._output_controller = LegacyUM3PrinterOutputController(self) self._output_controller = LegacyUM3PrinterOutputController(self)

View file

@ -326,7 +326,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if self._firmware_name is None: if self._firmware_name is None:
self.sendCommand("M115") self.sendCommand("M115")
if (b"ok " in line and b"T:" in line) or b"ok T:" in line or line.startswith(b"T:") or b"ok B:" in line or line.startswith(b"B:"): # Temperature message. 'T:' for extruder and 'B:' for bed if (b"ok " in line and b"T:" in line) or line.startswith(b"T:") or b"ok B:" in line or line.startswith(b"B:"): # Temperature message. 'T:' for extruder and 'B:' for bed
extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line) extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line)
# Update all temperature values # Update all temperature values
matched_extruder_nrs = [] matched_extruder_nrs = []

View file

@ -60,8 +60,8 @@ _RENAMED_MATERIAL_PROFILES = {
} }
## Upgrades configurations from the state they were in at version 3.4 to the ## Upgrades configurations from the state they were in at version 3.4 to the
# state they should be in at version 4.0. # state they should be in at version 3.5.
class VersionUpgrade34to40(VersionUpgrade): class VersionUpgrade34to35(VersionUpgrade):
## Gets the version number from a CFG file in Uranium's 3.3 format. ## Gets the version number from a CFG file in Uranium's 3.3 format.
# #

View file

@ -1,9 +1,9 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from . import VersionUpgrade34to40 from . import VersionUpgrade34to35
upgrade = VersionUpgrade34to40.VersionUpgrade34to40() upgrade = VersionUpgrade34to35.VersionUpgrade34to35()
def getMetaData(): def getMetaData():

View file

@ -1,8 +1,8 @@
{ {
"name": "Version Upgrade 3.4 to 4.0", "name": "Version Upgrade 3.4 to 3.5",
"author": "Ultimaker B.V.", "author": "Ultimaker B.V.",
"version": "1.0.0", "version": "1.0.0",
"description": "Upgrades configurations from Cura 3.4 to Cura 4.0.", "description": "Upgrades configurations from Cura 3.4 to Cura 3.5.",
"api": 5, "api": 5,
"i18n-catalog": "cura" "i18n-catalog": "cura"
} }

View file

@ -4,12 +4,12 @@
import configparser #To parse the resulting config files. import configparser #To parse the resulting config files.
import pytest #To register tests with. import pytest #To register tests with.
import VersionUpgrade34to40 #The module we're testing. import VersionUpgrade34to35 #The module we're testing.
## Creates an instance of the upgrader to test with. ## Creates an instance of the upgrader to test with.
@pytest.fixture @pytest.fixture
def upgrader(): def upgrader():
return VersionUpgrade34to40.VersionUpgrade34to40() return VersionUpgrade34to35.VersionUpgrade34to35()
test_upgrade_version_nr_data = [ test_upgrade_version_nr_data = [
("Empty config file", ("Empty config file",

Some files were not shown because too many files have changed in this diff Show more