Lipu Fei 7 лет назад
Родитель
Сommit
4cc2d882d3
10 измененных файлов с 779 добавлено и 182 удалено
  1. 4 0
      .dockerignore
  2. 10 4
      .github/ISSUE_TEMPLATE.md
  3. 45 0
      Dockerfile
  4. 4 0
      README.md
  5. 3 3
      cura.appdata.xml
  6. 48 47
      cura/BuildVolume.py
  7. 180 25
      cura/CrashHandler.py
  8. 255 103
      cura/CuraApplication.py
  9. 49 0
      cura/Machines/ContainerNode.py
  10. 181 0
      cura/Machines/MachineErrorChecker.py

+ 4 - 0
.dockerignore

@@ -0,0 +1,4 @@
+.git
+.github
+resources/materials
+CuraEngine

+ 10 - 4
.github/ISSUE_TEMPLATE.md

@@ -1,9 +1,12 @@
 <!--
-The following template is useful for filing new issues. Processing an issue will go much faster when this is filled out.
-Before filing, please check if the issue already exists (either open or closed).
+The following template is useful for filing new issues. Processing an issue will go much faster when this is filled out, and issues which do not use this template will be removed.
 
-It is also helpful to attach a project (.3MF) file and Cura log file so we can debug issues quicker.
-Information about how to find the log file can be found at https://github.com/Ultimaker/Cura/wiki/Cura-Preferences-and-Settings-Locations.
+Before filing, PLEASE check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
+
+Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do not write thigns like "Request:" or "[BUG]" in the title; this is what labels are for.
+
+It is also helpful to attach a project (.3mf or .curaproject) file and Cura log file so we can debug issues quicker.
+Information about how to find the log file can be found at https://github.com/Ultimaker/Cura/wiki/Cura-Preferences-and-Settings-Locations. To upload a project, try changing the extension to e.g. .curaproject.3mf.zip so that github accepts uploading the file. Otherwise we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too.
 
 Thank you for using Cura!
 -->
@@ -23,6 +26,9 @@ Thank you for using Cura!
 **Display Driver**
 <!--  Video driver name and version -->
 
+**Printer**
+<!-- Which printer was selected in Cura. Please attach project file as .curaproject.3mf.zip -->
+
 **Steps to Reproduce**
 <!-- Add the steps needed that lead up to the issue (replace this text) -->
 

+ 45 - 0
Dockerfile

@@ -0,0 +1,45 @@
+FROM ultimaker/cura-build-environment:1
+
+# Environment vars for easy configuration
+ENV CURA_APP_DIR=/srv/cura
+
+# Ensure our sources dir exists
+RUN mkdir $CURA_APP_DIR
+
+# Setup CuraEngine
+ENV CURA_ENGINE_BRANCH=master
+WORKDIR $CURA_APP_DIR
+RUN git clone -b $CURA_ENGINE_BRANCH --depth 1 https://github.com/Ultimaker/CuraEngine
+WORKDIR $CURA_APP_DIR/CuraEngine
+RUN mkdir build
+WORKDIR $CURA_APP_DIR/CuraEngine/build
+RUN cmake3 ..
+RUN make
+RUN make install
+
+# TODO: setup libCharon
+
+# Setup Uranium
+ENV URANIUM_BRANCH=master
+WORKDIR $CURA_APP_DIR
+RUN git clone -b $URANIUM_BRANCH --depth 1 https://github.com/Ultimaker/Uranium
+
+# Setup materials
+ENV MATERIALS_BRANCH=master
+WORKDIR $CURA_APP_DIR
+RUN git clone -b $MATERIALS_BRANCH --depth 1 https://github.com/Ultimaker/fdm_materials materials
+
+# Setup Cura
+WORKDIR $CURA_APP_DIR/Cura
+ADD . .
+RUN mv $CURA_APP_DIR/materials resources/materials
+
+# Make sure Cura can find CuraEngine
+RUN ln -s /usr/local/bin/CuraEngine $CURA_APP_DIR/Cura
+
+# Run Cura
+WORKDIR $CURA_APP_DIR/Cura
+ENV PYTHONPATH=${PYTHONPATH}:$CURA_APP_DIR/Uranium
+RUN chmod +x ./CuraEngine
+RUN chmod +x ./run_in_docker.sh
+CMD "./run_in_docker.sh"

+ 4 - 0
README.md

