mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-08-07 14:04:03 -06:00
Merge branch 'main'
This commit is contained in:
commit
b1138e12d9
5455 changed files with 203722 additions and 208531 deletions
|
@ -1,11 +1,11 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
# Copyright (c) 2021-2022 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os.path
|
||||
import zipfile
|
||||
from typing import List, Optional, Union, TYPE_CHECKING, cast
|
||||
|
||||
import Savitar
|
||||
import pySavitar as Savitar
|
||||
import numpy
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
@ -304,4 +304,4 @@ class ThreeMFReader(MeshReader):
|
|||
unit = "millimeter"
|
||||
|
||||
scale = conversion_to_mm[unit]
|
||||
return Vector(scale, scale, scale)
|
||||
return Vector(scale, scale, scale)
|
||||
|
|
|
@ -23,6 +23,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
|||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
||||
from UM.Job import Job
|
||||
from UM.Preferences import Preferences
|
||||
from cura.CuraPackageManager import CuraPackageManager
|
||||
|
||||
from cura.Machines.ContainerTree import ContainerTree
|
||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||
|
@ -34,7 +35,7 @@ from cura.Settings.CuraContainerStack import _ContainerIndexes
|
|||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Utils.Threading import call_on_qt_thread
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication
|
||||
from PyQt6.QtCore import QCoreApplication
|
||||
|
||||
from .WorkspaceDialog import WorkspaceDialog
|
||||
|
||||
|
@ -52,6 +53,7 @@ _ignored_machine_network_metadata = {
|
|||
"connection_type",
|
||||
"capabilities",
|
||||
"octoprint_api_key",
|
||||
"is_abstract_machine"
|
||||
} # type: Set[str]
|
||||
|
||||
|
||||
|
@ -579,6 +581,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
is_printer_group = True
|
||||
machine_name = group_name
|
||||
|
||||
# Getting missing required package ids
|
||||
package_metadata = self._parse_packages_metadata(archive)
|
||||
missing_package_metadata = self._filter_missing_package_metadata(package_metadata)
|
||||
|
||||
# Show the dialog, informing the user what is about to happen.
|
||||
self._dialog.setMachineConflict(machine_conflict)
|
||||
self._dialog.setIsPrinterGroup(is_printer_group)
|
||||
|
@ -599,6 +605,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
self._dialog.setExtruders(extruders)
|
||||
self._dialog.setVariantType(variant_type_name)
|
||||
self._dialog.setHasObjectsOnPlate(Application.getInstance().platformActivity)
|
||||
self._dialog.setMissingPackagesMetadata(missing_package_metadata)
|
||||
self._dialog.show()
|
||||
|
||||
# Block until the dialog is closed.
|
||||
|
@ -658,10 +665,22 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
|
||||
cura_file_names = [name for name in archive.namelist() if name.startswith("Cura/")]
|
||||
|
||||
# Create a shadow copy of the preferences (we don't want all of the preferences, but we do want to re-use its
|
||||
# Create a shadow copy of the preferences (We don't want all of the preferences, but we do want to re-use its
|
||||
# parsing code.
|
||||
temp_preferences = Preferences()
|
||||
serialized = archive.open("Cura/preferences.cfg").read().decode("utf-8")
|
||||
try:
|
||||
serialized = archive.open("Cura/preferences.cfg").read().decode("utf-8")
|
||||
except KeyError:
|
||||
# If there is no preferences file, it's not a workspace, so notify user of failure.
|
||||
Logger.log("w", "File %s is not a valid workspace.", file_name)
|
||||
message = Message(i18n_catalog.i18nc("@info:error Don't translate the XML tags <filename> or <message>!",
|
||||
"Project file <filename>{0}</filename> is corrupt: <message>{1}</message>.",
|
||||
file_name, str(e)),
|
||||
title=i18n_catalog.i18nc("@info:title", "Can't Open Project File"),
|
||||
message_type=Message.MessageType.ERROR)
|
||||
message.show()
|
||||
self.setWorkspaceName("")
|
||||
return [], {}
|
||||
temp_preferences.deserialize(serialized)
|
||||
|
||||
# Copy a number of settings from the temp preferences to the global
|
||||
|
@ -1243,3 +1262,29 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"})
|
||||
for entry in metadata:
|
||||
return entry.text
|
||||
|
||||
@staticmethod
|
||||
def _parse_packages_metadata(archive: zipfile.ZipFile) -> List[Dict[str, str]]:
|
||||
try:
|
||||
package_metadata = json.loads(archive.open("Cura/packages.json").read().decode("utf-8"))
|
||||
return package_metadata["packages"]
|
||||
except KeyError:
|
||||
Logger.warning("No package metadata was found in .3mf file.")
|
||||
except Exception:
|
||||
Logger.error("Failed to load packages metadata from .3mf file.")
|
||||
|
||||
return []
|
||||
|
||||
|
||||
@staticmethod
|
||||
def _filter_missing_package_metadata(package_metadata: List[Dict[str, str]]) -> List[Dict[str, str]]:
|
||||
"""Filters out installed packages from package_metadata"""
|
||||
missing_packages = []
|
||||
package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
|
||||
|
||||
for package in package_metadata:
|
||||
package_id = package["id"]
|
||||
if not package_manager.isPackageInstalled(package_id):
|
||||
missing_packages.append(package)
|
||||
|
||||
return missing_packages
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
from typing import Dict, List
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
@ -25,10 +25,10 @@ class UpdatableMachinesModel(ListModel):
|
|||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.addRoleName(Qt.UserRole + 1, "id")
|
||||
self.addRoleName(Qt.UserRole + 2, "name")
|
||||
self.addRoleName(Qt.UserRole + 3, "displayName")
|
||||
self.addRoleName(Qt.UserRole + 4, "type") # Either "default_option" or "machine"
|
||||
self.addRoleName(Qt.ItemDataRole.UserRole + 1, "id")
|
||||
self.addRoleName(Qt.ItemDataRole.UserRole + 2, "name")
|
||||
self.addRoleName(Qt.ItemDataRole.UserRole + 3, "displayName")
|
||||
self.addRoleName(Qt.ItemDataRole.UserRole + 4, "type") # Either "default_option" or "machine"
|
||||
|
||||
def update(self, machines: List[GlobalStack]) -> None:
|
||||
items = [create_new_list_item] # type: List[Dict[str, str]]
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
# Copyright (c) 2020 Ultimaker B.V.
|
||||
# Copyright (c) 2022 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt6.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication, QUrl
|
||||
from PyQt6.QtGui import QDesktopServices
|
||||
from typing import List, Optional, Dict, cast
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, pyqtProperty, QCoreApplication
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Application import Application
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from UM.Application import Application
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
from .UpdatableMachinesModel import UpdatableMachinesModel
|
||||
|
||||
import os
|
||||
|
@ -23,7 +28,7 @@ i18n_catalog = i18nCatalog("cura")
|
|||
class WorkspaceDialog(QObject):
|
||||
showDialogSignal = pyqtSignal()
|
||||
|
||||
def __init__(self, parent = None):
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._component = None
|
||||
self._context = None
|
||||
|
@ -59,6 +64,9 @@ class WorkspaceDialog(QObject):
|
|||
self._objects_on_plate = False
|
||||
self._is_printer_group = False
|
||||
self._updatable_machines_model = UpdatableMachinesModel(self)
|
||||
self._missing_package_metadata: List[Dict[str, str]] = []
|
||||
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
|
||||
self._install_missing_package_dialog: Optional[QObject] = None
|
||||
|
||||
machineConflictChanged = pyqtSignal()
|
||||
qualityChangesConflictChanged = pyqtSignal()
|
||||
|
@ -79,6 +87,7 @@ class WorkspaceDialog(QObject):
|
|||
variantTypeChanged = pyqtSignal()
|
||||
extrudersChanged = pyqtSignal()
|
||||
isPrinterGroupChanged = pyqtSignal()
|
||||
missingPackagesChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = isPrinterGroupChanged)
|
||||
def isPrinterGroup(self) -> bool:
|
||||
|
@ -274,6 +283,21 @@ class WorkspaceDialog(QObject):
|
|||
self._has_quality_changes_conflict = quality_changes_conflict
|
||||
self.qualityChangesConflictChanged.emit()
|
||||
|
||||
def setMissingPackagesMetadata(self, missing_package_metadata: List[Dict[str, str]]) -> None:
|
||||
self._missing_package_metadata = missing_package_metadata
|
||||
self.missingPackagesChanged.emit()
|
||||
|
||||
@pyqtProperty("QVariantList", notify=missingPackagesChanged)
|
||||
def missingPackages(self) -> List[Dict[str, str]]:
|
||||
return self._missing_package_metadata
|
||||
|
||||
@pyqtSlot()
|
||||
def installMissingPackages(self) -> None:
|
||||
marketplace_plugin = PluginRegistry.getInstance().getPluginObject("Marketplace")
|
||||
if not marketplace_plugin:
|
||||
Logger.warning("Could not show dialog to install missing plug-ins. Is Marketplace plug-in not available?")
|
||||
marketplace_plugin.showInstallMissingPackageDialog(self._missing_package_metadata, self.showMissingMaterialsWarning) # type: ignore
|
||||
|
||||
def getResult(self) -> Dict[str, Optional[str]]:
|
||||
if "machine" in self._result and self.updatableMachinesModel.count <= 1:
|
||||
self._result["machine"] = None
|
||||
|
@ -360,6 +384,41 @@ class WorkspaceDialog(QObject):
|
|||
time.sleep(1 / 50)
|
||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||
|
||||
@pyqtSlot()
|
||||
def showMissingMaterialsWarning(self) -> None:
|
||||
result_message = Message(
|
||||
i18n_catalog.i18nc("@info:status", "The material used in this project relies on some material definitions not available in Cura, this might produce undesirable print results. We highly recommend installing the full material package from the Marketplace."),
|
||||
lifetime=0,
|
||||
title=i18n_catalog.i18nc("@info:title", "Material profiles not installed"),
|
||||
message_type=Message.MessageType.WARNING
|
||||
)
|
||||
result_message.addAction(
|
||||
"learn_more",
|
||||
name=i18n_catalog.i18nc("@action:button", "Learn more"),
|
||||
icon="",
|
||||
description="Learn more about project materials.",
|
||||
button_align=Message.ActionButtonAlignment.ALIGN_LEFT,
|
||||
button_style=Message.ActionButtonStyle.LINK
|
||||
)
|
||||
result_message.addAction(
|
||||
"install_materials",
|
||||
name=i18n_catalog.i18nc("@action:button", "Install Materials"),
|
||||
icon="",
|
||||
description="Install missing materials from project file.",
|
||||
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT,
|
||||
button_style=Message.ActionButtonStyle.DEFAULT
|
||||
)
|
||||
result_message.actionTriggered.connect(self._onMessageActionTriggered)
|
||||
result_message.show()
|
||||
|
||||
def _onMessageActionTriggered(self, message: Message, sync_message_action: str) -> None:
|
||||
if sync_message_action == "install_materials":
|
||||
self.installMissingPackages()
|
||||
message.hide()
|
||||
elif sync_message_action == "learn_more":
|
||||
QDesktopServices.openUrl(QUrl("https://support.ultimaker.com/hc/en-us/articles/360011968360-Using-the-Ultimaker-Marketplace"))
|
||||
|
||||
|
||||
def __show(self) -> None:
|
||||
if self._view is None:
|
||||
self._createViewFromQML()
|
||||
|
|
|
@ -17,7 +17,8 @@ UM.Dialog
|
|||
minimumWidth: UM.Theme.getSize("popup_dialog").width
|
||||
minimumHeight: UM.Theme.getSize("popup_dialog").height
|
||||
width: minimumWidth
|
||||
|
||||
backgroundColor: UM.Theme.getColor("main_background")
|
||||
margin: UM.Theme.getSize("default_margin").width
|
||||
property int comboboxHeight: UM.Theme.getSize("default_margin").height
|
||||
|
||||
onClosing: manager.notifyClosed()
|
||||
|
@ -31,337 +32,220 @@ UM.Dialog
|
|||
}
|
||||
}
|
||||
|
||||
Item
|
||||
Flickable
|
||||
{
|
||||
id: dialogSummaryItem
|
||||
clip: true
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
anchors.margins: 10 * screenScaleFactor
|
||||
height: parent.height
|
||||
contentHeight: dialogSummaryItem.height
|
||||
ScrollBar.vertical: UM.ScrollBar { id: verticalScrollBar }
|
||||
|
||||
UM.I18nCatalog
|
||||
Item
|
||||
{
|
||||
id: catalog
|
||||
name: "cura"
|
||||
}
|
||||
|
||||
ListModel
|
||||
{
|
||||
id: resolveStrategiesModel
|
||||
// Instead of directly adding the list elements, we add them afterwards.
|
||||
// This is because it's impossible to use setting function results to be bound to listElement properties directly.
|
||||
// See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties
|
||||
Component.onCompleted:
|
||||
{
|
||||
append({"key": "override", "label": catalog.i18nc("@action:ComboBox Update/override existing profile", "Update existing")});
|
||||
append({"key": "new", "label": catalog.i18nc("@action:ComboBox Save settings in a new profile", "Create new")});
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
width: parent.width
|
||||
id: dialogSummaryItem
|
||||
width: verticalScrollBar.visible ? parent.width - verticalScrollBar.width - UM.Theme.getSize("default_margin").width : parent.width
|
||||
height: childrenRect.height
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
anchors.margins: 10 * screenScaleFactor
|
||||
|
||||
UM.I18nCatalog
|
||||
{
|
||||
id: catalog
|
||||
name: "cura"
|
||||
}
|
||||
|
||||
ListModel
|
||||
{
|
||||
id: resolveStrategiesModel
|
||||
// Instead of directly adding the list elements, we add them afterwards.
|
||||
// This is because it's impossible to use setting function results to be bound to listElement properties directly.
|
||||
// See http://stackoverflow.com/questions/7659442/listelement-fields-as-properties
|
||||
Component.onCompleted:
|
||||
{
|
||||
append({"key": "override", "label": catalog.i18nc("@action:ComboBox Update/override existing profile", "Update existing")});
|
||||
append({"key": "new", "label": catalog.i18nc("@action:ComboBox Save settings in a new profile", "Create new")});
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
|
||||
UM.Label
|
||||
Column
|
||||
{
|
||||
id: titleLabel
|
||||
text: catalog.i18nc("@action:title", "Summary - Cura Project")
|
||||
font: UM.Theme.getFont("large")
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: separator
|
||||
color: UM.Theme.getColor("text")
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("default_lining").height
|
||||
}
|
||||
}
|
||||
height: childrenRect.height
|
||||
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.TooltipArea
|
||||
{
|
||||
id: machineResolveStrategyTooltip
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
width: (parent.width / 3) | 0
|
||||
height: visible ? comboboxHeight : 0
|
||||
visible: base.visible && machineResolveComboBox.model.count > 1
|
||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
|
||||
Cura.ComboBox
|
||||
UM.Label
|
||||
{
|
||||
id: machineResolveComboBox
|
||||
model: manager.updatableMachinesModel
|
||||
visible: machineResolveStrategyTooltip.visible
|
||||
textRole: "displayName"
|
||||
id: titleLabel
|
||||
text: catalog.i18nc("@action:title", "Summary - Cura Project")
|
||||
font: UM.Theme.getFont("large")
|
||||
}
|
||||
|
||||
Rectangle
|
||||
{
|
||||
id: separator
|
||||
color: UM.Theme.getColor("text")
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("button").height
|
||||
onCurrentIndexChanged:
|
||||
{
|
||||
if (model.getItem(currentIndex).id == "new"
|
||||
&& model.getItem(currentIndex).type == "default_option")
|
||||
{
|
||||
manager.setResolveStrategy("machine", "new")
|
||||
}
|
||||
else
|
||||
{
|
||||
manager.setResolveStrategy("machine", "override")
|
||||
manager.setMachineToOverride(model.getItem(currentIndex).id)
|
||||
}
|
||||
}
|
||||
height: UM.Theme.getSize("default_lining").height
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged:
|
||||
{
|
||||
if (!visible) {return}
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
currentIndex = 0
|
||||
// If the project printer exists in Cura, set it as the default dropdown menu option.
|
||||
// No need to check object 0, which is the "Create new" option
|
||||
for (var i = 1; i < model.count; i++)
|
||||
UM.TooltipArea
|
||||
{
|
||||
id: machineResolveStrategyTooltip
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
width: (parent.width / 3) | 0
|
||||
height: visible ? comboboxHeight : 0
|
||||
visible: base.visible && machineResolveComboBox.model.count > 1
|
||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the machine be resolved?")
|
||||
Cura.ComboBox
|
||||
{
|
||||
id: machineResolveComboBox
|
||||
model: manager.updatableMachinesModel
|
||||
visible: machineResolveStrategyTooltip.visible
|
||||
textRole: "displayName"
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("button").height
|
||||
onCurrentIndexChanged:
|
||||
{
|
||||
if (model.getItem(i).name == manager.machineName)
|
||||
if (model.getItem(currentIndex).id == "new"
|
||||
&& model.getItem(currentIndex).type == "default_option")
|
||||
{
|
||||
currentIndex = i
|
||||
break
|
||||
manager.setResolveStrategy("machine", "new")
|
||||
}
|
||||
else
|
||||
{
|
||||
manager.setResolveStrategy("machine", "override")
|
||||
manager.setMachineToOverride(model.getItem(currentIndex).id)
|
||||
}
|
||||
}
|
||||
// The project printer does not exist in Cura. If there is at least one printer of the same
|
||||
// type, select the first one, else set the index to "Create new"
|
||||
if (currentIndex == 0 && model.count > 1)
|
||||
|
||||
onVisibleChanged:
|
||||
{
|
||||
currentIndex = 1
|
||||
if (!visible) {return}
|
||||
|
||||
currentIndex = 0
|
||||
// If the project printer exists in Cura, set it as the default dropdown menu option.
|
||||
// No need to check object 0, which is the "Create new" option
|
||||
for (var i = 1; i < model.count; i++)
|
||||
{
|
||||
if (model.getItem(i).name == manager.machineName)
|
||||
{
|
||||
currentIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
// The project printer does not exist in Cura. If there is at least one printer of the same
|
||||
// type, select the first one, else set the index to "Create new"
|
||||
if (currentIndex == 0 && model.count > 1)
|
||||
{
|
||||
currentIndex = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
id: printer_settings_label
|
||||
text: catalog.i18nc("@action:label", "Printer settings")
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
}
|
||||
|
||||
Row
|
||||
Column
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Type")
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: manager.machineType
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name")
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: manager.machineName
|
||||
width: (parent.width / 3) | 0
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.TooltipArea
|
||||
{
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
width: (parent.width / 3) | 0
|
||||
height: visible ? comboboxHeight : 0
|
||||
visible: manager.qualityChangesConflict
|
||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?")
|
||||
Cura.ComboBox
|
||||
{
|
||||
model: resolveStrategiesModel
|
||||
textRole: "label"
|
||||
id: qualityChangesResolveComboBox
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("button").height
|
||||
onActivated:
|
||||
{
|
||||
manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Profile settings")
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Name")
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: manager.qualityName
|
||||
width: (parent.width / 3) | 0
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Intent")
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: manager.intentName
|
||||
width: (parent.width / 3) | 0
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Not in profile")
|
||||
visible: manager.numUserSettings != 0
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings)
|
||||
visible: manager.numUserSettings != 0
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Derivative from")
|
||||
visible: manager.numSettingsOverridenByQualityChanges != 0
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
|
||||
width: (parent.width / 3) | 0
|
||||
visible: manager.numSettingsOverridenByQualityChanges != 0
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.TooltipArea
|
||||
{
|
||||
id: materialResolveTooltip
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
width: (parent.width / 3) | 0
|
||||
height: visible ? comboboxHeight : 0
|
||||
visible: manager.materialConflict
|
||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the material be resolved?")
|
||||
Cura.ComboBox
|
||||
{
|
||||
model: resolveStrategiesModel
|
||||
textRole: "label"
|
||||
id: materialResolveComboBox
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("button").height
|
||||
onActivated:
|
||||
{
|
||||
manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
Row
|
||||
{
|
||||
height: childrenRect.height
|
||||
width: parent.width
|
||||
spacing: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Material settings")
|
||||
id: printer_settings_label
|
||||
text: catalog.i18nc("@action:label", "Printer settings")
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
}
|
||||
|
||||
Repeater
|
||||
{
|
||||
model: manager.materialLabels
|
||||
delegate: Row
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Type")
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: manager.machineType
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", manager.isPrinterGroup ? "Printer Group" : "Printer Name")
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: manager.machineName
|
||||
width: (parent.width / 3) | 0
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.TooltipArea
|
||||
{
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
width: (parent.width / 3) | 0
|
||||
height: visible ? comboboxHeight : 0
|
||||
visible: manager.qualityChangesConflict
|
||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the profile be resolved?")
|
||||
Cura.ComboBox
|
||||
{
|
||||
model: resolveStrategiesModel
|
||||
textRole: "label"
|
||||
id: qualityChangesResolveComboBox
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("button").height
|
||||
onActivated:
|
||||
{
|
||||
manager.setResolveStrategy("quality_changes", resolveStrategiesModel.get(index).key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Profile settings")
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Name")
|
||||
|
@ -369,76 +253,249 @@ UM.Dialog
|
|||
}
|
||||
UM.Label
|
||||
{
|
||||
text: modelData
|
||||
text: manager.qualityName
|
||||
width: (parent.width / 3) | 0
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Intent")
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: manager.intentName
|
||||
width: (parent.width / 3) | 0
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Not in profile")
|
||||
visible: manager.numUserSettings != 0
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18ncp("@action:label", "%1 override", "%1 overrides", manager.numUserSettings).arg(manager.numUserSettings)
|
||||
visible: manager.numUserSettings != 0
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Derivative from")
|
||||
visible: manager.numSettingsOverridenByQualityChanges != 0
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18ncp("@action:label", "%1, %2 override", "%1, %2 overrides", manager.numSettingsOverridenByQualityChanges).arg(manager.qualityType).arg(manager.numSettingsOverridenByQualityChanges)
|
||||
width: (parent.width / 3) | 0
|
||||
visible: manager.numSettingsOverridenByQualityChanges != 0
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
Item
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Setting visibility")
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.TooltipArea
|
||||
{
|
||||
id: materialResolveTooltip
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
width: (parent.width / 3) | 0
|
||||
height: visible ? comboboxHeight : 0
|
||||
visible: manager.materialConflict
|
||||
text: catalog.i18nc("@info:tooltip", "How should the conflict in the material be resolved?")
|
||||
Cura.ComboBox
|
||||
{
|
||||
model: resolveStrategiesModel
|
||||
textRole: "label"
|
||||
id: materialResolveComboBox
|
||||
width: parent.width
|
||||
height: UM.Theme.getSize("button").height
|
||||
onActivated:
|
||||
{
|
||||
manager.setResolveStrategy("material", resolveStrategiesModel.get(index).key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
Row
|
||||
{
|
||||
height: childrenRect.height
|
||||
width: parent.width
|
||||
spacing: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Material settings")
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
}
|
||||
|
||||
Repeater
|
||||
{
|
||||
model: manager.materialLabels
|
||||
delegate: Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Name")
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: modelData
|
||||
width: (parent.width / 3) | 0
|
||||
wrapMode: Text.WordWrap
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Setting visibility")
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
}
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Mode")
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: manager.activeMode
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
}
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
visible: manager.hasVisibleSettingsField
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Visible settings:")
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "%1 out of %2" ).arg(manager.numVisibleSettings).arg(manager.totalNumberOfSettings)
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
UM.Label
|
||||
visible: manager.hasObjectsOnPlate
|
||||
UM.ColorImage
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Mode")
|
||||
width: (parent.width / 3) | 0
|
||||
width: warningLabel.height
|
||||
height: width
|
||||
source: UM.Theme.getIcon("Information")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: manager.activeMode
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
}
|
||||
Row
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
visible: manager.hasVisibleSettingsField
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "Visible settings:")
|
||||
width: (parent.width / 3) | 0
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@action:label", "%1 out of %2" ).arg(manager.numVisibleSettings).arg(manager.totalNumberOfSettings)
|
||||
width: (parent.width / 3) | 0
|
||||
id: warningLabel
|
||||
text: catalog.i18nc("@action:warning", "Loading a project will clear all models on the build plate.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row
|
||||
property bool warning: manager.missingPackages.length > 0
|
||||
|
||||
footerComponent: Rectangle
|
||||
{
|
||||
color: warning ? UM.Theme.getColor("warning") : "transparent"
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width
|
||||
height: childrenRect.height + 2 * base.margin
|
||||
|
||||
Column
|
||||
{
|
||||
height: childrenRect.height
|
||||
spacing: base.margin
|
||||
|
||||
anchors.margins: base.margin
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
|
||||
RowLayout
|
||||
{
|
||||
id: warningRow
|
||||
height: childrenRect.height
|
||||
visible: warning
|
||||
spacing: base.margin
|
||||
UM.ColorImage
|
||||
{
|
||||
width: UM.Theme.getSize("extruder_icon").width
|
||||
height: UM.Theme.getSize("extruder_icon").height
|
||||
source: UM.Theme.getIcon("Warning")
|
||||
}
|
||||
|
||||
UM.Label
|
||||
{
|
||||
id: warningText
|
||||
text: catalog.i18nc("@label", "The material used in this project is currently not installed in Cura.<br/>Install the material profile and reopen the project.")
|
||||
}
|
||||
}
|
||||
|
||||
Loader
|
||||
{
|
||||
width: parent.width
|
||||
height: childrenRect.height
|
||||
visible: manager.hasObjectsOnPlate
|
||||
UM.RecolorImage
|
||||
{
|
||||
width: warningLabel.height
|
||||
height: width
|
||||
source: UM.Theme.getIcon("Information")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
UM.Label
|
||||
{
|
||||
id: warningLabel
|
||||
text: catalog.i18nc("@action:warning", "Loading a project will clear all models on the build plate.")
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
sourceComponent: buttonRow
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -448,13 +505,30 @@ UM.Dialog
|
|||
rightButtons: [
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
visible: !warning
|
||||
text: catalog.i18nc("@action:button", "Cancel")
|
||||
onClicked: reject()
|
||||
},
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
visible: !warning
|
||||
text: catalog.i18nc("@action:button", "Open")
|
||||
onClicked: accept()
|
||||
},
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
visible: warning
|
||||
text: catalog.i18nc("@action:button", "Open project anyway")
|
||||
onClicked: {
|
||||
manager.showMissingMaterialsWarning();
|
||||
accept();
|
||||
}
|
||||
},
|
||||
Cura.PrimaryButton
|
||||
{
|
||||
visible: warning
|
||||
text: catalog.i18nc("@action:button", "Install missing material")
|
||||
onClicked: manager.installMissingPackages()
|
||||
}
|
||||
]
|
||||
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -156,6 +156,7 @@ class ThreeMFWorkspaceWriter(WorkspaceWriter):
|
|||
"connection_type",
|
||||
"capabilities",
|
||||
"octoprint_api_key",
|
||||
"is_online",
|
||||
}
|
||||
serialized_data = container.serialize(ignored_metadata_keys = ignore_keys)
|
||||
|
||||
|
|
|
@ -1,21 +1,28 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Uranium is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional
|
||||
# Copyright (c) 2015-2022 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import json
|
||||
|
||||
from typing import Optional, cast, List, Dict
|
||||
|
||||
from UM.Mesh.MeshWriter import MeshWriter
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Logger import Logger
|
||||
from UM.Math.Matrix import Matrix
|
||||
from UM.Application import Application
|
||||
from UM.Message import Message
|
||||
from UM.Resources import Resources
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.CuraPackageManager import CuraPackageManager
|
||||
from cura.Utils.Threading import call_on_qt_thread
|
||||
from cura.Snapshot import Snapshot
|
||||
|
||||
from PyQt5.QtCore import QBuffer
|
||||
from PyQt6.QtCore import QBuffer
|
||||
|
||||
import Savitar
|
||||
import pySavitar as Savitar
|
||||
|
||||
import numpy
|
||||
import datetime
|
||||
|
@ -34,6 +41,9 @@ import UM.Application
|
|||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
THUMBNAIL_PATH = "Metadata/thumbnail.png"
|
||||
MODEL_PATH = "3D/3dmodel.model"
|
||||
PACKAGE_METADATA_PATH = "Cura/packages.json"
|
||||
|
||||
class ThreeMFWriter(MeshWriter):
|
||||
def __init__(self):
|
||||
|
@ -46,7 +56,7 @@ class ThreeMFWriter(MeshWriter):
|
|||
}
|
||||
|
||||
self._unit_matrix_string = self._convertMatrixToString(Matrix())
|
||||
self._archive = None # type: Optional[zipfile.ZipFile]
|
||||
self._archive: Optional[zipfile.ZipFile] = None
|
||||
self._store_archive = False
|
||||
|
||||
def _convertMatrixToString(self, matrix):
|
||||
|
@ -132,11 +142,11 @@ class ThreeMFWriter(MeshWriter):
|
|||
def getArchive(self):
|
||||
return self._archive
|
||||
|
||||
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
|
||||
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode) -> bool:
|
||||
self._archive = None # Reset archive
|
||||
archive = zipfile.ZipFile(stream, "w", compression = zipfile.ZIP_DEFLATED)
|
||||
try:
|
||||
model_file = zipfile.ZipInfo("3D/3dmodel.model")
|
||||
model_file = zipfile.ZipInfo(MODEL_PATH)
|
||||
# Because zipfile is stupid and ignores archive-level compression settings when writing with ZipInfo.
|
||||
model_file.compress_type = zipfile.ZIP_DEFLATED
|
||||
|
||||
|
@ -151,37 +161,41 @@ class ThreeMFWriter(MeshWriter):
|
|||
relations_file = zipfile.ZipInfo("_rels/.rels")
|
||||
relations_file.compress_type = zipfile.ZIP_DEFLATED
|
||||
relations_element = ET.Element("Relationships", xmlns = self._namespaces["relationships"])
|
||||
model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/3D/3dmodel.model", Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
|
||||
model_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/" + MODEL_PATH, Id = "rel0", Type = "http://schemas.microsoft.com/3dmanufacturing/2013/01/3dmodel")
|
||||
|
||||
# Attempt to add a thumbnail
|
||||
snapshot = self._createSnapshot()
|
||||
if snapshot:
|
||||
thumbnail_buffer = QBuffer()
|
||||
thumbnail_buffer.open(QBuffer.ReadWrite)
|
||||
thumbnail_buffer.open(QBuffer.OpenModeFlag.ReadWrite)
|
||||
snapshot.save(thumbnail_buffer, "PNG")
|
||||
|
||||
thumbnail_file = zipfile.ZipInfo("Metadata/thumbnail.png")
|
||||
thumbnail_file = zipfile.ZipInfo(THUMBNAIL_PATH)
|
||||
# Don't try to compress snapshot file, because the PNG is pretty much as compact as it will get
|
||||
archive.writestr(thumbnail_file, thumbnail_buffer.data())
|
||||
|
||||
# Add PNG to content types file
|
||||
thumbnail_type = ET.SubElement(content_types, "Default", Extension = "png", ContentType = "image/png")
|
||||
# Add thumbnail relation to _rels/.rels file
|
||||
thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/Metadata/thumbnail.png", Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
|
||||
thumbnail_relation_element = ET.SubElement(relations_element, "Relationship", Target = "/" + THUMBNAIL_PATH, Id = "rel1", Type = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail")
|
||||
|
||||
# Write material metadata
|
||||
material_metadata = self._getMaterialPackageMetadata()
|
||||
self._storeMetadataJson({"packages": material_metadata}, archive, PACKAGE_METADATA_PATH)
|
||||
|
||||
savitar_scene = Savitar.Scene()
|
||||
|
||||
metadata_to_store = CuraApplication.getInstance().getController().getScene().getMetaData()
|
||||
scene_metadata = CuraApplication.getInstance().getController().getScene().getMetaData()
|
||||
|
||||
for key, value in metadata_to_store.items():
|
||||
for key, value in scene_metadata.items():
|
||||
savitar_scene.setMetaDataEntry(key, value)
|
||||
|
||||
current_time_string = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
if "Application" not in metadata_to_store:
|
||||
if "Application" not in scene_metadata:
|
||||
# This might sound a bit strange, but this field should store the original application that created
|
||||
# the 3mf. So if it was already set, leave it to whatever it was.
|
||||
savitar_scene.setMetaDataEntry("Application", CuraApplication.getInstance().getApplicationDisplayName())
|
||||
if "CreationDate" not in metadata_to_store:
|
||||
if "CreationDate" not in scene_metadata:
|
||||
savitar_scene.setMetaDataEntry("CreationDate", current_time_string)
|
||||
|
||||
savitar_scene.setMetaDataEntry("ModificationDate", current_time_string)
|
||||
|
@ -233,6 +247,55 @@ class ThreeMFWriter(MeshWriter):
|
|||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _storeMetadataJson(metadata: Dict[str, List[Dict[str, str]]], archive: zipfile.ZipFile, path: str) -> None:
|
||||
"""Stores metadata inside archive path as json file"""
|
||||
metadata_file = zipfile.ZipInfo(path)
|
||||
# We have to set the compress type of each file as well (it doesn't keep the type of the entire archive)
|
||||
metadata_file.compress_type = zipfile.ZIP_DEFLATED
|
||||
archive.writestr(metadata_file, json.dumps(metadata, separators=(", ", ": "), indent=4, skipkeys=True, ensure_ascii=False))
|
||||
|
||||
@staticmethod
|
||||
def _getMaterialPackageMetadata() -> List[Dict[str, str]]:
|
||||
"""Get metadata for installed materials in active extruder stack, this does not include bundled materials.
|
||||
|
||||
:return: List of material metadata dictionaries.
|
||||
"""
|
||||
metadata = {}
|
||||
|
||||
package_manager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
|
||||
|
||||
for extruder in CuraApplication.getInstance().getExtruderManager().getActiveExtruderStacks():
|
||||
if not extruder.isEnabled:
|
||||
# Don't export materials not in use
|
||||
continue
|
||||
|
||||
if isinstance(extruder.material, type(ContainerRegistry.getInstance().getEmptyInstanceContainer())):
|
||||
# This is an empty material container, no material to export
|
||||
continue
|
||||
|
||||
if package_manager.isMaterialBundled(extruder.material.getFileName(), extruder.material.getMetaDataEntry("GUID")):
|
||||
# Don't export bundled materials
|
||||
continue
|
||||
|
||||
package_id = package_manager.getMaterialFilePackageId(extruder.material.getFileName(), extruder.material.getMetaDataEntry("GUID"))
|
||||
package_data = package_manager.getInstalledPackageInfo(package_id)
|
||||
|
||||
# We failed to find the package for this material
|
||||
if not package_data:
|
||||
Logger.info(f"Could not find package for material in extruder {extruder.id}, skipping.")
|
||||
continue
|
||||
|
||||
material_metadata = {"id": package_id,
|
||||
"display_name": package_data.get("display_name") if package_data.get("display_name") else "",
|
||||
"package_version": package_data.get("package_version") if package_data.get("package_version") else "",
|
||||
"sdk_version_semver": package_data.get("sdk_version_semver") if package_data.get("sdk_version_semver") else ""}
|
||||
|
||||
metadata[package_id] = material_metadata
|
||||
|
||||
# Storing in a dict and fetching values to avoid duplicates
|
||||
return list(metadata.values())
|
||||
|
||||
@call_on_qt_thread # must be called from the main thread because of OpenGL
|
||||
def _createSnapshot(self):
|
||||
Logger.log("d", "Creating thumbnail image...")
|
||||
|
|
|
@ -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 threading
|
|||
from datetime import datetime
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
from PyQt6.QtNetwork import QNetworkReply
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
from typing import Any, Optional, List, Dict, Callable
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
from PyQt6.QtNetwork import QNetworkReply
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import Signal, signalemitter
|
||||
|
|
|
@ -5,7 +5,7 @@ import os
|
|||
from datetime import datetime
|
||||
from typing import Any, cast, Dict, List, Optional
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
from PyQt6.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
|
||||
from UM.Extension import Extension
|
||||
from UM.Logger import Logger
|
||||
|
|
|
@ -7,7 +7,7 @@ import threading
|
|||
from tempfile import NamedTemporaryFile
|
||||
from typing import Optional, Any, Dict
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
||||
from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
|
@ -53,7 +53,7 @@ class RestoreBackupJob(Job):
|
|||
def _onRestoreRequestCompleted(self, reply: QNetworkReply, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
|
||||
if not HttpRequestManager.replyIndicatesSuccess(reply, error):
|
||||
Logger.warning("Requesting backup failed, response code %s while trying to connect to %s",
|
||||
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url())
|
||||
reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute), reply.url())
|
||||
self.restore_backup_error_message = self.DEFAULT_ERROR_MESSAGE
|
||||
self._job_done.set()
|
||||
return
|
||||
|
|
|
@ -17,7 +17,7 @@ RowLayout
|
|||
property alias label: detailName.text
|
||||
property alias value: detailValue.text
|
||||
|
||||
UM.RecolorImage
|
||||
UM.ColorImage
|
||||
{
|
||||
id: icon
|
||||
width: 18 * screenScaleFactor
|
||||
|
|
|
@ -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 {
|
||||
}
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
# Copyright (c) 2021-2022 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import argparse #To run the engine in debug mode if the front-end is in debug mode.
|
||||
from collections import defaultdict
|
||||
import os
|
||||
from PyQt5.QtCore import QObject, QTimer, QUrl, pyqtSlot
|
||||
from PyQt6.QtCore import QObject, QTimer, QUrl, pyqtSlot
|
||||
import sys
|
||||
from time import time
|
||||
from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtGui import QDesktopServices, QImage
|
||||
from PyQt6.QtGui import QDesktopServices, QImage
|
||||
|
||||
from UM.Backend.Backend import Backend, BackendState
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
@ -31,7 +31,7 @@ from cura.Utils.Threading import call_on_qt_thread
|
|||
from .ProcessSlicedLayersJob import ProcessSlicedLayersJob
|
||||
from .StartSliceJob import StartSliceJob, StartJobResult
|
||||
|
||||
import Arcus
|
||||
import pyArcus as Arcus
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
|
||||
|
@ -60,7 +60,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
executable_name = "CuraEngine"
|
||||
if Platform.isWindows():
|
||||
executable_name += ".exe"
|
||||
default_engine_location = executable_name
|
||||
self._default_engine_location = executable_name
|
||||
|
||||
search_path = [
|
||||
os.path.abspath(os.path.dirname(sys.executable)),
|
||||
|
@ -74,29 +74,29 @@ class CuraEngineBackend(QObject, Backend):
|
|||
for path in search_path:
|
||||
engine_path = os.path.join(path, executable_name)
|
||||
if os.path.isfile(engine_path):
|
||||
default_engine_location = engine_path
|
||||
self._default_engine_location = engine_path
|
||||
break
|
||||
|
||||
if Platform.isLinux() and not default_engine_location:
|
||||
if Platform.isLinux() and not self._default_engine_location:
|
||||
if not os.getenv("PATH"):
|
||||
raise OSError("There is something wrong with your Linux installation.")
|
||||
for pathdir in cast(str, os.getenv("PATH")).split(os.pathsep):
|
||||
execpath = os.path.join(pathdir, executable_name)
|
||||
if os.path.exists(execpath):
|
||||
default_engine_location = execpath
|
||||
self._default_engine_location = execpath
|
||||
break
|
||||
|
||||
application = CuraApplication.getInstance() #type: CuraApplication
|
||||
self._multi_build_plate_model = None #type: Optional[MultiBuildPlateModel]
|
||||
self._machine_error_checker = None #type: Optional[MachineErrorChecker]
|
||||
|
||||
if not default_engine_location:
|
||||
if not self._default_engine_location:
|
||||
raise EnvironmentError("Could not find CuraEngine")
|
||||
|
||||
Logger.log("i", "Found CuraEngine at: %s", default_engine_location)
|
||||
Logger.log("i", "Found CuraEngine at: %s", self._default_engine_location)
|
||||
|
||||
default_engine_location = os.path.abspath(default_engine_location)
|
||||
application.getPreferences().addPreference("backend/location", default_engine_location)
|
||||
self._default_engine_location = os.path.abspath(self._default_engine_location)
|
||||
application.getPreferences().addPreference("backend/location", self._default_engine_location)
|
||||
|
||||
# Workaround to disable layer view processing if layer view is not active.
|
||||
self._layer_view_active = False #type: bool
|
||||
|
@ -124,6 +124,7 @@ class CuraEngineBackend(QObject, Backend):
|
|||
self._message_handlers["cura.proto.Progress"] = self._onProgressMessage
|
||||
self._message_handlers["cura.proto.GCodeLayer"] = self._onGCodeLayerMessage
|
||||
self._message_handlers["cura.proto.GCodePrefix"] = self._onGCodePrefixMessage
|
||||
self._message_handlers["cura.proto.SliceUUID"] = self._onSliceUUIDMessage
|
||||
self._message_handlers["cura.proto.PrintTimeMaterialEstimates"] = self._onPrintTimeMaterialEstimates
|
||||
self._message_handlers["cura.proto.SlicingFinished"] = self._onSlicingFinishedMessage
|
||||
|
||||
|
@ -215,7 +216,12 @@ class CuraEngineBackend(QObject, Backend):
|
|||
This is useful for debugging and used to actually start the engine.
|
||||
:return: list of commands and args / parameters.
|
||||
"""
|
||||
command = [CuraApplication.getInstance().getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""]
|
||||
from cura import ApplicationMetadata
|
||||
if ApplicationMetadata.IsEnterpriseVersion:
|
||||
command = [self._default_engine_location]
|
||||
else:
|
||||
command = [CuraApplication.getInstance().getPreferences().getValue("backend/location")]
|
||||
command += ["connect", "127.0.0.1:{0}".format(self._port), ""]
|
||||
|
||||
parser = argparse.ArgumentParser(prog = "cura", add_help = False)
|
||||
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
|
||||
|
@ -807,6 +813,10 @@ class CuraEngineBackend(QObject, Backend):
|
|||
except KeyError: # Can occur if the g-code has been cleared while a slice message is still arriving from the other end.
|
||||
pass # Throw the message away.
|
||||
|
||||
def _onSliceUUIDMessage(self, message: Arcus.PythonMessage) -> None:
|
||||
application = CuraApplication.getInstance()
|
||||
application.getPrintInformation().slice_uuid = message.slice_uuid
|
||||
|
||||
def _createSocket(self, protocol_file: str = None) -> None:
|
||||
"""Creates a new socket connection."""
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
# Copyright (c) 2021-2022 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import numpy
|
||||
from string import Formatter
|
||||
|
@ -7,8 +7,8 @@ from enum import IntEnum
|
|||
import time
|
||||
from typing import Any, cast, Dict, List, Optional, Set
|
||||
import re
|
||||
import Arcus #For typing.
|
||||
from PyQt5.QtCore import QCoreApplication
|
||||
import pyArcus as Arcus # For typing.
|
||||
from PyQt6.QtCore import QCoreApplication
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Logger import Logger
|
||||
|
@ -94,7 +94,7 @@ class StartSliceJob(Job):
|
|||
super().__init__()
|
||||
|
||||
self._scene = CuraApplication.getInstance().getController().getScene() #type: Scene
|
||||
self._slice_message = slice_message #type: Arcus.PythonMessage
|
||||
self._slice_message: Arcus.PythonMessage = slice_message
|
||||
self._is_cancelled = False #type: bool
|
||||
self._build_plate_number = None #type: Optional[int]
|
||||
|
||||
|
@ -369,6 +369,9 @@ class StartSliceJob(Job):
|
|||
result["material_name"] = stack.material.getMetaDataEntry("name", "")
|
||||
result["material_brand"] = stack.material.getMetaDataEntry("brand", "")
|
||||
|
||||
result["quality_name"] = stack.quality.getMetaDataEntry("name", "")
|
||||
result["quality_changes_name"] = stack.qualityChanges.getMetaDataEntry("name")
|
||||
|
||||
# Renamed settings.
|
||||
result["print_bed_temperature"] = result["material_bed_temperature"]
|
||||
result["print_temperature"] = result["material_print_temperature"]
|
||||
|
@ -484,6 +487,10 @@ class StartSliceJob(Job):
|
|||
settings["machine_start_gcode"] = self._expandGcodeTokens(settings["machine_start_gcode"], initial_extruder_nr)
|
||||
settings["machine_end_gcode"] = self._expandGcodeTokens(settings["machine_end_gcode"], initial_extruder_nr)
|
||||
|
||||
# Manually add 'nozzle offsetting', since that is a metadata-entry instead for some reason.
|
||||
# NOTE: This probably needs to be an actual setting at some point.
|
||||
settings["nozzle_offsetting_for_disallowed_areas"] = CuraApplication.getInstance().getGlobalContainerStack().getMetaDataEntry("nozzle_offsetting_for_disallowed_areas", True)
|
||||
|
||||
# Add all sub-messages for each individual setting.
|
||||
for key, value in settings.items():
|
||||
setting_message = self._slice_message.getMessage("global_settings").addRepeatedMessage("settings")
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
//Copyright (C) 2022 Ultimaker B.V.
|
||||
//Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
import UM 1.2 as UM
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
import DigitalFactory 1.0 as DF
|
||||
|
@ -64,12 +64,10 @@ Popup
|
|||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: projectNameLabel
|
||||
text: "Project Name"
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
anchors
|
||||
{
|
||||
top: createNewLibraryProjectLabel.bottom
|
||||
|
@ -90,9 +88,9 @@ Popup
|
|||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
validator: RegExpValidator
|
||||
validator: RegularExpressionValidator
|
||||
{
|
||||
regExp: /^[^\\\/\*\?\|\[\]]{0,99}$/
|
||||
regularExpression: /^[^\\\/\*\?\|\[\]]{0,99}$/
|
||||
}
|
||||
|
||||
text: PrintInformation.jobName
|
||||
|
@ -107,13 +105,12 @@ Popup
|
|||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: errorWhileCreatingProjectLabel
|
||||
text: manager.projectCreationErrorText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("error")
|
||||
visible: manager.creatingNewProjectStatus == DF.RetrievalStatus.Failed
|
||||
anchors
|
||||
|
|
|
@ -30,14 +30,14 @@ Cura.RoundedRectangle
|
|||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
UM.RecolorImage
|
||||
UM.ColorImage
|
||||
{
|
||||
id: projectImage
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: UM.Theme.getSize("section").height
|
||||
height: width
|
||||
color: UM.Theme.getColor("text_link")
|
||||
source: "../images/arrow_down.svg"
|
||||
source: Qt.resolvedUrl("../images/arrow_down.svg")
|
||||
}
|
||||
|
||||
Label
|
||||
|
@ -65,7 +65,7 @@ Cura.RoundedRectangle
|
|||
{
|
||||
target: projectImage
|
||||
color: UM.Theme.getColor("text_link")
|
||||
source: "../images/arrow_down.svg"
|
||||
source: Qt.resolvedUrl("../images/arrow_down.svg")
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
|
@ -88,7 +88,7 @@ Cura.RoundedRectangle
|
|||
{
|
||||
target: projectImage
|
||||
color: UM.Theme.getColor("text_link")
|
||||
source: "../images/arrow_down.svg"
|
||||
source: Qt.resolvedUrl("../images/arrow_down.svg")
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
|
@ -111,7 +111,7 @@ Cura.RoundedRectangle
|
|||
{
|
||||
target: projectImage
|
||||
color: UM.Theme.getColor("action_button_disabled_text")
|
||||
source: "../images/update.svg"
|
||||
source: Qt.resolvedUrl("../images/update.svg")
|
||||
}
|
||||
PropertyChanges
|
||||
{
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
//Copyright (C) 2022 Ultimaker B.V.
|
||||
//Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import Qt.labs.qmlmodels 1.0
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
import UM 1.2 as UM
|
||||
import UM 1.6 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
import DigitalFactory 1.0 as DF
|
||||
|
@ -19,6 +18,7 @@ Item
|
|||
height: parent.height
|
||||
|
||||
property var fileModel: manager.digitalFactoryFileModel
|
||||
property var modelRows: manager.digitalFactoryFileModel.items
|
||||
|
||||
signal openFilePressed()
|
||||
signal selectDifferentProjectPressed()
|
||||
|
@ -57,21 +57,19 @@ Item
|
|||
border.width: UM.Theme.getSize("default_lining").width
|
||||
border.color: UM.Theme.getColor("lining")
|
||||
|
||||
//We can't use Cura's TableView here, since in Cura >= 5.0 this uses QtQuick.TableView, while in Cura < 5.0 this uses QtControls1.TableView.
|
||||
//So we have to define our own. Once support for 4.13 and earlier is dropped, we can switch to Cura.TableView.
|
||||
Table
|
||||
// This is not backwards compatible with Cura < 5.0 due to QT.labs being removed in PyQt6
|
||||
Cura.TableView
|
||||
{
|
||||
id: filesTableView
|
||||
anchors.fill: parent
|
||||
anchors.margins: parent.border.width
|
||||
|
||||
columnHeaders: ["Name", "Uploaded by", "Uploaded at"]
|
||||
model: TableModel
|
||||
model: UM.TableModel
|
||||
{
|
||||
TableModelColumn { display: "fileName" }
|
||||
TableModelColumn { display: "username" }
|
||||
TableModelColumn { display: "uploadedAt" }
|
||||
rows: manager.digitalFactoryFileModel.items
|
||||
id: tableModel
|
||||
headers: ["fileName", "username", "uploadedAt"]
|
||||
rows: modelRows
|
||||
}
|
||||
|
||||
onCurrentRowChanged:
|
||||
|
@ -85,13 +83,12 @@ Item
|
|||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: emptyProjectLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Select a project to view its files."
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("setting_category_text")
|
||||
|
||||
Connections
|
||||
|
@ -104,14 +101,13 @@ Item
|
|||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: noFilesInProjectLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible)
|
||||
text: "No supported files in this project."
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("setting_category_text")
|
||||
}
|
||||
|
||||
|
@ -179,4 +175,10 @@ Item
|
|||
openFilesButton.clicked.connect(base.openFilePressed)
|
||||
selectDifferentProjectButton.clicked.connect(base.selectDifferentProjectPressed)
|
||||
}
|
||||
|
||||
onModelRowsChanged:
|
||||
{
|
||||
tableModel.clear()
|
||||
tableModel.rows = modelRows
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
//Copyright (C) 2022 Ultimaker B.V.
|
||||
//Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import Qt.labs.qmlmodels 1.0
|
||||
import QtQuick 2.10
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
import UM 1.5 as UM
|
||||
import UM 1.6 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
import DigitalFactory 1.0 as DF
|
||||
|
@ -15,9 +14,14 @@ import DigitalFactory 1.0 as DF
|
|||
Item
|
||||
{
|
||||
id: base
|
||||
|
||||
property variant catalog: UM.I18nCatalog { name: "cura" }
|
||||
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
|
||||
property var fileModel: manager.digitalFactoryFileModel
|
||||
property var modelRows: manager.digitalFactoryFileModel.items
|
||||
|
||||
signal savePressed()
|
||||
signal selectDifferentProjectPressed()
|
||||
|
@ -43,14 +47,13 @@ Item
|
|||
cardMouseAreaEnabled: false
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: fileNameLabel
|
||||
anchors.top: projectSummaryCard.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
text: "Cura project name"
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
|
||||
|
@ -61,9 +64,9 @@ Item
|
|||
anchors.left: parent.left
|
||||
anchors.top: fileNameLabel.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("thin_margin").height
|
||||
validator: RegExpValidator
|
||||
validator: RegularExpressionValidator
|
||||
{
|
||||
regExp: /^[\w\-\. ()]{0,255}$/
|
||||
regularExpression: /^[\w\-\. ()]{0,255}$/
|
||||
}
|
||||
|
||||
text: PrintInformation.jobName
|
||||
|
@ -92,9 +95,8 @@ Item
|
|||
border.width: UM.Theme.getSize("default_lining").width
|
||||
border.color: UM.Theme.getColor("lining")
|
||||
|
||||
//We can't use Cura's TableView here, since in Cura >= 5.0 this uses QtQuick.TableView, while in Cura < 5.0 this uses QtControls1.TableView.
|
||||
//So we have to define our own. Once support for 4.13 and earlier is dropped, we can switch to Cura.TableView.
|
||||
Table
|
||||
// This is not backwards compatible with Cura < 5.0 due to QT.labs being removed in PyQt6
|
||||
Cura.TableView
|
||||
{
|
||||
id: filesTableView
|
||||
anchors.fill: parent
|
||||
|
@ -102,22 +104,20 @@ Item
|
|||
|
||||
allowSelection: false
|
||||
columnHeaders: ["Name", "Uploaded by", "Uploaded at"]
|
||||
model: TableModel
|
||||
model: UM.TableModel
|
||||
{
|
||||
TableModelColumn { display: "fileName" }
|
||||
TableModelColumn { display: "username" }
|
||||
TableModelColumn { display: "uploadedAt" }
|
||||
id: tableModel
|
||||
headers: ["fileName", "username", "uploadedAt"]
|
||||
rows: manager.digitalFactoryFileModel.items
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: emptyProjectLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Select a project to view its files."
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("setting_category_text")
|
||||
|
||||
Connections
|
||||
|
@ -130,14 +130,13 @@ Item
|
|||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: noFilesInProjectLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: (manager.digitalFactoryFileModel.count == 0 && !emptyProjectLabel.visible && !retrievingFilesBusyIndicator.visible)
|
||||
text: "No supported files in this project."
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("setting_category_text")
|
||||
}
|
||||
|
||||
|
@ -194,53 +193,29 @@ Item
|
|||
text: "Save"
|
||||
enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text.length >= 1 && dfFilenameTextfield.state !== 'invalid'
|
||||
|
||||
onClicked:
|
||||
{
|
||||
let saveAsFormats = [];
|
||||
if (asProjectCheckbox.checked)
|
||||
{
|
||||
saveAsFormats.push("3mf");
|
||||
}
|
||||
if (asSlicedCheckbox.checked)
|
||||
{
|
||||
saveAsFormats.push("ufp");
|
||||
}
|
||||
manager.saveFileToSelectedProject(dfFilenameTextfield.text, saveAsFormats);
|
||||
}
|
||||
onClicked: manager.saveFileToSelectedProject(dfFilenameTextfield.text, asProjectComboBox.currentValue)
|
||||
busy: false
|
||||
}
|
||||
|
||||
Row
|
||||
Cura.ComboBox
|
||||
{
|
||||
id: asProjectComboBox
|
||||
|
||||
id: saveAsFormatRow
|
||||
width: UM.Theme.getSize("combobox_wide").width
|
||||
height: saveButton.height
|
||||
anchors.verticalCenter: saveButton.verticalCenter
|
||||
anchors.right: saveButton.left
|
||||
anchors.rightMargin: UM.Theme.getSize("thin_margin").height
|
||||
width: childrenRect.width
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
|
||||
UM.CheckBox
|
||||
{
|
||||
id: asProjectCheckbox
|
||||
height: UM.Theme.getSize("checkbox").height
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
checked: true
|
||||
text: "Save Cura project"
|
||||
font: UM.Theme.getFont("medium")
|
||||
}
|
||||
enabled: UM.Backend.state == UM.Backend.Done
|
||||
currentIndex: UM.Backend.state == UM.Backend.Done ? 0 : 1
|
||||
textRole: "text"
|
||||
valueRole: "value"
|
||||
|
||||
UM.CheckBox
|
||||
{
|
||||
id: asSlicedCheckbox
|
||||
height: UM.Theme.getSize("checkbox").height
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
enabled: UM.Backend.state == UM.Backend.Done
|
||||
checked: UM.Backend.state == UM.Backend.Done
|
||||
text: "Save print file"
|
||||
font: UM.Theme.getFont("medium")
|
||||
}
|
||||
model: [
|
||||
{ text: catalog.i18nc("@option", "Save Cura project and print file"), key: "3mf_ufp", value: ["3mf", "ufp"] },
|
||||
{ text: catalog.i18nc("@option", "Save Cura project"), key: "3mf", value: ["3mf"] },
|
||||
]
|
||||
}
|
||||
|
||||
Component.onCompleted:
|
||||
|
@ -248,4 +223,10 @@ Item
|
|||
saveButton.clicked.connect(base.savePressed)
|
||||
selectDifferentProjectButton.clicked.connect(base.selectDifferentProjectPressed)
|
||||
}
|
||||
|
||||
onModelRowsChanged:
|
||||
{
|
||||
tableModel.clear()
|
||||
tableModel.rows = modelRows
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import QtQuick.Window 2.2
|
|||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.1
|
||||
|
||||
import UM 1.2 as UM
|
||||
import UM 1.6 as UM
|
||||
import Cura 1.7 as Cura
|
||||
|
||||
import DigitalFactory 1.0 as DF
|
||||
|
@ -56,7 +56,7 @@ Item
|
|||
id: createNewProjectButton
|
||||
|
||||
text: "New Library project"
|
||||
visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed)
|
||||
visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == 2 || manager.retrievingProjectsStatus == 3) // Status is succeeded or failed
|
||||
|
||||
onClicked:
|
||||
{
|
||||
|
@ -99,18 +99,17 @@ Item
|
|||
{
|
||||
id: digitalFactoryImage
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
source: searchBar.text === "" ? "../images/digital_factory.svg" : "../images/projects_not_found.svg"
|
||||
source: Qt.resolvedUrl(searchBar.text === "" ? "../images/digital_factory.svg" : "../images/projects_not_found.svg")
|
||||
fillMode: Image.PreserveAspectFit
|
||||
width: parent.width - 2 * UM.Theme.getSize("thick_margin").width
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: noLibraryProjectsLabel
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: searchBar.text === "" ? "It appears that you don't have any projects in the Library yet." : "No projects found that match the search query."
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
Cura.TertiaryButton
|
||||
|
@ -148,29 +147,7 @@ Item
|
|||
contentHeight: projectsListView.implicitHeight
|
||||
anchors.fill: parent
|
||||
|
||||
ScrollBar.vertical: ScrollBar
|
||||
{
|
||||
// Vertical ScrollBar, styled similarly to the scrollBar in the settings panel
|
||||
id: verticalScrollBar
|
||||
visible: flickableView.contentHeight > flickableView.height
|
||||
|
||||
background: Rectangle
|
||||
{
|
||||
implicitWidth: UM.Theme.getSize("scrollbar").width
|
||||
radius: Math.round(implicitWidth / 2)
|
||||
color: UM.Theme.getColor("scrollbar_background")
|
||||
}
|
||||
|
||||
contentItem: Rectangle
|
||||
{
|
||||
id: scrollViewHandle
|
||||
implicitWidth: UM.Theme.getSize("scrollbar").width
|
||||
radius: Math.round(implicitWidth / 2)
|
||||
|
||||
color: verticalScrollBar.pressed ? UM.Theme.getColor("scrollbar_handle_down") : verticalScrollBar.hovered ? UM.Theme.getColor("scrollbar_handle_hover") : UM.Theme.getColor("scrollbar_handle")
|
||||
Behavior on color { ColorAnimation { duration: 50; } }
|
||||
}
|
||||
}
|
||||
ScrollBar.vertical: UM.ScrollBar { id: verticalScrollBar }
|
||||
|
||||
Column
|
||||
{
|
||||
|
|
|
@ -1,203 +0,0 @@
|
|||
//Copyright (C) 2022 Ultimaker B.V.
|
||||
//Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import Qt.labs.qmlmodels 1.0
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
|
||||
import UM 1.2 as UM
|
||||
|
||||
/*
|
||||
* A re-sizeable table of data.
|
||||
*
|
||||
* This table combines a list of headers with a TableView to show certain roles in a table.
|
||||
* The columns of the table can be resized.
|
||||
* When the table becomes too big, you can scroll through the table. When a column becomes too small, the contents of
|
||||
* the table are elided.
|
||||
* The table gets Cura's themeing.
|
||||
*/
|
||||
Item
|
||||
{
|
||||
id: tableBase
|
||||
|
||||
required property var columnHeaders //The text to show in the headers of each column.
|
||||
property alias model: tableView.model //A TableModel to display in this table. To use a ListModel for the rows, use "rows: listModel.items"
|
||||
property int currentRow: -1 //The selected row index.
|
||||
property var onDoubleClicked: function(row) {} //Something to execute when double clicked. Accepts one argument: The index of the row that was clicked on.
|
||||
property bool allowSelection: true //Whether to allow the user to select items.
|
||||
|
||||
Row
|
||||
{
|
||||
id: headerBar
|
||||
Repeater
|
||||
{
|
||||
id: headerRepeater
|
||||
model: columnHeaders
|
||||
Rectangle
|
||||
{
|
||||
//minimumWidth: Math.max(1, Math.round(tableBase.width / headerRepeater.count))
|
||||
width: 300
|
||||
height: UM.Theme.getSize("section").height
|
||||
|
||||
color: UM.Theme.getColor("secondary")
|
||||
|
||||
Label
|
||||
{
|
||||
id: contentText
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: UM.Theme.getSize("narrow_margin").width
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
text: modelData
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
Rectangle //Resize handle.
|
||||
{
|
||||
anchors
|
||||
{
|
||||
right: parent.right
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
width: UM.Theme.getSize("thick_lining").width
|
||||
|
||||
color: UM.Theme.getColor("thick_lining")
|
||||
|
||||
MouseArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
|
||||
cursorShape: Qt.SizeHorCursor
|
||||
drag
|
||||
{
|
||||
target: parent
|
||||
axis: Drag.XAxis
|
||||
}
|
||||
onMouseXChanged:
|
||||
{
|
||||
if(drag.active)
|
||||
{
|
||||
let new_width = parent.parent.width + mouseX;
|
||||
let sum_widths = mouseX;
|
||||
for(let i = 0; i < headerBar.children.length; ++i)
|
||||
{
|
||||
sum_widths += headerBar.children[i].width;
|
||||
}
|
||||
if(sum_widths > tableBase.width)
|
||||
{
|
||||
new_width -= sum_widths - tableBase.width; //Limit the total width to not exceed the view.
|
||||
}
|
||||
let width_fraction = new_width / tableBase.width; //Scale with the same fraction along with the total width, if the table is resized.
|
||||
parent.parent.width = Qt.binding(function() { return Math.max(10, Math.round(tableBase.width * width_fraction)) });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onWidthChanged:
|
||||
{
|
||||
tableView.forceLayout(); //Rescale table cells underneath as well.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TableView
|
||||
{
|
||||
id: tableView
|
||||
anchors
|
||||
{
|
||||
top: headerBar.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
|
||||
flickableDirection: Flickable.AutoFlickIfNeeded
|
||||
clip: true
|
||||
ScrollBar.vertical: ScrollBar
|
||||
{
|
||||
// Vertical ScrollBar, styled similarly to the scrollBar in the settings panel
|
||||
id: verticalScrollBar
|
||||
visible: tableView.contentHeight > tableView.height
|
||||
|
||||
background: Rectangle
|
||||
{
|
||||
implicitWidth: UM.Theme.getSize("scrollbar").width
|
||||
radius: Math.round(implicitWidth / 2)
|
||||
color: UM.Theme.getColor("scrollbar_background")
|
||||
}
|
||||
|
||||
contentItem: Rectangle
|
||||
{
|
||||
id: scrollViewHandle
|
||||
implicitWidth: UM.Theme.getSize("scrollbar").width
|
||||
radius: Math.round(implicitWidth / 2)
|
||||
|
||||
color: verticalScrollBar.pressed ? UM.Theme.getColor("scrollbar_handle_down") : verticalScrollBar.hovered ? UM.Theme.getColor("scrollbar_handle_hover") : UM.Theme.getColor("scrollbar_handle")
|
||||
Behavior on color { ColorAnimation { duration: 50; } }
|
||||
}
|
||||
}
|
||||
columnWidthProvider: function(column)
|
||||
{
|
||||
return headerBar.children[column].width; //Cells get the same width as their column header.
|
||||
}
|
||||
|
||||
delegate: Rectangle
|
||||
{
|
||||
implicitHeight: Math.max(1, cellContent.height)
|
||||
|
||||
color: UM.Theme.getColor((tableBase.currentRow == row) ? "primary" : ((row % 2 == 0) ? "main_background" : "viewport_background"))
|
||||
|
||||
Label
|
||||
{
|
||||
id: cellContent
|
||||
width: parent.width
|
||||
|
||||
text: display
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
elide: Text.ElideRight
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
TextMetrics
|
||||
{
|
||||
id: cellTextMetrics
|
||||
text: cellContent.text
|
||||
font: cellContent.font
|
||||
elide: cellContent.elide
|
||||
elideWidth: cellContent.width
|
||||
}
|
||||
UM.TooltipArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
|
||||
acceptedButtons: Qt.LeftButton
|
||||
text: (cellTextMetrics.elidedText == cellContent.text) ? "" : cellContent.text //Show full text in tooltip if it was elided.
|
||||
onClicked:
|
||||
{
|
||||
if(tableBase.allowSelection)
|
||||
{
|
||||
tableBase.currentRow = row; //Select this row.
|
||||
}
|
||||
}
|
||||
onDoubleClicked:
|
||||
{
|
||||
tableBase.onDoubleClicked(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections
|
||||
{
|
||||
target: model
|
||||
function onRowCountChanged()
|
||||
{
|
||||
tableView.contentY = 0; //When the number of rows is reduced, make sure to scroll back to the start.
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,13 +1,14 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Copyright (c) 2022 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import json
|
||||
import threading
|
||||
from json import JSONDecodeError
|
||||
from typing import List, Dict, Any, Callable, Union, Optional
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
from PyQt6.QtCore import QUrl
|
||||
from PyQt6.QtGui import QDesktopServices
|
||||
from PyQt6.QtNetwork import QNetworkReply
|
||||
|
||||
from UM.FileHandler.FileHandler import FileHandler
|
||||
from UM.Logger import Logger
|
||||
|
@ -37,17 +38,18 @@ class DFFileExportAndUploadManager:
|
|||
formats: List[str],
|
||||
on_upload_error: Callable[[], Any],
|
||||
on_upload_success: Callable[[], Any],
|
||||
on_upload_finished: Callable[[], Any] ,
|
||||
on_upload_finished: Callable[[], Any],
|
||||
on_upload_progress: Callable[[int], Any]) -> None:
|
||||
|
||||
self._file_handlers = file_handlers # type: Dict[str, FileHandler]
|
||||
self._nodes = nodes # type: List[SceneNode]
|
||||
self._library_project_id = library_project_id # type: str
|
||||
self._library_project_name = library_project_name # type: str
|
||||
self._file_name = file_name # type: str
|
||||
self._upload_jobs = [] # type: List[ExportFileJob]
|
||||
self._formats = formats # type: List[str]
|
||||
self._file_handlers: Dict[str, FileHandler] = file_handlers
|
||||
self._nodes: List[SceneNode] = nodes
|
||||
self._library_project_id: str = library_project_id
|
||||
self._library_project_name: str = library_project_name
|
||||
self._file_name: str = file_name
|
||||
self._upload_jobs: List[ExportFileJob] = []
|
||||
self._formats: List[str] = formats
|
||||
self._api = DigitalFactoryApiClient(application = CuraApplication.getInstance(), on_error = lambda error: Logger.log("e", str(error)))
|
||||
self._source_file_id: Optional[str] = None
|
||||
|
||||
# Functions of the parent class that should be called based on the upload process output
|
||||
self._on_upload_error = on_upload_error
|
||||
|
@ -59,7 +61,7 @@ class DFFileExportAndUploadManager:
|
|||
# show the success message (once both upload jobs are done)
|
||||
self._message_lock = threading.Lock()
|
||||
|
||||
self._file_upload_job_metadata = self.initializeFileUploadJobMetadata() # type: Dict[str, Dict[str, Any]]
|
||||
self._file_upload_job_metadata: Dict[str, Dict[str, Any]] = self.initializeFileUploadJobMetadata()
|
||||
|
||||
self.progress_message = Message(
|
||||
title = "Uploading...",
|
||||
|
@ -113,7 +115,8 @@ class DFFileExportAndUploadManager:
|
|||
content_type = job.getMimeType(),
|
||||
job_name = job.getFileName(),
|
||||
file_size = len(job.getOutput()),
|
||||
library_project_id = self._library_project_id
|
||||
library_project_id = self._library_project_id,
|
||||
source_file_id = self._source_file_id
|
||||
)
|
||||
self._api.requestUploadUFP(request, on_finished = self._uploadFileData, on_error = self._onRequestUploadPrintFileFailed)
|
||||
|
||||
|
@ -125,11 +128,17 @@ class DFFileExportAndUploadManager:
|
|||
"""
|
||||
if isinstance(file_upload_response, DFLibraryFileUploadResponse):
|
||||
file_name = file_upload_response.file_name
|
||||
|
||||
# store the `file_id` so it can be as `source_file_id` when uploading the print file
|
||||
self._source_file_id = file_upload_response.file_id
|
||||
elif isinstance(file_upload_response, DFPrintJobUploadResponse):
|
||||
file_name = file_upload_response.job_name if file_upload_response.job_name is not None else ""
|
||||
else:
|
||||
Logger.log("e", "Wrong response type received. Aborting uploading file to the Digital Library")
|
||||
return
|
||||
if file_name not in self._file_upload_job_metadata:
|
||||
Logger.error(f"API response for uploading doesn't match the file name we just uploaded: {file_name} was never uploaded.")
|
||||
return
|
||||
with self._message_lock:
|
||||
self.progress_message.show()
|
||||
self._file_upload_job_metadata[file_name]["file_upload_response"] = file_upload_response
|
||||
|
@ -145,6 +154,8 @@ class DFFileExportAndUploadManager:
|
|||
on_progress = self._onUploadProgress,
|
||||
on_error = self._onUploadError)
|
||||
|
||||
self._handleNextUploadJob()
|
||||
|
||||
def _onUploadProgress(self, filename: str, progress: int) -> None:
|
||||
"""
|
||||
Updates the progress message according to the total progress of the two files and displays it to the user. It is
|
||||
|
@ -325,8 +336,14 @@ class DFFileExportAndUploadManager:
|
|||
message.hide()
|
||||
|
||||
def start(self) -> None:
|
||||
for job in self._upload_jobs:
|
||||
self._handleNextUploadJob()
|
||||
|
||||
def _handleNextUploadJob(self):
|
||||
try:
|
||||
job = self._upload_jobs.pop(0)
|
||||
job.start()
|
||||
except IndexError:
|
||||
pass # Empty list, do nothing.
|
||||
|
||||
def initializeFileUploadJobMetadata(self) -> Dict[str, Any]:
|
||||
metadata = {}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
|
||||
from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
|
||||
from typing import Callable, Any, cast, Optional, Union
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
@ -39,8 +39,8 @@ class DFFileUploader:
|
|||
:param on_error: The method to be called when an error occurs.
|
||||
"""
|
||||
|
||||
self._http = http # type: HttpRequestManager
|
||||
self._df_file = df_file # type: Union[DFLibraryFileUploadResponse, DFPrintJobUploadResponse]
|
||||
self._http: HttpRequestManager = http
|
||||
self._df_file: Union[DFLibraryFileUploadResponse, DFPrintJobUploadResponse] = df_file
|
||||
self._file_name = ""
|
||||
if isinstance(self._df_file, DFLibraryFileUploadResponse):
|
||||
self._file_name = self._df_file.file_name
|
||||
|
@ -51,7 +51,7 @@ class DFFileUploader:
|
|||
self._file_name = ""
|
||||
else:
|
||||
raise TypeError("Incorrect input type")
|
||||
self._data = data # type: bytes
|
||||
self._data: bytes = data
|
||||
|
||||
self._on_finished = on_finished
|
||||
self._on_success = on_success
|
||||
|
@ -120,9 +120,9 @@ class DFFileUploader:
|
|||
"""
|
||||
|
||||
Logger.log("i", "Finished callback %s %s",
|
||||
reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString())
|
||||
reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute), reply.url().toString())
|
||||
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) # type: Optional[int]
|
||||
status_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) # type: Optional[int]
|
||||
if not status_code:
|
||||
Logger.log("e", "Reply contained no status code.")
|
||||
self._onUploadError(reply, None)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -7,7 +7,7 @@ import re
|
|||
from time import time
|
||||
from typing import List, Any, Optional, Union, Type, Tuple, Dict, cast, TypeVar, Callable
|
||||
|
||||
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
|
||||
from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
|
||||
|
@ -40,7 +40,7 @@ class DigitalFactoryApiClient:
|
|||
DEFAULT_REQUEST_TIMEOUT = 10 # seconds
|
||||
|
||||
# In order to avoid garbage collection we keep the callbacks in this list.
|
||||
_anti_gc_callbacks = [] # type: List[Callable[[Any], None]]
|
||||
_anti_gc_callbacks: List[Callable[[Any], None]] = []
|
||||
|
||||
def __init__(self, application: CuraApplication, on_error: Callable[[List[CloudError]], None], projects_limit_per_page: Optional[int] = None) -> None:
|
||||
"""Initializes a new digital factory API client.
|
||||
|
@ -54,7 +54,7 @@ class DigitalFactoryApiClient:
|
|||
self._scope = JsonDecoratorScope(UltimakerCloudScope(application))
|
||||
self._http = HttpRequestManager.getInstance()
|
||||
self._on_error = on_error
|
||||
self._file_uploader = None # type: Optional[DFFileUploader]
|
||||
self._file_uploader: Optional[DFFileUploader] = None
|
||||
self._library_max_private_projects: Optional[int] = None
|
||||
|
||||
self._projects_pagination_mgr = PaginationManager(limit = projects_limit_per_page) if projects_limit_per_page else None # type: Optional[PaginationManager]
|
||||
|
@ -71,8 +71,6 @@ class DigitalFactoryApiClient:
|
|||
has_access = response.library_max_private_projects == -1 or response.library_max_private_projects > 0
|
||||
callback(has_access)
|
||||
self._library_max_private_projects = response.library_max_private_projects
|
||||
# update the account with the additional user rights
|
||||
self._account.updateAdditionalRight(df_access = has_access)
|
||||
else:
|
||||
Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}")
|
||||
callback(False)
|
||||
|
@ -228,7 +226,7 @@ class DigitalFactoryApiClient:
|
|||
self._anti_gc_callbacks.remove(parse)
|
||||
|
||||
# Don't try to parse the reply if we didn't get one
|
||||
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) is None:
|
||||
if reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) is None:
|
||||
if on_error is not None:
|
||||
on_error()
|
||||
return
|
||||
|
@ -250,7 +248,7 @@ class DigitalFactoryApiClient:
|
|||
:return: A tuple with a status code and a dictionary.
|
||||
"""
|
||||
|
||||
status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
||||
status_code = reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute)
|
||||
try:
|
||||
response = bytes(reply.readAll()).decode()
|
||||
return status_code, json.loads(response)
|
||||
|
|
|
@ -10,9 +10,9 @@ from enum import IntEnum
|
|||
from pathlib import Path
|
||||
from typing import Optional, List, Dict, Any, cast
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, Q_ENUMS, QTimer, QUrl
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
from PyQt5.QtQml import qmlRegisterType, qmlRegisterUncreatableType
|
||||
from PyQt6.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, pyqtEnum, QTimer, QUrl, QMetaObject
|
||||
from PyQt6.QtNetwork import QNetworkReply
|
||||
from PyQt6.QtQml import qmlRegisterType, qmlRegisterUncreatableMetaObject
|
||||
|
||||
from UM.FileHandler.FileHandler import FileHandler
|
||||
from UM.Logger import Logger
|
||||
|
@ -32,26 +32,6 @@ from .DigitalFactoryProjectModel import DigitalFactoryProjectModel
|
|||
from .DigitalFactoryProjectResponse import DigitalFactoryProjectResponse
|
||||
|
||||
|
||||
class RetrievalStatus(IntEnum):
|
||||
"""
|
||||
The status of an http get request.
|
||||
|
||||
This is not an enum, because we want to use it in QML and QML doesn't recognize Python enums.
|
||||
"""
|
||||
Idle = 0
|
||||
InProgress = 1
|
||||
Success = 2
|
||||
Failed = 3
|
||||
|
||||
|
||||
class DFRetrievalStatus(QObject):
|
||||
"""
|
||||
Used as an intermediate QObject that registers the RetrievalStatus as a recognizable enum in QML, so that it can
|
||||
be used within QML objects as DigitalFactory.RetrievalStatus.<status>
|
||||
"""
|
||||
|
||||
Q_ENUMS(RetrievalStatus)
|
||||
|
||||
|
||||
class DigitalFactoryController(QObject):
|
||||
|
||||
|
@ -98,6 +78,19 @@ class DigitalFactoryController(QObject):
|
|||
"""Signal to inform whether the user is allowed to create more Library projects."""
|
||||
userCanCreateNewLibraryProjectChanged = pyqtSignal(bool)
|
||||
|
||||
class RetrievalStatus(IntEnum):
|
||||
"""
|
||||
The status of an http get request.
|
||||
|
||||
This is not an enum, because we want to use it in QML and QML doesn't recognize Python enums.
|
||||
"""
|
||||
Idle = 0
|
||||
InProgress = 1
|
||||
Success = 2
|
||||
Failed = 3
|
||||
|
||||
pyqtEnum(RetrievalStatus)
|
||||
|
||||
def __init__(self, application: CuraApplication) -> None:
|
||||
super().__init__(parent = None)
|
||||
|
||||
|
@ -139,9 +132,9 @@ class DigitalFactoryController(QObject):
|
|||
self._erase_temp_files_lock = threading.Lock()
|
||||
|
||||
# The statuses which indicate whether Cura is waiting for a response from the DigitalFactory API
|
||||
self.retrieving_files_status = RetrievalStatus.Idle
|
||||
self.retrieving_projects_status = RetrievalStatus.Idle
|
||||
self.creating_new_project_status = RetrievalStatus.Idle
|
||||
self.retrieving_files_status = self.RetrievalStatus.Idle
|
||||
self.retrieving_projects_status = self.RetrievalStatus.Idle
|
||||
self.creating_new_project_status = self.RetrievalStatus.Idle
|
||||
|
||||
self._application.engineCreatedSignal.connect(self._onEngineCreated)
|
||||
self._application.initializationFinished.connect(self._applicationInitializationFinished)
|
||||
|
@ -155,9 +148,9 @@ class DigitalFactoryController(QObject):
|
|||
self._has_preselected_project = False
|
||||
self.preselectedProjectChanged.emit()
|
||||
|
||||
self.setRetrievingFilesStatus(RetrievalStatus.Idle)
|
||||
self.setRetrievingProjectsStatus(RetrievalStatus.Idle)
|
||||
self.setCreatingNewProjectStatus(RetrievalStatus.Idle)
|
||||
self.setRetrievingFilesStatus(self.RetrievalStatus.Idle)
|
||||
self.setRetrievingProjectsStatus(self.RetrievalStatus.Idle)
|
||||
self.setCreatingNewProjectStatus(self.RetrievalStatus.Idle)
|
||||
|
||||
self.setSelectedProjectIndex(-1)
|
||||
|
||||
|
@ -182,7 +175,7 @@ class DigitalFactoryController(QObject):
|
|||
self.clear()
|
||||
|
||||
if self._account.isLoggedIn and self.userAccountHasLibraryAccess():
|
||||
self.setRetrievingProjectsStatus(RetrievalStatus.InProgress)
|
||||
self.setRetrievingProjectsStatus(self.RetrievalStatus.InProgress)
|
||||
if preselected_project_id:
|
||||
self._api.getProject(preselected_project_id, on_finished = self.setProjectAsPreselected, failed = self._onGetProjectFailed)
|
||||
else:
|
||||
|
@ -199,8 +192,8 @@ class DigitalFactoryController(QObject):
|
|||
self._project_model.setProjects([df_project])
|
||||
self.setSelectedProjectIndex(0)
|
||||
self.setHasPreselectedProject(True)
|
||||
self.setRetrievingProjectsStatus(RetrievalStatus.Success)
|
||||
self.setCreatingNewProjectStatus(RetrievalStatus.Success)
|
||||
self.setRetrievingProjectsStatus(self.RetrievalStatus.Success)
|
||||
self.setCreatingNewProjectStatus(self.RetrievalStatus.Success)
|
||||
|
||||
def _onGetProjectFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
|
||||
reply_string = bytes(reply.readAll()).decode()
|
||||
|
@ -216,7 +209,7 @@ class DigitalFactoryController(QObject):
|
|||
"""
|
||||
self.setHasMoreProjectsToLoad(self._api.hasMoreProjectsToLoad())
|
||||
self._project_model.setProjects(df_projects)
|
||||
self.setRetrievingProjectsStatus(RetrievalStatus.Success)
|
||||
self.setRetrievingProjectsStatus(self.RetrievalStatus.Success)
|
||||
|
||||
@pyqtSlot()
|
||||
def loadMoreProjects(self) -> None:
|
||||
|
@ -224,7 +217,7 @@ class DigitalFactoryController(QObject):
|
|||
Initiates the process of retrieving the next page of the projects list from the API.
|
||||
"""
|
||||
self._api.getMoreProjects(on_finished = self.loadMoreProjectsFinished, failed = self._onGetProjectsFailed)
|
||||
self.setRetrievingProjectsStatus(RetrievalStatus.InProgress)
|
||||
self.setRetrievingProjectsStatus(self.RetrievalStatus.InProgress)
|
||||
|
||||
def loadMoreProjectsFinished(self, df_projects: List[DigitalFactoryProjectResponse]) -> None:
|
||||
"""
|
||||
|
@ -235,13 +228,13 @@ class DigitalFactoryController(QObject):
|
|||
"""
|
||||
self.setHasMoreProjectsToLoad(self._api.hasMoreProjectsToLoad())
|
||||
self._project_model.extendProjects(df_projects)
|
||||
self.setRetrievingProjectsStatus(RetrievalStatus.Success)
|
||||
self.setRetrievingProjectsStatus(self.RetrievalStatus.Success)
|
||||
|
||||
def _onGetProjectsFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
|
||||
"""
|
||||
Error function, called whenever the retrieval of projects fails.
|
||||
"""
|
||||
self.setRetrievingProjectsStatus(RetrievalStatus.Failed)
|
||||
self.setRetrievingProjectsStatus(self.RetrievalStatus.Failed)
|
||||
Logger.log("w", "Failed to retrieve the list of projects from the Digital Library. Error encountered: {}".format(error))
|
||||
|
||||
def getProjectFilesFinished(self, df_files_in_project: List[DigitalFactoryFileResponse]) -> None:
|
||||
|
@ -255,7 +248,7 @@ class DigitalFactoryController(QObject):
|
|||
# Filter to show only the files that can be opened in Cura
|
||||
self._file_model.setFilters({"file_name": lambda x: Path(x).suffix[1:].lower() in self._supported_file_types}) # the suffix is in format '.xyz', so omit the dot at the start
|
||||
self._file_model.setFiles(df_files_in_project)
|
||||
self.setRetrievingFilesStatus(RetrievalStatus.Success)
|
||||
self.setRetrievingFilesStatus(self.RetrievalStatus.Success)
|
||||
|
||||
def getProjectFilesFailed(self, reply: QNetworkReply, error: QNetworkReply.NetworkError) -> None:
|
||||
"""
|
||||
|
@ -265,7 +258,7 @@ class DigitalFactoryController(QObject):
|
|||
Logger.warning(f"Failed to retrieve the list of files in project '{self._project_model._projects[self._selected_project_idx]}' from the Digital Library")
|
||||
except IndexError:
|
||||
Logger.warning(f"Failed to retrieve the list of files in a project from the Digital Library. And failed to get the project too.")
|
||||
self.setRetrievingFilesStatus(RetrievalStatus.Failed)
|
||||
self.setRetrievingFilesStatus(self.RetrievalStatus.Failed)
|
||||
|
||||
@pyqtSlot()
|
||||
def clearProjectSelection(self) -> None:
|
||||
|
@ -297,7 +290,7 @@ class DigitalFactoryController(QObject):
|
|||
self.selectedFileIndicesChanged.emit([])
|
||||
if 0 <= project_idx < len(self._project_model.items):
|
||||
library_project_id = self._project_model.items[project_idx]["libraryProjectId"]
|
||||
self.setRetrievingFilesStatus(RetrievalStatus.InProgress)
|
||||
self.setRetrievingFilesStatus(self.RetrievalStatus.InProgress)
|
||||
self._api.getListOfFilesInProject(library_project_id, on_finished = self.getProjectFilesFinished, failed = self.getProjectFilesFailed)
|
||||
|
||||
@pyqtProperty(int, fset = setSelectedProjectIndex, notify = selectedProjectIndexChanged)
|
||||
|
@ -381,7 +374,7 @@ class DigitalFactoryController(QObject):
|
|||
"""
|
||||
if project_name:
|
||||
self._api.createNewProject(project_name, self.setProjectAsPreselected, self._createNewLibraryProjectFailed)
|
||||
self.setCreatingNewProjectStatus(RetrievalStatus.InProgress)
|
||||
self.setCreatingNewProjectStatus(self.RetrievalStatus.InProgress)
|
||||
else:
|
||||
Logger.log("w", "No project name provided while attempting to create a new project. Aborting the project creation.")
|
||||
|
||||
|
@ -395,7 +388,7 @@ class DigitalFactoryController(QObject):
|
|||
self._project_creation_error_text = "Error while creating the new project: {}".format(reply_dict["errors"][0]["title"])
|
||||
self.projectCreationErrorTextChanged.emit()
|
||||
|
||||
self.setCreatingNewProjectStatus(RetrievalStatus.Failed)
|
||||
self.setCreatingNewProjectStatus(self.RetrievalStatus.Failed)
|
||||
Logger.log("e", "Something went wrong while trying to create a new a project. Error: {}".format(reply_string))
|
||||
|
||||
def setRetrievingProjectsStatus(self, new_status: RetrievalStatus) -> None:
|
||||
|
@ -439,7 +432,7 @@ class DigitalFactoryController(QObject):
|
|||
|
||||
@staticmethod
|
||||
def _onEngineCreated() -> None:
|
||||
qmlRegisterUncreatableType(DFRetrievalStatus, "DigitalFactory", 1, 0, "RetrievalStatus", "Could not create RetrievalStatus enum type")
|
||||
qmlRegisterUncreatableMetaObject(DigitalFactoryController.staticMetaObject, "DigitalFactory", 1, 0, "RetrievalStatus", "RetrievalStatus is an Enum-only type")
|
||||
|
||||
def _applicationInitializationFinished(self) -> None:
|
||||
self._supported_file_types = self._application.getInstance().getMeshFileHandler().getSupportedFileTypesRead()
|
||||
|
@ -565,7 +558,7 @@ class DigitalFactoryController(QObject):
|
|||
self.setSelectedProjectIndex(-1)
|
||||
self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
|
||||
self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject)
|
||||
self.setRetrievingProjectsStatus(RetrievalStatus.InProgress)
|
||||
self.setRetrievingProjectsStatus(self.RetrievalStatus.InProgress)
|
||||
self._has_preselected_project = new_has_preselected_project
|
||||
self.preselectedProjectChanged.emit()
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import List, Dict, Callable
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
@ -13,13 +13,13 @@ DIGITAL_FACTORY_DISPLAY_DATETIME_FORMAT = "%d-%m-%Y %H:%M"
|
|||
|
||||
|
||||
class DigitalFactoryFileModel(ListModel):
|
||||
FileNameRole = Qt.UserRole + 1
|
||||
FileIdRole = Qt.UserRole + 2
|
||||
FileSizeRole = Qt.UserRole + 3
|
||||
LibraryProjectIdRole = Qt.UserRole + 4
|
||||
DownloadUrlRole = Qt.UserRole + 5
|
||||
UsernameRole = Qt.UserRole + 6
|
||||
UploadedAtRole = Qt.UserRole + 7
|
||||
FileNameRole = Qt.ItemDataRole.UserRole + 1
|
||||
FileIdRole = Qt.ItemDataRole.UserRole + 2
|
||||
FileSizeRole = Qt.ItemDataRole.UserRole + 3
|
||||
LibraryProjectIdRole = Qt.ItemDataRole.UserRole + 4
|
||||
DownloadUrlRole = Qt.ItemDataRole.UserRole + 5
|
||||
UsernameRole = Qt.ItemDataRole.UserRole + 6
|
||||
UploadedAtRole = Qt.ItemDataRole.UserRole + 7
|
||||
|
||||
dfFileModelChanged = pyqtSignal()
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import List, Optional
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal
|
||||
from PyQt6.QtCore import Qt, pyqtSignal
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
@ -12,12 +12,12 @@ PROJECT_UPDATED_AT_DATETIME_FORMAT = "%d-%m-%Y"
|
|||
|
||||
|
||||
class DigitalFactoryProjectModel(ListModel):
|
||||
DisplayNameRole = Qt.UserRole + 1
|
||||
LibraryProjectIdRole = Qt.UserRole + 2
|
||||
DescriptionRole = Qt.UserRole + 3
|
||||
ThumbnailUrlRole = Qt.UserRole + 5
|
||||
UsernameRole = Qt.UserRole + 6
|
||||
LastUpdatedRole = Qt.UserRole + 7
|
||||
DisplayNameRole = Qt.ItemDataRole.UserRole + 1
|
||||
LibraryProjectIdRole = Qt.ItemDataRole.UserRole + 2
|
||||
DescriptionRole = Qt.ItemDataRole.UserRole + 3
|
||||
ThumbnailUrlRole = Qt.ItemDataRole.UserRole + 5
|
||||
UsernameRole = Qt.ItemDataRole.UserRole + 6
|
||||
LastUpdatedRole = Qt.ItemDataRole.UserRole + 7
|
||||
|
||||
dfProjectModelChanged = pyqtSignal()
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from PyQt6.QtCore import QUrl
|
||||
from PyQt6.QtGui import QDesktopServices
|
||||
|
||||
from typing import Set
|
||||
|
||||
|
|
|
@ -3,6 +3,6 @@
|
|||
"author": "Ultimaker B.V.",
|
||||
"version": "1.0.1",
|
||||
"description": "Checks for firmware updates.",
|
||||
"api": 7,
|
||||
"api": 8,
|
||||
"i18n-catalog": "cura"
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ from UM.i18n import i18nCatalog
|
|||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdateState
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject
|
||||
from PyQt6.QtCore import pyqtSignal, pyqtProperty, QObject
|
||||
from typing import Optional
|
||||
|
||||
MYPY = False
|
||||
|
|
|
@ -5,7 +5,7 @@ import QtQuick 2.2
|
|||
import QtQuick.Controls 2.1
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Window 2.1
|
||||
import QtQuick.Dialogs 1.2 // For filedialog
|
||||
import QtQuick.Dialogs // For filedialog
|
||||
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
@ -92,11 +92,10 @@ Cura.MachineAction
|
|||
id: customFirmwareDialog
|
||||
title: catalog.i18nc("@title:window", "Select custom firmware")
|
||||
nameFilters: "Firmware image files (*.hex)"
|
||||
selectExisting: true
|
||||
onAccepted:
|
||||
{
|
||||
updateProgressDialog.visible = true;
|
||||
activeOutputDevice.updateFirmware(fileUrl);
|
||||
activeOutputDevice.updateFirmware(selectedFile);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -148,8 +147,6 @@ Cura.MachineAction
|
|||
return catalog.i18nc("@label","Firmware update failed due to missing firmware.");
|
||||
}
|
||||
}
|
||||
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
|
||||
UM.ProgressBar
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Copyright (c) 2022 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import re # For escaping characters in the settings.
|
||||
import json
|
||||
import copy
|
||||
|
||||
from UM.Mesh.MeshWriter import MeshWriter
|
||||
from UM.Logger import Logger
|
||||
|
@ -12,6 +11,8 @@ from UM.Settings.InstanceContainer import InstanceContainer
|
|||
from cura.Machines.ContainerTree import ContainerTree
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
|
@ -96,25 +97,6 @@ class GCodeWriter(MeshWriter):
|
|||
self.setInformation(catalog.i18nc("@warning:status", "Please prepare G-code before exporting."))
|
||||
return False
|
||||
|
||||
def _createFlattenedContainerInstance(self, instance_container1, instance_container2):
|
||||
"""Create a new container with container 2 as base and container 1 written over it."""
|
||||
|
||||
flat_container = InstanceContainer(instance_container2.getName())
|
||||
|
||||
# The metadata includes id, name and definition
|
||||
flat_container.setMetaData(copy.deepcopy(instance_container2.getMetaData()))
|
||||
|
||||
if instance_container1.getDefinition():
|
||||
flat_container.setDefinition(instance_container1.getDefinition().getId())
|
||||
|
||||
for key in instance_container2.getAllKeys():
|
||||
flat_container.setProperty(key, "value", instance_container2.getProperty(key, "value"))
|
||||
|
||||
for key in instance_container1.getAllKeys():
|
||||
flat_container.setProperty(key, "value", instance_container1.getProperty(key, "value"))
|
||||
|
||||
return flat_container
|
||||
|
||||
def _serialiseSettings(self, stack):
|
||||
"""Serialises a container stack to prepare it for writing at the end of the g-code.
|
||||
|
||||
|
@ -145,22 +127,22 @@ class GCodeWriter(MeshWriter):
|
|||
container_with_profile.setDefinition(machine_definition_id_for_quality)
|
||||
container_with_profile.setMetaDataEntry("setting_version", stack.quality.getMetaDataEntry("setting_version"))
|
||||
|
||||
flat_global_container = self._createFlattenedContainerInstance(stack.userChanges, container_with_profile)
|
||||
merged_global_instance_container = InstanceContainer.createMergedInstanceContainer(stack.userChanges, container_with_profile)
|
||||
# If the quality changes is not set, we need to set type manually
|
||||
if flat_global_container.getMetaDataEntry("type", None) is None:
|
||||
flat_global_container.setMetaDataEntry("type", "quality_changes")
|
||||
if merged_global_instance_container.getMetaDataEntry("type", None) is None:
|
||||
merged_global_instance_container.setMetaDataEntry("type", "quality_changes")
|
||||
|
||||
# Ensure that quality_type is set. (Can happen if we have empty quality changes).
|
||||
if flat_global_container.getMetaDataEntry("quality_type", None) is None:
|
||||
flat_global_container.setMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal"))
|
||||
if merged_global_instance_container.getMetaDataEntry("quality_type", None) is None:
|
||||
merged_global_instance_container.setMetaDataEntry("quality_type", stack.quality.getMetaDataEntry("quality_type", "normal"))
|
||||
|
||||
# Get the machine definition ID for quality profiles
|
||||
flat_global_container.setMetaDataEntry("definition", machine_definition_id_for_quality)
|
||||
merged_global_instance_container.setMetaDataEntry("definition", machine_definition_id_for_quality)
|
||||
|
||||
serialized = flat_global_container.serialize()
|
||||
serialized = merged_global_instance_container.serialize()
|
||||
data = {"global_quality": serialized}
|
||||
|
||||
all_setting_keys = flat_global_container.getAllKeys()
|
||||
all_setting_keys = merged_global_instance_container.getAllKeys()
|
||||
for extruder in stack.extruderList:
|
||||
extruder_quality = extruder.qualityChanges
|
||||
if extruder_quality.getId() == "empty_quality_changes":
|
||||
|
@ -174,7 +156,7 @@ class GCodeWriter(MeshWriter):
|
|||
extruder_quality.setDefinition(machine_definition_id_for_quality)
|
||||
extruder_quality.setMetaDataEntry("setting_version", stack.quality.getMetaDataEntry("setting_version"))
|
||||
|
||||
flat_extruder_quality = self._createFlattenedContainerInstance(extruder.userChanges, extruder_quality)
|
||||
flat_extruder_quality = InstanceContainer.createMergedInstanceContainer(extruder.userChanges, extruder_quality)
|
||||
# If the quality changes is not set, we need to set type manually
|
||||
if flat_extruder_quality.getMetaDataEntry("type", None) is None:
|
||||
flat_extruder_quality.setMetaDataEntry("type", "quality_changes")
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2022 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.1
|
||||
import QtQuick 2.15
|
||||
import QtQuick.Controls 2.15
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.1
|
||||
|
@ -47,7 +47,7 @@ UM.Dialog
|
|||
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
|
||||
selectByMouse: true
|
||||
objectName: "Peak_Height"
|
||||
validator: RegExpValidator { regExp: /^\d{0,3}([\,|\.]\d*)?$/ }
|
||||
validator: RegularExpressionValidator { regularExpression: /^\d{0,3}([\,|\.]\d*)?$/ }
|
||||
onTextChanged: manager.onPeakHeightChanged(text)
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,7 @@ UM.Dialog
|
|||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
|
||||
objectName: "Base_Height"
|
||||
validator: RegExpValidator { regExp: /^\d{0,3}([\,|\.]\d*)?$/ }
|
||||
validator: RegularExpressionValidator { regularExpression: /^\d{0,3}([\,|\.]\d*)?$/ }
|
||||
onTextChanged: manager.onBaseHeightChanged(text)
|
||||
}
|
||||
|
||||
|
@ -115,7 +115,7 @@ UM.Dialog
|
|||
Layout.fillWidth: true
|
||||
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
|
||||
focus: true
|
||||
validator: RegExpValidator { regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/ }
|
||||
validator: RegularExpressionValidator { regularExpression: /^[1-9]\d{0,2}([\,|\.]\d*)?$/ }
|
||||
onTextChanged: manager.onWidthChanged(text)
|
||||
}
|
||||
|
||||
|
@ -149,7 +149,7 @@ UM.Dialog
|
|||
selectByMouse: true
|
||||
objectName: "Depth"
|
||||
focus: true
|
||||
validator: RegExpValidator { regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/ }
|
||||
validator: RegularExpressionValidator { regularExpression: /^[1-9]\d{0,2}([\,|\.]\d*)?$/ }
|
||||
onTextChanged: manager.onDepthChanged(text)
|
||||
}
|
||||
|
||||
|
@ -255,7 +255,7 @@ UM.Dialog
|
|||
Layout.minimumWidth: UM.Theme.getSize("setting_control").width
|
||||
selectByMouse: true
|
||||
objectName: "Transmittance"
|
||||
validator: RegExpValidator { regExp: /^[1-9]\d{0,2}([\,|\.]\d*)?$/ }
|
||||
validator: RegularExpressionValidator { regularExpression: /^[1-9]\d{0,2}([\,|\.]\d*)?$/ }
|
||||
onTextChanged: manager.onTransmittanceChanged(text)
|
||||
|
||||
UM.ToolTip
|
||||
|
|
|
@ -5,8 +5,8 @@ import numpy
|
|||
|
||||
import math
|
||||
|
||||
from PyQt5.QtGui import QImage, qRed, qGreen, qBlue, qAlpha
|
||||
from PyQt5.QtCore import Qt
|
||||
from PyQt6.QtGui import QImage, qRed, qGreen, qBlue, qAlpha
|
||||
from PyQt6.QtCore import Qt
|
||||
|
||||
from UM.Mesh.MeshReader import MeshReader
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
|
@ -63,7 +63,7 @@ class ImageReader(MeshReader):
|
|||
aspect = height / width
|
||||
|
||||
if img.width() < 2 or img.height() < 2:
|
||||
img = img.scaled(width, height, Qt.IgnoreAspectRatio)
|
||||
img = img.scaled(width, height, Qt.AspectRatioMode.IgnoreAspectRatio)
|
||||
|
||||
height_from_base = max(height_from_base, 0)
|
||||
base_height = max(base_height, 0)
|
||||
|
@ -84,15 +84,15 @@ class ImageReader(MeshReader):
|
|||
|
||||
width = int(max(round(width * scale_factor), 2))
|
||||
height = int(max(round(height * scale_factor), 2))
|
||||
img = img.scaled(width, height, Qt.IgnoreAspectRatio)
|
||||
img = img.scaled(width, height, Qt.AspectRatioMode.IgnoreAspectRatio)
|
||||
|
||||
width_minus_one = width - 1
|
||||
height_minus_one = height - 1
|
||||
|
||||
Job.yieldThread()
|
||||
|
||||
texel_width = 1.0 / (width_minus_one) * scale_vector.x
|
||||
texel_height = 1.0 / (height_minus_one) * scale_vector.z
|
||||
texel_width = 1.0 / width_minus_one * scale_vector.x
|
||||
texel_height = 1.0 / height_minus_one * scale_vector.z
|
||||
|
||||
height_data = numpy.zeros((height, width), dtype = numpy.float32)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import os
|
||||
import threading
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, QObject
|
||||
from PyQt6.QtCore import Qt, pyqtSignal, QObject
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Application import Application
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
|
@ -85,7 +85,7 @@ class ImageReaderUI(QObject):
|
|||
Logger.log("d", "Creating ImageReader config UI")
|
||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath("ImageReader"), "ConfigUI.qml")
|
||||
self._ui_view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowCloseButtonHint & ~Qt.WindowMinimizeButtonHint & ~Qt.WindowMaximizeButtonHint)
|
||||
self._ui_view.setFlags(self._ui_view.flags() & ~Qt.WindowType.WindowCloseButtonHint & ~Qt.WindowType.WindowMinimizeButtonHint & ~Qt.WindowType.WindowMaximizeButtonHint)
|
||||
self._disable_size_callbacks = False
|
||||
|
||||
@pyqtSlot()
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty
|
||||
from PyQt6.QtCore import pyqtProperty
|
||||
|
||||
import UM.i18n
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
|
@ -18,7 +18,7 @@ from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
|||
from cura.Settings.cura_empty_instance_containers import isEmptyContainer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt5.QtCore import QObject
|
||||
from PyQt6.QtCore import QObject
|
||||
|
||||
catalog = UM.i18n.i18nCatalog("cura")
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
@ -51,12 +51,10 @@ Item
|
|||
|
||||
spacing: base.columnSpacing
|
||||
|
||||
Label // Title Label
|
||||
UM.Label // Title Label
|
||||
{
|
||||
text: catalog.i18nc("@title:label", "Printer Settings")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
renderType: Text.NativeRendering
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
@ -182,12 +180,10 @@ Item
|
|||
|
||||
spacing: base.columnSpacing
|
||||
|
||||
Label // Title Label
|
||||
UM.Label // Title Label
|
||||
{
|
||||
text: catalog.i18nc("@title:label", "Printhead Settings")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
renderType: Text.NativeRendering
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
|
|
@ -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()
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
from typing import Any, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QObject
|
||||
from PyQt6.QtCore import pyqtSlot, QObject
|
||||
|
||||
from UM.Version import Version
|
||||
from UM.i18n import i18nCatalog
|
||||
|
@ -15,8 +15,8 @@ from .PackageModel import PackageModel
|
|||
from .Constants import PACKAGE_UPDATES_URL
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt5.QtCore import QObject
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
from PyQt6.QtCore import QObject
|
||||
from PyQt6.QtNetwork import QNetworkReply
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -108,7 +108,7 @@ class LocalPackageList(PackageList):
|
|||
:param reply: A reply containing information about a number of packages.
|
||||
"""
|
||||
response_data = HttpRequestManager.readJSON(reply)
|
||||
if "data" not in response_data:
|
||||
if response_data is None or "data" not in response_data:
|
||||
Logger.error(
|
||||
f"Could not interpret the server's response. Missing 'data' from response data. Keys in response: {response_data.keys()}")
|
||||
return
|
||||
|
|
|
@ -2,16 +2,16 @@
|
|||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os.path
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||
from typing import Optional, cast
|
||||
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||
from typing import Callable, cast, Dict, List, Optional
|
||||
|
||||
from cura.CuraApplication import CuraApplication # Creating QML objects and managing packages.
|
||||
|
||||
from UM.Extension import Extension # We are implementing the main object of an extension here.
|
||||
from UM.PluginRegistry import PluginRegistry # To find out where we are stored (the proper way).
|
||||
|
||||
from .RemotePackageList import RemotePackageList # To register this type with QML.
|
||||
from .InstallMissingPackagesDialog import InstallMissingPackageDialog # To allow creating this dialogue from outside of the plug-in.
|
||||
from .LocalPackageList import LocalPackageList # To register this type with QML.
|
||||
from .RemotePackageList import RemotePackageList # To register this type with QML.
|
||||
|
||||
|
||||
class Marketplace(Extension, QObject):
|
||||
|
@ -22,7 +22,6 @@ class Marketplace(Extension, QObject):
|
|||
QObject.__init__(self, parent)
|
||||
Extension.__init__(self)
|
||||
self._window: Optional["QObject"] = None # If the window has been loaded yet, it'll be cached in here.
|
||||
self._plugin_registry: Optional[PluginRegistry] = None
|
||||
self._package_manager = CuraApplication.getInstance().getPackageManager()
|
||||
|
||||
self._material_package_list: Optional[RemotePackageList] = None
|
||||
|
@ -41,6 +40,7 @@ class Marketplace(Extension, QObject):
|
|||
|
||||
self._tab_shown: int = 0
|
||||
self._restart_needed = False
|
||||
self.missingPackageDialog = None
|
||||
|
||||
def getTabShown(self) -> int:
|
||||
return self._tab_shown
|
||||
|
@ -80,9 +80,9 @@ class Marketplace(Extension, QObject):
|
|||
If the window hadn't been loaded yet into Qt, it will be created lazily.
|
||||
"""
|
||||
if self._window is None:
|
||||
self._plugin_registry = PluginRegistry.getInstance()
|
||||
self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded)
|
||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded)
|
||||
plugin_path = plugin_registry.getPluginPath(self.getPluginId())
|
||||
if plugin_path is None:
|
||||
plugin_path = os.path.dirname(__file__)
|
||||
path = os.path.join(plugin_path, "resources", "qml", "Marketplace.qml")
|
||||
|
@ -103,8 +103,11 @@ class Marketplace(Extension, QObject):
|
|||
self.setTabShown(1)
|
||||
|
||||
def checkIfRestartNeeded(self) -> None:
|
||||
if self._window is None:
|
||||
return
|
||||
|
||||
if self._package_manager.hasPackagesToRemoveOrInstall or \
|
||||
cast(PluginRegistry, self._plugin_registry).getCurrentSessionActivationChangedPlugins():
|
||||
PluginRegistry.getInstance().getCurrentSessionActivationChangedPlugins():
|
||||
self._restart_needed = True
|
||||
else:
|
||||
self._restart_needed = False
|
||||
|
@ -112,6 +115,18 @@ class Marketplace(Extension, QObject):
|
|||
|
||||
showRestartNotificationChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify=showRestartNotificationChanged)
|
||||
@pyqtProperty(bool, notify = showRestartNotificationChanged)
|
||||
def showRestartNotification(self) -> bool:
|
||||
return self._restart_needed
|
||||
|
||||
def showInstallMissingPackageDialog(self, packages_metadata: List[Dict[str, str]], ignore_warning_callback: Callable[[], None]) -> None:
|
||||
"""
|
||||
Show a dialog that prompts the user to install certain packages.
|
||||
|
||||
The dialog is worded for packages that are missing and required for a certain operation.
|
||||
:param packages_metadata: The metadata of the packages that are missing.
|
||||
:param ignore_warning_callback: A callback that gets executed when the user ignores the pop-up, to show them a
|
||||
warning.
|
||||
"""
|
||||
self.missingPackageDialog = InstallMissingPackageDialog(packages_metadata, ignore_warning_callback)
|
||||
self.missingPackageDialog.show()
|
||||
|
|
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()
|
||||
|
||||
|
|
@ -4,7 +4,7 @@ import tempfile
|
|||
import json
|
||||
import os.path
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt
|
||||
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt
|
||||
from typing import cast, Dict, Optional, Set, TYPE_CHECKING
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
|
@ -22,8 +22,8 @@ from .PackageModel import PackageModel
|
|||
from .Constants import USER_PACKAGES_URL, PACKAGES_URL
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt5.QtCore import QObject
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
from PyQt6.QtCore import QObject
|
||||
from PyQt6.QtNetwork import QNetworkReply
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -32,7 +32,7 @@ class PackageList(ListModel):
|
|||
""" A List model for Packages, this class serves as parent class for more detailed implementations.
|
||||
such as Packages obtained from Remote or Local source
|
||||
"""
|
||||
PackageRole = Qt.UserRole + 1
|
||||
PackageRole = Qt.ItemDataRole.UserRole + 1
|
||||
DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB
|
||||
|
||||
def __init__(self, parent: Optional["QObject"] = None) -> None:
|
||||
|
@ -244,7 +244,10 @@ class PackageList(ListModel):
|
|||
|
||||
def _downloadError(self, package_id: str, update: bool = False, reply: Optional["QNetworkReply"] = None, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
|
||||
if reply:
|
||||
reply_string = bytes(reply.readAll()).decode()
|
||||
try:
|
||||
reply_string = bytes(reply.readAll()).decode()
|
||||
except UnicodeDecodeError:
|
||||
reply_string = "<error message is corrupt too>"
|
||||
Logger.error(f"Failed to download package: {package_id} due to {reply_string}")
|
||||
self._package_manager.packageInstallingFailed.emit(package_id)
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ import re
|
|||
from enum import Enum
|
||||
from typing import Any, cast, Dict, List, Optional
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtQml import QQmlEngine
|
||||
from PyQt6.QtCore import pyqtProperty, QObject, pyqtSignal, pyqtSlot
|
||||
from PyQt6.QtQml import QQmlEngine
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.CuraPackageManager import CuraPackageManager
|
||||
|
@ -31,7 +31,7 @@ class PackageModel(QObject):
|
|||
:param parent: The parent QML object that controls the lifetime of this model (normally a PackageList).
|
||||
"""
|
||||
super().__init__(parent)
|
||||
QQmlEngine.setObjectOwnership(self, QQmlEngine.CppOwnership)
|
||||
QQmlEngine.setObjectOwnership(self, QQmlEngine.ObjectOwnership.CppOwnership)
|
||||
self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
|
||||
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
|
||||
|
||||
|
@ -84,6 +84,20 @@ class PackageModel(QObject):
|
|||
|
||||
self._is_busy = False
|
||||
|
||||
self._is_missing_package_information = False
|
||||
|
||||
@classmethod
|
||||
def fromIncompletePackageInformation(cls, display_name: str, package_version: str, package_type: str) -> "PackageModel":
|
||||
package_data = {
|
||||
"display_name": display_name,
|
||||
"package_version": package_version,
|
||||
"package_type": package_type,
|
||||
"description": "The material package associated with the Cura project could not be found on the Ultimaker marketplace. Use the partial material profile definition stored in the Cura project file at your own risk."
|
||||
}
|
||||
package_model = cls(package_data)
|
||||
package_model.setIsMissingPackageInformation(True)
|
||||
return package_model
|
||||
|
||||
@pyqtSlot()
|
||||
def _processUpdatedPackages(self):
|
||||
self.setCanUpdate(self._package_manager.checkIfPackageCanUpdate(self._package_id))
|
||||
|
@ -385,3 +399,14 @@ class PackageModel(QObject):
|
|||
def canUpdate(self) -> bool:
|
||||
"""Flag indicating if the package can be updated"""
|
||||
return self._can_update
|
||||
|
||||
isMissingPackageInformationChanged = pyqtSignal()
|
||||
|
||||
def setIsMissingPackageInformation(self, isMissingPackageInformation: bool) -> None:
|
||||
self._is_missing_package_information = isMissingPackageInformation
|
||||
self.isMissingPackageInformationChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify=isMissingPackageInformationChanged)
|
||||
def isMissingPackageInformation(self) -> bool:
|
||||
"""Flag indicating if the package can be updated"""
|
||||
return self._is_missing_package_information
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Copyright (c) 2021 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtNetwork import QNetworkReply
|
||||
from PyQt6.QtCore import pyqtProperty, pyqtSignal, pyqtSlot
|
||||
from PyQt6.QtNetwork import QNetworkReply
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
|
@ -14,7 +14,7 @@ from .PackageList import PackageList
|
|||
from .PackageModel import PackageModel # The contents of this list.
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt5.QtCore import QObject
|
||||
from PyQt6.QtCore import QObject
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -28,6 +28,7 @@ class RemotePackageList(PackageList):
|
|||
self._package_type_filter = ""
|
||||
self._requested_search_string = ""
|
||||
self._current_search_string = ""
|
||||
self._search_type = "search"
|
||||
self._request_url = self._initialRequestUrl()
|
||||
self._ongoing_requests["get_packages"] = None
|
||||
self.isLoadingChanged.connect(self._onLoadingChanged)
|
||||
|
@ -100,7 +101,7 @@ class RemotePackageList(PackageList):
|
|||
if self._package_type_filter != "":
|
||||
request_url += f"&package_type={self._package_type_filter}"
|
||||
if self._current_search_string != "":
|
||||
request_url += f"&search={self._current_search_string}"
|
||||
request_url += f"&{self._search_type}={self._current_search_string}"
|
||||
return request_url
|
||||
|
||||
def _parseResponse(self, reply: "QNetworkReply") -> None:
|
||||
|
@ -138,9 +139,10 @@ class RemotePackageList(PackageList):
|
|||
:param reply: The reply with packages. This will most likely be incomplete and should be ignored.
|
||||
:param error: The error status of the request.
|
||||
"""
|
||||
if error == QNetworkReply.NetworkError.OperationCanceledError:
|
||||
if error == QNetworkReply.NetworkError.OperationCanceledError or error == QNetworkReply.NetworkError.ProtocolUnknownError:
|
||||
Logger.debug("Cancelled request for packages.")
|
||||
self._ongoing_requests["get_packages"] = None
|
||||
self.setIsLoading(False)
|
||||
return # Don't show an error about this to the user.
|
||||
Logger.error("Could not reach Marketplace server.")
|
||||
self.setErrorMessage(catalog.i18nc("@info:error", "Could not reach Marketplace."))
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M39 14V8H27V14H21V8H9V14H7C6.20435 14 5.44129 14.3161 4.87868 14.8787C4.31607 15.4413 4 16.2044 4 17V37C4 37.7956 4.31607 38.5587 4.87868 39.1213C5.44129 39.6839 6.20435 40 7 40H41C41.7957 40 42.5587 39.6839 43.1213 39.1213C43.6839 38.5587 44 37.7956 44 37V17C44 16.2044 43.6839 15.4413 43.1213 14.8787C42.5587 14.3161 41.7957 14 41 14H39ZM29 10H37V14H29V10ZM11 10H19V14H11V10ZM42 38H6V16H42V38Z" fill="#000E1A"/>
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M39 14V8H27V14H21V8H9V14H7C6.20435 14 5.44129 14.3161 4.87868 14.8787C4.31607 15.4413 4 16.2044 4 17V37C4 37.7956 4.31607 38.5587 4.87868 39.1213C5.44129 39.6839 6.20435 40 7 40H41C41.7957 40 42.5587 39.6839 43.1213 39.1213C43.6839 38.5587 44 37.7956 44 37V17C44 16.2044 43.6839 15.4413 43.1213 14.8787C42.5587 14.3161 41.7957 14 41 14H39ZM29 10H37V14H29V10ZM11 10H19V14H11V10ZM42 38H6V16H42V38Z"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 526 B After Width: | Height: | Size: 499 B |
|
@ -1,3 +1,3 @@
|
|||
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 4C18.6975 4.00609 13.614 6.11518 9.86459 9.86459C6.11518 13.614 4.00609 18.6975 4 24V42H6V32.66C7.54979 35.8792 9.93405 38.6241 12.9046 40.6092C15.8752 42.5942 19.3236 43.7468 22.8908 43.947C26.4579 44.1472 30.0136 43.3876 33.1876 41.7474C36.3616 40.1071 39.038 37.6462 40.9382 34.6206C42.8385 31.595 43.893 28.1155 43.9922 24.544C44.0914 20.9726 43.2315 17.4399 41.5021 14.3136C39.7727 11.1872 37.237 8.5815 34.1589 6.76765C31.0808 4.9538 27.5728 3.99809 24 4ZM24 42C20.4399 42 16.9598 40.9443 13.9997 38.9665C11.0397 36.9886 8.73255 34.1774 7.37017 30.8883C6.00779 27.5992 5.65133 23.98 6.34586 20.4884C7.0404 16.9967 8.75473 13.7894 11.2721 11.2721C13.7894 8.75473 16.9967 7.0404 20.4884 6.34587C23.98 5.65133 27.5992 6.00779 30.8883 7.37017C34.1774 8.73255 36.9886 11.0397 38.9665 13.9997C40.9443 16.9598 42 20.4399 42 24C41.9947 28.7723 40.0966 33.3476 36.7221 36.7221C33.3476 40.0966 28.7723 41.9947 24 42ZM24 17C22.6155 17 21.2622 17.4105 20.111 18.1797C18.9599 18.9489 18.0627 20.0421 17.5328 21.3212C17.003 22.6003 16.8644 24.0078 17.1345 25.3656C17.4046 26.7235 18.0713 27.9708 19.0503 28.9498C20.0292 29.9287 21.2765 30.5954 22.6344 30.8655C23.9922 31.1356 25.3997 30.997 26.6788 30.4672C27.9579 29.9373 29.0511 29.0401 29.8203 27.889C30.5895 26.7379 31 25.3845 31 24C30.9979 22.1441 30.2598 20.3648 28.9475 19.0525C27.6352 17.7402 25.8559 17.0021 24 17ZM24 29C23.0111 29 22.0444 28.7068 21.2221 28.1574C20.3999 27.6079 19.759 26.8271 19.3806 25.9134C19.0022 24.9998 18.9031 23.9945 19.0961 23.0246C19.289 22.0546 19.7652 21.1637 20.4645 20.4645C21.1637 19.7652 22.0546 19.289 23.0245 19.0961C23.9945 18.9031 24.9998 19.0022 25.9134 19.3806C26.827 19.759 27.6079 20.3999 28.1573 21.2222C28.7068 22.0444 29 23.0111 29 24C28.9984 25.3256 28.4712 26.5965 27.5338 27.5338C26.5965 28.4712 25.3256 28.9984 24 29ZM24 11C25.7079 10.9954 27.3997 11.3295 28.9776 11.9831C30.5554 12.6367 31.988 13.5967 33.1924 14.8076L31.7783 16.2217C30.7592 15.1971 29.547 14.3848 28.2118 13.8318C26.8767 13.2788 25.4451 12.9961 24 13V11Z" fill="#000E1A"/>
|
||||
<svg width="48" height="48" viewBox="0 0 48 48" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M24 4C18.6975 4.00609 13.614 6.11518 9.86459 9.86459C6.11518 13.614 4.00609 18.6975 4 24V42H6V32.66C7.54979 35.8792 9.93405 38.6241 12.9046 40.6092C15.8752 42.5942 19.3236 43.7468 22.8908 43.947C26.4579 44.1472 30.0136 43.3876 33.1876 41.7474C36.3616 40.1071 39.038 37.6462 40.9382 34.6206C42.8385 31.595 43.893 28.1155 43.9922 24.544C44.0914 20.9726 43.2315 17.4399 41.5021 14.3136C39.7727 11.1872 37.237 8.5815 34.1589 6.76765C31.0808 4.9538 27.5728 3.99809 24 4ZM24 42C20.4399 42 16.9598 40.9443 13.9997 38.9665C11.0397 36.9886 8.73255 34.1774 7.37017 30.8883C6.00779 27.5992 5.65133 23.98 6.34586 20.4884C7.0404 16.9967 8.75473 13.7894 11.2721 11.2721C13.7894 8.75473 16.9967 7.0404 20.4884 6.34587C23.98 5.65133 27.5992 6.00779 30.8883 7.37017C34.1774 8.73255 36.9886 11.0397 38.9665 13.9997C40.9443 16.9598 42 20.4399 42 24C41.9947 28.7723 40.0966 33.3476 36.7221 36.7221C33.3476 40.0966 28.7723 41.9947 24 42ZM24 17C22.6155 17 21.2622 17.4105 20.111 18.1797C18.9599 18.9489 18.0627 20.0421 17.5328 21.3212C17.003 22.6003 16.8644 24.0078 17.1345 25.3656C17.4046 26.7235 18.0713 27.9708 19.0503 28.9498C20.0292 29.9287 21.2765 30.5954 22.6344 30.8655C23.9922 31.1356 25.3997 30.997 26.6788 30.4672C27.9579 29.9373 29.0511 29.0401 29.8203 27.889C30.5895 26.7379 31 25.3845 31 24C30.9979 22.1441 30.2598 20.3648 28.9475 19.0525C27.6352 17.7402 25.8559 17.0021 24 17ZM24 29C23.0111 29 22.0444 28.7068 21.2221 28.1574C20.3999 27.6079 19.759 26.8271 19.3806 25.9134C19.0022 24.9998 18.9031 23.9945 19.0961 23.0246C19.289 22.0546 19.7652 21.1637 20.4645 20.4645C21.1637 19.7652 22.0546 19.289 23.0245 19.0961C23.9945 18.9031 24.9998 19.0022 25.9134 19.3806C26.827 19.759 27.6079 20.3999 28.1573 21.2222C28.7068 22.0444 29 23.0111 29 24C28.9984 25.3256 28.4712 26.5965 27.5338 27.5338C26.5965 28.4712 25.3256 28.9984 24 29ZM24 11C25.7079 10.9954 27.3997 11.3295 28.9776 11.9831C30.5554 12.6367 31.988 13.5967 33.1924 14.8076L31.7783 16.2217C30.7592 15.1971 29.547 14.3848 28.2118 13.8318C26.8767 13.2788 25.4451 12.9961 24 13V11Z"/>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
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()
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
//Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.10
|
||||
import QtQuick.Dialogs 1.1
|
||||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.3
|
||||
|
@ -34,7 +33,7 @@ UM.Dialog
|
|||
spacing: UM.Theme.getSize("default_margin").width
|
||||
leftPadding: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
UM.RecolorImage
|
||||
UM.ColorImage
|
||||
{
|
||||
id: icon
|
||||
width: UM.Theme.getSize("marketplace_large_icon").width
|
||||
|
@ -43,16 +42,13 @@ UM.Dialog
|
|||
source: UM.Theme.getIcon("Certificate", "high")
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
text: catalog.i18nc("@text", "Please read and agree with the plugin licence.")
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("large")
|
||||
anchors.verticalCenter: icon.verticalCenter
|
||||
height: UM.Theme.getSize("marketplace_large_icon").height
|
||||
verticalAlignment: Qt.AlignVCenter
|
||||
wrapMode: Text.Wrap
|
||||
renderType: Text.NativeRendering
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -46,7 +46,7 @@ Item
|
|||
height: UM.Theme.getSize("action_button").height
|
||||
width: childrenRect.width
|
||||
|
||||
UM.RecolorImage
|
||||
UM.ColorImage
|
||||
{
|
||||
id: busyIndicator
|
||||
visible: parent.visible
|
||||
|
|
|
@ -33,7 +33,7 @@ TabButton
|
|||
visible: root.hovered
|
||||
}
|
||||
|
||||
UM.RecolorImage
|
||||
UM.ColorImage
|
||||
{
|
||||
id: icon
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ import QtQuick.Controls 2.15
|
|||
import QtQuick.Layouts 1.15
|
||||
import QtQuick.Window 2.2
|
||||
|
||||
import UM 1.2 as UM
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
Window
|
||||
|
@ -16,6 +16,10 @@ Window
|
|||
|
||||
signal searchStringChanged(string new_search)
|
||||
|
||||
property alias showOnboadBanner: onBoardBanner.visible
|
||||
property alias showSearchHeader: searchHeader.visible
|
||||
property alias pageContentsSource: content.source
|
||||
|
||||
minimumWidth: UM.Theme.getSize("modal_window_minimum").width
|
||||
minimumHeight: UM.Theme.getSize("modal_window_minimum").height
|
||||
width: minimumWidth
|
||||
|
@ -67,7 +71,7 @@ Window
|
|||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: childrenRect.height + UM.Theme.getSize("default_margin").height
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: pageTitle
|
||||
anchors
|
||||
|
@ -80,13 +84,13 @@ Window
|
|||
}
|
||||
|
||||
font: UM.Theme.getFont("large")
|
||||
color: UM.Theme.getColor("text")
|
||||
text: content.item ? content.item.pageTitle: catalog.i18nc("@title", "Loading...")
|
||||
}
|
||||
}
|
||||
|
||||
OnboardBanner
|
||||
{
|
||||
id: onBoardBanner
|
||||
visible: content.item && content.item.bannerVisible
|
||||
text: content.item && content.item.bannerText
|
||||
icon: content.item && content.item.bannerIcon
|
||||
|
@ -101,6 +105,7 @@ Window
|
|||
// Search & Top-Level Tabs
|
||||
Item
|
||||
{
|
||||
id: searchHeader
|
||||
implicitHeight: childrenRect.height
|
||||
implicitWidth: parent.width - 2 * UM.Theme.getSize("default_margin").width
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
|
@ -187,7 +192,7 @@ Window
|
|||
{
|
||||
text: catalog.i18nc("@info", "Search in the browser")
|
||||
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||
visible: pageSelectionTabBar.currentItem.hasSearch
|
||||
visible: pageSelectionTabBar.currentItem.hasSearch && searchHeader.visible
|
||||
isIconOnRightSide: true
|
||||
height: fontMetrics.height
|
||||
textFont: fontMetrics.font
|
||||
|
@ -251,7 +256,7 @@ Window
|
|||
margins: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
UM.RecolorImage
|
||||
UM.ColorImage
|
||||
{
|
||||
id: bannerIcon
|
||||
source: UM.Theme.getIcon("Plugin")
|
||||
|
|
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,8 +19,7 @@ Rectangle
|
|||
implicitHeight: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height
|
||||
color: UM.Theme.getColor("action_panel_secondary")
|
||||
|
||||
// Icon
|
||||
UM.RecolorImage
|
||||
UM.ColorImage
|
||||
{
|
||||
id: onboardingIcon
|
||||
anchors
|
||||
|
@ -31,9 +30,10 @@ Rectangle
|
|||
}
|
||||
width: UM.Theme.getSize("banner_icon_size").width
|
||||
height: UM.Theme.getSize("banner_icon_size").height
|
||||
|
||||
color: UM.Theme.getColor("primary_text")
|
||||
}
|
||||
|
||||
// Close button
|
||||
UM.SimpleButton
|
||||
{
|
||||
id: onboardingClose
|
||||
|
@ -52,8 +52,8 @@ Rectangle
|
|||
onClicked: onRemove()
|
||||
}
|
||||
|
||||
// Body
|
||||
Label {
|
||||
UM.Label
|
||||
{
|
||||
id: infoText
|
||||
anchors
|
||||
{
|
||||
|
@ -63,14 +63,10 @@ Rectangle
|
|||
margins: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
|
||||
font: UM.Theme.getFont("default")
|
||||
|
||||
renderType: Text.NativeRendering
|
||||
color: UM.Theme.getColor("primary_text")
|
||||
wrapMode: Text.Wrap
|
||||
elide: Text.ElideRight
|
||||
|
||||
onLineLaidOut:
|
||||
onLineLaidOut: (line) =>
|
||||
{
|
||||
if(line.isLast)
|
||||
{
|
||||
|
@ -102,7 +98,7 @@ Rectangle
|
|||
id: readMoreButton
|
||||
anchors.left: infoText.left
|
||||
anchors.bottom: infoText.bottom
|
||||
text: "Learn More"
|
||||
text: catalog.i18nc("@button:label", "Learn More")
|
||||
textFont: UM.Theme.getFont("default")
|
||||
textColor: infoText.color
|
||||
leftPadding: 0
|
||||
|
|
|
@ -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
|
||||
|
@ -47,23 +49,21 @@ Item
|
|||
sourceSize.width: width
|
||||
}
|
||||
|
||||
UM.RecolorImage
|
||||
UM.ColorImage
|
||||
{
|
||||
visible: !parent.packageHasIcon
|
||||
anchors.fill: parent
|
||||
sourceSize.height: height
|
||||
sourceSize.width: width
|
||||
color: UM.Theme.getColor("text")
|
||||
source:
|
||||
{
|
||||
switch (packageData.packageType)
|
||||
{
|
||||
case "plugin":
|
||||
return "../images/Plugin.svg";
|
||||
return Qt.resolvedUrl("../images/Plugin.svg");
|
||||
case "material":
|
||||
return "../images/Spool.svg";
|
||||
return Qt.resolvedUrl("../images/Spool.svg");
|
||||
default:
|
||||
return "../images/placeholder.svg";
|
||||
return Qt.resolvedUrl("../images/placeholder.svg");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -89,11 +89,18 @@ Item
|
|||
Layout.preferredWidth: parent.width
|
||||
Layout.preferredHeight: childrenRect.height
|
||||
|
||||
Label
|
||||
UM.StatusIcon
|
||||
{
|
||||
width: UM.Theme.getSize("section_icon").width + UM.Theme.getSize("narrow_margin").width
|
||||
height: UM.Theme.getSize("section_icon").height
|
||||
status: UM.StatusIcon.Status.WARNING
|
||||
visible: packageData.isMissingPackageInformation
|
||||
}
|
||||
|
||||
UM.Label
|
||||
{
|
||||
text: packageData.displayName
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
verticalAlignment: Text.AlignTop
|
||||
}
|
||||
VerifiedIcon
|
||||
|
@ -102,18 +109,17 @@ Item
|
|||
visible: packageData.isCheckedByUltimaker
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: packageVersionLabel
|
||||
text: packageData.packageVersion
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Button
|
||||
{
|
||||
id: externalLinkButton
|
||||
visible: !packageData.isMissingPackageInformation
|
||||
|
||||
// For some reason if i set padding, they don't match up. If i set all of them explicitly, it does work?
|
||||
leftPadding: UM.Theme.getSize("narrow_margin").width
|
||||
|
@ -121,9 +127,9 @@ Item
|
|||
topPadding: UM.Theme.getSize("narrow_margin").width
|
||||
bottomPadding: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
|
||||
Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
|
||||
contentItem: UM.RecolorImage
|
||||
width: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
|
||||
height: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
|
||||
contentItem: UM.ColorImage
|
||||
{
|
||||
source: UM.Theme.getIcon("LinkExternal")
|
||||
color: UM.Theme.getColor("icon")
|
||||
|
@ -157,12 +163,13 @@ Item
|
|||
spacing: UM.Theme.getSize("narrow_margin").width
|
||||
|
||||
// label "By"
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
id: authorBy
|
||||
visible: !packageData.isMissingPackageInformation
|
||||
Layout.alignment: Qt.AlignCenter
|
||||
|
||||
text: catalog.i18nc("@label", "By")
|
||||
text: catalog.i18nc("@label Is followed by the name of an author", "By")
|
||||
font: UM.Theme.getFont("default")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
@ -170,6 +177,7 @@ Item
|
|||
// clickable author name
|
||||
Item
|
||||
{
|
||||
visible: !packageData.isMissingPackageInformation
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: authorBy.height
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
@ -187,10 +195,29 @@ Item
|
|||
}
|
||||
}
|
||||
|
||||
Item
|
||||
{
|
||||
visible: packageData.isMissingPackageInformation
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: readMoreButton.height
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Cura.TertiaryButton
|
||||
{
|
||||
id: readMoreButton
|
||||
text: catalog.i18nc("@button:label", "Learn More")
|
||||
leftPadding: 0
|
||||
rightPadding: 0
|
||||
iconSource: UM.Theme.getIcon("LinkExternal")
|
||||
isIconOnRightSide: true
|
||||
|
||||
onClicked: Qt.openUrlExternally(missingPackageReadMoreUrl)
|
||||
}
|
||||
}
|
||||
|
||||
ManageButton
|
||||
{
|
||||
id: enableManageButton
|
||||
visible: showDisableButton && packageData.isInstalled && !packageData.isToBeInstalled && packageData.packageType != "material"
|
||||
visible: showDisableButton && packageData.isInstalled && !packageData.isToBeInstalled && packageData.packageType != "material" && !packageData.isMissingPackageInformation
|
||||
enabled: !packageData.busy
|
||||
|
||||
button_style: !packageData.isActive
|
||||
|
@ -204,7 +231,7 @@ Item
|
|||
ManageButton
|
||||
{
|
||||
id: installManageButton
|
||||
visible: showInstallButton && (packageData.canDowngrade || !packageData.isBundled)
|
||||
visible: showInstallButton && (packageData.canDowngrade || !packageData.isBundled) && !packageData.isMissingPackageInformation
|
||||
enabled: !packageData.busy
|
||||
busy: packageData.busy
|
||||
button_style: !(packageData.isInstalled || packageData.isToBeInstalled)
|
||||
|
@ -234,7 +261,7 @@ Item
|
|||
ManageButton
|
||||
{
|
||||
id: updateManageButton
|
||||
visible: showUpdateButton && packageData.canUpdate
|
||||
visible: showUpdateButton && packageData.canUpdate && !packageData.isMissingPackageInformation
|
||||
enabled: !packageData.busy
|
||||
busy: packageData.busy
|
||||
Layout.alignment: Qt.AlignTop
|
||||
|
|
|
@ -45,14 +45,13 @@ Item
|
|||
iconSize: height - leftPadding * 2
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
Layout.fillWidth: true
|
||||
|
||||
text: detailPage.title
|
||||
font: UM.Theme.getFont("large")
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -93,4 +92,4 @@ Item
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ Rectangle
|
|||
// But we re-use the package page for the manage plugins as well. The one user that doesn't see
|
||||
// the num downloads is an acceptable "sacrifice" to make this easy to fix.
|
||||
visible: packageData.downloadCount != "0"
|
||||
UM.RecolorImage
|
||||
UM.ColorImage
|
||||
{
|
||||
id: downloadsIcon
|
||||
width: UM.Theme.getSize("card_tiny_icon").width
|
||||
|
@ -56,13 +56,9 @@ Rectangle
|
|||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
|
||||
anchors.verticalCenter: downloadsIcon.verticalCenter
|
||||
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("default")
|
||||
text: packageData.downloadCount
|
||||
}
|
||||
}
|
||||
|
@ -78,25 +74,22 @@ Rectangle
|
|||
topPadding: 0
|
||||
spacing: UM.Theme.getSize("default_margin").height
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width - parent.padding * 2
|
||||
|
||||
text: catalog.i18nc("@header", "Description")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width - parent.padding * 2
|
||||
|
||||
text: packageData.formattedDescription
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
linkColor: UM.Theme.getColor("text_link")
|
||||
wrapMode: Text.Wrap
|
||||
textFormat: Text.RichText
|
||||
|
||||
onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
|
||||
|
@ -110,13 +103,12 @@ Rectangle
|
|||
visible: packageData.packageType === "material"
|
||||
spacing: 0
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
text: catalog.i18nc("@header", "Compatible printers")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
|
@ -124,25 +116,23 @@ Rectangle
|
|||
{
|
||||
model: packageData.compatiblePrinters
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: compatiblePrinterColumn.width
|
||||
|
||||
text: modelData
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
visible: packageData.compatiblePrinters.length == 0
|
||||
text: "(" + catalog.i18nc("@info", "No compatibility information") + ")"
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
@ -155,13 +145,12 @@ Rectangle
|
|||
visible: packageData.packageType === "material"
|
||||
spacing: 0
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
text: catalog.i18nc("@header", "Compatible support materials")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
|
@ -169,25 +158,23 @@ Rectangle
|
|||
{
|
||||
model: packageData.compatibleSupportMaterials
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: compatibleSupportMaterialColumn.width
|
||||
|
||||
text: modelData
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
visible: packageData.compatibleSupportMaterials.length == 0
|
||||
text: "(" + catalog.i18nc("@info No materials", "None") + ")"
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
@ -199,23 +186,21 @@ Rectangle
|
|||
visible: packageData.packageType === "material"
|
||||
spacing: 0
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
text: catalog.i18nc("@header", "Compatible with Material Station")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
text: packageData.isCompatibleMaterialStation ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
@ -227,23 +212,21 @@ Rectangle
|
|||
visible: packageData.packageType === "material"
|
||||
spacing: 0
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
text: catalog.i18nc("@header", "Optimized for Air Manager")
|
||||
font: UM.Theme.getFont("medium_bold")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
Label
|
||||
UM.Label
|
||||
{
|
||||
width: parent.width
|
||||
|
||||
text: packageData.isCompatibleAirManager ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
|
||||
font: UM.Theme.getFont("medium")
|
||||
color: UM.Theme.getColor("text")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
@ -213,7 +200,7 @@ ListView
|
|||
status: UM.StatusIcon.Status.ERROR
|
||||
visible: false
|
||||
}
|
||||
UM.RecolorImage
|
||||
UM.ColorImage
|
||||
{
|
||||
id: loadMoreIcon
|
||||
anchors.fill: parent
|
||||
|
|
|
@ -32,7 +32,7 @@ Control
|
|||
anchors.fill: parent
|
||||
color: UM.Theme.getColor("action_button_hovered")
|
||||
radius: width
|
||||
UM.RecolorImage
|
||||
UM.ColorImage
|
||||
{
|
||||
anchors.fill: parent
|
||||
color: UM.Theme.getColor("primary")
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import os
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtSignal, pyqtProperty, QTimer
|
||||
from PyQt6.QtCore import QObject, pyqtSlot, pyqtSignal, pyqtProperty, QTimer
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Extension import Extension
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue