diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py index 7a3b5ced63..922f23d30f 100755 --- a/cura/BuildVolume.py +++ b/cura/BuildVolume.py @@ -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. from cura.Scene.CuraSceneNode import CuraSceneNode @@ -74,6 +74,11 @@ class BuildVolume(SceneNode): self._adhesion_type = None self._platform = Platform(self) + self._build_volume_message = Message(catalog.i18nc("@info:status", + "The build volume height has been reduced due to the value of the" + " \"Print Sequence\" setting to prevent the gantry from colliding" + " with printed models."), title = catalog.i18nc("@info:title", "Build Volume")) + self._global_container_stack = None Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged) self._onStackChanged() @@ -97,11 +102,6 @@ class BuildVolume(SceneNode): self._setting_change_timer.setSingleShot(True) self._setting_change_timer.timeout.connect(self._onSettingChangeTimerFinished) - self._build_volume_message = Message(catalog.i18nc("@info:status", - "The build volume height has been reduced due to the value of the" - " \"Print Sequence\" setting to prevent the gantry from colliding" - " with printed models."), title = catalog.i18nc("@info:title","Build Volume")) - # 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. diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py index 0c6740f740..fa09db3e50 100644 --- a/cura/CrashHandler.py +++ b/cura/CrashHandler.py @@ -1,7 +1,6 @@ # Copyright (c) 2017 Ultimaker B.V. # Cura is released under the terms of the LGPLv3 or higher. -import sys import platform import traceback import faulthandler @@ -13,9 +12,11 @@ import json import ssl import urllib.request import urllib.error +import shutil +import sys -from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QCoreApplication -from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox +from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR +from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton from UM.Application import Application from UM.Logger import Logger @@ -49,10 +50,11 @@ fatal_exception_types = [ class CrashHandler: crash_url = "https://stats.ultimaker.com/api/cura" - def __init__(self, exception_type, value, tb): + def __init__(self, exception_type, value, tb, has_started = True): self.exception_type = exception_type self.value = value self.traceback = tb + self.has_started = has_started self.dialog = None # Don't create a QDialog before there is a QApplication # While we create the GUI, the information will be stored for sending afterwards @@ -64,21 +66,130 @@ class CrashHandler: for part in line.rstrip("\n").split("\n"): Logger.log("c", part) - if not CuraDebugMode and exception_type not in fatal_exception_types: + # 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: return - application = QCoreApplication.instance() - if not application: - sys.exit(1) + if not has_started: + self._send_report_checkbox = None + self.early_crash_dialog = self._createEarlyCrashDialog() self.dialog = QDialog() self._createDialog() + def _createEarlyCrashDialog(self): + dialog = QDialog() + dialog.setMinimumWidth(500) + dialog.setMinimumHeight(170) + dialog.setWindowTitle(catalog.i18nc("@title:window", "Cura Crashed")) + dialog.finished.connect(self._closeEarlyCrashDialog) + + layout = QVBoxLayout(dialog) + + label = QLabel() + label.setText(catalog.i18nc("@label crash message", """

A fatal error has occurred.

+

Unfortunately, Cura encountered an unrecoverable error during start up. It was possibly caused by some incorrect configuration files. We suggest to backup and reset your configuration.

+

Please send us this Crash Report to fix the problem.