@@ -27,6 +27,10 @@ Build scripts
 -------------
 Please checkout [cura-build](https://github.com/Ultimaker/cura-build) for detailed building instructions.
 
+Running from Source
+-------------
+Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Running-Cura-from-Source) for details about running Cura from source.
+
 Plugins
 -------------
 Please check our [Wiki page](https://github.com/Ultimaker/Cura/wiki/Plugin-Directory) for details about creating and using plugins.

+ 3 - 3
cura.appdata.xml

@@ -3,7 +3,7 @@
 <component type="desktop">
   <id>cura.desktop</id>
   <metadata_license>CC0-1.0</metadata_license>
-  <project_license>AGPL-3.0 and CC-BY-SA-4.0</project_license>
+  <project_license>LGPL-3.0 and CC-BY-SA-4.0</project_license>
   <name>Cura</name>
   <summary>The world's most advanced 3d printer software</summary>
   <description>
@@ -15,7 +15,7 @@
     </p>
     <ul>
       <li>Novices can start printing right away</li>
-      <li>Experts are able to customize 200 settings to achieve the best results</li>
+      <li>Experts are able to customize 300 settings to achieve the best results</li>
       <li>Optimized profiles for Ultimaker materials</li>
       <li>Supported by a global network of Ultimaker certified service partners</li>
       <li>Print multiple objects at once with different settings for each object</li>
@@ -26,6 +26,6 @@
   <screenshots>
     <screenshot type="default" width="1280" height="720">http://software.ultimaker.com/Cura.png</screenshot>
   </screenshots>
-  <url type="homepage">https://ultimaker.com/en/products/cura-software</url>
+  <url type="homepage">https://ultimaker.com/en/products/cura-software?utm_source=cura&utm_medium=software&utm_campaign=resources</url>
   <translation type="gettext">Cura</translation>
 </component>

+ 48 - 47
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.
@@ -111,6 +111,9 @@ class BuildVolume(SceneNode):
         # but it does not update the disallowed areas after material change
         Application.getInstance().getMachineManager().activeStackChanged.connect(self._onStackChanged)
 
+        # Enable and disable extruder
+        Application.getInstance().getMachineManager().extruderChanged.connect(self.updateNodeBoundaryCheck)
+
         # list of settings which were updated
         self._changed_settings_since_last_rebuild = []
 
@@ -217,30 +220,26 @@ class BuildVolume(SceneNode):
                 group_nodes.append(node)  # Keep list of affected group_nodes
 
             if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
-                node._outside_buildarea = False
-                bbox = node.getBoundingBox()
+                if node.collidesWithBbox(build_volume_bounding_box):
+                    node.setOutsideBuildArea(True)
+                    continue
 
-                # Mark the node as outside the build volume if the bounding box test fails.
-                if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
-                    node._outside_buildarea = True
+                if node.collidesWithArea(self.getDisallowedAreas()):
+                    node.setOutsideBuildArea(True)
                     continue
 
-                convex_hull = node.callDecoration("getConvexHull")
-                if convex_hull:
-                    if not convex_hull.isValid():
-                        return
-                    # Check for collisions between disallowed areas and the object
-                    for area in self.getDisallowedAreas():
-                        overlap = convex_hull.intersectsPolygon(area)
-                        if overlap is None:
-                            continue
-                        node._outside_buildarea = True
-                        continue
+                # Mark the node as outside build volume if the set extruder is disabled
+                extruder_position = node.callDecoration("getActiveExtruderPosition")
+                if not self._global_container_stack.extruders[extruder_position].isEnabled:
+                    node.setOutsideBuildArea(True)
+                    continue
+
+                node.setOutsideBuildArea(False)
 
         # Group nodes should override the _outside_buildarea property of their children.
         for group_node in group_nodes:
             for child_node in group_node.getAllChildren():
-                child_node._outside_buildarea = group_node._outside_buildarea
+                child_node.setOutsideBuildArea(group_node.isOutsideBuildArea())
 
     ##  Update the outsideBuildArea of a single node, given bounds or current build volume
     def checkBoundsAndUpdate(self, node: CuraSceneNode, bounds: Optional[AxisAlignedBox] = None):
@@ -260,24 +259,20 @@ class BuildVolume(SceneNode):
             build_volume_bounding_box = bounds
 
         if node.callDecoration("isSliceable") or node.callDecoration("isGroup"):
-            bbox = node.getBoundingBox()
+            if node.collidesWithBbox(build_volume_bounding_box):
+                node.setOutsideBuildArea(True)
+                return
 
-            # Mark the node as outside the build volume if the bounding box test fails.
-            if build_volume_bounding_box.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
+            if node.collidesWithArea(self.getDisallowedAreas()):
+                node.setOutsideBuildArea(True)
+                return
+
+            # Mark the node as outside build volume if the set extruder is disabled
+            extruder_position = node.callDecoration("getActiveExtruderPosition")
+            if not self._global_container_stack.extruders[extruder_position].isEnabled:
                 node.setOutsideBuildArea(True)
                 return
 
-            convex_hull = self.callDecoration("getConvexHull")
-            if convex_hull:
-                if not convex_hull.isValid():
-                    return
-                # Check for collisions between disallowed areas and the object
-                for area in self.getDisallowedAreas():
-                    overlap = convex_hull.intersectsPolygon(area)
-                    if overlap is None:
-                        continue
-                    node.setOutsideBuildArea(True)
-                    return
             node.setOutsideBuildArea(False)
 
     ##  Recalculates the build volume & disallowed areas.
@@ -737,12 +732,17 @@ class BuildVolume(SceneNode):
                 prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
                 prime_tower_y = prime_tower_y + machine_depth / 2
 
-            prime_tower_area = Polygon([
-                [prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size],
-                [prime_tower_x, prime_tower_y - prime_tower_size],
-                [prime_tower_x, prime_tower_y],
-                [prime_tower_x - prime_tower_size, prime_tower_y],
-            ])
+            if self._global_container_stack.getProperty("prime_tower_circular", "value"):
+                radius = prime_tower_size / 2
+                prime_tower_area = Polygon.approximatedCircle(radius)
+                prime_tower_area = prime_tower_area.translate(prime_tower_x - radius, prime_tower_y - radius)
+            else:
+                prime_tower_area = Polygon([
+                    [prime_tower_x - prime_tower_size, prime_tower_y - prime_tower_size],
+                    [prime_tower_x, prime_tower_y - prime_tower_size],
+                    [prime_tower_x, prime_tower_y],
+                    [prime_tower_x - prime_tower_size, prime_tower_y],
+                ])
             prime_tower_area = prime_tower_area.getMinkowskiHull(Polygon.approximatedCircle(0))
             for extruder in used_extruders:
                 result[extruder.getId()].append(prime_tower_area) #The prime tower location is the same for each extruder, regardless of offset.
@@ -821,6 +821,7 @@ class BuildVolume(SceneNode):
             offset_y = extruder.getProperty("machine_nozzle_offset_y", "value")
             if offset_y is None:
                 offset_y = 0
+            offset_y = -offset_y #Y direction of g-code is the inverse of Y direction of Cura's scene space.
             result[extruder_id] = []
 
             for polygon in machine_disallowed_polygons:
@@ -931,8 +932,8 @@ class BuildVolume(SceneNode):
     #   stack.
     #
     #   \return A sequence of setting values, one for each extruder.
-    def _getSettingFromAllExtruders(self, setting_key, property = "value"):
-        all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, property)
+    def _getSettingFromAllExtruders(self, setting_key):
+        all_values = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "value")
         all_types = ExtruderManager.getInstance().getAllExtruderSettings(setting_key, "type")
         for i in range(len(all_values)):
             if not all_values[i] and (all_types[i] == "int" or all_types[i] == "float"):
@@ -945,7 +946,7 @@ class BuildVolume(SceneNode):
     #   not part of the collision radius, such as bed adhesion (skirt/brim/raft)
     #   and travel avoid distance.
     def _getEdgeDisallowedSize(self):
-        if not self._global_container_stack:
+        if not self._global_container_stack or not self._global_container_stack.extruders:
             return 0
 
         container_stack = self._global_container_stack
@@ -1023,7 +1024,7 @@ class BuildVolume(SceneNode):
     _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
     _extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
     _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
-    _tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
+    _tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"]
     _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
     _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts"]
     _extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.

+ 180 - 25
cura/CrashHandler.py

@@ -1,7 +1,6 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
-import sys
 import platform
 import traceback
 import faulthandler
