Merge remote-tracking branch 'origin/5.7' into CURA-11772_fix_crash_on_open

This commit is contained in:
Remco Burema 2024-03-28 14:58:13 +01:00
commit e85e50cce4
59 changed files with 68925 additions and 71512 deletions

View file

@ -24,29 +24,34 @@ UM.Dialog
{
height: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height
color: UM.Theme.getColor("main_background")
UM.Label
ColumnLayout
{
id: titleLabel
text: manager.isUcp? catalog.i18nc("@action:title Don't translate 'Universal Cura Project'", "Summary - Open Universal Cura Project (UCP)"): catalog.i18nc("@action:title", "Summary - Cura Project")
font: UM.Theme.getFont("large")
id: headerColumn
anchors.top: parent.top
anchors.left: parent.left
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.leftMargin: UM.Theme.getSize("default_margin").height
}
Cura.TertiaryButton
{
id: learnMoreButton
visible: manager.isUcp
anchors.right: parent.right
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.rightMargin: UM.Theme.getSize("default_margin").height
text: catalog.i18nc("@button", "Learn more")
iconSource: UM.Theme.getIcon("LinkExternal")
isIconOnRightSide: true
onClicked: Qt.openUrlExternally("https://support.ultimaker.com/s/article/000002979")
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.rightMargin: anchors.leftMargin
RowLayout
{
UM.Label
{
id: titleLabel
text: manager.isUcp? catalog.i18nc("@action:title Don't translate 'Universal Cura Project'", "Summary - Open Universal Cura Project (UCP)"): catalog.i18nc("@action:title", "Summary - Cura Project")
font: UM.Theme.getFont("large")
}
Cura.TertiaryButton
{
id: learnMoreButton
visible: manager.isUcp
text: catalog.i18nc("@button", "Learn more")
iconSource: UM.Theme.getIcon("LinkExternal")
isIconOnRightSide: true
onClicked: Qt.openUrlExternally("https://support.ultimaker.com/s/article/000002979")
}
}
}
}

View file

@ -6,13 +6,14 @@ from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal
class SettingExport(QObject):
def __init__(self, id, name, value, selectable):
def __init__(self, id, name, value, selectable, show):
super().__init__()
self.id = id
self._name = name
self._value = value
self._selected = selectable
self._selectable = selectable
self._show_in_menu = show
@pyqtProperty(str, constant=True)
def name(self):
@ -36,3 +37,7 @@ class SettingExport(QObject):
@pyqtProperty(bool, constant=True)
def selectable(self):
return self._selectable
@pyqtProperty(bool, constant=True)
def isVisible(self):
return self._show_in_menu

View file

@ -23,6 +23,7 @@ class SettingsExportGroup(QObject):
self._category_details = category_details
self._extruder_index = extruder_index
self._extruder_color = extruder_color
self._visible_settings = []
@pyqtProperty(str, constant=True)
def name(self):
@ -32,6 +33,12 @@ class SettingsExportGroup(QObject):
def settings(self):
return self._settings
@pyqtProperty(list, constant=True)
def visibleSettings(self):
if self._visible_settings == []:
self._visible_settings = list(filter(lambda item : item.isVisible, self._settings))
return self._visible_settings
@pyqtProperty(int, constant=True)
def category(self):
return self._category

View file

@ -117,6 +117,7 @@ class SettingsExportModel(QObject):
is_exportable = any(key in SettingsExportModel.PER_MODEL_EXPORTABLE_SETTINGS_KEYS for key in user_keys)
for setting_to_export in user_keys:
show_in_menu = setting_to_export not in SettingsExportModel.PER_MODEL_EXPORTABLE_SETTINGS_KEYS
label = settings_stack.getProperty(setting_to_export, "label")
value = settings_stack.getProperty(setting_to_export, "value")
unit = settings_stack.getProperty(setting_to_export, "unit")
@ -130,6 +131,7 @@ class SettingsExportModel(QObject):
settings_export.append(SettingExport(setting_to_export,
label,
value,
is_exportable or setting_to_export in exportable_settings))
is_exportable or setting_to_export in exportable_settings,
show_in_menu))
return settings_export

View file

@ -71,8 +71,8 @@ ColumnLayout
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
spacing: 0
model: modelData.settings
visible: modelData.settings.length > 0
model: modelData.visibleSettings
visible: modelData.visibleSettings.length > 0
delegate: SettingSelection { }
}
@ -80,8 +80,7 @@ ColumnLayout
UM.Label
{
UM.I18nCatalog { id: catalog; name: "cura" }
text: catalog.i18nc("@label", "No specific value has been set")
visible: modelData.settings.length === 0
visible: modelData.visibleSettings.length === 0
}
}

View file