+ """)) + label.setWordWrap(True) + layout.addWidget(label) + + # "send report" check box and show details + self._send_report_checkbox = QCheckBox(catalog.i18nc("@action:button", "Send crash report to Ultimaker"), dialog) + self._send_report_checkbox.setChecked(True) + + show_details_button = QPushButton(catalog.i18nc("@action:button", "Show detailed crash report"), dialog) + show_details_button.setMaximumWidth(200) + show_details_button.clicked.connect(self._showDetailedReport) + + layout.addWidget(self._send_report_checkbox) + layout.addWidget(show_details_button) + + # "backup and start clean" and "close" buttons + buttons = QDialogButtonBox() + buttons.addButton(QDialogButtonBox.Close) + buttons.addButton(catalog.i18nc("@action:button", "Backup and Reset Configuration"), QDialogButtonBox.AcceptRole) + buttons.rejected.connect(self._closeEarlyCrashDialog) + buttons.accepted.connect(self._backupAndStartClean) + + layout.addWidget(buttons) + + return dialog + + def _closeEarlyCrashDialog(self): + if self._send_report_checkbox.isChecked(): + self._sendCrashReport() + os._exit(1) + + def _backupAndStartClean(self): + # backup the current cura directories and create clean ones + from cura.CuraVersion import CuraVersion + from UM.Resources import Resources + # The early crash may happen before those information is set in Resources, so we need to set them here to + # make sure that Resources can find the correct place. + Resources.ApplicationIdentifier = "cura" + Resources.ApplicationVersion = CuraVersion + config_path = Resources.getConfigStoragePath() + data_path = Resources.getDataStoragePath() + cache_path = Resources.getCacheStoragePath() + + folders_to_backup = [] + folders_to_remove = [] # only cache folder needs to be removed + + folders_to_backup.append(config_path) + if data_path != config_path: + folders_to_backup.append(data_path) + + # Only remove the cache folder if it's not the same as data or config + if cache_path not in (config_path, data_path): + folders_to_remove.append(cache_path) + + for folder in folders_to_remove: + shutil.rmtree(folder, ignore_errors = True) + for folder in folders_to_backup: + base_name = os.path.basename(folder) + root_dir = os.path.dirname(folder) + + import datetime + date_now = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + idx = 0 + file_name = base_name + "_" + date_now + zip_file_path = os.path.join(root_dir, file_name + ".zip") + while os.path.exists(zip_file_path): + idx += 1 + file_name = base_name + "_" + date_now + "_" + idx + zip_file_path = os.path.join(root_dir, file_name + ".zip") + try: + # remove the .zip extension because make_archive() adds it + zip_file_path = zip_file_path[:-4] + shutil.make_archive(zip_file_path, "zip", root_dir = root_dir, base_dir = base_name) + + # remove the folder only when the backup is successful + shutil.rmtree(folder, ignore_errors = True) + # create an empty folder so Resources will not try to copy the old ones + os.makedirs(folder, 0o0755, exist_ok=True) + + except Exception as e: + Logger.logException("e", "Failed to backup [%s] to file [%s]", folder, zip_file_path) + if not self.has_started: + print("Failed to backup [%s] to file [%s]: %s", folder, zip_file_path, e) + + self.early_crash_dialog.close() + + def _showDetailedReport(self): + self.dialog.exec_() + ## Creates a modal dialog. def _createDialog(self): self.dialog.setMinimumWidth(640) self.dialog.setMinimumHeight(640) self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report")) + # if the application has not fully started, this will be a detailed report dialog which should not + # close the application when it's closed. + if self.has_started: + self.dialog.finished.connect(self._close) layout = QVBoxLayout(self.dialog) @@ -89,6 +200,9 @@ class CrashHandler: layout.addWidget(self._userDescriptionWidget()) layout.addWidget(self._buttonsWidget()) + def _close(self): + os._exit(1) + def _messageWidget(self): label = QLabel() label.setText(catalog.i18nc("@label crash message", """

A fatal error has occurred. Please send us this Crash Report to fix the problem

