Browse Source

Merge branch 'master' into WIP_improve_initialization

Conflicts:
	cura/AutoSave.py
	cura/BuildVolume.py
	cura/CuraApplication.py

Contributes to CURA-5164
Diego Prado Gesto 6 years ago
parent
commit
5704a7b184

+ 1 - 0
.gitignore

@@ -40,6 +40,7 @@ plugins/cura-siemensnx-plugin
 plugins/CuraBlenderPlugin
 plugins/CuraCloudPlugin
 plugins/CuraDrivePlugin
+plugins/CuraDrive
 plugins/CuraLiveScriptingPlugin
 plugins/CuraOpenSCADPlugin
 plugins/CuraPrintProfileCreator

+ 32 - 0
cura/API/Backups.py

@@ -0,0 +1,32 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from cura.Backups.BackupsManager import BackupsManager
+
+
+class Backups:
+    """
+    The backups API provides a version-proof bridge between Cura's BackupManager and plugins that hook into it.
+
+    Usage:
+        from cura.API import CuraAPI
+        api = CuraAPI()
+        api.backups.createBackup()
+        api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})
+    """
+
+    manager = BackupsManager()  # Re-used instance of the backups manager.
+
+    def createBackup(self) -> (bytes, dict):
+        """
+        Create a new backup using the BackupsManager.
+        :return: Tuple containing a ZIP file with the backup data and a dict with meta data about the backup.
+        """
+        return self.manager.createBackup()
+
+    def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
+        """
+        Restore a backup using the BackupManager.
+        :param zip_file: A ZIP file containing the actual backup data.
+        :param meta_data: Some meta data needed for restoring a backup, like the Cura version number.
+        """
+        return self.manager.restoreBackup(zip_file, meta_data)

+ 19 - 0
cura/API/__init__.py

@@ -0,0 +1,19 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from UM.PluginRegistry import PluginRegistry
+from cura.API.Backups import Backups
+
+
+class CuraAPI:
+    """
+    The official Cura API that plugins can use to interact with Cura.
+    Python does not technically prevent talking to other classes as well,
+    but this API provides a version-safe interface with proper deprecation warnings etc.
+    Usage of any other methods than the ones provided in this API can cause plugins to be unstable.
+    """
+
+    # For now we use the same API version to be consistent.
+    VERSION = PluginRegistry.APIVersion
+
+    # Backups API.
+    backups = Backups()

+ 40 - 21
cura/Arranging/Arrange.py

@@ -1,3 +1,6 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
 from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
 from UM.Logger import Logger
 from UM.Math.Vector import Vector
