diff options
author | Diego Prado Gesto <d.pradogesto@ultimaker.com> | 2019-05-07 12:57:31 +0300 |
---|---|---|
committer | Diego Prado Gesto <d.pradogesto@ultimaker.com> | 2019-05-07 12:57:31 +0300 |
commit | 9e5e57e6c54e5e54a32799115d8b61119182d99f (patch) | |
tree | 198587223d07174490942a529405d46fee45fb1d /cura/UI | |
parent | 730564345b0797b541888899493f17e7d4c3ba44 (diff) | |
parent | 6708d9da642230bfd71c57d103916a1c5c5b369c (diff) |
Merge branch 'master' into feature_model_list
Diffstat (limited to 'cura/UI')
-rw-r--r-- | cura/UI/AddPrinterPagesModel.py | 31 | ||||
-rw-r--r-- | cura/UI/CuraSplashScreen.py | 106 | ||||
-rw-r--r-- | cura/UI/MachineActionManager.py | 161 | ||||
-rw-r--r-- | cura/UI/MachineSettingsManager.py | 82 | ||||
-rw-r--r-- | cura/UI/ObjectsModel.py | 112 | ||||
-rw-r--r-- | cura/UI/PrintInformation.py | 435 | ||||
-rw-r--r-- | cura/UI/TextManager.py | 69 | ||||
-rw-r--r-- | cura/UI/WelcomePagesModel.py | 294 | ||||
-rw-r--r-- | cura/UI/WhatsNewPagesModel.py | 22 | ||||
-rw-r--r-- | cura/UI/__init__.py | 0 |
10 files changed, 1312 insertions, 0 deletions
diff --git a/cura/UI/AddPrinterPagesModel.py b/cura/UI/AddPrinterPagesModel.py new file mode 100644 index 0000000000..d40da59b2a --- /dev/null +++ b/cura/UI/AddPrinterPagesModel.py @@ -0,0 +1,31 @@ +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from .WelcomePagesModel import WelcomePagesModel + + +# +# This Qt ListModel is more or less the same the WelcomePagesModel, except that this model is only for adding a printer, +# so only the steps for adding a printer is included. +# +class AddPrinterPagesModel(WelcomePagesModel): + + def initialize(self) -> None: + self._pages.append({"id": "add_network_or_local_printer", + "page_url": self._getBuiltinWelcomePagePath("AddNetworkOrLocalPrinterContent.qml"), + "next_page_id": "machine_actions", + "next_page_button_text": self._catalog.i18nc("@action:button", "Add"), + "previous_page_button_text": self._catalog.i18nc("@action:button", "Cancel"), + }) + self._pages.append({"id": "add_printer_by_ip", + "page_url": self._getBuiltinWelcomePagePath("AddPrinterByIpContent.qml"), + "next_page_id": "machine_actions", + }) + self._pages.append({"id": "machine_actions", + "page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"), + "should_show_function": self.shouldShowMachineActions, + }) + self.setItems(self._pages) + + +__all__ = ["AddPrinterPagesModel"] diff --git a/cura/UI/CuraSplashScreen.py b/cura/UI/CuraSplashScreen.py new file mode 100644 index 0000000000..77c9ad1427 --- /dev/null +++ b/cura/UI/CuraSplashScreen.py @@ -0,0 +1,106 @@ +# Copyright (c) 2017 Ultimaker B.V. +# Uranium is released under the terms of the LGPLv3 or higher. + +from PyQt5.QtCore import Qt, QCoreApplication, QTimer +from PyQt5.QtGui import QPixmap, QColor, QFont, QPen, QPainter +from PyQt5.QtWidgets import QSplashScreen + +from UM.Resources import Resources +from UM.Application import Application + + +class CuraSplashScreen(QSplashScreen): + def __init__(self): + super().__init__() + self._scale = 0.7 + + splash_image = QPixmap(Resources.getPath(Resources.Images, "cura.png")) + self.setPixmap(splash_image) + + self._current_message = "" + + self._loading_image_rotation_angle = 0 + + self._to_stop = False + self._change_timer = QTimer() + self._change_timer.setInterval(50) + self._change_timer.setSingleShot(False) + self._change_timer.timeout.connect(self.updateLoadingImage) + + def show(self): + super().show() + self._change_timer.start() + + def updateLoadingImage(self): + if self._to_stop: + return + + self._loading_image_rotation_angle -= 10 + self.repaint() + + # Override the mousePressEvent so the splashscreen doesn't disappear when clicked + def mousePressEvent(self, mouse_event): + pass + + def drawContents(self, painter): + if self._to_stop: + return + + painter.save() + painter.setPen(QColor(255, 255, 255, 255)) + painter.setRenderHint(QPainter.Antialiasing) + painter.setRenderHint(QPainter.Antialiasing, True) + + version = Application.getInstance().getVersion().split("-") + buildtype = Application.getInstance().getBuildType() + if buildtype: + version[0] += " (%s)" % buildtype + + # draw version text + font = QFont() # Using system-default font here + font.setPixelSize(37) + painter.setFont(font) + painter.drawText(215, 66, 330 * self._scale, 230 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[0]) + if len(version) > 1: + font.setPixelSize(16) + painter.setFont(font) + painter.setPen(QColor(200, 200, 200, 255)) + painter.drawText(247, 105, 330 * self._scale, 255 * self._scale, Qt.AlignLeft | Qt.AlignTop, version[1]) + painter.setPen(QColor(255, 255, 255, 255)) + + # draw the loading image + pen = QPen() + pen.setWidth(6 * self._scale) + pen.setColor(QColor(32, 166, 219, 255)) + painter.setPen(pen) + painter.drawArc(60, 150, 32 * self._scale, 32 * self._scale, self._loading_image_rotation_angle * 16, 300 * 16) + + # draw message text + if self._current_message: + font = QFont() # Using system-default font here + font.setPixelSize(13) + pen = QPen() + pen.setColor(QColor(255, 255, 255, 255)) + painter.setPen(pen) + painter.setFont(font) + painter.drawText(100, 128, 170, 64, + Qt.AlignLeft | Qt.AlignVCenter | Qt.TextWordWrap, + self._current_message) + + painter.restore() + super().drawContents(painter) + + def showMessage(self, message, *args, **kwargs): + if self._to_stop: + return + + self._current_message = message + self.messageChanged.emit(message) + QCoreApplication.flush() + self.repaint() + + def close(self): + # set stop flags + self._to_stop = True + self._change_timer.stop() + super().close() diff --git a/cura/UI/MachineActionManager.py b/cura/UI/MachineActionManager.py new file mode 100644 index 0000000000..aa90e909e2 --- /dev/null +++ b/cura/UI/MachineActionManager.py @@ -0,0 +1,161 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import TYPE_CHECKING, Optional, List, Set, Dict + +from PyQt5.QtCore import QObject + +from UM.FlameProfiler import pyqtSlot +from UM.Logger import Logger +from UM.PluginRegistry import PluginRegistry # So MachineAction can be added as plugin type + +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication + from cura.Settings.GlobalStack import GlobalStack + from cura.MachineAction import MachineAction + + +## Raised when trying to add an unknown machine action as a required action +class UnknownMachineActionError(Exception): + pass + + +## Raised when trying to add a machine action that does not have an unique key. +class NotUniqueMachineActionError(Exception): + pass + + +class MachineActionManager(QObject): + def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None: + super().__init__(parent = parent) + self._application = application + self._container_registry = self._application.getContainerRegistry() + + # Keeps track of which machines have already been processed so we don't do that again. + self._definition_ids_with_default_actions_added = set() # type: Set[str] + + # Dict of all known machine actions + self._machine_actions = {} # type: Dict[str, MachineAction] + # Dict of all required actions by definition ID + self._required_actions = {} # type: Dict[str, List[MachineAction]] + # Dict of all supported actions by definition ID + self._supported_actions = {} # type: Dict[str, List[MachineAction]] + # Dict of all actions that need to be done when first added by definition ID + self._first_start_actions = {} # type: Dict[str, List[MachineAction]] + + def initialize(self): + # Add machine_action as plugin type + PluginRegistry.addType("machine_action", self.addMachineAction) + + # Adds all default machine actions that are defined in the machine definition for the given machine. + def addDefaultMachineActions(self, global_stack: "GlobalStack") -> None: + definition_id = global_stack.definition.getId() + + if definition_id in self._definition_ids_with_default_actions_added: + Logger.log("i", "Default machine actions have been added for machine definition [%s], do nothing.", + definition_id) + return + + supported_actions = global_stack.getMetaDataEntry("supported_actions", []) + for action_key in supported_actions: + self.addSupportedAction(definition_id, action_key) + + required_actions = global_stack.getMetaDataEntry("required_actions", []) + for action_key in required_actions: + self.addRequiredAction(definition_id, action_key) + + first_start_actions = global_stack.getMetaDataEntry("first_start_actions", []) + for action_key in first_start_actions: + self.addFirstStartAction(definition_id, action_key) + + self._definition_ids_with_default_actions_added.add(definition_id) + Logger.log("i", "Default machine actions added for machine definition [%s]", definition_id) + + ## Add a required action to a machine + # Raises an exception when the action is not recognised. + def addRequiredAction(self, definition_id: str, action_key: str) -> None: + if action_key in self._machine_actions: + if definition_id in self._required_actions: + if self._machine_actions[action_key] not in self._required_actions[definition_id]: + self._required_actions[definition_id].append(self._machine_actions[action_key]) + else: + self._required_actions[definition_id] = [self._machine_actions[action_key]] + else: + raise UnknownMachineActionError("Action %s, which is required for %s is not known." % (action_key, definition_id)) + + ## Add a supported action to a machine. + def addSupportedAction(self, definition_id: str, action_key: str) -> None: + if action_key in self._machine_actions: + if definition_id in self._supported_actions: + if self._machine_actions[action_key] not in self._supported_actions[definition_id]: + self._supported_actions[definition_id].append(self._machine_actions[action_key]) + else: + self._supported_actions[definition_id] = [self._machine_actions[action_key]] + else: + Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id) + + ## Add an action to the first start list of a machine. + def addFirstStartAction(self, definition_id: str, action_key: str) -> None: + if action_key in self._machine_actions: + if definition_id in self._first_start_actions: + self._first_start_actions[definition_id].append(self._machine_actions[action_key]) + else: + self._first_start_actions[definition_id] = [self._machine_actions[action_key]] + else: + Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id) + + ## Add a (unique) MachineAction + # if the Key of the action is not unique, an exception is raised. + def addMachineAction(self, action: "MachineAction") -> None: + if action.getKey() not in self._machine_actions: + self._machine_actions[action.getKey()] = action + else: + raise NotUniqueMachineActionError("MachineAction with key %s was already added. Actions must have unique keys.", action.getKey()) + + ## Get all actions supported by given machine + # \param definition_id The ID of the definition you want the supported actions of + # \returns set of supported actions. + @pyqtSlot(str, result = "QVariantList") + def getSupportedActions(self, definition_id: str) -> List["MachineAction"]: + if definition_id in self._supported_actions: + return list(self._supported_actions[definition_id]) + else: + return list() + + ## Get all actions required by given machine + # \param definition_id The ID of the definition you want the required actions of + # \returns set of required actions. + def getRequiredActions(self, definition_id: str) -> List["MachineAction"]: + if definition_id in self._required_actions: + return self._required_actions[definition_id] + else: + return list() + + ## Get all actions that need to be performed upon first start of a given machine. + # Note that contrary to required / supported actions a list is returned (as it could be required to run the same + # action multiple times). + # \param definition_id The ID of the definition that you want to get the "on added" actions for. + # \returns List of actions. + @pyqtSlot(str, result = "QVariantList") + def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]: + if definition_id in self._first_start_actions: + return self._first_start_actions[definition_id] + else: + return [] + + ## Remove Machine action from manager + # \param action to remove + def removeMachineAction(self, action: "MachineAction") -> None: + try: + del self._machine_actions[action.getKey()] + except KeyError: + Logger.log("w", "Trying to remove MachineAction (%s) that was already removed", action.getKey()) + + ## Get MachineAction by key + # \param key String of key to select + # \return Machine action if found, None otherwise + def getMachineAction(self, key: str) -> Optional["MachineAction"]: + if key in self._machine_actions: + return self._machine_actions[key] + else: + return None diff --git a/cura/UI/MachineSettingsManager.py b/cura/UI/MachineSettingsManager.py new file mode 100644 index 0000000000..7ecd9ed65f --- /dev/null +++ b/cura/UI/MachineSettingsManager.py @@ -0,0 +1,82 @@ +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from typing import Optional, TYPE_CHECKING + +from PyQt5.QtCore import QObject, pyqtSlot + +from UM.i18n import i18nCatalog + +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication + + +# +# This manager provides (convenience) functions to the Machine Settings Dialog QML to update certain machine settings. +# +class MachineSettingsManager(QObject): + + def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None: + super().__init__(parent) + self._i18n_catalog = i18nCatalog("cura") + + self._application = application + + # Force rebuilding the build volume by reloading the global container stack. This is a bit of a hack, but it seems + # quite enough. + @pyqtSlot() + def forceUpdate(self) -> None: + self._application.getMachineManager().globalContainerChanged.emit() + + # Function for the Machine Settings panel (QML) to update the compatible material diameter after a user has changed + # an extruder's compatible material diameter. This ensures that after the modification, changes can be notified + # and updated right away. + @pyqtSlot(int) + def updateMaterialForDiameter(self, extruder_position: int) -> None: + # Updates the material container to a material that matches the material diameter set for the printer + self._application.getMachineManager().updateMaterialWithVariant(str(extruder_position)) + + @pyqtSlot(int) + def setMachineExtruderCount(self, extruder_count: int) -> None: + # Note: this method was in this class before, but since it's quite generic and other plugins also need it + # it was moved to the machine manager instead. Now this method just calls the machine manager. + self._application.getMachineManager().setActiveMachineExtruderCount(extruder_count) + + # Function for the Machine Settings panel (QML) to update after the usre changes "Number of Extruders". + # + # fieldOfView: The Ultimaker 2 family (not 2+) does not have materials in Cura by default, because the material is + # to be set on the printer. But when switching to Marlin flavor, the printer firmware can not change/insert material + # settings on the fly so they need to be configured in Cura. So when switching between gcode flavors, materials may + # need to be enabled/disabled. + @pyqtSlot() + def updateHasMaterialsMetadata(self): + machine_manager = self._application.getMachineManager() + material_manager = self._application.getMaterialManager() + + global_stack = machine_manager.activeMachine + + definition = global_stack.definition + if definition.getProperty("machine_gcode_flavor", "value") != "UltiGCode" or definition.getMetaDataEntry( + "has_materials", False): + # In other words: only continue for the UM2 (extended), but not for the UM2+ + return + + extruder_positions = list(global_stack.extruders.keys()) + has_materials = global_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode" + + material_node = None + if has_materials: + global_stack.setMetaDataEntry("has_materials", True) + else: + # The metadata entry is stored in an ini, and ini files are parsed as strings only. + # Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False. + if "has_materials" in global_stack.getMetaData(): + global_stack.removeMetaDataEntry("has_materials") + + # set materials + for position in extruder_positions: + if has_materials: + material_node = material_manager.getDefaultMaterial(global_stack, position, None) + machine_manager.setMaterial(position, material_node) + + self.forceUpdate() diff --git a/cura/UI/ObjectsModel.py b/cura/UI/ObjectsModel.py new file mode 100644 index 0000000000..d1ca3353f5 --- /dev/null +++ b/cura/UI/ObjectsModel.py @@ -0,0 +1,112 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from collections import defaultdict +from typing import Dict + +from PyQt5.QtCore import QTimer, Qt + +from UM.Application import Application +from UM.Qt.ListModel import ListModel +from UM.Scene.Camera import Camera +from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator +from UM.Scene.SceneNode import SceneNode +from UM.Scene.Selection import Selection +from UM.i18n import i18nCatalog + +catalog = i18nCatalog("cura") + + +## Keep track of all objects in the project +class ObjectsModel(ListModel): + NameRole = Qt.UserRole + 1 + SelectedRole = Qt.UserRole + 2 + OutsideAreaRole = Qt.UserRole + 3 + BuilplateNumberRole = Qt.UserRole + 4 + NodeRole = Qt.UserRole + 5 + + def __init__(self, parent = None) -> None: + super().__init__(parent) + + self.addRoleName(self.NameRole, "name") + self.addRoleName(self.SelectedRole, "selected") + self.addRoleName(self.OutsideAreaRole, "outside_build_area") + self.addRoleName(self.BuilplateNumberRole, "buildplate_number") + self.addRoleName(self.NodeRole, "node") + + Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSceneDelayed) + Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed) + + self._update_timer = QTimer() + self._update_timer.setInterval(200) + self._update_timer.setSingleShot(True) + self._update_timer.timeout.connect(self._update) + + self._build_plate_number = -1 + + def setActiveBuildPlate(self, nr: int) -> None: + if self._build_plate_number != nr: + self._build_plate_number = nr + self._update() + + def _updateSceneDelayed(self, source) -> None: + if not isinstance(source, Camera): + self._update_timer.start() + + def _updateDelayed(self, *args) -> None: + self._update_timer.start() + + def _update(self, *args) -> None: + nodes = [] + filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate") + active_build_plate_number = self._build_plate_number + group_nr = 1 + name_count_dict = defaultdict(int) # type: Dict[str, int] + + for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): # type: ignore + if not isinstance(node, SceneNode): + continue + if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"): + continue + + parent = node.getParent() + if parent and parent.callDecoration("isGroup"): + continue # Grouped nodes don't need resetting as their parent (the group) is resetted) + if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"): + continue + node_build_plate_number = node.callDecoration("getBuildPlateNumber") + if filter_current_build_plate and node_build_plate_number != active_build_plate_number: + continue + + if not node.callDecoration("isGroup"): + name = node.getName() + + else: + name = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = str(group_nr)) + group_nr += 1 + + if hasattr(node, "isOutsideBuildArea"): + is_outside_build_area = node.isOutsideBuildArea() # type: ignore + else: + is_outside_build_area = False + + # Check if we already have an instance of the object based on name + name_count_dict[name] += 1 + name_count = name_count_dict[name] + + if name_count > 1: + name = "{0}({1})".format(name, name_count-1) + node.setName(name) + + nodes.append({ + "name": name, + "selected": Selection.isSelected(node), + "outside_build_area": is_outside_build_area, + "buildplate_number": node_build_plate_number, + "node": node + }) + + nodes = sorted(nodes, key=lambda n: n["name"]) + self.setItems(nodes) + + self.itemsChanged.emit() diff --git a/cura/UI/PrintInformation.py b/cura/UI/PrintInformation.py new file mode 100644 index 0000000000..3fafaaba12 --- /dev/null +++ b/cura/UI/PrintInformation.py @@ -0,0 +1,435 @@ +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +import json +import math +import os +import unicodedata +from typing import Dict, List, Optional, TYPE_CHECKING + +from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot + +from UM.Logger import Logger +from UM.Qt.Duration import Duration +from UM.Scene.SceneNode import SceneNode +from UM.i18n import i18nCatalog +from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError + +if TYPE_CHECKING: + from cura.CuraApplication import CuraApplication + +catalog = i18nCatalog("cura") + + +## A class for processing and the print times per build plate as well as managing the job name +# +# This class also mangles the current machine name and the filename of the first loaded mesh into a job name. +# This job name is requested by the JobSpecs qml file. +class PrintInformation(QObject): + + UNTITLED_JOB_NAME = "Untitled" + + def __init__(self, application: "CuraApplication", parent = None) -> None: + super().__init__(parent) + self._application = application + + self.initializeCuraMessagePrintTimeProperties() + + # Indexed by build plate number + self._material_lengths = {} # type: Dict[int, List[float]] + self._material_weights = {} # type: Dict[int, List[float]] + self._material_costs = {} # type: Dict[int, List[float]] + self._material_names = {} # type: Dict[int, List[str]] + + self._pre_sliced = False + + self._backend = self._application.getBackend() + if self._backend: + self._backend.printDurationMessage.connect(self._onPrintDurationMessage) + + self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged) + + self._is_user_specified_job_name = False + self._base_name = "" + self._abbr_machine = "" + self._job_name = "" + self._active_build_plate = 0 + self._initVariablesByBuildPlate(self._active_build_plate) + + self._multi_build_plate_model = self._application.getMultiBuildPlateModel() + + self._application.globalContainerStackChanged.connect(self._updateJobName) + self._application.globalContainerStackChanged.connect(self.setToZeroPrintInformation) + self._application.fileLoaded.connect(self.setBaseName) + self._application.workspaceLoaded.connect(self.setProjectName) + self._application.getMachineManager().rootMaterialChanged.connect(self._onActiveMaterialsChanged) + self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged) + + self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged) + self._material_amounts = [] # type: List[float] + self._onActiveMaterialsChanged() + + def initializeCuraMessagePrintTimeProperties(self) -> None: + self._current_print_time = {} # type: Dict[int, Duration] + + self._print_time_message_translations = { + "inset_0": catalog.i18nc("@tooltip", "Outer Wall"), + "inset_x": catalog.i18nc("@tooltip", "Inner Walls"), + "skin": catalog.i18nc("@tooltip", "Skin"), + "infill": catalog.i18nc("@tooltip", "Infill"), + "support_infill": catalog.i18nc("@tooltip", "Support Infill"), + "support_interface": catalog.i18nc("@tooltip", "Support Interface"), + "support": catalog.i18nc("@tooltip", "Support"), + "skirt": catalog.i18nc("@tooltip", "Skirt"), + "prime_tower": catalog.i18nc("@tooltip", "Prime Tower"), + "travel": catalog.i18nc("@tooltip", "Travel"), + "retract": catalog.i18nc("@tooltip", "Retractions"), + "none": catalog.i18nc("@tooltip", "Other") + } + + self._print_times_per_feature = {} # type: Dict[int, Dict[str, Duration]] + + def _initPrintTimesPerFeature(self, build_plate_number: int) -> None: + # Full fill message values using keys from _print_time_message_translations + self._print_times_per_feature[build_plate_number] = {} + for key in self._print_time_message_translations.keys(): + self._print_times_per_feature[build_plate_number][key] = Duration(None, self) + + def _initVariablesByBuildPlate(self, build_plate_number: int) -> None: + if build_plate_number not in self._print_times_per_feature: + self._initPrintTimesPerFeature(build_plate_number) + if self._active_build_plate not in self._material_lengths: + self._material_lengths[self._active_build_plate] = [] + if self._active_build_plate not in self._material_weights: + self._material_weights[self._active_build_plate] = [] + if self._active_build_plate not in self._material_costs: + self._material_costs[self._active_build_plate] = [] + if self._active_build_plate not in self._material_names: + self._material_names[self._active_build_plate] = [] + if self._active_build_plate not in self._current_print_time: + self._current_print_time[self._active_build_plate] = Duration(parent = self) + + currentPrintTimeChanged = pyqtSignal() + + preSlicedChanged = pyqtSignal() + + @pyqtProperty(bool, notify=preSlicedChanged) + def preSliced(self) -> bool: + return self._pre_sliced + + def setPreSliced(self, pre_sliced: bool) -> None: + if self._pre_sliced != pre_sliced: + self._pre_sliced = pre_sliced + self._updateJobName() + self.preSlicedChanged.emit() + + @pyqtProperty(Duration, notify = currentPrintTimeChanged) + def currentPrintTime(self) -> Duration: + return self._current_print_time[self._active_build_plate] + + materialLengthsChanged = pyqtSignal() + + @pyqtProperty("QVariantList", notify = materialLengthsChanged) + def materialLengths(self): + return self._material_lengths[self._active_build_plate] + + materialWeightsChanged = pyqtSignal() + + @pyqtProperty("QVariantList", notify = materialWeightsChanged) + def materialWeights(self): + return self._material_weights[self._active_build_plate] + + materialCostsChanged = pyqtSignal() + + @pyqtProperty("QVariantList", notify = materialCostsChanged) + def materialCosts(self): + return self._material_costs[self._active_build_plate] + + materialNamesChanged = pyqtSignal() + + @pyqtProperty("QVariantList", notify = materialNamesChanged) + def materialNames(self): + return self._material_names[self._active_build_plate] + + # Get all print times (by feature) of the active buildplate. + def printTimes(self) -> Dict[str, Duration]: + return self._print_times_per_feature[self._active_build_plate] + + def _onPrintDurationMessage(self, build_plate_number: int, print_times_per_feature: Dict[str, int], material_amounts: List[float]) -> None: + self._updateTotalPrintTimePerFeature(build_plate_number, print_times_per_feature) + self.currentPrintTimeChanged.emit() + + self._material_amounts = material_amounts + self._calculateInformation(build_plate_number) + + def _updateTotalPrintTimePerFeature(self, build_plate_number: int, print_times_per_feature: Dict[str, int]) -> None: + total_estimated_time = 0 + + if build_plate_number not in self._print_times_per_feature: + self._initPrintTimesPerFeature(build_plate_number) + + for feature, time in print_times_per_feature.items(): + if feature not in self._print_times_per_feature[build_plate_number]: + self._print_times_per_feature[build_plate_number][feature] = Duration(parent=self) + duration = self._print_times_per_feature[build_plate_number][feature] + + if time != time: # Check for NaN. Engine can sometimes give us weird values. + duration.setDuration(0) + Logger.log("w", "Received NaN for print duration message") + continue + + total_estimated_time += time + duration.setDuration(time) + + if build_plate_number not in self._current_print_time: + self._current_print_time[build_plate_number] = Duration(None, self) + self._current_print_time[build_plate_number].setDuration(total_estimated_time) + + def _calculateInformation(self, build_plate_number: int) -> None: + global_stack = self._application.getGlobalContainerStack() + if global_stack is None: + return + + self._material_lengths[build_plate_number] = [] + self._material_weights[build_plate_number] = [] + self._material_costs[build_plate_number] = [] + self._material_names[build_plate_number] = [] + + material_preference_values = json.loads(self._application.getInstance().getPreferences().getValue("cura/material_settings")) + + extruder_stacks = global_stack.extruders + + for position in extruder_stacks: + extruder_stack = extruder_stacks[position] + index = int(position) + if index >= len(self._material_amounts): + continue + amount = self._material_amounts[index] + # Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some + # list comprehension filtering to solve this for us. + density = extruder_stack.getMetaDataEntry("properties", {}).get("density", 0) + material = extruder_stack.material + radius = extruder_stack.getProperty("material_diameter", "value") / 2 + + weight = float(amount) * float(density) / 1000 + cost = 0. + + material_guid = material.getMetaDataEntry("GUID") + material_name = material.getName() + + if material_guid in material_preference_values: + material_values = material_preference_values[material_guid] + + if material_values and "spool_weight" in material_values: + weight_per_spool = float(material_values["spool_weight"]) + else: + weight_per_spool = float(extruder_stack.getMetaDataEntry("properties", {}).get("weight", 0)) + + cost_per_spool = float(material_values["spool_cost"] if material_values and "spool_cost" in material_values else 0) + + if weight_per_spool != 0: + cost = cost_per_spool * weight / weight_per_spool + else: + cost = 0 + + # Material amount is sent as an amount of mm^3, so calculate length from that + if radius != 0: + length = round((amount / (math.pi * radius ** 2)) / 1000, 2) + else: + length = 0 + + self._material_weights[build_plate_number].append(weight) + self._material_lengths[build_plate_number].append(length) + self._material_costs[build_plate_number].append(cost) + self._material_names[build_plate_number].append(material_name) + + self.materialLengthsChanged.emit() + self.materialWeightsChanged.emit() + self.materialCostsChanged.emit() + self.materialNamesChanged.emit() + + def _onPreferencesChanged(self, preference: str) -> None: + if preference != "cura/material_settings": + return + + for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1): + self._calculateInformation(build_plate_number) + + def _onActiveBuildPlateChanged(self) -> None: + new_active_build_plate = self._multi_build_plate_model.activeBuildPlate + if new_active_build_plate != self._active_build_plate: + self._active_build_plate = new_active_build_plate + self._updateJobName() + + self._initVariablesByBuildPlate(self._active_build_plate) + + self.materialLengthsChanged.emit() + self.materialWeightsChanged.emit() + self.materialCostsChanged.emit() + self.materialNamesChanged.emit() + self.currentPrintTimeChanged.emit() + + def _onActiveMaterialsChanged(self, *args, **kwargs) -> None: + for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1): + self._calculateInformation(build_plate_number) + + # Manual override of job name should also set the base name so that when the printer prefix is updated, it the + # prefix can be added to the manually added name, not the old base name + @pyqtSlot(str, bool) + def setJobName(self, name: str, is_user_specified_job_name = False) -> None: + self._is_user_specified_job_name = is_user_specified_job_name + self._job_name = name + self._base_name = name.replace(self._abbr_machine + "_", "") + if name == "": + self._is_user_specified_job_name = False + self.jobNameChanged.emit() + + jobNameChanged = pyqtSignal() + + @pyqtProperty(str, notify = jobNameChanged) + def jobName(self): + return self._job_name + + def _updateJobName(self) -> None: + if self._base_name == "": + self._job_name = self.UNTITLED_JOB_NAME + self._is_user_specified_job_name = False + self.jobNameChanged.emit() + return + + base_name = self._stripAccents(self._base_name) + self._defineAbbreviatedMachineName() + + # Only update the job name when it's not user-specified. + if not self._is_user_specified_job_name: + if self._pre_sliced: + self._job_name = catalog.i18nc("@label", "Pre-sliced file {0}", base_name) + elif self._application.getInstance().getPreferences().getValue("cura/jobname_prefix"): + # Don't add abbreviation if it already has the exact same abbreviation. + if base_name.startswith(self._abbr_machine + "_"): + self._job_name = base_name + else: + self._job_name = self._abbr_machine + "_" + base_name + else: + self._job_name = base_name + + # In case there are several buildplates, a suffix is attached + if self._multi_build_plate_model.maxBuildPlate > 0: + connector = "_#" + suffix = connector + str(self._active_build_plate + 1) + if connector in self._job_name: + self._job_name = self._job_name.split(connector)[0] # get the real name + if self._active_build_plate != 0: + self._job_name += suffix + + self.jobNameChanged.emit() + + @pyqtSlot(str) + def setProjectName(self, name: str) -> None: + self.setBaseName(name, is_project_file = True) + + baseNameChanged = pyqtSignal() + + def setBaseName(self, base_name: str, is_project_file: bool = False) -> None: + self._is_user_specified_job_name = False + + # Ensure that we don't use entire path but only filename + name = os.path.basename(base_name) + + # when a file is opened using the terminal; the filename comes from _onFileLoaded and still contains its + # extension. This cuts the extension off if necessary. + check_name = os.path.splitext(name)[0] + filename_parts = os.path.basename(base_name).split(".") + + # If it's a gcode, also always update the job name + is_gcode = False + if len(filename_parts) > 1: + # Only check the extension(s) + is_gcode = "gcode" in filename_parts[1:] + + # if this is a profile file, always update the job name + # name is "" when I first had some meshes and afterwards I deleted them so the naming should start again + is_empty = check_name == "" + if is_gcode or is_project_file or (is_empty or (self._base_name == "" and self._base_name != check_name)): + # Only take the file name part, Note : file name might have 'dot' in name as well + + data = "" + try: + mime_type = MimeTypeDatabase.getMimeTypeForFile(name) + data = mime_type.stripExtension(name) + except MimeTypeNotFoundError: + Logger.log("w", "Unsupported Mime Type Database file extension %s", name) + + if data is not None and check_name is not None: + self._base_name = data + else: + self._base_name = "" + + # Strip the old "curaproject" extension from the name + OLD_CURA_PROJECT_EXT = ".curaproject" + if self._base_name.lower().endswith(OLD_CURA_PROJECT_EXT): + self._base_name = self._base_name[:len(self._base_name) - len(OLD_CURA_PROJECT_EXT)] + + # CURA-5896 Try to strip extra extensions with an infinite amount of ".curaproject.3mf". + OLD_CURA_PROJECT_3MF_EXT = ".curaproject.3mf" + while self._base_name.lower().endswith(OLD_CURA_PROJECT_3MF_EXT): + self._base_name = self._base_name[:len(self._base_name) - len(OLD_CURA_PROJECT_3MF_EXT)] + + self._updateJobName() + + @pyqtProperty(str, fset = setBaseName, notify = baseNameChanged) + def baseName(self): + return self._base_name + + ## Created an acronym-like abbreviated machine name from the currently + # active machine name. + # Called each time the global stack is switched. + def _defineAbbreviatedMachineName(self) -> None: + global_container_stack = self._application.getGlobalContainerStack() + if not global_container_stack: + self._abbr_machine = "" + return + active_machine_type_name = global_container_stack.definition.getName() + + self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name) + + ## Utility method that strips accents from characters (eg: รข -> a) + def _stripAccents(self, to_strip: str) -> str: + return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn') + + @pyqtSlot(result = "QVariantMap") + def getFeaturePrintTimes(self) -> Dict[str, Duration]: + result = {} + if self._active_build_plate not in self._print_times_per_feature: + self._initPrintTimesPerFeature(self._active_build_plate) + for feature, time in self._print_times_per_feature[self._active_build_plate].items(): + if feature in self._print_time_message_translations: + result[self._print_time_message_translations[feature]] = time + else: + result[feature] = time + return result + + # Simulate message with zero time duration + def setToZeroPrintInformation(self, build_plate: Optional[int] = None) -> None: + if build_plate is None: + build_plate = self._active_build_plate + + # Construct the 0-time message + temp_message = {} + if build_plate not in self._print_times_per_feature: + self._print_times_per_feature[build_plate] = {} + for key in self._print_times_per_feature[build_plate].keys(): + temp_message[key] = 0 + temp_material_amounts = [0.] + + self._onPrintDurationMessage(build_plate, temp_message, temp_material_amounts) + + ## Listen to scene changes to check if we need to reset the print information + def _onSceneChanged(self, scene_node: SceneNode) -> None: + # Ignore any changes that are not related to sliceable objects + if not isinstance(scene_node, SceneNode)\ + or not scene_node.callDecoration("isSliceable")\ + or not scene_node.callDecoration("getBuildPlateNumber") == self._active_build_plate: + return + + self.setToZeroPrintInformation(self._active_build_plate) diff --git a/cura/UI/TextManager.py b/cura/UI/TextManager.py new file mode 100644 index 0000000000..86838a0b48 --- /dev/null +++ b/cura/UI/TextManager.py @@ -0,0 +1,69 @@ +# Copyright (c) 2019 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+import collections
+from typing import Optional, Dict, List, cast
+
+from PyQt5.QtCore import QObject, pyqtSlot
+
+from UM.Resources import Resources
+from UM.Version import Version
+
+
+#
+# This manager provides means to load texts to QML.
+#
+class TextManager(QObject):
+
+ def __init__(self, parent: Optional["QObject"] = None) -> None:
+ super().__init__(parent)
+
+ self._change_log_text = ""
+
+ @pyqtSlot(result = str)
+ def getChangeLogText(self) -> str:
+ if not self._change_log_text:
+ self._change_log_text = self._loadChangeLogText()
+ return self._change_log_text
+
+ def _loadChangeLogText(self) -> str:
+ # Load change log texts and organize them with a dict
+ file_path = Resources.getPath(Resources.Texts, "change_log.txt")
+ change_logs_dict = {} # type: Dict[Version, Dict[str, List[str]]]
+ with open(file_path, "r", encoding = "utf-8") as f:
+ open_version = None # type: Optional[Version]
+ open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog
+ for line in f:
+ line = line.replace("\n", "")
+ if "[" in line and "]" in line:
+ line = line.replace("[", "")
+ line = line.replace("]", "")
+ open_version = Version(line)
+ if open_version > Version([14, 99, 99]): # Bit of a hack: We released the 15.x.x versions before 2.x
+ open_version = Version([0, open_version.getMinor(), open_version.getRevision(), open_version.getPostfixVersion()])
+ open_header = ""
+ change_logs_dict[open_version] = collections.OrderedDict()
+ elif line.startswith("*"):
+ open_header = line.replace("*", "")
+ change_logs_dict[cast(Version, open_version)][open_header] = []
+ elif line != "":
+ if open_header not in change_logs_dict[cast(Version, open_version)]:
+ change_logs_dict[cast(Version, open_version)][open_header] = []
+ change_logs_dict[cast(Version, open_version)][open_header].append(line)
+
+ # Format changelog text
+ content = ""
+ for version in sorted(change_logs_dict.keys(), reverse = True):
+ text_version = version
+ if version < Version([1, 0, 0]): # Bit of a hack: We released the 15.x.x versions before 2.x
+ text_version = Version([15, version.getMinor(), version.getRevision(), version.getPostfixVersion()])
+ content += "<h1>" + str(text_version) + "</h1><br>"
+ content += ""
+ for change in change_logs_dict[version]:
+ if str(change) != "":
+ content += "<b>" + str(change) + "</b><br>"
+ for line in change_logs_dict[version][change]:
+ content += str(line) + "<br>"
+ content += "<br>"
+
+ return content
diff --git a/cura/UI/WelcomePagesModel.py b/cura/UI/WelcomePagesModel.py new file mode 100644 index 0000000000..c16ec3763e --- /dev/null +++ b/cura/UI/WelcomePagesModel.py @@ -0,0 +1,294 @@ +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. +from collections import deque +import os +from typing import TYPE_CHECKING, Optional, List, Dict, Any + +from PyQt5.QtCore import QUrl, Qt, pyqtSlot, pyqtProperty, pyqtSignal + +from UM.i18n import i18nCatalog +from UM.Logger import Logger +from UM.Qt.ListModel import ListModel +from UM.Resources import Resources + +if TYPE_CHECKING: + from PyQt5.QtCore import QObject + from cura.CuraApplication import CuraApplication + + +# +# This is the Qt ListModel that contains all welcome pages data. Each page is a page that can be shown as a step in the +# welcome wizard dialog. Each item in this ListModel represents a page, which contains the following fields: +# +# - id : A unique page_id which can be used in function goToPage(page_id) +# - page_url : The QUrl to the QML file that contains the content of this page +# - next_page_id : (OPTIONAL) The next page ID to go to when this page finished. This is optional. If this is not +# provided, it will go to the page with the current index + 1 +# - next_page_button_text: (OPTIONAL) The text to show for the "next" button, by default it's the translated text of +# "Next". Note that each step QML can decide whether to use this text or not, so it's not +# mandatory. +# - should_show_function : (OPTIONAL) An optional function that returns True/False indicating if this page should be +# shown. By default all pages should be shown. If a function returns False, that page will +# be skipped and its next page will be shown. +# +# Note that in any case, a page that has its "should_show_function" == False will ALWAYS be skipped. +# +class WelcomePagesModel(ListModel): + + IdRole = Qt.UserRole + 1 # Page ID + PageUrlRole = Qt.UserRole + 2 # URL to the page's QML file + NextPageIdRole = Qt.UserRole + 3 # The next page ID it should go to + NextPageButtonTextRole = Qt.UserRole + 4 # The text for the next page button + PreviousPageButtonTextRole = Qt.UserRole + 5 # The text for the previous page button + + def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None: + super().__init__(parent) + + self.addRoleName(self.IdRole, "id") + self.addRoleName(self.PageUrlRole, "page_url") + self.addRoleName(self.NextPageIdRole, "next_page_id") + self.addRoleName(self.NextPageButtonTextRole, "next_page_button_text") + self.addRoleName(self.PreviousPageButtonTextRole, "previous_page_button_text") + + self._application = application + self._catalog = i18nCatalog("cura") + + self._default_next_button_text = self._catalog.i18nc("@action:button", "Next") + + self._pages = [] # type: List[Dict[str, Any]] + + self._current_page_index = 0 + # Store all the previous page indices so it can go back. + self._previous_page_indices_stack = deque() # type: deque + + # If the welcome flow should be shown. It can show the complete flow or just the changelog depending on the + # specific case. See initialize() for how this variable is set. + self._should_show_welcome_flow = False + + allFinished = pyqtSignal() # emitted when all steps have been finished + currentPageIndexChanged = pyqtSignal() + + @pyqtProperty(int, notify = currentPageIndexChanged) + def currentPageIndex(self) -> int: + return self._current_page_index + + # Returns a float number in [0, 1] which indicates the current progress. + @pyqtProperty(float, notify = currentPageIndexChanged) + def currentProgress(self) -> float: + if len(self._items) == 0: + return 0 + else: + return self._current_page_index / len(self._items) + + # Indicates if the current page is the last page. + @pyqtProperty(bool, notify = currentPageIndexChanged) + def isCurrentPageLast(self) -> bool: + return self._current_page_index == len(self._items) - 1 + + def _setCurrentPageIndex(self, page_index: int) -> None: + if page_index != self._current_page_index: + self._previous_page_indices_stack.append(self._current_page_index) + self._current_page_index = page_index + self.currentPageIndexChanged.emit() + + # Ends the Welcome-Pages. Put as a separate function for cases like the 'decline' in the User-Agreement. + @pyqtSlot() + def atEnd(self) -> None: + self.allFinished.emit() + self.resetState() + + # Goes to the next page. + # If "from_index" is given, it will look for the next page to show starting from the "from_index" page instead of + # the "self._current_page_index". + @pyqtSlot() + def goToNextPage(self, from_index: Optional[int] = None) -> None: + # Look for the next page that should be shown + current_index = self._current_page_index if from_index is None else from_index + while True: + page_item = self._items[current_index] + + # Check if there's a "next_page_id" assigned. If so, go to that page. Otherwise, go to the page with the + # current index + 1. + next_page_id = page_item.get("next_page_id") + next_page_index = current_index + 1 + if next_page_id: + idx = self.getPageIndexById(next_page_id) + if idx is None: + # FIXME: If we cannot find the next page, we cannot do anything here. + Logger.log("e", "Cannot find page with ID [%s]", next_page_id) + return + next_page_index = idx + + # If we have reached the last page, emit allFinished signal and reset. + if next_page_index == len(self._items): + self.atEnd() + return + + # Check if the this page should be shown (default yes), if not, keep looking for the next one. + next_page_item = self.getItem(next_page_index) + if self._shouldPageBeShown(next_page_index): + break + + Logger.log("d", "Page [%s] should not be displayed, look for the next page.", next_page_item["id"]) + current_index = next_page_index + + # Move to the next page + self._setCurrentPageIndex(next_page_index) + + # Goes to the previous page. If there's no previous page, do nothing. + @pyqtSlot() + def goToPreviousPage(self) -> None: + if len(self._previous_page_indices_stack) == 0: + Logger.log("i", "No previous page, do nothing") + return + + previous_page_index = self._previous_page_indices_stack.pop() + self._current_page_index = previous_page_index + self.currentPageIndexChanged.emit() + + # Sets the current page to the given page ID. If the page ID is not found, do nothing. + @pyqtSlot(str) + def goToPage(self, page_id: str) -> None: + page_index = self.getPageIndexById(page_id) + if page_index is None: + # FIXME: If we cannot find the next page, we cannot do anything here. + Logger.log("e", "Cannot find page with ID [%s], go to the next page by default", page_index) + self.goToNextPage() + return + + if self._shouldPageBeShown(page_index): + # Move to that page if it should be shown + self._setCurrentPageIndex(page_index) + else: + # Find the next page to show starting from the "page_index" + self.goToNextPage(from_index = page_index) + + # Checks if the page with the given index should be shown by calling the "should_show_function" associated with it. + # If the function is not present, returns True (show page by default). + def _shouldPageBeShown(self, page_index: int) -> bool: + next_page_item = self.getItem(page_index) + should_show_function = next_page_item.get("should_show_function", lambda: True) + return should_show_function() + + # Resets the state of the WelcomePagesModel. This functions does the following: + # - Resets current_page_index to 0 + # - Clears the previous page indices stack + @pyqtSlot() + def resetState(self) -> None: + self._current_page_index = 0 + self._previous_page_indices_stack.clear() + + self.currentPageIndexChanged.emit() + + shouldShowWelcomeFlowChanged = pyqtSignal() + + @pyqtProperty(bool, notify = shouldShowWelcomeFlowChanged) + def shouldShowWelcomeFlow(self) -> bool: + return self._should_show_welcome_flow + + # Gets the page index with the given page ID. If the page ID doesn't exist, returns None. + def getPageIndexById(self, page_id: str) -> Optional[int]: + page_idx = None + for idx, page_item in enumerate(self._items): + if page_item["id"] == page_id: + page_idx = idx + break + return page_idx + + # Convenience function to get QUrl path to pages that's located in "resources/qml/WelcomePages". + def _getBuiltinWelcomePagePath(self, page_filename: str) -> "QUrl": + from cura.CuraApplication import CuraApplication + return QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, + os.path.join("WelcomePages", page_filename))) + + # FIXME: HACKs for optimization that we don't update the model every time the active machine gets changed. + def _onActiveMachineChanged(self) -> None: + self._application.getMachineManager().globalContainerChanged.disconnect(self._onActiveMachineChanged) + self._initialize(update_should_show_flag = False) + + def initialize(self) -> None: + self._application.getMachineManager().globalContainerChanged.connect(self._onActiveMachineChanged) + self._initialize() + + def _initialize(self, update_should_show_flag: bool = True) -> None: + show_whatsnew_only = False + if update_should_show_flag: + has_active_machine = self._application.getMachineManager().activeMachine is not None + has_app_just_upgraded = self._application.hasJustUpdatedFromOldVersion() + + # Only show the what's new dialog if there's no machine and we have just upgraded + show_complete_flow = not has_active_machine + show_whatsnew_only = has_active_machine and has_app_just_upgraded + + # FIXME: This is a hack. Because of the circular dependency between MachineManager, ExtruderManager, and + # possibly some others, setting the initial active machine is not done when the MachineManager gets initialized. + # So at this point, we don't know if there will be an active machine or not. It could be that the active machine + # files are corrupted so we cannot rely on Preferences either. This makes sure that once the active machine + # gets changed, this model updates the flags, so it can decide whether to show the welcome flow or not. + should_show_welcome_flow = show_complete_flow or show_whatsnew_only + if should_show_welcome_flow != self._should_show_welcome_flow: + self._should_show_welcome_flow = should_show_welcome_flow + self.shouldShowWelcomeFlowChanged.emit() + + # All pages + all_pages_list = [{"id": "welcome", + "page_url": self._getBuiltinWelcomePagePath("WelcomeContent.qml"), + }, + {"id": "user_agreement", + "page_url": self._getBuiltinWelcomePagePath("UserAgreementContent.qml"), + }, + {"id": "whats_new", + "page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"), + }, + {"id": "data_collections", + "page_url": self._getBuiltinWelcomePagePath("DataCollectionsContent.qml"), + }, + {"id": "add_network_or_local_printer", + "page_url": self._getBuiltinWelcomePagePath("AddNetworkOrLocalPrinterContent.qml"), + "next_page_id": "machine_actions", + }, + {"id": "add_printer_by_ip", + "page_url": self._getBuiltinWelcomePagePath("AddPrinterByIpContent.qml"), + "next_page_id": "machine_actions", + }, + {"id": "machine_actions", + "page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"), + "next_page_id": "cloud", + "should_show_function": self.shouldShowMachineActions, + }, + {"id": "cloud", + "page_url": self._getBuiltinWelcomePagePath("CloudContent.qml"), + }, + ] + + pages_to_show = all_pages_list + if show_whatsnew_only: + pages_to_show = list(filter(lambda x: x["id"] == "whats_new", all_pages_list)) + + self._pages = pages_to_show + self.setItems(self._pages) + + # For convenience, inject the default "next" button text to each item if it's not present. + def setItems(self, items: List[Dict[str, Any]]) -> None: + for item in items: + if "next_page_button_text" not in item: + item["next_page_button_text"] = self._default_next_button_text + + super().setItems(items) + + # Indicates if the machine action panel should be shown by checking if there's any first start machine actions + # available. + def shouldShowMachineActions(self) -> bool: + global_stack = self._application.getMachineManager().activeMachine + if global_stack is None: + return False + + definition_id = global_stack.definition.getId() + first_start_actions = self._application.getMachineActionManager().getFirstStartActions(definition_id) + return len([action for action in first_start_actions if action.needsUserInteraction()]) > 0 + + def addPage(self) -> None: + pass + + +__all__ = ["WelcomePagesModel"] diff --git a/cura/UI/WhatsNewPagesModel.py b/cura/UI/WhatsNewPagesModel.py new file mode 100644 index 0000000000..5b968ae574 --- /dev/null +++ b/cura/UI/WhatsNewPagesModel.py @@ -0,0 +1,22 @@ +# Copyright (c) 2019 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. + +from .WelcomePagesModel import WelcomePagesModel + + +# +# This Qt ListModel is more or less the same the WelcomePagesModel, except that this model is only for showing the +# "what's new" page. This is also used in the "Help" menu to show the changes log. +# +class WhatsNewPagesModel(WelcomePagesModel): + + def initialize(self) -> None: + self._pages = [] + self._pages.append({"id": "whats_new", + "page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"), + "next_page_button_text": self._catalog.i18nc("@action:button", "Close"), + }) + self.setItems(self._pages) + + +__all__ = ["WhatsNewPagesModel"] diff --git a/cura/UI/__init__.py b/cura/UI/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/cura/UI/__init__.py |