Merge 3.2 into master

This commit is contained in:
Lipu Fei 2018-02-07 13:20:24 +01:00
commit ffa4df6a06
12 changed files with 277 additions and 55 deletions

View file

@ -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. # Cura is released under the terms of the LGPLv3 or higher.
from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.CuraSceneNode import CuraSceneNode
@ -74,6 +74,11 @@ class BuildVolume(SceneNode):
self._adhesion_type = None self._adhesion_type = None
self._platform = Platform(self) 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 self._global_container_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged) Application.getInstance().globalContainerStackChanged.connect(self._onStackChanged)
self._onStackChanged() self._onStackChanged()
@ -97,11 +102,6 @@ class BuildVolume(SceneNode):
self._setting_change_timer.setSingleShot(True) self._setting_change_timer.setSingleShot(True)
self._setting_change_timer.timeout.connect(self._onSettingChangeTimerFinished) 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. # Must be after setting _build_volume_message, apparently that is used in getMachineManager.
# activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality. # activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
# Therefore this works. # Therefore this works.

View file

@ -1,7 +1,6 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import sys
import platform import platform
import traceback import traceback
import faulthandler import faulthandler
@ -13,9 +12,11 @@ import json
import ssl import ssl
import urllib.request import urllib.request
import urllib.error import urllib.error
import shutil
import sys
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QCoreApplication from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
@ -49,10 +50,11 @@ fatal_exception_types = [
class CrashHandler: class CrashHandler:
crash_url = "https://stats.ultimaker.com/api/cura" 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.exception_type = exception_type
self.value = value self.value = value
self.traceback = tb self.traceback = tb
self.has_started = has_started
self.dialog = None # Don't create a QDialog before there is a QApplication 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 # 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"): for part in line.rstrip("\n").split("\n"):
Logger.log("c", part) 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 return
application = QCoreApplication.instance() if not has_started:
if not application: self._send_report_checkbox = None
sys.exit(1) self.early_crash_dialog = self._createEarlyCrashDialog()
self.dialog = QDialog() self.dialog = QDialog()
self._createDialog() 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", """<p><b>A fatal error has occurred.</p></b>
<p>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.</p>
<p>Please send us this Crash Report to fix the problem.</p>
"""))
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. ## Creates a modal dialog.
def _createDialog(self): def _createDialog(self):
self.dialog.setMinimumWidth(640) self.dialog.setMinimumWidth(640)
self.dialog.setMinimumHeight(640) self.dialog.setMinimumHeight(640)
self.dialog.setWindowTitle(catalog.i18nc("@title:window", "Crash Report")) 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) layout = QVBoxLayout(self.dialog)
@ -89,6 +200,9 @@ class CrashHandler:
layout.addWidget(self._userDescriptionWidget()) layout.addWidget(self._userDescriptionWidget())
layout.addWidget(self._buttonsWidget()) layout.addWidget(self._buttonsWidget())
def _close(self):
os._exit(1)
def _messageWidget(self): def _messageWidget(self):
label = QLabel() label = QLabel()
label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred. Please send us this Crash Report to fix the problem</p></b> label.setText(catalog.i18nc("@label crash message", """<p><b>A fatal error has occurred. Please send us this Crash Report to fix the problem</p></b>
@ -148,8 +262,8 @@ class CrashHandler:
layout = QVBoxLayout() layout = QVBoxLayout()
text_area = QTextEdit() text_area = QTextEdit()
trace_dict = traceback.format_exception(self.exception_type, self.value, self.traceback) trace_list = traceback.format_exception(self.exception_type, self.value, self.traceback)
trace = "".join(trace_dict) trace = "".join(trace_list)
text_area.setText(trace) text_area.setText(trace)
text_area.setReadOnly(True) text_area.setReadOnly(True)
@ -157,14 +271,28 @@ class CrashHandler:
group.setLayout(layout) group.setLayout(layout)
# Parsing all the information to fill the dictionary # Parsing all the information to fill the dictionary
summary = trace_dict[len(trace_dict)-1].rstrip("\n") summary = ""
module = trace_dict[len(trace_dict)-2].rstrip("\n").split("\n") 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(", ") 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) directory, filename = os.path.split(filepath)
line = int(module_split[1].lstrip("line ")) line = ""
function = module_split[2].lstrip("in ") if len(module_split) > 1:
code = module[1].lstrip(" ") 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 # Using this workaround for a cross-platform path splitting
split_path = [] split_path = []
@ -249,9 +377,13 @@ class CrashHandler:
def _buttonsWidget(self): def _buttonsWidget(self):
buttons = QDialogButtonBox() buttons = QDialogButtonBox()
buttons.addButton(QDialogButtonBox.Close) 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.rejected.connect(self.dialog.close)
buttons.accepted.connect(self._sendCrashReport)
return buttons return buttons
@ -269,15 +401,23 @@ class CrashHandler:
kwoptions["context"] = ssl._create_unverified_context() kwoptions["context"] = ssl._create_unverified_context()
Logger.log("i", "Sending crash report info to [%s]...", self.crash_url) 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: try:
f = urllib.request.urlopen(self.crash_url, **kwoptions) f = urllib.request.urlopen(self.crash_url, **kwoptions)
Logger.log("i", "Sent crash report info.") Logger.log("i", "Sent crash report info.")
if not self.has_started:
print("Sent crash report info.\n")
f.close() 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") 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") 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) os._exit(1)