@@ -18,17 +21,20 @@ LocationSuggestion = namedtuple("LocationSuggestion", ["x", "y", "penalty_points
 #   good locations for objects that you try to put on a build place.
 #   Different priority schemes can be defined so it alters the behavior while using
 #   the same logic.
+#
+#   Note: Make sure the scale is the same between ShapeArray objects and the Arrange instance.
 class Arrange:
     build_volume = None
 
-    def __init__(self, x, y, offset_x, offset_y, scale= 1.0):
-        self.shape = (y, x)
-        self._priority = numpy.zeros((x, y), dtype=numpy.int32)
-        self._priority_unique_values = []
-        self._occupied = numpy.zeros((x, y), dtype=numpy.int32)
+    def __init__(self, x, y, offset_x, offset_y, scale= 0.5):
         self._scale = scale  # convert input coordinates to arrange coordinates
-        self._offset_x = offset_x
-        self._offset_y = offset_y
+        world_x, world_y = int(x * self._scale), int(y * self._scale)
+        self._shape = (world_y, world_x)
+        self._priority = numpy.zeros((world_y, world_x), dtype=numpy.int32)  # beware: these are indexed (y, x)
+        self._priority_unique_values = []
+        self._occupied = numpy.zeros((world_y, world_x), dtype=numpy.int32)  # beware: these are indexed (y, x)
+        self._offset_x = int(offset_x * self._scale)
+        self._offset_y = int(offset_y * self._scale)
         self._last_priority = 0
         self._is_empty = True
 
@@ -39,7 +45,7 @@ class Arrange:
     #   \param scene_root   Root for finding all scene nodes
     #   \param fixed_nodes  Scene nodes to be placed
     @classmethod
-    def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 220, y = 220):
+    def create(cls, scene_root = None, fixed_nodes = None, scale = 0.5, x = 350, y = 250):
         arranger = Arrange(x, y, x // 2, y // 2, scale = scale)
         arranger.centerFirst()
 
@@ -61,13 +67,17 @@ class Arrange:
 
         # If a build volume was set, add the disallowed areas
         if Arrange.build_volume:
-            disallowed_areas = Arrange.build_volume.getDisallowedAreas()
+            disallowed_areas = Arrange.build_volume.getDisallowedAreasNoBrim()
             for area in disallowed_areas:
                 points = copy.deepcopy(area._points)
                 shape_arr = ShapeArray.fromPolygon(points, scale = scale)
                 arranger.place(0, 0, shape_arr, update_empty = False)
         return arranger
 
+    ##  This resets the optimization for finding location based on size
+    def resetLastPriority(self):
+        self._last_priority = 0
+
     ##  Find placement for a node (using offset shape) and place it (using hull shape)
     #   return the nodes that should be placed
     #   \param node
@@ -104,7 +114,7 @@ class Arrange:
     def centerFirst(self):
         # Square distance: creates a more round shape
         self._priority = numpy.fromfunction(
-            lambda i, j: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self.shape, dtype=numpy.int32)
+            lambda j, i: (self._offset_x - i) ** 2 + (self._offset_y - j) ** 2, self._shape, dtype=numpy.int32)
         self._priority_unique_values = numpy.unique(self._priority)
         self._priority_unique_values.sort()
 
@@ -112,7 +122,7 @@ class Arrange:
     #   This is a strategy for the arranger.
     def backFirst(self):
         self._priority = numpy.fromfunction(
-            lambda i, j: 10 * j + abs(self._offset_x - i), self.shape, dtype=numpy.int32)
+            lambda j, i: 10 * j + abs(self._offset_x - i), self._shape, dtype=numpy.int32)
         self._priority_unique_values = numpy.unique(self._priority)
         self._priority_unique_values.sort()
 
@@ -126,9 +136,15 @@ class Arrange:
         y = int(self._scale * y)
         offset_x = x + self._offset_x + shape_arr.offset_x
         offset_y = y + self._offset_y + shape_arr.offset_y
+        if offset_x < 0 or offset_y < 0:
+            return None  # out of bounds in self._occupied
+        occupied_x_max = offset_x + shape_arr.arr.shape[1]
+        occupied_y_max = offset_y + shape_arr.arr.shape[0]
+        if occupied_x_max > self._occupied.shape[1] + 1 or occupied_y_max > self._occupied.shape[0] + 1:
+            return None  # out of bounds in self._occupied
         occupied_slice = self._occupied[
-            offset_y:offset_y + shape_arr.arr.shape[0],
-            offset_x:offset_x + shape_arr.arr.shape[1]]
+            offset_y:occupied_y_max,
+            offset_x:occupied_x_max]
         try:
             if numpy.any(occupied_slice[numpy.where(shape_arr.arr == 1)]):
                 return None
@@ -140,7 +156,7 @@ class Arrange:
         return numpy.sum(prio_slice[numpy.where(shape_arr.arr == 1)])
 
     ##  Find "best" spot for ShapeArray
-    #   Return namedtuple with properties x, y, penalty_points, priority
+    #   Return namedtuple with properties x, y, penalty_points, priority.
     #   \param shape_arr ShapeArray
     #   \param start_prio Start with this priority value (and skip the ones before)
     #   \param step Slicing value, higher = more skips = faster but less accurate
@@ -153,12 +169,11 @@ class Arrange:
         for priority in self._priority_unique_values[start_idx::step]:
             tryout_idx = numpy.where(self._priority == priority)
             for idx in range(len(tryout_idx[0])):
-                x = tryout_idx[0][idx]
-                y = tryout_idx[1][idx]
-                projected_x = x - self._offset_x
-                projected_y = y - self._offset_y
+                x = tryout_idx[1][idx]
+                y = tryout_idx[0][idx]
+                projected_x = int((x - self._offset_x) / self._scale)
+                projected_y = int((y - self._offset_y) / self._scale)
 