@@ -13,15 +12,19 @@ import json
 import ssl
 import urllib.request
 import urllib.error
+import shutil
 
-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, Qt, QUrl
+from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
+from PyQt5.QtGui import QDesktopServices
 
+from UM.Resources import Resources
 from UM.Application import Application
 from UM.Logger import Logger
 from UM.View.GL.OpenGL import OpenGL
 from UM.i18n import i18nCatalog
 from UM.Platform import Platform
+from UM.Resources import Resources
 
 catalog = i18nCatalog("cura")
 
@@ -49,10 +52,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 +68,143 @@ 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", """<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>Backups can be found in the configuration folder.</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)
+
+        show_configuration_folder_button = QPushButton(catalog.i18nc("@action:button", "Show configuration folder"), dialog)
+        show_configuration_folder_button.setMaximumWidth(200)
+        show_configuration_folder_button.clicked.connect(self._showConfigurationFolder)
+
+        layout.addWidget(self._send_report_checkbox)
+        layout.addWidget(show_details_button)
+        layout.addWidget(show_configuration_folder_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:
+                # only create the zip backup when the folder exists
+                if os.path.exists(folder):
+                    # 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 _showConfigurationFolder(self):
+        path = Resources.getConfigStoragePath();
+        QDesktopServices.openUrl(QUrl.fromLocalFile( path ))
+
+    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 +215,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", """<p><b>A fatal error has occurred. Please send us this Crash Report to fix the problem</p></b>
@@ -130,7 +259,7 @@ class CrashHandler:
         opengl_instance = OpenGL.getInstance()
         if not opengl_instance:
             self.data["opengl"] = {"version": "n/a", "vendor": "n/a", "type": "n/a"}
-            return catalog.i18nc("@label", "not yet initialised<br/>")
+            return catalog.i18nc("@label", "Not yet initialized<br/>")
 
         info = "<ul>"
         info += catalog.i18nc("@label OpenGL version", "<li>OpenGL Version: {version}</li>").format(version = opengl_instance.getOpenGLVersion())
@@ -148,8 +277,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 +286,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 = []
@@ -189,7 +332,7 @@ class CrashHandler:
 
             json_metadata_file = os.path.join(directory, "plugin.json")
             try:
-                with open(json_metadata_file, "r") as f:
+                with open(json_metadata_file, "r", encoding = "utf-8") as f:
                     try:
                         metadata = json.loads(f.read())
                         module_version = metadata["version"]
@@ -217,9 +360,9 @@ class CrashHandler:
         text_area = QTextEdit()
         tmp_file_fd, tmp_file_path = tempfile.mkstemp(prefix = "cura-crash", text = True)
         os.close(tmp_file_fd)
-        with open(tmp_file_path, "w") as f:
+        with open(tmp_file_path, "w", encoding = "utf-8") as f:
             faulthandler.dump_traceback(f, all_threads=True)
-        with open(tmp_file_path, "r") as f:
+        with open(tmp_file_path, "r", encoding = "utf-8") as f:
             logdata = f.read()
 
         text_area.setText(logdata)
@@ -249,9 +392,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 +416,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)
 

+ 255 - 103
cura/CuraApplication.py

@@ -1,6 +1,10 @@
-# Copyright (c) 2017 Ultimaker B.V.
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
+
+#Type hinting.
+from typing import Dict
+
+from PyQt5.QtCore import QObject
 from PyQt5.QtNetwork import QLocalServer
 from PyQt5.QtNetwork import QLocalSocket
 
@@ -11,6 +15,7 @@ from UM.Math.Vector import Vector
 from UM.Math.Quaternion import Quaternion
 from UM.Math.AxisAlignedBox import AxisAlignedBox
 from UM.Math.Matrix import Matrix
+from UM.Platform import Platform
 from UM.Resources import Resources
 from UM.Scene.ToolHandle import ToolHandle
 from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
@@ -50,13 +55,25 @@ from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyT
 from UM.Settings.ContainerRegistry import ContainerRegistry
 from UM.Settings.SettingFunction import SettingFunction
 from cura.Settings.MachineNameValidator import MachineNameValidator
-from cura.Settings.ProfilesModel import ProfilesModel
-from cura.Settings.MaterialsModel import MaterialsModel
-from cura.Settings.QualityAndUserProfilesModel import QualityAndUserProfilesModel
+
+from cura.Machines.Models.BuildPlateModel import BuildPlateModel
+from cura.Machines.Models.NozzleModel import NozzleModel
+from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
+from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
+from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
+from cura.Machines.Models.MaterialManagementModel import MaterialManagementModel
+from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
+from cura.Machines.Models.BrandMaterialsModel import BrandMaterialsModel
+from cura.Machines.Models.QualityManagementModel import QualityManagementModel
+from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
+from cura.Machines.Models.MachineManagementModel import MachineManagementModel
+
+from cura.Machines.MachineErrorChecker import MachineErrorChecker
+
 from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
-from cura.Settings.UserProfilesModel import UserProfilesModel
 from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
 
+from cura.Machines.VariantManager import VariantManager
 
 from . import PlatformPhysics
 from . import BuildVolume
@@ -69,17 +86,13 @@ from . import CameraImageProvider
 from . import MachineActionManager
 
 from cura.Settings.MachineManager import MachineManager
-from cura.Settings.MaterialManager import MaterialManager
 from cura.Settings.ExtruderManager import ExtruderManager
 from cura.Settings.UserChangesModel import UserChangesModel
 from cura.Settings.ExtrudersModel import ExtrudersModel
-from cura.Settings.ContainerSettingsModel import ContainerSettingsModel
 from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
-from cura.Settings.QualitySettingsModel import QualitySettingsModel
 from cura.Settings.ContainerManager import ContainerManager
 
 from cura.ObjectsModel import ObjectsModel
-from cura.BuildPlateModel import BuildPlateModel
 
 from PyQt5.QtCore import QUrl, pyqtSignal, pyqtProperty, QEvent, Q_ENUMS
 from UM.FlameProfiler import pyqtSlot
@@ -87,6 +100,7 @@ from PyQt5.QtGui import QColor, QIcon
 from PyQt5.QtWidgets import QMessageBox
 from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
 
+from configparser import ConfigParser
 import sys
 import os.path
 import numpy
@@ -95,6 +109,7 @@ import os
 import argparse
 import json
 
+
 numpy.seterr(all="ignore")
 
 MYPY = False