View file

@ -116,6 +116,8 @@ class CuraApplication(QtApplication):
# changes of the settings. # changes of the settings.
SettingVersion = 4 SettingVersion = 4
Created = False
class ResourceTypes: class ResourceTypes:
QmlFiles = Resources.UserType + 1 QmlFiles = Resources.UserType + 1
Firmware = Resources.UserType + 2 Firmware = Resources.UserType + 2
@ -136,7 +138,6 @@ class CuraApplication(QtApplication):
stacksValidationFinished = pyqtSignal() # Emitted whenever a validation is finished stacksValidationFinished = pyqtSignal() # Emitted whenever a validation is finished
def __init__(self, **kwargs): def __init__(self, **kwargs):
# this list of dir names will be used by UM to detect an old cura directory # 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"]: for dir_name in ["extruders", "machine_instances", "materials", "plugins", "quality", "user", "variants"]:
Resources.addExpectedDirNameInData(dir_name) Resources.addExpectedDirNameInData(dir_name)
@ -227,6 +228,10 @@ class CuraApplication(QtApplication):
tray_icon_name = "cura-icon-32.png", tray_icon_name = "cura-icon-32.png",
**kwargs) **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.default_theme = "cura-light"
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png"))) self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
@ -260,7 +265,7 @@ class CuraApplication(QtApplication):
self._center_after_select = False self._center_after_select = False
self._camera_animation = None self._camera_animation = None
self._cura_actions = None self._cura_actions = None
self._started = False self.started = False
self._message_box_callback = None self._message_box_callback = None
self._message_box_callback_arguments = [] self._message_box_callback_arguments = []
@ -375,6 +380,8 @@ class CuraApplication(QtApplication):
self.getCuraSceneController().setActiveBuildPlate(0) # Initialize self.getCuraSceneController().setActiveBuildPlate(0) # Initialize
CuraApplication.Created = True
@pyqtSlot(str, result = str) @pyqtSlot(str, result = str)
def getVisibilitySettingPreset(self, settings_preset_name) -> str: def getVisibilitySettingPreset(self, settings_preset_name) -> str:
result = self._loadPresetSettingVisibilityGroup(settings_preset_name) result = self._loadPresetSettingVisibilityGroup(settings_preset_name)
@ -461,7 +468,6 @@ class CuraApplication(QtApplication):
return result return result
def _onEngineCreated(self): def _onEngineCreated(self):
self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider()) self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
@ -556,7 +562,7 @@ class CuraApplication(QtApplication):
# #
# Note that the AutoSave plugin also calls this method. # Note that the AutoSave plugin also calls this method.
def saveSettings(self): 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 return
ContainerRegistry.getInstance().saveDirtyContainers() 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. 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._openFile(file_name)
self._started = True self.started = True
self.exec_() self.exec_()
## Run Cura without GUI elements and interaction (server mode). ## Run Cura without GUI elements and interaction (server mode).

View file

@ -606,6 +606,7 @@ class CuraContainerRegistry(ContainerRegistry):
extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) extruder_quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName())
if extruder_quality_changes_container: if extruder_quality_changes_container:
quality_changes_id = extruder_quality_changes_container.getId() quality_changes_id = extruder_quality_changes_container.getId()
extruder_quality_changes_container.addMetaDataEntry("extruder", extruder_stack.definition.getId())
extruder_stack.setQualityChangesById(quality_changes_id) extruder_stack.setQualityChangesById(quality_changes_id)
else: else:
# if we still cannot find a quality changes container for the extruder, create a new one # 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): if not os.path.isfile(file_path):
continue continue
parser = configparser.ConfigParser() parser = configparser.ConfigParser(interpolation=None)
try: try:
parser.read([file_path]) parser.read([file_path])
except: except:

View file

@ -519,13 +519,19 @@ class ExtruderManager(QObject):
material_diameter = 0 material_diameter = 0
material_approximate_diameter = str(round(material_diameter)) material_approximate_diameter = str(round(material_diameter))
machine_diameter = extruder_stack.definitionChanges.getProperty("material_diameter", "value") material_diameter = extruder_stack.definitionChanges.getProperty("material_diameter", "value")
if not machine_diameter: setting_provider = extruder_stack
if not material_diameter:
if extruder_stack.definition.hasProperty("material_diameter", "value"): 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: else:
machine_diameter = global_stack.definition.getProperty("material_diameter", "value") material_diameter = global_stack.definition.getProperty("material_diameter", "value")
machine_approximate_diameter = str(round(machine_diameter)) 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: 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.") Logger.log("i", "The the currently active material(s) do not match the diameter set for the printer. Finding alternatives.")

View file

@ -16,6 +16,12 @@ parser.add_argument('--debug',
default = False, default = False,
help = "Turn on the debug mode by setting this option." 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]) known_args = vars(parser.parse_known_args()[0])
if not known_args["debug"]: if not known_args["debug"]:
@ -26,7 +32,7 @@ if not known_args["debug"]:
return os.path.expanduser("~/.local/share/cura") return os.path.expanduser("~/.local/share/cura")
elif Platform.isOSX(): elif Platform.isOSX():
return os.path.expanduser("~/Library/Logs/cura") return os.path.expanduser("~/Library/Logs/cura")
if hasattr(sys, "frozen"): if hasattr(sys, "frozen"):
dirpath = get_cura_dir_path() dirpath = get_cura_dir_path()
os.makedirs(dirpath, exist_ok = True) 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): def exceptHook(hook_type, value, traceback):
from cura.CrashHandler import CrashHandler from cura.CrashHandler import CrashHandler
_crash_handler = CrashHandler(hook_type, value, traceback) from cura.CuraApplication import CuraApplication
_crash_handler.show() 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"]: if not known_args["debug"]:
sys.excepthook = exceptHook sys.excepthook = exceptHook

View file

@ -122,7 +122,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# The default ContainerStack.deserialize() will connect signals, which is not desired in this case. # 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. # 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") serialized = archive.open(file_name).read().decode("utf-8")
stack_config = ConfigParser() stack_config = ConfigParser(interpolation = None)
stack_config.read_string(serialized) stack_config.read_string(serialized)
# sanity check # sanity check
@ -303,7 +303,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
if containers_found_dict["machine"] and not machine_conflict: if containers_found_dict["machine"] and not machine_conflict:
for extruder_stack_file in extruder_stack_files: for extruder_stack_file in extruder_stack_files:
serialized = archive.open(extruder_stack_file).read().decode("utf-8") serialized = archive.open(extruder_stack_file).read().decode("utf-8")
parser = configparser.ConfigParser() parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized) parser.read_string(serialized)
# The check should be done for the extruder stack that's associated with the existing global stack, # 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. ## Overrides an ExtruderStack in the given GlobalStack and returns the new ExtruderStack.
def _overrideExtruderStack(self, global_stack, extruder_file_content, extruder_stack_file): def _overrideExtruderStack(self, global_stack, extruder_file_content, extruder_stack_file):
# Get extruder position first # Get extruder position first
extruder_config = configparser.ConfigParser() extruder_config = configparser.ConfigParser(interpolation = None)
extruder_config.read_string(extruder_file_content) extruder_config.read_string(extruder_file_content)
if not extruder_config.has_option("metadata", "position"): if not extruder_config.has_option("metadata", "position"):
msg = "Could not find 'metadata/position' in extruder stack file" 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") serialized = archive.open(instance_container_file).read().decode("utf-8")
# HACK! we ignore "quality" and "variant" instance containers! # HACK! we ignore "quality" and "variant" instance containers!
parser = configparser.ConfigParser() parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized) parser.read_string(serialized)
if not parser.has_option("metadata", "type"): if not parser.has_option("metadata", "type"):
Logger.log("w", "Cannot find metadata/type in %s, ignoring it", instance_container_file) 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() # 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 # also does addExtruder() to its machine stack, so we have to make sure that it's pointing
# to the right machine BEFORE deserialization. # 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.read_string(extruder_file_content)
extruder_config.set("metadata", "machine", global_stack_id_new) extruder_config.set("metadata", "machine", global_stack_id_new)
tmp_string_io = io.StringIO() tmp_string_io = io.StringIO()

View file

@ -57,7 +57,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
# Save Cura version # Save Cura version
version_file = zipfile.ZipInfo("Cura/version.ini") 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.add_section("versions")
version_config_parser.set("versions", "cura_version", Application.getInstance().getVersion()) version_config_parser.set("versions", "cura_version", Application.getInstance().getVersion())
version_config_parser.set("versions", "build_type", Application.getInstance().getBuildType()) version_config_parser.set("versions", "build_type", Application.getInstance().getBuildType())

View file

@ -9,6 +9,7 @@ from UM.Application import Application
from UM.Resources import Resources from UM.Resources import Resources
from UM.Logger import Logger from UM.Logger import Logger
class AutoSave(Extension): class AutoSave(Extension):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
@ -16,18 +17,40 @@ class AutoSave(Extension):
Preferences.getInstance().preferenceChanged.connect(self._triggerTimer) Preferences.getInstance().preferenceChanged.connect(self._triggerTimer)
self._global_stack = None self._global_stack = None
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
self._onGlobalStackChanged()
Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10) Preferences.getInstance().addPreference("cura/autosave_delay", 1000 * 10)
self._change_timer = QTimer() self._change_timer = QTimer()
self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay")) self._change_timer.setInterval(Preferences.getInstance().getValue("cura/autosave_delay"))
self._change_timer.setSingleShot(True) self._change_timer.setSingleShot(True)
self._change_timer.timeout.connect(self._onTimeout)
self._saving = False 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): def _triggerTimer(self, *args):
if not self._saving: if not self._saving:
self._change_timer.start() self._change_timer.start()

View file

@ -5,6 +5,7 @@ import numpy
from string import Formatter from string import Formatter
from enum import IntEnum from enum import IntEnum
import time import time
import re
from UM.Job import Job from UM.Job import Job
from UM.Application import Application from UM.Application import Application
@ -343,10 +344,12 @@ class StartSliceJob(Job):
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend # Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
start_gcode = settings["machine_start_gcode"] start_gcode = settings["machine_start_gcode"]
bed_temperature_settings = {"material_bed_temperature", "material_bed_temperature_layer_0"} 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)) pattern = r"\{(%s)(,\s?\w+)?\}" % "|".join(bed_temperature_settings) # match {setting} as well as {setting, extruder_nr}
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_bed_temp_prepend"] = re.search(pattern, start_gcode) == None
settings["material_print_temp_prepend"] = all(("{" + setting + "}" not in start_gcode for setting in print_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"]
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. # 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 # Use values from the first used extruder by default so we get the expected temperatures

View file

@ -51,7 +51,7 @@
"machine_height": { "default_value": 120 }, "machine_height": { "default_value": 120 },
"machine_heated_bed": { "default_value": true }, "machine_heated_bed": { "default_value": true },
"machine_center_is_zero": { "default_value": false }, "machine_center_is_zero": { "default_value": false },
"material_diameter": { "value": 1.75 }, "material_diameter": { "default_value": 1.75 },
"machine_nozzle_size": { "machine_nozzle_size": {
"default_value": 0.4, "default_value": 0.4,
"minimum_value": 0.15 "minimum_value": 0.15

View file

@ -1343,7 +1343,7 @@ msgstr "인터페이스로드 중 ..."
#, python-format #, python-format
msgctxt "@info 'width', 'depth' and 'height' are variable names that must NOT be translated; just translate the format of ##x##x## mm." 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" 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 #: /home/ruben/Projects/Cura/cura/CuraApplication.py:1417
#, python-brace-format #, python-brace-format