Merge branch 'main'

This commit is contained in:
Remco Burema 2022-11-04 08:47:50 +01:00
commit b1138e12d9
5455 changed files with 203722 additions and 208531 deletions

View file

@ -1,11 +1,11 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# Copyright (c) 2021-2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
import zipfile
from typing import List, Optional, Union, TYPE_CHECKING, cast
import Savitar
import pySavitar as Savitar
import numpy
from UM.Logger import Logger
@ -304,4 +304,4 @@ class ThreeMFReader(MeshReader):
unit = "millimeter"
scale = conversion_to_mm[unit]
return Vector(scale, scale, scale)
return Vector(scale, scale, scale)

View file

@ -23,6 +23,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
from UM.Job import Job
from UM.Preferences import Preferences
from cura.CuraPackageManager import CuraPackageManager
from cura.Machines.ContainerTree import ContainerTree
from cura.Settings.CuraStackBuilder import CuraStackBuilder
@ -34,7 +35,7 @@ from cura.Settings.CuraContainerStack import _ContainerIndexes
from cura.CuraApplication import CuraApplication
from cura.Utils.Threading import call_on_qt_thread
from PyQt5.QtCore import QCoreApplication
from PyQt6.QtCore import QCoreApplication
from .WorkspaceDialog import WorkspaceDialog
@ -52,6 +53,7 @@ _ignored_machine_network_metadata = {
"connection_type",
"capabilities",
"octoprint_api_key",
"is_abstract_machine"
} # type: Set[str]
@ -579,6 +581,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
is_printer_group = True
machine_name = group_name
# Getting missing required package ids
package_metadata = self._parse_packages_metadata(archive)
missing_package_metadata = self._filter_missing_package_metadata(package_metadata)
# Show the dialog, informing the user what is about to happen.
self._dialog.setMachineConflict(machine_conflict)
self._dialog.setIsPrinterGroup(is_printer_group)
@ -599,6 +605,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._dialog.setExtruders(extruders)
self._dialog.setVariantType(variant_type_name)
self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity)
self._dialog.setMissingPackagesMetadata(missing_package_metadata)
self._dialog.show()
# Block until the dialog is closed.
@ -658,10 +665,22 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
# Create a shadow copy of the preferences (we don't want all of the preferences, but we do want to re-use its
# Create a shadow copy of the preferences (We don't want all of the preferences, but we do want to re-use its
# parsing code.
temp_preferences = Preferences()
serialized = archive.open("Cura/preferences.cfg").read().decode("utf-8")
try:
serialized = archive.open("Cura/preferences.cfg").read().decode("utf-8")
except KeyError:
# If there is no preferences file, it's not a workspace, so notify user of failure.
Logger.log("w", "File %s is not a valid workspace.", file_name)
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
"Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.",
file_name, str(e)),
title=i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
message_type=Message.MessageType.ERROR)
message.show()
self.setWorkspaceName("")
return [], {}
temp_preferences.deserialize(serialized)
# Copy a number of settings from the temp preferences to the global
@ -1243,3 +1262,29 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"})
for entry in metadata:
return entry.text
@staticmethod
def _parse_packages_metadata(archive: zipfile.ZipFile) -> List[Dict[str, str]]:
try:
package_metadata = json.loads(archive.open("Cura/packages.json").read().decode("utf-8"))
return package_metadata["packages"]
except KeyError:
Logger.warning("No package metadata was found in .3mf file.")
except Exception:
Logger.error("Failed to load packages metadata from .3mf file.")
return []
@staticmethod
def _filter_missing_package_metadata(package_metadata: List[Dict[str, str]]) -> List[Dict[str, str]]:
"""Filters out installed packages from package_metadata"""
missing_packages = []
package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
for package in package_metadata:
package_id = package["id"]
if not package_manager.isPackageInstalled(package_id):
missing_packages.append(package)
return missing_packages

View file

@ -3,7 +3,7 @@
from typing import Dict, List
from PyQt5.QtCore import Qt
from PyQt6.QtCore import Qt
from UM.Qt.ListModel import ListModel
from cura.Settings.GlobalStack import GlobalStack
@ -25,10 +25,10 @@ class UpdatableMachinesModel(ListModel):
def __init__(self, parent = None) -> None:
super().__init__(parent)
self.addRoleName(Qt.UserRole + 1, "id")
self.addRoleName(Qt.UserRole + 2, "name")
self.addRoleName(Qt.UserRole + 3, "displayName")
self.addRoleName(Qt.UserRole + 4, "type") # Either "default_option" or "machine"
self.addRoleName(Qt.ItemDataRole.UserRole + 1, "id")
self.addRoleName(Qt.ItemDataRole.UserRole + 2, "name")
self.addRoleName(Qt.ItemDataRole.UserRole + 3, "displayName")
self.addRoleName(Qt.ItemDataRole.UserRole + 4, "type") # Either "default_option" or "machine"
def update(self, machines: List[GlobalStack]) -> None:
items = [create_new_list_item] # type: List[Dict[str, str]]

View file

@ -1,14 +1,19 @@
# Copyright (c) 2020 Ultimaker B.V.
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication, QUrl
from PyQt6.QtGui import QDesktopServices
from typing import List, Optional, Dict, cast
from PyQt5.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
from UM.FlameProfiler import pyqtSlot
from UM.PluginRegistry import PluginRegistry
from UM.Application import Application
from UM.i18n import i18nCatalog
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.GlobalStack import GlobalStack
from UM.Application import Application
from UM.FlameProfiler import pyqtSlot
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Message import Message
from UM.PluginRegistry import PluginRegistry
from UM.Settings.ContainerRegistry import ContainerRegistry
from .UpdatableMachinesModel import UpdatableMachinesModel
import os
@ -23,7 +28,7 @@ i18n_catalog = i18nCatalog("cura")
class WorkspaceDialog(QObject):
showDialogSignal = pyqtSignal()
def __init__(self, parent = None):
def __init__(self, parent = None) -> None:
super().__init__(parent)
self._component = None
self._context = None
@ -59,6 +64,9 @@ class WorkspaceDialog(QObject):
self._objects_on_plate = False
self._is_printer_group = False
self._updatable_machines_model = UpdatableMachinesModel(self)
self._missing_package_metadata: List[Dict[str, str]] = []
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
self._install_missing_package_dialog: Optional[QObject] = None
machineConflictChanged = pyqtSignal()
qualityChangesConflictChanged = pyqtSignal()
@ -79,6 +87,7 @@ class WorkspaceDialog(QObject):
variantTypeChanged = pyqtSignal()
extrudersChanged = pyqtSignal()
isPrinterGroupChanged = pyqtSignal()
missingPackagesChanged = pyqtSignal()
@pyqtProperty(bool, notify = isPrinterGroupChanged)
def isPrinterGroup(self) -> bool:
@ -274,6 +283,21 @@ class WorkspaceDialog(QObject):
self._has_quality_changes_conflict = quality_changes_conflict
self.qualityChangesConflictChanged.emit()
def setMissingPackagesMetadata(self, missing_package_metadata: List[Dict[str, str]]) -> None:
self._missing_package_metadata = missing_package_metadata
self.missingPackagesChanged.emit()
@pyqtProperty("QVariantList", notify=missingPackagesChanged)
def missingPackages(self) -> List[Dict[str, str]]:
return self._missing_package_metadata
@pyqtSlot()
def installMissingPackages(self) -> None:
marketplace_plugin = PluginRegistry.getInstance().getPluginObject("Marketplace")
if not marketplace_plugin:
Logger.warning("Could not show dialog to install missing plug-ins. Is Marketplace plug-in not available?")
marketplace_plugin.showInstallMissingPackageDialog(self._missing_package_metadata, self.showMissingMaterialsWarning) # type: ignore
def getResult(self) -> Dict[str, Optional[str]]:
if "machine" in self._result and self.updatableMachinesModel.count <= 1:
self._result["machine"] = None
@ -360,6 +384,41 @@ class WorkspaceDialog(QObject):
time.sleep(1 / 50)
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
@pyqtSlot()
def showMissingMaterialsWarning(self) -> None:
result_message = Message(
i18n_catalog.i18nc("@info:status", "The material used in this project relies on some material definitions not available in Cura, this might produce undesirable print results. We highly recommend installing the full material package from the Marketplace."),
lifetime=0,
title=i18n_catalog.i18nc("@info:title", "Material profiles not installed"),
message_type=Message.MessageType.WARNING
)
result_message.addAction(
"learn_more",
name=i18n_catalog.i18nc("@action:button", "Learn more"),
icon="",
description="Learn more about project materials.",
button_align=Message.ActionButtonAlignment.ALIGN_LEFT,
button_style=Message.ActionButtonStyle.LINK
)
result_message.addAction(
"install_materials",
name=i18n_catalog.i18nc("@action:button", "Install Materials"),
icon="",
description="Install missing materials from project file.",
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT,
button_style=Message.ActionButtonStyle.DEFAULT
)
result_message.actionTriggered.connect(self._onMessageActionTriggered)
result_message.show()
def _onMessageActionTriggered(self, message: Message, sync_message_action: str) -> None:
if sync_message_action == "install_materials":
self.installMissingPackages()
message.hide()
elif sync_message_action == "learn_more":
QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360011968360-Using-the-Ultimaker-Marketplace"))
def __show(self) -> None:
if self._view is None:
self._createViewFromQML()

View file

@ -17,7 +17,8 @@ UM.Dialog
minimumWidth: UM.Theme.getSize("popup_dialog").width
minimumHeight: UM.Theme.getSize("popup_dialog").height
width: minimumWidth
backgroundColor: UM.Theme.getColor("main_background")
margin: UM.Theme.getSize("default_margin").width
property int comboboxHeight: UM.Theme.getSize("default_margin").height
onClosing: manager.notifyClosed()
@ -31,337 +32,220 @@ UM.Dialog
}
}
Item
Flickable
{
id: dialogSummaryItem
clip: true
width: parent.width
height: childrenRect.height
anchors.margins: 10 * screenScaleFactor
height: parent.height
contentHeight: dialogSummaryItem.height
ScrollBar.vertical: UM.ScrollBar { id: verticalScrollBar }
UM.I18nCatalog
Item
{
id: catalog
name: "cura"
}
ListModel
{
id: resolveStrategiesModel
// Instead of directly adding the list elements, we add them afterwards.
// This is because it's impossible to use setting function results to be bound to listElement properties directly.
// See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties
Component.onCompleted:
{
append({"key": "override", "label": catalog.i18nc("@action:ComboBox Update/override existing profile", "Update existing")});
append({"key": "new", "label": catalog.i18nc("@action:ComboBox Save settings in a new profile", "Create new")});
}
}
Column
{
width: parent.width
id: dialogSummaryItem
width: verticalScrollBar.visible ? parent.width - verticalScrollBar.width - UM.Theme.getSize("default_margin").width : parent.width
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").height
anchors.margins: 10 * screenScaleFactor
UM.I18nCatalog
{
id: catalog
name: "cura"
}
ListModel
{
id: resolveStrategiesModel
// Instead of directly adding the list elements, we add them afterwards.
// This is because it's impossible to use setting function results to be bound to listElement properties directly.
// See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties
Component.onCompleted:
{
append({"key": "override", "label": catalog.i18nc("@action:ComboBox Update/override existing profile", "Update existing")});
append({"key": "new", "label": catalog.i18nc("@action:ComboBox Save settings in a new profile", "Create new")});
}
}
Column
{
width: parent.width
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").height
UM.Label
Column
{
id: titleLabel
text: catalog.i18nc("@action:title", "Summary - Cura Project")
font: UM.Theme.getFont("large")
}
Rectangle
{
id: separator
color: UM.Theme.getColor("text")
width: parent.width
height: UM.Theme.getSize("default_lining").height
}
}
height: childrenRect.height
Item
{
width: parent.width
height: childrenRect.height
UM.TooltipArea
{
id: machineResolveStrategyTooltip
anchors.top: parent.top
anchors.right: parent.right
width: (parent.width / 3) | 0
height: visible ? comboboxHeight : 0
visible: base.visible && machineResolveComboBox.model.count > 1
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
Cura.ComboBox
UM.Label
{
id: machineResolveComboBox
model: manager.updatableMachinesModel
visible: machineResolveStrategyTooltip.visible
textRole: "displayName"
id: titleLabel
text: catalog.i18nc("@action:title", "Summary - Cura Project")
font: UM.Theme.getFont("large")
}
Rectangle
{
id: separator
color: UM.Theme.getColor("text")
width: parent.width
height: UM.Theme.getSize("button").height
onCurrentIndexChanged:
{
if (model.getItem(currentIndex).id == "new"
&& model.getItem(currentIndex).type == "default_option")
{
manager.setResolveStrategy("machine", "new")
}
else
{
manager.setResolveStrategy("machine", "override")
manager.setMachineToOverride(model.getItem(currentIndex).id)
}
}
height: UM.Theme.getSize("default_lining").height
}
}
onVisibleChanged:
{
if (!visible) {return}
Item
{
width: parent.width
height: childrenRect.height
currentIndex = 0
// If the project printer exists in Cura, set it as the default dropdown menu option.
// No need to check object 0, which is the "Create new" option
for (var i = 1; i < model.count; i++)
UM.TooltipArea
{
id: machineResolveStrategyTooltip
anchors.top: parent.top
anchors.right: parent.right
width: (parent.width / 3) | 0
height: visible ? comboboxHeight : 0
visible: base.visible && machineResolveComboBox.model.count > 1
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
Cura.ComboBox
{
id: machineResolveComboBox
model: manager.updatableMachinesModel
visible: machineResolveStrategyTooltip.visible
textRole: "displayName"
width: parent.width
height: UM.Theme.getSize("button").height
onCurrentIndexChanged:
{
if (model.getItem(i).name == manager.machineName)
if (model.getItem(currentIndex).id == "new"
&& model.getItem(currentIndex).type == "default_option")
{
currentIndex = i
break
manager.setResolveStrategy("machine", "new")
}
else
{
manager.setResolveStrategy("machine", "override")
manager.setMachineToOverride(model.getItem(currentIndex).id)
}
}
// The project printer does not exist in Cura. If there is at least one printer of the same
// type, select the first one, else set the index to "Create new"
if (currentIndex == 0 && model.count > 1)
onVisibleChanged:
{
currentIndex = 1
if (!visible) {return}
currentIndex = 0
// If the project printer exists in Cura, set it as the default dropdown menu option.
// No need to check object 0, which is the "Create new" option
for (var i = 1; i < model.count; i++)
{
if (model.getItem(i).name == manager.machineName)
{
currentIndex = i
break
}
}
// The project printer does not exist in Cura. If there is at least one printer of the same
// type, select the first one, else set the index to "Create new"
if (currentIndex == 0 && model.count > 1)
{
currentIndex = 1
}
}
}
}
}
Column
{
width: parent.width
height: childrenRect.height
UM.Label
{
id: printer_settings_label
text: catalog.i18nc("@action:label", "Printer settings")
font: UM.Theme.getFont("default_bold")
}
Row
Column
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Type")
width: (parent.width / 3) | 0
}
UM.Label
{
text: manager.machineType
width: (parent.width / 3) | 0
}
}
Row
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name")
width: (parent.width / 3) | 0
}
UM.Label
{
text: manager.machineName
width: (parent.width / 3) | 0
wrapMode: Text.WordWrap
}
}
}
}
Item
{
width: parent.width
height: childrenRect.height
UM.TooltipArea
{
anchors.right: parent.right
anchors.top: parent.top
width: (parent.width / 3) | 0
height: visible ? comboboxHeight : 0
visible: manager.qualityChangesConflict
text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?")
Cura.ComboBox
{
model: resolveStrategiesModel
textRole: "label"
id: qualityChangesResolveComboBox
width: parent.width
height: UM.Theme.getSize("button").height
onActivated:
{
manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key)
}
}
}
Column
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Profile settings")
font: UM.Theme.getFont("default_bold")
}
Row
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Name")
width: (parent.width / 3) | 0
}
UM.Label
{
text: manager.qualityName
width: (parent.width / 3) | 0
wrapMode: Text.WordWrap
}
}
Row
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Intent")
width: (parent.width / 3) | 0
}
UM.Label
{
text: manager.intentName
width: (parent.width / 3) | 0
wrapMode: Text.WordWrap
}
}
Row
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Not in profile")
visible: manager.numUserSettings != 0
width: (parent.width / 3) | 0
}
UM.Label
{
text: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings)
visible: manager.numUserSettings != 0
width: (parent.width / 3) | 0
}
}
Row
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Derivative from")
visible: manager.numSettingsOverridenByQualityChanges != 0
width: (parent.width / 3) | 0
}
UM.Label
{
text: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
width: (parent.width / 3) | 0
visible: manager.numSettingsOverridenByQualityChanges != 0
wrapMode: Text.WordWrap
}
}
}
}
Item
{
width: parent.width
height: childrenRect.height
UM.TooltipArea
{
id: materialResolveTooltip
anchors.right: parent.right
anchors.top: parent.top
width: (parent.width / 3) | 0
height: visible ? comboboxHeight : 0
visible: manager.materialConflict
text: catalog.i18nc("@info:tooltip", "How should the conflict in the material be resolved?")
Cura.ComboBox
{
model: resolveStrategiesModel
textRole: "label"
id: materialResolveComboBox
width: parent.width
height: UM.Theme.getSize("button").height
onActivated:
{
manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key)
}
}
}
Column
{
width: parent.width
height: childrenRect.height
Row
{
height: childrenRect.height
width: parent.width
spacing: UM.Theme.getSize("narrow_margin").width
UM.Label
{
text: catalog.i18nc("@action:label", "Material settings")
id: printer_settings_label
text: catalog.i18nc("@action:label", "Printer settings")
font: UM.Theme.getFont("default_bold")
width: (parent.width / 3) | 0
}
}
Repeater
{
model: manager.materialLabels
delegate: Row
Row
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Type")
width: (parent.width / 3) | 0
}
UM.Label
{
text: manager.machineType
width: (parent.width / 3) | 0
}
}
Row
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name")
width: (parent.width / 3) | 0
}
UM.Label
{
text: manager.machineName
width: (parent.width / 3) | 0
wrapMode: Text.WordWrap
}
}
}
}
Item
{
width: parent.width
height: childrenRect.height
UM.TooltipArea
{
anchors.right: parent.right
anchors.top: parent.top
width: (parent.width / 3) | 0
height: visible ? comboboxHeight : 0
visible: manager.qualityChangesConflict
text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?")
Cura.ComboBox
{
model: resolveStrategiesModel
textRole: "label"
id: qualityChangesResolveComboBox
width: parent.width
height: UM.Theme.getSize("button").height
onActivated:
{
manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key)
}
}
}
Column
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Profile settings")
font: UM.Theme.getFont("default_bold")
}
Row
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Name")
@ -369,76 +253,249 @@ UM.Dialog
}
UM.Label
{
text: modelData
text: manager.qualityName
width: (parent.width / 3) | 0
wrapMode: Text.WordWrap
}
}
Row
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Intent")
width: (parent.width / 3) | 0
}
UM.Label
{
text: manager.intentName
width: (parent.width / 3) | 0
wrapMode: Text.WordWrap
}
}
Row
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Not in profile")
visible: manager.numUserSettings != 0
width: (parent.width / 3) | 0
}
UM.Label
{
text: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings)
visible: manager.numUserSettings != 0
width: (parent.width / 3) | 0
}
}
Row
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Derivative from")
visible: manager.numSettingsOverridenByQualityChanges != 0
width: (parent.width / 3) | 0
}
UM.Label
{
text: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
width: (parent.width / 3) | 0
visible: manager.numSettingsOverridenByQualityChanges != 0
wrapMode: Text.WordWrap
}
}
}
}
}
Column
{
width: parent.width
height: childrenRect.height
UM.Label
Item
{
text: catalog.i18nc("@action:label", "Setting visibility")
font: UM.Theme.getFont("default_bold")
width: parent.width
height: childrenRect.height
UM.TooltipArea
{
id: materialResolveTooltip
anchors.right: parent.right
anchors.top: parent.top
width: (parent.width / 3) | 0
height: visible ? comboboxHeight : 0
visible: manager.materialConflict
text: catalog.i18nc("@info:tooltip", "How should the conflict in the material be resolved?")
Cura.ComboBox
{
model: resolveStrategiesModel
textRole: "label"
id: materialResolveComboBox
width: parent.width
height: UM.Theme.getSize("button").height
onActivated:
{
manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key)
}
}
}
Column
{
width: parent.width
height: childrenRect.height
Row
{
height: childrenRect.height
width: parent.width
spacing: UM.Theme.getSize("narrow_margin").width
UM.Label
{
text: catalog.i18nc("@action:label", "Material settings")
font: UM.Theme.getFont("default_bold")
width: (parent.width / 3) | 0
}
}
Repeater
{
model: manager.materialLabels
delegate: Row
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Name")
width: (parent.width / 3) | 0
}
UM.Label
{
text: modelData
width: (parent.width / 3) | 0
wrapMode: Text.WordWrap
}
}
}
}
}
Column
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Setting visibility")
font: UM.Theme.getFont("default_bold")
}
Row
{
width: parent.width
height: childrenRect.height
UM.Label
{
text: catalog.i18nc("@action:label", "Mode")
width: (parent.width / 3) | 0
}
UM.Label
{
text: manager.activeMode
width: (parent.width / 3) | 0
}
}
Row
{
width: parent.width
height: childrenRect.height
visible: manager.hasVisibleSettingsField
UM.Label
{
text: catalog.i18nc("@action:label", "Visible settings:")
width: (parent.width / 3) | 0
}
UM.Label
{
text: catalog.i18nc("@action:label", "%1 out of %2" ).arg(manager.numVisibleSettings).arg(manager.totalNumberOfSettings)
width: (parent.width / 3) | 0
}
}
}
Row
{
width: parent.width
height: childrenRect.height
UM.Label
visible: manager.hasObjectsOnPlate
UM.ColorImage
{
text: catalog.i18nc("@action:label", "Mode")
width: (parent.width / 3) | 0
width: warningLabel.height
height: width
source: UM.Theme.getIcon("Information")
color: UM.Theme.getColor("text")
}
UM.Label
{
text: manager.activeMode
width: (parent.width / 3) | 0
}
}
Row
{
width: parent.width
height: childrenRect.height
visible: manager.hasVisibleSettingsField
UM.Label
{
text: catalog.i18nc("@action:label", "Visible settings:")
width: (parent.width / 3) | 0
}
UM.Label
{
text: catalog.i18nc("@action:label", "%1 out of %2" ).arg(manager.numVisibleSettings).arg(manager.totalNumberOfSettings)
width: (parent.width / 3) | 0
id: warningLabel
text: catalog.i18nc("@action:warning", "Loading a project will clear all models on the build plate.")
}
}
}
}
}
Row
property bool warning: manager.missingPackages.length > 0
footerComponent: Rectangle
{
color: warning ? UM.Theme.getColor("warning") : "transparent"
anchors.bottom: parent.bottom
width: parent.width
height: childrenRect.height + 2 * base.margin
Column
{
height: childrenRect.height
spacing: base.margin
anchors.margins: base.margin
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
RowLayout
{
id: warningRow
height: childrenRect.height
visible: warning
spacing: base.margin
UM.ColorImage
{
width: UM.Theme.getSize("extruder_icon").width
height: UM.Theme.getSize("extruder_icon").height
source: UM.Theme.getIcon("Warning")
}
UM.Label
{
id: warningText
text: catalog.i18nc("@label", "The material used in this project is currently not installed in Cura.<br/>Install the material profile and reopen the project.")
}
}
Loader
{
width: parent.width
height: childrenRect.height
visible: manager.hasObjectsOnPlate
UM.RecolorImage
{
width: warningLabel.height
height: width
source: UM.Theme.getIcon("Information")
color: UM.Theme.getColor("text")
}
UM.Label
{
id: warningLabel
text: catalog.i18nc("@action:warning", "Loading a project will clear all models on the build plate.")
wrapMode: Text.Wrap
}
sourceComponent: buttonRow
}
}
}
@ -448,13 +505,30 @@ UM.Dialog
rightButtons: [
Cura.TertiaryButton
{
visible: !warning
text: catalog.i18nc("@action:button", "Cancel")
onClicked: reject()
},
Cura.PrimaryButton
{
visible: !warning
text: catalog.i18nc("@action:button", "Open")
onClicked: accept()
},
Cura.TertiaryButton
{
visible: warning
text: catalog.i18nc("@action:button", "Open project anyway")
onClicked: {
manager.showMissingMaterialsWarning();
accept();
}
},
Cura.PrimaryButton
{
visible: warning
text: catalog.i18nc("@action:button", "Install missing material")
onClicked: manager.installMissingPackages()
}
]

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for reading 3MF files.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -156,6 +156,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
"connection_type",
"capabilities",
"octoprint_api_key",
"is_online",
}
serialized_data = container.serialize(ignored_metadata_keys = ignore_keys)