@@ -113,6 +128,8 @@ class CuraApplication(QtApplication):
     # changes of the settings.
     SettingVersion = 4
 
+    Created = False
+
     class ResourceTypes:
         QmlFiles = Resources.UserType + 1
         Firmware = Resources.UserType + 2
@@ -126,14 +143,7 @@ class CuraApplication(QtApplication):
 
     Q_ENUMS(ResourceTypes)
 
-    # FIXME: This signal belongs to the MachineManager, but the CuraEngineBackend plugin requires on it.
-    #        Because plugins are initialized before the ContainerRegistry, putting this signal in MachineManager
-    #        will make it initialized before ContainerRegistry does, and it won't find the active machine, thus
-    #        Cura will always show the Add Machine Dialog upon start.
-    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)
@@ -194,10 +204,10 @@ class CuraApplication(QtApplication):
         UM.VersionUpgradeManager.VersionUpgradeManager.getInstance().setCurrentVersions(
             {
                 ("quality_changes", InstanceContainer.Version * 1000000 + self.SettingVersion):    (self.ResourceTypes.QualityInstanceContainer, "application/x-uranium-instancecontainer"),
-                ("machine_stack", ContainerStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.MachineStack, "application/x-cura-globalstack"),
-                ("extruder_train", ContainerStack.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.ExtruderStack, "application/x-cura-extruderstack"),
-                ("preferences", Preferences.Version * 1000000 + self.SettingVersion):               (Resources.Preferences, "application/x-uranium-preferences"),
-                ("user", InstanceContainer.Version * 1000000 + self.SettingVersion):       (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer"),
+                ("machine_stack", ContainerStack.Version * 1000000 + self.SettingVersion):         (self.ResourceTypes.MachineStack, "application/x-cura-globalstack"),
+                ("extruder_train", ContainerStack.Version * 1000000 + self.SettingVersion):        (self.ResourceTypes.ExtruderStack, "application/x-cura-extruderstack"),
+                ("preferences", Preferences.Version * 1000000 + self.SettingVersion):              (Resources.Preferences, "application/x-uranium-preferences"),
+                ("user", InstanceContainer.Version * 1000000 + self.SettingVersion):               (self.ResourceTypes.UserInstanceContainer, "application/x-uranium-instancecontainer"),
                 ("definition_changes", InstanceContainer.Version * 1000000 + self.SettingVersion): (self.ResourceTypes.DefinitionChangesContainer, "application/x-uranium-instancecontainer"),
             }
         )
@@ -209,11 +219,14 @@ class CuraApplication(QtApplication):
         self._machine_manager = None    # This is initialized on demand.
         self._extruder_manager = None
         self._material_manager = None
+        self._quality_manager = None
         self._object_manager = None
         self._build_plate_model = None
+        self._multi_build_plate_model = None
         self._setting_inheritance_manager = None
         self._simple_mode_settings_manager = None
         self._cura_scene_controller = None
+        self._machine_error_checker = None
 
         self._additional_components = {} # Components to add to certain areas in the interface
 
@@ -224,6 +237,12 @@ 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._variant_manager = None
+
         self.default_theme = "cura-light"
 
         self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
@@ -257,7 +276,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 = []
@@ -278,21 +297,25 @@ class CuraApplication(QtApplication):
         # Since they are empty, they should never be serialized and instead just programmatically created.
         # We need them to simplify the switching between materials.
         empty_container = ContainerRegistry.getInstance().getEmptyInstanceContainer()
+        self.empty_container = empty_container
 
         empty_definition_changes_container = copy.deepcopy(empty_container)
         empty_definition_changes_container.setMetaDataEntry("id", "empty_definition_changes")
         empty_definition_changes_container.addMetaDataEntry("type", "definition_changes")
         ContainerRegistry.getInstance().addContainer(empty_definition_changes_container)
+        self.empty_definition_changes_container = empty_definition_changes_container
 
         empty_variant_container = copy.deepcopy(empty_container)
         empty_variant_container.setMetaDataEntry("id", "empty_variant")
         empty_variant_container.addMetaDataEntry("type", "variant")
         ContainerRegistry.getInstance().addContainer(empty_variant_container)
+        self.empty_variant_container = empty_variant_container
 
         empty_material_container = copy.deepcopy(empty_container)
         empty_material_container.setMetaDataEntry("id", "empty_material")
         empty_material_container.addMetaDataEntry("type", "material")
         ContainerRegistry.getInstance().addContainer(empty_material_container)
+        self.empty_material_container = empty_material_container
 
         empty_quality_container = copy.deepcopy(empty_container)
         empty_quality_container.setMetaDataEntry("id", "empty_quality")
@@ -301,12 +324,14 @@ class CuraApplication(QtApplication):
         empty_quality_container.addMetaDataEntry("type", "quality")
         empty_quality_container.addMetaDataEntry("supported", False)
         ContainerRegistry.getInstance().addContainer(empty_quality_container)
+        self.empty_quality_container = empty_quality_container
 
         empty_quality_changes_container = copy.deepcopy(empty_container)
         empty_quality_changes_container.setMetaDataEntry("id", "empty_quality_changes")
         empty_quality_changes_container.addMetaDataEntry("type", "quality_changes")
         empty_quality_changes_container.addMetaDataEntry("quality_type", "not_supported")
         ContainerRegistry.getInstance().addContainer(empty_quality_changes_container)
+        self.empty_quality_changes_container = empty_quality_changes_container
 
         with ContainerRegistry.getInstance().lockFile():
             ContainerRegistry.getInstance().loadAllMetadata()
@@ -348,57 +373,19 @@ class CuraApplication(QtApplication):
 
         preferences.setDefault("local_file/last_used_type", "text/x-gcode")
 
-        preferences.setDefault("general/visible_settings", """
-            machine_settings
-            resolution
-                layer_height
-            shell
-                wall_thickness
-                top_bottom_thickness
-                z_seam_x
-                z_seam_y
-            infill
-                infill_sparse_density
-                gradual_infill_steps
-            material
-                material_print_temperature
-                material_bed_temperature
-                material_diameter
-                material_flow
-                retraction_enable
-            speed
-                speed_print
-                speed_travel
-                acceleration_print
-                acceleration_travel
-                jerk_print
-                jerk_travel
-            travel
-            cooling
-                cool_fan_enabled
-            support
-                support_enable
-                support_extruder_nr
-                support_type
-            platform_adhesion
-                adhesion_type
-                adhesion_extruder_nr
-                brim_width
-                raft_airgap
-                layer_0_z_overlap
-                raft_surface_layers
-            dual
-                prime_tower_enable
-                prime_tower_size
-                prime_tower_position_x
-                prime_tower_position_y
-            meshfix
-            blackmagic
-                print_sequence
-                infill_mesh
-                cutting_mesh
-            experimental
-        """.replace("\n", ";").replace(" ", ""))
+        setting_visibily_preset_names = self.getVisibilitySettingPresetTypes()
+        preferences.setDefault("general/visible_settings_preset", setting_visibily_preset_names)
+
+        preset_setting_visibility_choice = Preferences.getInstance().getValue("general/preset_setting_visibility_choice")
+
+        default_preset_visibility_group_name = "Basic"
+        if preset_setting_visibility_choice == "" or preset_setting_visibility_choice is None:
+            if preset_setting_visibility_choice not in setting_visibily_preset_names:
+                preset_setting_visibility_choice = default_preset_visibility_group_name
+
+        visible_settings = self.getVisibilitySettingPreset(settings_preset_name = preset_setting_visibility_choice)
+        preferences.setDefault("general/visible_settings", visible_settings)
+        preferences.setDefault("general/preset_setting_visibility_choice", preset_setting_visibility_choice)
 
         self.applicationShuttingDown.connect(self.saveSettings)
         self.engineCreatedSignal.connect(self._onEngineCreated)
@@ -410,6 +397,97 @@ class CuraApplication(QtApplication):
 
         self.getCuraSceneController().setActiveBuildPlate(0)  # Initialize
 
+        self._quality_profile_drop_down_menu_model = None
+        self._custom_quality_profile_drop_down_menu_model = None
+
+        CuraApplication.Created = True
+
+    @pyqtSlot(str, result = str)
+    def getVisibilitySettingPreset(self, settings_preset_name) -> str:
+        result = self._loadPresetSettingVisibilityGroup(settings_preset_name)
+        formatted_preset_settings = self._serializePresetSettingVisibilityData(result)
+
+        return formatted_preset_settings
+
+    ## Serialise the given preset setting visibitlity group dictionary into a string which is concatenated by ";"
+    #
+    def _serializePresetSettingVisibilityData(self, settings_data: dict) -> str:
+        result_string = ""
+
+        for key in settings_data:
+            result_string += key + ";"
+            for value in settings_data[key]:
+                result_string += value + ";"
+
+        return result_string
+
+    ## Load the preset setting visibility group with the given name
+    #
+    def _loadPresetSettingVisibilityGroup(self, visibility_preset_name) -> Dict[str, str]:
+        preset_dir = Resources.getPath(Resources.PresetSettingVisibilityGroups)
+
+        result = {}
+        right_preset_found = False
+
+        for item in os.listdir(preset_dir):
+            file_path = os.path.join(preset_dir, item)
+            if not os.path.isfile(file_path):
+                continue
+
+            parser = ConfigParser(allow_no_value = True)  # accept options without any value,
+
+            try:
+                parser.read([file_path])
+
+                if not parser.has_option("general", "name"):
+                    continue
+
+                if parser["general"]["name"] == visibility_preset_name:
+                    right_preset_found = True
+                    for section in parser.sections():
+                        if section == 'general':
+                            continue
+                        else:
+                            section_settings = []
+                            for option in parser[section].keys():
+                                section_settings.append(option)
+
+                            result[section] = section_settings
+
+                if right_preset_found:
+                    break
+
+            except Exception as e:
+                Logger.log("e", "Failed to load setting visibility preset %s: %s", file_path, str(e))
+
+        return result
+
+    ## Check visibility setting preset folder and returns available types
+    #
+    def getVisibilitySettingPresetTypes(self):
+        preset_dir = Resources.getPath(Resources.PresetSettingVisibilityGroups)
+        result = {}
+
+        for item in os.listdir(preset_dir):
+            file_path = os.path.join(preset_dir, item)
+            if not os.path.isfile(file_path):
+                continue
+
+            parser = ConfigParser(allow_no_value=True)  # accept options without any value,
+
+            try:
+                parser.read([file_path])
+
+                if not parser.has_option("general", "name") and not parser.has_option("general", "weight"):
+                    continue
+
+                result[parser["general"]["weight"]] = parser["general"]["name"]
+
+            except Exception as e:
+                Logger.log("e", "Failed to load setting preset %s: %s", file_path, str(e))
+
+        return result
+
     def _onEngineCreated(self):
         self._engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
 
@@ -464,8 +542,6 @@ class CuraApplication(QtApplication):
             has_user_interaction = True
         return has_user_interaction
 
-    onDiscardOrKeepProfileChangesClosed = pyqtSignal()  # Used to notify other managers that the dialog was closed
-
     @pyqtSlot(str)
     def discardOrKeepProfileChangesClosed(self, option):
         if option == "discard":
@@ -488,7 +564,6 @@ class CuraApplication(QtApplication):
                 user_global_container.update()
 
         # notify listeners that quality has changed (after user selected discard or keep)
-        self.onDiscardOrKeepProfileChangesClosed.emit()
         self.getMachineManager().activeQualityChanged.emit()
 
     @pyqtSlot(int)
@@ -504,7 +579,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()
@@ -530,7 +605,13 @@ class CuraApplication(QtApplication):
     def _loadPlugins(self):
         self._plugin_registry.addType("profile_reader", self._addProfileReader)
         self._plugin_registry.addType("profile_writer", self._addProfileWriter)
-        self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib", "cura"))
+
+        if Platform.isLinux():
+            lib_suffixes = {"", "64", "32", "x32"} #A few common ones on different distributions.
+        else:
+            lib_suffixes = {""}
+        for suffix in lib_suffixes:
+            self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib" + suffix, "cura"))
         if not hasattr(sys, "frozen"):
             self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins"))
             self._plugin_registry.loadPlugin("ConsoleLogger")
@@ -658,6 +739,29 @@ class CuraApplication(QtApplication):
     def run(self):
         self.preRun()
 
+        container_registry = ContainerRegistry.getInstance()
+
+        Logger.log("i", "Initializing variant manager")
+        self._variant_manager = VariantManager(container_registry)
+        self._variant_manager.initialize()
+
+        Logger.log("i", "Initializing material manager")
+        from cura.Machines.MaterialManager import MaterialManager
+        self._material_manager = MaterialManager(container_registry, parent = self)
+        self._material_manager.initialize()
+
+        Logger.log("i", "Initializing quality manager")
+        from cura.Machines.QualityManager import QualityManager
+        self._quality_manager = QualityManager(container_registry, parent = self)
+        self._quality_manager.initialize()
+
+        Logger.log("i", "Initializing machine manager")
+        self._machine_manager = MachineManager(self)
+
+        Logger.log("i", "Initializing machine error checker")
+        self._machine_error_checker = MachineErrorChecker(self)
+        self._machine_error_checker.initialize()
+
         # Check if we should run as single instance or not
         self._setUpSingleInstanceServer()
 
@@ -682,9 +786,12 @@ 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.initializationFinished.emit()
         self.exec_()
 
+    initializationFinished = pyqtSignal()
+
     ##  Run Cura without GUI elements and interaction (server mode).
     def runWithoutGUI(self):
         self._use_gui = False
@@ -749,9 +856,12 @@ class CuraApplication(QtApplication):
     def hasGui(self):
         return self._use_gui
 
+    def getMachineErrorChecker(self, *args) -> MachineErrorChecker:
+        return self._machine_error_checker
+
     def getMachineManager(self, *args) -> MachineManager:
         if self._machine_manager is None:
-            self._machine_manager = MachineManager.createMachineManager()
+            self._machine_manager = MachineManager(self)
         return self._machine_manager
 
     def getExtruderManager(self, *args):
@@ -759,20 +869,32 @@ class CuraApplication(QtApplication):
             self._extruder_manager = ExtruderManager.createExtruderManager()
         return self._extruder_manager
 
+    def getVariantManager(self, *args):
+        return self._variant_manager
+
+    @pyqtSlot(result = QObject)
     def getMaterialManager(self, *args):
-        if self._material_manager is None:
-            self._material_manager = MaterialManager.createMaterialManager()
         return self._material_manager
 
+    @pyqtSlot(result = QObject)
+    def getQualityManager(self, *args):
+        return self._quality_manager
+
     def getObjectsModel(self, *args):
         if self._object_manager is None:
             self._object_manager = ObjectsModel.createObjectsModel()
         return self._object_manager
 
+    @pyqtSlot(result = QObject)
+    def getMultiBuildPlateModel(self, *args):
+        if self._multi_build_plate_model is None:
+            self._multi_build_plate_model = MultiBuildPlateModel(self)
+        return self._multi_build_plate_model
+
+    @pyqtSlot(result = QObject)
     def getBuildPlateModel(self, *args):
         if self._build_plate_model is None:
-            self._build_plate_model = BuildPlateModel.createBuildPlateModel()
-
+            self._build_plate_model = BuildPlateModel(self)
         return self._build_plate_model
 
     def getCuraSceneController(self, *args):
@@ -810,6 +932,16 @@ class CuraApplication(QtApplication):
     def getPrintInformation(self):
         return self._print_information
 
+    def getQualityProfilesDropDownMenuModel(self, *args, **kwargs):
+        if self._quality_profile_drop_down_menu_model is None:
+            self._quality_profile_drop_down_menu_model = QualityProfilesDropDownMenuModel(self)
+        return self._quality_profile_drop_down_menu_model
+
+    def getCustomQualityProfilesDropDownMenuModel(self, *args, **kwargs):
+        if self._custom_quality_profile_drop_down_menu_model is None:
+            self._custom_quality_profile_drop_down_menu_model = CustomQualityProfilesDropDownMenuModel(self)
+        return self._custom_quality_profile_drop_down_menu_model
+
     ##  Registers objects for the QML engine to use.
     #
     #   \param engine The QML engine.
@@ -824,27 +956,35 @@ class CuraApplication(QtApplication):
 
         qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
 
-        qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 2, "SceneController", self.getCuraSceneController)
+        qmlRegisterSingletonType(CuraSceneController, "Cura", 1, 0, "SceneController", self.getCuraSceneController)
         qmlRegisterSingletonType(ExtruderManager, "Cura", 1, 0, "ExtruderManager", self.getExtruderManager)
         qmlRegisterSingletonType(MachineManager, "Cura", 1, 0, "MachineManager", self.getMachineManager)
