Merge branch 'Ultimaker:main' into master

This commit is contained in:
Anson Liu 2022-08-06 16:53:48 -04:00 committed by GitHub
commit 38b04b0a4d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
1127 changed files with 121074 additions and 99912 deletions

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
@ -579,6 +580,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 +604,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.
@ -1243,3 +1249,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

@ -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 PyQt6.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,77 +253,250 @@ 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
{
width: parent.width
id: warningRow
height: childrenRect.height
visible: manager.hasObjectsOnPlate
visible: warning
spacing: base.margin
UM.ColorImage
{
width: warningLabel.height
height: width
source: UM.Theme.getIcon("Information")
color: UM.Theme.getColor("text")
width: UM.Theme.getSize("extruder_icon").width
height: UM.Theme.getSize("extruder_icon").height
source: UM.Theme.getIcon("Warning")
}
UM.Label
{
id: warningLabel
text: catalog.i18nc("@action:warning", "Loading a project will clear all models on the build plate.")
wrapMode: Text.Wrap
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
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

@ -1,15 +1,22 @@
# Copyright (c) 2015-2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
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
@ -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,7 +161,7 @@ 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()
@ -160,28 +170,32 @@ class ThreeMFWriter(MeshWriter):
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 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

@ -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

@ -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"]

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

@ -5,7 +5,7 @@ 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
@ -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

@ -37,7 +37,7 @@ Cura.RoundedRectangle
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

@ -83,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
@ -102,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")
}

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

@ -14,6 +14,9 @@ import DigitalFactory 1.0 as DF
Item
{
id: base
property variant catalog: UM.I18nCatalog { name: "cura" }
width: parent.width
height: parent.height
@ -44,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")
}
@ -110,13 +112,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
@ -129,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")
}
@ -193,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:

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
@ -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,5 +1,6 @@
# 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
@ -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

@ -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

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

@ -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)

View file