View file

@ -1,21 +1,28 @@
# Copyright (c) 2015 Ultimaker B.V.
# Uranium is released under the terms of the LGPLv3 or higher.
from typing import Optional
# Copyright (c) 2015-2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
from typing import Optional, cast, List, Dict
from UM.Mesh.MeshWriter import MeshWriter
from UM.Math.Vector import Vector
from UM.Logger import Logger
from UM.Math.Matrix import Matrix
from UM.Application import Application
from UM.Message import Message
from UM.Resources import Resources
from UM.Scene.SceneNode import SceneNode
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
from cura.CuraApplication import CuraApplication
from cura.CuraPackageManager import CuraPackageManager
from cura.Utils.Threading import call_on_qt_thread
from cura.Snapshot import Snapshot
from PyQt5.QtCore import QBuffer
from PyQt6.QtCore import QBuffer
import Savitar
import pySavitar as Savitar
import numpy
import datetime
@ -34,6 +41,9 @@ import UM.Application
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
THUMBNAIL_PATH = "Metadata/thumbnail.png"
MODEL_PATH = "3D/3dmodel.model"
PACKAGE_METADATA_PATH = "Cura/packages.json"
class ThreeMFWriter(MeshWriter):
def __init__(self):
@ -46,7 +56,7 @@ class ThreeMFWriter(MeshWriter):
}
self._unit_matrix_string = self._convertMatrixToString(Matrix())
self._archive = None # type: Optional[zipfile.ZipFile]
self._archive: Optional[zipfile.ZipFile] = None
self._store_archive = False
def _convertMatrixToString(self, matrix):
@ -132,11 +142,11 @@ class ThreeMFWriter(MeshWriter):
def getArchive(self):
return self._archive
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode) -> bool:
self._archive = None # Reset archive
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
try:
model_file = zipfile.ZipInfo("3D/3dmodel.model")
model_file = zipfile.ZipInfo(MODEL_PATH)
# Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
model_file.compress_type = zipfile.ZIP_DEFLATED
@ -151,37 +161,41 @@ class ThreeMFWriter(MeshWriter):
relations_file = zipfile.ZipInfo("_rels/.rels")
relations_file.compress_type = zipfile.ZIP_DEFLATED
relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"])
model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/" + MODEL_PATH, Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
# Attempt to add a thumbnail
snapshot = self._createSnapshot()
if snapshot:
thumbnail_buffer = QBuffer()
thumbnail_buffer.open(QBuffer.ReadWrite)
thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
snapshot.save(thumbnail_buffer, "PNG")
thumbnail_file = zipfile.ZipInfo("Metadata/thumbnail.png")
thumbnail_file = zipfile.ZipInfo(THUMBNAIL_PATH)
# Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get
archive.writestr(thumbnail_file, thumbnail_buffer.data())
# Add PNG to content types file
thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png")
# Add thumbnail relation to _rels/.rels file
thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/Metadata/thumbnail.png", Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/" + THUMBNAIL_PATH, Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
# Write material metadata
material_metadata = self._getMaterialPackageMetadata()
self._storeMetadataJson({"packages": material_metadata}, archive, PACKAGE_METADATA_PATH)
savitar_scene = Savitar.Scene()
metadata_to_store = CuraApplication.getInstance().getController().getScene().getMetaData()
scene_metadata = CuraApplication.getInstance().getController().getScene().getMetaData()
for key, value in metadata_to_store.items():
for key, value in scene_metadata.items():
savitar_scene.setMetaDataEntry(key, value)
current_time_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if "Application" not in metadata_to_store:
if "Application" not in scene_metadata:
# This might sound a bit strange, but this field should store the original application that created
# the 3mf. So if it was already set, leave it to whatever it was.
savitar_scene.setMetaDataEntry("Application", CuraApplication.getInstance().getApplicationDisplayName())
if "CreationDate" not in metadata_to_store:
if "CreationDate" not in scene_metadata:
savitar_scene.setMetaDataEntry("CreationDate", current_time_string)
savitar_scene.setMetaDataEntry("ModificationDate", current_time_string)
@ -233,6 +247,55 @@ class ThreeMFWriter(MeshWriter):
return True
@staticmethod
def _storeMetadataJson(metadata: Dict[str, List[Dict[str, str]]], archive: zipfile.ZipFile, path: str) -> None:
"""Stores metadata inside archive path as json file"""
metadata_file = zipfile.ZipInfo(path)
# We have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
metadata_file.compress_type = zipfile.ZIP_DEFLATED
archive.writestr(metadata_file, json.dumps(metadata, separators=(", ", ": "), indent=4, skipkeys=True, ensure_ascii=False))
@staticmethod
def _getMaterialPackageMetadata() -> List[Dict[str, str]]:
"""Get metadata for installed materials in active extruder stack, this does not include bundled materials.
:return: List of material metadata dictionaries.
"""
metadata = {}
package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
for extruder in CuraApplication.getInstance().getExtruderManager().getActiveExtruderStacks():
if not extruder.isEnabled:
# Don't export materials not in use
continue
if isinstance(extruder.material, type(ContainerRegistry.getInstance().getEmptyInstanceContainer())):
# This is an empty material container, no material to export
continue
if package_manager.isMaterialBundled(extruder.material.getFileName(), extruder.material.getMetaDataEntry("GUID")):
# Don't export bundled materials
continue
package_id = package_manager.getMaterialFilePackageId(extruder.material.getFileName(), extruder.material.getMetaDataEntry("GUID"))
package_data = package_manager.getInstalledPackageInfo(package_id)
# We failed to find the package for this material
if not package_data:
Logger.info(f"Could not find package for material in extruder {extruder.id}, skipping.")
continue
material_metadata = {"id": package_id,
"display_name": package_data.get("display_name") if package_data.get("display_name") else "",
"package_version": package_data.get("package_version") if package_data.get("package_version") else "",
"sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get("sdk_version_semver") else ""}
metadata[package_id] = material_metadata
# Storing in a dict and fetching values to avoid duplicates
return list(metadata.values())
@call_on_qt_thread # must be called from the main thread because of OpenGL
def _createSnapshot(self):
Logger.log("d", "Creating thumbnail image...")

View file

@ -5,21 +5,23 @@ import sys
from UM.Logger import Logger
try:
from . import ThreeMFWriter
threemf_writer_was_imported = True
except ImportError:
Logger.log("w", "Could not import ThreeMFWriter; libSavitar may be missing")
from . import ThreeMFWorkspaceWriter
threemf_writer_was_imported = False
from . import ThreeMFWorkspaceWriter
from UM.i18n import i18nCatalog
from UM.Platform import Platform
i18n_catalog = i18nCatalog("cura")
def getMetaData():
workspace_extension = "3mf"
metaData = {}
if "3MFWriter.ThreeMFWriter" in sys.modules:
if threemf_writer_was_imported:
metaData["mesh_writer"] = {
"output": [{
"extension": "3mf",
@ -39,6 +41,7 @@ def getMetaData():
return metaData
def register(app):
if "3MFWriter.ThreeMFWriter" in sys.modules:
return {"mesh_writer": ThreeMFWriter.ThreeMFWriter(),

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for writing 3MF files.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -3,5 +3,5 @@
"author": "fieldOfView",
"version": "1.0.0",
"description": "Provides support for reading AMF files.",
"api": 7
"api": 8
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"description": "Backup and restore your configuration.",
"version": "1.2.0",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -5,7 +5,7 @@ import threading
from datetime import datetime
from typing import Any, Dict, Optional
from PyQt5.QtNetwork import QNetworkReply
from PyQt6.QtNetwork import QNetworkReply
from UM.Job import Job
from UM.Logger import Logger

View file

@ -3,7 +3,7 @@
from typing import Any, Optional, List, Dict, Callable
from PyQt5.QtNetwork import QNetworkReply
from PyQt6.QtNetwork import QNetworkReply
from UM.Logger import Logger
from UM.Signal import Signal, signalemitter

View file

@ -5,7 +5,7 @@ import os
from datetime import datetime
from typing import Any, cast, Dict, List, Optional
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
from PyQt6.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
from UM.Extension import Extension
from UM.Logger import Logger

View file

@ -7,7 +7,7 @@ import threading
from tempfile import NamedTemporaryFile
from typing import Optional, Any, Dict
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest
from UM.Job import Job
from UM.Logger import Logger
@ -53,7 +53,7 @@ class RestoreBackupJob(Job):
def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if not HttpRequestManager.replyIndicatesSuccess(reply, error):
Logger.warning("Requesting backup failed, response code %s while trying to connect to %s",
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute), reply.url())
self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE
self._job_done.set()
return

View file

@ -17,7 +17,7 @@ RowLayout
property alias label: detailName.text
property alias value: detailValue.text
UM.RecolorImage
UM.ColorImage
{
id: icon
width: 18 * screenScaleFactor

View file

@ -5,7 +5,7 @@ import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import UM 1.3 as UM
import UM 1.5 as UM
import Cura 1.1 as Cura
import "../components"
@ -22,28 +22,23 @@ Item
width: parent.width
anchors.fill: parent
Label
UM.Label
{
id: backupTitle
text: catalog.i18nc("@title", "My Backups")
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
Layout.fillWidth: true
renderType: Text.NativeRendering
}
Label
UM.Label
{
text: catalog.i18nc("@empty_state",
"You don't have any backups currently. Use the 'Backup Now' button to create one.")
width: parent.width
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
wrapMode: Label.WordWrap
visible: backupList.model.length == 0
Layout.fillWidth: true
Layout.fillHeight: true
renderType: Text.NativeRendering
}
BackupList
@ -54,16 +49,13 @@ Item
Layout.fillHeight: true
}
Label
UM.Label
{
text: catalog.i18nc("@backup_limit_info",
"During the preview phase, you'll be limited to 5 visible backups. Remove a backup to see older ones.")
width: parent.width
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
wrapMode: Label.WordWrap
visible: backupList.model.length > 4
renderType: Text.NativeRendering
}
BackupListFooter

View file

@ -23,7 +23,7 @@ Column
{
id: profileImage
fillMode: Image.PreserveAspectFit
source: "../images/backup.svg"
source: Qt.resolvedUrl("../images/backup.svg")
anchors.horizontalCenter: parent.horizontalCenter
width: Math.round(parent.width / 4)
}

View file

@ -139,5 +139,9 @@ message GCodePrefix {
bytes data = 2; //Header string to be prepended before the rest of the g-code sent from the engine.
}
message SliceUUID {
string slice_uuid = 1; //The UUID of the slice.
}
message SlicingFinished {
}

View file

@ -1,15 +1,15 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# Copyright (c) 2021-2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import argparse #To run the engine in debug mode if the front-end is in debug mode.
from collections import defaultdict
import os
from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSlot
from PyQt6.QtCore import QObject, QTimer, QUrl, pyqtSlot
import sys
from time import time
from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING
from PyQt5.QtGui import QDesktopServices, QImage
from PyQt6.QtGui import QDesktopServices, QImage
from UM.Backend.Backend import Backend, BackendState
from UM.Scene.SceneNode import SceneNode
@ -31,7 +31,7 @@ from cura.Utils.Threading import call_on_qt_thread
from .ProcessSlicedLayersJob import ProcessSlicedLayersJob
from .StartSliceJob import StartSliceJob, StartJobResult
import Arcus
import pyArcus as Arcus
if TYPE_CHECKING:
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
@ -60,7 +60,7 @@ class CuraEngineBackend(QObject, Backend):
executable_name = "CuraEngine"
if Platform.isWindows():
executable_name += ".exe"
default_engine_location = executable_name
self._default_engine_location = executable_name
search_path = [
os.path.abspath(os.path.dirname(sys.executable)),
@ -74,29 +74,29 @@ class CuraEngineBackend(QObject, Backend):
for path in search_path:
engine_path = os.path.join(path, executable_name)
if os.path.isfile(engine_path):
default_engine_location = engine_path
self._default_engine_location = engine_path
break
if Platform.isLinux() and not default_engine_location:
if Platform.isLinux() and not self._default_engine_location:
if not os.getenv("PATH"):
raise OSError("There is something wrong with your Linux installation.")
for pathdir in cast(str, os.getenv("PATH")).split(os.pathsep):
execpath = os.path.join(pathdir, executable_name)
if os.path.exists(execpath):
default_engine_location = execpath
self._default_engine_location = execpath
break
application = CuraApplication.getInstance() #type: CuraApplication
self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel]
self._machine_error_checker = None #type: Optional[MachineErrorChecker]
if not default_engine_location:
if not self._default_engine_location:
raise EnvironmentError("Could not find CuraEngine")
Logger.log("i", "Found CuraEngine at: %s", default_engine_location)
Logger.log("i", "Found CuraEngine at: %s", self._default_engine_location)
default_engine_location = os.path.abspath(default_engine_location)
application.getPreferences().addPreference("backend/location", default_engine_location)
self._default_engine_location = os.path.abspath(self._default_engine_location)
application.getPreferences().addPreference("backend/location", self._default_engine_location)
# Workaround to disable layer view processing if layer view is not active.
self._layer_view_active = False #type: bool
@ -124,6 +124,7 @@ class CuraEngineBackend(QObject, Backend):
self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
self._message_handlers["cura.proto.SliceUUID"] = self._onSliceUUIDMessage
self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
@ -215,7 +216,12 @@ class CuraEngineBackend(QObject, Backend):
This is useful for debugging and used to actually start the engine.
:return: list of commands and args / parameters.
"""
command = [CuraApplication.getInstance().getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""]
from cura import ApplicationMetadata
if ApplicationMetadata.IsEnterpriseVersion:
command = [self._default_engine_location]
else:
command = [CuraApplication.getInstance().getPreferences().getValue("backend/location")]
command += ["connect", "127.0.0.1:{0}".format(self._port), ""]
parser = argparse.ArgumentParser(prog = "cura", add_help = False)
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
@ -807,6 +813,10 @@ class CuraEngineBackend(QObject, Backend):
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
pass # Throw the message away.
def _onSliceUUIDMessage(self, message: Arcus.PythonMessage) -> None:
application = CuraApplication.getInstance()
application.getPrintInformation().slice_uuid = message.slice_uuid
def _createSocket(self, protocol_file: str = None) -> None:
"""Creates a new socket connection."""

View file

@ -1,5 +1,5 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# Copyright (c) 2021-2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import numpy
from string import Formatter
@ -7,8 +7,8 @@ from enum import IntEnum
import time
from typing import Any, cast, Dict, List, Optional, Set
import re
import Arcus #For typing.
from PyQt5.QtCore import QCoreApplication
import pyArcus as Arcus # For typing.
from PyQt6.QtCore import QCoreApplication
from UM.Job import Job
from UM.Logger import Logger
@ -94,7 +94,7 @@ class StartSliceJob(Job):
super().__init__()
self._scene = CuraApplication.getInstance().getController().getScene() #type: Scene
self._slice_message = slice_message #type: Arcus.PythonMessage
self._slice_message: Arcus.PythonMessage = slice_message
self._is_cancelled = False #type: bool
self._build_plate_number = None #type: Optional[int]
@ -369,6 +369,9 @@ class StartSliceJob(Job):
result["material_name"] = stack.material.getMetaDataEntry("name", "")
result["material_brand"] = stack.material.getMetaDataEntry("brand", "")
result["quality_name"] = stack.quality.getMetaDataEntry("name", "")
result["quality_changes_name"] = stack.qualityChanges.getMetaDataEntry("name")
# Renamed settings.
result["print_bed_temperature"] = result["material_bed_temperature"]
result["print_temperature"] = result["material_print_temperature"]
@ -484,6 +487,10 @@ class StartSliceJob(Job):
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], initial_extruder_nr)
settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], initial_extruder_nr)
# Manually add 'nozzle offsetting', since that is a metadata-entry instead for some reason.
# NOTE: This probably needs to be an actual setting at some point.
settings["nozzle_offsetting_for_disallowed_areas"] = CuraApplication.getInstance().getGlobalContainerStack().getMetaDataEntry("nozzle_offsetting_for_disallowed_areas", True)
# Add all sub-messages for each individual setting.
for key, value in settings.items():
setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings")

View file

@ -2,7 +2,7 @@
"name": "CuraEngine Backend",
"author": "Ultimaker B.V.",
"description": "Provides the link to the CuraEngine slicing backend.",
"api": 7,
"api": 8,
"version": "1.0.1",
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for importing Cura profiles.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for exporting Cura profiles.",
"api": 7,
"api": 8,
"i18n-catalog":"cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"description": "Connects to the Digital Library, allowing Cura to open files from and save files to the Digital Library.",
"version": "1.1.0",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -1,11 +1,11 @@
//Copyright (C) 2022 Ultimaker B.V.
//Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick 2.15
import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import UM 1.2 as UM
import UM 1.5 as UM
import Cura 1.6 as Cura
import DigitalFactory 1.0 as DF
@ -64,12 +64,10 @@ Popup
}
}
Label
UM.Label
{
id: projectNameLabel
text: "Project Name"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
anchors
{
top: createNewLibraryProjectLabel.bottom
@ -90,9 +88,9 @@ Popup
left: parent.left
right: parent.right
}
validator: RegExpValidator
validator: RegularExpressionValidator
{
regExp: /^[^\\\/\*\?\|\[\]]{0,99}$/
regularExpression: /^[^\\\/\*\?\|\[\]]{0,99}$/
}
text: PrintInformation.jobName
@ -107,13 +105,12 @@ Popup
}
}
Label
UM.Label
{
id: errorWhileCreatingProjectLabel
text: manager.projectCreationErrorText
width: parent.width
wrapMode: Text.WordWrap
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("error")
visible: manager.creatingNewProjectStatus == DF.RetrievalStatus.Failed
anchors

View file

@ -30,14 +30,14 @@ Cura.RoundedRectangle
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
UM.RecolorImage
UM.ColorImage
{
id: projectImage
anchors.verticalCenter: parent.verticalCenter
width: UM.Theme.getSize("section").height
height: width
color: UM.Theme.getColor("text_link")
source: "../images/arrow_down.svg"
source: Qt.resolvedUrl("../images/arrow_down.svg")
}
Label
@ -65,7 +65,7 @@ Cura.RoundedRectangle
{
target: projectImage
color: UM.Theme.getColor("text_link")
source: "../images/arrow_down.svg"
source: Qt.resolvedUrl("../images/arrow_down.svg")
}
PropertyChanges
{
@ -88,7 +88,7 @@ Cura.RoundedRectangle
{
target: projectImage
color: UM.Theme.getColor("text_link")
source: "../images/arrow_down.svg"
source: Qt.resolvedUrl("../images/arrow_down.svg")
}
PropertyChanges
{
@ -111,7 +111,7 @@ Cura.RoundedRectangle
{
target: projectImage
color: UM.Theme.getColor("action_button_disabled_text")
source: "../images/update.svg"
source: Qt.resolvedUrl("../images/update.svg")
}
PropertyChanges
{

View file

@ -1,12 +1,11 @@
//Copyright (C) 2022 Ultimaker B.V.
//Cura is released under the terms of the LGPLv3 or higher.
import Qt.labs.qmlmodels 1.0
import QtQuick 2.15
import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import UM 1.2 as UM
import UM 1.6 as UM
import Cura 1.6 as Cura
import DigitalFactory 1.0 as DF
@ -19,6 +18,7 @@ Item
height: parent.height
property var fileModel: manager.digitalFactoryFileModel
property var modelRows: manager.digitalFactoryFileModel.items
signal openFilePressed()
signal selectDifferentProjectPressed()
@ -57,21 +57,19 @@ Item
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
//We can't use Cura's TableView here, since in Cura >= 5.0 this uses QtQuick.TableView, while in Cura < 5.0 this uses QtControls1.TableView.
//So we have to define our own. Once support for 4.13 and earlier is dropped, we can switch to Cura.TableView.
Table
// This is not backwards compatible with Cura < 5.0 due to QT.labs being removed in PyQt6
Cura.TableView
{
id: filesTableView
anchors.fill: parent
anchors.margins: parent.border.width
columnHeaders: ["Name", "Uploaded by", "Uploaded at"]
model: TableModel
model: UM.TableModel
{
TableModelColumn { display: "fileName" }
TableModelColumn { display: "username" }
TableModelColumn { display: "uploadedAt" }
rows: manager.digitalFactoryFileModel.items
id: tableModel
headers: ["fileName", "username", "uploadedAt"]
rows: modelRows
}
onCurrentRowChanged:
@ -85,13 +83,12 @@ Item
}
}
Label
UM.Label
{
id: emptyProjectLabel
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
text: "Select a project to view its files."
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("setting_category_text")
Connections
@ -104,14 +101,13 @@ Item
}
}
Label
UM.Label
{
id: noFilesInProjectLabel
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible)
text: "No supported files in this project."
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("setting_category_text")
}
@ -179,4 +175,10 @@ Item
openFilesButton.clicked.connect(base.openFilePressed)
selectDifferentProjectButton.clicked.connect(base.selectDifferentProjectPressed)
}
onModelRowsChanged:
{
tableModel.clear()
tableModel.rows = modelRows
}
}

View file

@ -2,7 +2,7 @@
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.2 as UM
import UM 1.5 as UM
import Cura 1.6 as Cura
Cura.RoundedRectangle
@ -58,34 +58,31 @@ Cura.RoundedRectangle
width: parent.width - x - UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
Label
UM.Label
{
id: displayNameLabel
width: parent.width
height: Math.round(parent.height / 3)
elide: Text.ElideRight
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default_bold")
}
Label
UM.Label
{
id: usernameLabel
width: parent.width
height: Math.round(parent.height / 3)
elide: Text.ElideRight
color: UM.Theme.getColor("small_button_text")
font: UM.Theme.getFont("default")
}
Label
UM.Label
{
id: lastUpdatedLabel
width: parent.width
height: Math.round(parent.height / 3)
elide: Text.ElideRight
color: UM.Theme.getColor("small_button_text")
font: UM.Theme.getFont("default")
}
}
}

View file

@ -1,12 +1,11 @@
//Copyright (C) 2022 Ultimaker B.V.
//Cura is released under the terms of the LGPLv3 or higher.
import Qt.labs.qmlmodels 1.0
import QtQuick 2.10
import QtQuick 2.15
import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import UM 1.5 as UM
import UM 1.6 as UM
import Cura 1.6 as Cura
import DigitalFactory 1.0 as DF
@ -15,9 +14,14 @@ import DigitalFactory 1.0 as DF
Item
{
id: base
property variant catalog: UM.I18nCatalog { name: "cura" }
width: parent.width
height: parent.height
property var fileModel: manager.digitalFactoryFileModel
property var modelRows: manager.digitalFactoryFileModel.items
signal savePressed()
signal selectDifferentProjectPressed()
@ -43,14 +47,13 @@ Item
cardMouseAreaEnabled: false
}
Label
UM.Label
{
id: fileNameLabel
anchors.top: projectSummaryCard.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
text: "Cura project name"
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
}
@ -61,9 +64,9 @@ Item
anchors.left: parent.left
anchors.top: fileNameLabel.bottom
anchors.topMargin: UM.Theme.getSize("thin_margin").height
validator: RegExpValidator
validator: RegularExpressionValidator
{
regExp: /^[\w\-\. ()]{0,255}$/
regularExpression: /^[\w\-\. ()]{0,255}$/
}
text: PrintInformation.jobName
@ -92,9 +95,8 @@ Item
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
//We can't use Cura's TableView here, since in Cura >= 5.0 this uses QtQuick.TableView, while in Cura < 5.0 this uses QtControls1.TableView.
//So we have to define our own. Once support for 4.13 and earlier is dropped, we can switch to Cura.TableView.
Table
// This is not backwards compatible with Cura < 5.0 due to QT.labs being removed in PyQt6
Cura.TableView
{
id: filesTableView
anchors.fill: parent
@ -102,22 +104,20 @@ Item
allowSelection: false
columnHeaders: ["Name", "Uploaded by", "Uploaded at"]
model: TableModel
model: UM.TableModel
{
TableModelColumn { display: "fileName" }
TableModelColumn { display: "username" }
TableModelColumn { display: "uploadedAt" }
id: tableModel
headers: ["fileName", "username", "uploadedAt"]
rows: manager.digitalFactoryFileModel.items
}
}
Label
UM.Label
{
id: emptyProjectLabel
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
text: "Select a project to view its files."
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("setting_category_text")
Connections
@ -130,14 +130,13 @@ Item
}
}
Label
UM.Label
{
id: noFilesInProjectLabel
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible)
text: "No supported files in this project."
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("setting_category_text")
}
@ -194,53 +193,29 @@ Item
text: "Save"
enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text.length >= 1 && dfFilenameTextfield.state !== 'invalid'
onClicked:
{
let saveAsFormats = [];
if (asProjectCheckbox.checked)
{
saveAsFormats.push("3mf");
}
if (asSlicedCheckbox.checked)
{
saveAsFormats.push("ufp");
}
manager.saveFileToSelectedProject(dfFilenameTextfield.text, saveAsFormats);
}
onClicked: manager.saveFileToSelectedProject(dfFilenameTextfield.text, asProjectComboBox.currentValue)
busy: false
}
Row
Cura.ComboBox
{
id: asProjectComboBox
id: saveAsFormatRow
width: UM.Theme.getSize("combobox_wide").width
height: saveButton.height
anchors.verticalCenter: saveButton.verticalCenter
anchors.right: saveButton.left
anchors.rightMargin: UM.Theme.getSize("thin_margin").height
width: childrenRect.width
spacing: UM.Theme.getSize("default_margin").width
UM.CheckBox
{
id: asProjectCheckbox
height: UM.Theme.getSize("checkbox").height
anchors.verticalCenter: parent.verticalCenter
checked: true
text: "Save Cura project"
font: UM.Theme.getFont("medium")
}
enabled: UM.Backend.state == UM.Backend.Done
currentIndex: UM.Backend.state == UM.Backend.Done ? 0 : 1
textRole: "text"
valueRole: "value"
UM.CheckBox
{
id: asSlicedCheckbox
height: UM.Theme.getSize("checkbox").height
anchors.verticalCenter: parent.verticalCenter
enabled: UM.Backend.state == UM.Backend.Done
checked: UM.Backend.state == UM.Backend.Done
text: "Save print file"
font: UM.Theme.getFont("medium")
}
model: [
{ text: catalog.i18nc("@option", "Save Cura project and print file"), key: "3mf_ufp", value: ["3mf", "ufp"] },
{ text: catalog.i18nc("@option", "Save Cura project"), key: "3mf", value: ["3mf"] },
]
}
Component.onCompleted:
@ -248,4 +223,10 @@ Item
saveButton.clicked.connect(base.savePressed)
selectDifferentProjectButton.clicked.connect(base.selectDifferentProjectPressed)
}
onModelRowsChanged:
{
tableModel.clear()
tableModel.rows = modelRows
}
}

View file

@ -6,7 +6,7 @@ import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.1
import UM 1.2 as UM
import UM 1.6 as UM
import Cura 1.7 as Cura
import DigitalFactory 1.0 as DF
@ -56,7 +56,7 @@ Item
id: createNewProjectButton
text: "New Library project"
visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed)
visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == 2 || manager.retrievingProjectsStatus == 3) // Status is succeeded or failed
onClicked:
{
@ -99,18 +99,17 @@ Item
{
id: digitalFactoryImage
anchors.horizontalCenter: parent.horizontalCenter
source: searchBar.text === "" ? "../images/digital_factory.svg" : "../images/projects_not_found.svg"
source: Qt.resolvedUrl(searchBar.text === "" ? "../images/digital_factory.svg" : "../images/projects_not_found.svg")
fillMode: Image.PreserveAspectFit
width: parent.width - 2 * UM.Theme.getSize("thick_margin").width
}
Label
UM.Label
{
id: noLibraryProjectsLabel
anchors.horizontalCenter: parent.horizontalCenter
text: searchBar.text === "" ? "It appears that you don't have any projects in the Library yet." : "No projects found that match the search query."
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
}
Cura.TertiaryButton
@ -148,29 +147,7 @@ Item
contentHeight: projectsListView.implicitHeight
anchors.fill: parent
ScrollBar.vertical: ScrollBar
{
// Vertical ScrollBar, styled similarly to the scrollBar in the settings panel
id: verticalScrollBar
visible: flickableView.contentHeight > flickableView.height
background: Rectangle
{
implicitWidth: UM.Theme.getSize("scrollbar").width
radius: Math.round(implicitWidth / 2)
color: UM.Theme.getColor("scrollbar_background")
}
contentItem: Rectangle
{
id: scrollViewHandle
implicitWidth: UM.Theme.getSize("scrollbar").width
radius: Math.round(implicitWidth / 2)
color: verticalScrollBar.pressed ? UM.Theme.getColor("scrollbar_handle_down") : verticalScrollBar.hovered ? UM.Theme.getColor("scrollbar_handle_hover") : UM.Theme.getColor("scrollbar_handle")
Behavior on color { ColorAnimation { duration: 50; } }
}
}
ScrollBar.vertical: UM.ScrollBar { id: verticalScrollBar }
Column
{

View file

@ -1,203 +0,0 @@
//Copyright (C) 2022 Ultimaker B.V.
//Cura is released under the terms of the LGPLv3 or higher.
import Qt.labs.qmlmodels 1.0
import QtQuick 2.15
import QtQuick.Controls 2.15
import UM 1.2 as UM
/*
* A re-sizeable table of data.
*
* This table combines a list of headers with a TableView to show certain roles in a table.
* The columns of the table can be resized.
* When the table becomes too big, you can scroll through the table. When a column becomes too small, the contents of
* the table are elided.
* The table gets Cura's themeing.
*/
Item
{
id: tableBase
required property var columnHeaders //The text to show in the headers of each column.
property alias model: tableView.model //A TableModel to display in this table. To use a ListModel for the rows, use "rows: listModel.items"
property int currentRow: -1 //The selected row index.
property var onDoubleClicked: function(row) {} //Something to execute when double clicked. Accepts one argument: The index of the row that was clicked on.
property bool allowSelection: true //Whether to allow the user to select items.
Row
{
id: headerBar
Repeater
{
id: headerRepeater
model: columnHeaders
Rectangle
{
//minimumWidth: Math.max(1, Math.round(tableBase.width / headerRepeater.count))
width: 300
height: UM.Theme.getSize("section").height
color: UM.Theme.getColor("secondary")
Label
{
id: contentText
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("narrow_margin").width
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("narrow_margin").width
text: modelData
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Rectangle //Resize handle.
{
anchors
{
right: parent.right
top: parent.top
bottom: parent.bottom
}
width: UM.Theme.getSize("thick_lining").width
color: UM.Theme.getColor("thick_lining")
MouseArea
{
anchors.fill: parent
cursorShape: Qt.SizeHorCursor
drag
{
target: parent
axis: Drag.XAxis
}
onMouseXChanged:
{
if(drag.active)
{
let new_width = parent.parent.width + mouseX;
let sum_widths = mouseX;
for(let i = 0; i < headerBar.children.length; ++i)
{
sum_widths += headerBar.children[i].width;
}
if(sum_widths > tableBase.width)
{
new_width -= sum_widths - tableBase.width; //Limit the total width to not exceed the view.
}
let width_fraction = new_width / tableBase.width; //Scale with the same fraction along with the total width, if the table is resized.
parent.parent.width = Qt.binding(function() { return Math.max(10, Math.round(tableBase.width * width_fraction)) });
}
}
}
}
onWidthChanged:
{
tableView.forceLayout(); //Rescale table cells underneath as well.
}
}
}
}
TableView
{
id: tableView
anchors
{
top: headerBar.bottom
left: parent.left
right: parent.right
bottom: parent.bottom
}
flickableDirection: Flickable.AutoFlickIfNeeded
clip: true
ScrollBar.vertical: ScrollBar
{
// Vertical ScrollBar, styled similarly to the scrollBar in the settings panel
id: verticalScrollBar
visible: tableView.contentHeight > tableView.height
background: Rectangle
{
implicitWidth: UM.Theme.getSize("scrollbar").width
radius: Math.round(implicitWidth / 2)
color: UM.Theme.getColor("scrollbar_background")
}
contentItem: Rectangle
{
id: scrollViewHandle
implicitWidth: UM.Theme.getSize("scrollbar").width
radius: Math.round(implicitWidth / 2)
color: verticalScrollBar.pressed ? UM.Theme.getColor("scrollbar_handle_down") : verticalScrollBar.hovered ? UM.Theme.getColor("scrollbar_handle_hover") : UM.Theme.getColor("scrollbar_handle")
Behavior on color { ColorAnimation { duration: 50; } }
}
}
columnWidthProvider: function(column)
{
return headerBar.children[column].width; //Cells get the same width as their column header.
}
delegate: Rectangle
{
implicitHeight: Math.max(1, cellContent.height)
color: UM.Theme.getColor((tableBase.currentRow == row) ? "primary" : ((row % 2 == 0) ? "main_background" : "viewport_background"))
Label
{
id: cellContent
width: parent.width
text: display
verticalAlignment: Text.AlignVCenter
elide: Text.ElideRight
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
}
TextMetrics
{
id: cellTextMetrics
text: cellContent.text
font: cellContent.font
elide: cellContent.elide
elideWidth: cellContent.width
}
UM.TooltipArea
{
anchors.fill: parent
acceptedButtons: Qt.LeftButton
text: (cellTextMetrics.elidedText == cellContent.text) ? "" : cellContent.text //Show full text in tooltip if it was elided.
onClicked:
{
if(tableBase.allowSelection)
{
tableBase.currentRow = row; //Select this row.
}
}
onDoubleClicked:
{
tableBase.onDoubleClicked(row);
}
}
}
Connections
{
target: model
function onRowCountChanged()
{
tableView.contentY = 0; //When the number of rows is reduced, make sure to scroll back to the start.
}
}
}
}

View file

@ -1,13 +1,14 @@
# Copyright (c) 2021 Ultimaker B.V.
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
import threading
from json import JSONDecodeError
from typing import List, Dict, Any, Callable, Union, Optional
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from PyQt5.QtNetwork import QNetworkReply
from PyQt6.QtCore import QUrl
from PyQt6.QtGui import QDesktopServices
from PyQt6.QtNetwork import QNetworkReply
from UM.FileHandler.FileHandler import FileHandler
from UM.Logger import Logger
@ -37,17 +38,18 @@ class DFFileExportAndUploadManager:
formats: List[str],
on_upload_error: Callable[[], Any],
on_upload_success: Callable[[], Any],
on_upload_finished: Callable[[], Any] ,
on_upload_finished: Callable[[], Any],
on_upload_progress: Callable[[int], Any]) -> None:
self._file_handlers = file_handlers # type: Dict[str, FileHandler]
self._nodes = nodes # type: List[SceneNode]
self._library_project_id = library_project_id # type: str
self._library_project_name = library_project_name # type: str
self._file_name = file_name # type: str
self._upload_jobs = [] # type: List[ExportFileJob]
self._formats = formats # type: List[str]
self._file_handlers: Dict[str, FileHandler] = file_handlers
self._nodes: List[SceneNode] = nodes
self._library_project_id: str = library_project_id
self._library_project_name: str = library_project_name
self._file_name: str = file_name
self._upload_jobs: List[ExportFileJob] = []
self._formats: List[str] = formats
self._api = DigitalFactoryApiClient(application = CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error)))
self._source_file_id: Optional[str] = None
# Functions of the parent class that should be called based on the upload process output
self._on_upload_error = on_upload_error
@ -59,7 +61,7 @@ class DFFileExportAndUploadManager:
# show the success message (once both upload jobs are done)
self._message_lock = threading.Lock()
self._file_upload_job_metadata = self.initializeFileUploadJobMetadata() # type: Dict[str, Dict[str, Any]]
self._file_upload_job_metadata: Dict[str, Dict[str, Any]] = self.initializeFileUploadJobMetadata()
self.progress_message = Message(
title = "Uploading...",
@ -113,7 +115,8 @@ class DFFileExportAndUploadManager:
content_type = job.getMimeType(),
job_name = job.getFileName(),
file_size = len(job.getOutput()),
library_project_id = self._library_project_id
library_project_id = self._library_project_id,
source_file_id = self._source_file_id
)
self._api.requestUploadUFP(request, on_finished = self._uploadFileData, on_error = self._onRequestUploadPrintFileFailed)
@ -125,11 +128,17 @@ class DFFileExportAndUploadManager:
"""
if isinstance(file_upload_response, DFLibraryFileUploadResponse):
file_name = file_upload_response.file_name
# store the `file_id` so it can be as `source_file_id` when uploading the print file
self._source_file_id = file_upload_response.file_id
elif isinstance(file_upload_response, DFPrintJobUploadResponse):
file_name = file_upload_response.job_name if file_upload_response.job_name is not None else ""
else:
Logger.log("e", "Wrong response type received. Aborting uploading file to the Digital Library")
return
if file_name not in self._file_upload_job_metadata:
Logger.error(f"API response for uploading doesn't match the file name we just uploaded: {file_name} was never uploaded.")
return
with self._message_lock:
self.progress_message.show()
self._file_upload_job_metadata[file_name]["file_upload_response"] = file_upload_response
@ -145,6 +154,8 @@ class DFFileExportAndUploadManager:
on_progress = self._onUploadProgress,
on_error = self._onUploadError)
self._handleNextUploadJob()
def _onUploadProgress(self, filename: str, progress: int) -> None:
"""
Updates the progress message according to the total progress of the two files and displays it to the user. It is
@ -325,8 +336,14 @@ class DFFileExportAndUploadManager:
message.hide()
def start(self) -> None:
for job in self._upload_jobs:
self._handleNextUploadJob()
def _handleNextUploadJob(self):
try:
job = self._upload_jobs.pop(0)
job.start()
except IndexError:
pass # Empty list, do nothing.
def initializeFileUploadJobMetadata(self) -> Dict[str, Any]:
metadata = {}

View file

@ -1,7 +1,7 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
from typing import Callable, Any, cast, Optional, Union
from UM.Logger import Logger
@ -39,8 +39,8 @@ class DFFileUploader:
:param on_error: The method to be called when an error occurs.
"""
self._http = http # type: HttpRequestManager
self._df_file = df_file # type: Union[DFLibraryFileUploadResponse, DFPrintJobUploadResponse]
self._http: HttpRequestManager = http
self._df_file: Union[DFLibraryFileUploadResponse, DFPrintJobUploadResponse] = df_file
self._file_name = ""
if isinstance(self._df_file, DFLibraryFileUploadResponse):
self._file_name = self._df_file.file_name
@ -51,7 +51,7 @@ class DFFileUploader:
self._file_name = ""
else:
raise TypeError("Incorrect input type")
self._data = data # type: bytes
self._data: bytes = data
self._on_finished = on_finished
self._on_success = on_success
@ -120,9 +120,9 @@ class DFFileUploader:
"""
Logger.log("i", "Finished callback %s %s",
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString())
reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute), reply.url().toString())
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) # type: Optional[int]
status_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) # type: Optional[int]
if not status_code:
Logger.log("e", "Reply contained no status code.")
self._onUploadError(reply, None)

View file

@ -1,12 +1,14 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from .BaseModel import BaseModel
# Model that represents the request to upload a print job to the cloud
class DFPrintJobUploadRequest(BaseModel):
def __init__(self, job_name: str, file_size: int, content_type: str, library_project_id: str, **kwargs) -> None:
def __init__(self, job_name: str, file_size: int, content_type: str, library_project_id: str, source_file_id: str, **kwargs) -> None:
"""Creates a new print job upload request.
:param job_name: The name of the print job.
@ -18,4 +20,5 @@ class DFPrintJobUploadRequest(BaseModel):
self.file_size = file_size
self.content_type = content_type
self.library_project_id = library_project_id
self.source_file_id = source_file_id
super().__init__(**kwargs)

View file

@ -7,7 +7,7 @@ import re
from time import time
from typing import List, Any, Optional, Union, Type, Tuple, Dict, cast, TypeVar, Callable
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest
from UM.Logger import Logger
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
@ -40,7 +40,7 @@ class DigitalFactoryApiClient:
DEFAULT_REQUEST_TIMEOUT = 10 # seconds
# In order to avoid garbage collection we keep the callbacks in this list.
_anti_gc_callbacks = [] # type: List[Callable[[Any], None]]
_anti_gc_callbacks: List[Callable[[Any], None]] = []
def __init__(self, application: CuraApplication, on_error: Callable[[List[CloudError]], None], projects_limit_per_page: Optional[int] = None) -> None:
"""Initializes a new digital factory API client.
@ -54,7 +54,7 @@ class DigitalFactoryApiClient:
self._scope = JsonDecoratorScope(UltimakerCloudScope(application))
self._http = HttpRequestManager.getInstance()
self._on_error = on_error
self._file_uploader = None # type: Optional[DFFileUploader]
self._file_uploader: Optional[DFFileUploader] = None
self._library_max_private_projects: Optional[int] = None
self._projects_pagination_mgr = PaginationManager(limit = projects_limit_per_page) if projects_limit_per_page else None # type: Optional[PaginationManager]
@ -71,8 +71,6 @@ class DigitalFactoryApiClient:
has_access = response.library_max_private_projects == -1 or response.library_max_private_projects > 0
callback(has_access)
self._library_max_private_projects = response.library_max_private_projects
# update the account with the additional user rights
self._account.updateAdditionalRight(df_access = has_access)
else:
Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}")
callback(False)
@ -228,7 +226,7 @@ class DigitalFactoryApiClient:
self._anti_gc_callbacks.remove(parse)
# Don't try to parse the reply if we didn't get one
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None:
if reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) is None:
if on_error is not None:
on_error()
return
@ -250,7 +248,7 @@ class DigitalFactoryApiClient:
:return: A tuple with a status code and a dictionary.
"""
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
status_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
try:
response = bytes(reply.readAll()).decode()
return status_code, json.loads(response)

View file

@ -10,9 +10,9 @@ from enum import IntEnum
from pathlib import Path
from typing import Optional, List, Dict, Any, cast
from PyQt5.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, Q_ENUMS, QTimer, QUrl
from PyQt5.QtNetwork import QNetworkReply
from PyQt5.QtQml import qmlRegisterType, qmlRegisterUncreatableType
from PyQt6.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, pyqtEnum, QTimer, QUrl, QMetaObject
from PyQt6.QtNetwork import QNetworkReply
from PyQt6.QtQml import qmlRegisterType, qmlRegisterUncreatableMetaObject
from UM.FileHandler.FileHandler import FileHandler
from UM.Logger import Logger
@ -32,26 +32,6 @@ from .DigitalFactoryProjectModel import DigitalFactoryProjectModel
from .DigitalFactoryProjectResponse import DigitalFactoryProjectResponse
class RetrievalStatus(IntEnum):
"""
The status of an http get request.
This is not an enum, because we want to use it in QML and QML doesn't recognize Python enums.
"""
Idle = 0
InProgress = 1
Success = 2
Failed = 3
class DFRetrievalStatus(QObject):
"""
Used as an intermediate QObject that registers the RetrievalStatus as a recognizable enum in QML, so that it can
be used within QML objects as DigitalFactory.RetrievalStatus.<status>
"""
Q_ENUMS(RetrievalStatus)
class DigitalFactoryController(QObject):
@ -98,6 +78,19 @@ class DigitalFactoryController(QObject):
"""Signal to inform whether the user is allowed to create more Library projects."""
userCanCreateNewLibraryProjectChanged = pyqtSignal(bool)
class RetrievalStatus(IntEnum):
"""
The status of an http get request.
This is not an enum, because we want to use it in QML and QML doesn't recognize Python enums.
"""
Idle = 0
InProgress = 1
Success = 2
Failed = 3
pyqtEnum(RetrievalStatus)
def __init__(self, application: CuraApplication) -> None:
super().__init__(parent = None)
@ -139,9 +132,9 @@ class DigitalFactoryController(QObject):
self._erase_temp_files_lock = threading.Lock()
# The statuses which indicate whether Cura is waiting for a response from the DigitalFactory API
self.retrieving_files_status = RetrievalStatus.Idle
self.retrieving_projects_status = RetrievalStatus.Idle
self.creating_new_project_status = RetrievalStatus.Idle
self.retrieving_files_status = self.RetrievalStatus.Idle
self.retrieving_projects_status = self.RetrievalStatus.Idle
self.creating_new_project_status = self.RetrievalStatus.Idle
self._application.engineCreatedSignal.connect(self._onEngineCreated)
self._application.initializationFinished.connect(self._applicationInitializationFinished)
@ -155,9 +148,9 @@ class DigitalFactoryController(QObject):
self._has_preselected_project = False
self.preselectedProjectChanged.emit()
self.setRetrievingFilesStatus(RetrievalStatus.Idle)
self.setRetrievingProjectsStatus(RetrievalStatus.Idle)
self.setCreatingNewProjectStatus(RetrievalStatus.Idle)
self.setRetrievingFilesStatus(self.RetrievalStatus.Idle)
self.setRetrievingProjectsStatus(self.RetrievalStatus.Idle)
self.setCreatingNewProjectStatus(self.RetrievalStatus.Idle)
self.setSelectedProjectIndex(-1)
@ -182,7 +175,7 @@ class DigitalFactoryController(QObject):
self.clear()
if self._account.isLoggedIn and self.userAccountHasLibraryAccess():
self.setRetrievingProjectsStatus(RetrievalStatus.InProgress)
self.setRetrievingProjectsStatus(self.RetrievalStatus.InProgress)
if preselected_project_id:
self._api.getProject(preselected_project_id, on_finished = self.setProjectAsPreselected, failed = self._onGetProjectFailed)
else:
@ -199,8 +192,8 @@ class DigitalFactoryController(QObject):
self._project_model.setProjects([df_project])
self.setSelectedProjectIndex(0)
self.setHasPreselectedProject(True)
self.setRetrievingProjectsStatus(RetrievalStatus.Success)
self.setCreatingNewProjectStatus(RetrievalStatus.Success)
self.setRetrievingProjectsStatus(self.RetrievalStatus.Success)
self.setCreatingNewProjectStatus(self.RetrievalStatus.Success)
def _onGetProjectFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
reply_string = bytes(reply.readAll()).decode()
@ -216,7 +209,7 @@ class DigitalFactoryController(QObject):
"""
self.setHasMoreProjectsToLoad(self._api.hasMoreProjectsToLoad())
self._project_model.setProjects(df_projects)
self.setRetrievingProjectsStatus(RetrievalStatus.Success)
self.setRetrievingProjectsStatus(self.RetrievalStatus.Success)
@pyqtSlot()
def loadMoreProjects(self) -> None:
@ -224,7 +217,7 @@ class DigitalFactoryController(QObject):
Initiates the process of retrieving the next page of the projects list from the API.
"""
self._api.getMoreProjects(on_finished = self.loadMoreProjectsFinished, failed = self._onGetProjectsFailed)
self.setRetrievingProjectsStatus(RetrievalStatus.InProgress)
self.setRetrievingProjectsStatus(self.RetrievalStatus.InProgress)
def loadMoreProjectsFinished(self, df_projects: List[DigitalFactoryProjectResponse]) -> None:
"""
@ -235,13 +228,13 @@ class DigitalFactoryController(QObject):
"""
self.setHasMoreProjectsToLoad(self._api.hasMoreProjectsToLoad())
self._project_model.extendProjects(df_projects)
self.setRetrievingProjectsStatus(RetrievalStatus.Success)
self.setRetrievingProjectsStatus(self.RetrievalStatus.Success)
def _onGetProjectsFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
"""
Error function, called whenever the retrieval of projects fails.
"""
self.setRetrievingProjectsStatus(RetrievalStatus.Failed)
self.setRetrievingProjectsStatus(self.RetrievalStatus.Failed)
Logger.log("w", "Failed to retrieve the list of projects from the Digital Library. Error encountered: {}".format(error))
def getProjectFilesFinished(self, df_files_in_project: List[DigitalFactoryFileResponse]) -> None:
@ -255,7 +248,7 @@ class DigitalFactoryController(QObject):
# Filter to show only the files that can be opened in Cura
self._file_model.setFilters({"file_name": lambda x: Path(x).suffix[1:].lower() in self._supported_file_types}) # the suffix is in format '.xyz', so omit the dot at the start
self._file_model.setFiles(df_files_in_project)
self.setRetrievingFilesStatus(RetrievalStatus.Success)
self.setRetrievingFilesStatus(self.RetrievalStatus.Success)
def getProjectFilesFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
"""
@ -265,7 +258,7 @@ class DigitalFactoryController(QObject):
Logger.warning(f"Failed to retrieve the list of files in project '{self._project_model._projects[self._selected_project_idx]}' from the Digital Library")
except IndexError:
Logger.warning(f"Failed to retrieve the list of files in a project from the Digital Library. And failed to get the project too.")
self.setRetrievingFilesStatus(RetrievalStatus.Failed)
self.setRetrievingFilesStatus(self.RetrievalStatus.Failed)
@pyqtSlot()
def clearProjectSelection(self) -> None:
@ -297,7 +290,7 @@ class DigitalFactoryController(QObject):
self.selectedFileIndicesChanged.emit([])
if 0 <= project_idx < len(self._project_model.items):
library_project_id = self._project_model.items[project_idx]["libraryProjectId"]
self.setRetrievingFilesStatus(RetrievalStatus.InProgress)
self.setRetrievingFilesStatus(self.RetrievalStatus.InProgress)
self._api.getListOfFilesInProject(library_project_id, on_finished = self.getProjectFilesFinished, failed = self.getProjectFilesFailed)
@pyqtProperty(int, fset = setSelectedProjectIndex, notify = selectedProjectIndexChanged)
@ -381,7 +374,7 @@ class DigitalFactoryController(QObject):
"""
if project_name:
self._api.createNewProject(project_name, self.setProjectAsPreselected, self._createNewLibraryProjectFailed)
self.setCreatingNewProjectStatus(RetrievalStatus.InProgress)
self.setCreatingNewProjectStatus(self.RetrievalStatus.InProgress)
else:
Logger.log("w", "No project name provided while attempting to create a new project. Aborting the project creation.")
@ -395,7 +388,7 @@ class DigitalFactoryController(QObject):
self._project_creation_error_text = "Error while creating the new project: {}".format(reply_dict["errors"][0]["title"])
self.projectCreationErrorTextChanged.emit()
self.setCreatingNewProjectStatus(RetrievalStatus.Failed)
self.setCreatingNewProjectStatus(self.RetrievalStatus.Failed)
Logger.log("e", "Something went wrong while trying to create a new a project. Error: {}".format(reply_string))
def setRetrievingProjectsStatus(self, new_status: RetrievalStatus) -> None:
@ -439,7 +432,7 @@ class DigitalFactoryController(QObject):
@staticmethod
def _onEngineCreated() -> None:
qmlRegisterUncreatableType(DFRetrievalStatus, "DigitalFactory", 1, 0, "RetrievalStatus", "Could not create RetrievalStatus enum type")
qmlRegisterUncreatableMetaObject(DigitalFactoryController.staticMetaObject, "DigitalFactory", 1, 0, "RetrievalStatus", "RetrievalStatus is an Enum-only type")
def _applicationInitializationFinished(self) -> None:
self._supported_file_types = self._application.getInstance().getMeshFileHandler().getSupportedFileTypesRead()
@ -565,7 +558,7 @@ class DigitalFactoryController(QObject):
self.setSelectedProjectIndex(-1)
self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject)
self.setRetrievingProjectsStatus(RetrievalStatus.InProgress)
self.setRetrievingProjectsStatus(self.RetrievalStatus.InProgress)
self._has_preselected_project = new_has_preselected_project
self.preselectedProjectChanged.emit()

View file

@ -2,7 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher.
from typing import List, Dict, Callable
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt6.QtCore import Qt, pyqtSignal
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
@ -13,13 +13,13 @@ DIGITAL_FACTORY_DISPLAY_DATETIME_FORMAT = "%d-%m-%Y %H:%M"
class DigitalFactoryFileModel(ListModel):
FileNameRole = Qt.UserRole + 1
FileIdRole = Qt.UserRole + 2
FileSizeRole = Qt.UserRole + 3
LibraryProjectIdRole = Qt.UserRole + 4
DownloadUrlRole = Qt.UserRole + 5
UsernameRole = Qt.UserRole + 6
UploadedAtRole = Qt.UserRole + 7
FileNameRole = Qt.ItemDataRole.UserRole + 1
FileIdRole = Qt.ItemDataRole.UserRole + 2
FileSizeRole = Qt.ItemDataRole.UserRole + 3
LibraryProjectIdRole = Qt.ItemDataRole.UserRole + 4
DownloadUrlRole = Qt.ItemDataRole.UserRole + 5
UsernameRole = Qt.ItemDataRole.UserRole + 6
UploadedAtRole = Qt.ItemDataRole.UserRole + 7
dfFileModelChanged = pyqtSignal()

View file

@ -2,7 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher.
from typing import List, Optional
from PyQt5.QtCore import Qt, pyqtSignal
from PyQt6.QtCore import Qt, pyqtSignal
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
@ -12,12 +12,12 @@ PROJECT_UPDATED_AT_DATETIME_FORMAT = "%d-%m-%Y"
class DigitalFactoryProjectModel(ListModel):
DisplayNameRole = Qt.UserRole + 1
LibraryProjectIdRole = Qt.UserRole + 2
DescriptionRole = Qt.UserRole + 3
ThumbnailUrlRole = Qt.UserRole + 5
UsernameRole = Qt.UserRole + 6
LastUpdatedRole = Qt.UserRole + 7
DisplayNameRole = Qt.ItemDataRole.UserRole + 1
LibraryProjectIdRole = Qt.ItemDataRole.UserRole + 2
DescriptionRole = Qt.ItemDataRole.UserRole + 3
ThumbnailUrlRole = Qt.ItemDataRole.UserRole + 5
UsernameRole = Qt.ItemDataRole.UserRole + 6
LastUpdatedRole = Qt.ItemDataRole.UserRole + 7
dfProjectModelChanged = pyqtSignal()

View file

@ -1,11 +1,14 @@
# Copyright (c) 2021 Ultimaker B.V.
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from datetime import datetime
from typing import Optional, List, Dict, Any
from .BaseModel import BaseModel
from .DigitalFactoryFileResponse import DIGITAL_FACTORY_RESPONSE_DATETIME_FORMAT
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
class DigitalFactoryProjectResponse(BaseModel):
"""Class representing a cloud project."""
@ -13,8 +16,8 @@ class DigitalFactoryProjectResponse(BaseModel):
def __init__(self,
library_project_id: str,
display_name: str,
username: str,
organization_shared: bool,
username: str = catalog.i18nc("@text Placeholder for the username if it has been deleted", "deleted user"),
organization_shared: bool = False,
last_updated: Optional[str] = None,
created_at: Optional[str] = None,
thumbnail_url: Optional[str] = None,

View file

@ -1,8 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices
from PyQt6.QtCore import QUrl
from PyQt6.QtGui import QDesktopServices
from typing import Set

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Checks for firmware updates.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -8,7 +8,7 @@ from UM.i18n import i18nCatalog
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdateState
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject
from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject
from typing import Optional
MYPY = False

View file

@ -5,7 +5,7 @@ import QtQuick 2.2
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import QtQuick.Dialogs 1.2 // For filedialog
import QtQuick.Dialogs // For filedialog
import UM 1.5 as UM
import Cura 1.0 as Cura
@ -92,11 +92,10 @@ Cura.MachineAction
id: customFirmwareDialog
title: catalog.i18nc("@title:window", "Select custom firmware")
nameFilters: "Firmware image files (*.hex)"
selectExisting: true
onAccepted:
{
updateProgressDialog.visible = true;
activeOutputDevice.updateFirmware(fileUrl);
activeOutputDevice.updateFirmware(selectedFile);
}
}
@ -148,8 +147,6 @@ Cura.MachineAction
return catalog.i18nc("@label","Firmware update failed due to missing firmware.");
}
}
wrapMode: Text.Wrap
}
UM.ProgressBar

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides a machine actions for updating firmware.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Reads g-code from a compressed archive.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Writes g-code to a compressed archive.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for importing profiles from g-code files.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Victor Larchenko, Ultimaker B.V.",
"version": "1.0.1",
"description": "Allows loading and displaying G-code files.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -1,9 +1,8 @@
# Copyright (c) 2019 Ultimaker B.V.
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import re # For escaping characters in the settings.
import json
import copy
from UM.Mesh.MeshWriter import MeshWriter
from UM.Logger import Logger
@ -12,6 +11,8 @@ from UM.Settings.InstanceContainer import InstanceContainer
from cura.Machines.ContainerTree import ContainerTree
from UM.i18n import i18nCatalog
from cura.Settings.CuraStackBuilder import CuraStackBuilder
catalog = i18nCatalog("cura")
@ -96,25 +97,6 @@ class GCodeWriter(MeshWriter):
self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting."))
return False
def _createFlattenedContainerInstance(self, instance_container1, instance_container2):
"""Create a new container with container 2 as base and container 1 written over it."""
flat_container = InstanceContainer(instance_container2.getName())
# The metadata includes id, name and definition
flat_container.setMetaData(copy.deepcopy(instance_container2.getMetaData()))
if instance_container1.getDefinition():
flat_container.setDefinition(instance_container1.getDefinition().getId())
for key in instance_container2.getAllKeys():
flat_container.setProperty(key, "value", instance_container2.getProperty(key, "value"))
for key in instance_container1.getAllKeys():
flat_container.setProperty(key, "value", instance_container1.getProperty(key, "value"))
return flat_container
def _serialiseSettings(self, stack):
"""Serialises a container stack to prepare it for writing at the end of the g-code.
@ -145,22 +127,22 @@ class GCodeWriter(MeshWriter):
container_with_profile.setDefinition(machine_definition_id_for_quality)
container_with_profile.setMetaDataEntry("setting_version", stack.quality.getMetaDataEntry("setting_version"))
flat_global_container = self._createFlattenedContainerInstance(stack.userChanges, container_with_profile)
merged_global_instance_container = InstanceContainer.createMergedInstanceContainer(stack.userChanges, container_with_profile)
# If the quality changes is not set, we need to set type manually
if flat_global_container.getMetaDataEntry("type", None) is None:
flat_global_container.setMetaDataEntry("type", "quality_changes")
if merged_global_instance_container.getMetaDataEntry("type", None) is None:
merged_global_instance_container.setMetaDataEntry("type", "quality_changes")
# Ensure that quality_type is set. (Can happen if we have empty quality changes).
if flat_global_container.getMetaDataEntry("quality_type", None) is None:
flat_global_container.setMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal"))
if merged_global_instance_container.getMetaDataEntry("quality_type", None) is None:
merged_global_instance_container.setMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal"))
# Get the machine definition ID for quality profiles
flat_global_container.setMetaDataEntry("definition", machine_definition_id_for_quality)
merged_global_instance_container.setMetaDataEntry("definition", machine_definition_id_for_quality)
serialized = flat_global_container.serialize()
serialized = merged_global_instance_container.serialize()
data = {"global_quality": serialized}
all_setting_keys = flat_global_container.getAllKeys()
all_setting_keys = merged_global_instance_container.getAllKeys()
for extruder in stack.extruderList:
extruder_quality = extruder.qualityChanges
if extruder_quality.getId() == "empty_quality_changes":
@ -174,7 +156,7 @@ class GCodeWriter(MeshWriter):
extruder_quality.setDefinition(machine_definition_id_for_quality)
extruder_quality.setMetaDataEntry("setting_version", stack.quality.getMetaDataEntry("setting_version"))
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.userChanges, extruder_quality)
flat_extruder_quality = InstanceContainer.createMergedInstanceContainer(extruder.userChanges, extruder_quality)
# If the quality changes is not set, we need to set type manually
if flat_extruder_quality.getMetaDataEntry("type", None) is None:
flat_extruder_quality.setMetaDataEntry("type", "quality_changes")

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Writes g-code to a file.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2022 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.1
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3
import QtQuick.Window 2.1
@ -47,7 +47,7 @@ UM.Dialog
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
selectByMouse: true
objectName: "Peak_Height"
validator: RegExpValidator { regExp: /^\d{0,3}([\,|\.]\d*)?$/ }
validator: RegularExpressionValidator { regularExpression: /^\d{0,3}([\,|\.]\d*)?$/ }
onTextChanged: manager.onPeakHeightChanged(text)
}
@ -81,7 +81,7 @@ UM.Dialog
Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
objectName: "Base_Height"
validator: RegExpValidator { regExp: /^\d{0,3}([\,|\.]\d*)?$/ }
validator: RegularExpressionValidator { regularExpression: /^\d{0,3}([\,|\.]\d*)?$/ }
onTextChanged: manager.onBaseHeightChanged(text)
}
@ -115,7 +115,7 @@ UM.Dialog
Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
focus: true
validator: RegExpValidator { regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/ }
validator: RegularExpressionValidator { regularExpression: /^[1-9]\d{0,2}([\,|\.]\d*)?$/ }
onTextChanged: manager.onWidthChanged(text)
}
@ -149,7 +149,7 @@ UM.Dialog
selectByMouse: true
objectName: "Depth"
focus: true
validator: RegExpValidator { regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/ }
validator: RegularExpressionValidator { regularExpression: /^[1-9]\d{0,2}([\,|\.]\d*)?$/ }
onTextChanged: manager.onDepthChanged(text)
}
@ -255,7 +255,7 @@ UM.Dialog
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
selectByMouse: true
objectName: "Transmittance"
validator: RegExpValidator { regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/ }
validator: RegularExpressionValidator { regularExpression: /^[1-9]\d{0,2}([\,|\.]\d*)?$/ }
onTextChanged: manager.onTransmittanceChanged(text)
UM.ToolTip

View file

@ -5,8 +5,8 @@ import numpy
import math
from PyQt5.QtGui import QImage, qRed, qGreen, qBlue, qAlpha
from PyQt5.QtCore import Qt
from PyQt6.QtGui import QImage, qRed, qGreen, qBlue, qAlpha
from PyQt6.QtCore import Qt
from UM.Mesh.MeshReader import MeshReader
from UM.Mesh.MeshBuilder import MeshBuilder
@ -63,7 +63,7 @@ class ImageReader(MeshReader):
aspect = height / width
if img.width() < 2 or img.height() < 2:
img = img.scaled(width, height, Qt.IgnoreAspectRatio)
img = img.scaled(width, height, Qt.AspectRatioMode.IgnoreAspectRatio)
height_from_base = max(height_from_base, 0)
base_height = max(base_height, 0)
@ -84,15 +84,15 @@ class ImageReader(MeshReader):
width = int(max(round(width * scale_factor), 2))
height = int(max(round(height * scale_factor), 2))
img = img.scaled(width, height, Qt.IgnoreAspectRatio)
img = img.scaled(width, height, Qt.AspectRatioMode.IgnoreAspectRatio)
width_minus_one = width - 1
height_minus_one = height - 1
Job.yieldThread()
texel_width = 1.0 / (width_minus_one) * scale_vector.x
texel_height = 1.0 / (height_minus_one) * scale_vector.z
texel_width = 1.0 / width_minus_one * scale_vector.x
texel_height = 1.0 / height_minus_one * scale_vector.z
height_data = numpy.zeros((height, width), dtype = numpy.float32)

View file

@ -4,7 +4,7 @@
import os
import threading
from PyQt5.QtCore import Qt, pyqtSignal, QObject
from PyQt6.QtCore import Qt, pyqtSignal, QObject
from UM.FlameProfiler import pyqtSlot
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
@ -85,7 +85,7 @@ class ImageReaderUI(QObject):
Logger.log("d", "Creating ImageReader config UI")
path = os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml")
self._ui_view = Application.getInstance().createQmlComponent(path, {"manager": self})
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint)
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowType.WindowCloseButtonHint & ~Qt.WindowType.WindowMinimizeButtonHint & ~Qt.WindowType.WindowMaximizeButtonHint)
self._disable_size_callbacks = False
@pyqtSlot()

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Enables ability to generate printable geometry from 2D image files.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides support for importing profiles from legacy Cura versions.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -3,7 +3,7 @@
from typing import Optional, TYPE_CHECKING
from PyQt5.QtCore import pyqtProperty
from PyQt6.QtCore import pyqtProperty
import UM.i18n
from UM.FlameProfiler import pyqtSlot
@ -18,7 +18,7 @@ from cura.Settings.CuraStackBuilder import CuraStackBuilder
from cura.Settings.cura_empty_instance_containers import isEmptyContainer
if TYPE_CHECKING:
from PyQt5.QtCore import QObject
from PyQt6.QtCore import QObject
catalog = UM.i18n.i18nCatalog("cura")

View file

@ -4,7 +4,7 @@
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.3 as UM
import UM 1.5 as UM
import Cura 1.1 as Cura
@ -58,11 +58,10 @@ Item
spacing: base.columnSpacing
Label // Title Label
UM.Label // Title Label
{
text: catalog.i18nc("@title:label", "Nozzle Settings")
font: UM.Theme.getFont("medium_bold")
renderType: Text.NativeRendering
}
Cura.NumericTextFieldWithUnit // "Nozzle size"

View file

@ -5,7 +5,7 @@ import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import UM 1.3 as UM
import UM 1.5 as UM
import Cura 1.1 as Cura
@ -51,12 +51,10 @@ Item
spacing: base.columnSpacing
Label // Title Label
UM.Label // Title Label
{
text: catalog.i18nc("@title:label", "Printer Settings")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
width: parent.width
elide: Text.ElideRight
}
@ -182,12 +180,10 @@ Item
spacing: base.columnSpacing
Label // Title Label
UM.Label // Title Label
{
text: catalog.i18nc("@title:label", "Printhead Settings")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
width: parent.width
elide: Text.ElideRight
}

View file

@ -3,6 +3,6 @@
"author": "fieldOfView, Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -0,0 +1,29 @@
from typing import Union
from cura import ApplicationMetadata
from cura.UltimakerCloud import UltimakerCloudConstants
class CloudApiModel:
sdk_version: Union[str, int] = ApplicationMetadata.CuraSDKVersion
cloud_api_version: str = UltimakerCloudConstants.CuraCloudAPIVersion
cloud_api_root: str = UltimakerCloudConstants.CuraCloudAPIRoot
api_url: str = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
cloud_api_root = cloud_api_root,
cloud_api_version = cloud_api_version,
sdk_version = sdk_version
)
# https://api.ultimaker.com/cura-packages/v1/user/packages
api_url_user_packages = "{cloud_api_root}/cura-packages/v{cloud_api_version}/user/packages".format(
cloud_api_root = cloud_api_root,
cloud_api_version = cloud_api_version,
)
@classmethod
def userPackageUrl(cls, package_id: str) -> str:
"""https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id}"""
return (CloudApiModel.api_url_user_packages + "/{package_id}").format(
package_id = package_id
)

View file

@ -0,0 +1,55 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from cura.CuraApplication import CuraApplication
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
from ..CloudApiModel import CloudApiModel
class CloudApiClient:
"""Manages Cloud subscriptions
When a package is added to a user's account, the user is 'subscribed' to that package.
Whenever the user logs in on another instance of Cura, these subscriptions can be used to sync the user's plugins
Singleton: use CloudApiClient.getInstance() instead of CloudApiClient()
"""
__instance = None
@classmethod
def getInstance(cls, app: CuraApplication):
if not cls.__instance:
cls.__instance = CloudApiClient(app)
return cls.__instance
def __init__(self, app: CuraApplication) -> None:
if self.__instance is not None:
raise RuntimeError("This is a Singleton. use getInstance()")
self._scope: JsonDecoratorScope = JsonDecoratorScope(UltimakerCloudScope(app))
app.getPackageManager().packageInstalled.connect(self._onPackageInstalled)
def unsubscribe(self, package_id: str) -> None:
url = CloudApiModel.userPackageUrl(package_id)
HttpRequestManager.getInstance().delete(url = url, scope = self._scope)
def _subscribe(self, package_id: str) -> None:
"""You probably don't want to use this directly. All installed packages will be automatically subscribed."""
Logger.debug("Subscribing to {}", package_id)
data = "{\"data\": {\"package_id\": \"%s\", \"sdk_version\": \"%s\"}}" % (package_id, CloudApiModel.sdk_version)
HttpRequestManager.getInstance().put(
url = CloudApiModel.api_url_user_packages,
data = data.encode(),
scope = self._scope
)
def _onPackageInstalled(self, package_id: str):
if CuraApplication.getInstance().getCuraAPI().account.isLoggedIn:
# We might already be subscribed, but checking would take one extra request. Instead, simply subscribe
self._subscribe(package_id)