-                # array to "world" coordinates
                 penalty_points = self.checkShape(projected_x, projected_y, shape_arr)
                 if penalty_points is not None:
                     return LocationSuggestion(x = projected_x, y = projected_y, penalty_points = penalty_points, priority = priority)
@@ -191,8 +206,12 @@ class Arrange:
 
         # Set priority to low (= high number), so it won't get picked at trying out.
         prio_slice = self._priority[min_y:max_y, min_x:max_x]
-        prio_slice[numpy.where(shape_arr.arr[
-            min_y - offset_y:max_y - offset_y, min_x - offset_x:max_x - offset_x] == 1)] = 999
+        prio_slice[new_occupied] = 999
+
+        # If you want to see how the rasterized arranger build plate looks like, uncomment this code
+        # numpy.set_printoptions(linewidth=500, edgeitems=200)
+        # print(self._occupied.shape)
+        # print(self._occupied)
 
     @property
     def isEmpty(self):

+ 9 - 12
cura/Arranging/ArrangeObjectsAllBuildPlatesJob.py

@@ -1,6 +1,7 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
+from UM.Application import Application
 from UM.Job import Job
 from UM.Scene.SceneNode import SceneNode
 from UM.Math.Vector import Vector
@@ -17,6 +18,7 @@ from cura.Arranging.ShapeArray import ShapeArray
 from typing import List
 
 
+##  Do arrangements on multiple build plates (aka builtiplexer)
 class ArrangeArray:
     def __init__(self, x: int, y: int, fixed_nodes: List[SceneNode]):
         self._x = x
@@ -79,7 +81,11 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
         nodes_arr.sort(key=lambda item: item[0])
         nodes_arr.reverse()
 
-        x, y = 200, 200
+        global_container_stack = Application.getInstance().getGlobalContainerStack()
+        machine_width = global_container_stack.getProperty("machine_width", "value")
+        machine_depth = global_container_stack.getProperty("machine_depth", "value")
+
+        x, y = machine_width, machine_depth
 
         arrange_array = ArrangeArray(x = x, y = y, fixed_nodes = [])
         arrange_array.add()
@@ -93,27 +99,18 @@ class ArrangeObjectsAllBuildPlatesJob(Job):
         for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
             # For performance reasons, we assume that when a location does not fit,
             # it will also not fit for the next object (while what can be untrue).
-            # We also skip possibilities by slicing through the possibilities (step = 10)
 
             try_placement = True
 
             current_build_plate_number = 0  # always start with the first one
 
-            # # Only for first build plate
-            # if last_size == size and last_build_plate_number == current_build_plate_number:
-            #     # This optimization works if many of the objects have the same size
-            #     # Continue with same build plate number
-            #     start_priority = last_priority
-            # else:
-            #     start_priority = 0
-
             while try_placement:
                 # make sure that current_build_plate_number is not going crazy or you'll have a lot of arrange objects
                 while current_build_plate_number >= arrange_array.count():
                     arrange_array.add()
                 arranger = arrange_array.get(current_build_plate_number)
 
-                best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10)
+                best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority)
                 x, y = best_spot.x, best_spot.y
                 node.removeDecorator(ZOffsetDecorator)
                 if node.getBoundingBox():

+ 10 - 5
cura/Arranging/ArrangeObjectsJob.py

@@ -1,6 +1,7 @@
 # Copyright (c) 2017 Ultimaker B.V.
 # Cura is released under the terms of the LGPLv3 or higher.
 
+from UM.Application import Application
 from UM.Job import Job
 from UM.Scene.SceneNode import SceneNode
 from UM.Math.Vector import Vector
@@ -32,7 +33,11 @@ class ArrangeObjectsJob(Job):
                                  progress = 0,
                                  title = i18n_catalog.i18nc("@info:title", "Finding Location"))
         status_message.show()
-        arranger = Arrange.create(fixed_nodes = self._fixed_nodes)
+        global_container_stack = Application.getInstance().getGlobalContainerStack()
+        machine_width = global_container_stack.getProperty("machine_width", "value")
+        machine_depth = global_container_stack.getProperty("machine_depth", "value")
+
+        arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes)
 
         # Collect nodes to be placed
         nodes_arr = []  # fill with (size, node, offset_shape_arr, hull_shape_arr)
@@ -50,15 +55,15 @@ class ArrangeObjectsJob(Job):
         last_size = None
         grouped_operation = GroupedOperation()
         found_solution_for_all = True
+        not_fit_count = 0
         for idx, (size, node, offset_shape_arr, hull_shape_arr) in enumerate(nodes_arr):
             # For performance reasons, we assume that when a location does not fit,
             # it will also not fit for the next object (while what can be untrue).
-            # We also skip possibilities by slicing through the possibilities (step = 10)
             if last_size == size:  # This optimization works if many of the objects have the same size
                 start_priority = last_priority
             else:
                 start_priority = 0
-            best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority, step=10)
+            best_spot = arranger.bestSpot(offset_shape_arr, start_prio=start_priority)
             x, y = best_spot.x, best_spot.y
             node.removeDecorator(ZOffsetDecorator)
             if node.getBoundingBox():
@@ -70,12 +75,12 @@ class ArrangeObjectsJob(Job):
                 last_priority = best_spot.priority
 
                 arranger.place(x, y, hull_shape_arr)  # take place before the next one
-
                 grouped_operation.addOperation(TranslateOperation(node, Vector(x, center_y, y), set_position = True))
             else:
                 Logger.log("d", "Arrange all: could not find spot!")
                 found_solution_for_all = False
-                grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, - idx * 20), set_position = True))
+                grouped_operation.addOperation(TranslateOperation(node, Vector(200, center_y, -not_fit_count * 20), set_position = True))
+                not_fit_count += 1
 
             status_message.setProgress((idx + 1) / len(nodes_arr) * 100)
             Job.yieldThread()

+ 1 - 1
cura/Arranging/ShapeArray.py

@@ -74,7 +74,7 @@ class ShapeArray:
     #   \param vertices
     @classmethod
     def arrayFromPolygon(cls, shape, vertices):
-        base_array = numpy.zeros(shape, dtype=float)  # Initialize your array of zeros
+        base_array = numpy.zeros(shape, dtype = numpy.int32)  # Initialize your array of zeros
 
         fill = numpy.ones(base_array.shape) * True  # Initialize boolean array defining shape fill
 

+ 52 - 0
cura/AutoSave.py

@@ -0,0 +1,52 @@
+# Copyright (c) 2016 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from PyQt5.QtCore import QTimer
+
+from UM.Logger import Logger
+
+
+class AutoSave:
+    def __init__(self, application):
+        self._application = application
+        self._application.getPreferences().preferenceChanged.connect(self._triggerTimer)
+
+        self._global_stack = None
+
+        self._application.getPreferences().addPreference("cura/autosave_delay", 1000 * 10)
+
+        self._change_timer = QTimer()
+        self._change_timer.setInterval(self._application.getPreferences().getValue("cura/autosave_delay"))
+        self._change_timer.setSingleShot(True)
+
+        self._saving = False
+
+    def initialize(self):
+        # only initialise if the application is created and has started
+        self._change_timer.timeout.connect(self._onTimeout)
+        self._application.globalContainerStackChanged.connect(self._onGlobalStackChanged)
+        self._onGlobalStackChanged()
+        self._triggerTimer()
+
+    def _triggerTimer(self, *args):
+        if not self._saving:
+            self._change_timer.start()
+
+    def _onGlobalStackChanged(self):
+        if self._global_stack:
+            self._global_stack.propertyChanged.disconnect(self._triggerTimer)
+            self._global_stack.containersChanged.disconnect(self._triggerTimer)
+
+        self._global_stack = self._application.getGlobalContainerStack()
+
+        if self._global_stack:
+            self._global_stack.propertyChanged.connect(self._triggerTimer)
+            self._global_stack.containersChanged.connect(self._triggerTimer)
+
+    def _onTimeout(self):
+        self._saving = True # To prevent the save process from triggering another autosave.
+        Logger.log("d", "Autosaving preferences, instances and profiles")
+
+        self._application.saveSettings()
+
+        self._saving = False

