mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-12-05 16:51:12 -07:00
Merge branch 'Ultimaker:main' into master
This commit is contained in:
commit
38b04b0a4d
1127 changed files with 121074 additions and 99912 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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...")
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,5 +3,5 @@
|
|||
"author": "fieldOfView",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for reading AMF files.",
|
||||
"api": 7
|
||||
"api": 8
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
||||
|
|
|
|||
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Checks for firmware updates.",
|
||||
"api": 7,
|
||||
"api": 8,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
29
plugins/Marketplace/CloudApiModel.py
Normal file
29
plugins/Marketplace/CloudApiModel.py
Normal 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
|
||||
)
|
||||
55
plugins/Marketplace/CloudSync/CloudApiClient.py
Normal file
55
plugins/Marketplace/CloudSync/CloudApiClient.py
Normal 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)
|
||||
166
plugins/Marketplace/CloudSync/CloudPackageChecker.py
Normal file
166
plugins/Marketplace/CloudSync/CloudPackageChecker.py
Normal 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)
|
||||
44
plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py
Normal file
44
plugins/Marketplace/CloudSync/DiscrepanciesPresenter.py
Normal 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)
|
||||
153
plugins/Marketplace/CloudSync/DownloadPresenter.py
Normal file
153
plugins/Marketplace/CloudSync/DownloadPresenter.py
Normal 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
|
||||
80
plugins/Marketplace/CloudSync/LicenseModel.py
Normal file
80
plugins/Marketplace/CloudSync/LicenseModel.py
Normal 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()
|
||||
139
plugins/Marketplace/CloudSync/LicensePresenter.py
Normal file
139
plugins/Marketplace/CloudSync/LicensePresenter.py
Normal 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)
|
||||
|
||||
|
||||
|
||||
35
plugins/Marketplace/CloudSync/RestartApplicationPresenter.py
Normal file
35
plugins/Marketplace/CloudSync/RestartApplicationPresenter.py
Normal 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()
|
||||
74
plugins/Marketplace/CloudSync/SubscribedPackagesModel.py
Normal file
74
plugins/Marketplace/CloudSync/SubscribedPackagesModel.py
Normal 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)
|
||||
114
plugins/Marketplace/CloudSync/SyncOrchestrator.py
Normal file
114
plugins/Marketplace/CloudSync/SyncOrchestrator.py
Normal 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()
|
||||
0
plugins/Marketplace/CloudSync/__init__.py
Normal file
0
plugins/Marketplace/CloudSync/__init__.py
Normal file
67
plugins/Marketplace/InstallMissingPackagesDialog.py
Normal file
67
plugins/Marketplace/InstallMissingPackagesDialog.py
Normal 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()
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
46
plugins/Marketplace/MissingPackageList.py
Normal file
46
plugins/Marketplace/MissingPackageList.py
Normal 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()
|
||||
|
||||
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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."))
|
||||
|
|
|
|||
|
|
@ -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()] }
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
146
plugins/Marketplace/resources/qml/CompatibilityDialog.qml
Normal file
146
plugins/Marketplace/resources/qml/CompatibilityDialog.qml
Normal 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
15
plugins/Marketplace/resources/qml/MissingPackages.qml
Normal file
15
plugins/Marketplace/resources/qml/MissingPackages.qml
Normal 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
|
||||
}
|
||||
98
plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml
Normal file
98
plugins/Marketplace/resources/qml/MultipleLicenseDialog.qml
Normal 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()
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 !== ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue