mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-10-10 15:27:53 -06:00
Merge branch 'master' into feature_accelerations_and_jerk_per_feature_settigns_rework
This commit is contained in:
commit
149af8af28
119 changed files with 3484 additions and 1784 deletions
93
cura/ContainerSettingsModel.py
Normal file
93
cura/ContainerSettingsModel.py
Normal file
|
@ -0,0 +1,93 @@
|
|||
from UM.Application import Application
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot, QUrl
|
||||
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
|
||||
class ContainerSettingsModel(ListModel):
|
||||
LabelRole = Qt.UserRole + 1
|
||||
CategoryRole = Qt.UserRole + 2
|
||||
UnitRole = Qt.UserRole + 3
|
||||
ValuesRole = Qt.UserRole + 4
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self.addRoleName(self.LabelRole, "label")
|
||||
self.addRoleName(self.CategoryRole, "category")
|
||||
self.addRoleName(self.UnitRole, "unit")
|
||||
self.addRoleName(self.ValuesRole, "values")
|
||||
|
||||
self._container_ids = []
|
||||
self._containers = []
|
||||
|
||||
def _onPropertyChanged(self, key, property_name):
|
||||
if property_name == "value":
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
self.clear()
|
||||
|
||||
if len(self._container_ids) == 0:
|
||||
return
|
||||
|
||||
keys = []
|
||||
for container in self._containers:
|
||||
keys = keys + list(container.getAllKeys())
|
||||
|
||||
keys = list(set(keys)) # remove duplicate keys
|
||||
keys.sort()
|
||||
|
||||
for key in keys:
|
||||
definition = None
|
||||
category = None
|
||||
values = []
|
||||
for container in self._containers:
|
||||
instance = container.getInstance(key)
|
||||
if instance:
|
||||
definition = instance.definition
|
||||
|
||||
# Traverse up to find the category
|
||||
category = definition
|
||||
while category.type != "category":
|
||||
category = category.parent
|
||||
|
||||
value = container.getProperty(key, "value")
|
||||
if type(value) == SettingFunction:
|
||||
values.append("=\u0192")
|
||||
else:
|
||||
values.append(container.getProperty(key, "value"))
|
||||
else:
|
||||
values.append("")
|
||||
|
||||
self.appendItem({
|
||||
"key": key,
|
||||
"values": values,
|
||||
"label": definition.label,
|
||||
"unit": definition.unit,
|
||||
"category": category.label
|
||||
})
|
||||
|
||||
## Set the ids of the containers which have the settings this model should list.
|
||||
# Also makes sure the model updates when the containers have property changes
|
||||
def setContainers(self, container_ids):
|
||||
for container in self._containers:
|
||||
container.propertyChanged.disconnect(self._onPropertyChanged)
|
||||
|
||||
self._container_ids = container_ids
|
||||
self._containers = []
|
||||
|
||||
for container_id in self._container_ids:
|
||||
containers = ContainerRegistry.getInstance().findContainers(id = container_id)
|
||||
if containers:
|
||||
containers[0].propertyChanged.connect(self._onPropertyChanged)
|
||||
self._containers.append(containers[0])
|
||||
|
||||
self._update()
|
||||
|
||||
containersChanged = pyqtSignal()
|
||||
@pyqtProperty("QVariantList", fset = setContainers, notify = containersChanged)
|
||||
def containers(self):
|
||||
return self.container_ids
|
|
@ -27,7 +27,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
# Keep track of the previous parent so we can clear its convex hull when the object is reparented
|
||||
self._parent_node = None
|
||||
|
||||
self._profile = None
|
||||
self._global_stack = None
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||
self._onGlobalStackChanged()
|
||||
#Application.getInstance().getMachineManager().activeProfileChanged.connect(self._onActiveProfileChanged)
|
||||
#Application.getInstance().getMachineManager().activeMachineInstanceChanged.connect(self._onActiveMachineInstanceChanged)
|
||||
#self._onActiveProfileChanged()
|
||||
|
@ -95,28 +97,30 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
def setConvexHullNode(self, node):
|
||||
self._convex_hull_node = node
|
||||
|
||||
def _onActiveProfileChanged(self):
|
||||
if self._profile:
|
||||
self._profile.settingValueChanged.disconnect(self._onSettingValueChanged)
|
||||
def _onSettingValueChanged(self, key, property_name):
|
||||
if key == "print_sequence" and property_name == "value":
|
||||
self._onChanged()
|
||||
|
||||
self._profile = Application.getInstance().getMachineManager().getWorkingProfile()
|
||||
|
||||
if self._profile:
|
||||
self._profile.settingValueChanged.connect(self._onSettingValueChanged)
|
||||
|
||||
def _onActiveMachineInstanceChanged(self):
|
||||
def _onChanged(self, *args):
|
||||
if self._convex_hull_job:
|
||||
self._convex_hull_job.cancel()
|
||||
self.setConvexHull(None)
|
||||
|
||||
def _onSettingValueChanged(self, setting):
|
||||
if setting == "print_sequence":
|
||||
if self._convex_hull_job:
|
||||
self._convex_hull_job.cancel()
|
||||
self.setConvexHull(None)
|
||||
|
||||
def _onParentChanged(self, node):
|
||||
# Force updating the convex hull of the parent group if the object is in a group
|
||||
if self._parent_node and self._parent_node.callDecoration("isGroup"):
|
||||
self._parent_node.callDecoration("setConvexHull", None)
|
||||
self._parent_node = self.getNode().getParent()
|
||||
|
||||
def _onGlobalStackChanged(self):
|
||||
if self._global_stack:
|
||||
self._global_stack.propertyChanged.disconnect(self._onSettingValueChanged)
|
||||
self._global_stack.containersChanged.disconnect(self._onChanged)
|
||||
|
||||
self._global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
|
||||
if self._global_stack:
|
||||
self._global_stack.propertyChanged.connect(self._onSettingValueChanged)
|
||||
self._global_stack.containersChanged.connect(self._onChanged)
|
||||
|
||||
self._onChanged()
|
||||
|
|
|
@ -18,6 +18,7 @@ from UM.JobQueue import JobQueue
|
|||
from UM.SaveFile import SaveFile
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Scene.GroupDecorator import GroupDecorator
|
||||
import UM.Settings.Validator
|
||||
|
||||
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||
|
@ -30,6 +31,8 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
|||
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
from . import ExtruderManager
|
||||
from . import ExtrudersModel
|
||||
from . import PlatformPhysics
|
||||
from . import BuildVolume
|
||||
from . import CameraAnimation
|
||||
|
@ -39,11 +42,13 @@ from . import MultiMaterialDecorator
|
|||
from . import ZOffsetDecorator
|
||||
from . import CuraSplashScreen
|
||||
from . import MachineManagerModel
|
||||
from . import ContainerSettingsModel
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
|
||||
from PyQt5.QtGui import QColor, QIcon
|
||||
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType
|
||||
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
||||
|
||||
import ast #For literal eval of extruder setting types.
|
||||
import platform
|
||||
import sys
|
||||
import os.path
|
||||
|
@ -61,9 +66,10 @@ if platform.system() == "Linux": # Needed for platform.linux_distribution, which
|
|||
ctypes.CDLL(find_library('GL'), ctypes.RTLD_GLOBAL)
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraVersion
|
||||
from cura.CuraVersion import CuraVersion, CuraBuildType
|
||||
except ImportError:
|
||||
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
|
||||
CuraBuildType = ""
|
||||
|
||||
|
||||
class CuraApplication(QtApplication):
|
||||
|
@ -87,9 +93,13 @@ class CuraApplication(QtApplication):
|
|||
self._open_file_queue = [] # Files to open when plug-ins are loaded.
|
||||
|
||||
# Need to do this before ContainerRegistry tries to load the machines
|
||||
SettingDefinition.addSupportedProperty("global_only", DefinitionPropertyType.Function, default = False)
|
||||
SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True)
|
||||
SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True)
|
||||
SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default = True)
|
||||
SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default = True)
|
||||
SettingDefinition.addSettingType("extruder", int, str, UM.Settings.Validator)
|
||||
|
||||
super().__init__(name = "cura", version = CuraVersion)
|
||||
super().__init__(name = "cura", version = CuraVersion, buildtype = CuraBuildType)
|
||||
|
||||
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
|
||||
|
||||
|
@ -116,6 +126,7 @@ class CuraApplication(QtApplication):
|
|||
self._center_after_select = False
|
||||
self._camera_animation = None
|
||||
self._cura_actions = None
|
||||
self._started = False
|
||||
|
||||
self.getController().getScene().sceneChanged.connect(self.updatePlatformActivity)
|
||||
self.getController().toolOperationStopped.connect(self._onToolOperationStopped)
|
||||
|
@ -127,17 +138,34 @@ class CuraApplication(QtApplication):
|
|||
Resources.addStorageType(self.ResourceTypes.QualityInstanceContainer, "quality")
|
||||
Resources.addStorageType(self.ResourceTypes.VariantInstanceContainer, "variants")
|
||||
Resources.addStorageType(self.ResourceTypes.MaterialInstanceContainer, "materials")
|
||||
Resources.addStorageType(self.ResourceTypes.ExtruderStack, "extruders")
|
||||
Resources.addStorageType(self.ResourceTypes.UserInstanceContainer, "user")
|
||||
Resources.addStorageType(self.ResourceTypes.ExtruderStack, "extruders")
|
||||
Resources.addStorageType(self.ResourceTypes.MachineStack, "machine_instances")
|
||||
|
||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.QualityInstanceContainer)
|
||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.VariantInstanceContainer)
|
||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MaterialInstanceContainer)
|
||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.ExtruderStack)
|
||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.UserInstanceContainer)
|
||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.ExtruderStack)
|
||||
ContainerRegistry.getInstance().addResourceType(self.ResourceTypes.MachineStack)
|
||||
|
||||
# Add empty variant, material and quality containers.
|
||||
# Since they are empty, they should never be serialized and instead just programmatically created.
|
||||
# We need them to simplify the switching between materials.
|
||||
empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
||||
empty_variant_container = copy.deepcopy(empty_container)
|
||||
empty_variant_container._id = "empty_variant"
|
||||
empty_variant_container.addMetaDataEntry("type", "variant")
|
||||
ContainerRegistry.getInstance().addContainer(empty_variant_container)
|
||||
empty_material_container = copy.deepcopy(empty_container)
|
||||
empty_material_container._id = "empty_material"
|
||||
empty_material_container.addMetaDataEntry("type", "material")
|
||||
ContainerRegistry.getInstance().addContainer(empty_material_container)
|
||||
empty_quality_container = copy.deepcopy(empty_container)
|
||||
empty_quality_container._id = "empty_quality"
|
||||
empty_quality_container.addMetaDataEntry("type", "quality")
|
||||
ContainerRegistry.getInstance().addContainer(empty_quality_container)
|
||||
|
||||
ContainerRegistry.getInstance().load()
|
||||
|
||||
Preferences.getInstance().addPreference("cura/active_mode", "simple")
|
||||
|
@ -192,7 +220,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
JobQueue.getInstance().jobFinished.connect(self._onJobFinished)
|
||||
|
||||
self.applicationShuttingDown.connect(self._onExit)
|
||||
self.applicationShuttingDown.connect(self.saveSettings)
|
||||
|
||||
self._recent_files = []
|
||||
files = Preferences.getInstance().getValue("cura/recent_files").split(";")
|
||||
|
@ -202,8 +230,13 @@ class CuraApplication(QtApplication):
|
|||
|
||||
self._recent_files.append(QUrl.fromLocalFile(f))
|
||||
|
||||
## Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
|
||||
def _onExit(self):
|
||||
## Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
|
||||
#
|
||||
# Note that the AutoSave plugin also calls this method.
|
||||
def saveSettings(self):
|
||||
if not self._started: # Do not do saving during application start
|
||||
return
|
||||
|
||||
for instance in ContainerRegistry.getInstance().findInstanceContainers():
|
||||
if not instance.isDirty():
|
||||
continue
|
||||
|
@ -245,9 +278,15 @@ class CuraApplication(QtApplication):
|
|||
continue
|
||||
|
||||
file_name = urllib.parse.quote_plus(stack.getId()) + ".stack.cfg"
|
||||
path = Resources.getStoragePath(self.ResourceTypes.MachineStack, file_name)
|
||||
with SaveFile(path, "wt", -1, "utf-8") as f:
|
||||
f.write(data)
|
||||
stack_type = stack.getMetaDataEntry("type", None)
|
||||
path = None
|
||||
if not stack_type or stack_type == "machine":
|
||||
path = Resources.getStoragePath(self.ResourceTypes.MachineStack, file_name)
|
||||
elif stack_type == "extruder":
|
||||
path = Resources.getStoragePath(self.ResourceTypes.ExtruderStack, file_name)
|
||||
if path:
|
||||
with SaveFile(path, "wt", -1, "utf-8") as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
@pyqtSlot(result = QUrl)
|
||||
|
@ -258,6 +297,7 @@ class CuraApplication(QtApplication):
|
|||
# \sa PluginRegistery
|
||||
def _loadPlugins(self):
|
||||
self._plugin_registry.addType("profile_reader", self._addProfileReader)
|
||||
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
|
||||
self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib", "cura"))
|
||||
if not hasattr(sys, "frozen"):
|
||||
self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins"))
|
||||
|
@ -320,6 +360,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
self.showSplashMessage(self._i18n_catalog.i18nc("@info:progress", "Loading interface..."))
|
||||
|
||||
ExtruderManager.ExtruderManager.getInstance() #Initialise extruder so as to listen to global container stack changes before the first global container stack is set.
|
||||
qmlRegisterSingletonType(MachineManagerModel.MachineManagerModel, "Cura", 1, 0, "MachineManager",
|
||||
MachineManagerModel.createMachineManagerModel)
|
||||
|
||||
|
@ -335,6 +376,8 @@ class CuraApplication(QtApplication):
|
|||
for file_name in self._open_file_queue: #Open all the files that were queued up while plug-ins were loading.
|
||||
self._openFile(file_name)
|
||||
|
||||
self._started = True
|
||||
|
||||
self.exec_()
|
||||
|
||||
## Handle Qt events
|
||||
|
@ -351,6 +394,9 @@ class CuraApplication(QtApplication):
|
|||
def getPrintInformation(self):
|
||||
return self._print_information
|
||||
|
||||
## Registers objects for the QML engine to use.
|
||||
#
|
||||
# \param engine The QML engine.
|
||||
def registerObjects(self, engine):
|
||||
engine.rootContext().setContextProperty("Printer", self)
|
||||
self._print_information = PrintInformation.PrintInformation()
|
||||
|
@ -360,6 +406,21 @@ class CuraApplication(QtApplication):
|
|||
|
||||
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
|
||||
|
||||
qmlRegisterType(ExtrudersModel.ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
|
||||
|
||||
qmlRegisterType(ContainerSettingsModel.ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
|
||||
|
||||
qmlRegisterSingletonType(QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")), "Cura", 1, 0, "Actions")
|
||||
|
||||
engine.rootContext().setContextProperty("ExtruderManager", ExtruderManager.ExtruderManager.getInstance())
|
||||
|
||||
for path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.QmlFiles):
|
||||
type_name = os.path.splitext(os.path.basename(path))[0]
|
||||
if type_name in ("Cura", "Actions"):
|
||||
continue
|
||||
|
||||
qmlRegisterType(QUrl.fromLocalFile(path), "Cura", 1, 0, type_name)
|
||||
|
||||
def onSelectionChanged(self):
|
||||
if Selection.hasSelection():
|
||||
if not self.getController().getActiveTool():
|
||||
|
@ -419,21 +480,6 @@ class CuraApplication(QtApplication):
|
|||
self._platform_activity = True if count > 0 else False
|
||||
self.activityChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setJobName(self, name):
|
||||
# when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its
|
||||
# extension. This cuts the extension off if necessary.
|
||||
name = os.path.splitext(name)[0]
|
||||
if self._job_name != name:
|
||||
self._job_name = name
|
||||
self.jobNameChanged.emit()
|
||||
|
||||
jobNameChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(str, notify = jobNameChanged)
|
||||
def jobName(self):
|
||||
return self._job_name
|
||||
|
||||
# Remove all selected objects from the scene.
|
||||
@pyqtSlot()
|
||||
def deleteSelection(self):
|
||||
|
@ -702,16 +748,18 @@ class CuraApplication(QtApplication):
|
|||
def _onActiveMachineChanged(self):
|
||||
pass
|
||||
|
||||
fileLoaded = pyqtSignal(str)
|
||||
|
||||
def _onFileLoaded(self, job):
|
||||
node = job.getResult()
|
||||
if node != None:
|
||||
self.setJobName(os.path.basename(job.getFileName()))
|
||||
self.fileLoaded.emit(job.getFileName())
|
||||
node.setSelectable(True)
|
||||
node.setName(os.path.basename(job.getFileName()))
|
||||
op = AddSceneNodeOperation(node, self.getController().getScene().getRoot())
|
||||
op.push()
|
||||
|
||||
self.getController().getScene().sceneChanged.emit(node) #F orce scene change.
|
||||
self.getController().getScene().sceneChanged.emit(node) #Force scene change.
|
||||
|
||||
def _onJobFinished(self, job):
|
||||
if type(job) is not ReadMeshJob or not job.getResult():
|
||||
|
@ -744,3 +792,6 @@ class CuraApplication(QtApplication):
|
|||
def _addProfileReader(self, profile_reader):
|
||||
# TODO: Add the profile reader to the list of plug-ins that can be used when importing profiles.
|
||||
pass
|
||||
|
||||
def _addProfileWriter(self, profile_writer):
|
||||
pass
|
||||
|
|
|
@ -21,6 +21,9 @@ class CuraSplashScreen(QSplashScreen):
|
|||
painter.setPen(QColor(0, 0, 0, 255))
|
||||
|
||||
version = Application.getInstance().getVersion().split("-")
|
||||
buildtype = Application.getInstance().getBuildType()
|
||||
if buildtype:
|
||||
version[0] += " (%s)" %(buildtype)
|
||||
|
||||
painter.setFont(QFont("Proxima Nova Rg", 20 ))
|
||||
painter.drawText(0, 0, 330 * self._scale, 230 * self._scale, Qt.AlignHCenter | Qt.AlignBottom, version[0])
|
||||
|
|
|
@ -2,3 +2,4 @@
|
|||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
CuraVersion = "@CURA_VERSION@"
|
||||
CuraBuildType = "@CURA_BUILDTYPE@"
|
207
cura/ExtruderManager.py
Normal file
207
cura/ExtruderManager.py
Normal file
|
@ -0,0 +1,207 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject, QVariant #For communicating data and events to Qt.
|
||||
|
||||
import UM.Application #To get the global container stack to find the current machine.
|
||||
import UM.Logger
|
||||
import UM.Settings.ContainerRegistry #Finding containers by ID.
|
||||
|
||||
|
||||
## Manages all existing extruder stacks.
|
||||
#
|
||||
# This keeps a list of extruder stacks for each machine.
|
||||
class ExtruderManager(QObject):
|
||||
## Signal to notify other components when the list of extruders changes.
|
||||
extrudersChanged = pyqtSignal(QVariant)
|
||||
|
||||
## Notify when the user switches the currently active extruder.
|
||||
activeExtruderChanged = pyqtSignal()
|
||||
|
||||
## Registers listeners and such to listen to changes to the extruders.
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self._extruder_trains = { } #Per machine, a dictionary of extruder container stack IDs.
|
||||
self._active_extruder_index = 0
|
||||
UM.Application.getInstance().globalContainerStackChanged.connect(self._addCurrentMachineExtruders)
|
||||
|
||||
## Gets the unique identifier of the currently active extruder stack.
|
||||
#
|
||||
# The currently active extruder stack is the stack that is currently being
|
||||
# edited.
|
||||
#
|
||||
# \return The unique ID of the currently active extruder stack.
|
||||
@pyqtProperty(str, notify = activeExtruderChanged)
|
||||
def activeExtruderStackId(self):
|
||||
if not UM.Application.getInstance().getGlobalContainerStack():
|
||||
return None #No active machine, so no active extruder.
|
||||
try:
|
||||
return self._extruder_trains[UM.Application.getInstance().getGlobalContainerStack().getBottom().getId()][str(self._active_extruder_index)].getId()
|
||||
except KeyError: #Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
|
||||
return None
|
||||
|
||||
## The instance of the singleton pattern.
|
||||
#
|
||||
# It's None if the extruder manager hasn't been created yet.
|
||||
__instance = None
|
||||
|
||||
## Gets an instance of the extruder manager, or creates one if no instance
|
||||
# exists yet.
|
||||
#
|
||||
# This is an implementation of singleton. If an extruder manager already
|
||||
# exists, it is re-used.
|
||||
#
|
||||
# \return The extruder manager.
|
||||
@classmethod
|
||||
def getInstance(cls):
|
||||
if not cls.__instance:
|
||||
cls.__instance = ExtruderManager()
|
||||
return cls.__instance
|
||||
|
||||
## Changes the active extruder by index.
|
||||
#
|
||||
# \param index The index of the new active extruder.
|
||||
@pyqtSlot(int)
|
||||
def setActiveExtruderIndex(self, index):
|
||||
self._active_extruder_index = index
|
||||
self.activeExtruderChanged.emit()
|
||||
|
||||
## Adds all extruders of a specific machine definition to the extruder
|
||||
# manager.
|
||||
#
|
||||
# \param machine_definition The machine to add the extruders for.
|
||||
def addMachineExtruders(self, machine_definition):
|
||||
machine_id = machine_definition.getId()
|
||||
if machine_id not in self._extruder_trains:
|
||||
self._extruder_trains[machine_id] = { }
|
||||
|
||||
container_registry = UM.Settings.ContainerRegistry.getInstance()
|
||||
if not container_registry: #Then we shouldn't have any machine definition either. In any case, there are no extruder trains then so bye bye.
|
||||
return
|
||||
|
||||
#Add the extruder trains that don't exist yet.
|
||||
for extruder_definition in container_registry.findDefinitionContainers(machine = machine_definition.getId()):
|
||||
position = extruder_definition.getMetaDataEntry("position", None)
|
||||
if not position:
|
||||
UM.Logger.log("w", "Extruder definition %s specifies no position metadata entry.", extruder_definition.getId())
|
||||
if not container_registry.findContainerStacks(machine = machine_id, position = position): #Doesn't exist yet.
|
||||
self.createExtruderTrain(extruder_definition, machine_definition, position)
|
||||
|
||||
#Gets the extruder trains that we just created as well as any that still existed.
|
||||
extruder_trains = container_registry.findContainerStacks(type = "extruder_train", machine = machine_definition.getId())
|
||||
for extruder_train in extruder_trains:
|
||||
self._extruder_trains[machine_id][extruder_train.getMetaDataEntry("position")] = extruder_train
|
||||
if extruder_trains:
|
||||
self.extrudersChanged.emit(machine_definition)
|
||||
|
||||
## Creates a container stack for an extruder train.
|
||||
#
|
||||
# The container stack has an extruder definition at the bottom, which is
|
||||
# linked to a machine definition. Then it has a nozzle profile, a material
|
||||
# profile, a quality profile and a user profile, in that order.
|
||||
#
|
||||
# The resulting container stack is added to the registry.
|
||||
#
|
||||
# \param extruder_definition The extruder to create the extruder train
|
||||
# for.
|
||||
# \param machine_definition The machine that the extruder train belongs
|
||||
# to.
|
||||
# \param position The position of this extruder train in the extruder
|
||||
# slots of the machine.
|
||||
def createExtruderTrain(self, extruder_definition, machine_definition, position):
|
||||
#Cache some things.
|
||||
container_registry = UM.Settings.ContainerRegistry.getInstance()
|
||||
machine_id = machine_definition.getId()
|
||||
|
||||
#Create a container stack for this extruder.
|
||||
extruder_stack_id = container_registry.uniqueName(extruder_definition.getId())
|
||||
container_stack = UM.Settings.ContainerStack(extruder_stack_id)
|
||||
container_stack.setName(extruder_definition.getName()) #Take over the display name to display the stack with.
|
||||
container_stack.addMetaDataEntry("type", "extruder_train")
|
||||
container_stack.addMetaDataEntry("machine", machine_definition.getId())
|
||||
container_stack.addMetaDataEntry("position", position)
|
||||
container_stack.addContainer(extruder_definition)
|
||||
|
||||
#Find the nozzle to use for this extruder.
|
||||
nozzle = container_registry.getEmptyInstanceContainer()
|
||||
if machine_definition.getMetaDataEntry("has_nozzles", default = "False") == "True":
|
||||
#First add any nozzle. Later, overwrite with preference if the preference is valid.
|
||||
nozzles = container_registry.findInstanceContainers(machine = machine_id, type = "nozzle")
|
||||
if len(nozzles) >= 1:
|
||||
nozzle = nozzles[0]
|
||||
preferred_nozzle_id = machine_definition.getMetaDataEntry("preferred_nozzle")
|
||||
if preferred_nozzle_id:
|
||||
preferred_nozzles = container_registry.findInstanceContainers(id = preferred_nozzle_id, type = "nozzle")
|
||||
if len(preferred_nozzles) >= 1:
|
||||
nozzle = preferred_nozzles[0]
|
||||
else:
|
||||
UM.Logger.log("w", "The preferred nozzle \"%s\" of machine %s doesn't exist or is not a nozzle profile.", preferred_nozzle_id, machine_id)
|
||||
#And leave it at the default nozzle.
|
||||
container_stack.addContainer(nozzle)
|
||||
|
||||
#Find a material to use for this nozzle.
|
||||
material = container_registry.getEmptyInstanceContainer()
|
||||
if machine_definition.getMetaDataEntry("has_materials", default = "False") == "True":
|
||||
#First add any material. Later, overwrite with preference if the preference is valid.
|
||||
if machine_definition.getMetaDataEntry("has_nozzle_materials", default = "False") == "True":
|
||||
materials = container_registry.findInstanceContainers(type = "material", machine = machine_id, nozzle = nozzle.getId())
|
||||
else:
|
||||
materials = container_registry.findInstanceContainers(type = "material", machine = machine_id)
|
||||
if len(materials) >= 1:
|
||||
material = materials[0]
|
||||
preferred_material_id = machine_definition.getMetaDataEntry("preferred_material")
|
||||
if preferred_material_id:
|
||||
preferred_materials = container_registry.findInstanceContainers(id = preferred_material_id, type = "material")
|
||||
if len(preferred_materials) >= 1:
|
||||
material = preferred_materials[0]
|
||||
else:
|
||||
UM.Logger.log("w", "The preferred material \"%s\" of machine %s doesn't exist or is not a material profile.", preferred_material_id, machine_id)
|
||||
#And leave it at the default material.
|
||||
container_stack.addContainer(material)
|
||||
|
||||
#Find a quality to use for this extruder.
|
||||
quality = container_registry.getEmptyInstanceContainer()
|
||||
if machine_definition.getMetaDataEntry("has_machine_quality"):
|
||||
#First add any quality. Later, overwrite with preference if the preference is valid.
|
||||
qualities = container_registry.findInstanceContainers(type = "quality")
|
||||
if len(qualities) >= 1:
|
||||
quality = qualities[0]
|
||||
preferred_quality_id = machine_definition.getMetaDataEntry("preferred_quality")
|
||||
if preferred_quality_id:
|
||||
preferred_quality = container_registry.findInstanceContainers(id = preferred_quality_id.lower(), type = "quality")
|
||||
if len(preferred_quality) >= 1:
|
||||
quality = preferred_quality[0]
|
||||
else:
|
||||
UM.Logger.log("w", "The preferred quality \"%s\" of machine %s doesn't exist or is not a quality profile.", preferred_quality_id, machine_id)
|
||||
#And leave it at the default quality.
|
||||
container_stack.addContainer(quality)
|
||||
|
||||
user_profile = container_registry.findInstanceContainers(id = extruder_stack_id + "_current_settings")
|
||||
if user_profile: #There was already a user profile, loaded from settings.
|
||||
user_profile = user_profile[0]
|
||||
else:
|
||||
user_profile = UM.Settings.InstanceContainer(extruder_stack_id + "_current_settings") #Add an empty user profile.
|
||||
user_profile.addMetaDataEntry("type", "user")
|
||||
user_profile.setDefinition(machine_definition)
|
||||
container_registry.addContainer(user_profile)
|
||||
container_stack.addContainer(user_profile)
|
||||
|
||||
container_stack.setNextStack(UM.Application.getInstance().getGlobalContainerStack())
|
||||
|
||||
container_registry.addContainer(container_stack)
|
||||
|
||||
## Generates extruders for a specific machine.
|
||||
#
|
||||
# \param machine_id The machine to get the extruders of.
|
||||
def getMachineExtruders(self, machine_id):
|
||||
if machine_id not in self._extruder_trains:
|
||||
UM.Logger.log("w", "Tried to get the extruder trains for machine %s, which doesn't exist.", machine_id)
|
||||
return
|
||||
for name in self._extruder_trains[machine_id]:
|
||||
yield self._extruder_trains[machine_id][name]
|
||||
|
||||
## Adds the extruders of the currently active machine.
|
||||
def _addCurrentMachineExtruders(self):
|
||||
global_stack = UM.Application.getInstance().getGlobalContainerStack()
|
||||
if global_stack and global_stack.getBottom():
|
||||
self.addMachineExtruders(global_stack.getBottom())
|
103
cura/ExtrudersModel.py
Normal file
103
cura/ExtrudersModel.py
Normal file
|
@ -0,0 +1,103 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
||||
|
||||
import cura.ExtruderManager
|
||||
import UM.Qt.ListModel
|
||||
|
||||
## Model that holds extruders.
|
||||
#
|
||||
# This model is designed for use by any list of extruders, but specifically
|
||||
# intended for drop-down lists of the current machine's extruders in place of
|
||||
# settings.
|
||||
class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
# The ID of the container stack for the extruder.
|
||||
IdRole = Qt.UserRole + 1
|
||||
|
||||
## Human-readable name of the extruder.
|
||||
NameRole = Qt.UserRole + 2
|
||||
|
||||
## Colour of the material loaded in the extruder.
|
||||
ColourRole = Qt.UserRole + 3
|
||||
|
||||
## Index of the extruder, which is also the value of the setting itself.
|
||||
#
|
||||
# An index of 0 indicates the first extruder, an index of 1 the second
|
||||
# one, and so on. This is the value that will be saved in instance
|
||||
# containers.
|
||||
IndexRole = Qt.UserRole + 4
|
||||
|
||||
## Colour to display if there is no material or the material has no known
|
||||
# colour.
|
||||
defaultColour = "#FFFF00"
|
||||
|
||||
## Initialises the extruders model, defining the roles and listening for
|
||||
# changes in the data.
|
||||
#
|
||||
# \param parent Parent QtObject of this list.
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.addRoleName(self.IdRole, "id")
|
||||
self.addRoleName(self.NameRole, "name")
|
||||
self.addRoleName(self.ColourRole, "colour")
|
||||
self.addRoleName(self.IndexRole, "index")
|
||||
|
||||
self._add_global = False
|
||||
|
||||
#Listen to changes.
|
||||
manager = cura.ExtruderManager.ExtruderManager.getInstance()
|
||||
manager.extrudersChanged.connect(self._updateExtruders) #When the list of extruders changes in general.
|
||||
UM.Application.globalContainerStackChanged.connect(self._updateExtruders) #When the current machine changes.
|
||||
self._updateExtruders()
|
||||
|
||||
def setAddGlobal(self, add):
|
||||
if add != self._add_global:
|
||||
self._add_global = add
|
||||
self._updateExtruders()
|
||||
self.addGlobalChanged.emit()
|
||||
|
||||
addGlobalChanged = pyqtSignal()
|
||||
@pyqtProperty(bool, fset = setAddGlobal, notify = addGlobalChanged)
|
||||
def addGlobal(self):
|
||||
return self._add_global
|
||||
|
||||
## Update the list of extruders.
|
||||
#
|
||||
# This should be called whenever the list of extruders changes.
|
||||
def _updateExtruders(self):
|
||||
self.clear()
|
||||
manager = cura.ExtruderManager.ExtruderManager.getInstance()
|
||||
global_container_stack = UM.Application.getInstance().getGlobalContainerStack()
|
||||
if not global_container_stack:
|
||||
return #There is no machine to get the extruders of.
|
||||
|
||||
if self._add_global:
|
||||
material = global_container_stack.findContainer({ "type": "material" })
|
||||
colour = material.getMetaDataEntry("color_code", default = self.defaultColour) if material else self.defaultColour
|
||||
item = {
|
||||
"id": global_container_stack.getId(),
|
||||
"name": "Global",
|
||||
"colour": colour,
|
||||
"index": -1
|
||||
}
|
||||
self.appendItem(item)
|
||||
|
||||
for extruder in manager.getMachineExtruders(global_container_stack.getBottom().getId()):
|
||||
material = extruder.findContainer({ "type": "material" })
|
||||
colour = material.getMetaDataEntry("color_code", default = self.defaultColour) if material else self.defaultColour
|
||||
position = extruder.getBottom().getMetaDataEntry("position", default = "0") #Position in the definition.
|
||||
try:
|
||||
position = int(position)
|
||||
except ValueError: #Not a proper int.
|
||||
position = -1
|
||||
item = { #Construct an item with only the relevant information.
|
||||
"id": extruder.getId(),
|
||||
"name": extruder.getName(),
|
||||
"colour": colour,
|
||||
"index": position
|
||||
}
|
||||
self.appendItem(item)
|
||||
|
||||
self.sort(lambda item: item["index"])
|
|
@ -1,11 +1,18 @@
|
|||
|
||||
import re
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
from UM.Application import Application
|
||||
from UM.Preferences import Preferences
|
||||
|
||||
import UM.Settings
|
||||
from UM.Settings.Validator import ValidatorState
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from . import ExtruderManager
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
import re
|
||||
|
||||
class MachineManagerModel(QObject):
|
||||
def __init__(self, parent = None):
|
||||
|
@ -13,6 +20,7 @@ class MachineManagerModel(QObject):
|
|||
|
||||
self._global_container_stack = None
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._global_stack_valid = None
|
||||
self._onGlobalContainerChanged()
|
||||
|
||||
## When the global container is changed, active material probably needs to be updated.
|
||||
|
@ -20,6 +28,10 @@ class MachineManagerModel(QObject):
|
|||
self.globalContainerChanged.connect(self.activeVariantChanged)
|
||||
self.globalContainerChanged.connect(self.activeQualityChanged)
|
||||
|
||||
self._empty_variant_container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_variant")[0]
|
||||
self._empty_material_container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_material")[0]
|
||||
self._empty_quality_container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = "empty_quality")[0]
|
||||
|
||||
Preferences.getInstance().addPreference("cura/active_machine", "")
|
||||
|
||||
active_machine_id = Preferences.getInstance().getValue("cura/active_machine")
|
||||
|
@ -29,15 +41,42 @@ class MachineManagerModel(QObject):
|
|||
self.setActiveMachine(active_machine_id)
|
||||
pass
|
||||
|
||||
|
||||
globalContainerChanged = pyqtSignal()
|
||||
activeMaterialChanged = pyqtSignal()
|
||||
activeVariantChanged = pyqtSignal()
|
||||
activeQualityChanged = pyqtSignal()
|
||||
|
||||
globalValueChanged = pyqtSignal() # Emitted whenever a value inside global container is changed.
|
||||
globalValidationChanged = pyqtSignal() # Emitted whenever a validation inside global container is changed.
|
||||
|
||||
@pyqtProperty("QVariantMap", notify = globalContainerChanged)
|
||||
def extrudersIds(self):
|
||||
## Find all extruders that reference the new stack
|
||||
extruders = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(**{"machine": self._global_container_stack.getId()})
|
||||
result = {}
|
||||
for extruder in extruders:
|
||||
result[extruder.getMetaDataEntry("position")] = extruder.getId()
|
||||
return result
|
||||
|
||||
def _onGlobalPropertyChanged(self, key, property_name):
|
||||
if property_name == "value":
|
||||
self.globalValueChanged.emit()
|
||||
if property_name == "validationState":
|
||||
if self._global_stack_valid:
|
||||
changed_validation_state = self._global_container_stack.getProperty(key, property_name)
|
||||
if changed_validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
|
||||
self._global_stack_valid = False
|
||||
self.globalValidationChanged.emit()
|
||||
else:
|
||||
new_validation_state = self._checkStackForErrors(self._global_container_stack)
|
||||
if new_validation_state:
|
||||
self._global_stack_valid = True
|
||||
self.globalValidationChanged.emit()
|
||||
|
||||
def _onGlobalContainerChanged(self):
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.containersChanged.disconnect(self._onInstanceContainersChanged)
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onGlobalPropertyChanged)
|
||||
|
||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
self.globalContainerChanged.emit()
|
||||
|
@ -45,6 +84,8 @@ class MachineManagerModel(QObject):
|
|||
if self._global_container_stack:
|
||||
Preferences.getInstance().setValue("cura/active_machine", self._global_container_stack.getId())
|
||||
self._global_container_stack.containersChanged.connect(self._onInstanceContainersChanged)
|
||||
self._global_container_stack.propertyChanged.connect(self._onGlobalPropertyChanged)
|
||||
self._global_stack_valid = not self._checkStackForErrors(self._global_container_stack)
|
||||
|
||||
def _onInstanceContainersChanged(self, container):
|
||||
container_type = container.getMetaDataEntry("type")
|
||||
|
@ -62,55 +103,18 @@ class MachineManagerModel(QObject):
|
|||
Application.getInstance().setGlobalContainerStack(containers[0])
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def addMachine(self,name, definition_id):
|
||||
definitions = UM.Settings.ContainerRegistry.getInstance().findDefinitionContainers(id=definition_id)
|
||||
def addMachine(self, name, definition_id):
|
||||
definitions = UM.Settings.ContainerRegistry.getInstance().findDefinitionContainers(id = definition_id)
|
||||
if definitions:
|
||||
definition = definitions[0]
|
||||
name = self._uniqueMachineName(name, definition.getName())
|
||||
|
||||
name = self._createUniqueName("machine", "", name, definition.getName())
|
||||
new_global_stack = UM.Settings.ContainerStack(name)
|
||||
new_global_stack.addMetaDataEntry("type", "machine")
|
||||
UM.Settings.ContainerRegistry.getInstance().addContainer(new_global_stack)
|
||||
|
||||
empty_container = UM.Settings.ContainerRegistry.getInstance().getEmptyInstanceContainer()
|
||||
|
||||
variants = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = definition.id)
|
||||
if variants:
|
||||
new_global_stack.addMetaDataEntry("has_variants", True)
|
||||
|
||||
preferred_variant_id = definitions[0].getMetaDataEntry("preferred_variant")
|
||||
variant_instance_container = empty_container
|
||||
if preferred_variant_id:
|
||||
preferred_variant_id = preferred_variant_id.lower()
|
||||
container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = preferred_variant_id)
|
||||
if container:
|
||||
variant_instance_container = container[0]
|
||||
|
||||
if variants and variant_instance_container == empty_container:
|
||||
variant_instance_container = variants[0]
|
||||
|
||||
materials = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "material", definition = definition.id)
|
||||
if materials:
|
||||
new_global_stack.addMetaDataEntry("has_materials", True)
|
||||
|
||||
preferred_material_id = definitions[0].getMetaDataEntry("preferred_material")
|
||||
material_instance_container = empty_container
|
||||
if preferred_material_id:
|
||||
preferred_material_id = preferred_material_id.lower()
|
||||
container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = preferred_material_id)
|
||||
if container:
|
||||
material_instance_container = container[0]
|
||||
|
||||
if materials and material_instance_container == empty_container:
|
||||
material_instance_container = materials[0]
|
||||
|
||||
preferred_quality_id = definitions[0].getMetaDataEntry("preferred_quality")
|
||||
quality_instance_container = empty_container
|
||||
if preferred_quality_id:
|
||||
preferred_quality_id = preferred_quality_id.lower()
|
||||
container = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = preferred_quality_id)
|
||||
if container:
|
||||
quality_instance_container = container[0]
|
||||
variant_instance_container = self._updateVariantContainer(definition)
|
||||
material_instance_container = self._updateMaterialContainer(definition, variant_instance_container)
|
||||
quality_instance_container = self._updateQualityContainer(definition, material_instance_container)
|
||||
|
||||
current_settings_instance_container = UM.Settings.InstanceContainer(name + "_current_settings")
|
||||
current_settings_instance_container.addMetaDataEntry("machine", name)
|
||||
|
@ -119,7 +123,7 @@ class MachineManagerModel(QObject):
|
|||
UM.Settings.ContainerRegistry.getInstance().addContainer(current_settings_instance_container)
|
||||
|
||||
# If a definition is found, its a list. Should only have one item.
|
||||
new_global_stack.addContainer(definitions[0])
|
||||
new_global_stack.addContainer(definition)
|
||||
if variant_instance_container:
|
||||
new_global_stack.addContainer(variant_instance_container)
|
||||
if material_instance_container:
|
||||
|
@ -128,27 +132,85 @@ class MachineManagerModel(QObject):
|
|||
new_global_stack.addContainer(quality_instance_container)
|
||||
new_global_stack.addContainer(current_settings_instance_container)
|
||||
|
||||
ExtruderManager.ExtruderManager.getInstance().addMachineExtruders(definition)
|
||||
|
||||
Application.getInstance().setGlobalContainerStack(new_global_stack)
|
||||
|
||||
# Create a name that is not empty and unique
|
||||
def _uniqueMachineName(self, name, fallback_name):
|
||||
name = name.strip()
|
||||
num_check = re.compile("(.*?)\s*#\d$").match(name)
|
||||
if(num_check):
|
||||
name = num_check.group(1)
|
||||
if name == "":
|
||||
name = fallback_name
|
||||
unique_name = name
|
||||
i = 1
|
||||
## Create a name that is not empty and unique
|
||||
# \param container_type \type{string} Type of the container (machine, quality, ...)
|
||||
# \param current_name \type{} Current name of the container, which may be an acceptable option
|
||||
# \param new_name \type{string} Base name, which may not be unique
|
||||
# \param fallback_name \type{string} Name to use when (stripped) new_name is empty
|
||||
# \return \type{string} Name that is unique for the specified type and name/id
|
||||
def _createUniqueName(self, container_type, current_name, new_name, fallback_name):
|
||||
new_name = new_name.strip()
|
||||
num_check = re.compile("(.*?)\s*#\d+$").match(new_name)
|
||||
if num_check:
|
||||
new_name = num_check.group(1)
|
||||
if new_name == "":
|
||||
new_name = fallback_name
|
||||
|
||||
#Check both the id and the name, because they may not be the same and it is better if they are both unique
|
||||
while UM.Settings.ContainerRegistry.getInstance().findContainers(None, id = unique_name) or \
|
||||
UM.Settings.ContainerRegistry.getInstance().findContainers(None, name = unique_name):
|
||||
i = i + 1
|
||||
unique_name = "%s #%d" % (name, i)
|
||||
unique_name = new_name
|
||||
i = 1
|
||||
# In case we are renaming, the current name of the container is also a valid end-result
|
||||
while self._containerExists(container_type, unique_name) and unique_name != current_name:
|
||||
i += 1
|
||||
unique_name = "%s #%d" % (new_name, i)
|
||||
|
||||
return unique_name
|
||||
|
||||
## Check if a container with of a certain type and a certain name or id exists
|
||||
# Both the id and the name are checked, because they may not be the same and it is better if they are both unique
|
||||
# \param container_type \type{string} Type of the container (machine, quality, ...)
|
||||
# \param container_name \type{string} Name to check
|
||||
def _containerExists(self, container_type, container_name):
|
||||
container_class = ContainerStack if container_type == "machine" else InstanceContainer
|
||||
|
||||
return UM.Settings.ContainerRegistry.getInstance().findContainers(container_class, id = container_name, type = container_type) or \
|
||||
UM.Settings.ContainerRegistry.getInstance().findContainers(container_class, name = container_name, type = container_type)
|
||||
|
||||
## Convenience function to check if a stack has errors.
|
||||
def _checkStackForErrors(self, stack):
|
||||
if stack is None:
|
||||
return False
|
||||
|
||||
for key in stack.getAllKeys():
|
||||
validation_state = stack.getProperty(key, "validationState")
|
||||
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
|
||||
return True
|
||||
return False
|
||||
|
||||
## Remove all instances from the top instanceContainer (effectively removing all user-changed settings)
|
||||
@pyqtSlot()
|
||||
def clearUserSettings(self):
|
||||
if not self._global_container_stack:
|
||||
return
|
||||
user_settings = self._global_container_stack.getTop()
|
||||
user_settings.clear()
|
||||
|
||||
## Check if the global_container has instances in the user container
|
||||
@pyqtProperty(bool, notify = globalValueChanged)
|
||||
def hasUserSettings(self):
|
||||
if not self._global_container_stack:
|
||||
return
|
||||
|
||||
user_settings = self._global_container_stack.getTop().findInstances(**{})
|
||||
return len(user_settings) != 0
|
||||
|
||||
## Check if the global profile does not contain error states
|
||||
# Note that the _global_stack_valid is cached due to performance issues
|
||||
# Calling _checkStackForErrors on every change is simply too expensive
|
||||
@pyqtProperty(bool, notify = globalValidationChanged)
|
||||
def isGlobalStackValid(self):
|
||||
return self._global_stack_valid
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
def activeUserProfileId(self):
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.getTop().getId()
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
def activeMachineName(self):
|
||||
if self._global_container_stack:
|
||||
|
@ -197,6 +259,101 @@ class MachineManagerModel(QObject):
|
|||
return quality.getId()
|
||||
return ""
|
||||
|
||||
## Check if a container is read_only
|
||||
@pyqtSlot(str, result = bool)
|
||||
def isReadOnly(self, container_id):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id=container_id)
|
||||
if not containers or not self._global_container_stack:
|
||||
return True
|
||||
return containers[0].isReadOnly()
|
||||
|
||||
@pyqtSlot(result = str)
|
||||
def newQualityContainerFromQualityAndUser(self):
|
||||
new_container_id = self.duplicateContainer(self.activeQualityId)
|
||||
if new_container_id == "":
|
||||
return
|
||||
self.setActiveQuality(new_container_id)
|
||||
self.updateQualityContainerFromUserContainer()
|
||||
|
||||
|
||||
@pyqtSlot(str, result=str)
|
||||
def duplicateContainer(self, container_id):
|
||||
if not self._global_container_stack:
|
||||
return ""
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id=container_id)
|
||||
if containers:
|
||||
new_name = self._createUniqueName("quality", "", containers[0].getName(), catalog.i18nc("@label", "Custom profile"))
|
||||
|
||||
new_container = InstanceContainer("")
|
||||
|
||||
## Copy all values
|
||||
new_container.deserialize(containers[0].serialize())
|
||||
|
||||
new_container.setReadOnly(False)
|
||||
new_container.setName(new_name)
|
||||
new_container._id = new_name
|
||||
UM.Settings.ContainerRegistry.getInstance().addContainer(new_container)
|
||||
return new_name
|
||||
|
||||
return ""
|
||||
|
||||
@pyqtSlot(str, str)
|
||||
def renameQualityContainer(self, container_id, new_name):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id, type = "quality")
|
||||
if containers:
|
||||
new_name = self._createUniqueName("quality", containers[0].getName(), new_name,
|
||||
catalog.i18nc("@label", "Custom profile"))
|
||||
|
||||
# As we also want the id of the container to be changed (so that profile name is the name of the file
|
||||
# on disk. We need to create a new instance and remove it (so the old file of the container is removed)
|
||||
# If we don't do that, we might get duplicates & other weird issues.
|
||||
new_container = InstanceContainer("")
|
||||
new_container.deserialize(containers[0].serialize())
|
||||
|
||||
# Actually set the name
|
||||
new_container.setName(new_name)
|
||||
new_container._id = new_name # Todo: Fix proper id change function for this.
|
||||
|
||||
# Add the "new" container.
|
||||
UM.Settings.ContainerRegistry.getInstance().addContainer(new_container)
|
||||
|
||||
# Ensure that the renamed profile is saved -before- we remove the old profile.
|
||||
Application.getInstance().saveSettings()
|
||||
|
||||
# Actually set & remove new / old quality.
|
||||
self.setActiveQuality(new_name)
|
||||
self.removeQualityContainer(containers[0].getId())
|
||||
|
||||
@pyqtSlot(str)
|
||||
def removeQualityContainer(self, container_id):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
|
||||
if not containers or not self._global_container_stack:
|
||||
return
|
||||
|
||||
# If the container that is being removed is the currently active container, set another machine as the active container
|
||||
activate_new_container = container_id == self.activeQualityId
|
||||
|
||||
UM.Settings.ContainerRegistry.getInstance().removeContainer(container_id)
|
||||
|
||||
if activate_new_container:
|
||||
definition_id = "fdmprinter" if not self.filterQualityByMachine else self.activeDefinitionId
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "quality", definition = definition_id)
|
||||
if containers:
|
||||
self.setActiveQuality(containers[0].getId())
|
||||
self.activeQualityChanged.emit()
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def updateQualityContainerFromUserContainer(self):
|
||||
if not self._global_container_stack:
|
||||
return
|
||||
user_settings = self._global_container_stack.getTop()
|
||||
quality = self._global_container_stack.findContainer({"type": "quality"})
|
||||
for key in user_settings.getAllKeys():
|
||||
quality.setProperty(key, "value", user_settings.getProperty(key, "value"))
|
||||
self.clearUserSettings() # As all users settings are noq a quality, remove them.
|
||||
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActiveMaterial(self, material_id):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id=material_id)
|
||||
|
@ -208,6 +365,8 @@ class MachineManagerModel(QObject):
|
|||
material_index = self._global_container_stack.getContainerIndex(old_material)
|
||||
self._global_container_stack.replaceContainer(material_index, containers[0])
|
||||
|
||||
self.setActiveQuality(self._updateQualityContainer(self._global_container_stack.getBottom(), containers[0]).id)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActiveVariant(self, variant_id):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id=variant_id)
|
||||
|
@ -219,6 +378,8 @@ class MachineManagerModel(QObject):
|
|||
variant_index = self._global_container_stack.getContainerIndex(old_variant)
|
||||
self._global_container_stack.replaceContainer(variant_index, containers[0])
|
||||
|
||||
self.setActiveMaterial(self._updateMaterialContainer(self._global_container_stack.getBottom(), containers[0]).id)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActiveQuality(self, quality_id):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = quality_id)
|
||||
|
@ -261,31 +422,116 @@ class MachineManagerModel(QObject):
|
|||
def renameMachine(self, machine_id, new_name):
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(id = machine_id)
|
||||
if containers:
|
||||
new_name = self._uniqueMachineName(new_name, containers[0].getBottom().getName())
|
||||
new_name = self._createUniqueName("machine", containers[0].getName(), new_name, containers[0].getBottom().getName())
|
||||
containers[0].setName(new_name)
|
||||
self.globalContainerChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def removeMachine(self, machine_id):
|
||||
# If the machine that is being removed is the currently active machine, set another machine as the active machine
|
||||
if self._global_container_stack and self._global_container_stack.getId() == machine_id:
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findContainerStacks()
|
||||
if containers:
|
||||
Application.getInstance().setGlobalContainerStack(containers[0])
|
||||
activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id)
|
||||
|
||||
current_settings_id = machine_id + "_current_settings"
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(id = current_settings_id)
|
||||
for container in containers:
|
||||
UM.Settings.ContainerRegistry.getInstance().removeContainer(container.getId())
|
||||
UM.Settings.ContainerRegistry.getInstance().removeContainer(machine_id)
|
||||
|
||||
if activate_new_machine:
|
||||
stacks = UM.Settings.ContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
||||
if stacks:
|
||||
Application.getInstance().setGlobalContainerStack(stacks[0])
|
||||
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def hasMaterials(self):
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.getMetaDataEntry("has_materials", False)
|
||||
return bool(self._global_container_stack.getMetaDataEntry("has_materials", False))
|
||||
|
||||
return False
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def hasVariants(self):
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.getMetaDataEntry("has_variants", False)
|
||||
return bool(self._global_container_stack.getMetaDataEntry("has_variants", False))
|
||||
|
||||
return False
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def filterMaterialsByMachine(self):
|
||||
if self._global_container_stack:
|
||||
return bool(self._global_container_stack.getMetaDataEntry("has_machine_materials", False))
|
||||
|
||||
return False
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def filterQualityByMachine(self):
|
||||
if self._global_container_stack:
|
||||
return bool(self._global_container_stack.getMetaDataEntry("has_machine_quality", False))
|
||||
|
||||
return False
|
||||
|
||||
def _updateVariantContainer(self, definition):
|
||||
if not definition.getMetaDataEntry("has_variants"):
|
||||
return self._empty_variant_container
|
||||
|
||||
containers = []
|
||||
preferred_variant = definition.getMetaDataEntry("preferred_variant")
|
||||
if preferred_variant:
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = definition.id, id = preferred_variant)
|
||||
|
||||
if not containers:
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(type = "variant", definition = definition.id)
|
||||
|
||||
if containers:
|
||||
return containers[0]
|
||||
|
||||
return self._empty_variant_container
|
||||
|
||||
def _updateMaterialContainer(self, definition, variant_container = None):
|
||||
if not definition.getMetaDataEntry("has_materials"):
|
||||
return self._empty_material_container
|
||||
|
||||
search_criteria = { "type": "material" }
|
||||
|
||||
if definition.getMetaDataEntry("has_machine_materials"):
|
||||
search_criteria["definition"] = definition.id
|
||||
|
||||
if definition.getMetaDataEntry("has_variants") and variant_container:
|
||||
search_criteria["variant"] = variant_container.id
|
||||
else:
|
||||
search_criteria["definition"] = "fdmprinter"
|
||||
|
||||
preferred_material = definition.getMetaDataEntry("preferred_material")
|
||||
if preferred_material:
|
||||
search_criteria["id"] = preferred_material
|
||||
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
return containers[0]
|
||||
|
||||
return self._empty_material_container
|
||||
|
||||
def _updateQualityContainer(self, definition, material_container = None):
|
||||
search_criteria = { "type": "quality" }
|
||||
|
||||
if definition.getMetaDataEntry("has_machine_quality"):
|
||||
search_criteria["definition"] = definition.id
|
||||
|
||||
if definition.getMetaDataEntry("has_materials") and material_container:
|
||||
search_criteria["material"] = material_container.id
|
||||
else:
|
||||
search_criteria["definition"] = "fdmprinter"
|
||||
|
||||
preferred_quality = definition.getMetaDataEntry("preferred_quality")
|
||||
if preferred_quality:
|
||||
search_criteria["id"] = preferred_quality
|
||||
|
||||
containers = UM.Settings.ContainerRegistry.getInstance().findInstanceContainers(**search_criteria)
|
||||
if containers:
|
||||
return containers[0]
|
||||
|
||||
return self._empty_quality_container
|
||||
|
||||
def createMachineManagerModel(engine, script_engine):
|
||||
return MachineManagerModel()
|
||||
|
|
|
@ -1,14 +1,17 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Qt.Duration import Duration
|
||||
from UM.Preferences import Preferences
|
||||
|
||||
import math
|
||||
import os.path
|
||||
import unicodedata
|
||||
|
||||
## A class for processing and calculating minimum, current and maximum print time.
|
||||
## A class for processing and calculating minimum, current and maximum print time as well as managing the job name
|
||||
#
|
||||
# This class contains all the logic relating to calculation and slicing for the
|
||||
# time/quality slider concept. It is a rather tricky combination of event handling
|
||||
|
@ -22,6 +25,8 @@ import math
|
|||
# - When that is done, we update the minimum print time and start the final slice pass, the "high quality settings pass".
|
||||
# - When the high quality pass is done, we update the maximum print time.
|
||||
#
|
||||
# This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
|
||||
# This job name is requested by the JobSpecs qml file.
|
||||
class PrintInformation(QObject):
|
||||
class SlicePass:
|
||||
CurrentSettings = 1
|
||||
|
@ -45,14 +50,20 @@ class PrintInformation(QObject):
|
|||
if self._backend:
|
||||
self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
|
||||
|
||||
self._job_name = ""
|
||||
self._abbr_machine = ""
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._setAbbreviatedMachineName)
|
||||
Application.getInstance().fileLoaded.connect(self.setJobName)
|
||||
|
||||
currentPrintTimeChanged = pyqtSignal()
|
||||
|
||||
|
||||
@pyqtProperty(Duration, notify = currentPrintTimeChanged)
|
||||
def currentPrintTime(self):
|
||||
return self._current_print_time
|
||||
|
||||
materialAmountChanged = pyqtSignal()
|
||||
|
||||
|
||||
@pyqtProperty(float, notify = materialAmountChanged)
|
||||
def materialAmount(self):
|
||||
return self._material_amount
|
||||
|
@ -66,3 +77,46 @@ class PrintInformation(QObject):
|
|||
r = Application.getInstance().getGlobalContainerStack().getProperty("material_diameter", "value") / 2
|
||||
self._material_amount = round((amount / (math.pi * r ** 2)) / 1000, 2)
|
||||
self.materialAmountChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setJobName(self, name):
|
||||
# when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its
|
||||
# extension. This cuts the extension off if necessary.
|
||||
name = os.path.splitext(name)[0]
|
||||
if self._job_name != name:
|
||||
self._job_name = name
|
||||
self.jobNameChanged.emit()
|
||||
|
||||
jobNameChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(str, notify = jobNameChanged)
|
||||
def jobName(self):
|
||||
return self._job_name
|
||||
|
||||
@pyqtSlot(str, result = str)
|
||||
def createJobName(self, base_name):
|
||||
base_name = self._stripAccents(base_name)
|
||||
if Preferences.getInstance().getValue("cura/jobname_prefix"):
|
||||
return self._abbr_machine + "_" + base_name
|
||||
else:
|
||||
return base_name
|
||||
|
||||
## Created an acronymn-like abbreviated machine name from the currently active machine name
|
||||
# Called each time the global stack is switched
|
||||
def _setAbbreviatedMachineName(self):
|
||||
global_stack_name = Application.getInstance().getGlobalContainerStack().getName()
|
||||
split_name = global_stack_name.split(" ")
|
||||
abbr_machine = ""
|
||||
for word in split_name:
|
||||
if word.lower() == "ultimaker":
|
||||
abbr_machine += "UM"
|
||||
elif word.isdigit():
|
||||
abbr_machine += word
|
||||
else:
|
||||
abbr_machine += self._stripAccents(word.strip("()[]{}#").upper())[0]
|
||||
|
||||
self._abbr_machine = abbr_machine
|
||||
|
||||
## Utility method that strips accents from characters (eg: â -> a)
|
||||
def _stripAccents(self, str):
|
||||
return ''.join(char for char in unicodedata.normalize('NFD', str) if unicodedata.category(char) != 'Mn')
|
25
cura/ProfileWriter.py
Normal file
25
cura/ProfileWriter.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from UM.PluginObject import PluginObject
|
||||
|
||||
## Base class for profile writer plugins.
|
||||
#
|
||||
# This class defines a write() function to write profiles to files with.
|
||||
class ProfileWriter(PluginObject):
|
||||
## Initialises the profile writer.
|
||||
#
|
||||
# This currently doesn't do anything since the writer is basically static.
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
## Writes a profile to the specified file path.
|
||||
#
|
||||
# The profile writer may write its own file format to the specified file.
|
||||
#
|
||||
# \param path \type{string} The file to output to.
|
||||
# \param profile \type{Profile} The profile to write to the file.
|
||||
# \return \code True \endcode if the writing was successful, or \code
|
||||
# False \endcode if it wasn't.
|
||||
def write(self, path, node):
|
||||
raise NotImplementedError("Profile writer plugin was not correctly implemented. No write was specified.")
|
|
@ -1,31 +1,78 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the AGPLv3 or higher.
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
|
||||
import copy
|
||||
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
from UM.Signal import Signal, signalemitter
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
import UM.Logger
|
||||
|
||||
from UM.Application import Application
|
||||
|
||||
## A decorator that adds a container stack to a Node. This stack should be queried for all settings regarding
|
||||
# the linked node. The Stack in question will refer to the global stack (so that settings that are not defined by
|
||||
# this stack still resolve.
|
||||
@signalemitter
|
||||
class SettingOverrideDecorator(SceneNodeDecorator):
|
||||
## Event indicating that the user selected a different extruder.
|
||||
activeExtruderChanged = Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._stack = ContainerStack(stack_id = id(self))
|
||||
self._stack.setDirty(False) # This stack does not need to be saved.
|
||||
self._instance = InstanceContainer(container_id = "SettingOverrideInstanceContainer")
|
||||
self._stack.addContainer(self._instance)
|
||||
self._extruder_stack = None #Stack upon which our stack is based.
|
||||
|
||||
self._stack.propertyChanged.connect(self._onSettingChanged)
|
||||
|
||||
ContainerRegistry.getInstance().addContainer(self._stack)
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged)
|
||||
self._onGlobalContainerStackChanged()
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._updateNextStack)
|
||||
self.activeExtruderChanged.connect(self._updateNextStack)
|
||||
self._updateNextStack()
|
||||
|
||||
def _onGlobalContainerStackChanged(self):
|
||||
## Ensure that the next stack is always the global stack.
|
||||
self._stack.setNextStack(Application.getInstance().getGlobalContainerStack())
|
||||
def __deepcopy__(self, memo):
|
||||
## Create a fresh decorator object
|
||||
deep_copy = SettingOverrideDecorator()
|
||||
## Copy the instance
|
||||
deep_copy._instance = copy.deepcopy(self._instance, memo)
|
||||
## Set the copied instance as the first (and only) instance container of the stack.
|
||||
deep_copy._stack.replaceContainer(0, deep_copy._instance)
|
||||
return deep_copy
|
||||
|
||||
## Gets the currently active extruder to print this object with.
|
||||
#
|
||||
# \return An extruder's container stack.
|
||||
def getActiveExtruder(self):
|
||||
return self._extruder_stack
|
||||
|
||||
def _onSettingChanged(self, instance, property):
|
||||
if property == "value": # Only reslice if the value has changed.
|
||||
Application.getInstance().getBackend().forceSlice()
|
||||
|
||||
## Makes sure that the stack upon which the container stack is placed is
|
||||
# kept up to date.
|
||||
def _updateNextStack(self):
|
||||
if self._extruder_stack:
|
||||
extruder_stack = ContainerRegistry.getInstance().findContainerStacks(id = self._extruder_stack)
|
||||
if extruder_stack:
|
||||
self._stack.setNextStack(extruder_stack)
|
||||
else:
|
||||
UM.Logger.log("e", "Extruder stack %s below per-object settings does not exist.", self._extruder_stack)
|
||||
else:
|
||||
self._stack.setNextStack(Application.getInstance().getGlobalContainerStack())
|
||||
|
||||
## Changes the extruder with which to print this node.
|
||||
#
|
||||
# \param extruder_stack_id The new extruder stack to print with.
|
||||
def setActiveExtruder(self, extruder_stack_id):
|
||||
self._extruder_stack = extruder_stack_id
|
||||
self.activeExtruderChanged.emit()
|
||||
|
||||
def getStack(self):
|
||||
return self._stack
|
Loading…
Add table
Add a link
Reference in a new issue