+ 155 - 0
cura/Backups/Backup.py

@@ -0,0 +1,155 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+import io
+import os
+import re
+
+import shutil
+
+from typing import Optional
+from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
+
+from UM import i18nCatalog
+from UM.Logger import Logger
+from UM.Message import Message
+from UM.Platform import Platform
+from UM.Resources import Resources
+from cura.CuraApplication import CuraApplication
+
+
+class Backup:
+    """
+    The backup class holds all data about a backup.
+    It is also responsible for reading and writing the zip file to the user data folder.
+    """
+
+    # These files should be ignored when making a backup.
+    IGNORED_FILES = [r"cura\.log", r"plugins\.json", r"cache", r"__pycache__", r"\.qmlc", r"\.pyc"]
+
+    # Re-use translation catalog.
+    catalog = i18nCatalog("cura")
+
+    def __init__(self, zip_file: bytes = None, meta_data: dict = None):
+        self.zip_file = zip_file  # type: Optional[bytes]
+        self.meta_data = meta_data  # type: Optional[dict]
+
+    def makeFromCurrent(self) -> (bool, Optional[str]):
+        """
+        Create a backup from the current user config folder.
+        """
+        cura_release = CuraApplication.getInstance().getVersion()
+        version_data_dir = Resources.getDataStoragePath()
+
+        Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
+
+        # Ensure all current settings are saved.
+        CuraApplication.getInstance().saveSettings()
+
+        # We copy the preferences file to the user data directory in Linux as it's in a different location there.
+        # When restoring a backup on Linux, we move it back.
+        if Platform.isLinux():
+            preferences_file_name = CuraApplication.getInstance().getApplicationName()
+            preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
+            backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
+            Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
+            shutil.copyfile(preferences_file, backup_preferences_file)
+
+        # Create an empty buffer and write the archive to it.
+        buffer = io.BytesIO()
+        archive = self._makeArchive(buffer, version_data_dir)
+        files = archive.namelist()
+        
+        # Count the metadata items. We do this in a rather naive way at the moment.
+        machine_count = len([s for s in files if "machine_instances/" in s]) - 1
+        material_count = len([s for s in files if "materials/" in s]) - 1
+        profile_count = len([s for s in files if "quality_changes/" in s]) - 1
+        plugin_count = len([s for s in files if "plugin.json" in s])
+        
+        # Store the archive and metadata so the BackupManager can fetch them when needed.
+        self.zip_file = buffer.getvalue()
+        self.meta_data = {
+            "cura_release": cura_release,
+            "machine_count": str(machine_count),
+            "material_count": str(material_count),
+            "profile_count": str(profile_count),
+            "plugin_count": str(plugin_count)
+        }
+
+    def _makeArchive(self, buffer: "io.BytesIO", root_path: str) -> Optional[ZipFile]:
+        """
+        Make a full archive from the given root path with the given name.
+        :param root_path: The root directory to archive recursively.
+        :return: The archive as bytes.
+        """
+        ignore_string = re.compile("|".join(self.IGNORED_FILES))
+        try:
+            archive = ZipFile(buffer, "w", ZIP_DEFLATED)
+            for root, folders, files in os.walk(root_path):
+                for item_name in folders + files:
+                    absolute_path = os.path.join(root, item_name)
+                    if ignore_string.search(absolute_path):
+                        continue
+                    archive.write(absolute_path, absolute_path[len(root_path) + len(os.sep):])
+            archive.close()
+            return archive
+        except (IOError, OSError, BadZipfile) as error:
+            Logger.log("e", "Could not create archive from user data directory: %s", error)
+            self._showMessage(
+                self.catalog.i18nc("@info:backup_failed",
+                                   "Could not create archive from user data directory: {}".format(error)))
+            return None
+
+    def _showMessage(self, message: str) -> None:
+        """Show a UI message"""
+        Message(message, title=self.catalog.i18nc("@info:title", "Backup"), lifetime=30).show()
+
+    def restore(self) -> bool:
+        """
+        Restore this backups
+        :return: A boolean whether we had success or not.
+        """
+        if not self.zip_file or not self.meta_data or not self.meta_data.get("cura_release", None):
+            # We can restore without the minimum required information.
+            Logger.log("w", "Tried to restore a Cura backup without having proper data or meta data.")
+            self._showMessage(
+                self.catalog.i18nc("@info:backup_failed",
+                                   "Tried to restore a Cura backup without having proper data or meta data."))
+            return False
+
+        current_version = CuraApplication.getInstance().getVersion()
+        version_to_restore = self.meta_data.get("cura_release", "master")
+        if current_version != version_to_restore:
+            # Cannot restore version older or newer than current because settings might have changed.
+            # Restoring this will cause a lot of issues so we don't allow this for now.
+            self._showMessage(
+                self.catalog.i18nc("@info:backup_failed",
+                                   "Tried to restore a Cura backup that does not match your current version."))
+            return False
+
+        version_data_dir = Resources.getDataStoragePath()
+        archive = ZipFile(io.BytesIO(self.zip_file), "r")
+        extracted = self._extractArchive(archive, version_data_dir)
+
+        # Under Linux, preferences are stored elsewhere, so we copy the file to there.
+        if Platform.isLinux():
+            preferences_file_name = CuraApplication.getInstance().getApplicationName()
+            preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
+            backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
+            Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)
+            shutil.move(backup_preferences_file, preferences_file)
+
+        return extracted
+
+    @staticmethod
+    def _extractArchive(archive: "ZipFile", target_path: str) -> bool:
+        """
+        Extract the whole archive to the given target path.
+        :param archive: The archive as ZipFile.
+        :param target_path: The target path.
+        :return: A boolean whether we had success or not.
+        """
+        Logger.log("d", "Removing current data in location: %s", target_path)
+        Resources.factoryReset()
+        Logger.log("d", "Extracting backup to location: %s", target_path)
+        archive.extractall(target_path)
+        return True