View file

@ -0,0 +1,166 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
from typing import List, Dict, Any, Set
from typing import Optional
from PyQt6.QtCore import QObject
from PyQt6.QtNetwork import QNetworkReply
from UM import i18nCatalog
from UM.Logger import Logger
from UM.Message import Message
from UM.Signal import Signal
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from cura.API.Account import SyncState
from cura.CuraApplication import CuraApplication, ApplicationMetadata
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
from .SubscribedPackagesModel import SubscribedPackagesModel
from ..CloudApiModel import CloudApiModel
class CloudPackageChecker(QObject):
SYNC_SERVICE_NAME = "CloudPackageChecker"
def __init__(self, application: CuraApplication) -> None:
super().__init__()
self.discrepancies = Signal() # Emits SubscribedPackagesModel
self._application: CuraApplication = application
self._scope = JsonDecoratorScope(UltimakerCloudScope(application))
self._model = SubscribedPackagesModel()
self._message: Optional[Message] = None
self._application.initializationFinished.connect(self._onAppInitialized)
self._i18n_catalog = i18nCatalog("cura")
self._sdk_version = ApplicationMetadata.CuraSDKVersion
self._last_notified_packages = set() # type: Set[str]
"""Packages for which a notification has been shown. No need to bother the user twice for equal content"""
# This is a plugin, so most of the components required are not ready when
# this is initialized. Therefore, we wait until the application is ready.
def _onAppInitialized(self) -> None:
self._package_manager = self._application.getPackageManager()
# initial check
self._getPackagesIfLoggedIn()
self._application.getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged)
self._application.getCuraAPI().account.syncRequested.connect(self._getPackagesIfLoggedIn)
def _onLoginStateChanged(self) -> None:
# reset session
self._last_notified_packages = set()
self._getPackagesIfLoggedIn()
def _getPackagesIfLoggedIn(self) -> None:
if self._application.getCuraAPI().account.isLoggedIn:
self._getUserSubscribedPackages()
else:
self._hideSyncMessage()
def _getUserSubscribedPackages(self) -> None:
self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SYNCING)
url = CloudApiModel.api_url_user_packages
self._application.getHttpRequestManager().get(url,
callback = self._onUserPackagesRequestFinished,
error_callback = self._onUserPackagesRequestFinished,
timeout = 10,
scope = self._scope)
def _onUserPackagesRequestFinished(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if error is not None or HttpRequestManager.safeHttpStatus(reply) != 200:
Logger.log("w",
"Requesting user packages failed, response code %s while trying to connect to %s",
HttpRequestManager.safeHttpStatus(reply), reply.url())
self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
return
try:
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
# Check for errors:
if "errors" in json_data:
for error in json_data["errors"]:
Logger.log("e", "%s", error["title"])
self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
return
self._handleCompatibilityData(json_data["data"])
except json.decoder.JSONDecodeError:
Logger.log("w", "Received invalid JSON for user subscribed packages from the Web Marketplace")
self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS)
def _handleCompatibilityData(self, subscribed_packages_payload: List[Dict[str, Any]]) -> None:
user_subscribed_packages = {plugin["package_id"] for plugin in subscribed_packages_payload}
user_installed_packages = self._package_manager.getAllInstalledPackageIDs()
# We need to re-evaluate the dismissed packages
# (i.e. some package might got updated to the correct SDK version in the meantime,
# hence remove them from the Dismissed Incompatible list)
self._package_manager.reEvaluateDismissedPackages(subscribed_packages_payload, self._sdk_version)
user_dismissed_packages = self._package_manager.getDismissedPackages()
if user_dismissed_packages:
user_installed_packages.update(user_dismissed_packages)
# We check if there are packages installed in Web Marketplace but not in Cura marketplace
package_discrepancy = list(user_subscribed_packages.difference(user_installed_packages))
if user_subscribed_packages != self._last_notified_packages:
# scenario:
# 1. user subscribes to a package
# 2. dismisses the license/unsubscribes
# 3. subscribes to the same package again
# in this scenario we want to notify the user again. To capture that there was a change during
# step 2, we clear the last_notified after step 2. This way, the user will be notified after
# step 3 even though the list of packages for step 1 and 3 are equal
self._last_notified_packages = set()
if package_discrepancy:
account = self._application.getCuraAPI().account
account.setUpdatePackagesAction(lambda: self._onSyncButtonClicked(None, None))
if user_subscribed_packages == self._last_notified_packages:
# already notified user about these
return
Logger.log("d", "Discrepancy found between Cloud subscribed packages and Cura installed packages")
self._model.addDiscrepancies(package_discrepancy)
self._model.initialize(self._package_manager, subscribed_packages_payload)
self._showSyncMessage()
self._last_notified_packages = user_subscribed_packages
def _showSyncMessage(self) -> None:
"""Show the message if it is not already shown"""
if self._message is not None:
self._message.show()
return
sync_message = Message(self._i18n_catalog.i18nc(
"@info:generic",
"Do you want to sync material and software packages with your account?"),
title = self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
sync_message.addAction("sync",
name = self._i18n_catalog.i18nc("@action:button", "Sync"),
icon = "",
description = "Sync your plugins and print profiles to Ultimaker Cura.",
button_align = Message.ActionButtonAlignment.ALIGN_RIGHT)
sync_message.actionTriggered.connect(self._onSyncButtonClicked)
sync_message.show()
self._message = sync_message
def _hideSyncMessage(self) -> None:
"""Hide the message if it is showing"""
if self._message is not None:
self._message.hide()
self._message = None
def _onSyncButtonClicked(self, sync_message: Optional[Message], sync_message_action: Optional[str]) -> None:
if sync_message is not None:
sync_message.hide()
self._hideSyncMessage() # Should be the same message, but also sets _message to None
self.discrepancies.emit(self._model)

View file

@ -0,0 +1,44 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from typing import Optional
from PyQt6.QtCore import QObject
from UM.Qt.QtApplication import QtApplication
from UM.Signal import Signal
from .SubscribedPackagesModel import SubscribedPackagesModel
class DiscrepanciesPresenter(QObject):
"""Shows a list of packages to be added or removed. The user can select which packages to (un)install. The user's
choices are emitted on the `packageMutations` Signal.
"""
def __init__(self, app: QtApplication) -> None:
super().__init__()
self.packageMutations = Signal() # Emits SubscribedPackagesModel
self._app = app
self._package_manager = app.getPackageManager()
self._dialog: Optional[QObject] = None
self._compatibility_dialog_path = "resources/qml/CompatibilityDialog.qml"
def present(self, plugin_path: str, model: SubscribedPackagesModel) -> None:
path = os.path.join(plugin_path, self._compatibility_dialog_path)
self._dialog = self._app.createQmlComponent(path, {"subscribedPackagesModel": model, "handler": self})
assert self._dialog
self._dialog.accepted.connect(lambda: self._onConfirmClicked(model))
def _onConfirmClicked(self, model: SubscribedPackagesModel) -> None:
# If there are incompatible packages - automatically dismiss them
if model.getIncompatiblePackages():
self._package_manager.dismissAllIncompatiblePackages(model.getIncompatiblePackages())
# For now, all compatible packages presented to the user should be installed.
# Later, we might remove items for which the user unselected the package
if model.getCompatiblePackages():
model.setItems(model.getCompatiblePackages())
self.packageMutations.emit(model)

View file

@ -0,0 +1,153 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import tempfile
from typing import Dict, List, Any
from PyQt6.QtNetwork import QNetworkReply
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Message import Message
from UM.Signal import Signal
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from cura.CuraApplication import CuraApplication
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
from .SubscribedPackagesModel import SubscribedPackagesModel
i18n_catalog = i18nCatalog("cura")
class DownloadPresenter:
"""Downloads a set of packages from the Ultimaker Cloud Marketplace
use download() exactly once: should not be used for multiple sets of downloads since this class contains state
"""
DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB
def __init__(self, app: CuraApplication) -> None:
# Emits (Dict[str, str], List[str]) # (success_items, error_items)
# Dict{success_package_id, temp_file_path}
# List[errored_package_id]
self.done = Signal()
self._app = app
self._scope = UltimakerCloudScope(app)
self._started = False
self._progress_message = self._createProgressMessage()
self._progress: Dict[str, Dict[str, Any]] = {}
self._error: List[str] = []
def download(self, model: SubscribedPackagesModel) -> None:
if self._started:
Logger.error("Download already started. Create a new %s instead", self.__class__.__name__)
return
manager = HttpRequestManager.getInstance()
for item in model.items:
package_id = item["package_id"]
def finishedCallback(reply: QNetworkReply, pid = package_id) -> None:
self._onFinished(pid, reply)
def progressCallback(rx: int, rt: int, pid = package_id) -> None:
self._onProgress(pid, rx, rt)
def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, pid = package_id) -> None:
self._onError(pid)
request_data = manager.get(
item["download_url"],
callback = finishedCallback,
download_progress_callback = progressCallback,
error_callback = errorCallback,
scope = self._scope)
self._progress[package_id] = {
"received": 0,
"total": 1, # make sure this is not considered done yet. Also divByZero-safe
"file_written": None,
"request_data": request_data,
"package_model": item
}
self._started = True
self._progress_message.show()
def abort(self) -> None:
manager = HttpRequestManager.getInstance()
for item in self._progress.values():
manager.abortRequest(item["request_data"])
# Aborts all current operations and returns a copy with the same settings such as app and scope
def resetCopy(self) -> "DownloadPresenter":
self.abort()
self.done.disconnectAll()
return DownloadPresenter(self._app)
def _createProgressMessage(self) -> Message:
return Message(i18n_catalog.i18nc("@info:generic", "Syncing..."),
lifetime = 0,
use_inactivity_timer = False,
progress = 0.0,
title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account"))
def _onFinished(self, package_id: str, reply: QNetworkReply) -> None:
self._progress[package_id]["received"] = self._progress[package_id]["total"]
try:
with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file:
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
while bytes_read:
temp_file.write(bytes_read)
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
self._app.processEvents()
self._progress[package_id]["file_written"] = temp_file.name
except IOError as e:
Logger.logException("e", "Failed to write downloaded package to temp file", e)
self._onError(package_id)
temp_file.close()
self._checkDone()
def _onProgress(self, package_id: str, rx: int, rt: int) -> None:
self._progress[package_id]["received"] = rx
self._progress[package_id]["total"] = rt
received = 0
total = 0
for item in self._progress.values():
received += item["received"]
total += item["total"]
if total == 0: # Total download size is 0, or unknown, or there are no progress items at all.
self._progress_message.setProgress(100.0)
return
self._progress_message.setProgress(100.0 * (received / total)) # [0 .. 100] %
def _onError(self, package_id: str) -> None:
self._progress.pop(package_id)
self._error.append(package_id)
self._checkDone()
def _checkDone(self) -> bool:
for item in self._progress.values():
if not item["file_written"]:
return False
success_items = {
package_id:
{
"package_path": value["file_written"],
"icon_url": value["package_model"]["icon_url"]
}
for package_id, value in self._progress.items()
}
error_items = [package_id for package_id in self._error]
self._progress_message.hide()
self.done.emit(success_items, error_items)
return True

View file

@ -0,0 +1,80 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
# Model for the ToolboxLicenseDialog
class LicenseModel(QObject):
DEFAULT_DECLINE_BUTTON_TEXT = catalog.i18nc("@button", "Decline")
ACCEPT_BUTTON_TEXT = catalog.i18nc("@button", "Agree")
dialogTitleChanged = pyqtSignal()
packageNameChanged = pyqtSignal()
licenseTextChanged = pyqtSignal()
iconChanged = pyqtSignal()
def __init__(self, decline_button_text: str = DEFAULT_DECLINE_BUTTON_TEXT, parent = None) -> None:
super().__init__(parent)
self._current_page_idx = 0
self._page_count = 1
self._dialogTitle = ""
self._license_text = ""
self._package_name = ""
self._icon_url = ""
self._decline_button_text = decline_button_text
@pyqtProperty(str, constant = True)
def acceptButtonText(self):
return self.ACCEPT_BUTTON_TEXT
@pyqtProperty(str, constant = True)
def declineButtonText(self):
return self._decline_button_text
@pyqtProperty(str, notify=dialogTitleChanged)
def dialogTitle(self) -> str:
return self._dialogTitle
@pyqtProperty(str, notify=packageNameChanged)
def packageName(self) -> str:
return self._package_name
def setPackageName(self, name: str) -> None:
self._package_name = name
self.packageNameChanged.emit()
@pyqtProperty(str, notify=iconChanged)
def iconUrl(self) -> str:
return self._icon_url
def setIconUrl(self, url: str):
self._icon_url = url
self.iconChanged.emit()
@pyqtProperty(str, notify=licenseTextChanged)
def licenseText(self) -> str:
return self._license_text
def setLicenseText(self, license_text: str) -> None:
if self._license_text != license_text:
self._license_text = license_text
self.licenseTextChanged.emit()
def setCurrentPageIdx(self, idx: int) -> None:
self._current_page_idx = idx
self._updateDialogTitle()
def setPageCount(self, count: int) -> None:
self._page_count = count
self._updateDialogTitle()
def _updateDialogTitle(self):
self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement")
if self._page_count > 1:
self._dialogTitle = self._dialogTitle + " ({}/{})".format(self._current_page_idx + 1, self._page_count)
self.dialogTitleChanged.emit()

View file

@ -0,0 +1,139 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from typing import Dict, Optional, List, Any
from PyQt6.QtCore import QObject, pyqtSlot
from UM.Logger import Logger
from UM.PackageManager import PackageManager
from UM.Signal import Signal
from cura.CuraApplication import CuraApplication
from UM.i18n import i18nCatalog
from .LicenseModel import LicenseModel
class LicensePresenter(QObject):
"""Presents licenses for a set of packages for the user to accept or reject.
Call present() exactly once to show a licenseDialog for a set of packages
Before presenting another set of licenses, create a new instance using resetCopy().
licenseAnswers emits a list of Dicts containing answers when the user has made a choice for all provided packages.
"""
def __init__(self, app: CuraApplication) -> None:
super().__init__()
self._presented = False
"""Whether present() has been called and state is expected to be initialized"""
self._dialog: Optional[QObject] = None
self._package_manager: PackageManager = app.getPackageManager()
# Emits List[Dict[str, [Any]] containing for example
# [{ "package_id": "BarbarianPlugin", "package_path" : "/tmp/dg345as", "accepted" : True }]
self.licenseAnswers = Signal()
self._current_package_idx = 0
self._package_models: List[Dict] = []
self._catalog = i18nCatalog("cura")
decline_button_text = self._catalog.i18nc("@button", "Decline and remove from account")
self._license_model: LicenseModel = LicenseModel(decline_button_text=decline_button_text)
self._page_count = 0
self._app = app
self._compatibility_dialog_path = "resources/qml/MultipleLicenseDialog.qml"
def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None:
"""Show a license dialog for multiple packages where users can read a license and accept or decline them
:param plugin_path: Root directory of the Toolbox plugin
:param packages: Dict[package id, file path]
"""
if self._presented:
Logger.error("{clazz} is single-use. Create a new {clazz} instead", clazz=self.__class__.__name__)
return
path = os.path.join(plugin_path, self._compatibility_dialog_path)
self._initState(packages)
if self._page_count == 0:
self.licenseAnswers.emit(self._package_models)
return
if self._dialog is None:
context_properties = {
"licenseModel": self._license_model,
"handler": self
}
self._dialog = self._app.createQmlComponent(path, context_properties)
self._presentCurrentPackage()
self._presented = True
def resetCopy(self) -> "LicensePresenter":
"""Clean up and return a new copy with the same settings such as app"""
if self._dialog:
self._dialog.close()
self.licenseAnswers.disconnectAll()
return LicensePresenter(self._app)
@pyqtSlot()
def onLicenseAccepted(self) -> None:
self._package_models[self._current_package_idx]["accepted"] = True
self._checkNextPage()
@pyqtSlot()
def onLicenseDeclined(self) -> None:
self._package_models[self._current_package_idx]["accepted"] = False
self._checkNextPage()
def _initState(self, packages: Dict[str, Dict[str, Any]]) -> None:
implicitly_accepted_count = 0
for package_id, item in packages.items():
item["package_id"] = package_id
try:
item["licence_content"] = self._package_manager.getPackageLicense(item["package_path"])
except EnvironmentError as e:
Logger.error(f"Could not open downloaded package {package_id} to read license file! {type(e)} - {e}")
continue # Skip this package.
if item["licence_content"] is None:
# Implicitly accept when there is no license
item["accepted"] = True
implicitly_accepted_count = implicitly_accepted_count + 1
self._package_models.append(item)
else:
item["accepted"] = None #: None: no answer yet
# When presenting the packages, we want to show packages which have a license first.
# In fact, we don't want to show the others at all because they are implicitly accepted
self._package_models.insert(0, item)
CuraApplication.getInstance().processEvents()
self._page_count = len(self._package_models) - implicitly_accepted_count
self._license_model.setPageCount(self._page_count)
def _presentCurrentPackage(self) -> None:
package_model = self._package_models[self._current_package_idx]
package_info = self._package_manager.getPackageInfo(package_model["package_path"])
self._license_model.setCurrentPageIdx(self._current_package_idx)
self._license_model.setPackageName(package_info["display_name"])
self._license_model.setIconUrl(package_model["icon_url"])
self._license_model.setLicenseText(package_model["licence_content"])
if self._dialog:
self._dialog.open() # Does nothing if already open
def _checkNextPage(self) -> None:
if self._current_package_idx + 1 < self._page_count:
self._current_package_idx += 1
self._presentCurrentPackage()
else:
if self._dialog:
self._dialog.close()
self.licenseAnswers.emit(self._package_models)

View file

@ -0,0 +1,35 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM import i18nCatalog
from UM.Message import Message
from cura.CuraApplication import CuraApplication
class RestartApplicationPresenter:
"""Presents a dialog telling the user that a restart is required to apply changes
Since we cannot restart Cura, the app is closed instead when the button is clicked
"""
def __init__(self, app: CuraApplication) -> None:
self._app = app
self._i18n_catalog = i18nCatalog("cura")
def present(self) -> None:
app_name = self._app.getApplicationDisplayName()
message = Message(self._i18n_catalog.i18nc("@info:generic",
"You need to quit and restart {} before changes have effect.",
app_name))
message.addAction("quit",
name="Quit " + app_name,
icon = "",
description="Close the application",
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)
message.actionTriggered.connect(self._quitClicked)
message.show()
def _quitClicked(self, *_):
self._app.windowClosed()

View file

@ -0,0 +1,74 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import Qt, pyqtProperty
from UM.PackageManager import PackageManager
from UM.Qt.ListModel import ListModel
from UM.Version import Version
from cura import ApplicationMetadata
from typing import List, Dict, Any
class SubscribedPackagesModel(ListModel):
def __init__(self, parent = None):
super().__init__(parent)
self._items = []
self._metadata = None
self._discrepancies = None
self._sdk_version = ApplicationMetadata.CuraSDKVersion
self.addRoleName(Qt.ItemDataRole.UserRole + 1, "package_id")
self.addRoleName(Qt.ItemDataRole.UserRole + 2, "display_name")
self.addRoleName(Qt.ItemDataRole.UserRole + 3, "icon_url")
self.addRoleName(Qt.ItemDataRole.UserRole + 4, "is_compatible")
self.addRoleName(Qt.ItemDataRole.UserRole + 5, "is_dismissed")
@pyqtProperty(bool, constant=True)
def hasCompatiblePackages(self) -> bool:
for item in self._items:
if item['is_compatible']:
return True
return False
@pyqtProperty(bool, constant=True)
def hasIncompatiblePackages(self) -> bool:
for item in self._items:
if not item['is_compatible']:
return True
return False
def addDiscrepancies(self, discrepancy: List[str]) -> None:
self._discrepancies = discrepancy
def getCompatiblePackages(self) -> List[Dict[str, Any]]:
return [package for package in self._items if package["is_compatible"]]
def getIncompatiblePackages(self) -> List[str]:
return [package["package_id"] for package in self._items if not package["is_compatible"]]
def initialize(self, package_manager: PackageManager, subscribed_packages_payload: List[Dict[str, Any]]) -> None:
self._items.clear()
for item in subscribed_packages_payload:
if item["package_id"] not in self._discrepancies:
continue
package = {
"package_id": item["package_id"],
"display_name": item["display_name"],
"sdk_versions": item["sdk_versions"],
"download_url": item["download_url"],
"md5_hash": item["md5_hash"],
"is_dismissed": False,
}
compatible = any(package_manager.isPackageCompatible(Version(version)) for version in item["sdk_versions"])
package.update({"is_compatible": compatible})
try:
package.update({"icon_url": item["icon_url"]})
except KeyError: # There is no 'icon_url" in the response payload for this package
package.update({"icon_url": ""})
self._items.append(package)
self.setItems(self._items)

View file

@ -0,0 +1,114 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from typing import List, Dict, Any, cast
from UM import i18n_catalog
from UM.Extension import Extension
from UM.Logger import Logger
from UM.Message import Message
from UM.PluginRegistry import PluginRegistry
from cura.CuraApplication import CuraApplication
from .CloudPackageChecker import CloudPackageChecker
from .CloudApiClient import CloudApiClient
from .DiscrepanciesPresenter import DiscrepanciesPresenter
from .DownloadPresenter import DownloadPresenter
from .LicensePresenter import LicensePresenter
from .RestartApplicationPresenter import RestartApplicationPresenter
from .SubscribedPackagesModel import SubscribedPackagesModel
class SyncOrchestrator(Extension):
"""Orchestrates the synchronizing of packages from the user account to the installed packages
Example flow:
- CloudPackageChecker compares a list of packages the user `subscribed` to in their account
If there are `discrepancies` between the account and locally installed packages, they are emitted
- DiscrepanciesPresenter shows a list of packages to be added or removed to the user. It emits the `packageMutations`
the user selected to be performed
- The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed
- The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads
- The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to
be installed. It emits the `licenseAnswers` signal for accept or declines
- The CloudApiClient removes the declined packages from the account
- The SyncOrchestrator uses PackageManager to install the downloaded packages and delete temp files.
- The RestartApplicationPresenter notifies the user that a restart is required for changes to take effect
"""
def __init__(self, app: CuraApplication) -> None:
super().__init__()
# Differentiate This PluginObject from the Marketplace. self.getId() includes _name.
# getPluginId() will return the same value for The Marketplace extension and this one
self._name = "SyncOrchestrator"
self._package_manager = app.getPackageManager()
# Keep a reference to the CloudApiClient. it watches for installed packages and subscribes to them
self._cloud_api: CloudApiClient = CloudApiClient.getInstance(app)
self._checker: CloudPackageChecker = CloudPackageChecker(app)
self._checker.discrepancies.connect(self._onDiscrepancies)
self._discrepancies_presenter: DiscrepanciesPresenter = DiscrepanciesPresenter(app)
self._discrepancies_presenter.packageMutations.connect(self._onPackageMutations)
self._download_presenter: DownloadPresenter = DownloadPresenter(app)
self._license_presenter: LicensePresenter = LicensePresenter(app)
self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers)
self._restart_presenter = RestartApplicationPresenter(app)
def _onDiscrepancies(self, model: SubscribedPackagesModel) -> None:
plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId()))
self._discrepancies_presenter.present(plugin_path, model)
def _onPackageMutations(self, mutations: SubscribedPackagesModel) -> None:
self._download_presenter = self._download_presenter.resetCopy()
self._download_presenter.done.connect(self._onDownloadFinished)
self._download_presenter.download(mutations)
def _onDownloadFinished(self, success_items: Dict[str, Dict[str, str]], error_items: List[str]) -> None:
"""Called when a set of packages have finished downloading
:param success_items:: Dict[package_id, Dict[str, str]]
:param error_items:: List[package_id]
"""
if error_items:
message = i18n_catalog.i18nc("@info:generic", "{} plugins failed to download".format(len(error_items)))
self._showErrorMessage(message)
plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId()))
self._license_presenter = self._license_presenter.resetCopy()
self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers)
self._license_presenter.present(plugin_path, success_items)
# Called when user has accepted / declined all licenses for the downloaded packages
def _onLicenseAnswers(self, answers: List[Dict[str, Any]]) -> None:
has_changes = False # True when at least one package is installed
for item in answers:
if item["accepted"]:
# install and subscribe packages
if not self._package_manager.installPackage(item["package_path"]):
message = "Could not install {}".format(item["package_id"])
self._showErrorMessage(message)
continue
has_changes = True
else:
self._cloud_api.unsubscribe(item["package_id"])
# delete temp file
try:
os.remove(item["package_path"])
except EnvironmentError as e: # File was already removed, no access rights, etc.
Logger.error("Can't delete temporary package file: {err}".format(err = str(e)))
if has_changes:
self._restart_presenter.present()
def _showErrorMessage(self, text: str):
"""Logs an error and shows it to the user"""
Logger.error(text)
Message(text, lifetime = 0, message_type = Message.MessageType.ERROR).show()