@ -5,6 +5,7 @@ import urllib.parse
from json import JSONDecodeError
from time import time
from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any, cast
from pathlib import Path
from PyQt6.QtCore import QUrl
from PyQt6.QtNetwork import QNetworkRequest, QNetworkReply
@ -38,14 +39,17 @@ class CloudApiClient:
# The cloud URL to use for this remote cluster.
ROOT_PATH = UltimakerCloudConstants.CuraCloudAPIRoot
CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH)
CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH)
CLUSTER_API_ROOT = f"{ROOT_PATH}/connect/v1"
CURA_API_ROOT = f"{ROOT_PATH}/cura/v1"
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]]
# Custom machine definition ID to cloud cluster name mapping
_machine_id_to_name: Dict[str, str] = None
def __init__(self, app: CuraApplication, on_error: Callable[[List[CloudError]], None]) -> None:
"""Initializes a new cloud API client.
@ -73,10 +77,10 @@ class CloudApiClient:
url = f"{self.CLUSTER_API_ROOT}/clusters?status=active"
self._http.get(url,
scope = self._scope,
callback = self._parseCallback(on_finished, CloudClusterResponse, failed),
error_callback = failed,
timeout = self.DEFAULT_REQUEST_TIMEOUT)
scope=self._scope,
callback=self._parseCallback(on_finished, CloudClusterResponse, failed),
error_callback=failed,
timeout=self.DEFAULT_REQUEST_TIMEOUT)
def getClustersByMachineType(self, machine_type, on_finished: Callable[[List[CloudClusterWithConfigResponse]], Any], failed: Callable) -> None:
# HACK: There is something weird going on with the API, as it reports printer types in formats like
@ -84,13 +88,9 @@ class CloudApiClient:
# conversion!
# API points to "MakerBot Method" for a makerbot printertypes which we already changed to allign with other printer_type
method_x = {
"ultimaker_method":"MakerBot Method",
"ultimaker_methodx":"MakerBot Method X",
"ultimaker_methodxl":"MakerBot Method XL"
}
if machine_type in method_x:
machine_type = method_x[machine_type]
machine_id_to_name = self.getMachineIDMap()
if machine_type in machine_id_to_name:
machine_type = machine_id_to_name[machine_type]
else:
machine_type = machine_type.replace("_plus", "+")
machine_type = machine_type.replace("_", " ")
@ -114,9 +114,9 @@ class CloudApiClient:
url = f"{self.CLUSTER_API_ROOT}/clusters/{cluster_id}/status"
self._http.get(url,
scope = self._scope,
callback = self._parseCallback(on_finished, CloudClusterStatus),
timeout = self.DEFAULT_REQUEST_TIMEOUT)
scope=self._scope,
callback=self._parseCallback(on_finished, CloudClusterStatus),
timeout=self.DEFAULT_REQUEST_TIMEOUT)
def requestUpload(self, request: CloudPrintJobUploadRequest,
on_finished: Callable[[CloudPrintJobResponse], Any]) -> None:
@ -131,10 +131,10 @@ class CloudApiClient:
data = json.dumps({"data": request.toDict()}).encode()
self._http.put(url,
scope = self._scope,
data = data,
callback = self._parseCallback(on_finished, CloudPrintJobResponse),
timeout = self.DEFAULT_REQUEST_TIMEOUT)
scope=self._scope,
data=data,
callback=self._parseCallback(on_finished, CloudPrintJobResponse),
timeout=self.DEFAULT_REQUEST_TIMEOUT)
def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any],
on_progress: Callable[[int], Any], on_error: Callable[[], Any]):
@ -160,11 +160,11 @@ class CloudApiClient:
def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any], on_error) -> None:
url = f"{self.CLUSTER_API_ROOT}/clusters/{cluster_id}/print/{job_id}"
self._http.post(url,
scope = self._scope,
data = b"",
callback = self._parseCallback(on_finished, CloudPrintResponse),
error_callback = on_error,
timeout = self.DEFAULT_REQUEST_TIMEOUT)
scope=self._scope,
data=b"",
callback=self._parseCallback(on_finished, CloudPrintResponse),
error_callback=on_error,
timeout=self.DEFAULT_REQUEST_TIMEOUT)
def doPrintJobAction(self, cluster_id: str, cluster_job_id: str, action: str,
data: Optional[Dict[str, Any]] = None) -> None:
@ -174,14 +174,15 @@ class CloudApiClient:
:param cluster_id: The ID of the cluster.
:param cluster_job_id: The ID of the print job within the cluster.
:param action: The name of the action to execute.
:param data: Optional data to send with the POST request
"""
body = json.dumps({"data": data}).encode() if data else b""
url = f"{self.CLUSTER_API_ROOT}/clusters/{cluster_id}/print_jobs/{cluster_job_id}/action/{action}"
self._http.post(url,
scope = self._scope,
data = body,
timeout = self.DEFAULT_REQUEST_TIMEOUT)
scope=self._scope,
data=body,
timeout=self.DEFAULT_REQUEST_TIMEOUT)
def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest:
"""We override _createEmptyRequest in order to add the user credentials.
@ -216,8 +217,11 @@ class CloudApiClient:
Logger.logException("e", "Could not parse the stardust response: %s", error.toDict())
return status_code, {"errors": [error.toDict()]}
def _parseResponse(self, response: Dict[str, Any], on_finished: Union[Callable[[CloudApiClientModel], Any],
Callable[[List[CloudApiClientModel]], Any]], model_class: Type[CloudApiClientModel]) -> None:
def _parseResponse(self,
response: Dict[str, Any],
on_finished: Union[Callable[[CloudApiClientModel], Any],
Callable[[List[CloudApiClientModel]], Any]],
model_class: Type[CloudApiClientModel]) -> None:
"""Parses the given response and calls the correct callback depending on the result.
:param response: The response from the server, after being converted to a dict.
@ -276,3 +280,14 @@ class CloudApiClient:
self._anti_gc_callbacks.append(parse)
return parse
@classmethod
def getMachineIDMap(cls) -> Dict[str, str]:
if cls._machine_id_to_name is None:
try:
with open(Path(__file__).parent / "machine_id_to_name.json", "rt") as f:
cls._machine_id_to_name = json.load(f)
except Exception as e:
Logger.logException("e", f"Could not load machine_id_to_name.json: '{e}'")
cls._machine_id_to_name = {}
return cls._machine_id_to_name

View file

@ -0,0 +1,5 @@
{
"ultimaker_method": "MakerBot Method",
"ultimaker_methodx": "MakerBot Method X",
"ultimaker_methodxl": "MakerBot Method XL"
}