@@ -148,8 +262,8 @@ class CrashHandler: layout = QVBoxLayout() text_area = QTextEdit() - trace_dict = traceback.format_exception(self.exception_type, self.value, self.traceback) - trace = "".join(trace_dict) + trace_list = traceback.format_exception(self.exception_type, self.value, self.traceback) + trace = "".join(trace_list) text_area.setText(trace) text_area.setReadOnly(True) @@ -157,14 +271,28 @@ class CrashHandler: group.setLayout(layout) # Parsing all the information to fill the dictionary - summary = trace_dict[len(trace_dict)-1].rstrip("\n") - module = trace_dict[len(trace_dict)-2].rstrip("\n").split("\n") + summary = "" + if len(trace_list) >= 1: + summary = trace_list[len(trace_list)-1].rstrip("\n") + module = [""] + if len(trace_list) >= 2: + module = trace_list[len(trace_list)-2].rstrip("\n").split("\n") module_split = module[0].split(", ") - filepath = module_split[0].split("\"")[1] + + filepath_directory_split = module_split[0].split("\"") + filepath = "" + if len(filepath_directory_split) > 1: + filepath = filepath_directory_split[1] directory, filename = os.path.split(filepath) - line = int(module_split[1].lstrip("line ")) - function = module_split[2].lstrip("in ") - code = module[1].lstrip(" ") + line = "" + if len(module_split) > 1: + line = int(module_split[1].lstrip("line ")) + function = "" + if len(module_split) > 2: + function = module_split[2].lstrip("in ") + code = "" + if len(module) > 1: + code = module[1].lstrip(" ") # Using this workaround for a cross-platform path splitting split_path = [] @@ -249,9 +377,13 @@ class CrashHandler: def _buttonsWidget(self): buttons = QDialogButtonBox() buttons.addButton(QDialogButtonBox.Close) - buttons.addButton(catalog.i18nc("@action:button", "Send report"), QDialogButtonBox.AcceptRole) + # Like above, this will be served as a separate detailed report dialog if the application has not yet been + # fully loaded. In this case, "send report" will be a check box in the early crash dialog, so there is no + # need for this extra button. + if self.has_started: + buttons.addButton(catalog.i18nc("@action:button", "Send report"), QDialogButtonBox.AcceptRole) + buttons.accepted.connect(self._sendCrashReport) buttons.rejected.connect(self.dialog.close) - buttons.accepted.connect(self._sendCrashReport) return buttons @@ -269,15 +401,23 @@ class CrashHandler: kwoptions["context"] = ssl._create_unverified_context() Logger.log("i", "Sending crash report info to [%s]...", self.crash_url) + if not self.has_started: + print("Sending crash report info to [%s]...\n" % self.crash_url) try: f = urllib.request.urlopen(self.crash_url, **kwoptions) Logger.log("i", "Sent crash report info.") + if not self.has_started: + print("Sent crash report info.\n") f.close() - except urllib.error.HTTPError: + except urllib.error.HTTPError as e: Logger.logException("e", "An HTTP error occurred while trying to send crash report") - except Exception: # We don't want any exception to cause problems + if not self.has_started: + print("An HTTP error occurred while trying to send crash report: %s" % e) + except Exception as e: # We don't want any exception to cause problems Logger.logException("e", "An exception occurred while trying to send crash report") + if not self.has_started: + print("An exception occurred while trying to send crash report: %s" % e) os._exit(1) diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py index 086cc8bd72..4ff39b84a4 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -116,6 +116,8 @@ class CuraApplication(QtApplication): # changes of the settings. SettingVersion = 4 + Created = False + class ResourceTypes: QmlFiles = Resources.UserType + 1 Firmware = Resources.UserType + 2 @@ -136,7 +138,6 @@ class CuraApplication(QtApplication): stacksValidationFinished = pyqtSignal() # Emitted whenever a validation is finished def __init__(self, **kwargs): - # this list of dir names will be used by UM to detect an old cura directory for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "user", "variants"]: Resources.addExpectedDirNameInData(dir_name) @@ -227,6 +228,10 @@ class CuraApplication(QtApplication): tray_icon_name = "cura-icon-32.png", **kwargs) + # FOR TESTING ONLY + if kwargs["parsed_command_line"].get("trigger_early_crash", False): + assert not "This crash is triggered by the trigger_early_crash command line argument." + self.default_theme = "cura-light" self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png"))) @@ -260,7 +265,7 @@ class CuraApplication(QtApplication): self._center_after_select = False self._camera_animation = None self._cura_actions = None - self._started = False + self.started = False self._message_box_callback = None self._message_box_callback_arguments = [] @@ -375,6 +380,8 @@ class CuraApplication(QtApplication): self.getCuraSceneController().setActiveBuildPlate(0) # Initialize + CuraApplication.Created = True + @pyqtSlot(str, result = str) def getVisibilitySettingPreset(self, settings_preset_name) -> str: result = self._loadPresetSettingVisibilityGroup(settings_preset_name) @@ -461,7 +468,6 @@ class CuraApplication(QtApplication): return result - def _onEngineCreated(self): self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider()) @@ -556,7 +562,7 @@ class CuraApplication(QtApplication): # # Note that the AutoSave plugin also calls this method. def saveSettings(self): - if not self._started: # Do not do saving during application start + if not self.started: # Do not do saving during application start return ContainerRegistry.getInstance().saveDirtyContainers() @@ -734,7 +740,7 @@ class CuraApplication(QtApplication): for file_name in self._open_file_queue: # Open all the files that were queued up while plug-ins were loading. self._openFile(file_name) - self._started = True + self.started = True self.exec_() ## Run Cura without GUI elements and interaction (server mode). diff --git a/cura/Settings/CuraContainerRegistry.py b/cura/Settings/CuraContainerRegistry.py index 0b328cebda..148ed6fe59 100644 --- a/cura/Settings/CuraContainerRegistry.py +++ b/cura/Settings/CuraContainerRegistry.py @@ -606,6 +606,7 @@ class CuraContainerRegistry(ContainerRegistry): extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) if extruder_quality_changes_container: quality_changes_id = extruder_quality_changes_container.getId() + extruder_quality_changes_container.addMetaDataEntry("extruder", extruder_stack.definition.getId()) extruder_stack.setQualityChangesById(quality_changes_id) else: # if we still cannot find a quality changes container for the extruder, create a new one @@ -711,7 +712,7 @@ class CuraContainerRegistry(ContainerRegistry): if not os.path.isfile(file_path): continue - parser = configparser.ConfigParser() + parser = configparser.ConfigParser(interpolation=None) try: parser.read([file_path]) except: diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py index f9f0fbb401..35b5b1320b 100755 --- a/cura/Settings/ExtruderManager.py +++ b/cura/Settings/ExtruderManager.py @@ -519,13 +519,19 @@ class ExtruderManager(QObject): material_diameter = 0 material_approximate_diameter = str(round(material_diameter)) - machine_diameter = extruder_stack.definitionChanges.getProperty("material_diameter", "value") - if not machine_diameter: + material_diameter = extruder_stack.definitionChanges.getProperty("material_diameter", "value") + setting_provider = extruder_stack + if not material_diameter: if extruder_stack.definition.hasProperty("material_diameter", "value"): - machine_diameter = extruder_stack.definition.getProperty("material_diameter", "value") + material_diameter = extruder_stack.definition.getProperty("material_diameter", "value") else: - machine_diameter = global_stack.definition.getProperty("material_diameter", "value") - machine_approximate_diameter = str(round(machine_diameter)) + material_diameter = global_stack.definition.getProperty("material_diameter", "value") + setting_provider = global_stack + + if isinstance(material_diameter, SettingFunction): + material_diameter = material_diameter(setting_provider) + + machine_approximate_diameter = str(round(material_diameter)) if material_approximate_diameter != machine_approximate_diameter: Logger.log("i", "The the currently active material(s) do not match the diameter set for the printer. Finding alternatives.") diff --git a/cura_app.py b/cura_app.py index b9afb9bbcc..6c2d1c2937 100755 --- a/cura_app.py +++ b/cura_app.py @@ -16,6 +16,12 @@ 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"]: @@ -26,7 +32,7 @@ if not known_args["debug"]: return os.path.expanduser("~/.local/share/cura") elif Platform.isOSX(): return os.path.expanduser("~/Library/Logs/cura") - + if hasattr(sys, "frozen"): dirpath = get_cura_dir_path() os.makedirs(dirpath, exist_ok = True) @@ -71,8 +77,45 @@ if "PYTHONPATH" in os.environ.keys(): # If PYTHONPATH is u def exceptHook(hook_type, value, traceback): from cura.CrashHandler import CrashHandler - _crash_handler = CrashHandler(hook_type, value, traceback) - _crash_handler.show() + from cura.CuraApplication import CuraApplication + has_started = False + if CuraApplication.Created: + has_started = CuraApplication.getInstance().started + + # + # When the exception hook is triggered, the QApplication may not have been initialized yet. In this case, we don't + # have an QApplication to handle the event loop, which is required by the Crash Dialog. + # The flag "CuraApplication.Created" is set to True when CuraApplication finishes its constructor call. + # + # Before the "started" flag is set to True, the Qt event loop has not started yet. The event loop is a blocking + # call to the QApplication.exec_(). In this case, we need to: + # 1. Remove all scheduled events so no more unnecessary events will be processed, such as loading the main dialog, + # loading the machine, etc. + # 2. Start the Qt event loop with exec_() and show the Crash Dialog. + # + # If the application has finished its initialization and was running fine, and then something causes a crash, + # we run the old routine to show the Crash Dialog. + # + from PyQt5.Qt import QApplication + if CuraApplication.Created: + _crash_handler = CrashHandler(hook_type, value, traceback, has_started) + if CuraApplication.splash is not None: + CuraApplication.splash.close() + if not has_started: + CuraApplication.getInstance().removePostedEvents(None) + _crash_handler.early_crash_dialog.show() + sys.exit(CuraApplication.getInstance().exec_()) + else: + _crash_handler.show() + else: + application = QApplication(sys.argv) + application.removePostedEvents(None) + _crash_handler = CrashHandler(hook_type, value, traceback, has_started) + # This means the QtApplication could be created and so the splash screen. Then Cura closes it + if CuraApplication.splash is not None: + CuraApplication.splash.close() + _crash_handler.early_crash_dialog.show() + sys.exit(application.exec_()) if not known_args["debug"]: sys.excepthook = exceptHook diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py index b28490ce4e..a2e02fa9d4 100755 --- a/plugins/3MFReader/ThreeMFWorkspaceReader.py +++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py @@ -122,7 +122,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # The default ContainerStack.deserialize() will connect signals, which is not desired in this case. # Since we know that the stack files are INI files, so we directly use the ConfigParser to parse them. serialized = archive.open(file_name).read().decode("utf-8") - stack_config = ConfigParser() + stack_config = ConfigParser(interpolation = None) stack_config.read_string(serialized) # sanity check @@ -303,7 +303,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): if containers_found_dict["machine"] and not machine_conflict: for extruder_stack_file in extruder_stack_files: serialized = archive.open(extruder_stack_file).read().decode("utf-8") - parser = configparser.ConfigParser() + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) # The check should be done for the extruder stack that's associated with the existing global stack, @@ -407,7 +407,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): ## Overrides an ExtruderStack in the given GlobalStack and returns the new ExtruderStack. def _overrideExtruderStack(self, global_stack, extruder_file_content, extruder_stack_file): # Get extruder position first - extruder_config = configparser.ConfigParser() + extruder_config = configparser.ConfigParser(interpolation = None) extruder_config.read_string(extruder_file_content) if not extruder_config.has_option("metadata", "position"): msg = "Could not find 'metadata/position' in extruder stack file" @@ -565,7 +565,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): serialized = archive.open(instance_container_file).read().decode("utf-8") # HACK! we ignore "quality" and "variant" instance containers! - parser = configparser.ConfigParser() + parser = configparser.ConfigParser(interpolation = None) parser.read_string(serialized) if not parser.has_option("metadata", "type"): Logger.log("w", "Cannot find metadata/type in %s, ignoring it", instance_container_file) @@ -763,7 +763,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader): # deserialize() by setting the metadata, but in the case of ExtruderStack, deserialize() # also does addExtruder() to its machine stack, so we have to make sure that it's pointing # to the right machine BEFORE deserialization. - extruder_config = configparser.ConfigParser() + extruder_config = configparser.ConfigParser(interpolation = None) extruder_config.read_string(extruder_file_content) extruder_config.set("metadata", "machine", global_stack_id_new) tmp_string_io = io.StringIO() diff --git a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py index 825259ad58..507274d355 100644 --- a/plugins/3MFWriter/ThreeMFWorkspaceWriter.py +++ b/plugins/3MFWriter/ThreeMFWorkspaceWriter.py @@ -57,7 +57,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter): # Save Cura version version_file = zipfile.ZipInfo("Cura/version.ini") - version_config_parser = configparser.ConfigParser() + version_config_parser = configparser.ConfigParser(interpolation = None) version_config_parser.add_section("versions") version_config_parser.set("versions", "cura_version", Application.getInstance().getVersion()) version_config_parser.set("versions", "build_type", Application.getInstance().getBuildType()) diff --git a/plugins/AutoSave/AutoSave.py b/plugins/AutoSave/AutoSave.py index 331f328f2d..5fdac502b5 100644 --- a/plugins/AutoSave/AutoSave.py +++ b/plugins/AutoSave/AutoSave.py @@ -9,6 +9,7 @@ from UM.Application import Application from UM.Resources import Resources from UM.Logger import Logger + class AutoSave(Extension): def __init__(self): super().__init__() @@ -16,18 +17,40 @@ class AutoSave(Extension): Preferences.getInstance().preferenceChanged.connect(self._triggerTimer) self._global_stack = None - Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) - self._onGlobalStackChanged() Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10) self._change_timer = QTimer() self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay")) self._change_timer.setSingleShot(True) - self._change_timer.timeout.connect(self._onTimeout) self._saving = False + # At this point, the Application instance has not finished its constructor call yet, so directly using something + # like Application.getInstance() is not correct. The initialisation now will only gets triggered after the + # application finishes its start up successfully. + self._init_timer = QTimer() + self._init_timer.setInterval(1000) + self._init_timer.setSingleShot(True) + self._init_timer.timeout.connect(self.initialize) + self._init_timer.start() + + def initialize(self): + # only initialise if the application is created and has started + from cura.CuraApplication import CuraApplication + if not CuraApplication.Created: + self._init_timer.start() + return + if not CuraApplication.getInstance().started: + self._init_timer.start() + return + + self._change_timer.timeout.connect(self._onTimeout) + Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) + self._onGlobalStackChanged() + + self._triggerTimer() + def _triggerTimer(self, *args): if not self._saving: self._change_timer.start() diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py index ae8acdaf54..df19b1dc9e 100644 --- a/plugins/CuraEngineBackend/StartSliceJob.py +++ b/plugins/CuraEngineBackend/StartSliceJob.py @@ -5,6 +5,7 @@ import numpy from string import Formatter from enum import IntEnum import time +import re from UM.Job import Job from UM.Application import Application @@ -343,10 +344,12 @@ class StartSliceJob(Job): # Pre-compute material material_bed_temp_prepend and material_print_temp_prepend start_gcode = settings["machine_start_gcode"] - bed_temperature_settings = {"material_bed_temperature", "material_bed_temperature_layer_0"} - settings["material_bed_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in bed_temperature_settings)) - print_temperature_settings = {"material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"} - settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_temperature_settings)) + bed_temperature_settings = ["material_bed_temperature", "material_bed_temperature_layer_0"] + pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr} + settings["material_bed_temp_prepend"] = re.search(pattern, start_gcode) == None + print_temperature_settings = ["material_print_temperature", "material_print_temperature_layer_0", "default_material_print_temperature", "material_initial_print_temperature", "material_final_print_temperature", "material_standby_temperature"] + pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(print_temperature_settings) # match {setting} as well as {setting, extruder_nr} + settings["material_print_temp_prepend"] = re.search(pattern, start_gcode) == None # Replace the setting tokens in start and end g-code. # Use values from the first used extruder by default so we get the expected temperatures diff --git a/resources/definitions/malyan_m200.def.json b/resources/definitions/malyan_m200.def.json index 9aae3a5244..365b031c43 100644 --- a/resources/definitions/malyan_m200.def.json +++ b/resources/definitions/malyan_m200.def.json @@ -51,7 +51,7 @@ "machine_height": { "default_value": 120 }, "machine_heated_bed": { "default_value": true }, "machine_center_is_zero": { "default_value": false }, - "material_diameter": { "value": 1.75 }, + "material_diameter": { "default_value": 1.75 }, "machine_nozzle_size": { "default_value": 0.4, "minimum_value": 0.15 diff --git a/resources/i18n/ko_KR/cura.po b/resources/i18n/ko_KR/cura.po index 1cabac3628..f63d7ba64f 100644 --- a/resources/i18n/ko_KR/cura.po +++ b/resources/i18n/ko_KR/cura.po @@ -1343,7 +1343,7 @@ msgstr "인터페이스로드 중 ..." #, python-format msgctxt "@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm." msgid "%(width).1f x %(depth).1f x %(height).1f mm" -msgstr "% (너비) .1f x % (깊이) .1f x % (높이) .1f mm" +msgstr "%(width).1f x %(depth).1f x %(height).1f mm" #: /home/ruben/Projects/Cura/cura/CuraApplication.py:1417 #, python-brace-format