-        qmlRegisterSingletonType(MaterialManager, "Cura", 1, 0, "MaterialManager", self.getMaterialManager)
         qmlRegisterSingletonType(SettingInheritanceManager, "Cura", 1, 0, "SettingInheritanceManager", self.getSettingInheritanceManager)
-        qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 2, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
+        qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
         qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
 
-        qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 2, "ObjectsModel", self.getObjectsModel)
-        qmlRegisterSingletonType(BuildPlateModel, "Cura", 1, 2, "BuildPlateModel", self.getBuildPlateModel)
+        qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 0, "ObjectsModel", self.getObjectsModel)
+        qmlRegisterType(BuildPlateModel, "Cura", 1, 0, "BuildPlateModel")
+        qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
         qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
         qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
-        qmlRegisterType(ContainerSettingsModel, "Cura", 1, 0, "ContainerSettingsModel")
-        qmlRegisterSingletonType(ProfilesModel, "Cura", 1, 0, "ProfilesModel", ProfilesModel.createProfilesModel)
-        qmlRegisterType(MaterialsModel, "Cura", 1, 0, "MaterialsModel")
-        qmlRegisterType(QualityAndUserProfilesModel, "Cura", 1, 0, "QualityAndUserProfilesModel")
-        qmlRegisterType(UserProfilesModel, "Cura", 1, 0, "UserProfilesModel")
+
+        qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
+        qmlRegisterType(BrandMaterialsModel, "Cura", 1, 0, "BrandMaterialsModel")
+        qmlRegisterType(MaterialManagementModel, "Cura", 1, 0, "MaterialManagementModel")
+        qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
+        qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel")
+
+        qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
+                                 "QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
+        qmlRegisterSingletonType(CustomQualityProfilesDropDownMenuModel, "Cura", 1, 0,
+                                 "CustomQualityProfilesDropDownMenuModel", self.getCustomQualityProfilesDropDownMenuModel)
+        qmlRegisterType(NozzleModel, "Cura", 1, 0, "NozzleModel")
+
         qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
         qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
         qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