View file

@ -0,0 +1,67 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from PyQt6.QtCore import QObject, pyqtSignal, pyqtProperty
from typing import Optional, List, Dict, cast, Callable
from cura.CuraApplication import CuraApplication
from UM.PluginRegistry import PluginRegistry
from cura.CuraPackageManager import CuraPackageManager
from UM.i18n import i18nCatalog
from UM.FlameProfiler import pyqtSlot
from .MissingPackageList import MissingPackageList
i18n_catalog = i18nCatalog("cura")
class InstallMissingPackageDialog(QObject):
"""Dialog used to display packages that need to be installed to load 3mf file materials"""
def __init__(self, packages_metadata: List[Dict[str, str]], show_missing_materials_warning: Callable[[], None]) -> None:
"""Initialize
:param packages_metadata: List of dictionaries containing information about missing packages.
"""
super().__init__()
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
self._package_manager.installedPackagesChanged.connect(self.checkIfRestartNeeded)
self._dialog: Optional[QObject] = None
self._restart_needed = False
self._package_metadata: List[Dict[str, str]] = packages_metadata
self._package_model: MissingPackageList = MissingPackageList(packages_metadata)
self._show_missing_materials_warning = show_missing_materials_warning
def show(self) -> None:
plugin_path = self._plugin_registry.getPluginPath("Marketplace")
if plugin_path is None:
plugin_path = os.path.dirname(__file__)
# create a QML component for the license dialog
license_dialog_component_path = os.path.join(plugin_path, "resources", "qml", "InstallMissingPackagesDialog.qml")
self._dialog = CuraApplication.getInstance().createQmlComponent(license_dialog_component_path, {"manager": self})
self._dialog.show()
def checkIfRestartNeeded(self) -> None:
if self._dialog is None:
return
self._restart_needed = self._package_manager.hasPackagesToRemoveOrInstall
self.showRestartChanged.emit()
showRestartChanged = pyqtSignal()
@pyqtProperty(bool, notify=showRestartChanged)
def showRestartNotification(self) -> bool:
return self._restart_needed
@pyqtProperty(QObject)
def model(self) -> MissingPackageList:
return self._package_model
@pyqtSlot()
def showMissingMaterialsWarning(self) -> None:
self._show_missing_materials_warning()

View file

@ -3,7 +3,7 @@
from typing import Any, Dict, List, Optional, TYPE_CHECKING
from PyQt5.QtCore import pyqtSlot, QObject
from PyQt6.QtCore import pyqtSlot, QObject
from UM.Version import Version
from UM.i18n import i18nCatalog
@ -15,8 +15,8 @@ from .PackageModel import PackageModel
from .Constants import PACKAGE_UPDATES_URL
if TYPE_CHECKING:
from PyQt5.QtCore import QObject
from PyQt5.QtNetwork import QNetworkReply
from PyQt6.QtCore import QObject
from PyQt6.QtNetwork import QNetworkReply
catalog = i18nCatalog("cura")
@ -108,7 +108,7 @@ class LocalPackageList(PackageList):
:param reply: A reply containing information about a number of packages.
"""
response_data = HttpRequestManager.readJSON(reply)
if "data" not in response_data:
if response_data is None or "data" not in response_data:
Logger.error(
f"Could not interpret the server's response. Missing 'data' from response data. Keys in response: {response_data.keys()}")
return

View file

@ -2,16 +2,16 @@
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from typing import Optional, cast
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from typing import Callable, cast, Dict, List, Optional
from cura.CuraApplication import CuraApplication # Creating QML objects and managing packages.
from UM.Extension import Extension # We are implementing the main object of an extension here.
from UM.PluginRegistry import PluginRegistry # To find out where we are stored (the proper way).
from .RemotePackageList import RemotePackageList # To register this type with QML.
from .InstallMissingPackagesDialog import InstallMissingPackageDialog # To allow creating this dialogue from outside of the plug-in.
from .LocalPackageList import LocalPackageList # To register this type with QML.
from .RemotePackageList import RemotePackageList # To register this type with QML.
class Marketplace(Extension, QObject):
@ -22,7 +22,6 @@ class Marketplace(Extension, QObject):
QObject.__init__(self, parent)
Extension.__init__(self)
self._window: Optional["QObject"] = None # If the window has been loaded yet, it'll be cached in here.
self._plugin_registry: Optional[PluginRegistry] = None
self._package_manager = CuraApplication.getInstance().getPackageManager()
self._material_package_list: Optional[RemotePackageList] = None
@ -41,6 +40,7 @@ class Marketplace(Extension, QObject):
self._tab_shown: int = 0
self._restart_needed = False
self.missingPackageDialog = None
def getTabShown(self) -> int:
return self._tab_shown
@ -80,9 +80,9 @@ class Marketplace(Extension, QObject):
If the window hadn't been loaded yet into Qt, it will be created lazily.
"""
if self._window is None:
self._plugin_registry = PluginRegistry.getInstance()
self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded)
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
plugin_registry = PluginRegistry.getInstance()
plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded)
plugin_path = plugin_registry.getPluginPath(self.getPluginId())
if plugin_path is None:
plugin_path = os.path.dirname(__file__)
path = os.path.join(plugin_path, "resources", "qml", "Marketplace.qml")
@ -103,8 +103,11 @@ class Marketplace(Extension, QObject):
self.setTabShown(1)
def checkIfRestartNeeded(self) -> None:
if self._window is None:
return
if self._package_manager.hasPackagesToRemoveOrInstall or \
cast(PluginRegistry, self._plugin_registry).getCurrentSessionActivationChangedPlugins():
PluginRegistry.getInstance().getCurrentSessionActivationChangedPlugins():
self._restart_needed = True
else:
self._restart_needed = False
@ -112,6 +115,18 @@ class Marketplace(Extension, QObject):
showRestartNotificationChanged = pyqtSignal()
@pyqtProperty(bool, notify=showRestartNotificationChanged)
@pyqtProperty(bool, notify = showRestartNotificationChanged)
def showRestartNotification(self) -> bool:
return self._restart_needed
def showInstallMissingPackageDialog(self, packages_metadata: List[Dict[str, str]], ignore_warning_callback: Callable[[], None]) -> None:
"""
Show a dialog that prompts the user to install certain packages.
The dialog is worded for packages that are missing and required for a certain operation.
:param packages_metadata: The metadata of the packages that are missing.
:param ignore_warning_callback: A callback that gets executed when the user ignores the pop-up, to show them a
warning.
"""
self.missingPackageDialog = InstallMissingPackageDialog(packages_metadata, ignore_warning_callback)
self.missingPackageDialog.show()