@ -10,7 +10,7 @@ from enum import IntEnum
from pathlib import Path
from typing import Optional, List, Dict, Any, cast
from PyQt6.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, pyqtEnum, QTimer, QUrl
from PyQt6.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, pyqtEnum, QTimer, QUrl, QMetaObject
from PyQt6.QtNetwork import QNetworkReply
from PyQt6.QtQml import qmlRegisterType, qmlRegisterUncreatableMetaObject
@ -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>
"""
pyqtEnum(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:
@ -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

@ -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

@ -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

@ -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 6.0 // For filedialog
import QtQuick.Dialogs // For filedialog
import UM 1.5 as UM
import Cura 1.0 as Cura
@ -147,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

@ -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

@ -31,7 +31,7 @@ UM.Dialog
Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: catalog.i18nc("@action:label", "Height (mm)")
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
Layout.alignment: Qt.AlignVCenter
MouseArea {
id: peak_height_label
@ -64,7 +64,7 @@ UM.Dialog
Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: catalog.i18nc("@action:label", "Base (mm)")
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
Layout.alignment: Qt.AlignVCenter
MouseArea
{
@ -98,7 +98,7 @@ UM.Dialog
Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: catalog.i18nc("@action:label", "Width (mm)")
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
Layout.alignment: Qt.AlignVCenter
MouseArea {
id: width_label
@ -132,7 +132,7 @@ UM.Dialog
Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: catalog.i18nc("@action:label", "Depth (mm)")
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
Layout.alignment: Qt.AlignVCenter
MouseArea {
id: depth_label
@ -166,7 +166,7 @@ UM.Dialog
Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: ""
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
Layout.alignment: Qt.AlignVCenter
MouseArea {
id: lighter_is_higher_label
@ -203,7 +203,7 @@ UM.Dialog
Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: catalog.i18nc("@action:label", "Color Model")
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
Layout.alignment: Qt.AlignVCenter
MouseArea {
id: color_model_label
@ -240,7 +240,7 @@ UM.Dialog
Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: catalog.i18nc("@action:label", "1mm Transmittance (%)")
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
Layout.alignment: Qt.AlignVCenter
MouseArea {
id: transmittance_label
@ -272,7 +272,7 @@ UM.Dialog
Layout.fillWidth: true
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
text: catalog.i18nc("@action:label", "Smoothing")
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
Layout.alignment: Qt.AlignVCenter
MouseArea
{

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

@ -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
@ -47,16 +47,14 @@ Item
Column
{
Layout.fillWidth: true
Layout.alignment: Qt.AlignmentFlag.AlignTop
Layout.alignment: Qt.AlignTop
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
}
@ -178,16 +176,14 @@ Item
Column
{
Layout.fillWidth: true
Layout.alignment: Qt.AlignmentFlag.AlignTop
Layout.alignment: Qt.AlignTop
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

@ -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

@ -3,15 +3,15 @@
import os.path
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from typing import Optional, cast
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

@ -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

@ -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

@ -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

@ -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

@ -42,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.AlignmentFlag.AlignVCenter
wrapMode: Text.Wrap
renderType: Text.NativeRendering
verticalAlignment: Qt.AlignVCenter
}
}

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

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,7 +19,6 @@ Rectangle
implicitHeight: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height
color: UM.Theme.getColor("action_panel_secondary")
// Icon
UM.ColorImage
{
id: onboardingIcon
@ -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,11 +63,7 @@ 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: (line) =>
@ -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
@ -87,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
@ -100,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
@ -119,8 +127,8 @@ 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
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")
@ -155,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")
}
@ -168,6 +177,7 @@ Item
// clickable author name
Item
{
visible: !packageData.isMissingPackageInformation
Layout.fillWidth: true
implicitHeight: authorBy.height
Layout.alignment: Qt.AlignTop
@ -185,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
@ -202,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)
@ -232,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

@ -31,7 +31,7 @@ Item
Cura.SecondaryButton
{
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: UM.Theme.getSize("action_button").height
Layout.preferredWidth: height
@ -45,14 +45,13 @@ Item
iconSize: height - leftPadding * 2
}
Label
UM.Label
{
Layout.alignment: Qt.AlignmentFlag.AlignVCenter
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
text: detailPage.title
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
}
}

View file

@ -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

View file

@ -14,7 +14,7 @@ UM.SimpleButton
width: UM.Theme.getSize("save_button_specs_icons").width
height: UM.Theme.getSize("save_button_specs_icons").height
iconSource: "model_checker.svg"
iconSource: Qt.resolvedUrl("model_checker.svg")
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
color: UM.Theme.getColor("text_scene")
hoverColor: UM.Theme.getColor("text_scene_hover")

View file

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="30px" height="30px" viewBox="0 0 30 30" version="1.1" xmlns="http://www.w3.org/2000/svg">
<polygon fill="#000000" points="19 11 30 8 30 24 19 27" />
<path d="M10,19 C5.581722,19 2,15.418278 2,11 C2,6.581722 5.581722,3 10,3 C14.418278,3 18,6.581722 18,11 C18,15.418278 14.418278,19 10,19 Z M10,17 C13.3137085,17 16,14.3137085 16,11 C16,7.6862915 13.3137085,5 10,5 C6.6862915,5 4,7.6862915 4,11 C4,14.3137085 6.6862915,17 10,17 Z" fill="#000000" />
<polygon fill="#000000" points="4.2 15 6 16.8 1.8 21 0 19.2" />
<path d="M18.7333454,8.81666365 C18.2107269,6.71940704 16.9524304,4.91317986 15.248379,3.68790525 L18,3 L30,6 L18.7333454,8.81666365 Z M17,16.6573343 L17,27 L6,24 L6,19.0644804 C7.20495897,19.6632939 8.56315852,20 10,20 C12.8272661,20 15.3500445,18.6963331 17,16.6573343 Z" fill="#000000" />
<polygon points="19 11 30 8 30 24 19 27" />
<path d="M10,19 C5.581722,19 2,15.418278 2,11 C2,6.581722 5.581722,3 10,3 C14.418278,3 18,6.581722 18,11 C18,15.418278 14.418278,19 10,19 Z M10,17 C13.3137085,17 16,14.3137085 16,11 C16,7.6862915 13.3137085,5 10,5 C6.6862915,5 4,7.6862915 4,11 C4,14.3137085 6.6862915,17 10,17 Z" />
<polygon points="4.2 15 6 16.8 1.8 21 0 19.2" />
<path d="M18.7333454,8.81666365 C18.2107269,6.71940704 16.9524304,4.91317986 15.248379,3.68790525 L18,3 L30,6 L18.7333454,8.81666365 Z M17,16.6573343 L17,27 L6,24 L6,19.0644804 C7.20495897,19.6632939 8.56315852,20 10,20 C12.8272661,20 15.3500445,18.6963331 17,16.6573343 Z" />
</svg>

Before

Width:  |  Height:  |  Size: 877 B

After

Width:  |  Height:  |  Size: 817 B

Before After
Before After

View file

@ -2,7 +2,7 @@
"name": "Model Checker",
"author": "Ultimaker B.V.",
"version": "1.0.1",
"api": 7,
"api": 8,
"description": "Checks models and print configuration for possible printing issues and give suggestions.",
"i18n-catalog": "cura"
}

View file

@ -122,6 +122,7 @@ Rectangle
}
visible: !isNetworkConfigured && isNetworkConfigurable
width: childrenRect.width
height: childrenRect.height
UM.ColorImage
{
@ -132,7 +133,7 @@ Rectangle
width: UM.Theme.getSize("icon_indicator").width
height: UM.Theme.getSize("icon_indicator").height
}
Label
UM.Label
{
id: manageQueueText
anchors
@ -144,7 +145,6 @@ Rectangle
color: UM.Theme.getColor("text_link")
font: UM.Theme.getFont("medium")
text: catalog.i18nc("@label link to technical assistance", "View user manuals online")
renderType: Text.NativeRendering
}
MouseArea
{
@ -155,14 +155,13 @@ Rectangle
onExited: manageQueueText.font.underline = false
}
}
Label
UM.Label
{
id: noConnectionLabel
anchors.horizontalCenter: parent.horizontalCenter
visible: !isNetworkConfigurable
text: catalog.i18nc("@info", "In order to monitor your print from Cura, please connect the printer.")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
wrapMode: Text.WordWrap
width: contentWidth
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides a monitor stage in Cura.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -196,7 +196,7 @@ Item
height: parent.height
width: UM.Theme.getSize("setting").width + UM.Theme.getSize("default_margin").width
ScrollBar.vertical: UM.ScrollBar {}
ScrollBar.vertical: UM.ScrollBar { id: scrollBar }
clip: true
spacing: UM.Theme.getSize("default_lining").height
@ -244,7 +244,7 @@ Item
Loader
{
id: settingLoader
width: UM.Theme.getSize("setting").width - removeButton.width
width: UM.Theme.getSize("setting").width - removeButton.width - scrollBar.width
height: UM.Theme.getSize("section").height + UM.Theme.getSize("narrow_margin").height
enabled: provider.properties.enabled === "True"
property var definition: model
@ -299,7 +299,7 @@ Item
{
id: removeButton
width: UM.Theme.getSize("setting").height
height: UM.Theme.getSize("setting").height
height: UM.Theme.getSize("setting").height + UM.Theme.getSize("narrow_margin").height
onClicked: addedSettingsModel.setVisible(model.key, false)

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides the Per Model Settings.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -19,7 +19,7 @@ UM.Dialog
height: 500 * screenScaleFactor
minimumWidth: 400 * screenScaleFactor
minimumHeight: 250 * screenScaleFactor
backgroundColor: UM.Theme.getColor("main_background")
onVisibleChanged:
{
// Whenever the window is closed (either via the "Close" button or the X on the window frame), we want to update it in the stack.
@ -232,7 +232,7 @@ UM.Dialog
}
onObjectAdded: function(index, object) { scriptsMenu.insertItem(index, object)}
onObjectRemoved: function(object) { scriptsMenu.removeItem(object) }
onObjectRemoved: function(index, object) { scriptsMenu.removeItem(object) }
}
}
@ -245,7 +245,7 @@ UM.Dialog
height: parent.height
id: settingsPanel
Label
UM.Label
{
id: scriptSpecsHeader
text: manager.selectedScriptIndex == -1 ? catalog.i18nc("@label", "Settings") : base.activeScriptName
@ -262,7 +262,6 @@ UM.Dialog
elide: Text.ElideRight
height: 20 * screenScaleFactor
font: UM.Theme.getFont("large_bold")
color: UM.Theme.getColor("text")
}
ListView
@ -287,6 +286,7 @@ UM.Dialog
{
id: definitionsModel
containerId: manager.selectedScriptDefinitionId
onContainerIdChanged: definitionsModel.setAllVisible(true)
showAll: true
}
@ -475,7 +475,7 @@ UM.Dialog
}
toolTipContentAlignment: UM.Enums.ContentAlignment.AlignLeft
onClicked: dialog.show()
// iconSource: "Script.svg"
iconSource: Qt.resolvedUrl("Script.svg")
fixedWidthMode: false
}

View file

@ -2,7 +2,7 @@
"name": "Post Processing",
"author": "Ultimaker",
"version": "2.2.1",
"api": 7,
"api": 8,
"description": "Extension that allows for user created scripts for post processing",
"catalog": "cura"
}

View file

@ -330,7 +330,7 @@ class PauseAtHeight(Script):
current_height = current_z - layer_0_z
if current_height < pause_height:
continue # Scan the enitre layer, z-changes are not always on the same/first line.
continue # Scan the entire layer, z-changes are not always on the same/first line.
# Pause at layer
else:

View file

@ -106,26 +106,34 @@ Item
{
id: openProviderColumn
//The column doesn't automatically listen to its children rect if the children change internally, so we need to explicitly update the size.
onChildrenRectChanged:
// Automatically set the width to fit the widest MenuItem
// Based on https://martin.rpdev.net/2018/03/13/qt-quick-controls-2-automatically-set-the-width-of-menus.html
function setWidth()
{
popup.implicitHeight = childrenRect.height
popup.implicitWidth = childrenRect.width
}
onPositioningComplete:
{
popup.implicitHeight = childrenRect.height
popup.implicitWidth = childrenRect.width
var result = 0;
var padding = 0;
for (var i = 0; i < fileProviderRepeater.count; ++i) {
var item = fileProviderRepeater.itemAt(i);
if (item.hasOwnProperty("implicitWidth"))
{
var itemWidth = item.implicitWidth;
result = Math.max(itemWidth, result);
padding = Math.max(item.padding, padding);
}
}
return result + padding * 2;
}
width: setWidth()
Repeater
{
id: fileProviderRepeater
model: prepareMenu.fileProviderModel
delegate: Button
{
leftPadding: UM.Theme.getSize("default_margin").width
rightPadding: UM.Theme.getSize("default_margin").width
width: contentItem.width + leftPadding + rightPadding
width: openProviderColumn.width
height: UM.Theme.getSize("action_button").height
hoverEnabled: true

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides a prepare stage in Cura.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides a preview stage in Cura.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -80,6 +80,18 @@ class RemovableDriveOutputDevice(OutputDevice):
if extension: # Not empty string.
extension = "." + extension
file_name = os.path.join(self.getId(), file_name + extension)
self._performWrite(file_name, preferred_format, writer, nodes)
def _performWrite(self, file_name, preferred_format, writer, nodes):
"""Writes the specified nodes to the removable drive. This is split from
requestWrite to allow interception in other plugins. See Ultimaker/Cura#10917.
:param file_name: File path to write to.
:param preferred_format: Preferred file format to write to.
:param writer: Writer for writing to the file.
:param nodes: A collection of scene nodes that should be written to the
file.
"""
try:
Logger.log("d", "Writing to %s", file_name)

View file

@ -21,7 +21,7 @@ class RemovableDrivePlugin(OutputDevicePlugin):
super().__init__()
self._update_thread = threading.Thread(target = self._updateThread)
self._update_thread.deamon = True
self._update_thread.daemon = True
self._check_updates = True

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"description": "Provides removable drive hotplugging and writing support.",
"version": "1.0.1",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Logs certain events so that they can be used by the crash reporter",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -286,9 +286,7 @@ Cura.ExpandableComponent
UM.Label
{
text: label
font: UM.Theme.getFont("default")
elide: Text.ElideRight
renderType: Text.NativeRendering
color: UM.Theme.getColor("setting_control_text")
anchors.verticalCenter: parent.verticalCenter
anchors.left: legendModelCheckBox.left

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Provides the preview of sliced layerdata.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

View file

@ -61,7 +61,7 @@ Window
right: parent.right
}
Label
UM.Label
{
id: headerText
anchors
@ -71,9 +71,7 @@ Window
right: parent.right
}
text: catalog.i18nc("@text:window", "Ultimaker Cura collects anonymous data in order to improve the print quality and user experience. Below is an example of all the data that is shared:")
color: UM.Theme.getColor("text")
wrapMode: Text.WordWrap
renderType: Text.NativeRendering
}
Cura.ScrollableTextArea

View file

@ -133,6 +133,7 @@ class SliceInfo(QObject, Extension):
data["is_logged_in"] = self._application.getCuraAPI().account.isLoggedIn
data["organization_id"] = org_id if org_id else None
data["subscriptions"] = user_profile.get("subscriptions", []) if user_profile else []
data["slice_uuid"] = print_information.slice_uuid
active_mode = self._application.getPreferences().getValue("cura/active_mode")
if active_mode == 0:

View file

@ -9,6 +9,7 @@
<b>Using Custom Settings:</b> No<br/>
<b>Is Logged In:</b> Yes<br/>
<b>Organization ID (if any):</b> ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE=<br/>
<b>Slice ID:</b> aBcDeF01-2345-6789-aBcD-eF0123456789<br/>
<b>Subscriptions (if any):</b>
<ul>
<li><b>Level:</b> 10, <b>Type:</b> Enterprise, <b>Plan:</b> Basic</li>

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.1",
"description": "Submits anonymous slice info. Can be disabled through preferences.",
"api": 7,
"api": 8,
"i18n-catalog": "cura"
}

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