-        qmlRegisterType(UserChangesModel, "Cura", 1, 1, "UserChangesModel")
+        qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
         qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.createContainerManager)
 
         # As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
@@ -926,7 +1066,7 @@ class CuraApplication(QtApplication):
         count = 0
         scene_bounding_box = None
         is_block_slicing_node = False
-        active_build_plate = self.getBuildPlateModel().activeBuildPlate
+        active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
         for node in DepthFirstIterator(self.getController().getScene().getRoot()):
             if (
                 not issubclass(type(node), CuraSceneNode) or
@@ -1175,7 +1315,7 @@ class CuraApplication(QtApplication):
     @pyqtSlot()
     def arrangeAll(self):
         nodes = []
-        active_build_plate = self.getBuildPlateModel().activeBuildPlate
+        active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
         for node in DepthFirstIterator(self.getController().getScene().getRoot()):
             if not isinstance(node, SceneNode):
                 continue
@@ -1229,7 +1369,7 @@ class CuraApplication(QtApplication):
         Logger.log("i", "Reloading all loaded mesh data.")
         nodes = []
         for node in DepthFirstIterator(self.getController().getScene().getRoot()):
-            if not isinstance(node, SceneNode) or not node.getMeshData():
+            if not isinstance(node, CuraSceneNode) or not node.getMeshData():
                 continue
 
             nodes.append(node)
@@ -1324,7 +1464,7 @@ class CuraApplication(QtApplication):
         group_decorator = GroupDecorator()
         group_node.addDecorator(group_decorator)
         group_node.addDecorator(ConvexHullDecorator())
-        group_node.addDecorator(BuildPlateDecorator(self.getBuildPlateModel().activeBuildPlate))
+        group_node.addDecorator(BuildPlateDecorator(self.getMultiBuildPlateModel().activeBuildPlate))
         group_node.setParent(self.getController().getScene().getRoot())
         group_node.setSelectable(True)
         center = Selection.getSelectionCenter()
@@ -1469,7 +1609,7 @@ class CuraApplication(QtApplication):
         arrange_objects_on_load = (
             not Preferences.getInstance().getValue("cura/use_multi_build_plate") or
             not Preferences.getInstance().getValue("cura/not_arrange_objects_on_load"))
-        target_build_plate = self.getBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1
+        target_build_plate = self.getMultiBuildPlateModel().activeBuildPlate if arrange_objects_on_load else -1
 
         root = self.getController().getScene().getRoot()
         fixed_nodes = []
@@ -1478,12 +1618,22 @@ class CuraApplication(QtApplication):
                 fixed_nodes.append(node_)
         arranger = Arrange.create(fixed_nodes = fixed_nodes)
         min_offset = 8
+        default_extruder_position = self.getMachineManager().defaultExtruderPosition
+        default_extruder_id = self._global_container_stack.extruders[default_extruder_position].getId()
 
         for original_node in nodes:
 
             # Create a CuraSceneNode just if the original node is not that type
-            node = original_node if isinstance(original_node, CuraSceneNode) else CuraSceneNode()
-            node.setMeshData(original_node.getMeshData())
+            if isinstance(original_node, CuraSceneNode):
+                node = original_node
+            else:
+                node = CuraSceneNode()
+                node.setMeshData(original_node.getMeshData())
+
+                #Setting meshdata does not apply scaling.
+                if(original_node.getScale() != Vector(1.0, 1.0, 1.0)):
+                    node.scale(original_node.getScale())
+
 
             node.setSelectable(True)
             node.setName(os.path.basename(filename))
@@ -1535,6 +1685,8 @@ class CuraApplication(QtApplication):
 
             op = AddSceneNodeOperation(node, scene.getRoot())
             op.push()
+
+            node.callDecoration("setActiveExtruder", default_extruder_id)
             scene.sceneChanged.emit(node)
 
         self.fileCompleted.emit(filename)
@@ -1555,7 +1707,7 @@ class CuraApplication(QtApplication):
             result = workspace_reader.preRead(file_path, show_dialog=False)
             return result == WorkspaceReader.PreReadResult.accepted
         except Exception as e:
-            Logger.log("e", "Could not check file %s: %s", file_url, e)
+            Logger.logException("e", "Could not check file %s: %s", file_url)
             return False
 
     def _onContextMenuRequested(self, x: float, y: float) -> None:

+ 49 - 0
cura/Machines/ContainerNode.py

@@ -0,0 +1,49 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from typing import Optional
+
+from collections import OrderedDict
+
+from UM.Logger import Logger
+from UM.Settings.InstanceContainer import InstanceContainer
+
+
+##
+# A metadata / container combination. Use getContainer() to get the container corresponding to the metadata.
+#
+# ContainerNode is a multi-purpose class. It has two main purposes:
+#  1. It encapsulates an InstanceContainer. It contains that InstanceContainer's
+#          - metadata (Always)
+#          - container (lazy-loaded when needed)
+#  2. It also serves as a node in a hierarchical InstanceContainer lookup table/tree.
+#     This is used in Variant, Material, and Quality Managers.
+#
+class ContainerNode:
+    __slots__ = ("metadata", "container", "children_map")
+
+    def __init__(self, metadata: Optional[dict] = None):
+        self.metadata = metadata
+        self.container = None
+        self.children_map = OrderedDict()
+
+    def getChildNode(self, child_key: str) -> Optional["ContainerNode"]:
+        return self.children_map.get(child_key)
+
+    def getContainer(self) -> "InstanceContainer":
+        if self.metadata is None:
+            raise RuntimeError("Cannot get container for a ContainerNode without metadata")
+
+        if self.container is None:
+            container_id = self.metadata["id"]
+            Logger.log("i", "Lazy-loading container [%s]", container_id)
+            from UM.Settings.ContainerRegistry import ContainerRegistry
+            container_list = ContainerRegistry.getInstance().findInstanceContainers(id = container_id)
+            if not container_list:
+                raise RuntimeError("Failed to lazy-load container [%s], cannot find it" % container_id)
+            self.container = container_list[0]
+
+        return self.container
+
+    def __str__(self) -> str:
+        return "%s[%s]" % (self.__class__.__name__, self.metadata.get("id"))

+ 181 - 0
cura/Machines/MachineErrorChecker.py

@@ -0,0 +1,181 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+import time
+
+from collections import deque
+
+from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtProperty
+
+from UM.Application import Application
+from UM.Logger import Logger
+from UM.Settings.SettingDefinition import SettingDefinition
+from UM.Settings.Validator import ValidatorState
+
+
+#
+# This class performs setting error checks for the currently active machine.
+#
+# The whole error checking process is pretty heavy which can take ~0.5 secs, so it can cause GUI to lag.
+# The idea here is to split the whole error check into small tasks, each of which only checks a single setting key
+# in a stack. According to my profiling results, the maximal runtime for such a sub-task is <0.03 secs, which should
+# be good enough. Moreover, if any changes happened to the machine, we can cancel the check in progress without wait
+# for it to finish the complete work.
+#
+class MachineErrorChecker(QObject):
+
+    def __init__(self, parent = None):
+        super().__init__(parent)
+
+        self._global_stack = None
+
+        self._has_errors = True  # Result of the error check, indicating whether there are errors in the stack
+        self._error_keys = set()  # A set of settings keys that have errors
+        self._error_keys_in_progress = set()  # The variable that stores the results of the currently in progress check
+
+        self._stacks_and_keys_to_check = None  # a FIFO queue of tuples (stack, key) to check for errors
+
+        self._need_to_check = False  # Whether we need to schedule a new check or not. This flag is set when a new
+                                     # error check needs to take place while there is already one running at the moment.
+        self._check_in_progress = False  # Whether there is an error check running in progress at the moment.
+
+        self._application = Application.getInstance()
+        self._machine_manager = self._application.getMachineManager()
+
+        self._start_time = 0  # measure checking time
+
+        # This timer delays the starting of error check so we can react less frequently if the user is frequently
+        # changing settings.
+        self._error_check_timer = QTimer(self)
+        self._error_check_timer.setInterval(100)
+        self._error_check_timer.setSingleShot(True)
+
+    def initialize(self):
+        self._error_check_timer.timeout.connect(self._rescheduleCheck)
+
+        # Reconnect all signals when the active machine gets changed.
+        self._machine_manager.globalContainerChanged.connect(self._onMachineChanged)
+
+        # Whenever the machine settings get changed, we schedule an error check.
+        self._machine_manager.globalContainerChanged.connect(self.startErrorCheck)
+        self._machine_manager.globalValueChanged.connect(self.startErrorCheck)
+
+        self._onMachineChanged()
+
+    def _onMachineChanged(self):
+        if self._global_stack:
+            self._global_stack.propertyChanged.disconnect(self.startErrorCheck)
+            self._global_stack.containersChanged.disconnect(self.startErrorCheck)
+
+            for extruder in self._global_stack.extruders.values():
+                extruder.propertyChanged.disconnect(self.startErrorCheck)
+                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.containersChanged.connect(self.startErrorCheck)
+
+            for extruder in self._global_stack.extruders.values():
+                extruder.propertyChanged.connect(self.startErrorCheck)
+                extruder.containersChanged.connect(self.startErrorCheck)
+
+    hasErrorUpdated = pyqtSignal()
+    needToWaitForResultChanged = pyqtSignal()
+    errorCheckFinished = pyqtSignal()
+
+    @pyqtProperty(bool, notify = hasErrorUpdated)
+    def hasError(self) -> bool:
+        return self._has_errors
+
+    @pyqtProperty(bool, notify = needToWaitForResultChanged)
+    def needToWaitForResult(self) -> bool:
+        return self._need_to_check or self._check_in_progress
+
+    # Starts the error check timer to schedule a new error check.
+    def startErrorCheck(self, *args):
+        if not self._check_in_progress:
+            self._need_to_check = True
+            self.needToWaitForResultChanged.emit()
+        self._error_check_timer.start()
+
+    # This function is called by the timer to reschedule a new error check.
+    # If there is no check in progress, it will start a new one. If there is any, it sets the "_need_to_check" flag
+    # to notify the current check to stop and start a new one.
+    def _rescheduleCheck(self):
+        if self._check_in_progress and not self._need_to_check:
+            self._need_to_check = True
+            self.needToWaitForResultChanged.emit()
+            return
+
+        self._error_keys_in_progress = set()
+        self._need_to_check = False
+        self.needToWaitForResultChanged.emit()
+
+        global_stack = self._machine_manager.activeMachine
+        if global_stack is None:
+            Logger.log("i", "No active machine, nothing to check.")
+            return
+
+        # Populate the (stack, key) tuples to check
+        self._stacks_and_keys_to_check = deque()
+        for stack in [global_stack] + list(global_stack.extruders.values()):
+            for key in stack.getAllKeys():
+                self._stacks_and_keys_to_check.append((stack, key))
+
+        self._application.callLater(self._checkStack)
+        self._start_time = time.time()
+        Logger.log("d", "New error check scheduled.")
+
+    def _checkStack(self):
+        if self._need_to_check:
+            Logger.log("d", "Need to check for errors again. Discard the current progress and reschedule a check.")
+            self._check_in_progress = False
+            self._application.callLater(self.startErrorCheck)
+            return
+
+        self._check_in_progress = True
+
+        # If there is nothing to check any more, it means there is no error.
+        if not self._stacks_and_keys_to_check:
+            # Finish
+            self._setResult(False)
+            return
+
+        # Get the next stack and key to check
+        stack, key = self._stacks_and_keys_to_check.popleft()
+
+        enabled = stack.getProperty(key, "enabled")
+        if not enabled:
+            self._application.callLater(self._checkStack)
+            return
+
+        validation_state = stack.getProperty(key, "validationState")
+        if validation_state is None:
+            # Setting is not validated. This can happen if there is only a setting definition.
+            # We do need to validate it, because a setting definitions value can be set by a function, which could
+            # be an invalid setting.
+            definition = stack.getSettingDefinition(key)
+            validator_type = SettingDefinition.getValidatorForType(definition.type)
+            if validator_type:
+                validator = validator_type(key)
+                validation_state = validator(stack)
+        if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
+            # Finish
+            self._setResult(True)
+            return
+
+        # Schedule the check for the next key
+        self._application.callLater(self._checkStack)
+
+    def _setResult(self, result: bool):
+        if result != self._has_errors:
+            self._has_errors = result
+            self.hasErrorUpdated.emit()
+            self._machine_manager.stacksValidationChanged.emit()
+        self._need_to_check = False
+        self._check_in_progress = False
+        self.needToWaitForResultChanged.emit()
+        self.errorCheckFinished.emit()
+        Logger.log("i", "Error check finished, result = %s, time = %0.1fs", result, time.time() - self._start_time)

Некоторые файлы не были показаны из-за большого количества измененных файлов