View file

@ -0,0 +1,46 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, TYPE_CHECKING, Dict, List
from .Constants import PACKAGES_URL
from .PackageModel import PackageModel
from .RemotePackageList import RemotePackageList
from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To request the package list from the API.
from UM.i18n import i18nCatalog
if TYPE_CHECKING:
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal
catalog = i18nCatalog("cura")
class MissingPackageList(RemotePackageList):
def __init__(self, packages_metadata: List[Dict[str, str]], parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._packages_metadata: List[Dict[str, str]] = packages_metadata
self._package_type_filter = "material"
self._search_type = "package_ids"
self._requested_search_string = ",".join(map(lambda package: package["id"], packages_metadata))
def _parseResponse(self, reply: "QNetworkReply") -> None:
super()._parseResponse(reply)
# At the end of the list we want to show some information about packages the user is missing that can't be found
# This will add cards with some information about the missing packages
if not self.hasMore:
self._addPackagesMissingFromRequest()
def _addPackagesMissingFromRequest(self) -> None:
"""Create cards for packages the user needs to install that could not be found"""
returned_packages_ids = [item["package"].packageId for item in self._items]
for package_metadata in self._packages_metadata:
if package_metadata["id"] not in returned_packages_ids:
package = PackageModel.fromIncompletePackageInformation(package_metadata["display_name"], package_metadata["package_version"], self._package_type_filter)
self.appendItem({"package": package})
self.itemsChanged.emit()

View file

@ -4,7 +4,7 @@ import tempfile
import json
import os.path
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt
from typing import cast, Dict, Optional, Set, TYPE_CHECKING
from UM.i18n import i18nCatalog
@ -22,8 +22,8 @@ from .PackageModel import PackageModel
from .Constants import USER_PACKAGES_URL, PACKAGES_URL
if TYPE_CHECKING:
from PyQt5.QtCore import QObject
from PyQt5.QtNetwork import QNetworkReply
from PyQt6.QtCore import QObject
from PyQt6.QtNetwork import QNetworkReply
catalog = i18nCatalog("cura")
@ -32,7 +32,7 @@ class PackageList(ListModel):
""" A List model for Packages, this class serves as parent class for more detailed implementations.
such as Packages obtained from Remote or Local source
"""
PackageRole = Qt.UserRole + 1
PackageRole = Qt.ItemDataRole.UserRole + 1
DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB
def __init__(self, parent: Optional["QObject"] = None) -> None:
@ -244,7 +244,10 @@ class PackageList(ListModel):
def _downloadError(self, package_id: str, update: bool = False, reply: Optional["QNetworkReply"] = None, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if reply:
reply_string = bytes(reply.readAll()).decode()
try:
reply_string = bytes(reply.readAll()).decode()
except UnicodeDecodeError:
reply_string = "<error message is corrupt too>"
Logger.error(f"Failed to download package: {package_id} due to {reply_string}")
self._package_manager.packageInstallingFailed.emit(package_id)

View file

@ -5,8 +5,8 @@ import re
from enum import Enum
from typing import Any, cast, Dict, List, Optional
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal, pyqtSlot
from PyQt5.QtQml import QQmlEngine
from PyQt6.QtCore import pyqtProperty, QObject, pyqtSignal, pyqtSlot
from PyQt6.QtQml import QQmlEngine
from cura.CuraApplication import CuraApplication
from cura.CuraPackageManager import CuraPackageManager
@ -31,7 +31,7 @@ class PackageModel(QObject):
:param parent: The parent QML object that controls the lifetime of this model (normally a PackageList).
"""
super().__init__(parent)
QQmlEngine.setObjectOwnership(self, QQmlEngine.CppOwnership)
QQmlEngine.setObjectOwnership(self, QQmlEngine.ObjectOwnership.CppOwnership)
self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
@ -84,6 +84,20 @@ class PackageModel(QObject):
self._is_busy = False
self._is_missing_package_information = False
@classmethod
def fromIncompletePackageInformation(cls, display_name: str, package_version: str, package_type: str) -> "PackageModel":
package_data = {
"display_name": display_name,
"package_version": package_version,
"package_type": package_type,
"description": "The material package associated with the Cura project could not be found on the Ultimaker marketplace. Use the partial material profile definition stored in the Cura project file at your own risk."
}
package_model = cls(package_data)
package_model.setIsMissingPackageInformation(True)
return package_model
@pyqtSlot()
def _processUpdatedPackages(self):
self.setCanUpdate(self._package_manager.checkIfPackageCanUpdate(self._package_id))
@ -385,3 +399,14 @@ class PackageModel(QObject):
def canUpdate(self) -> bool:
"""Flag indicating if the package can be updated"""
return self._can_update
isMissingPackageInformationChanged = pyqtSignal()
def setIsMissingPackageInformation(self, isMissingPackageInformation: bool) -> None:
self._is_missing_package_information = isMissingPackageInformation
self.isMissingPackageInformationChanged.emit()
@pyqtProperty(bool, notify=isMissingPackageInformationChanged)
def isMissingPackageInformation(self) -> bool:
"""Flag indicating if the package can be updated"""
return self._is_missing_package_information

View file

@ -1,8 +1,8 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtNetwork import QNetworkReply
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot
from PyQt6.QtNetwork import QNetworkReply
from typing import Optional, TYPE_CHECKING
from UM.i18n import i18nCatalog
@ -14,7 +14,7 @@ from .PackageList import PackageList
from .PackageModel import PackageModel # The contents of this list.
if TYPE_CHECKING:
from PyQt5.QtCore import QObject
from PyQt6.QtCore import QObject
catalog = i18nCatalog("cura")
@ -28,6 +28,7 @@ class RemotePackageList(PackageList):
self._package_type_filter = ""
self._requested_search_string = ""
self._current_search_string = ""
self._search_type = "search"
self._request_url = self._initialRequestUrl()
self._ongoing_requests["get_packages"] = None
self.isLoadingChanged.connect(self._onLoadingChanged)
@ -100,7 +101,7 @@ class RemotePackageList(PackageList):
if self._package_type_filter != "":
request_url += f"&package_type={self._package_type_filter}"
if self._current_search_string != "":
request_url += f"&search={self._current_search_string}"
request_url += f"&{self._search_type}={self._current_search_string}"
return request_url
def _parseResponse(self, reply: "QNetworkReply") -> None:
@ -138,9 +139,10 @@ class RemotePackageList(PackageList):
:param reply: The reply with packages. This will most likely be incomplete and should be ignored.
:param error: The error status of the request.
"""
if error == QNetworkReply.NetworkError.OperationCanceledError:
if error == QNetworkReply.NetworkError.OperationCanceledError or error == QNetworkReply.NetworkError.ProtocolUnknownError:
Logger.debug("Cancelled request for packages.")
self._ongoing_requests["get_packages"] = None
self.setIsLoading(False)
return # Don't show an error about this to the user.
Logger.error("Could not reach Marketplace server.")
self.setErrorMessage(catalog.i18nc("@info:error", "Could not reach Marketplace."))

View file

@ -1,6 +1,6 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from .CloudSync.SyncOrchestrator import SyncOrchestrator
from .Marketplace import Marketplace
def getMetaData():
@ -14,4 +14,4 @@ def register(app):
"""
Register the plug-in object with Uranium.
"""
return { "extension": Marketplace() }
return { "extension": [SyncOrchestrator(app), Marketplace()] }

View file

@ -2,7 +2,7 @@
"name": "Marketplace",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"api": 7,
"api": 8,
"description": "Manages extensions to the application and allows browsing extensions from the Ultimaker website.",
"i18n-catalog": "cura"
}

View file

@ -1,3 +1,3 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M39 14V8H27V14H21V8H9V14H7C6.20435 14 5.44129 14.3161 4.87868 14.8787C4.31607 15.4413 4 16.2044 4 17V37C4 37.7956 4.31607 38.5587 4.87868 39.1213C5.44129 39.6839 6.20435 40 7 40H41C41.7957 40 42.5587 39.6839 43.1213 39.1213C43.6839 38.5587 44 37.7956 44 37V17C44 16.2044 43.6839 15.4413 43.1213 14.8787C42.5587 14.3161 41.7957 14 41 14H39ZM29 10H37V14H29V10ZM11 10H19V14H11V10ZM42 38H6V16H42V38Z" fill="#000E1A"/>
<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path d="M39 14V8H27V14H21V8H9V14H7C6.20435 14 5.44129 14.3161 4.87868 14.8787C4.31607 15.4413 4 16.2044 4 17V37C4 37.7956 4.31607 38.5587 4.87868 39.1213C5.44129 39.6839 6.20435 40 7 40H41C41.7957 40 42.5587 39.6839 43.1213 39.1213C43.6839 38.5587 44 37.7956 44 37V17C44 16.2044 43.6839 15.4413 43.1213 14.8787C42.5587 14.3161 41.7957 14 41 14H39ZM29 10H37V14H29V10ZM11 10H19V14H11V10ZM42 38H6V16H42V38Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 526 B

After

Width:  |  Height:  |  Size: 499 B

Before After
Before After

View file

@ -1,3 +1,3 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M24 4C18.6975 4.00609 13.614 6.11518 9.86459 9.86459C6.11518 13.614 4.00609 18.6975 4 24V42H6V32.66C7.54979 35.8792 9.93405 38.6241 12.9046 40.6092C15.8752 42.5942 19.3236 43.7468 22.8908 43.947C26.4579 44.1472 30.0136 43.3876 33.1876 41.7474C36.3616 40.1071 39.038 37.6462 40.9382 34.6206C42.8385 31.595 43.893 28.1155 43.9922 24.544C44.0914 20.9726 43.2315 17.4399 41.5021 14.3136C39.7727 11.1872 37.237 8.5815 34.1589 6.76765C31.0808 4.9538 27.5728 3.99809 24 4ZM24 42C20.4399 42 16.9598 40.9443 13.9997 38.9665C11.0397 36.9886 8.73255 34.1774 7.37017 30.8883C6.00779 27.5992 5.65133 23.98 6.34586 20.4884C7.0404 16.9967 8.75473 13.7894 11.2721 11.2721C13.7894 8.75473 16.9967 7.0404 20.4884 6.34587C23.98 5.65133 27.5992 6.00779 30.8883 7.37017C34.1774 8.73255 36.9886 11.0397 38.9665 13.9997C40.9443 16.9598 42 20.4399 42 24C41.9947 28.7723 40.0966 33.3476 36.7221 36.7221C33.3476 40.0966 28.7723 41.9947 24 42ZM24 17C22.6155 17 21.2622 17.4105 20.111 18.1797C18.9599 18.9489 18.0627 20.0421 17.5328 21.3212C17.003 22.6003 16.8644 24.0078 17.1345 25.3656C17.4046 26.7235 18.0713 27.9708 19.0503 28.9498C20.0292 29.9287 21.2765 30.5954 22.6344 30.8655C23.9922 31.1356 25.3997 30.997 26.6788 30.4672C27.9579 29.9373 29.0511 29.0401 29.8203 27.889C30.5895 26.7379 31 25.3845 31 24C30.9979 22.1441 30.2598 20.3648 28.9475 19.0525C27.6352 17.7402 25.8559 17.0021 24 17ZM24 29C23.0111 29 22.0444 28.7068 21.2221 28.1574C20.3999 27.6079 19.759 26.8271 19.3806 25.9134C19.0022 24.9998 18.9031 23.9945 19.0961 23.0246C19.289 22.0546 19.7652 21.1637 20.4645 20.4645C21.1637 19.7652 22.0546 19.289 23.0245 19.0961C23.9945 18.9031 24.9998 19.0022 25.9134 19.3806C26.827 19.759 27.6079 20.3999 28.1573 21.2222C28.7068 22.0444 29 23.0111 29 24C28.9984 25.3256 28.4712 26.5965 27.5338 27.5338C26.5965 28.4712 25.3256 28.9984 24 29ZM24 11C25.7079 10.9954 27.3997 11.3295 28.9776 11.9831C30.5554 12.6367 31.988 13.5967 33.1924 14.8076L31.7783 16.2217C30.7592 15.1971 29.547 14.3848 28.2118 13.8318C26.8767 13.2788 25.4451 12.9961 24 13V11Z" fill="#000E1A"/>
<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
<path d="M24 4C18.6975 4.00609 13.614 6.11518 9.86459 9.86459C6.11518 13.614 4.00609 18.6975 4 24V42H6V32.66C7.54979 35.8792 9.93405 38.6241 12.9046 40.6092C15.8752 42.5942 19.3236 43.7468 22.8908 43.947C26.4579 44.1472 30.0136 43.3876 33.1876 41.7474C36.3616 40.1071 39.038 37.6462 40.9382 34.6206C42.8385 31.595 43.893 28.1155 43.9922 24.544C44.0914 20.9726 43.2315 17.4399 41.5021 14.3136C39.7727 11.1872 37.237 8.5815 34.1589 6.76765C31.0808 4.9538 27.5728 3.99809 24 4ZM24 42C20.4399 42 16.9598 40.9443 13.9997 38.9665C11.0397 36.9886 8.73255 34.1774 7.37017 30.8883C6.00779 27.5992 5.65133 23.98 6.34586 20.4884C7.0404 16.9967 8.75473 13.7894 11.2721 11.2721C13.7894 8.75473 16.9967 7.0404 20.4884 6.34587C23.98 5.65133 27.5992 6.00779 30.8883 7.37017C34.1774 8.73255 36.9886 11.0397 38.9665 13.9997C40.9443 16.9598 42 20.4399 42 24C41.9947 28.7723 40.0966 33.3476 36.7221 36.7221C33.3476 40.0966 28.7723 41.9947 24 42ZM24 17C22.6155 17 21.2622 17.4105 20.111 18.1797C18.9599 18.9489 18.0627 20.0421 17.5328 21.3212C17.003 22.6003 16.8644 24.0078 17.1345 25.3656C17.4046 26.7235 18.0713 27.9708 19.0503 28.9498C20.0292 29.9287 21.2765 30.5954 22.6344 30.8655C23.9922 31.1356 25.3997 30.997 26.6788 30.4672C27.9579 29.9373 29.0511 29.0401 29.8203 27.889C30.5895 26.7379 31 25.3845 31 24C30.9979 22.1441 30.2598 20.3648 28.9475 19.0525C27.6352 17.7402 25.8559 17.0021 24 17ZM24 29C23.0111 29 22.0444 28.7068 21.2221 28.1574C20.3999 27.6079 19.759 26.8271 19.3806 25.9134C19.0022 24.9998 18.9031 23.9945 19.0961 23.0246C19.289 22.0546 19.7652 21.1637 20.4645 20.4645C21.1637 19.7652 22.0546 19.289 23.0245 19.0961C23.9945 18.9031 24.9998 19.0022 25.9134 19.3806C26.827 19.759 27.6079 20.3999 28.1573 21.2222C28.7068 22.0444 29 23.0111 29 24C28.9984 25.3256 28.4712 26.5965 27.5338 27.5338C26.5965 28.4712 25.3256 28.9984 24 29ZM24 11C25.7079 10.9954 27.3997 11.3295 28.9776 11.9831C30.5554 12.6367 31.988 13.5967 33.1924 14.8076L31.7783 16.2217C30.7592 15.1971 29.547 14.3848 28.2118 13.8318C26.8767 13.2788 25.4451 12.9961 24 13V11Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before After
Before After

View file

@ -0,0 +1,146 @@
// Copyright (c) 2022 Ultimaker B.V.
// Marketplace is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import UM 1.5 as UM
import Cura 1.6 as Cura
UM.Dialog
{
visible: true
title: catalog.i18nc("@title", "Changes from your account")
width: UM.Theme.getSize("popup_dialog").width
height: UM.Theme.getSize("popup_dialog").height
minimumWidth: width
maximumWidth: minimumWidth
minimumHeight: height
maximumHeight: minimumHeight
margin: 0
property string actionButtonText: subscribedPackagesModel.hasIncompatiblePackages && !subscribedPackagesModel.hasCompatiblePackages ? catalog.i18nc("@button", "Dismiss") : catalog.i18nc("@button", "Next")
Rectangle
{
id: root
anchors.fill: parent
color: UM.Theme.getColor("main_background")
UM.I18nCatalog
{
id: catalog
name: "cura"
}
ScrollView
{
width: parent.width
height: parent.height - nextButton.height - nextButton.anchors.margins * 2 // We want some leftover space for the button at the bottom
clip: true
Column
{
anchors.fill: parent
anchors.margins: UM.Theme.getSize("default_margin").width
// Compatible packages
UM.Label
{
text: catalog.i18nc("@label", "The following packages will be added:")
visible: subscribedPackagesModel.hasCompatiblePackages
height: contentHeight + UM.Theme.getSize("default_margin").height
}
Repeater
{
model: subscribedPackagesModel
Component
{
Item
{
width: parent.width
property int lineHeight: 60
visible: model.is_compatible
height: visible ? (lineHeight + UM.Theme.getSize("default_margin").height) : 0 // We only show the compatible packages here
Image
{
id: packageIcon
source: model.icon_url || Qt.resolvedUrl("../images/placeholder.svg")
height: lineHeight
width: height
sourceSize.height: height
sourceSize.width: width
mipmap: true
fillMode: Image.PreserveAspectFit
}
UM.Label
{
text: model.display_name
font: UM.Theme.getFont("medium_bold")
anchors.left: packageIcon.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: packageIcon.verticalCenter
elide: Text.ElideRight
}
}
}
}
// Incompatible packages
UM.Label
{
text: catalog.i18nc("@label", "The following packages can not be installed because of an incompatible Cura version:")
visible: subscribedPackagesModel.hasIncompatiblePackages
height: contentHeight + UM.Theme.getSize("default_margin").height
}
Repeater
{
model: subscribedPackagesModel
Component
{
Item
{
width: parent.width
property int lineHeight: 60
visible: !model.is_compatible && !model.is_dismissed
height: visible ? (lineHeight + UM.Theme.getSize("default_margin").height) : 0 // We only show the incompatible packages here
Image
{
id: packageIcon
source: model.icon_url || Qt.resolvedUrl("../images/placeholder.svg")
height: lineHeight
width: height
sourceSize.height: height
sourceSize.width: width
mipmap: true
fillMode: Image.PreserveAspectFit
}
UM.Label
{
text: model.display_name
font: UM.Theme.getFont("medium_bold")
anchors.left: packageIcon.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: packageIcon.verticalCenter
elide: Text.ElideRight
}
}
}
}
}
} // End of ScrollView
Cura.PrimaryButton
{
id: nextButton
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").height
text: actionButtonText
onClicked: accept()
}
}
}

View file

@ -0,0 +1,21 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.2
import UM 1.5 as UM
import Cura 1.6 as Cura
Marketplace
{
modality: Qt.ApplicationModal
title: catalog.i18nc("@title", "Install missing Materials")
pageContentsSource: "MissingPackages.qml"
showSearchHeader: false
showOnboadBanner: false
onClosing: manager.showMissingMaterialsWarning()
}

View file

@ -2,7 +2,6 @@
//Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
@ -34,7 +33,7 @@ UM.Dialog
spacing: UM.Theme.getSize("default_margin").width
leftPadding: UM.Theme.getSize("narrow_margin").width
UM.RecolorImage
UM.ColorImage
{
id: icon
width: UM.Theme.getSize("marketplace_large_icon").width
@ -43,16 +42,13 @@ UM.Dialog
source: UM.Theme.getIcon("Certificate", "high")
}
Label
UM.Label
{
text: catalog.i18nc("@text", "Please read and agree with the plugin licence.")
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("large")
anchors.verticalCenter: icon.verticalCenter
height: UM.Theme.getSize("marketplace_large_icon").height
verticalAlignment: Qt.AlignVCenter
wrapMode: Text.Wrap
renderType: Text.NativeRendering
}
}

