mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-06-04 11:14:21 +08:00
Merge remote-tracking branch 'upstream/master'
This commit is contained in:
commit
9bc63fb1f9
@ -17,6 +17,7 @@ if(CURA_DEBUGMODE)
|
||||
set(_cura_debugmode "ON")
|
||||
endif()
|
||||
|
||||
set(CURA_APP_DISPLAY_NAME "Ultimaker Cura" CACHE STRING "Display name of Cura")
|
||||
set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
|
||||
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
|
||||
set(CURA_SDK_VERSION "" CACHE STRING "SDK version of Cura")
|
||||
|
43
Jenkinsfile
vendored
43
Jenkinsfile
vendored
@ -38,20 +38,9 @@ parallel_nodes(['linux && cura', 'windows && cura'])
|
||||
{
|
||||
if (isUnix())
|
||||
{
|
||||
// For Linux to show everything
|
||||
def branch = env.BRANCH_NAME
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
|
||||
{
|
||||
branch = "master"
|
||||
}
|
||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
|
||||
|
||||
// For Linux
|
||||
try {
|
||||
sh """
|
||||
cd ..
|
||||
export PYTHONPATH=.:"${uranium_dir}"
|
||||
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/pytest -x --verbose --full-trace --capture=no ./tests
|
||||
"""
|
||||
sh 'make CTEST_OUTPUT_ON_FAILURE=TRUE test'
|
||||
} catch(e)
|
||||
{
|
||||
currentBuild.result = "UNSTABLE"
|
||||
@ -70,34 +59,6 @@ parallel_nodes(['linux && cura', 'windows && cura'])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Code Style')
|
||||
{
|
||||
if (isUnix())
|
||||
{
|
||||
// For Linux to show everything.
|
||||
// CMake also runs this test, but if it fails then the test just shows "failed" without details of what exactly failed.
|
||||
def branch = env.BRANCH_NAME
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
|
||||
{
|
||||
branch = "master"
|
||||
}
|
||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
|
||||
|
||||
try
|
||||
{
|
||||
sh """
|
||||
cd ..
|
||||
export PYTHONPATH=.:"${uranium_dir}"
|
||||
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/python3 run_mypy.py
|
||||
"""
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
currentBuild.result = "UNSTABLE"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,8 +20,9 @@ Dependencies
|
||||
------------
|
||||
* [Uranium](https://github.com/Ultimaker/Uranium) Cura is built on top of the Uranium framework.
|
||||
* [CuraEngine](https://github.com/Ultimaker/CuraEngine) This will be needed at runtime to perform the actual slicing.
|
||||
* [fdm_materials](https://github.com/Ultimaker/fdm_materials) Required to load a printer that has swappable material profiles.
|
||||
* [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support.
|
||||
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers
|
||||
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers.
|
||||
|
||||
Build scripts
|
||||
-------------
|
||||
|
@ -6,6 +6,8 @@ include(CMakeParseArguments)
|
||||
|
||||
find_package(PythonInterp 3.5.0 REQUIRED)
|
||||
|
||||
add_custom_target(test-verbose COMMAND ${CMAKE_CTEST_COMMAND} --verbose)
|
||||
|
||||
function(cura_add_test)
|
||||
set(_single_args NAME DIRECTORY PYTHONPATH)
|
||||
cmake_parse_arguments("" "" "${_single_args}" "" ${ARGN})
|
||||
|
@ -13,6 +13,6 @@ TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
|
||||
Icon=cura-icon
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;
|
||||
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;text/x-gcode;
|
||||
Categories=Graphics;
|
||||
Keywords=3D;Printing;Slicer;
|
||||
|
@ -19,4 +19,12 @@
|
||||
<glob-deleteall/>
|
||||
<glob pattern="*.obj"/>
|
||||
</mime-type>
|
||||
<mime-type type="text/x-gcode">
|
||||
<sub-class-of type="text/plain"/>
|
||||
<comment>Gcode file</comment>
|
||||
<icon name="unknown"/>
|
||||
<glob-deleteall/>
|
||||
<glob pattern="*.gcode"/>
|
||||
<glob pattern="*.g"/>
|
||||
</mime-type>
|
||||
</mime-info>
|
@ -44,7 +44,7 @@ class Account(QObject):
|
||||
OAUTH_SERVER_URL= self._oauth_root,
|
||||
CALLBACK_PORT=self._callback_port,
|
||||
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
|
||||
CLIENT_ID="um---------------ultimaker_cura_drive_plugin",
|
||||
CLIENT_ID="um----------------------------ultimaker_cura",
|
||||
CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.write",
|
||||
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
|
||||
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.API.Interface.Settings import Settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -23,9 +22,6 @@ if TYPE_CHECKING:
|
||||
|
||||
class Interface:
|
||||
|
||||
# For now we use the same API version to be consistent.
|
||||
VERSION = PluginRegistry.APIVersion
|
||||
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
# API methods specific to the settings portion of the UI
|
||||
self.settings = Settings(application)
|
||||
|
@ -4,7 +4,6 @@ from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtProperty
|
||||
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.API.Backups import Backups
|
||||
from cura.API.Interface import Interface
|
||||
from cura.API.Account import Account
|
||||
@ -22,7 +21,6 @@ if TYPE_CHECKING:
|
||||
class CuraAPI(QObject):
|
||||
|
||||
# For now we use the same API version to be consistent.
|
||||
VERSION = PluginRegistry.APIVersion
|
||||
__instance = None # type: "CuraAPI"
|
||||
_application = None # type: CuraApplication
|
||||
|
||||
|
@ -66,6 +66,11 @@ class Arrange:
|
||||
continue
|
||||
vertices = vertices.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
|
||||
points = copy.deepcopy(vertices._points)
|
||||
|
||||
# After scaling (like up to 0.1 mm) the node might not have points
|
||||
if len(points) == 0:
|
||||
continue
|
||||
|
||||
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
||||
arranger.place(0, 0, shape_arr)
|
||||
|
||||
|
@ -46,10 +46,11 @@ class Backup:
|
||||
|
||||
# We copy the preferences file to the user data directory in Linux as it's in a different location there.
|
||||
# When restoring a backup on Linux, we move it back.
|
||||
if Platform.isLinux():
|
||||
if Platform.isLinux(): #TODO: This should check for the config directory not being the same as the data directory, rather than hard-coding that to Linux systems.
|
||||
preferences_file_name = self._application.getApplicationName()
|
||||
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
|
||||
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
|
||||
if os.path.exists(preferences_file) and (not os.path.exists(backup_preferences_file) or not os.path.samefile(preferences_file, backup_preferences_file)):
|
||||
Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
|
||||
shutil.copyfile(preferences_file, backup_preferences_file)
|
||||
|
||||
|
@ -83,7 +83,14 @@ class BuildVolume(SceneNode):
|
||||
" with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
|
||||
|
||||
self._global_container_stack = None
|
||||
|
||||
self._stack_change_timer = QTimer()
|
||||
self._stack_change_timer.setInterval(100)
|
||||
self._stack_change_timer.setSingleShot(True)
|
||||
self._stack_change_timer.timeout.connect(self._onStackChangeTimerFinished)
|
||||
|
||||
self._application.globalContainerStackChanged.connect(self._onStackChanged)
|
||||
|
||||
self._onStackChanged()
|
||||
|
||||
self._engine_ready = False
|
||||
@ -105,6 +112,8 @@ class BuildVolume(SceneNode):
|
||||
self._setting_change_timer.setSingleShot(True)
|
||||
self._setting_change_timer.timeout.connect(self._onSettingChangeTimerFinished)
|
||||
|
||||
|
||||
|
||||
# Must be after setting _build_volume_message, apparently that is used in getMachineManager.
|
||||
# activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
|
||||
# Therefore this works.
|
||||
@ -479,6 +488,8 @@ class BuildVolume(SceneNode):
|
||||
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
|
||||
)
|
||||
|
||||
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds
|
||||
|
||||
self.updateNodeBoundaryCheck()
|
||||
|
||||
def getBoundingBox(self) -> AxisAlignedBox:
|
||||
@ -489,6 +500,8 @@ class BuildVolume(SceneNode):
|
||||
|
||||
def _updateRaftThickness(self):
|
||||
old_raft_thickness = self._raft_thickness
|
||||
if self._global_container_stack.extruders:
|
||||
# This might be called before the extruder stacks have initialised, in which case getting the adhesion_type fails
|
||||
self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value")
|
||||
self._raft_thickness = 0.0
|
||||
if self._adhesion_type == "raft":
|
||||
@ -522,8 +535,11 @@ class BuildVolume(SceneNode):
|
||||
if extra_z != self._extra_z_clearance:
|
||||
self._extra_z_clearance = extra_z
|
||||
|
||||
## Update the build volume visualization
|
||||
def _onStackChanged(self):
|
||||
self._stack_change_timer.start()
|
||||
|
||||
## Update the build volume visualization
|
||||
def _onStackChangeTimerFinished(self):
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
||||
extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
|
@ -36,18 +36,14 @@ else:
|
||||
except ImportError:
|
||||
CuraDebugMode = False # [CodeStyle: Reflecting imported value]
|
||||
|
||||
# List of exceptions that should be considered "fatal" and abort the program.
|
||||
# These are primarily some exception types that we simply cannot really recover from
|
||||
# (MemoryError and SystemError) and exceptions that indicate grave errors in the
|
||||
# code that cause the Python interpreter to fail (SyntaxError, ImportError).
|
||||
fatal_exception_types = [
|
||||
MemoryError,
|
||||
SyntaxError,
|
||||
ImportError,
|
||||
SystemError,
|
||||
# List of exceptions that should not be considered "fatal" and abort the program.
|
||||
# These are primarily some exception types that we simply skip
|
||||
skip_exception_types = [
|
||||
SystemExit,
|
||||
KeyboardInterrupt,
|
||||
GeneratorExit
|
||||
]
|
||||
|
||||
|
||||
class CrashHandler:
|
||||
crash_url = "https://stats.ultimaker.com/api/cura"
|
||||
|
||||
@ -70,7 +66,7 @@ class CrashHandler:
|
||||
# If Cura has fully started, we only show fatal errors.
|
||||
# If Cura has not fully started yet, we always show the early crash dialog. Otherwise, Cura will just crash
|
||||
# without any information.
|
||||
if has_started and exception_type not in fatal_exception_types:
|
||||
if has_started and exception_type in skip_exception_types:
|
||||
return
|
||||
|
||||
if not has_started:
|
||||
@ -387,7 +383,7 @@ class CrashHandler:
|
||||
Application.getInstance().callLater(self._show)
|
||||
|
||||
def _show(self):
|
||||
# When the exception is not in the fatal_exception_types list, the dialog is not created, so we don't need to show it
|
||||
# When the exception is in the skip_exception_types list, the dialog is not created, so we don't need to show it
|
||||
if self.dialog:
|
||||
self.dialog.exec_()
|
||||
os._exit(1)
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
from PyQt5.QtCore import QObject, QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from typing import List, TYPE_CHECKING
|
||||
from typing import List, TYPE_CHECKING, cast
|
||||
|
||||
from UM.Event import CallFunctionEvent
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
@ -36,12 +36,12 @@ class CuraActions(QObject):
|
||||
# Starting a web browser from a signal handler connected to a menu will crash on windows.
|
||||
# So instead, defer the call to the next run of the event loop, since that does work.
|
||||
# Note that weirdly enough, only signal handlers that open a web browser fail like that.
|
||||
event = CallFunctionEvent(self._openUrl, [QUrl("http://ultimaker.com/en/support/software")], {})
|
||||
event = CallFunctionEvent(self._openUrl, [QUrl("https://ultimaker.com/en/resources/manuals/software")], {})
|
||||
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
||||
|
||||
@pyqtSlot()
|
||||
def openBugReportPage(self) -> None:
|
||||
event = CallFunctionEvent(self._openUrl, [QUrl("http://github.com/Ultimaker/Cura/issues")], {})
|
||||
event = CallFunctionEvent(self._openUrl, [QUrl("https://github.com/Ultimaker/Cura/issues")], {})
|
||||
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
||||
|
||||
## Reset camera position and direction to default
|
||||
@ -61,8 +61,10 @@ class CuraActions(QObject):
|
||||
operation = GroupedOperation()
|
||||
for node in Selection.getAllSelectedObjects():
|
||||
current_node = node
|
||||
while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
|
||||
current_node = current_node.getParent()
|
||||
parent_node = current_node.getParent()
|
||||
while parent_node and parent_node.callDecoration("isGroup"):
|
||||
current_node = parent_node
|
||||
parent_node = current_node.getParent()
|
||||
|
||||
# This was formerly done with SetTransformOperation but because of
|
||||
# unpredictable matrix deconstruction it was possible that mirrors
|
||||
@ -150,11 +152,11 @@ class CuraActions(QObject):
|
||||
|
||||
root = cura.CuraApplication.CuraApplication.getInstance().getController().getScene().getRoot()
|
||||
|
||||
nodes_to_change = []
|
||||
nodes_to_change = [] # type: List[SceneNode]
|
||||
for node in Selection.getAllSelectedObjects():
|
||||
parent_node = node # Find the parent node to change instead
|
||||
while parent_node.getParent() != root:
|
||||
parent_node = parent_node.getParent()
|
||||
parent_node = cast(SceneNode, parent_node.getParent())
|
||||
|
||||
for single_node in BreadthFirstIterator(parent_node): # type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||
nodes_to_change.append(single_node)
|
||||
|
@ -4,7 +4,7 @@
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import cast, TYPE_CHECKING, Optional, Callable
|
||||
from typing import cast, TYPE_CHECKING, Optional, Callable, List
|
||||
|
||||
import numpy
|
||||
|
||||
@ -51,6 +51,7 @@ from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
||||
from cura.Arranging.ShapeArray import ShapeArray
|
||||
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
||||
from cura.GlobalStacksModel import GlobalStacksModel
|
||||
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
||||
from cura.Operations.SetParentOperation import SetParentOperation
|
||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
@ -113,6 +114,7 @@ from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
|
||||
|
||||
from cura.ObjectsModel import ObjectsModel
|
||||
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
|
||||
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
@ -128,19 +130,20 @@ if TYPE_CHECKING:
|
||||
numpy.seterr(all = "ignore")
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion
|
||||
from cura.CuraVersion import CuraAppDisplayName, CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion # type: ignore
|
||||
except ImportError:
|
||||
CuraAppDisplayName = "Ultimaker Cura"
|
||||
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
|
||||
CuraBuildType = ""
|
||||
CuraDebugMode = False
|
||||
CuraSDKVersion = ""
|
||||
CuraSDKVersion = "6.0.0"
|
||||
|
||||
|
||||
class CuraApplication(QtApplication):
|
||||
# SettingVersion represents the set of settings available in the machine/extruder definitions.
|
||||
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
|
||||
# changes of the settings.
|
||||
SettingVersion = 5
|
||||
SettingVersion = 6
|
||||
|
||||
Created = False
|
||||
|
||||
@ -161,7 +164,9 @@ class CuraApplication(QtApplication):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(name = "cura",
|
||||
app_display_name = CuraAppDisplayName,
|
||||
version = CuraVersion,
|
||||
api_version = CuraSDKVersion,
|
||||
buildtype = CuraBuildType,
|
||||
is_debug_mode = CuraDebugMode,
|
||||
tray_icon_name = "cura-icon-32.png",
|
||||
@ -178,7 +183,6 @@ class CuraApplication(QtApplication):
|
||||
# Variables set from CLI
|
||||
self._files_to_open = []
|
||||
self._use_single_instance = False
|
||||
self._trigger_early_crash = False # For debug only
|
||||
|
||||
self._single_instance = None
|
||||
|
||||
@ -203,6 +207,8 @@ class CuraApplication(QtApplication):
|
||||
self._container_manager = None
|
||||
|
||||
self._object_manager = None
|
||||
self._extruders_model = None
|
||||
self._extruders_model_with_optional = None
|
||||
self._build_plate_model = None
|
||||
self._multi_build_plate_model = None
|
||||
self._setting_visibility_presets_model = None
|
||||
@ -289,7 +295,10 @@ class CuraApplication(QtApplication):
|
||||
sys.exit(0)
|
||||
|
||||
self._use_single_instance = self._cli_args.single_instance
|
||||
self._trigger_early_crash = self._cli_args.trigger_early_crash
|
||||
# FOR TESTING ONLY
|
||||
if self._cli_args.trigger_early_crash:
|
||||
assert not "This crash is triggered by the trigger_early_crash command line argument."
|
||||
|
||||
for filename in self._cli_args.file:
|
||||
self._files_to_open.append(os.path.abspath(filename))
|
||||
|
||||
@ -425,6 +434,7 @@ class CuraApplication(QtApplication):
|
||||
def startSplashWindowPhase(self) -> None:
|
||||
super().startSplashWindowPhase()
|
||||
|
||||
if not self.getIsHeadLess():
|
||||
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
|
||||
|
||||
self.setRequiredPlugins([
|
||||
@ -436,6 +446,7 @@ class CuraApplication(QtApplication):
|
||||
"XmlMaterialProfile", #Cura crashes without this one.
|
||||
"Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
|
||||
"PrepareStage", #Cura is useless without this one since you can't load models.
|
||||
"PreviewStage", #This shows the list of the plugin views that are installed in Cura.
|
||||
"MonitorStage", #Major part of Cura's functionality.
|
||||
"LocalFileOutputDevice", #Major part of Cura's functionality.
|
||||
"LocalContainerProvider", #Cura is useless without any profiles or setting definitions.
|
||||
@ -489,7 +500,8 @@ class CuraApplication(QtApplication):
|
||||
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
|
||||
preferences.addPreference("cura/choice_on_open_project", "always_ask")
|
||||
preferences.addPreference("cura/use_multi_build_plate", False)
|
||||
|
||||
preferences.addPreference("view/settings_list_height", 600)
|
||||
preferences.addPreference("view/settings_visible", False)
|
||||
preferences.addPreference("cura/currency", "€")
|
||||
preferences.addPreference("cura/material_settings", "{}")
|
||||
|
||||
@ -629,8 +641,6 @@ class CuraApplication(QtApplication):
|
||||
self._message_box_callback = None
|
||||
self._message_box_callback_arguments = []
|
||||
|
||||
showPrintMonitor = pyqtSignal(bool, arguments = ["show"])
|
||||
|
||||
def setSaveDataEnabled(self, enabled: bool) -> None:
|
||||
self._save_data_enabled = enabled
|
||||
|
||||
@ -656,7 +666,7 @@ class CuraApplication(QtApplication):
|
||||
|
||||
## Handle loading of all plugin types (and the backend explicitly)
|
||||
# \sa PluginRegistry
|
||||
def _loadPlugins(self):
|
||||
def _loadPlugins(self) -> None:
|
||||
self._plugin_registry.addType("profile_reader", self._addProfileReader)
|
||||
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
|
||||
|
||||
@ -858,6 +868,19 @@ class CuraApplication(QtApplication):
|
||||
self._object_manager = ObjectsModel.createObjectsModel()
|
||||
return self._object_manager
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getExtrudersModel(self, *args) -> "ExtrudersModel":
|
||||
if self._extruders_model is None:
|
||||
self._extruders_model = ExtrudersModel(self)
|
||||
return self._extruders_model
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getExtrudersModelWithOptional(self, *args) -> "ExtrudersModel":
|
||||
if self._extruders_model_with_optional is None:
|
||||
self._extruders_model_with_optional = ExtrudersModel(self)
|
||||
self._extruders_model_with_optional.setAddOptionalExtruder(True)
|
||||
return self._extruders_model_with_optional
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel:
|
||||
if self._multi_build_plate_model is None:
|
||||
@ -950,6 +973,7 @@ class CuraApplication(QtApplication):
|
||||
qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
|
||||
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
|
||||
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
|
||||
qmlRegisterType(GlobalStacksModel, "Cura", 1, 0, "GlobalStacksModel")
|
||||
|
||||
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
|
||||
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
|
||||
@ -971,6 +995,8 @@ class CuraApplication(QtApplication):
|
||||
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
|
||||
qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
|
||||
|
||||
qmlRegisterType(PrinterOutputDevice, "Cura", 1, 0, "PrinterOutputDevice")
|
||||
|
||||
from cura.API import CuraAPI
|
||||
qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
|
||||
|
||||
@ -1081,88 +1107,6 @@ class CuraApplication(QtApplication):
|
||||
self._platform_activity = True if count > 0 else False
|
||||
self.activityChanged.emit()
|
||||
|
||||
# Remove all selected objects from the scene.
|
||||
@pyqtSlot()
|
||||
@deprecated("Moved to CuraActions", "2.6")
|
||||
def deleteSelection(self):
|
||||
if not self.getController().getToolsEnabled():
|
||||
return
|
||||
removed_group_nodes = []
|
||||
op = GroupedOperation()
|
||||
nodes = Selection.getAllSelectedObjects()
|
||||
for node in nodes:
|
||||
op.addOperation(RemoveSceneNodeOperation(node))
|
||||
group_node = node.getParent()
|
||||
if group_node and group_node.callDecoration("isGroup") and group_node not in removed_group_nodes:
|
||||
remaining_nodes_in_group = list(set(group_node.getChildren()) - set(nodes))
|
||||
if len(remaining_nodes_in_group) == 1:
|
||||
removed_group_nodes.append(group_node)
|
||||
op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent()))
|
||||
op.addOperation(RemoveSceneNodeOperation(group_node))
|
||||
op.push()
|
||||
|
||||
## Remove an object from the scene.
|
||||
# Note that this only removes an object if it is selected.
|
||||
@pyqtSlot("quint64")
|
||||
@deprecated("Use deleteSelection instead", "2.6")
|
||||
def deleteObject(self, object_id):
|
||||
if not self.getController().getToolsEnabled():
|
||||
return
|
||||
|
||||
node = self.getController().getScene().findObject(object_id)
|
||||
|
||||
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
|
||||
node = Selection.getSelectedObject(0)
|
||||
|
||||
if node:
|
||||
op = GroupedOperation()
|
||||
op.addOperation(RemoveSceneNodeOperation(node))
|
||||
|
||||
group_node = node.getParent()
|
||||
if group_node:
|
||||
# Note that at this point the node has not yet been deleted
|
||||
if len(group_node.getChildren()) <= 2 and group_node.callDecoration("isGroup"):
|
||||
op.addOperation(SetParentOperation(group_node.getChildren()[0], group_node.getParent()))
|
||||
op.addOperation(RemoveSceneNodeOperation(group_node))
|
||||
|
||||
op.push()
|
||||
|
||||
## Create a number of copies of existing object.
|
||||
# \param object_id
|
||||
# \param count number of copies
|
||||
# \param min_offset minimum offset to other objects.
|
||||
@pyqtSlot("quint64", int)
|
||||
@deprecated("Use CuraActions::multiplySelection", "2.6")
|
||||
def multiplyObject(self, object_id, count, min_offset = 8):
|
||||
node = self.getController().getScene().findObject(object_id)
|
||||
if not node:
|
||||
node = Selection.getSelectedObject(0)
|
||||
|
||||
while node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
node = node.getParent()
|
||||
|
||||
job = MultiplyObjectsJob([node], count, min_offset)
|
||||
job.start()
|
||||
return
|
||||
|
||||
## Center object on platform.
|
||||
@pyqtSlot("quint64")
|
||||
@deprecated("Use CuraActions::centerSelection", "2.6")
|
||||
def centerObject(self, object_id):
|
||||
node = self.getController().getScene().findObject(object_id)
|
||||
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
|
||||
node = Selection.getSelectedObject(0)
|
||||
|
||||
if not node:
|
||||
return
|
||||
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
node = node.getParent()
|
||||
|
||||
if node:
|
||||
op = SetTransformOperation(node, Vector())
|
||||
op.push()
|
||||
|
||||
## Select all nodes containing mesh data in the scene.
|
||||
@pyqtSlot()
|
||||
def selectAll(self):
|
||||
@ -1242,61 +1186,74 @@ class CuraApplication(QtApplication):
|
||||
|
||||
## Arrange all objects.
|
||||
@pyqtSlot()
|
||||
def arrangeObjectsToAllBuildPlates(self):
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
def arrangeObjectsToAllBuildPlates(self) -> None:
|
||||
nodes_to_arrange = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
|
||||
parent_node = node.getParent()
|
||||
if parent_node and parent_node.callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is reset)
|
||||
|
||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||
continue # i.e. node with layer data
|
||||
|
||||
bounding_box = node.getBoundingBox()
|
||||
# Skip nodes that are too big
|
||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
||||
nodes.append(node)
|
||||
job = ArrangeObjectsAllBuildPlatesJob(nodes)
|
||||
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
|
||||
nodes_to_arrange.append(node)
|
||||
job = ArrangeObjectsAllBuildPlatesJob(nodes_to_arrange)
|
||||
job.start()
|
||||
self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
|
||||
|
||||
# Single build plate
|
||||
@pyqtSlot()
|
||||
def arrangeAll(self):
|
||||
nodes = []
|
||||
def arrangeAll(self) -> None:
|
||||
nodes_to_arrange = []
|
||||
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
||||
|
||||
parent_node = node.getParent()
|
||||
if parent_node and parent_node.callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
|
||||
if not node.isSelectable():
|
||||
continue # i.e. node with layer data
|
||||
|
||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||
continue # i.e. node with layer data
|
||||
|
||||
if node.callDecoration("getBuildPlateNumber") == active_build_plate:
|
||||
# Skip nodes that are too big
|
||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
||||
nodes.append(node)
|
||||
self.arrange(nodes, fixed_nodes = [])
|
||||
bounding_box = node.getBoundingBox()
|
||||
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
|
||||
nodes_to_arrange.append(node)
|
||||
self.arrange(nodes_to_arrange, fixed_nodes = [])
|
||||
|
||||
## Arrange a set of nodes given a set of fixed nodes
|
||||
# \param nodes nodes that we have to place
|
||||
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
|
||||
def arrange(self, nodes, fixed_nodes):
|
||||
def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None:
|
||||
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
||||
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
|
||||
job.start()
|
||||
|
||||
## Reload all mesh data on the screen from file.
|
||||
@pyqtSlot()
|
||||
def reloadAll(self):
|
||||
def reloadAll(self) -> None:
|
||||
Logger.log("i", "Reloading all loaded mesh data.")
|
||||
nodes = []
|
||||
has_merged_nodes = False
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||
if not isinstance(node, CuraSceneNode) or not node.getMeshData():
|
||||
if node.getName() == "MergedMesh":
|
||||
has_merged_nodes = True
|
||||
@ -1311,7 +1268,7 @@ class CuraApplication(QtApplication):
|
||||
file_name = node.getMeshData().getFileName()
|
||||
if file_name:
|
||||
job = ReadMeshJob(file_name)
|
||||
job._node = node
|
||||
job._node = node # type: ignore
|
||||
job.finished.connect(self._reloadMeshFinished)
|
||||
if has_merged_nodes:
|
||||
job.finished.connect(self.updateOriginOfMergedMeshes)
|
||||
@ -1320,20 +1277,8 @@ class CuraApplication(QtApplication):
|
||||
else:
|
||||
Logger.log("w", "Unable to reload data because we don't have a filename.")
|
||||
|
||||
|
||||
## Get logging data of the backend engine
|
||||
# \returns \type{string} Logging data
|
||||
@pyqtSlot(result = str)
|
||||
def getEngineLog(self):
|
||||
log = ""
|
||||
|
||||
for entry in self.getBackend().getLog():
|
||||
log += entry.decode()
|
||||
|
||||
return log
|
||||
|
||||
@pyqtSlot("QStringList")
|
||||
def setExpandedCategories(self, categories):
|
||||
def setExpandedCategories(self, categories: List[str]) -> None:
|
||||
categories = list(set(categories))
|
||||
categories.sort()
|
||||
joined = ";".join(categories)
|
||||
@ -1344,7 +1289,7 @@ class CuraApplication(QtApplication):
|
||||
expandedCategoriesChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty("QStringList", notify = expandedCategoriesChanged)
|
||||
def expandedCategories(self):
|
||||
def expandedCategories(self) -> List[str]:
|
||||
return self.getPreferences().getValue("cura/categories_expanded").split(";")
|
||||
|
||||
@pyqtSlot()
|
||||
@ -1394,13 +1339,12 @@ class CuraApplication(QtApplication):
|
||||
|
||||
|
||||
## Updates origin position of all merged meshes
|
||||
# \param jobNode \type{Job} empty object which passed which is required by JobQueue
|
||||
def updateOriginOfMergedMeshes(self, jobNode):
|
||||
def updateOriginOfMergedMeshes(self, _):
|
||||
group_nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh":
|
||||
|
||||
#checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator
|
||||
# Checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator
|
||||
for decorator in node.getDecorators():
|
||||
if isinstance(decorator, GroupDecorator):
|
||||
group_nodes.append(node)
|
||||
@ -1444,7 +1388,7 @@ class CuraApplication(QtApplication):
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def groupSelected(self):
|
||||
def groupSelected(self) -> None:
|
||||
# Create a group-node
|
||||
group_node = CuraSceneNode()
|
||||
group_decorator = GroupDecorator()
|
||||
@ -1460,7 +1404,8 @@ class CuraApplication(QtApplication):
|
||||
# Remove nodes that are directly parented to another selected node from the selection so they remain parented
|
||||
selected_nodes = Selection.getAllSelectedObjects().copy()
|
||||
for node in selected_nodes:
|
||||
if node.getParent() in selected_nodes and not node.getParent().callDecoration("isGroup"):
|
||||
parent = node.getParent()
|
||||
if parent is not None and parent in selected_nodes and not parent.callDecoration("isGroup"):
|
||||
Selection.remove(node)
|
||||
|
||||
# Move selected nodes into the group-node
|
||||
@ -1472,7 +1417,7 @@ class CuraApplication(QtApplication):
|
||||
Selection.add(group_node)
|
||||
|
||||
@pyqtSlot()
|
||||
def ungroupSelected(self):
|
||||
def ungroupSelected(self) -> None:
|
||||
selected_objects = Selection.getAllSelectedObjects().copy()
|
||||
for node in selected_objects:
|
||||
if node.callDecoration("isGroup"):
|
||||
@ -1495,7 +1440,7 @@ class CuraApplication(QtApplication):
|
||||
# Note: The group removes itself from the scene once all its children have left it,
|
||||
# see GroupDecorator._onChildrenChanged
|
||||
|
||||
def _createSplashScreen(self):
|
||||
def _createSplashScreen(self) -> Optional[CuraSplashScreen.CuraSplashScreen]:
|
||||
if self._is_headless:
|
||||
return None
|
||||
return CuraSplashScreen.CuraSplashScreen()
|
||||
@ -1661,7 +1606,9 @@ class CuraApplication(QtApplication):
|
||||
is_non_sliceable = "." + file_extension in self._non_sliceable_extensions
|
||||
|
||||
if is_non_sliceable:
|
||||
self.callLater(lambda: self.getController().setActiveView("SimulationView"))
|
||||
# Need to switch first to the preview stage and then to layer view
|
||||
self.callLater(lambda: (self.getController().setActiveStage("PreviewStage"),
|
||||
self.getController().setActiveView("SimulationView")))
|
||||
|
||||
block_slicing_decorator = BlockSlicingDecorator()
|
||||
node.addDecorator(block_slicing_decorator)
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
CuraAppDisplayName = "@CURA_APP_DISPLAY_NAME@"
|
||||
CuraVersion = "@CURA_VERSION@"
|
||||
CuraBuildType = "@CURA_BUILDTYPE@"
|
||||
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
||||
|
24
cura/CuraView.py
Normal file
24
cura/CuraView.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, QUrl
|
||||
|
||||
from UM.View.View import View
|
||||
|
||||
|
||||
# Since Cura has a few pre-defined "space claims" for the locations of certain components, we've provided some structure
|
||||
# to indicate this.
|
||||
# MainComponent works in the same way the MainComponent of a stage.
|
||||
# the stageMenuComponent returns an item that should be used somehwere in the stage menu. It's up to the active stage
|
||||
# to actually do something with this.
|
||||
class CuraView(View):
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
@pyqtProperty(QUrl, constant = True)
|
||||
def mainComponent(self) -> QUrl:
|
||||
return self.getDisplayComponent("main")
|
||||
|
||||
@pyqtProperty(QUrl, constant = True)
|
||||
def stageMenuComponent(self) -> QUrl:
|
||||
return self.getDisplayComponent("menu")
|
63
cura/GlobalStacksModel.py
Normal file
63
cura/GlobalStacksModel.py
Normal file
@ -0,0 +1,63 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal
|
||||
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
|
||||
from cura.PrinterOutputDevice import ConnectionType
|
||||
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
|
||||
class GlobalStacksModel(ListModel):
|
||||
NameRole = Qt.UserRole + 1
|
||||
IdRole = Qt.UserRole + 2
|
||||
HasRemoteConnectionRole = Qt.UserRole + 3
|
||||
ConnectionTypeRole = Qt.UserRole + 4
|
||||
MetaDataRole = Qt.UserRole + 5
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self.addRoleName(self.NameRole, "name")
|
||||
self.addRoleName(self.IdRole, "id")
|
||||
self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection")
|
||||
self.addRoleName(self.ConnectionTypeRole, "connectionType")
|
||||
self.addRoleName(self.MetaDataRole, "metadata")
|
||||
self._container_stacks = []
|
||||
|
||||
# Listen to changes
|
||||
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
|
||||
ContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
|
||||
ContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
|
||||
self._filter_dict = {}
|
||||
self._update()
|
||||
|
||||
## Handler for container added/removed events from registry
|
||||
def _onContainerChanged(self, container):
|
||||
# We only need to update when the added / removed container GlobalStack
|
||||
if isinstance(container, GlobalStack):
|
||||
self._update()
|
||||
|
||||
def _update(self) -> None:
|
||||
items = []
|
||||
|
||||
container_stacks = ContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
||||
|
||||
for container_stack in container_stacks:
|
||||
connection_type = int(container_stack.getMetaDataEntry("connection_type", ConnectionType.NotConnected.value))
|
||||
has_remote_connection = connection_type in [ConnectionType.NetworkConnection.value, ConnectionType.CloudConnection.value]
|
||||
if container_stack.getMetaDataEntry("hidden", False) in ["True", True]:
|
||||
continue
|
||||
|
||||
# TODO: Remove reference to connect group name.
|
||||
items.append({"name": container_stack.getMetaDataEntry("connect_group_name", container_stack.getName()),
|
||||
"id": container_stack.getId(),
|
||||
"hasRemoteConnection": has_remote_connection,
|
||||
"connectionType": connection_type,
|
||||
"metadata": container_stack.getMetaData().copy()})
|
||||
items.sort(key=lambda i: not i["hasRemoteConnection"])
|
||||
self.setItems(items)
|
@ -64,21 +64,21 @@ class MachineErrorChecker(QObject):
|
||||
|
||||
def _onMachineChanged(self) -> None:
|
||||
if self._global_stack:
|
||||
self._global_stack.propertyChanged.disconnect(self.startErrorCheck)
|
||||
self._global_stack.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
|
||||
self._global_stack.containersChanged.disconnect(self.startErrorCheck)
|
||||
|
||||
for extruder in self._global_stack.extruders.values():
|
||||
extruder.propertyChanged.disconnect(self.startErrorCheck)
|
||||
extruder.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
|
||||
extruder.containersChanged.disconnect(self.startErrorCheck)
|
||||
|
||||
self._global_stack = self._machine_manager.activeMachine
|
||||
|
||||
if self._global_stack:
|
||||
self._global_stack.propertyChanged.connect(self.startErrorCheck)
|
||||
self._global_stack.propertyChanged.connect(self.startErrorCheckPropertyChanged)
|
||||
self._global_stack.containersChanged.connect(self.startErrorCheck)
|
||||
|
||||
for extruder in self._global_stack.extruders.values():
|
||||
extruder.propertyChanged.connect(self.startErrorCheck)
|
||||
extruder.propertyChanged.connect(self.startErrorCheckPropertyChanged)
|
||||
extruder.containersChanged.connect(self.startErrorCheck)
|
||||
|
||||
hasErrorUpdated = pyqtSignal()
|
||||
@ -93,6 +93,13 @@ class MachineErrorChecker(QObject):
|
||||
def needToWaitForResult(self) -> bool:
|
||||
return self._need_to_check or self._check_in_progress
|
||||
|
||||
# Start the error check for property changed
|
||||
# this is seperate from the startErrorCheck because it ignores a number property types
|
||||
def startErrorCheckPropertyChanged(self, key, property_name):
|
||||
if property_name != "value":
|
||||
return
|
||||
self.startErrorCheck()
|
||||
|
||||
# Starts the error check timer to schedule a new error check.
|
||||
def startErrorCheck(self, *args) -> None:
|
||||
if not self._check_in_progress:
|
||||
|
@ -21,6 +21,7 @@ from .VariantType import VariantType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
|
||||
@ -298,7 +299,7 @@ class MaterialManager(QObject):
|
||||
def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
|
||||
return self._diameter_material_map.get(root_material_id, "")
|
||||
|
||||
def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]:
|
||||
def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]:
|
||||
return self._guid_material_groups_map.get(guid)
|
||||
|
||||
#
|
||||
@ -446,6 +447,28 @@ class MaterialManager(QObject):
|
||||
material_diameter, root_material_id)
|
||||
return node
|
||||
|
||||
# There are 2 ways to get fallback materials;
|
||||
# - A fallback by type (@sa getFallbackMaterialIdByMaterialType), which adds the generic version of this material
|
||||
# - A fallback by GUID; If a material has been duplicated, it should also check if the original materials do have
|
||||
# a GUID. This should only be done if the material itself does not have a quality just yet.
|
||||
def getFallBackMaterialIdsByMaterial(self, material: "InstanceContainer") -> List[str]:
|
||||
results = [] # type: List[str]
|
||||
|
||||
material_groups = self.getMaterialGroupListByGUID(material.getMetaDataEntry("GUID"))
|
||||
for material_group in material_groups: # type: ignore
|
||||
if material_group.name != material.getId():
|
||||
# If the material in the group is read only, put it at the front of the list (since that is the most
|
||||
# likely one to get a result)
|
||||
if material_group.is_read_only:
|
||||
results.insert(0, material_group.name)
|
||||
else:
|
||||
results.append(material_group.name)
|
||||
|
||||
fallback = self.getFallbackMaterialIdByMaterialType(material.getMetaDataEntry("material"))
|
||||
if fallback is not None:
|
||||
results.append(fallback)
|
||||
return results
|
||||
|
||||
#
|
||||
# Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla".
|
||||
# For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
|
||||
@ -602,7 +625,6 @@ class MaterialManager(QObject):
|
||||
container_to_add.setDirty(True)
|
||||
self._container_registry.addContainer(container_to_add)
|
||||
|
||||
|
||||
# if the duplicated material was favorite then the new material should also be added to favorite.
|
||||
if root_material_id in self.getFavorites():
|
||||
self.addFavorite(new_base_id)
|
||||
@ -622,8 +644,11 @@ class MaterialManager(QObject):
|
||||
machine_manager = self._application.getMachineManager()
|
||||
extruder_stack = machine_manager.activeStack
|
||||
|
||||
machine_definition = self._application.getGlobalContainerStack().definition
|
||||
preferred_material = machine_definition.getMetaDataEntry("preferred_material")
|
||||
|
||||
approximate_diameter = str(extruder_stack.approximateMaterialDiameter)
|
||||
root_material_id = "generic_pla"
|
||||
root_material_id = preferred_material if preferred_material else "generic_pla"
|
||||
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
|
||||
material_group = self.getMaterialGroup(root_material_id)
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional, Dict, Set
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
||||
from UM.Qt.ListModel import ListModel
|
||||
@ -9,6 +10,9 @@ from UM.Qt.ListModel import ListModel
|
||||
# Those 2 models are used by the material drop down menu to show generic materials and branded materials separately.
|
||||
# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top
|
||||
# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
|
||||
from cura.Machines.MaterialNode import MaterialNode
|
||||
|
||||
|
||||
class BaseMaterialsModel(ListModel):
|
||||
|
||||
extruderPositionChanged = pyqtSignal()
|
||||
@ -54,8 +58,8 @@ class BaseMaterialsModel(ListModel):
|
||||
self._extruder_position = 0
|
||||
self._extruder_stack = None
|
||||
|
||||
self._available_materials = None
|
||||
self._favorite_ids = None
|
||||
self._available_materials = None # type: Optional[Dict[str, MaterialNode]]
|
||||
self._favorite_ids = set() # type: Set[str]
|
||||
|
||||
def _updateExtruderStack(self):
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
@ -102,7 +106,6 @@ class BaseMaterialsModel(ListModel):
|
||||
return False
|
||||
|
||||
extruder_stack = global_stack.extruders[extruder_position]
|
||||
|
||||
self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack)
|
||||
if self._available_materials is None:
|
||||
return False
|
||||
|
@ -1,20 +1,16 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Logger import Logger
|
||||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||
|
||||
## Model that shows the list of favorite materials.
|
||||
class FavoriteMaterialsModel(BaseMaterialsModel):
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
|
||||
# Perform standard check and reset if the check fails
|
||||
if not self._canUpdate():
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
# Get updated list of favorites
|
||||
|
@ -11,10 +11,7 @@ class GenericMaterialsModel(BaseMaterialsModel):
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
|
||||
# Perform standard check and reset if the check fails
|
||||
if not self._canUpdate():
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
# Get updated list of favorites
|
||||
|
@ -28,12 +28,8 @@ class MaterialBrandsModel(BaseMaterialsModel):
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
|
||||
# Perform standard check and reset if the check fails
|
||||
if not self._canUpdate():
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
# Get updated list of favorites
|
||||
self._favorite_ids = self._material_manager.getFavorites()
|
||||
|
||||
|
@ -33,8 +33,6 @@ class NozzleModel(ListModel):
|
||||
def _update(self):
|
||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||
|
||||
self.items.clear()
|
||||
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
if global_stack is None:
|
||||
self.setItems([])
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt5.QtCore import Qt, QTimer
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
@ -21,6 +21,7 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
||||
AvailableRole = Qt.UserRole + 5
|
||||
QualityGroupRole = Qt.UserRole + 6
|
||||
QualityChangesGroupRole = Qt.UserRole + 7
|
||||
IsExperimentalRole = Qt.UserRole + 8
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
@ -32,20 +33,29 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
||||
self.addRoleName(self.AvailableRole, "available") #Whether the quality profile is available in our current nozzle + material.
|
||||
self.addRoleName(self.QualityGroupRole, "quality_group")
|
||||
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
|
||||
self.addRoleName(self.IsExperimentalRole, "is_experimental")
|
||||
|
||||
self._application = Application.getInstance()
|
||||
self._machine_manager = self._application.getMachineManager()
|
||||
self._quality_manager = Application.getInstance().getQualityManager()
|
||||
|
||||
self._application.globalContainerStackChanged.connect(self._update)
|
||||
self._machine_manager.activeQualityGroupChanged.connect(self._update)
|
||||
self._machine_manager.extruderChanged.connect(self._update)
|
||||
self._quality_manager.qualitiesUpdated.connect(self._update)
|
||||
self._application.globalContainerStackChanged.connect(self._onChange)
|
||||
self._machine_manager.activeQualityGroupChanged.connect(self._onChange)
|
||||
self._machine_manager.extruderChanged.connect(self._onChange)
|
||||
self._quality_manager.qualitiesUpdated.connect(self._onChange)
|
||||
|
||||
self._layer_height_unit = "" # This is cached
|
||||
|
||||
self._update_timer = QTimer() # type: QTimer
|
||||
self._update_timer.setInterval(100)
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self._update)
|
||||
|
||||
self._update()
|
||||
|
||||
def _onChange(self) -> None:
|
||||
self._update_timer.start()
|
||||
|
||||
def _update(self):
|
||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||
|
||||
@ -74,7 +84,8 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
||||
"layer_height": layer_height,
|
||||
"layer_height_unit": self._layer_height_unit,
|
||||
"available": quality_group.is_available,
|
||||
"quality_group": quality_group}
|
||||
"quality_group": quality_group,
|
||||
"is_experimental": quality_group.is_experimental}
|
||||
|
||||
item_list.append(item)
|
||||
|
||||
|
@ -6,6 +6,7 @@ from typing import Optional, List
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Resources import Resources
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
@ -18,14 +19,20 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
onItemsChanged = pyqtSignal()
|
||||
activePresetChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, preferences, parent = None):
|
||||
def __init__(self, preferences: Preferences, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self._items = [] # type: List[SettingVisibilityPreset]
|
||||
self._custom_preset = SettingVisibilityPreset(preset_id = "custom", name = "Custom selection", weight = -100)
|
||||
|
||||
self._populate()
|
||||
|
||||
basic_item = self.getVisibilityPresetById("basic")
|
||||
if basic_item is not None:
|
||||
basic_visibile_settings = ";".join(basic_item.settings)
|
||||
else:
|
||||
Logger.log("w", "Unable to find the basic visiblity preset.")
|
||||
basic_visibile_settings = ""
|
||||
|
||||
self._preferences = preferences
|
||||
|
||||
@ -42,7 +49,8 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
visible_settings = self._preferences.getValue("general/visible_settings")
|
||||
|
||||
if not visible_settings:
|
||||
self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item.settings))
|
||||
new_visible_settings = self._active_preset_item.settings if self._active_preset_item is not None else []
|
||||
self._preferences.setValue("general/visible_settings", ";".join(new_visible_settings))
|
||||
else:
|
||||
self._onPreferencesChanged("general/visible_settings")
|
||||
|
||||
@ -59,9 +67,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
def _populate(self) -> None:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
items = [] # type: List[SettingVisibilityPreset]
|
||||
|
||||
custom_preset = SettingVisibilityPreset(preset_id="custom", name ="Custom selection", weight = -100)
|
||||
items.append(custom_preset)
|
||||
items.append(self._custom_preset)
|
||||
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
||||
setting_visibility_preset = SettingVisibilityPreset()
|
||||
try:
|
||||
@ -77,7 +83,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
self.setItems(items)
|
||||
|
||||
@pyqtProperty("QVariantList", notify = onItemsChanged)
|
||||
def items(self):
|
||||
def items(self) -> List[SettingVisibilityPreset]:
|
||||
return self._items
|
||||
|
||||
def setItems(self, items: List[SettingVisibilityPreset]) -> None:
|
||||
@ -87,7 +93,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActivePreset(self, preset_id: str) -> None:
|
||||
if preset_id == self._active_preset_item.presetId:
|
||||
if self._active_preset_item is not None and preset_id == self._active_preset_item.presetId:
|
||||
Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id)
|
||||
return
|
||||
|
||||
@ -96,7 +102,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
|
||||
return
|
||||
|
||||
need_to_save_to_custom = self._active_preset_item.presetId == "custom" and preset_id != "custom"
|
||||
need_to_save_to_custom = self._active_preset_item is None or (self._active_preset_item.presetId == "custom" and preset_id != "custom")
|
||||
if need_to_save_to_custom:
|
||||
# Save the current visibility settings to custom
|
||||
current_visibility_string = self._preferences.getValue("general/visible_settings")
|
||||
@ -117,7 +123,9 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
|
||||
@pyqtProperty(str, notify = activePresetChanged)
|
||||
def activePreset(self) -> str:
|
||||
if self._active_preset_item is not None:
|
||||
return self._active_preset_item.presetId
|
||||
return ""
|
||||
|
||||
def _onPreferencesChanged(self, name: str) -> None:
|
||||
if name != "general/visible_settings":
|
||||
@ -140,7 +148,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
item_to_set = self._active_preset_item
|
||||
if matching_preset_item is None:
|
||||
# The new visibility setup is "custom" should be custom
|
||||
if self._active_preset_item.presetId == "custom":
|
||||
if self._active_preset_item is None or self._active_preset_item.presetId == "custom":
|
||||
# We are already in custom, just save the settings
|
||||
self._preferences.setValue("cura/custom_visible_settings", visibility_string)
|
||||
else:
|
||||
@ -149,7 +157,12 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
else:
|
||||
item_to_set = matching_preset_item
|
||||
|
||||
# If we didn't find a matching preset, fallback to custom.
|
||||
if item_to_set is None:
|
||||
item_to_set = self._custom_preset
|
||||
|
||||
if self._active_preset_item is None or self._active_preset_item.presetId != item_to_set.presetId:
|
||||
self._active_preset_item = item_to_set
|
||||
if self._active_preset_item is not None:
|
||||
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item.presetId)
|
||||
self.activePresetChanged.emit()
|
||||
|
@ -4,6 +4,9 @@
|
||||
from typing import Dict, Optional, List, Set
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
from UM.Util import parseBool
|
||||
|
||||
from cura.Machines.ContainerNode import ContainerNode
|
||||
|
||||
|
||||
@ -29,6 +32,7 @@ class QualityGroup(QObject):
|
||||
self.nodes_for_extruders = {} # type: Dict[int, ContainerNode]
|
||||
self.quality_type = quality_type
|
||||
self.is_available = False
|
||||
self.is_experimental = False
|
||||
|
||||
@pyqtSlot(result = str)
|
||||
def getName(self) -> str:
|
||||
@ -51,3 +55,17 @@ class QualityGroup(QObject):
|
||||
for extruder_node in self.nodes_for_extruders.values():
|
||||
result.append(extruder_node)
|
||||
return result
|
||||
|
||||
def setGlobalNode(self, node: "ContainerNode") -> None:
|
||||
self.node_for_global = node
|
||||
|
||||
# Update is_experimental flag
|
||||
is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
|
||||
self.is_experimental |= is_experimental
|
||||
|
||||
def setExtruderNode(self, position: int, node: "ContainerNode") -> None:
|
||||
self.nodes_for_extruders[position] = node
|
||||
|
||||
# Update is_experimental flag
|
||||
is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
|
||||
self.is_experimental |= is_experimental
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, cast, Dict, List
|
||||
from typing import TYPE_CHECKING, Optional, cast, Dict, List, Set
|
||||
|
||||
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
|
||||
|
||||
@ -16,7 +16,7 @@ from .QualityGroup import QualityGroup
|
||||
from .QualityNode import QualityNode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from .QualityChangesGroup import QualityChangesGroup
|
||||
from cura.CuraApplication import CuraApplication
|
||||
@ -235,7 +235,7 @@ class QualityManager(QObject):
|
||||
|
||||
for quality_type, quality_node in node.quality_type_map.items():
|
||||
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
|
||||
quality_group.node_for_global = quality_node
|
||||
quality_group.setGlobalNode(quality_node)
|
||||
quality_group_dict[quality_type] = quality_group
|
||||
break
|
||||
|
||||
@ -259,11 +259,15 @@ class QualityManager(QObject):
|
||||
root_material_id = self._material_manager.getRootMaterialIDWithoutDiameter(root_material_id)
|
||||
root_material_id_list.append(root_material_id)
|
||||
|
||||
# Also try to get the fallback material
|
||||
material_type = extruder.material.getMetaDataEntry("material")
|
||||
fallback_root_material_id = self._material_manager.getFallbackMaterialIdByMaterialType(material_type)
|
||||
if fallback_root_material_id:
|
||||
root_material_id_list.append(fallback_root_material_id)
|
||||
# Also try to get the fallback materials
|
||||
fallback_ids = self._material_manager.getFallBackMaterialIdsByMaterial(extruder.material)
|
||||
|
||||
if fallback_ids:
|
||||
root_material_id_list.extend(fallback_ids)
|
||||
|
||||
# Weed out duplicates while preserving the order.
|
||||
seen = set() # type: Set[str]
|
||||
root_material_id_list = [x for x in root_material_id_list if x not in seen and not seen.add(x)] # type: ignore
|
||||
|
||||
# Here we construct a list of nodes we want to look for qualities with the highest priority first.
|
||||
# The use case is that, when we look for qualities for a machine, we first want to search in the following
|
||||
@ -333,7 +337,7 @@ class QualityManager(QObject):
|
||||
|
||||
quality_group = quality_group_dict[quality_type]
|
||||
if position not in quality_group.nodes_for_extruders:
|
||||
quality_group.nodes_for_extruders[position] = quality_node
|
||||
quality_group.setExtruderNode(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.
|
||||
@ -363,7 +367,7 @@ class QualityManager(QObject):
|
||||
if node and node.quality_type_map:
|
||||
for quality_type, quality_node in node.quality_type_map.items():
|
||||
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
|
||||
quality_group.node_for_global = quality_node
|
||||
quality_group.setGlobalNode(quality_node)
|
||||
quality_group_dict[quality_type] = quality_group
|
||||
break
|
||||
|
||||
@ -534,7 +538,7 @@ class QualityManager(QObject):
|
||||
# Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
|
||||
# shares the same set of qualities profiles as Ultimaker 3.
|
||||
#
|
||||
def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainer",
|
||||
def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainerInterface",
|
||||
default_definition_id: str = "fdmprinter") -> str:
|
||||
machine_definition_id = default_definition_id
|
||||
if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)):
|
||||
|
@ -107,7 +107,7 @@ class VariantManager:
|
||||
break
|
||||
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.get(machine_definition_id, {}).get(variant_type, {}).get(variant_name)
|
||||
|
||||
def getVariantNodes(self, machine: "GlobalStack", variant_type: "VariantType") -> Dict[str, ContainerNode]:
|
||||
machine_definition_id = machine.definition.getId()
|
||||
|
@ -81,9 +81,14 @@ class AuthorizationHelpers:
|
||||
# \param access_token: The encoded JWT token.
|
||||
# \return: Dict containing some profile data.
|
||||
def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
|
||||
try:
|
||||
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
except ConnectionError:
|
||||
# Connection was suddenly dropped. Nothing we can do about that.
|
||||
Logger.logException("e", "Something failed while attempting to parse the JWT token")
|
||||
return None
|
||||
if token_request.status_code not in (200, 201):
|
||||
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
|
||||
return None
|
||||
|
@ -83,9 +83,11 @@ class AuthorizationService:
|
||||
if not self.getUserProfile():
|
||||
# We check if we can get the user profile.
|
||||
# If we can't get it, that means the access token (JWT) was invalid or expired.
|
||||
Logger.log("w", "Unable to get the user profile.")
|
||||
return None
|
||||
|
||||
if self._auth_data is None:
|
||||
Logger.log("d", "No auth data to retrieve the access_token from")
|
||||
return None
|
||||
|
||||
return self._auth_data.access_token
|
||||
@ -120,7 +122,7 @@ class AuthorizationService:
|
||||
"redirect_uri": self._settings.CALLBACK_URL,
|
||||
"scope": self._settings.CLIENT_SCOPES,
|
||||
"response_type": "code",
|
||||
"state": "CuraDriveIsAwesome",
|
||||
"state": "(.Y.)",
|
||||
"code_challenge": challenge_code,
|
||||
"code_challenge_method": "S512"
|
||||
})
|
||||
|
@ -1,4 +1,6 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
|
@ -14,8 +14,7 @@ from UM.Logger import Logger
|
||||
from UM.Qt.Duration import Duration
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.MimeTypeDatabase import MimeTypeDatabase
|
||||
|
||||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
@ -361,7 +360,7 @@ class PrintInformation(QObject):
|
||||
try:
|
||||
mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
|
||||
data = mime_type.stripExtension(name)
|
||||
except:
|
||||
except MimeTypeNotFoundError:
|
||||
Logger.log("w", "Unsupported Mime Type Database file extension %s", name)
|
||||
|
||||
if data is not None and check_name is not None:
|
||||
@ -369,6 +368,16 @@ class PrintInformation(QObject):
|
||||
else:
|
||||
self._base_name = ""
|
||||
|
||||
# Strip the old "curaproject" extension from the name
|
||||
OLD_CURA_PROJECT_EXT = ".curaproject"
|
||||
if self._base_name.lower().endswith(OLD_CURA_PROJECT_EXT):
|
||||
self._base_name = self._base_name[:len(self._base_name) - len(OLD_CURA_PROJECT_EXT)]
|
||||
|
||||
# CURA-5896 Try to strip extra extensions with an infinite amount of ".curaproject.3mf".
|
||||
OLD_CURA_PROJECT_3MF_EXT = ".curaproject.3mf"
|
||||
while self._base_name.lower().endswith(OLD_CURA_PROJECT_3MF_EXT):
|
||||
self._base_name = self._base_name[:len(self._base_name) - len(OLD_CURA_PROJECT_3MF_EXT)]
|
||||
|
||||
self._updateJobName()
|
||||
|
||||
@pyqtProperty(str, fset = setBaseName, notify = baseNameChanged)
|
||||
@ -385,28 +394,14 @@ class PrintInformation(QObject):
|
||||
return
|
||||
active_machine_type_name = global_container_stack.definition.getName()
|
||||
|
||||
abbr_machine = ""
|
||||
for word in re.findall(r"[\w']+", active_machine_type_name):
|
||||
if word.lower() == "ultimaker":
|
||||
abbr_machine += "UM"
|
||||
elif word.isdigit():
|
||||
abbr_machine += word
|
||||
else:
|
||||
stripped_word = self._stripAccents(word.upper())
|
||||
# - use only the first character if the word is too long (> 3 characters)
|
||||
# - use the whole word if it's not too long (<= 3 characters)
|
||||
if len(stripped_word) > 3:
|
||||
stripped_word = stripped_word[0]
|
||||
abbr_machine += stripped_word
|
||||
|
||||
self._abbr_machine = abbr_machine
|
||||
self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name)
|
||||
|
||||
## Utility method that strips accents from characters (eg: â -> a)
|
||||
def _stripAccents(self, to_strip: str) -> str:
|
||||
return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')
|
||||
|
||||
@pyqtSlot(result = "QVariantMap")
|
||||
def getFeaturePrintTimes(self):
|
||||
def getFeaturePrintTimes(self) -> Dict[str, Duration]:
|
||||
result = {}
|
||||
if self._active_build_plate not in self._print_times_per_feature:
|
||||
self._initPrintTimesPerFeature(self._active_build_plate)
|
||||
|
@ -6,7 +6,7 @@ from UM.Logger import Logger
|
||||
from UM.Scene.SceneNode import SceneNode #For typing.
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType
|
||||
|
||||
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
|
||||
@ -28,8 +28,8 @@ class AuthState(IntEnum):
|
||||
class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
authenticationStateChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], parent: QObject = None) -> None:
|
||||
super().__init__(device_id = device_id, parent = parent)
|
||||
def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType = ConnectionType.NetworkConnection, parent: QObject = None) -> None:
|
||||
super().__init__(device_id = device_id, connection_type = connection_type, parent = parent)
|
||||
self._manager = None # type: Optional[QNetworkAccessManager]
|
||||
self._last_manager_create_time = None # type: Optional[float]
|
||||
self._recreate_network_manager_time = 30
|
||||
@ -125,7 +125,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
if self._connection_state_before_timeout is None:
|
||||
self._connection_state_before_timeout = self._connection_state
|
||||
|
||||
self.setConnectionState(ConnectionState.closed)
|
||||
self.setConnectionState(ConnectionState.Closed)
|
||||
|
||||
# We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to
|
||||
# sleep.
|
||||
@ -133,7 +133,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
if self._last_manager_create_time is None or time() - self._last_manager_create_time > self._recreate_network_manager_time:
|
||||
self._createNetworkManager()
|
||||
assert(self._manager is not None)
|
||||
elif self._connection_state == ConnectionState.closed:
|
||||
elif self._connection_state == ConnectionState.Closed:
|
||||
# Go out of timeout.
|
||||
if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here
|
||||
self.setConnectionState(self._connection_state_before_timeout)
|
||||
@ -147,6 +147,9 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
|
||||
return request
|
||||
|
||||
def createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
|
||||
return self._createFormPart(content_header, data, content_type)
|
||||
|
||||
def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
|
||||
part = QHttpPart()
|
||||
|
||||
@ -213,7 +216,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
request = self._createEmptyRequest(target)
|
||||
self._last_request_time = time()
|
||||
if self._manager is not None:
|
||||
reply = self._manager.post(request, data)
|
||||
reply = self._manager.post(request, data.encode())
|
||||
if on_progress is not None:
|
||||
reply.uploadProgress.connect(on_progress)
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
@ -282,8 +285,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
|
||||
self._last_response_time = time()
|
||||
|
||||
if self._connection_state == ConnectionState.connecting:
|
||||
self.setConnectionState(ConnectionState.connected)
|
||||
if self._connection_state == ConnectionState.Connecting:
|
||||
self.setConnectionState(ConnectionState.Connected)
|
||||
|
||||
callback_key = reply.url().toString() + str(reply.operation())
|
||||
try:
|
||||
|
@ -118,17 +118,40 @@ class PrintJobOutputModel(QObject):
|
||||
self.nameChanged.emit()
|
||||
|
||||
@pyqtProperty(int, notify = timeTotalChanged)
|
||||
def timeTotal(self):
|
||||
def timeTotal(self) -> int:
|
||||
return self._time_total
|
||||
|
||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||
def timeElapsed(self):
|
||||
def timeElapsed(self) -> int:
|
||||
return self._time_elapsed
|
||||
|
||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||
def timeRemaining(self) -> int:
|
||||
# Never get a negative time remaining
|
||||
return max(self.timeTotal - self.timeElapsed, 0)
|
||||
|
||||
@pyqtProperty(float, notify = timeElapsedChanged)
|
||||
def progress(self) -> float:
|
||||
result = self.timeElapsed / self.timeTotal
|
||||
# Never get a progress past 1.0
|
||||
return min(result, 1.0)
|
||||
|
||||
@pyqtProperty(str, notify=stateChanged)
|
||||
def state(self):
|
||||
def state(self) -> str:
|
||||
return self._state
|
||||
|
||||
@pyqtProperty(bool, notify=stateChanged)
|
||||
def isActive(self) -> bool:
|
||||
inactiveStates = [
|
||||
"pausing",
|
||||
"paused",
|
||||
"resuming",
|
||||
"wait_cleanup"
|
||||
]
|
||||
if self.state in inactiveStates and self.timeRemaining > 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
def updateTimeTotal(self, new_time_total):
|
||||
if self._time_total != new_time_total:
|
||||
self._time_total = new_time_total
|
||||
|
@ -4,7 +4,7 @@
|
||||
from UM.Decorators import deprecated
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl, Q_ENUMS
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from UM.Logger import Logger
|
||||
@ -28,11 +28,18 @@ i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
## The current processing state of the backend.
|
||||
class ConnectionState(IntEnum):
|
||||
closed = 0
|
||||
connecting = 1
|
||||
connected = 2
|
||||
busy = 3
|
||||
error = 4
|
||||
Closed = 0
|
||||
Connecting = 1
|
||||
Connected = 2
|
||||
Busy = 3
|
||||
Error = 4
|
||||
|
||||
|
||||
class ConnectionType(IntEnum):
|
||||
NotConnected = 0
|
||||
UsbConnection = 1
|
||||
NetworkConnection = 2
|
||||
CloudConnection = 3
|
||||
|
||||
|
||||
## Printer output device adds extra interface options on top of output device.
|
||||
@ -46,6 +53,11 @@ class ConnectionState(IntEnum):
|
||||
# For all other uses it should be used in the same way as a "regular" OutputDevice.
|
||||
@signalemitter
|
||||
class PrinterOutputDevice(QObject, OutputDevice):
|
||||
|
||||
# Put ConnectionType here with Q_ENUMS() so it can be registered as a QML type and accessible via QML, and there is
|
||||
# no need to remember what those Enum integer values mean.
|
||||
Q_ENUMS(ConnectionType)
|
||||
|
||||
printersChanged = pyqtSignal()
|
||||
connectionStateChanged = pyqtSignal(str)
|
||||
acceptsCommandsChanged = pyqtSignal()
|
||||
@ -62,7 +74,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
# Signal to indicate that the configuration of one of the printers has changed.
|
||||
uniqueConfigurationsChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, device_id: str, parent: QObject = None) -> None:
|
||||
def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, parent: QObject = None) -> None:
|
||||
super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance
|
||||
|
||||
self._printers = [] # type: List[PrinterOutputModel]
|
||||
@ -83,7 +95,8 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
self._update_timer.setSingleShot(False)
|
||||
self._update_timer.timeout.connect(self._update)
|
||||
|
||||
self._connection_state = ConnectionState.closed #type: ConnectionState
|
||||
self._connection_state = ConnectionState.Closed #type: ConnectionState
|
||||
self._connection_type = connection_type
|
||||
|
||||
self._firmware_updater = None #type: Optional[FirmwareUpdater]
|
||||
self._firmware_name = None #type: Optional[str]
|
||||
@ -110,15 +123,18 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
callback(QMessageBox.Yes)
|
||||
|
||||
def isConnected(self) -> bool:
|
||||
return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error
|
||||
return self._connection_state != ConnectionState.Closed and self._connection_state != ConnectionState.Error
|
||||
|
||||
def setConnectionState(self, connection_state: ConnectionState) -> None:
|
||||
def setConnectionState(self, connection_state: "ConnectionState") -> None:
|
||||
if self._connection_state != connection_state:
|
||||
self._connection_state = connection_state
|
||||
self.connectionStateChanged.emit(self._id)
|
||||
|
||||
def getConnectionType(self) -> "ConnectionType":
|
||||
return self._connection_type
|
||||
|
||||
@pyqtProperty(str, notify = connectionStateChanged)
|
||||
def connectionState(self) -> ConnectionState:
|
||||
def connectionState(self) -> "ConnectionState":
|
||||
return self._connection_state
|
||||
|
||||
def _update(self) -> None:
|
||||
@ -174,13 +190,13 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
|
||||
## Attempt to establish connection
|
||||
def connect(self) -> None:
|
||||
self.setConnectionState(ConnectionState.connecting)
|
||||
self.setConnectionState(ConnectionState.Connecting)
|
||||
self._update_timer.start()
|
||||
|
||||
## Attempt to close the connection
|
||||
def close(self) -> None:
|
||||
self._update_timer.stop()
|
||||
self.setConnectionState(ConnectionState.closed)
|
||||
self.setConnectionState(ConnectionState.Closed)
|
||||
|
||||
## Ensure that close gets called when object is destroyed
|
||||
def __del__(self) -> None:
|
||||
@ -211,6 +227,11 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
self._unique_configurations.sort(key = lambda k: k.printerType)
|
||||
self.uniqueConfigurationsChanged.emit()
|
||||
|
||||
# Returns the unique configurations of the printers within this output device
|
||||
@pyqtProperty("QStringList", notify = uniqueConfigurationsChanged)
|
||||
def uniquePrinterTypes(self) -> List[str]:
|
||||
return list(sorted(set([configuration.printerType for configuration in self._unique_configurations])))
|
||||
|
||||
def _onPrintersChanged(self) -> None:
|
||||
for printer in self._printers:
|
||||
printer.configurationChanged.connect(self._updateUniqueConfigurations)
|
||||
|
@ -142,6 +142,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
controller = Application.getInstance().getController()
|
||||
root = controller.getScene().getRoot()
|
||||
if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node):
|
||||
# If the tool operation is still active, we need to compute the convex hull later after the controller is
|
||||
# no longer active.
|
||||
if controller.isToolOperationActive():
|
||||
self.recomputeConvexHullDelayed()
|
||||
return
|
||||
|
||||
if self._convex_hull_node:
|
||||
self._convex_hull_node.setParent(None)
|
||||
self._convex_hull_node = None
|
||||
@ -181,7 +187,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
for child in self._node.getChildren():
|
||||
child_hull = child.callDecoration("_compute2DConvexHull")
|
||||
if child_hull:
|
||||
try:
|
||||
points = numpy.append(points, child_hull.getPoints(), axis = 0)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if points.size < 3:
|
||||
return None
|
||||
@ -266,7 +275,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
|
||||
|
||||
# Min head hull is used for the push free
|
||||
convex_hull = self._compute2DConvexHeadFull()
|
||||
convex_hull = self._compute2DConvexHull()
|
||||
if convex_hull:
|
||||
return convex_hull.getMinkowskiHull(head_and_fans)
|
||||
return None
|
||||
|
@ -1,7 +1,10 @@
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Math.Polygon import Polygon
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Resources import Resources
|
||||
from UM.Math.Color import Color
|
||||
@ -16,7 +19,7 @@ class ConvexHullNode(SceneNode):
|
||||
# location an object uses on the buildplate. This area (or area's in case of one at a time printing) is
|
||||
# then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
|
||||
# to represent the raft as well.
|
||||
def __init__(self, node, hull, thickness, parent = None):
|
||||
def __init__(self, node: SceneNode, hull: Optional[Polygon], thickness: float, parent: Optional[SceneNode] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.setCalculateBoundingBox(False)
|
||||
@ -25,7 +28,11 @@ class ConvexHullNode(SceneNode):
|
||||
|
||||
# Color of the drawn convex hull
|
||||
if not Application.getInstance().getIsHeadLess():
|
||||
self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb())
|
||||
theme = QtApplication.getInstance().getTheme()
|
||||
if theme:
|
||||
self._color = Color(*theme.getColor("convex_hull").getRgb())
|
||||
else:
|
||||
self._color = Color(0, 0, 0)
|
||||
else:
|
||||
self._color = Color(0, 0, 0)
|
||||
|
||||
@ -75,7 +82,7 @@ class ConvexHullNode(SceneNode):
|
||||
|
||||
return True
|
||||
|
||||
def _onNodeDecoratorsChanged(self, node):
|
||||
def _onNodeDecoratorsChanged(self, node: SceneNode) -> None:
|
||||
convex_hull_head = self._node.callDecoration("getConvexHullHead")
|
||||
if convex_hull_head:
|
||||
convex_hull_head_builder = MeshBuilder()
|
||||
|
@ -420,12 +420,12 @@ class ContainerManager(QObject):
|
||||
|
||||
## Import single profile, file_url does not have to end with curaprofile
|
||||
@pyqtSlot(QUrl, result = "QVariantMap")
|
||||
def importProfile(self, file_url: QUrl):
|
||||
def importProfile(self, file_url: QUrl) -> Dict[str, str]:
|
||||
if not file_url.isValid():
|
||||
return
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
|
||||
path = file_url.toLocalFile()
|
||||
if not path:
|
||||
return
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
|
||||
return self._container_registry.importProfile(path)
|
||||
|
||||
@pyqtSlot(QObject, QUrl, str)
|
||||
|
@ -5,12 +5,12 @@ import os
|
||||
import re
|
||||
import configparser
|
||||
|
||||
from typing import cast, Optional
|
||||
|
||||
from typing import cast, Dict, Optional
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from UM.Decorators import override
|
||||
from UM.Settings.ContainerFormatError import ContainerFormatError
|
||||
from UM.Settings.Interfaces import ContainerInterface
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
@ -28,7 +28,7 @@ from . import GlobalStack
|
||||
|
||||
import cura.CuraApplication
|
||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||
from cura.ReaderWriters.ProfileReader import NoProfileException
|
||||
from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
@ -161,20 +161,20 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
|
||||
## Imports a profile from a file
|
||||
#
|
||||
# \param file_name \type{str} the full path and filename of the profile to import
|
||||
# \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key
|
||||
# containing a message for the user
|
||||
def importProfile(self, file_name):
|
||||
# \param file_name The full path and filename of the profile to import.
|
||||
# \return Dict with a 'status' key containing the string 'ok' or 'error',
|
||||
# and a 'message' key containing a message for the user.
|
||||
def importProfile(self, file_name: str) -> Dict[str, str]:
|
||||
Logger.log("d", "Attempting to import profile %s", file_name)
|
||||
if not file_name:
|
||||
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "Invalid path")}
|
||||
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>: {1}", file_name, "Invalid path")}
|
||||
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
extension = file_name.split(".")[-1]
|
||||
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Can't import profile from <filename>{0}</filename> before a printer is added.", file_name)}
|
||||
|
||||
machine_extruders = []
|
||||
for position in sorted(global_stack.extruders):
|
||||
@ -183,7 +183,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
||||
if meta_data["profile_reader"][0]["extension"] != extension:
|
||||
continue
|
||||
profile_reader = plugin_registry.getPluginObject(plugin_id)
|
||||
profile_reader = cast(ProfileReader, plugin_registry.getPluginObject(plugin_id))
|
||||
try:
|
||||
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
|
||||
except NoProfileException:
|
||||
@ -221,13 +221,13 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
# Make sure we have a profile_definition in the file:
|
||||
if profile_definition is None:
|
||||
break
|
||||
machine_definition = self.findDefinitionContainers(id = profile_definition)
|
||||
if not machine_definition:
|
||||
machine_definitions = self.findDefinitionContainers(id = profile_definition)
|
||||
if not machine_definitions:
|
||||
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
|
||||
return {"status": "error",
|
||||
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)
|
||||
}
|
||||
machine_definition = machine_definition[0]
|
||||
machine_definition = machine_definitions[0]
|
||||
|
||||
# Get the expected machine definition.
|
||||
# i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode...
|
||||
@ -274,6 +274,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
setting_value = global_profile.getProperty(qc_setting_key, "value")
|
||||
|
||||
setting_definition = global_stack.getSettingDefinition(qc_setting_key)
|
||||
if setting_definition is not None:
|
||||
new_instance = SettingInstance(setting_definition, profile)
|
||||
new_instance.setProperty("value", setting_value)
|
||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||
@ -290,7 +291,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
for profile_index, profile in enumerate(profile_or_list):
|
||||
if profile_index == 0:
|
||||
# This is assumed to be the global profile
|
||||
profile_id = (global_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
|
||||
profile_id = (cast(ContainerInterface, global_stack.getBottom()).getId() + "_" + name_seed).lower().replace(" ", "_")
|
||||
|
||||
elif profile_index < len(machine_extruders) + 1:
|
||||
# This is assumed to be an extruder profile
|
||||
|
@ -5,6 +5,7 @@ from typing import Any, List, Optional, TYPE_CHECKING
|
||||
|
||||
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from UM.Logger import Logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
@ -38,7 +39,11 @@ class CuraFormulaFunctions:
|
||||
extruder_position = int(machine_manager.defaultExtruderPosition)
|
||||
|
||||
global_stack = machine_manager.activeMachine
|
||||
try:
|
||||
extruder_stack = global_stack.extruders[str(extruder_position)]
|
||||
except KeyError:
|
||||
Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available" % (property_key, extruder_position))
|
||||
return None
|
||||
|
||||
value = extruder_stack.getRawProperty(property_key, "value", context = context)
|
||||
if isinstance(value, SettingFunction):
|
||||
|
@ -63,7 +63,7 @@ class ExtruderManager(QObject):
|
||||
if not self._application.getGlobalContainerStack():
|
||||
return None # No active machine, so no active extruder.
|
||||
try:
|
||||
return self._extruder_trains[self._application.getGlobalContainerStack().getId()][str(self._active_extruder_index)].getId()
|
||||
return self._extruder_trains[self._application.getGlobalContainerStack().getId()][str(self.activeExtruderIndex)].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
|
||||
|
||||
@ -83,6 +83,7 @@ class ExtruderManager(QObject):
|
||||
# \param index The index of the new active extruder.
|
||||
@pyqtSlot(int)
|
||||
def setActiveExtruderIndex(self, index: int) -> None:
|
||||
if self._active_extruder_index != index:
|
||||
self._active_extruder_index = index
|
||||
self.activeExtruderChanged.emit()
|
||||
|
||||
@ -144,7 +145,7 @@ class ExtruderManager(QObject):
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
|
||||
return self.getExtruderStack(self._active_extruder_index)
|
||||
return self.getExtruderStack(self.activeExtruderIndex)
|
||||
|
||||
## Get an extruder stack by index
|
||||
def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
|
||||
@ -300,12 +301,7 @@ class ExtruderManager(QObject):
|
||||
global_stack = self._application.getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return []
|
||||
|
||||
result_tuple_list = sorted(list(global_stack.extruders.items()), key = lambda x: int(x[0]))
|
||||
result_list = [item[1] for item in result_tuple_list]
|
||||
|
||||
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
|
||||
return result_list[:machine_extruder_count]
|
||||
return global_stack.extruderList
|
||||
|
||||
def _globalContainerStackChanged(self) -> None:
|
||||
# If the global container changed, the machine changed and might have extruders that were not registered yet
|
||||
@ -344,6 +340,7 @@ class ExtruderManager(QObject):
|
||||
if extruders_changed:
|
||||
self.extrudersChanged.emit(global_stack_id)
|
||||
self.setActiveExtruderIndex(0)
|
||||
self.activeExtruderChanged.emit()
|
||||
|
||||
# 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.
|
||||
|
@ -52,8 +52,8 @@ class ExtruderStack(CuraContainerStack):
|
||||
return super().getNextStack()
|
||||
|
||||
def setEnabled(self, enabled: bool) -> None:
|
||||
if "enabled" not in self._metadata:
|
||||
self.setMetaDataEntry("enabled", "True")
|
||||
if self.getMetaDataEntry("enabled", True) == enabled: # No change.
|
||||
return # Don't emit a signal then.
|
||||
self.setMetaDataEntry("enabled", str(enabled))
|
||||
self.enabledChanged.emit()
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, pyqtProperty, QTimer
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer
|
||||
from typing import Iterable
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
@ -24,8 +24,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
|
||||
## Human-readable name of the extruder.
|
||||
NameRole = Qt.UserRole + 2
|
||||
## Is the extruder enabled?
|
||||
EnabledRole = Qt.UserRole + 9
|
||||
|
||||
## Colour of the material loaded in the extruder.
|
||||
ColorRole = Qt.UserRole + 3
|
||||
@ -47,6 +45,12 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
VariantRole = Qt.UserRole + 7
|
||||
StackRole = Qt.UserRole + 8
|
||||
|
||||
MaterialBrandRole = Qt.UserRole + 9
|
||||
ColorNameRole = Qt.UserRole + 10
|
||||
|
||||
## Is the extruder enabled?
|
||||
EnabledRole = Qt.UserRole + 11
|
||||
|
||||
## List of colours to display if there is no material or the material has no known
|
||||
# colour.
|
||||
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
|
||||
@ -67,14 +71,13 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
self.addRoleName(self.MaterialRole, "material")
|
||||
self.addRoleName(self.VariantRole, "variant")
|
||||
self.addRoleName(self.StackRole, "stack")
|
||||
|
||||
self.addRoleName(self.MaterialBrandRole, "material_brand")
|
||||
self.addRoleName(self.ColorNameRole, "color_name")
|
||||
self._update_extruder_timer = QTimer()
|
||||
self._update_extruder_timer.setInterval(100)
|
||||
self._update_extruder_timer.setSingleShot(True)
|
||||
self._update_extruder_timer.timeout.connect(self.__updateExtruders)
|
||||
|
||||
self._simple_names = False
|
||||
|
||||
self._active_machine_extruders = [] # type: Iterable[ExtruderStack]
|
||||
self._add_optional_extruder = False
|
||||
|
||||
@ -96,21 +99,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
def addOptionalExtruder(self):
|
||||
return self._add_optional_extruder
|
||||
|
||||
## Set the simpleNames property.
|
||||
def setSimpleNames(self, simple_names):
|
||||
if simple_names != self._simple_names:
|
||||
self._simple_names = simple_names
|
||||
self.simpleNamesChanged.emit()
|
||||
self._updateExtruders()
|
||||
|
||||
## Emitted when the simpleNames property changes.
|
||||
simpleNamesChanged = pyqtSignal()
|
||||
|
||||
## Whether or not the model should show all definitions regardless of visibility.
|
||||
@pyqtProperty(bool, fset = setSimpleNames, notify = simpleNamesChanged)
|
||||
def simpleNames(self):
|
||||
return self._simple_names
|
||||
|
||||
## Links to the stack-changed signal of the new extruders when an extruder
|
||||
# is swapped out or added in the current machine.
|
||||
#
|
||||
@ -160,7 +148,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
def __updateExtruders(self):
|
||||
extruders_changed = False
|
||||
|
||||
if self.rowCount() != 0:
|
||||
if self.count != 0:
|
||||
extruders_changed = True
|
||||
|
||||
items = []
|
||||
@ -172,7 +160,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
|
||||
|
||||
for extruder in Application.getInstance().getExtruderManager().getActiveExtruderStacks():
|
||||
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
|
||||
position = extruder.getMetaDataEntry("position", default = "0")
|
||||
try:
|
||||
position = int(position)
|
||||
except ValueError:
|
||||
@ -183,7 +171,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
|
||||
default_color = self.defaultColors[position] if 0 <= position < len(self.defaultColors) else self.defaultColors[0]
|
||||
color = extruder.material.getMetaDataEntry("color_code", default = default_color) if extruder.material else default_color
|
||||
|
||||
material_brand = extruder.material.getMetaDataEntry("brand", default = "generic")
|
||||
color_name = extruder.material.getMetaDataEntry("color_name")
|
||||
# construct an item with only the relevant information
|
||||
item = {
|
||||
"id": extruder.getId(),
|
||||
@ -195,6 +184,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
"material": extruder.material.getName() if extruder.material else "",
|
||||
"variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core
|
||||
"stack": extruder,
|
||||
"material_brand": material_brand,
|
||||
"color_name": color_name
|
||||
}
|
||||
|
||||
items.append(item)
|
||||
@ -213,9 +204,14 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
"enabled": True,
|
||||
"color": "#ffffff",
|
||||
"index": -1,
|
||||
"definition": ""
|
||||
"definition": "",
|
||||
"material": "",
|
||||
"variant": "",
|
||||
"stack": None,
|
||||
"material_brand": "",
|
||||
"color_name": "",
|
||||
}
|
||||
items.append(item)
|
||||
|
||||
if self._items != items:
|
||||
self.setItems(items)
|
||||
self.modelChanged.emit()
|
||||
|
@ -3,8 +3,8 @@
|
||||
|
||||
from collections import defaultdict
|
||||
import threading
|
||||
from typing import Any, Dict, Optional, Set, TYPE_CHECKING
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSlot
|
||||
from typing import Any, Dict, Optional, Set, TYPE_CHECKING, List
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
|
||||
|
||||
from UM.Decorators import override
|
||||
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||
@ -42,13 +42,23 @@ class GlobalStack(CuraContainerStack):
|
||||
# Per thread we have our own resolving_settings, or strange things sometimes occur.
|
||||
self._resolving_settings = defaultdict(set) #type: Dict[str, Set[str]] # keys are thread names
|
||||
|
||||
extrudersChanged = pyqtSignal()
|
||||
|
||||
## Get the list of extruders of this stack.
|
||||
#
|
||||
# \return The extruders registered with this stack.
|
||||
@pyqtProperty("QVariantMap")
|
||||
@pyqtProperty("QVariantMap", notify = extrudersChanged)
|
||||
def extruders(self) -> Dict[str, "ExtruderStack"]:
|
||||
return self._extruders
|
||||
|
||||
@pyqtProperty("QVariantList", notify = extrudersChanged)
|
||||
def extruderList(self) -> List["ExtruderStack"]:
|
||||
result_tuple_list = sorted(list(self.extruders.items()), key=lambda x: int(x[0]))
|
||||
result_list = [item[1] for item in result_tuple_list]
|
||||
|
||||
machine_extruder_count = self.getProperty("machine_extruder_count", "value")
|
||||
return result_list[:machine_extruder_count]
|
||||
|
||||
@classmethod
|
||||
def getLoadingPriority(cls) -> int:
|
||||
return 2
|
||||
@ -87,6 +97,7 @@ class GlobalStack(CuraContainerStack):
|
||||
return
|
||||
|
||||
self._extruders[position] = extruder
|
||||
self.extrudersChanged.emit()
|
||||
Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)
|
||||
|
||||
## Overridden from ContainerStack
|
||||
|
@ -3,6 +3,8 @@
|
||||
|
||||
import collections
|
||||
import time
|
||||
import re
|
||||
import unicodedata
|
||||
from typing import Any, Callable, List, Dict, TYPE_CHECKING, Optional, cast
|
||||
|
||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||
@ -21,7 +23,7 @@ from UM.Settings.SettingFunction import SettingFunction
|
||||
from UM.Signal import postponeSignals, CompressTechnique
|
||||
|
||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionType
|
||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||
@ -62,8 +64,6 @@ class MachineManager(QObject):
|
||||
|
||||
self._default_extruder_position = "0" # to be updated when extruders are switched on and off
|
||||
|
||||
self.machine_extruder_material_update_dict = collections.defaultdict(list) #type: Dict[str, List[Callable[[], None]]]
|
||||
|
||||
self._instance_container_timer = QTimer() # type: QTimer
|
||||
self._instance_container_timer.setInterval(250)
|
||||
self._instance_container_timer.setSingleShot(True)
|
||||
@ -74,7 +74,7 @@ class MachineManager(QObject):
|
||||
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._container_registry.containerLoadComplete.connect(self._onContainersChanged)
|
||||
|
||||
## When the global container is changed, active material probably needs to be updated.
|
||||
# When the global container is changed, active material probably needs to be updated.
|
||||
self.globalContainerChanged.connect(self.activeMaterialChanged)
|
||||
self.globalContainerChanged.connect(self.activeVariantChanged)
|
||||
self.globalContainerChanged.connect(self.activeQualityChanged)
|
||||
@ -86,12 +86,14 @@ class MachineManager(QObject):
|
||||
|
||||
self._onGlobalContainerChanged()
|
||||
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
|
||||
extruder_manager = self._application.getExtruderManager()
|
||||
|
||||
extruder_manager.activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
|
||||
self._onActiveExtruderStackChanged()
|
||||
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeMaterialChanged)
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeVariantChanged)
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeQualityChanged)
|
||||
extruder_manager.activeExtruderChanged.connect(self.activeMaterialChanged)
|
||||
extruder_manager.activeExtruderChanged.connect(self.activeVariantChanged)
|
||||
extruder_manager.activeExtruderChanged.connect(self.activeQualityChanged)
|
||||
|
||||
self.globalContainerChanged.connect(self.activeStackChanged)
|
||||
self.globalValueChanged.connect(self.activeStackValueChanged)
|
||||
@ -201,7 +203,7 @@ class MachineManager(QObject):
|
||||
extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != empty_variant_container else None
|
||||
self._current_printer_configuration.extruderConfigurations.append(extruder_configuration)
|
||||
|
||||
# an empty build plate configuration from the network printer is presented as an empty string, so use "" for an
|
||||
# An empty build plate configuration from the network printer is presented as an empty string, so use "" for an
|
||||
# empty build plate.
|
||||
self._current_printer_configuration.buildplateConfiguration = self._global_container_stack.getProperty("machine_buildplate_type", "value") if self._global_container_stack.variant != empty_variant_container else ""
|
||||
self.currentConfigurationChanged.emit()
|
||||
@ -247,7 +249,7 @@ class MachineManager(QObject):
|
||||
self.updateNumberExtrudersEnabled()
|
||||
self.globalContainerChanged.emit()
|
||||
|
||||
# after switching the global stack we reconnect all the signals and set the variant and material references
|
||||
# After switching the global stack we reconnect all the signals and set the variant and material references
|
||||
if self._global_container_stack:
|
||||
self._application.getPreferences().setValue("cura/active_machine", self._global_container_stack.getId())
|
||||
|
||||
@ -261,7 +263,7 @@ class MachineManager(QObject):
|
||||
if global_variant.getMetaDataEntry("hardware_type") != "buildplate":
|
||||
self._global_container_stack.setVariant(empty_variant_container)
|
||||
|
||||
# set the global material to empty as we now use the extruder stack at all times - CURA-4482
|
||||
# Set the global material to empty as we now use the extruder stack at all times - CURA-4482
|
||||
global_material = self._global_container_stack.material
|
||||
if global_material != empty_material_container:
|
||||
self._global_container_stack.setMaterial(empty_material_container)
|
||||
@ -271,11 +273,6 @@ class MachineManager(QObject):
|
||||
extruder_stack.propertyChanged.connect(self._onPropertyChanged)
|
||||
extruder_stack.containersChanged.connect(self._onContainersChanged)
|
||||
|
||||
if self._global_container_stack.getId() in self.machine_extruder_material_update_dict:
|
||||
for func in self.machine_extruder_material_update_dict[self._global_container_stack.getId()]:
|
||||
self._application.callLater(func)
|
||||
del self.machine_extruder_material_update_dict[self._global_container_stack.getId()]
|
||||
|
||||
self.activeQualityGroupChanged.emit()
|
||||
|
||||
def _onActiveExtruderStackChanged(self) -> None:
|
||||
@ -295,6 +292,7 @@ class MachineManager(QObject):
|
||||
self.activeMaterialChanged.emit()
|
||||
|
||||
self.rootMaterialChanged.emit()
|
||||
self.numberExtrudersEnabledChanged.emit()
|
||||
|
||||
def _onContainersChanged(self, container: ContainerInterface) -> None:
|
||||
self._instance_container_timer.start()
|
||||
@ -419,7 +417,7 @@ class MachineManager(QObject):
|
||||
# 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")
|
||||
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:
|
||||
md = stack.getMetaData()
|
||||
if "position" in md and int(md["position"]) >= machine_extruder_count:
|
||||
@ -438,12 +436,12 @@ class MachineManager(QObject):
|
||||
if not self._global_container_stack:
|
||||
return False
|
||||
|
||||
if self._global_container_stack.getTop().findInstances():
|
||||
if self._global_container_stack.getTop().getNumInstances() != 0:
|
||||
return True
|
||||
|
||||
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
for stack in stacks:
|
||||
if stack.getTop().findInstances():
|
||||
if stack.getTop().getNumInstances() != 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
@ -453,10 +451,10 @@ class MachineManager(QObject):
|
||||
if not self._global_container_stack:
|
||||
return 0
|
||||
num_user_settings = 0
|
||||
num_user_settings += len(self._global_container_stack.getTop().findInstances())
|
||||
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
num_user_settings += self._global_container_stack.getTop().getNumInstances()
|
||||
stacks = self._global_container_stack.extruderList
|
||||
for stack in stacks:
|
||||
num_user_settings += len(stack.getTop().findInstances())
|
||||
num_user_settings += stack.getTop().getNumInstances()
|
||||
return num_user_settings
|
||||
|
||||
## Delete a user setting from the global stack and all extruder stacks.
|
||||
@ -519,7 +517,13 @@ class MachineManager(QObject):
|
||||
def printerConnected(self):
|
||||
return bool(self._printer_output_devices)
|
||||
|
||||
@pyqtProperty(str, notify = printerConnectedStatusChanged)
|
||||
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||
def activeMachineHasRemoteConnection(self) -> bool:
|
||||
if self._global_container_stack:
|
||||
connection_type = int(self._global_container_stack.getMetaDataEntry("connection_type", ConnectionType.NotConnected.value))
|
||||
return connection_type in [ConnectionType.NetworkConnection.value, ConnectionType.CloudConnection.value]
|
||||
return False
|
||||
|
||||
def activeMachineNetworkKey(self) -> str:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.getMetaDataEntry("um_network_key", "")
|
||||
@ -616,6 +620,14 @@ class MachineManager(QObject):
|
||||
is_supported = self._current_quality_group.is_available
|
||||
return is_supported
|
||||
|
||||
@pyqtProperty(bool, notify = activeQualityGroupChanged)
|
||||
def isActiveQualityExperimental(self) -> bool:
|
||||
is_experimental = False
|
||||
if self._global_container_stack:
|
||||
if self._current_quality_group:
|
||||
is_experimental = self._current_quality_group.is_experimental
|
||||
return is_experimental
|
||||
|
||||
## Returns whether there is anything unsupported in the current set-up.
|
||||
#
|
||||
# The current set-up signifies the global stack and all extruder stacks,
|
||||
@ -646,7 +658,7 @@ class MachineManager(QObject):
|
||||
new_value = self._active_container_stack.getProperty(key, "value")
|
||||
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:
|
||||
if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
|
||||
extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved
|
||||
@ -662,7 +674,7 @@ class MachineManager(QObject):
|
||||
for key in self._active_container_stack.userChanges.getAllKeys():
|
||||
new_value = self._active_container_stack.getProperty(key, "value")
|
||||
|
||||
# check if the value has to be replaced
|
||||
# Check if the value has to be replaced
|
||||
extruder_stack.userChanges.setProperty(key, "value", new_value)
|
||||
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
@ -731,7 +743,7 @@ class MachineManager(QObject):
|
||||
# If the machine that is being removed is the currently active machine, set another machine as the active machine.
|
||||
activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id)
|
||||
|
||||
# activate a new machine before removing a machine because this is safer
|
||||
# Activate a new machine before removing a machine because this is safer
|
||||
if activate_new_machine:
|
||||
machine_stacks = CuraContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine")
|
||||
other_machine_stacks = [s for s in machine_stacks if s["id"] != machine_id]
|
||||
@ -739,7 +751,7 @@ class MachineManager(QObject):
|
||||
self.setActiveMachine(other_machine_stacks[0]["id"])
|
||||
|
||||
metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0]
|
||||
network_key = metadata["um_network_key"] if "um_network_key" in metadata else None
|
||||
network_key = metadata.get("um_network_key", None)
|
||||
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
||||
containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
|
||||
for container in containers:
|
||||
@ -909,21 +921,18 @@ class MachineManager(QObject):
|
||||
# After CURA-4482 this should not be the case anymore, but we still want to support older project files.
|
||||
global_user_container = self._global_container_stack.userChanges
|
||||
|
||||
# Make sure extruder_stacks exists
|
||||
extruder_stacks = [] #type: List[ExtruderStack]
|
||||
|
||||
if previous_extruder_count == 1:
|
||||
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
global_user_container = self._global_container_stack.userChanges
|
||||
|
||||
for setting_instance in global_user_container.findInstances():
|
||||
setting_key = setting_instance.definition.key
|
||||
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
|
||||
|
||||
if settable_per_extruder:
|
||||
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
|
||||
extruder_stack = extruder_stacks[max(0, limit_to_extruder)]
|
||||
extruder_position = max(0, limit_to_extruder)
|
||||
extruder_stack = self.getExtruder(extruder_position)
|
||||
if extruder_stack:
|
||||
extruder_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
|
||||
else:
|
||||
Logger.log("e", "Unable to find extruder on position %s", extruder_position)
|
||||
global_user_container.removeInstance(setting_key)
|
||||
|
||||
# Signal that the global stack has changed
|
||||
@ -932,10 +941,9 @@ class MachineManager(QObject):
|
||||
|
||||
@pyqtSlot(int, result = QObject)
|
||||
def getExtruder(self, position: int) -> Optional[ExtruderStack]:
|
||||
extruder = None
|
||||
if self._global_container_stack:
|
||||
extruder = self._global_container_stack.extruders.get(str(position))
|
||||
return extruder
|
||||
return self._global_container_stack.extruders.get(str(position))
|
||||
return None
|
||||
|
||||
def updateDefaultExtruder(self) -> None:
|
||||
if self._global_container_stack is None:
|
||||
@ -1001,12 +1009,12 @@ class MachineManager(QObject):
|
||||
if not enabled and position == ExtruderManager.getInstance().activeExtruderIndex:
|
||||
ExtruderManager.getInstance().setActiveExtruderIndex(int(self._default_extruder_position))
|
||||
|
||||
# ensure that the quality profile is compatible with current combination, or choose a compatible one if available
|
||||
# Ensure that the quality profile is compatible with current combination, or choose a compatible one if available
|
||||
self._updateQualityWithMaterial()
|
||||
self.extruderChanged.emit()
|
||||
# update material compatibility color
|
||||
# Update material compatibility color
|
||||
self.activeQualityGroupChanged.emit()
|
||||
# update items in SettingExtruder
|
||||
# Update items in SettingExtruder
|
||||
ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId())
|
||||
# Make sure the front end reflects changes
|
||||
self.forceUpdateAllSettings()
|
||||
@ -1080,7 +1088,6 @@ class MachineManager(QObject):
|
||||
|
||||
return result
|
||||
|
||||
#
|
||||
# Sets all quality and quality_changes containers to empty_quality and empty_quality_changes containers
|
||||
# for all stacks in the currently active machine.
|
||||
#
|
||||
@ -1209,7 +1216,7 @@ class MachineManager(QObject):
|
||||
self.rootMaterialChanged.emit()
|
||||
|
||||
def activeMaterialsCompatible(self) -> bool:
|
||||
# check material - variant compatibility
|
||||
# Check material - variant compatibility
|
||||
if self._global_container_stack is not None:
|
||||
if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)):
|
||||
for position, extruder in self._global_container_stack.extruders.items():
|
||||
@ -1310,17 +1317,18 @@ class MachineManager(QObject):
|
||||
# Get the definition id corresponding to this machine name
|
||||
machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()
|
||||
# Try to find a machine with the same network key
|
||||
new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey})
|
||||
new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey()})
|
||||
# If there is no machine, then create a new one and set it to the non-hidden instance
|
||||
if not new_machine:
|
||||
new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id)
|
||||
if not new_machine:
|
||||
return
|
||||
new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey)
|
||||
new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey())
|
||||
new_machine.setMetaDataEntry("connect_group_name", self.activeMachineNetworkGroupName)
|
||||
new_machine.setMetaDataEntry("hidden", False)
|
||||
new_machine.setMetaDataEntry("connection_type", self._global_container_stack.getMetaDataEntry("connection_type"))
|
||||
else:
|
||||
Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey)
|
||||
Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey())
|
||||
new_machine.setMetaDataEntry("hidden", False)
|
||||
|
||||
# Set the current printer instance to hidden (the metadata entry must exist)
|
||||
@ -1380,10 +1388,10 @@ class MachineManager(QObject):
|
||||
# After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group
|
||||
# then all the container stacks are updated, both the current and the hidden ones.
|
||||
def checkCorrectGroupName(self, device_id: str, group_name: str) -> None:
|
||||
if self._global_container_stack and device_id == self.activeMachineNetworkKey:
|
||||
if self._global_container_stack and device_id == self.activeMachineNetworkKey():
|
||||
# Check if the connect_group_name is correct. If not, update all the containers connected to the same printer
|
||||
if self.activeMachineNetworkGroupName != group_name:
|
||||
metadata_filter = {"um_network_key": self.activeMachineNetworkKey}
|
||||
metadata_filter = {"um_network_key": self.activeMachineNetworkKey()}
|
||||
containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
||||
for container in containers:
|
||||
container.setMetaDataEntry("connect_group_name", group_name)
|
||||
@ -1419,7 +1427,7 @@ class MachineManager(QObject):
|
||||
material_diameter, root_material_id)
|
||||
self.setMaterial(position, material_node)
|
||||
|
||||
## global_stack: if you want to provide your own global_stack instead of the current active one
|
||||
## Global_stack: if you want to provide your own global_stack instead of the current active one
|
||||
# if you update an active machine, special measures have to be taken.
|
||||
@pyqtSlot(str, "QVariant")
|
||||
def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None:
|
||||
@ -1522,6 +1530,10 @@ class MachineManager(QObject):
|
||||
def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]:
|
||||
return self._current_quality_changes_group
|
||||
|
||||
@pyqtProperty(bool, notify = activeQualityChangesGroupChanged)
|
||||
def hasCustomQuality(self) -> bool:
|
||||
return self._current_quality_changes_group is not None
|
||||
|
||||
@pyqtProperty(str, notify = activeQualityGroupChanged)
|
||||
def activeQualityOrQualityChangesName(self) -> str:
|
||||
name = empty_quality_container.getName()
|
||||
@ -1531,9 +1543,32 @@ class MachineManager(QObject):
|
||||
name = self._current_quality_group.name
|
||||
return name
|
||||
|
||||
@pyqtProperty(bool, notify = activeQualityGroupChanged)
|
||||
def hasNotSupportedQuality(self) -> bool:
|
||||
return self._current_quality_group is None and self._current_quality_changes_group is None
|
||||
|
||||
def _updateUponMaterialMetadataChange(self) -> None:
|
||||
if self._global_container_stack is None:
|
||||
return
|
||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||
self.updateMaterialWithVariant(None)
|
||||
self._updateQualityWithMaterial()
|
||||
|
||||
## This function will translate any printer type name to an abbreviated printer type name
|
||||
@pyqtSlot(str, result = str)
|
||||
def getAbbreviatedMachineName(self, machine_type_name: str) -> str:
|
||||
abbr_machine = ""
|
||||
for word in re.findall(r"[\w']+", machine_type_name):
|
||||
if word.lower() == "ultimaker":
|
||||
abbr_machine += "UM"
|
||||
elif word.isdigit():
|
||||
abbr_machine += word
|
||||
else:
|
||||
stripped_word = "".join(char for char in unicodedata.normalize("NFD", word.upper()) if unicodedata.category(char) != "Mn")
|
||||
# - use only the first character if the word is too long (> 3 characters)
|
||||
# - use the whole word if it's not too long (<= 3 characters)
|
||||
if len(stripped_word) > 3:
|
||||
stripped_word = stripped_word[0]
|
||||
abbr_machine += stripped_word
|
||||
|
||||
return abbr_machine
|
||||
|
@ -1,7 +1,8 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Set
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
||||
|
||||
from UM.Application import Application
|
||||
|
||||
@ -16,15 +17,11 @@ class SimpleModeSettingsManager(QObject):
|
||||
self._is_profile_user_created = False # True when profile was custom created by user
|
||||
|
||||
self._machine_manager.activeStackValueChanged.connect(self._updateIsProfileCustomized)
|
||||
self._machine_manager.activeQualityGroupChanged.connect(self._updateIsProfileUserCreated)
|
||||
self._machine_manager.activeQualityChangesGroupChanged.connect(self._updateIsProfileUserCreated)
|
||||
|
||||
# update on create as the activeQualityChanged signal is emitted before this manager is created when Cura starts
|
||||
self._updateIsProfileCustomized()
|
||||
self._updateIsProfileUserCreated()
|
||||
|
||||
isProfileCustomizedChanged = pyqtSignal()
|
||||
isProfileUserCreatedChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = isProfileCustomizedChanged)
|
||||
def isProfileCustomized(self):
|
||||
@ -57,33 +54,6 @@ class SimpleModeSettingsManager(QObject):
|
||||
self._is_profile_customized = has_customized_user_settings
|
||||
self.isProfileCustomizedChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify = isProfileUserCreatedChanged)
|
||||
def isProfileUserCreated(self):
|
||||
return self._is_profile_user_created
|
||||
|
||||
def _updateIsProfileUserCreated(self):
|
||||
quality_changes_keys = set()
|
||||
|
||||
if not self._machine_manager.activeMachine:
|
||||
return False
|
||||
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
|
||||
# check quality changes settings in the global stack
|
||||
quality_changes_keys.update(global_stack.qualityChanges.getAllKeys())
|
||||
|
||||
# check quality changes settings in the extruder stacks
|
||||
if global_stack.extruders:
|
||||
for extruder_stack in global_stack.extruders.values():
|
||||
quality_changes_keys.update(extruder_stack.qualityChanges.getAllKeys())
|
||||
|
||||
# check if the qualityChanges container is not empty (meaning it is a user created profile)
|
||||
has_quality_changes = len(quality_changes_keys) > 0
|
||||
|
||||
if has_quality_changes != self._is_profile_user_created:
|
||||
self._is_profile_user_created = has_quality_changes
|
||||
self.isProfileUserCreatedChanged.emit()
|
||||
|
||||
# These are the settings included in the Simple ("Recommended") Mode, so only when the other settings have been
|
||||
# changed, we consider it as a user customized profile in the Simple ("Recommended") Mode.
|
||||
__ignored_custom_setting_keys = ["support_enable",
|
||||
|
@ -1,23 +1,29 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, QUrl
|
||||
|
||||
from UM.Stage import Stage
|
||||
|
||||
|
||||
# Since Cura has a few pre-defined "space claims" for the locations of certain components, we've provided some structure
|
||||
# to indicate this.
|
||||
# * The StageMenuComponent is the horizontal area below the stage bar. This should be used to show stage specific
|
||||
# buttons and elements. This component will be drawn over the bar & main component.
|
||||
# * The MainComponent is the component that will be drawn starting from the bottom of the stageBar and fills the rest
|
||||
# of the screen.
|
||||
class CuraStage(Stage):
|
||||
|
||||
def __init__(self, parent = None):
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def stageId(self):
|
||||
def stageId(self) -> str:
|
||||
return self.getPluginId()
|
||||
|
||||
@pyqtProperty(QUrl, constant = True)
|
||||
def mainComponent(self):
|
||||
def mainComponent(self) -> QUrl:
|
||||
return self.getDisplayComponent("main")
|
||||
|
||||
@pyqtProperty(QUrl, constant = True)
|
||||
def sidebarComponent(self):
|
||||
return self.getDisplayComponent("sidebar")
|
||||
def stageMenuComponent(self) -> QUrl:
|
||||
return self.getDisplayComponent("menu")
|
@ -17,12 +17,6 @@ parser.add_argument("--debug",
|
||||
default = False,
|
||||
help = "Turn on the debug mode by setting this option."
|
||||
)
|
||||
parser.add_argument("--trigger-early-crash",
|
||||
dest = "trigger_early_crash",
|
||||
action = "store_true",
|
||||
default = False,
|
||||
help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog."
|
||||
)
|
||||
known_args = vars(parser.parse_known_args()[0])
|
||||
|
||||
if not known_args["debug"]:
|
||||
|
156
installer.nsi
156
installer.nsi
@ -1,156 +0,0 @@
|
||||
!ifndef VERSION
|
||||
!define VERSION '15.09.80'
|
||||
!endif
|
||||
|
||||
; The name of the installer
|
||||
Name "Cura ${VERSION}"
|
||||
|
||||
; The file to write
|
||||
OutFile "Cura_${VERSION}.exe"
|
||||
|
||||
; The default installation directory
|
||||
InstallDir $PROGRAMFILES\Cura_${VERSION}
|
||||
|
||||
; Registry key to check for directory (so if you install again, it will
|
||||
; overwrite the old one automatically)
|
||||
InstallDirRegKey HKLM "Software\Cura_${VERSION}" "Install_Dir"
|
||||
|
||||
; Request application privileges for Windows Vista
|
||||
RequestExecutionLevel admin
|
||||
|
||||
; Set the LZMA compressor to reduce size.
|
||||
SetCompressor /SOLID lzma
|
||||
;--------------------------------
|
||||
|
||||
!include "MUI2.nsh"
|
||||
!include "Library.nsh"
|
||||
|
||||
; !define MUI_ICON "dist/resources/cura.ico"
|
||||
!define MUI_BGCOLOR FFFFFF
|
||||
|
||||
; Directory page defines
|
||||
!define MUI_DIRECTORYPAGE_VERIFYONLEAVE
|
||||
|
||||
; Header
|
||||
; Don't show the component description box
|
||||
!define MUI_COMPONENTSPAGE_NODESC
|
||||
|
||||
;Do not leave (Un)Installer page automaticly
|
||||
!define MUI_FINISHPAGE_NOAUTOCLOSE
|
||||
!define MUI_UNFINISHPAGE_NOAUTOCLOSE
|
||||
|
||||
;Run Cura after installing
|
||||
!define MUI_FINISHPAGE_RUN
|
||||
!define MUI_FINISHPAGE_RUN_TEXT "Start Cura ${VERSION}"
|
||||
!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchLink"
|
||||
|
||||
;Add an option to show release notes
|
||||
!define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\plugins\ChangeLogPlugin\changelog.txt"
|
||||
|
||||
; Pages
|
||||
;!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_COMPONENTS
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
!insertmacro MUI_UNPAGE_FINISH
|
||||
|
||||
; Languages
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
; Reserve Files
|
||||
!insertmacro MUI_RESERVEFILE_LANGDLL
|
||||
ReserveFile '${NSISDIR}\Plugins\InstallOptions.dll'
|
||||
|
||||
;--------------------------------
|
||||
|
||||
; The stuff to install
|
||||
Section "Cura ${VERSION}"
|
||||
|
||||
SectionIn RO
|
||||
|
||||
; Set output path to the installation directory.
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
; Put file there
|
||||
File /r "dist\"
|
||||
|
||||
; Write the installation path into the registry
|
||||
WriteRegStr HKLM "SOFTWARE\Cura_${VERSION}" "Install_Dir" "$INSTDIR"
|
||||
|
||||
; Write the uninstall keys for Windows
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}" "DisplayName" "Cura ${VERSION}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}" "NoModify" 1
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}" "NoRepair" 1
|
||||
WriteUninstaller "uninstall.exe"
|
||||
|
||||
; Write start menu entries for all users
|
||||
SetShellVarContext all
|
||||
|
||||
CreateDirectory "$SMPROGRAMS\Cura ${VERSION}"
|
||||
CreateShortCut "$SMPROGRAMS\Cura ${VERSION}\Uninstall Cura ${VERSION}.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0
|
||||
CreateShortCut "$SMPROGRAMS\Cura ${VERSION}\Cura ${VERSION}.lnk" "$INSTDIR\Cura.exe" '' "$INSTDIR\Cura.exe" 0
|
||||
|
||||
SectionEnd
|
||||
|
||||
Function LaunchLink
|
||||
; Write start menu entries for all users
|
||||
SetShellVarContext all
|
||||
Exec '"$WINDIR\explorer.exe" "$SMPROGRAMS\Cura ${VERSION}\Cura ${VERSION}.lnk"'
|
||||
FunctionEnd
|
||||
|
||||
Section "Install Visual Studio 2010 Redistributable"
|
||||
SetOutPath "$INSTDIR"
|
||||
File "vcredist_2010_20110908_x86.exe"
|
||||
|
||||
IfSilent +2
|
||||
ExecWait '"$INSTDIR\vcredist_2010_20110908_x86.exe" /q /norestart'
|
||||
|
||||
SectionEnd
|
||||
|
||||
Section "Install Arduino Drivers"
|
||||
; Set output path to the driver directory.
|
||||
SetOutPath "$INSTDIR\drivers\"
|
||||
File /r "drivers\"
|
||||
|
||||
${If} ${RunningX64}
|
||||
IfSilent +2
|
||||
ExecWait '"$INSTDIR\drivers\dpinst64.exe" /lm'
|
||||
${Else}
|
||||
IfSilent +2
|
||||
ExecWait '"$INSTDIR\drivers\dpinst32.exe" /lm'
|
||||
${EndIf}
|
||||
SectionEnd
|
||||
|
||||
Section "Open STL files with Cura"
|
||||
${registerExtension} "$INSTDIR\Cura.exe" ".stl" "STL_File"
|
||||
SectionEnd
|
||||
|
||||
Section /o "Open OBJ files with Cura"
|
||||
WriteRegStr HKCR .obj "" "Cura OBJ model file"
|
||||
DeleteRegValue HKCR .obj "Content Type"
|
||||
WriteRegStr HKCR "Cura OBJ model file\DefaultIcon" "" "$INSTDIR\Cura.exe,0"
|
||||
WriteRegStr HKCR "Cura OBJ model file\shell" "" "open"
|
||||
WriteRegStr HKCR "Cura OBJ model file\shell\open\command" "" '"$INSTDIR\Cura.exe" "%1"'
|
||||
SectionEnd
|
||||
|
||||
;--------------------------------
|
||||
|
||||
; Uninstaller
|
||||
|
||||
Section "Uninstall"
|
||||
|
||||
; Remove registry keys
|
||||
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}"
|
||||
DeleteRegKey HKLM "SOFTWARE\Cura_${VERSION}"
|
||||
|
||||
; Write start menu entries for all users
|
||||
SetShellVarContext all
|
||||
; Remove directories used
|
||||
RMDir /r "$SMPROGRAMS\Cura ${VERSION}"
|
||||
RMDir /r "$INSTDIR"
|
||||
|
||||
SectionEnd
|
@ -298,7 +298,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
values = parser["values"] if parser.has_section("values") else dict()
|
||||
num_settings_overriden_by_quality_changes += len(values)
|
||||
# Check if quality changes already exists.
|
||||
quality_changes = self._container_registry.findInstanceContainers(id = container_id)
|
||||
quality_changes = self._container_registry.findInstanceContainers(name = custom_quality_name,
|
||||
type = "quality_changes")
|
||||
if quality_changes:
|
||||
containers_found_dict["quality_changes"] = True
|
||||
# Check if there really is a conflict by comparing the values
|
||||
@ -793,6 +794,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
# Clear all existing containers
|
||||
quality_changes_info.global_info.container.clear()
|
||||
for container_info in quality_changes_info.extruder_info_dict.values():
|
||||
if container_info.container:
|
||||
container_info.container.clear()
|
||||
|
||||
# Loop over everything and override the existing containers
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "3MF Reader",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for reading 3MF files.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "3MF Writer",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for writing 3MF files.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
@ -29,6 +29,7 @@ class ChangeLog(Extension, QObject,):
|
||||
self._change_logs = None
|
||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||
Application.getInstance().getPreferences().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium
|
||||
self.setMenuName(catalog.i18nc("@item:inmenu", "Changelog"))
|
||||
self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog)
|
||||
|
||||
def getChangeLogs(self):
|
||||
|
@ -11,8 +11,8 @@ It is now possible to specify the cooling fan to use if your printer has multipl
|
||||
*Settings refactor
|
||||
The CuraEngine has been refactored to create a more testable, future-proof way of storing and representing settings. This makes slicing faster, and future development easier.
|
||||
|
||||
*Print core CC Red 0.6
|
||||
The new print core CC Red 0.6 is selectable when the Ultimaker S5 profile is active. This print core is optimized for use with abrasive materials and composites.
|
||||
*Print core CC 0.6
|
||||
The new print core CC 0.6 is selectable when the Ultimaker S5 profile is active. This print core is optimized for use with abrasive materials and composites.
|
||||
|
||||
*File name and layer display
|
||||
Added M117 commands to GCODE to give real-time information about the print job file name and layer number shown on the printer’s display when printing via USB. Contributed by adecastilho.
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Changelog",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Shows changes since latest checked version.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -203,7 +203,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
|
||||
@pyqtSlot()
|
||||
def stopSlicing(self) -> None:
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
self.setState(BackendState.NotStarted)
|
||||
if self._slicing: # We were already slicing. Stop the old job.
|
||||
self._terminate()
|
||||
self._createSocket()
|
||||
@ -229,6 +229,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
if not self._build_plates_to_be_sliced:
|
||||
self.processingProgress.emit(1.0)
|
||||
Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
|
||||
self.setState(BackendState.Done)
|
||||
return
|
||||
|
||||
if self._process_layers_job:
|
||||
@ -245,7 +246,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
num_objects = self._numObjectsPerBuildPlate()
|
||||
|
||||
self._stored_layer_data = []
|
||||
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
|
||||
|
||||
|
||||
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
|
||||
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #Because we created this attribute above.
|
||||
@ -253,7 +254,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
if self._build_plates_to_be_sliced:
|
||||
self.slice()
|
||||
return
|
||||
|
||||
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
|
||||
if self._application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
|
||||
self._application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
|
||||
|
||||
@ -322,7 +323,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
self._start_slice_job = None
|
||||
|
||||
if job.isCancelled() or job.getError() or job.getResult() == StartJobResult.Error:
|
||||
self.backendStateChange.emit(BackendState.Error)
|
||||
self.setState(BackendState.Error)
|
||||
self.backendError.emit(job)
|
||||
return
|
||||
|
||||
@ -331,10 +332,10 @@ class CuraEngineBackend(QObject, Backend):
|
||||
self._error_message = Message(catalog.i18nc("@info:status",
|
||||
"Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||
self._error_message.show()
|
||||
self.backendStateChange.emit(BackendState.Error)
|
||||
self.setState(BackendState.Error)
|
||||
self.backendError.emit(job)
|
||||
else:
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
self.setState(BackendState.NotStarted)
|
||||
return
|
||||
|
||||
if job.getResult() == StartJobResult.SettingError:
|
||||
@ -362,10 +363,10 @@ class CuraEngineBackend(QObject, Backend):
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}").format(", ".join(error_labels)),
|
||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||
self._error_message.show()
|
||||
self.backendStateChange.emit(BackendState.Error)
|
||||
self.setState(BackendState.Error)
|
||||
self.backendError.emit(job)
|
||||
else:
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
self.setState(BackendState.NotStarted)
|
||||
return
|
||||
|
||||
elif job.getResult() == StartJobResult.ObjectSettingError:
|
||||
@ -386,7 +387,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = ", ".join(errors.values())),
|
||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||
self._error_message.show()
|
||||
self.backendStateChange.emit(BackendState.Error)
|
||||
self.setState(BackendState.Error)
|
||||
self.backendError.emit(job)
|
||||
return
|
||||
|
||||
@ -395,28 +396,28 @@ class CuraEngineBackend(QObject, Backend):
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."),
|
||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||
self._error_message.show()
|
||||
self.backendStateChange.emit(BackendState.Error)
|
||||
self.setState(BackendState.Error)
|
||||
self.backendError.emit(job)
|
||||
else:
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
self.setState(BackendState.NotStarted)
|
||||
|
||||
if job.getResult() == StartJobResult.ObjectsWithDisabledExtruder:
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because there are objects associated with disabled Extruder %s." % job.getMessage()),
|
||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||
self._error_message.show()
|
||||
self.backendStateChange.emit(BackendState.Error)
|
||||
self.setState(BackendState.Error)
|
||||
self.backendError.emit(job)
|
||||
return
|
||||
|
||||
if job.getResult() == StartJobResult.NothingToSlice:
|
||||
if self._application.platformActivity:
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."),
|
||||
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume or are assigned to a disabled extruder. Please scale or rotate models to fit, or enable an extruder."),
|
||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||
self._error_message.show()
|
||||
self.backendStateChange.emit(BackendState.Error)
|
||||
self.setState(BackendState.Error)
|
||||
self.backendError.emit(job)
|
||||
else:
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
self.setState(BackendState.NotStarted)
|
||||
self._invokeSlice()
|
||||
return
|
||||
|
||||
@ -424,7 +425,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
self._socket.sendMessage(job.getSliceMessage())
|
||||
|
||||
# Notify the user that it's now up to the backend to do it's job
|
||||
self.backendStateChange.emit(BackendState.Processing)
|
||||
self.setState(BackendState.Processing)
|
||||
|
||||
if self._slice_start_time:
|
||||
Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
|
||||
@ -442,7 +443,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||
if node.callDecoration("isBlockSlicing"):
|
||||
enable_timer = False
|
||||
self.backendStateChange.emit(BackendState.Disabled)
|
||||
self.setState(BackendState.Disabled)
|
||||
self._is_disabled = True
|
||||
gcode_list = node.callDecoration("getGCodeList")
|
||||
if gcode_list is not None:
|
||||
@ -451,7 +452,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
if self._use_timer == enable_timer:
|
||||
return self._use_timer
|
||||
if enable_timer:
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
self.setState(BackendState.NotStarted)
|
||||
self.enableTimer()
|
||||
return True
|
||||
else:
|
||||
@ -518,7 +519,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
self._build_plates_to_be_sliced.append(build_plate_number)
|
||||
self.printDurationMessage.emit(source_build_plate_number, {}, [])
|
||||
self.processingProgress.emit(0.0)
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
self.setState(BackendState.NotStarted)
|
||||
# if not self._use_timer:
|
||||
# With manually having to slice, we want to clear the old invalid layer data.
|
||||
self._clearLayerData(build_plate_changed)
|
||||
@ -567,7 +568,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
self.stopSlicing()
|
||||
self.markSliceAll()
|
||||
self.processingProgress.emit(0.0)
|
||||
self.backendStateChange.emit(BackendState.NotStarted)
|
||||
self.setState(BackendState.NotStarted)
|
||||
if not self._use_timer:
|
||||
# With manually having to slice, we want to clear the old invalid layer data.
|
||||
self._clearLayerData()
|
||||
@ -613,7 +614,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
# \param message The protobuf message containing the slicing progress.
|
||||
def _onProgressMessage(self, message: Arcus.PythonMessage) -> None:
|
||||
self.processingProgress.emit(message.amount)
|
||||
self.backendStateChange.emit(BackendState.Processing)
|
||||
self.setState(BackendState.Processing)
|
||||
|
||||
def _invokeSlice(self) -> None:
|
||||
if self._use_timer:
|
||||
@ -632,7 +633,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||
#
|
||||
# \param message The protobuf message signalling that slicing is finished.
|
||||
def _onSlicingFinishedMessage(self, message: Arcus.PythonMessage) -> None:
|
||||
self.backendStateChange.emit(BackendState.Done)
|
||||
self.setState(BackendState.Done)
|
||||
self.processingProgress.emit(1.0)
|
||||
|
||||
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically.
|
||||
|
@ -195,7 +195,7 @@ class ProcessSlicedLayersJob(Job):
|
||||
if extruders:
|
||||
material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32)
|
||||
for extruder in extruders:
|
||||
position = int(extruder.getMetaDataEntry("position", default="0")) # Get the position
|
||||
position = int(extruder.getMetaDataEntry("position", default = "0"))
|
||||
try:
|
||||
default_color = ExtrudersModel.defaultColors[position]
|
||||
except IndexError:
|
||||
|
@ -66,11 +66,19 @@ class GcodeStartEndFormatter(Formatter):
|
||||
return "{" + key + "}"
|
||||
|
||||
key = key_fragments[0]
|
||||
try:
|
||||
return kwargs[str(extruder_nr)][key]
|
||||
except KeyError:
|
||||
|
||||
default_value_str = "{" + key + "}"
|
||||
value = default_value_str
|
||||
# "-1" is global stack, and if the setting value exists in the global stack, use it as the fallback value.
|
||||
if key in kwargs["-1"]:
|
||||
value = kwargs["-1"]
|
||||
if str(extruder_nr) in kwargs and key in kwargs[str(extruder_nr)]:
|
||||
value = kwargs[str(extruder_nr)][key]
|
||||
|
||||
if value == default_value_str:
|
||||
Logger.log("w", "Unable to replace '%s' placeholder in start/end g-code", key)
|
||||
return "{" + key + "}"
|
||||
|
||||
return value
|
||||
|
||||
|
||||
## Job class that builds up the message of scene data to send to CuraEngine.
|
||||
|
@ -2,7 +2,7 @@
|
||||
"name": "CuraEngine Backend",
|
||||
"author": "Ultimaker B.V.",
|
||||
"description": "Provides the link to the CuraEngine slicing backend.",
|
||||
"api": 5,
|
||||
"version": "1.0.0",
|
||||
"api": "6.0",
|
||||
"version": "1.0.1",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Cura Profile Reader",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for importing Cura profiles.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Cura Profile Writer",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for exporting Cura profiles.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog":"cura"
|
||||
}
|
||||
|
@ -93,6 +93,11 @@ class FirmwareUpdateCheckerJob(Job):
|
||||
|
||||
current_version = self.getCurrentVersion()
|
||||
|
||||
# This case indicates that was an error checking the version.
|
||||
# It happens for instance when not connected to internet.
|
||||
if current_version == self.ZERO_VERSION:
|
||||
return
|
||||
|
||||
# If it is the first time the version is checked, the checked_version is ""
|
||||
setting_key_str = getSettingsKeyForMachine(machine_id)
|
||||
checked_version = Version(Application.getInstance().getPreferences().getValue(setting_key_str))
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Firmware Update Checker",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Checks for firmware updates.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Firmware Updater",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a machine actions for updating firmware.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Compressed G-code Reader",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Reads g-code from a compressed archive.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Compressed G-code Writer",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Writes g-code to a compressed archive.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "G-code Profile Reader",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for importing profiles from g-code files.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -364,6 +364,8 @@ class FlavorParser:
|
||||
self._layer_type = LayerPolygon.SupportType
|
||||
elif type == "FILL":
|
||||
self._layer_type = LayerPolygon.InfillType
|
||||
elif type == "SUPPORT-INTERFACE":
|
||||
self._layer_type = LayerPolygon.SupportInterfaceType
|
||||
else:
|
||||
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "G-code Reader",
|
||||
"author": "Victor Larchenko",
|
||||
"version": "1.0.0",
|
||||
"author": "Victor Larchenko, Ultimaker",
|
||||
"version": "1.0.1",
|
||||
"description": "Allows loading and displaying G-code files.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "G-code Writer",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Writes g-code to a file.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Image Reader",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Enables ability to generate printable geometry from 2D image files.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"source_version": "15.04",
|
||||
"target_version": 3,
|
||||
"target_version": "4.5",
|
||||
|
||||
"translation": {
|
||||
"machine_nozzle_size": "nozzle_size",
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import configparser # For reading the legacy profile INI files.
|
||||
@ -6,6 +6,7 @@ import io
|
||||
import json # For reading the Dictionary of Doom.
|
||||
import math # For mathematical operations included in the Dictionary of Doom.
|
||||
import os.path # For concatenating the path to the plugin and the relative path to the Dictionary of Doom.
|
||||
from typing import Dict
|
||||
|
||||
from UM.Application import Application # To get the machine manager to create the new profile in.
|
||||
from UM.Logger import Logger # Logging errors.
|
||||
@ -33,8 +34,9 @@ class LegacyProfileReader(ProfileReader):
|
||||
# \param json The JSON file to load the default setting values from. This
|
||||
# should not be a URL but a pre-loaded JSON handle.
|
||||
# \return A dictionary of the default values of the legacy Cura version.
|
||||
def prepareDefaults(self, json):
|
||||
def prepareDefaults(self, json: Dict[str, Dict[str, str]]) -> Dict[str, str]:
|
||||
defaults = {}
|
||||
if "defaults" in json:
|
||||
for key in json["defaults"]: # We have to copy over all defaults from the JSON handle to a normal dict.
|
||||
defaults[key] = json["defaults"][key]
|
||||
return defaults
|
||||
@ -80,11 +82,10 @@ class LegacyProfileReader(ProfileReader):
|
||||
Logger.log("i", "Importing legacy profile from file " + file_name + ".")
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
profile_id = container_registry.uniqueName("Imported Legacy Profile")
|
||||
profile = InstanceContainer(profile_id) # Create an empty profile.
|
||||
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
input_parser = configparser.ConfigParser(interpolation = None)
|
||||
try:
|
||||
parser.read([file_name]) # Parse the INI file.
|
||||
input_parser.read([file_name]) # Parse the INI file.
|
||||
except Exception as e:
|
||||
Logger.log("e", "Unable to open legacy profile %s: %s", file_name, str(e))
|
||||
return None
|
||||
@ -92,7 +93,7 @@ class LegacyProfileReader(ProfileReader):
|
||||
# Legacy Cura saved the profile under the section "profile_N" where N is the ID of a machine, except when you export in which case it saves it in the section "profile".
|
||||
# Since importing multiple machine profiles is out of scope, just import the first section we find.
|
||||
section = ""
|
||||
for found_section in parser.sections():
|
||||
for found_section in input_parser.sections():
|
||||
if found_section.startswith("profile"):
|
||||
section = found_section
|
||||
break
|
||||
@ -110,15 +111,13 @@ class LegacyProfileReader(ProfileReader):
|
||||
return None
|
||||
|
||||
defaults = self.prepareDefaults(dict_of_doom)
|
||||
legacy_settings = self.prepareLocals(parser, section, defaults) #Gets the settings from the legacy profile.
|
||||
legacy_settings = self.prepareLocals(input_parser, section, defaults) #Gets the settings from the legacy profile.
|
||||
|
||||
#Check the target version in the Dictionary of Doom with this application version.
|
||||
if "target_version" not in dict_of_doom:
|
||||
Logger.log("e", "Dictionary of Doom has no target version. Is it the correct JSON file?")
|
||||
return None
|
||||
if InstanceContainer.Version != dict_of_doom["target_version"]:
|
||||
Logger.log("e", "Dictionary of Doom of legacy profile reader (version %s) is not in sync with the current instance container version (version %s)!", dict_of_doom["target_version"], str(InstanceContainer.Version))
|
||||
return None
|
||||
# Serialised format into version 4.5. Do NOT upgrade this, let the version upgrader handle it.
|
||||
output_parser = configparser.ConfigParser(interpolation = None)
|
||||
output_parser.add_section("general")
|
||||
output_parser.add_section("metadata")
|
||||
output_parser.add_section("values")
|
||||
|
||||
if "translation" not in dict_of_doom:
|
||||
Logger.log("e", "Dictionary of Doom has no translation. Is it the correct JSON file?")
|
||||
@ -127,7 +126,7 @@ class LegacyProfileReader(ProfileReader):
|
||||
quality_definition = current_printer_definition.getMetaDataEntry("quality_definition")
|
||||
if not quality_definition:
|
||||
quality_definition = current_printer_definition.getId()
|
||||
profile.setDefinition(quality_definition)
|
||||
output_parser["general"]["definition"] = quality_definition
|
||||
for new_setting in dict_of_doom["translation"]: # Evaluate all new settings that would get a value from the translations.
|
||||
old_setting_expression = dict_of_doom["translation"][new_setting]
|
||||
compiled = compile(old_setting_expression, new_setting, "eval")
|
||||
@ -140,37 +139,34 @@ class LegacyProfileReader(ProfileReader):
|
||||
definitions = current_printer_definition.findDefinitions(key = new_setting)
|
||||
if definitions:
|
||||
if new_value != value_using_defaults and definitions[0].default_value != new_value: # Not equal to the default in the new Cura OR the default in the legacy Cura.
|
||||
profile.setProperty(new_setting, "value", new_value) # Store the setting in the profile!
|
||||
output_parser["values"][new_setting] = str(new_value) # Store the setting in the profile!
|
||||
|
||||
if len(profile.getAllKeys()) == 0:
|
||||
if len(output_parser["values"]) == 0:
|
||||
Logger.log("i", "A legacy profile was imported but everything evaluates to the defaults, creating an empty profile.")
|
||||
|
||||
profile.setMetaDataEntry("type", "profile")
|
||||
# don't know what quality_type it is based on, so use "normal" by default
|
||||
profile.setMetaDataEntry("quality_type", "normal")
|
||||
profile.setName(profile_id)
|
||||
profile.setDirty(True)
|
||||
output_parser["general"]["version"] = "4"
|
||||
output_parser["general"]["name"] = profile_id
|
||||
output_parser["metadata"]["type"] = "quality_changes"
|
||||
output_parser["metadata"]["quality_type"] = "normal" # Don't know what quality_type it is based on, so use "normal" by default.
|
||||
output_parser["metadata"]["position"] = "0" # We only support single extrusion.
|
||||
output_parser["metadata"]["setting_version"] = "5" # What the dictionary of doom is made for.
|
||||
|
||||
#Serialise and deserialise in order to perform the version upgrade.
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
data = profile.serialize()
|
||||
parser.read_string(data)
|
||||
parser["general"]["version"] = "1"
|
||||
if parser.has_section("values"):
|
||||
parser["settings"] = parser["values"]
|
||||
del parser["values"]
|
||||
# Serialise in order to perform the version upgrade.
|
||||
stream = io.StringIO()
|
||||
parser.write(stream)
|
||||
output_parser.write(stream)
|
||||
data = stream.getvalue()
|
||||
profile.deserialize(data)
|
||||
|
||||
# The definition can get reset to fdmprinter during the deserialization's upgrade. Here we set the definition
|
||||
# again.
|
||||
profile.setDefinition(quality_definition)
|
||||
profile = InstanceContainer(profile_id)
|
||||
profile.deserialize(data) # Also performs the version upgrade.
|
||||
profile.setDirty(True)
|
||||
|
||||
#We need to return one extruder stack and one global stack.
|
||||
global_container_id = container_registry.uniqueName("Global Imported Legacy Profile")
|
||||
# We duplicate the extruder profile into the global stack.
|
||||
# This may introduce some settings that are global in the extruder stack and some settings that are per-extruder in the global stack.
|
||||
# We don't care about that. The engine will ignore them anyway.
|
||||
global_profile = profile.duplicate(new_id = global_container_id, new_name = profile_id) #Needs to have the same name as the extruder profile.
|
||||
del global_profile.getMetaData()["position"] # Has no position because it's global.
|
||||
global_profile.setDirty(True)
|
||||
|
||||
profile_definition = "fdmprinter"
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Legacy Cura Profile Reader",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides support for importing profiles from legacy Cura versions.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
190
plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py
Normal file
190
plugins/LegacyProfileReader/tests/TestLegacyProfileReader.py
Normal file
@ -0,0 +1,190 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import configparser # An input for some functions we're testing.
|
||||
import os.path # To find the integration test .ini files.
|
||||
import pytest # To register tests with.
|
||||
import unittest.mock # To mock the application, plug-in and container registry out.
|
||||
|
||||
import UM.Application # To mock the application out.
|
||||
import UM.PluginRegistry # To mock the plug-in registry out.
|
||||
import UM.Settings.ContainerRegistry # To mock the container registry out.
|
||||
import UM.Settings.InstanceContainer # To intercept the serialised data from the read() function.
|
||||
|
||||
import LegacyProfileReader as LegacyProfileReaderModule # To get the directory of the module.
|
||||
from LegacyProfileReader import LegacyProfileReader # The module we're testing.
|
||||
|
||||
@pytest.fixture
|
||||
def legacy_profile_reader():
|
||||
return LegacyProfileReader()
|
||||
|
||||
test_prepareDefaultsData = [
|
||||
{
|
||||
"defaults":
|
||||
{
|
||||
"foo": "bar"
|
||||
},
|
||||
"cheese": "delicious"
|
||||
},
|
||||
{
|
||||
"cat": "fluffy",
|
||||
"dog": "floofy"
|
||||
}
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("input", test_prepareDefaultsData)
|
||||
def test_prepareDefaults(legacy_profile_reader, input):
|
||||
output = legacy_profile_reader.prepareDefaults(input)
|
||||
if "defaults" in input:
|
||||
assert input["defaults"] == output
|
||||
else:
|
||||
assert output == {}
|
||||
|
||||
test_prepareLocalsData = [
|
||||
( # Ordinary case.
|
||||
{ # Parser data.
|
||||
"profile":
|
||||
{
|
||||
"layer_height": "0.2",
|
||||
"infill_density": "30"
|
||||
}
|
||||
},
|
||||
{ # Defaults.
|
||||
"layer_height": "0.1",
|
||||
"infill_density": "20",
|
||||
"line_width": "0.4"
|
||||
}
|
||||
),
|
||||
( # Empty data.
|
||||
{ # Parser data.
|
||||
"profile":
|
||||
{
|
||||
}
|
||||
},
|
||||
{ # Defaults.
|
||||
}
|
||||
),
|
||||
( # All defaults.
|
||||
{ # Parser data.
|
||||
"profile":
|
||||
{
|
||||
}
|
||||
},
|
||||
{ # Defaults.
|
||||
"foo": "bar",
|
||||
"boo": "far"
|
||||
}
|
||||
),
|
||||
( # Multiple config sections.
|
||||
{ # Parser data.
|
||||
"some_other_name":
|
||||
{
|
||||
"foo": "bar"
|
||||
},
|
||||
"profile":
|
||||
{
|
||||
"foo": "baz" #Not the same as in some_other_name
|
||||
}
|
||||
},
|
||||
{ # Defaults.
|
||||
"foo": "bla"
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsData)
|
||||
def test_prepareLocals(legacy_profile_reader, parser_data, defaults):
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read_dict(parser_data)
|
||||
|
||||
output = legacy_profile_reader.prepareLocals(parser, "profile", defaults)
|
||||
|
||||
assert set(defaults.keys()) <= set(output.keys()) # All defaults must be in there.
|
||||
assert set(parser_data["profile"]) <= set(output.keys()) # All overwritten values must be in there.
|
||||
for key in output:
|
||||
if key in parser_data["profile"]:
|
||||
assert output[key] == parser_data["profile"][key] # If overwritten, must be the overwritten value.
|
||||
else:
|
||||
assert output[key] == defaults[key] # Otherwise must be equal to the default.
|
||||
|
||||
test_prepareLocalsNoSectionErrorData = [
|
||||
( # Section does not exist.
|
||||
{ # Parser data.
|
||||
"some_other_name":
|
||||
{
|
||||
"foo": "bar"
|
||||
},
|
||||
},
|
||||
{ # Defaults.
|
||||
"foo": "baz"
|
||||
}
|
||||
)
|
||||
]
|
||||
|
||||
## Test cases where a key error is expected.
|
||||
@pytest.mark.parametrize("parser_data, defaults", test_prepareLocalsNoSectionErrorData)
|
||||
def test_prepareLocalsNoSectionError(legacy_profile_reader, parser_data, defaults):
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read_dict(parser_data)
|
||||
|
||||
with pytest.raises(configparser.NoSectionError):
|
||||
legacy_profile_reader.prepareLocals(parser, "profile", defaults)
|
||||
|
||||
intercepted_data = ""
|
||||
|
||||
@pytest.mark.parametrize("file_name", ["normal_case.ini"])
|
||||
def test_read(legacy_profile_reader, file_name):
|
||||
# Mock out all dependencies. Quite a lot!
|
||||
global_stack = unittest.mock.MagicMock()
|
||||
global_stack.getProperty = unittest.mock.MagicMock(return_value = 1) # For machine_extruder_count setting.
|
||||
def getMetaDataEntry(key, default_value = ""):
|
||||
if key == "quality_definition":
|
||||
return "mocked_quality_definition"
|
||||
if key == "has_machine_quality":
|
||||
return "True"
|
||||
global_stack.definition.getMetaDataEntry = getMetaDataEntry
|
||||
global_stack.definition.getId = unittest.mock.MagicMock(return_value = "mocked_global_definition")
|
||||
application = unittest.mock.MagicMock()
|
||||
application.getGlobalContainerStack = unittest.mock.MagicMock(return_value = global_stack)
|
||||
application_getInstance = unittest.mock.MagicMock(return_value = application)
|
||||
container_registry = unittest.mock.MagicMock()
|
||||
container_registry_getInstance = unittest.mock.MagicMock(return_value = container_registry)
|
||||
container_registry.uniqueName = unittest.mock.MagicMock(return_value = "Imported Legacy Profile")
|
||||
container_registry.findDefinitionContainers = unittest.mock.MagicMock(return_value = [global_stack.definition])
|
||||
UM.Settings.InstanceContainer.setContainerRegistry(container_registry)
|
||||
plugin_registry = unittest.mock.MagicMock()
|
||||
plugin_registry_getInstance = unittest.mock.MagicMock(return_value = plugin_registry)
|
||||
plugin_registry.getPluginPath = unittest.mock.MagicMock(return_value = os.path.dirname(LegacyProfileReaderModule.__file__))
|
||||
|
||||
# Mock out the resulting InstanceContainer so that we can intercept the data before it's passed through the version upgrader.
|
||||
def deserialize(self, data): # Intercepts the serialised data that we'd perform the version upgrade from when deserializing.
|
||||
global intercepted_data
|
||||
intercepted_data = data
|
||||
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read_string(data)
|
||||
self._metadata["position"] = parser["metadata"]["position"]
|
||||
def duplicate(self, new_id, new_name):
|
||||
self._metadata["id"] = new_id
|
||||
self._metadata["name"] = new_name
|
||||
return self
|
||||
|
||||
with unittest.mock.patch.object(UM.Application.Application, "getInstance", application_getInstance):
|
||||
with unittest.mock.patch.object(UM.Settings.ContainerRegistry.ContainerRegistry, "getInstance", container_registry_getInstance):
|
||||
with unittest.mock.patch.object(UM.PluginRegistry.PluginRegistry, "getInstance", plugin_registry_getInstance):
|
||||
with unittest.mock.patch.object(UM.Settings.InstanceContainer.InstanceContainer, "deserialize", deserialize):
|
||||
with unittest.mock.patch.object(UM.Settings.InstanceContainer.InstanceContainer, "duplicate", duplicate):
|
||||
result = legacy_profile_reader.read(os.path.join(os.path.dirname(__file__), file_name))
|
||||
|
||||
assert len(result) == 1
|
||||
|
||||
# Let's see what's inside the actual output file that we generated.
|
||||
parser = configparser.ConfigParser()
|
||||
parser.read_string(intercepted_data)
|
||||
assert parser["general"]["definition"] == "mocked_quality_definition"
|
||||
assert parser["general"]["version"] == "4" # Yes, before we upgraded.
|
||||
assert parser["general"]["name"] == "Imported Legacy Profile" # Because we overwrote uniqueName.
|
||||
assert parser["metadata"]["type"] == "quality_changes"
|
||||
assert parser["metadata"]["quality_type"] == "normal"
|
||||
assert parser["metadata"]["position"] == "0"
|
||||
assert parser["metadata"]["setting_version"] == "5" # Yes, before we upgraded.
|
7
plugins/LegacyProfileReader/tests/normal_case.ini
Normal file
7
plugins/LegacyProfileReader/tests/normal_case.ini
Normal file
@ -0,0 +1,7 @@
|
||||
[profile]
|
||||
foo = bar
|
||||
boo = far
|
||||
fill_overlap = 3
|
||||
|
||||
[alterations]
|
||||
some = values
|
@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2016 Ultimaker B.V.
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.2
|
||||
@ -13,7 +13,8 @@ import Cura 1.0 as Cura
|
||||
Cura.MachineAction
|
||||
{
|
||||
id: base
|
||||
property var extrudersModel: Cura.ExtrudersModel{}
|
||||
property var extrudersModel: Cura.ExtrudersModel{} // Do not retrieve the Model from a backend. Otherwise the tabs
|
||||
// in tabView will not removed/updated. Probably QML bug
|
||||
property int extruderTabsCount: 0
|
||||
|
||||
property var activeMachineId: Cura.MachineManager.activeMachine != null ? Cura.MachineManager.activeMachine.id : ""
|
||||
@ -23,7 +24,7 @@ Cura.MachineAction
|
||||
target: base.extrudersModel
|
||||
onModelChanged:
|
||||
{
|
||||
var extruderCount = base.extrudersModel.rowCount();
|
||||
var extruderCount = base.extrudersModel.count;
|
||||
base.extruderTabsCount = extruderCount;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Machine Settings action",
|
||||
"author": "fieldOfView",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -4,19 +4,19 @@
|
||||
import QtQuick 2.2
|
||||
import QtQuick.Controls 1.1
|
||||
import QtQuick.Controls.Styles 1.1
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Dialogs 1.1
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.2 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
|
||||
Button
|
||||
{
|
||||
id: modelCheckerButton
|
||||
|
||||
UM.I18nCatalog{id: catalog; name:"cura"}
|
||||
UM.I18nCatalog
|
||||
{
|
||||
id: catalog
|
||||
name: "cura"
|
||||
}
|
||||
|
||||
visible: manager.hasWarnings
|
||||
tooltip: catalog.i18nc("@info:tooltip", "Some things could be problematic in this print. Click to see tips for adjustment.")
|
||||
@ -25,6 +25,8 @@ Button
|
||||
width: UM.Theme.getSize("save_button_specs_icons").width
|
||||
height: UM.Theme.getSize("save_button_specs_icons").height
|
||||
|
||||
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
|
||||
|
||||
style: ButtonStyle
|
||||
{
|
||||
background: Item
|
||||
@ -33,7 +35,6 @@ Button
|
||||
{
|
||||
width: UM.Theme.getSize("save_button_specs_icons").width;
|
||||
height: UM.Theme.getSize("save_button_specs_icons").height;
|
||||
sourceSize.width: width;
|
||||
sourceSize.height: width;
|
||||
color: control.hovered ? UM.Theme.getColor("text_scene_hover") : UM.Theme.getColor("text_scene");
|
||||
source: "model_checker.svg"
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Model Checker",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "0.1",
|
||||
"api": 5,
|
||||
"version": "1.0.1",
|
||||
"api": "6.0",
|
||||
"description": "Checks models and print configuration for possible printing issues and give suggestions.",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -1,45 +1,48 @@
|
||||
// Copyright (c) 2017 Ultimaker B.V.
|
||||
|
||||
import QtQuick 2.2
|
||||
import QtQuick.Controls 1.1
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Controls 1.4
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
|
||||
Item
|
||||
{
|
||||
// parent could be undefined as this component is not visible at all times
|
||||
width: parent ? parent.width : 0
|
||||
height: parent ? parent.height : 0
|
||||
|
||||
// We show a nice overlay on the 3D viewer when the current output device has no monitor view
|
||||
Rectangle
|
||||
{
|
||||
id: viewportOverlay
|
||||
|
||||
color: UM.Theme.getColor("viewport_overlay")
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
anchors.fill: parent
|
||||
|
||||
// This mouse area is to prevent mouse clicks to be passed onto the scene.
|
||||
MouseArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.AllButtons
|
||||
onWheel: wheel.accepted = true
|
||||
}
|
||||
|
||||
// Disable dropping files into Cura when the monitor page is active
|
||||
DropArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
}
|
||||
}
|
||||
|
||||
Loader
|
||||
{
|
||||
id: monitorViewComponent
|
||||
|
||||
width: parent.width
|
||||
anchors.fill: parent
|
||||
|
||||
height: parent.height
|
||||
|
||||
property real maximumWidth: parent.width
|
||||
property real maximumHeight: parent.height
|
||||
|
||||
sourceComponent: Cura.MachineManager.printerOutputDevices.length > 0 ? Cura.MachineManager.printerOutputDevices[0].monitorItem : null
|
||||
visible: sourceComponent != null
|
||||
}
|
||||
}
|
23
plugins/MonitorStage/MonitorMenu.qml
Normal file
23
plugins/MonitorStage/MonitorMenu.qml
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
Item
|
||||
{
|
||||
signal showTooltip(Item item, point location, string text)
|
||||
signal hideTooltip()
|
||||
|
||||
Cura.MachineSelector
|
||||
{
|
||||
id: machineSelection
|
||||
headerCornerSide: Cura.RoundedRectangle.Direction.All
|
||||
width: UM.Theme.getSize("machine_selector_widget").width
|
||||
height: parent.height
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
@ -65,15 +65,10 @@ class MonitorStage(CuraStage):
|
||||
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
|
||||
Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
|
||||
self._onOutputDevicesChanged()
|
||||
self._updateMainOverlay()
|
||||
self._updateSidebar()
|
||||
|
||||
def _updateMainOverlay(self):
|
||||
main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"),
|
||||
"MonitorMainView.qml")
|
||||
plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId())
|
||||
if plugin_path is not None:
|
||||
menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml")
|
||||
main_component_path = os.path.join(plugin_path, "MonitorMain.qml")
|
||||
self.addDisplayComponent("menu", menu_component_path)
|
||||
self.addDisplayComponent("main", main_component_path)
|
||||
|
||||
def _updateSidebar(self):
|
||||
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles),
|
||||
"MonitorSidebar.qml")
|
||||
self.addDisplayComponent("sidebar", sidebar_component_path)
|
||||
|
@ -7,14 +7,16 @@ from . import MonitorStage
|
||||
from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"stage": {
|
||||
"name": i18n_catalog.i18nc("@item:inmenu", "Monitor"),
|
||||
"weight": 1
|
||||
"weight": 2
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def register(app):
|
||||
return {
|
||||
"stage": MonitorStage.MonitorStage()
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Monitor Stage",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a monitor stage in Cura.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
@ -265,7 +265,6 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width
|
||||
height: width
|
||||
sourceSize.width: width
|
||||
sourceSize.height: width
|
||||
color: control.hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button")
|
||||
source: UM.Theme.getIcon("minus")
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Per Model Settings Tool",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides the Per Model Settings.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
@ -32,7 +32,8 @@ class PostProcessingPlugin(QObject, Extension):
|
||||
def __init__(self, parent = None) -> None:
|
||||
QObject.__init__(self, parent)
|
||||
Extension.__init__(self)
|
||||
self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup)
|
||||
self.setMenuName(i18n_catalog.i18nc("@item:inmenu", "Post Processing"))
|
||||
self.addMenuItem(i18n_catalog.i18nc("@item:inmenu", "Modify G-Code"), self.showPopup)
|
||||
self._view = None
|
||||
|
||||
# Loaded scripts are all scripts that can be used
|
||||
@ -54,14 +55,14 @@ class PostProcessingPlugin(QObject, Extension):
|
||||
def selectedScriptDefinitionId(self) -> Optional[str]:
|
||||
try:
|
||||
return self._script_list[self._selected_script_index].getDefinitionId()
|
||||
except:
|
||||
except IndexError:
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify=selectedIndexChanged)
|
||||
def selectedScriptStackId(self) -> Optional[str]:
|
||||
try:
|
||||
return self._script_list[self._selected_script_index].getStackId()
|
||||
except:
|
||||
except IndexError:
|
||||
return ""
|
||||
|
||||
## Execute all post-processing scripts on the gcode.
|
||||
|
@ -25,7 +25,7 @@ UM.Dialog
|
||||
{
|
||||
if(!visible) //Whenever the window is closed (either via the "Close" button or the X on the window frame), we want to update it in the stack.
|
||||
{
|
||||
manager.writeScriptsToStack();
|
||||
manager.writeScriptsToStack()
|
||||
}
|
||||
}
|
||||
|
||||
@ -61,18 +61,23 @@ UM.Dialog
|
||||
anchors.leftMargin: base.textMargin
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: base.textMargin
|
||||
font: UM.Theme.getFont("large")
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
ListView
|
||||
{
|
||||
id: activeScriptsList
|
||||
anchors.top: activeScriptsHeader.bottom
|
||||
anchors.topMargin: base.textMargin
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: base.textMargin
|
||||
|
||||
anchors
|
||||
{
|
||||
top: activeScriptsHeader.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
rightMargin: base.textMargin
|
||||
topMargin: base.textMargin
|
||||
leftMargin: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
|
||||
height: childrenRect.height
|
||||
model: manager.scriptList
|
||||
delegate: Item
|
||||
@ -84,8 +89,12 @@ UM.Dialog
|
||||
id: activeScriptButton
|
||||
text: manager.getScriptLabelByKey(modelData.toString())
|
||||
exclusiveGroup: selectedScriptGroup
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("setting").height
|
||||
checkable: true
|
||||
checked: {
|
||||
|
||||
checked:
|
||||
{
|
||||
if (manager.selectedScriptIndex == index)
|
||||
{
|
||||
base.activeScriptName = manager.getScriptLabelByKey(modelData.toString())
|
||||
@ -102,8 +111,7 @@ UM.Dialog
|
||||
manager.setSelectedScriptIndex(index)
|
||||
base.activeScriptName = manager.getScriptLabelByKey(modelData.toString())
|
||||
}
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("setting").height
|
||||
|
||||
style: ButtonStyle
|
||||
{
|
||||
background: Rectangle
|
||||
@ -121,6 +129,7 @@ UM.Dialog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
id: removeButton
|
||||
@ -141,7 +150,6 @@ UM.Dialog
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: Math.round(control.width / 2.7)
|
||||
height: Math.round(control.height / 2.7)
|
||||
sourceSize.width: width
|
||||
sourceSize.height: width
|
||||
color: palette.text
|
||||
source: UM.Theme.getIcon("cross1")
|
||||
@ -176,7 +184,6 @@ UM.Dialog
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: Math.round(control.width / 2.5)
|
||||
height: Math.round(control.height / 2.5)
|
||||
sourceSize.width: width
|
||||
sourceSize.height: width
|
||||
color: control.enabled ? palette.text : disabledPalette.text
|
||||
source: UM.Theme.getIcon("arrow_bottom")
|
||||
@ -211,7 +218,6 @@ UM.Dialog
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: Math.round(control.width / 2.5)
|
||||
height: Math.round(control.height / 2.5)
|
||||
sourceSize.width: width
|
||||
sourceSize.height: width
|
||||
color: control.enabled ? palette.text : disabledPalette.text
|
||||
source: UM.Theme.getIcon("arrow_top")
|
||||
@ -252,15 +258,15 @@ UM.Dialog
|
||||
onTriggered: manager.addScriptToList(modelData.toString())
|
||||
}
|
||||
|
||||
onObjectAdded: scriptsMenu.insertItem(index, object);
|
||||
onObjectRemoved: scriptsMenu.removeItem(object);
|
||||
onObjectAdded: scriptsMenu.insertItem(index, object)
|
||||
onObjectRemoved: scriptsMenu.removeItem(object)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
color: UM.Theme.getColor("sidebar")
|
||||
color: UM.Theme.getColor("main_background")
|
||||
anchors.left: activeScripts.right
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||
anchors.right: parent.right
|
||||
@ -271,26 +277,35 @@ UM.Dialog
|
||||
{
|
||||
id: scriptSpecsHeader
|
||||
text: manager.selectedScriptIndex == -1 ? catalog.i18nc("@label", "Settings") : base.activeScriptName
|
||||
anchors.top: parent.top
|
||||
anchors.topMargin: base.textMargin
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: base.textMargin
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: base.textMargin
|
||||
anchors
|
||||
{
|
||||
top: parent.top
|
||||
topMargin: base.textMargin
|
||||
left: parent.left
|
||||
leftMargin: base.textMargin
|
||||
right: parent.right
|
||||
rightMargin: base.textMargin
|
||||
}
|
||||
|
||||
elide: Text.ElideRight
|
||||
height: 20 * screenScaleFactor
|
||||
font: UM.Theme.getFont("large")
|
||||
font: UM.Theme.getFont("large_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
ScrollView
|
||||
{
|
||||
id: scrollView
|
||||
anchors.top: scriptSpecsHeader.bottom
|
||||
anchors.topMargin: settingsPanel.textMargin
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors
|
||||
{
|
||||
top: scriptSpecsHeader.bottom
|
||||
topMargin: settingsPanel.textMargin
|
||||
left: parent.left
|
||||
leftMargin: UM.Theme.getSize("default_margin").width
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
visible: manager.selectedScriptDefinitionId != ""
|
||||
style: UM.Theme.styles.scrollview;
|
||||
|
||||
@ -300,10 +315,11 @@ UM.Dialog
|
||||
spacing: UM.Theme.getSize("default_lining").height
|
||||
model: UM.SettingDefinitionsModel
|
||||
{
|
||||
id: definitionsModel;
|
||||
id: definitionsModel
|
||||
containerId: manager.selectedScriptDefinitionId
|
||||
showAll: true
|
||||
}
|
||||
|
||||
delegate: Loader
|
||||
{
|
||||
id: settingLoader
|
||||
@ -315,23 +331,24 @@ UM.Dialog
|
||||
{
|
||||
if(model.type != undefined)
|
||||
{
|
||||
return UM.Theme.getSize("section").height;
|
||||
return UM.Theme.getSize("section").height
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
return 0
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return 0;
|
||||
return 0
|
||||
}
|
||||
|
||||
}
|
||||
Behavior on height { NumberAnimation { duration: 100 } }
|
||||
opacity: provider.properties.enabled == "True" ? 1 : 0
|
||||
|
||||
Behavior on opacity { NumberAnimation { duration: 100 } }
|
||||
enabled: opacity > 0
|
||||
|
||||
property var definition: model
|
||||
property var settingDefinitionsModel: definitionsModel
|
||||
property var propertyProvider: provider
|
||||
@ -342,11 +359,12 @@ UM.Dialog
|
||||
//causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely.
|
||||
asynchronous: model.type != "enum" && model.type != "extruder"
|
||||
|
||||
onLoaded: {
|
||||
onLoaded:
|
||||
{
|
||||
settingLoader.item.showRevertButton = false
|
||||
settingLoader.item.showInheritButton = false
|
||||
settingLoader.item.showLinkedSettingIcon = false
|
||||
settingLoader.item.doDepthIndentation = true
|
||||
settingLoader.item.doDepthIndentation = false
|
||||
settingLoader.item.doQualityUserSettingEmphasis = false
|
||||
}
|
||||
|
||||
@ -398,24 +416,20 @@ UM.Dialog
|
||||
|
||||
onShowTooltip:
|
||||
{
|
||||
tooltip.text = text;
|
||||
var position = settingLoader.mapToItem(settingsPanel, settingsPanel.x, 0);
|
||||
tooltip.show(position);
|
||||
tooltip.text = text
|
||||
var position = settingLoader.mapToItem(settingsPanel, settingsPanel.x, 0)
|
||||
tooltip.show(position)
|
||||
tooltip.target.x = position.x + 1
|
||||
}
|
||||
|
||||
onHideTooltip:
|
||||
{
|
||||
tooltip.hide();
|
||||
onHideTooltip: tooltip.hide()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Cura.SidebarTooltip
|
||||
Cura.PrintSetupTooltip
|
||||
{
|
||||
id: tooltip
|
||||
}
|
||||
@ -462,6 +476,7 @@ UM.Dialog
|
||||
Cura.SettingUnknown { }
|
||||
}
|
||||
}
|
||||
|
||||
rightButtons: Button
|
||||
{
|
||||
text: catalog.i18nc("@action:button", "Close")
|
||||
@ -469,44 +484,15 @@ UM.Dialog
|
||||
onClicked: dialog.accept()
|
||||
}
|
||||
|
||||
Button {
|
||||
Cura.SecondaryButton
|
||||
{
|
||||
objectName: "postProcessingSaveAreaButton"
|
||||
visible: activeScriptsList.count > 0
|
||||
height: UM.Theme.getSize("save_button_save_to_button").height
|
||||
width: height
|
||||
tooltip: catalog.i18nc("@info:tooltip", "Change active post-processing scripts")
|
||||
onClicked: dialog.show()
|
||||
|
||||
style: ButtonStyle {
|
||||
background: Rectangle {
|
||||
id: deviceSelectionIcon
|
||||
border.width: UM.Theme.getSize("default_lining").width
|
||||
border.color: !control.enabled ? UM.Theme.getColor("action_button_disabled_border") :
|
||||
control.pressed ? UM.Theme.getColor("action_button_active_border") :
|
||||
control.hovered ? UM.Theme.getColor("action_button_hovered_border") : UM.Theme.getColor("action_button_border")
|
||||
color: !control.enabled ? UM.Theme.getColor("action_button_disabled") :
|
||||
control.pressed ? UM.Theme.getColor("action_button_active") :
|
||||
control.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button")
|
||||
Behavior on color { ColorAnimation { duration: 50; } }
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Math.round(UM.Theme.getSize("save_button_text_margin").width / 2);
|
||||
width: parent.height
|
||||
height: parent.height
|
||||
|
||||
UM.RecolorImage {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: Math.round(parent.width / 2)
|
||||
height: Math.round(parent.height / 2)
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
color: !control.enabled ? UM.Theme.getColor("action_button_disabled_text") :
|
||||
control.pressed ? UM.Theme.getColor("action_button_active_text") :
|
||||
control.hovered ? UM.Theme.getColor("action_button_hovered_text") : UM.Theme.getColor("action_button_text");
|
||||
source: "postprocessing.svg"
|
||||
}
|
||||
}
|
||||
label: Label{ }
|
||||
}
|
||||
iconSource: "postprocessing.svg"
|
||||
fixedWidthMode: true
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Post Processing",
|
||||
"author": "Ultimaker",
|
||||
"version": "2.2",
|
||||
"api": 5,
|
||||
"version": "2.2.1",
|
||||
"api": "6.0",
|
||||
"description": "Extension that allows for user created scripts for post processing",
|
||||
"catalog": "cura"
|
||||
}
|
3
plugins/PostProcessingPlugin/scripts/ExampleScript.md
Normal file
3
plugins/PostProcessingPlugin/scripts/ExampleScript.md
Normal file
@ -0,0 +1,3 @@
|
||||
A good example script is SearchAndReplace.py.
|
||||
If you have any questions please ask them at:
|
||||
https://github.com/Ultimaker/Cura/issues
|
@ -1,43 +0,0 @@
|
||||
# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
|
||||
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||
from ..Script import Script
|
||||
|
||||
class ExampleScript(Script):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
def getSettingDataString(self):
|
||||
return """{
|
||||
"name":"Example script",
|
||||
"key": "ExampleScript",
|
||||
"metadata": {},
|
||||
"version": 2,
|
||||
"settings":
|
||||
{
|
||||
"test":
|
||||
{
|
||||
"label": "Test",
|
||||
"description": "None",
|
||||
"unit": "mm",
|
||||
"type": "float",
|
||||
"default_value": 0.5,
|
||||
"minimum_value": "0",
|
||||
"minimum_value_warning": "0.1",
|
||||
"maximum_value_warning": "1"
|
||||
},
|
||||
"derp":
|
||||
{
|
||||
"label": "zomg",
|
||||
"description": "afgasgfgasfgasf",
|
||||
"unit": "mm",
|
||||
"type": "float",
|
||||
"default_value": 0.5,
|
||||
"minimum_value": "0",
|
||||
"minimum_value_warning": "0.1",
|
||||
"maximum_value_warning": "1"
|
||||
}
|
||||
}
|
||||
}"""
|
||||
|
||||
def execute(self, data):
|
||||
return data
|
134
plugins/PrepareStage/PrepareMenu.qml
Normal file
134
plugins/PrepareStage/PrepareMenu.qml
Normal file
@ -0,0 +1,134 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
import QtGraphicalEffects 1.0 // For the dropshadow
|
||||
|
||||
Item
|
||||
{
|
||||
id: prepareMenu
|
||||
|
||||
UM.I18nCatalog
|
||||
{
|
||||
id: catalog
|
||||
name: "cura"
|
||||
}
|
||||
|
||||
// Item to ensure that all of the buttons are nicely centered.
|
||||
Item
|
||||
{
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: openFileButton.width + itemRow.width + UM.Theme.getSize("default_margin").width
|
||||
height: parent.height
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: itemRow
|
||||
|
||||
anchors.left: openFileButton.right
|
||||
anchors.leftMargin: UM.Theme.getSize("default_margin").width
|
||||
|
||||
width: Math.round(0.9 * prepareMenu.width)
|
||||
height: parent.height
|
||||
spacing: 0
|
||||
|
||||
Cura.MachineSelector
|
||||
{
|
||||
id: machineSelection
|
||||
headerCornerSide: Cura.RoundedRectangle.Direction.Left
|
||||
Layout.minimumWidth: UM.Theme.getSize("machine_selector_widget").width
|
||||
Layout.maximumWidth: UM.Theme.getSize("machine_selector_widget").width
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
// Separator line
|
||||
Rectangle
|
||||
{
|
||||
height: parent.height
|
||||
width: UM.Theme.getSize("default_lining").width
|
||||
color: UM.Theme.getColor("lining")
|
||||
}
|
||||
|
||||
Cura.ConfigurationMenu
|
||||
{
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: itemRow.width - machineSelection.width - printSetupSelectorItem.width - 2 * UM.Theme.getSize("default_lining").width
|
||||
}
|
||||
|
||||
// Separator line
|
||||
Rectangle
|
||||
{
|
||||
height: parent.height
|
||||
width: UM.Theme.getSize("default_lining").width
|
||||
color: UM.Theme.getColor("lining")
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
id: printSetupSelectorItem
|
||||
// This is a work around to prevent the printSetupSelector from having to be re-loaded every time
|
||||
// a stage switch is done.
|
||||
children: [printSetupSelector]
|
||||
height: childrenRect.height
|
||||
width: childrenRect.width
|
||||
}
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
id: openFileButton
|
||||
height: UM.Theme.getSize("stage_menu").height
|
||||
width: UM.Theme.getSize("stage_menu").height
|
||||
onClicked: Cura.Actions.open.trigger()
|
||||
hoverEnabled: true
|
||||
|
||||
contentItem: Item
|
||||
{
|
||||
anchors.fill: parent
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: buttonIcon
|
||||
anchors.centerIn: parent
|
||||
source: UM.Theme.getIcon("load")
|
||||
width: UM.Theme.getSize("button_icon").width
|
||||
height: UM.Theme.getSize("button_icon").height
|
||||
color: UM.Theme.getColor("icon")
|
||||
|
||||
sourceSize.height: height
|
||||
}
|
||||
}
|
||||
|
||||
background: Rectangle
|
||||
{
|
||||
id: background
|
||||
height: UM.Theme.getSize("stage_menu").height
|
||||
width: UM.Theme.getSize("stage_menu").height
|
||||
|
||||
radius: UM.Theme.getSize("default_radius").width
|
||||
color: openFileButton.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button")
|
||||
}
|
||||
|
||||
DropShadow
|
||||
{
|
||||
id: shadow
|
||||
// Don't blur the shadow
|
||||
radius: 0
|
||||
anchors.fill: background
|
||||
source: background
|
||||
verticalOffset: 2
|
||||
visible: true
|
||||
color: UM.Theme.getColor("action_button_shadow")
|
||||
// Should always be drawn behind the background.
|
||||
z: background.z - 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -1,19 +1,19 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import os.path
|
||||
from UM.Application import Application
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Resources import Resources
|
||||
from cura.Stages.CuraStage import CuraStage
|
||||
|
||||
|
||||
|
||||
## Stage for preparing model (slicing).
|
||||
class PrepareStage(CuraStage):
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
|
||||
|
||||
def _engineCreated(self):
|
||||
sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles),
|
||||
"PrepareSidebar.qml")
|
||||
self.addDisplayComponent("sidebar", sidebar_component_path)
|
||||
menu_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMenu.qml")
|
||||
self.addDisplayComponent("menu", menu_component_path)
|
||||
|
@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "Prepare Stage",
|
||||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "Provides a prepare stage in Cura.",
|
||||
"api": 5,
|
||||
"api": "6.0",
|
||||
"i18n-catalog": "cura"
|
||||
}
|
18
plugins/PreviewStage/PreviewMain.qml
Normal file
18
plugins/PreviewStage/PreviewMain.qml
Normal file
@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.4
|
||||
import QtQuick.Controls 1.2
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Controls.Styles 1.1
|
||||
|
||||
import UM 1.0 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
|
||||
Loader
|
||||
{
|
||||
id: previewMain
|
||||
|
||||
source: UM.Controller.activeView != null && UM.Controller.activeView.mainComponent != null ? UM.Controller.activeView.mainComponent : ""
|
||||
}
|
79
plugins/PreviewStage/PreviewMenu.qml
Normal file
79
plugins/PreviewStage/PreviewMenu.qml
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
import Cura 1.1 as Cura
|
||||
|
||||
Item
|
||||
{
|
||||
id: previewMenu
|
||||
|
||||
property real itemHeight: height - 2 * UM.Theme.getSize("default_lining").width
|
||||
|
||||
UM.I18nCatalog
|
||||
{
|
||||
id: catalog
|
||||
name: "cura"
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
id: stageMenuRow
|
||||
anchors.centerIn: parent
|
||||
height: parent.height
|
||||
width: childrenRect.width
|
||||
|
||||
// We want this row to have a preferred with equals to the 85% of the parent
|
||||
property int preferredWidth: Math.round(0.85 * previewMenu.width)
|
||||
|
||||
Cura.ViewsSelector
|
||||
{
|
||||
id: viewsSelector
|
||||
height: parent.height
|
||||
width: UM.Theme.getSize("views_selector").width
|
||||
headerCornerSide: Cura.RoundedRectangle.Direction.Left
|
||||
}
|
||||
|
||||
// Separator line
|
||||
Rectangle
|
||||
{
|
||||
height: parent.height
|
||||
// If there is no viewPanel, we only need a single spacer, so hide this one.
|
||||
visible: viewPanel.source != ""
|
||||
width: visible ? UM.Theme.getSize("default_lining").width : 0
|
||||
|
||||
color: UM.Theme.getColor("lining")
|
||||
}
|
||||
|
||||
// This component will grow freely up to complete the preferredWidth of the row.
|
||||
Loader
|
||||
{
|
||||
id: viewPanel
|
||||
height: parent.height
|
||||
width: source != "" ? (stageMenuRow.preferredWidth - viewsSelector.width - printSetupSelectorItem.width - 2 * UM.Theme.getSize("default_lining").width) : 0
|
||||
source: UM.Controller.activeView != null && UM.Controller.activeView.stageMenuComponent != null ? UM.Controller.activeView.stageMenuComponent : ""
|
||||
}
|
||||
|
||||
// Separator line
|
||||
Rectangle
|
||||
{
|
||||
height: parent.height
|
||||
width: UM.Theme.getSize("default_lining").width
|
||||
color: UM.Theme.getColor("lining")
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
id: printSetupSelectorItem
|
||||
// This is a work around to prevent the printSetupSelector from having to be re-loaded every time
|
||||
// a stage switch is done.
|
||||
children: [printSetupSelector]
|
||||
height: childrenRect.height
|
||||
width: childrenRect.width
|
||||
}
|
||||
}
|
||||
}
|
51
plugins/PreviewStage/PreviewStage.py
Normal file
51
plugins/PreviewStage/PreviewStage.py
Normal file
@ -0,0 +1,51 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os.path
|
||||
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from cura.Stages.CuraStage import CuraStage
|
||||
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.View.View import View
|
||||
|
||||
|
||||
## Displays a preview of what you're about to print.
|
||||
#
|
||||
# The Python component of this stage just loads PreviewMain.qml for display
|
||||
# when the stage is selected, and makes sure that it reverts to the previous
|
||||
# view when the previous stage is activated.
|
||||
class PreviewStage(CuraStage):
|
||||
def __init__(self, application: QtApplication, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._application = application
|
||||
self._application.engineCreatedSignal.connect(self._engineCreated)
|
||||
self._previously_active_view = None # type: Optional[View]
|
||||
|
||||
## When selecting the stage, remember which was the previous view so that
|
||||
# we can revert to that view when we go out of the stage later.
|
||||
def onStageSelected(self) -> None:
|
||||
self._previously_active_view = self._application.getController().getActiveView()
|
||||
|
||||
## Called when going to a different stage (away from the Preview Stage).
|
||||
#
|
||||
# When going to a different stage, the view should be reverted to what it
|
||||
# was before. Normally, that just reverts it to solid view.
|
||||
def onStageDeselected(self) -> None:
|
||||
if self._previously_active_view is not None:
|
||||
self._application.getController().setActiveView(self._previously_active_view.getPluginId())
|
||||
self._previously_active_view = None
|
||||
|
||||
## Delayed load of the QML files.
|
||||
#
|
||||
# We need to make sure that the QML engine is running before we can load
|
||||
# these.
|
||||
def _engineCreated(self) -> None:
|
||||
plugin_path = self._application.getPluginRegistry().getPluginPath(self.getPluginId())
|
||||
if plugin_path is not None:
|
||||
menu_component_path = os.path.join(plugin_path, "PreviewMenu.qml")
|
||||
main_component_path = os.path.join(plugin_path, "PreviewMain.qml")
|
||||
self.addDisplayComponent("menu", menu_component_path)
|
||||
self.addDisplayComponent("main", main_component_path)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user