+ 56 - 0
cura/Backups/BackupsManager.py

@@ -0,0 +1,56 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Optional
+
+from UM.Logger import Logger
+from cura.Backups.Backup import Backup
+from cura.CuraApplication import CuraApplication
+
+
+class BackupsManager:
+    """
+    The BackupsManager is responsible for managing the creating and restoring of backups.
+    Backups themselves are represented in a different class.
+    """
+    def __init__(self):
+        self._application = CuraApplication.getInstance()
+
+    def createBackup(self) -> (Optional[bytes], Optional[dict]):
+        """
+        Get a backup of the current configuration.
+        :return: A Tuple containing a ZipFile (the actual backup) and a dict containing some meta data (like version).
+        """
+        self._disableAutoSave()
+        backup = Backup()
+        backup.makeFromCurrent()
+        self._enableAutoSave()
+        # We don't return a Backup here because we want plugins only to interact with our API and not full objects.
+        return backup.zip_file, backup.meta_data
+
+    def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
+        """
+        Restore a backup from a given ZipFile.
+        :param zip_file: A bytes object containing the actual backup.
+        :param meta_data: A dict containing some meta data that is needed to restore the backup correctly.
+        """
+        if not meta_data.get("cura_release", None):
+            # If there is no "cura_release" specified in the meta data, we don't execute a backup restore.
+            Logger.log("w", "Tried to restore a backup without specifying a Cura version number.")
+            return
+
+        self._disableAutoSave()
+
+        backup = Backup(zip_file = zip_file, meta_data = meta_data)
+        restored = backup.restore()
+        if restored:
+            # At this point, Cura will need to restart for the changes to take effect.
+            # We don't want to store the data at this point as that would override the just-restored backup.
+            self._application.windowClosed(save_data=False)
+
+    def _disableAutoSave(self):
+        """Here we try to disable the auto-save plugin as it might interfere with restoring a backup."""
+        self._application.setSaveDataEnabled(False)
+
+    def _enableAutoSave(self):
+        """Re-enable auto-save after we're done."""
+        self._application.setSaveDataEnabled(True)

Some files were not shown because too many files changed in this diff