View file

@ -46,7 +46,7 @@ Item
height: UM.Theme.getSize("action_button").height
width: childrenRect.width
UM.RecolorImage
UM.ColorImage
{
id: busyIndicator
visible: parent.visible

View file

@ -33,7 +33,7 @@ TabButton
visible: root.hovered
}
UM.RecolorImage
UM.ColorImage
{
id: icon

View file

@ -6,7 +6,7 @@ import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.2
import UM 1.2 as UM
import UM 1.5 as UM
import Cura 1.6 as Cura
Window
@ -16,6 +16,10 @@ Window
signal searchStringChanged(string new_search)
property alias showOnboadBanner: onBoardBanner.visible
property alias showSearchHeader: searchHeader.visible
property alias pageContentsSource: content.source
minimumWidth: UM.Theme.getSize("modal_window_minimum").width
minimumHeight: UM.Theme.getSize("modal_window_minimum").height
width: minimumWidth
@ -67,7 +71,7 @@ Window
Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height + UM.Theme.getSize("default_margin").height
Label
UM.Label
{
id: pageTitle
anchors
@ -80,13 +84,13 @@ Window
}
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
text: content.item ? content.item.pageTitle: catalog.i18nc("@title", "Loading...")
}
}
OnboardBanner
{
id: onBoardBanner
visible: content.item && content.item.bannerVisible
text: content.item && content.item.bannerText
icon: content.item && content.item.bannerIcon
@ -101,6 +105,7 @@ Window
// Search & Top-Level Tabs
Item
{
id: searchHeader
implicitHeight: childrenRect.height
implicitWidth: parent.width - 2 * UM.Theme.getSize("default_margin").width
Layout.alignment: Qt.AlignHCenter
@ -187,7 +192,7 @@ Window
{
text: catalog.i18nc("@info", "Search in the browser")
iconSource: UM.Theme.getIcon("LinkExternal")
visible: pageSelectionTabBar.currentItem.hasSearch
visible: pageSelectionTabBar.currentItem.hasSearch && searchHeader.visible
isIconOnRightSide: true
height: fontMetrics.height
textFont: fontMetrics.font
@ -251,7 +256,7 @@ Window
margins: UM.Theme.getSize("default_margin").width
}
spacing: UM.Theme.getSize("default_margin").width
UM.RecolorImage
UM.ColorImage
{
id: bannerIcon
source: UM.Theme.getIcon("Plugin")

View file

@ -0,0 +1,15 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import UM 1.4 as UM
Packages
{
pageTitle: catalog.i18nc("@header", "Install Materials")
bannerVisible: false
showUpdateButton: false
showInstallButton: true
model: manager.model
}

View file

@ -0,0 +1,98 @@
// Copyright (c) 2021 Ultimaker B.V.
// Marketplace is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import UM 1.5 as UM
import Cura 1.6 as Cura
UM.Dialog
{
id: licenseDialog
title: licenseModel.dialogTitle
minimumWidth: UM.Theme.getSize("modal_window_minimum").width
minimumHeight: UM.Theme.getSize("modal_window_minimum").height
width: minimumWidth
height: minimumHeight
backgroundColor: UM.Theme.getColor("main_background")
margin: UM.Theme.getSize("default_margin").width
ColumnLayout
{
anchors.fill: parent
spacing: UM.Theme.getSize("thick_margin").height
UM.I18nCatalog { id: catalog; name: "cura" }
UM.Label
{
id: licenseHeader
Layout.fillWidth: true
text: catalog.i18nc("@label", "You need to accept the license to install the package")
}
Row {
id: packageRow
Layout.fillWidth: true
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
leftPadding: UM.Theme.getSize("narrow_margin").width
Image
{
id: icon
width: UM.Theme.getSize("card_icon").width
height: width
sourceSize.width: width
sourceSize.height: height
fillMode: Image.PreserveAspectFit
source: licenseModel.iconUrl || Qt.resolvedUrl("../images/placeholder.svg")
mipmap: true
}
UM.Label
{
id: packageName
text: licenseModel.packageName
font.bold: true
anchors.verticalCenter: icon.verticalCenter
height: contentHeight
}
}
Cura.ScrollableTextArea
{
Layout.fillWidth: true
Layout.fillHeight: true
anchors.topMargin: UM.Theme.getSize("default_margin").height
textArea.text: licenseModel.licenseText
textArea.readOnly: true
}
}
rightButtons:
[
Cura.PrimaryButton
{
text: licenseModel.acceptButtonText
onClicked: handler.onLicenseAccepted()
}
]
leftButtons:
[
Cura.SecondaryButton
{
id: declineButton
text: licenseModel.declineButtonText
onClicked: handler.onLicenseDeclined()
}
]
}

View file

@ -19,8 +19,7 @@ Rectangle
implicitHeight: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height
color: UM.Theme.getColor("action_panel_secondary")
// Icon
UM.RecolorImage
UM.ColorImage
{
id: onboardingIcon
anchors
@ -31,9 +30,10 @@ Rectangle
}
width: UM.Theme.getSize("banner_icon_size").width
height: UM.Theme.getSize("banner_icon_size").height
color: UM.Theme.getColor("primary_text")
}
// Close button
UM.SimpleButton
{
id: onboardingClose
@ -52,8 +52,8 @@ Rectangle
onClicked: onRemove()
}
// Body
Label {
UM.Label
{
id: infoText
anchors
{
@ -63,14 +63,10 @@ Rectangle
margins: UM.Theme.getSize("default_margin").width
}
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
color: UM.Theme.getColor("primary_text")
wrapMode: Text.Wrap
elide: Text.ElideRight
onLineLaidOut:
onLineLaidOut: (line) =>
{
if(line.isLast)
{
@ -102,7 +98,7 @@ Rectangle
id: readMoreButton
anchors.left: infoText.left
anchors.bottom: infoText.bottom
text: "Learn More"
text: catalog.i18nc("@button:label", "Learn More")
textFont: UM.Theme.getFont("default")
textColor: infoText.color
leftPadding: 0

View file

@ -18,6 +18,8 @@ Rectangle
height: childrenRect.height
color: UM.Theme.getColor("main_background")
radius: UM.Theme.getSize("default_radius").width
border.color: packageData.isMissingPackageInformation ? UM.Theme.getColor("warning") : "transparent"
border.width: packageData.isMissingPackageInformation ? UM.Theme.getSize("default_lining").width : 0
PackageCardHeader
{
@ -29,16 +31,13 @@ Rectangle
anchors.fill: parent
Label
UM.Label
{
id: descriptionLabel
width: parent.width
text: packageData.description
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
maximumLineCount: 2
wrapMode: Text.Wrap
elide: Text.ElideRight
visible: text !== ""
}

View file

@ -19,6 +19,8 @@ Item
property bool showInstallButton: false
property bool showUpdateButton: false
property string missingPackageReadMoreUrl: "https://support.ultimaker.com/hc/en-us/articles/360011968360-Using-the-Ultimaker-Marketplace?utm_source=cura&utm_medium=software&utm_campaign=load-file-material-missing"
width: parent.width
height: UM.Theme.getSize("card").height
@ -47,23 +49,21 @@ Item
sourceSize.width: width
}
UM.RecolorImage
UM.ColorImage
{
visible: !parent.packageHasIcon
anchors.fill: parent
sourceSize.height: height
sourceSize.width: width
color: UM.Theme.getColor("text")
source:
{
switch (packageData.packageType)
{
case "plugin":
return "../images/Plugin.svg";
return Qt.resolvedUrl("../images/Plugin.svg");
case "material":
return "../images/Spool.svg";
return Qt.resolvedUrl("../images/Spool.svg");
default:
return "../images/placeholder.svg";
return Qt.resolvedUrl("../images/placeholder.svg");
}
}
}
@ -89,11 +89,18 @@ Item
Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height
Label
UM.StatusIcon
{
width: UM.Theme.getSize("section_icon").width + UM.Theme.getSize("narrow_margin").width
height: UM.Theme.getSize("section_icon").height
status: UM.StatusIcon.Status.WARNING
visible: packageData.isMissingPackageInformation
}
UM.Label
{
text: packageData.displayName
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignTop
}
VerifiedIcon
@ -102,18 +109,17 @@ Item
visible: packageData.isCheckedByUltimaker
}
Label
UM.Label
{
id: packageVersionLabel
text: packageData.packageVersion
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
Layout.fillWidth: true
}
Button
{
id: externalLinkButton
visible: !packageData.isMissingPackageInformation
// For some reason if i set padding, they don't match up. If i set all of them explicitly, it does work?
leftPadding: UM.Theme.getSize("narrow_margin").width
@ -121,9 +127,9 @@ Item
topPadding: UM.Theme.getSize("narrow_margin").width
bottomPadding: UM.Theme.getSize("narrow_margin").width
Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
contentItem: UM.RecolorImage
width: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
height: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
contentItem: UM.ColorImage
{
source: UM.Theme.getIcon("LinkExternal")
color: UM.Theme.getColor("icon")
@ -157,12 +163,13 @@ Item
spacing: UM.Theme.getSize("narrow_margin").width
// label "By"
Label
UM.Label
{
id: authorBy
visible: !packageData.isMissingPackageInformation
Layout.alignment: Qt.AlignCenter
text: catalog.i18nc("@label", "By")
text: catalog.i18nc("@label Is followed by the name of an author", "By")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
}
@ -170,6 +177,7 @@ Item
// clickable author name
Item
{
visible: !packageData.isMissingPackageInformation
Layout.fillWidth: true
implicitHeight: authorBy.height
Layout.alignment: Qt.AlignTop
@ -187,10 +195,29 @@ Item
}
}
Item
{
visible: packageData.isMissingPackageInformation
Layout.fillWidth: true
implicitHeight: readMoreButton.height
Layout.alignment: Qt.AlignTop
Cura.TertiaryButton
{
id: readMoreButton
text: catalog.i18nc("@button:label", "Learn More")
leftPadding: 0
rightPadding: 0
iconSource: UM.Theme.getIcon("LinkExternal")
isIconOnRightSide: true
onClicked: Qt.openUrlExternally(missingPackageReadMoreUrl)
}
}
ManageButton
{
id: enableManageButton
visible: showDisableButton && packageData.isInstalled && !packageData.isToBeInstalled && packageData.packageType != "material"
visible: showDisableButton && packageData.isInstalled && !packageData.isToBeInstalled && packageData.packageType != "material" && !packageData.isMissingPackageInformation
enabled: !packageData.busy
button_style: !packageData.isActive
@ -204,7 +231,7 @@ Item
ManageButton
{
id: installManageButton
visible: showInstallButton && (packageData.canDowngrade || !packageData.isBundled)
visible: showInstallButton && (packageData.canDowngrade || !packageData.isBundled) && !packageData.isMissingPackageInformation
enabled: !packageData.busy
busy: packageData.busy
button_style: !(packageData.isInstalled || packageData.isToBeInstalled)
@ -234,7 +261,7 @@ Item
ManageButton
{
id: updateManageButton
visible: showUpdateButton && packageData.canUpdate
visible: showUpdateButton && packageData.canUpdate && !packageData.isMissingPackageInformation
enabled: !packageData.busy
busy: packageData.busy
Layout.alignment: Qt.AlignTop

View file

@ -45,14 +45,13 @@ Item
iconSize: height - leftPadding * 2
}
Label
UM.Label
{
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
text: detailPage.title
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
}
}
@ -93,4 +92,4 @@ Item
}
}
}
}
}

View file

@ -46,7 +46,7 @@ Rectangle
// But we re-use the package page for the manage plugins as well. The one user that doesn't see
// the num downloads is an acceptable "sacrifice" to make this easy to fix.
visible: packageData.downloadCount != "0"
UM.RecolorImage
UM.ColorImage
{
id: downloadsIcon
width: UM.Theme.getSize("card_tiny_icon").width
@ -56,13 +56,9 @@ Rectangle
color: UM.Theme.getColor("text")
}
Label
UM.Label
{
anchors.verticalCenter: downloadsIcon.verticalCenter
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
text: packageData.downloadCount
}
}
@ -78,25 +74,22 @@ Rectangle
topPadding: 0
spacing: UM.Theme.getSize("default_margin").height
Label
UM.Label
{
width: parent.width - parent.padding * 2
text: catalog.i18nc("@header", "Description")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
UM.Label
{
width: parent.width - parent.padding * 2
text: packageData.formattedDescription
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
wrapMode: Text.Wrap
textFormat: Text.RichText
onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
@ -110,13 +103,12 @@ Rectangle
visible: packageData.packageType === "material"
spacing: 0
Label
UM.Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible printers")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
@ -124,25 +116,23 @@ Rectangle
{
model: packageData.compatiblePrinters
Label
UM.Label
{
width: compatiblePrinterColumn.width
text: modelData
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Label
UM.Label
{
width: parent.width
visible: packageData.compatiblePrinters.length == 0
text: "(" + catalog.i18nc("@info", "No compatibility information") + ")"
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
@ -155,13 +145,12 @@ Rectangle
visible: packageData.packageType === "material"
spacing: 0
Label
UM.Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible support materials")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
@ -169,25 +158,23 @@ Rectangle
{
model: packageData.compatibleSupportMaterials
Label
UM.Label
{
width: compatibleSupportMaterialColumn.width
text: modelData
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Label
UM.Label
{
width: parent.width
visible: packageData.compatibleSupportMaterials.length == 0
text: "(" + catalog.i18nc("@info No materials", "None") + ")"
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
@ -199,23 +186,21 @@ Rectangle
visible: packageData.packageType === "material"
spacing: 0
Label
UM.Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible with Material Station")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
UM.Label
{
width: parent.width
text: packageData.isCompatibleMaterialStation ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
@ -227,23 +212,21 @@ Rectangle
visible: packageData.packageType === "material"
spacing: 0
Label
UM.Label
{
width: parent.width
text: catalog.i18nc("@header", "Optimized for Air Manager")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
UM.Label
{
width: parent.width
text: packageData.isCompatibleAirManager ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}

View file

@ -3,7 +3,7 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import UM 1.0 as UM
import UM 1.5 as UM
TabButton
{
@ -22,11 +22,10 @@ TabButton
border.width: UM.Theme.getSize("thick_lining").width
}
contentItem: Label
contentItem: UM.Label
{
text: parent.text
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
width: contentWidth
anchors.centerIn: parent
}

View file

@ -1,9 +1,10 @@
// Copyright (c) 2021 Ultimaker B.V.
// Copyright (c) 2022 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import UM 1.4 as UM
import UM 1.6 as UM
ListView
@ -39,7 +40,7 @@ ListView
color: UM.Theme.getColor("detail_background")
Label
UM.Label
{
id: sectionHeaderText
anchors.verticalCenter: parent.verticalCenter
@ -47,27 +48,10 @@ ListView
text: section
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
}
}
ScrollBar.vertical: ScrollBar
{
// Vertical ScrollBar, styled similarly to the scrollBar in the settings panel
id: verticalScrollBar
visible: packages.contentHeight > packages.height
anchors.right: parent.right
background: Item {}
contentItem: Rectangle
{
id: scrollViewHandle
implicitWidth: UM.Theme.getSize("scrollbar").width
radius: Math.round(implicitWidth / 2)
color: verticalScrollBar.pressed ? UM.Theme.getColor("scrollbar_handle_down") : verticalScrollBar.hovered ? UM.Theme.getColor("scrollbar_handle_hover") : UM.Theme.getColor("scrollbar_handle")
Behavior on color { ColorAnimation { duration: 50; } }
}
}
ScrollBar.vertical: UM.ScrollBar { id: verticalScrollBar }
delegate: MouseArea
{
@ -78,8 +62,11 @@ ListView
hoverEnabled: true
onClicked:
{
packages.selectedPackage = model.package;
contextStack.push(packageDetailsComponent);
if (!model.package.isMissingPackageInformation)
{
packages.selectedPackage = model.package;
contextStack.push(packageDetailsComponent);
}
}
PackageCard
@ -213,7 +200,7 @@ ListView
status: UM.StatusIcon.Status.ERROR
visible: false
}
UM.RecolorImage
UM.ColorImage
{
id: loadMoreIcon
anchors.fill: parent

View file

@ -32,7 +32,7 @@ Control
anchors.fill: parent
color: UM.Theme.getColor("action_button_hovered")
radius: width
UM.RecolorImage
UM.ColorImage
{
anchors.fill: parent
color: UM.Theme.getColor("primary")

View file

@ -3,7 +3,7 @@
import os
from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, pyqtProperty, QTimer
from PyQt6.QtCore import QObject, pyqtSlot, pyqtSignal, pyqtProperty, QTimer
from UM.Application import Application
from UM.Extension import Extension

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