This commit is contained in:
root 2021-08-01 19:55:05 +02:00
commit 22156a5421
59 changed files with 998 additions and 125 deletions

View File

@ -10,9 +10,9 @@ For crashes and similar issues, please attach the following information:
* (On Windows) The log as produced by dxdiag (start -> run -> dxdiag -> save output)
* The Cura GUI log file, located at
* `%APPDATA%\cura\<Cura version>\cura.log` (Windows), or usually `C:\Users\\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
* `$USER/Library/Application Support/cura/<Cura version>/cura.log` (OSX)
* `$USER/.local/share/cura/<Cura version>/cura.log` (Ubuntu/Linux)
* `%APPDATA%\cura\<Cura version>\cura.log` (Windows), or usually `C:\Users\<your username>\AppData\Roaming\cura\<Cura version>\cura.log`
* `$HOME/Library/Application Support/cura/<Cura version>/cura.log` (OSX)
* `$HOME/.local/share/cura/<Cura version>/cura.log` (Ubuntu/Linux)
If the Cura user interface still starts, you can also reach this directory from the application menu in Help -> Show settings folder

View File

@ -109,7 +109,6 @@ class Account(QObject):
self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged)
self._authorization_service.loadAuthDataFromPreferences()
@pyqtProperty(int, notify=syncStateChanged)
def syncState(self):
return self._sync_state
@ -178,6 +177,7 @@ class Account(QObject):
if error_message:
if self._error_message:
self._error_message.hide()
Logger.log("w", "Failed to login: %s", error_message)
self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed"))
self._error_message.show()
self._logged_in = False

View File

@ -2,9 +2,10 @@
# Cura is released under the terms of the LGPLv3 or higher.
import copy # To duplicate materials.
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot # To allow the preference page proxy to be used from the actual preferences page.
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl
from typing import Any, Dict, Optional, TYPE_CHECKING
import uuid # To generate new GUIDs for new materials.
import zipfile # To export all materials in a .zip archive.
from UM.i18n import i18nCatalog
from UM.Logger import Logger
@ -20,11 +21,6 @@ if TYPE_CHECKING:
catalog = i18nCatalog("cura")
class MaterialManagementModel(QObject):
"""Proxy class to the materials page in the preferences.
This class handles the actions in that page, such as creating new materials, renaming them, etc.
"""
favoritesChanged = pyqtSignal(str)
"""Triggered when a favorite is added or removed.
@ -264,3 +260,40 @@ class MaterialManagementModel(QObject):
self.favoritesChanged.emit(material_base_file)
except ValueError: # Material was not in the favorites list.
Logger.log("w", "Material {material_base_file} was already not a favorite material.".format(material_base_file = material_base_file))
@pyqtSlot(result = QUrl)
def getPreferredExportAllPath(self) -> QUrl:
"""
Get the preferred path to export materials to.
If there is a removable drive, that should be the preferred path. Otherwise it should be the most recent local
file path.
:return: The preferred path to export all materials to.
"""
cura_application = cura.CuraApplication.CuraApplication.getInstance()
device_manager = cura_application.getOutputDeviceManager()
devices = device_manager.getOutputDevices()
for device in devices:
if device.__class__.__name__ == "RemovableDriveOutputDevice":
return QUrl.fromLocalFile(device.getId())
else: # No removable drives? Use local path.
return cura_application.getDefaultPath("dialog_material_path")
@pyqtSlot(QUrl)
def exportAll(self, file_path: QUrl) -> None:
"""
Export all materials to a certain file path.
:param file_path: The path to export the materials to.
"""
registry = CuraContainerRegistry.getInstance()
archive = zipfile.ZipFile(file_path.toLocalFile(), "w", compression = zipfile.ZIP_DEFLATED)
for metadata in registry.findInstanceContainersMetadata(type = "material"):
if metadata["base_file"] != metadata["id"]: # Only process base files.
continue
if metadata["id"] == "empty_material": # Don't export the empty material.
continue
material = registry.findContainers(id = metadata["id"])[0]
suffix = registry.getMimeTypeForContainer(type(material)).preferredSuffix
filename = metadata["id"] + "." + suffix
archive.writestr(filename, material.serialize())

View File

@ -54,6 +54,7 @@ class LocalAuthorizationServer:
if self._web_server:
# If the server is already running (because of a previously aborted auth flow), we don't have to start it.
# We still inject the new verification code though.
Logger.log("d", "Auth web server was already running. Updating the verification code")
self._web_server.setVerificationCode(verification_code)
return
@ -85,6 +86,7 @@ class LocalAuthorizationServer:
except OSError:
# OS error can happen if the socket was already closed. We really don't care about that case.
pass
Logger.log("d", "Local oauth2 web server was shut down")
self._web_server = None
self._web_server_thread = None
@ -96,12 +98,13 @@ class LocalAuthorizationServer:
:return: None
"""
Logger.log("d", "Local web server for authorization has started")
if self._web_server:
if sys.platform == "win32":
try:
self._web_server.serve_forever()
except OSError as e:
Logger.warning(str(e))
except OSError:
Logger.logException("w", "An exception happened while serving the auth server")
else:
# Leave the default behavior in non-windows platforms
self._web_server.serve_forever()

View File

@ -86,6 +86,14 @@ class GlobalStack(CuraContainerStack):
def supportsNetworkConnection(self):
return self.getMetaDataEntry("supports_network_connection", False)
@pyqtProperty(bool, constant = True)
def supportsMaterialExport(self):
"""
Whether the printer supports Cura's export format of material profiles.
:return: ``True`` if it supports it, or ``False`` if not.
"""
return self.getMetaDataEntry("supports_material_export", False)
@classmethod
def getLoadingPriority(cls) -> int:
return 2

View File

@ -4,12 +4,12 @@
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, pyqtSlot
from PyQt5.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 QImage
from PyQt5.QtGui import QDesktopServices, QImage
from UM.Backend.Backend import Backend, BackendState
from UM.Scene.SceneNode import SceneNode
@ -157,6 +157,18 @@ class CuraEngineBackend(QObject, Backend):
self.determineAutoSlicing()
application.getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._slicing_error_message = Message(
text = catalog.i18nc("@message", "Slicing failed with an unexpected error. Please consider reporting a bug on our issue tracker."),
title = catalog.i18nc("@message:title", "Slicing failed")
)
self._slicing_error_message.addAction(
action_id = "report_bug",
name = catalog.i18nc("@message:button", "Report a bug"),
description = catalog.i18nc("@message:description", "Report a bug on Ultimaker Cura's issue tracker."),
icon = "[no_icon]"
)
self._slicing_error_message.actionTriggered.connect(self._reportBackendError)
self._snapshot = None #type: Optional[QImage]
application.initializationFinished.connect(self.initialize)
@ -922,9 +934,22 @@ class CuraEngineBackend(QObject, Backend):
if not self._restart:
if self._process: # type: ignore
Logger.log("d", "Backend quit with return code %s. Resetting process and socket.", self._process.wait()) # type: ignore
return_code = self._process.wait()
if return_code != 0:
Logger.log("e", f"Backend exited abnormally with return code {return_code}!")
self._slicing_error_message.show()
self.setState(BackendState.Error)
self.stopSlicing()
else:
Logger.log("d", "Backend finished slicing. Resetting process and socket.")
self._process = None # type: ignore
def _reportBackendError(self, _message_id: str, _action_id: str) -> None:
"""
Triggered when the user wants to report an error in the back-end.
"""
QDesktopServices.openUrl(QUrl("https://github.com/Ultimaker/Cura/issues/new/choose"))
def _onGlobalStackChanged(self) -> None:
"""Called when the global container stack changes"""

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8" ?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 378.13 348.13" version="1.1">
<defs
id="defs7">
<style
id="style2">
.cls-2,.cls-6{fill:#c5dbfb;}
.cls-6,.cls-7{stroke-width:2px;}
.cls-7{fill:#f3f8fe;}
.cls-6,.cls-7{stroke:#061884;}
</style>
</defs>
<path class="cls-2" d="M43,17V3H83a2,2,0,0,1,2,2V17Z" />
<path fill="white" d="M 3 1 C 1.8954305 1 1 1.8954305 1 3 L 1 67 C 1 68.104569 1.8954305 69 3 69 L 72.152344 69 A 100 100 0 0 1 89 49.873047 L 89 19 C 89 17.895431 88.104569 17 87 17 L 56 17 L 40 1 L 3 1 z " />
<path fill="#c5dbfb" d="M 3 0 C 1.3549904 0 0 1.3549904 0 3 L 0 67 C 0 68.64501 1.3549904 70 3 70 L 71.484375 70 A 100 100 0 0 1 72.835938 68 L 3 68 C 2.4358706 68 2 67.564129 2 67 L 2 3 C 2 2.4358706 2.4358706 2 3 2 L 39.585938 2 L 55.585938 18 L 87 18 C 87.564129 18 88 18.435871 88 19 L 88 50.765625 A 100 100 0 0 1 90 49.007812 L 90 19 C 90 17.35499 88.64501 16 87 16 L 56.414062 16 L 40.414062 0 L 3 0 z " />
<path class="cls-2" d="M153,17V3h40a2,2,0,0,1,2,2V17Z" />
<path fill="white" d="M 113 1 C 111.89543 1 111 1.8954305 111 3 L 111 35.201172 A 100 100 0 0 1 155 25 A 100 100 0 0 1 199 35.201172 L 199 19 C 199 17.895431 198.10457 17 197 17 L 166 17 L 150 1 L 113 1 z " />
<path fill="#c5dbfb" d="M 113 0 C 111.35499 0 110 1.3549904 110 3 L 110 35.699219 A 100 100 0 0 1 112 34.716797 L 112 3 C 112 2.4358706 112.43587 2 113 2 L 149.58594 2 L 165.58594 18 L 197 18 C 197.56413 18 198 18.435871 198 19 L 198 34.716797 A 100 100 0 0 1 200 35.699219 L 200 19 C 200 17.35499 198.64501 16 197 16 L 166.41406 16 L 150.41406 0 L 113 0 z " />
<path class="cls-2" d="M263,17V3h40a2,2,0,0,1,2,2V17Z" />
<path fill="white" d="M 223 1 C 221.89543 1 221 1.8954305 221 3 L 221 49.875 A 100 100 0 0 1 237.84961 69 L 307 69 C 308.10457 69 309 68.104569 309 67 L 309 19 C 309 17.895431 308.10457 17 307 17 L 276 17 L 260 1 L 223 1 z " />
<path fill="#c5dbfb" d="M 223 0 C 221.35499 0 220 1.3549904 220 3 L 220 49.005859 A 100 100 0 0 1 222 50.765625 L 222 3 C 222 2.4358706 222.43587 2 223 2 L 259.58594 2 L 275.58594 18 L 307 18 C 307.56413 18 308 18.435871 308 19 L 308 67 C 308 67.564129 307.56413 68 307 68 L 237.16406 68 A 100 100 0 0 1 238.51562 70 L 307 70 C 308.64501 70 310 68.64501 310 67 L 310 19 C 310 17.35499 308.64501 16 307 16 L 276.41406 16 L 260.41406 0 L 223 0 z " />
<path fill="#c5dbfb" d="M 43 93 L 43 107 L 56.634766 107 A 100 100 0 0 1 60.259766 93 L 43 93 z " />
<path fill="white" d="M 3 91 C 1.8954305 91 1 91.895431 1 93 L 1 157 C 1 158.10457 1.8954305 159 3 159 L 60.958984 159 A 100 100 0 0 1 55 125 A 100 100 0 0 1 56.634766 107 L 56 107 L 40 91 L 3 91 z " />
<path fill="#c5dbfb" d="M 3 90 C 1.3549904 90 0 91.35499 0 93 L 0 157 C 0 158.64501 1.3549904 160 3 160 L 61.324219 160 A 100 100 0 0 1 60.603516 158 L 3 158 C 2.4358706 158 2 157.56413 2 157 L 2 93 C 2 92.435871 2.4358706 92 3 92 L 39.585938 92 L 55.585938 108 L 56.455078 108 A 100 100 0 0 1 56.822266 106 L 56.414062 106 L 40.414062 90 L 3 90 z " />
<path class="cls-2" d="M263,107V93h40a2,2,0,0,1,2,2v12Z" />
<path fill="white" d="M 249.04102 91 A 100 100 0 0 1 255 125 A 100 100 0 0 1 249.04102 159 L 307 159 C 308.10457 159 309 158.10457 309 157 L 309 109 C 309 107.89543 308.10457 107 307 107 L 276 107 L 260 91 L 249.04102 91 z " />
<path fill="#c5dbfb" d="M 248.67578 90 A 100 100 0 0 1 249.39648 92 L 259.58594 92 L 275.58594 108 L 307 108 C 307.56413 108 308 108.43587 308 109 L 308 157 C 308 157.56413 307.56413 158 307 158 L 249.39844 158 A 100 100 0 0 1 248.67383 160 L 307 160 C 308.64501 160 310 158.64501 310 157 L 310 109 C 310 107.35499 308.64501 106 307 106 L 276.41406 106 L 260.41406 90 L 248.67578 90 z " />
<path fill="#c5dbfb" d="M 43 183 L 43 197 L 85 197 L 85 196.41406 A 100 100 0 0 1 73.539062 183 L 43 183 z " />
<path fill="white" d="M 3 181 C 1.8954305 181 1 181.89543 1 183 L 1 247 C 1 248.10457 1.8954305 249 3 249 L 87 249 C 88.104569 249 89 248.10457 89 247 L 89 200.125 A 100 100 0 0 1 85.603516 197 L 56 197 L 40 181 L 3 181 z " />
<path fill="#c5dbfb" d="M 3 180 C 1.3549904 180 0 181.35499 0 183 L 0 247 C 0 248.64501 1.3549904 250 3 250 L 87 250 C 88.64501 250 90 248.64501 90 247 L 90 200.99414 A 100 100 0 0 1 88 199.23438 L 88 247 C 88 247.56413 87.564129 248 87 248 L 3 248 C 2.4358706 248 2 247.56413 2 247 L 2 183 C 2 182.43587 2.4358706 182 3 182 L 39.585938 182 L 55.585938 198 L 86.65625 198 A 100 100 0 0 1 84.580078 196 L 56.414062 196 L 40.414062 180 L 3 180 z " />
<path fill="white" d="M 111 214.79883 L 111 247 C 111 248.10457 111.89543 249 113 249 L 197 249 C 198.10457 249 199 248.10457 199 247 L 199 214.79883 A 100 100 0 0 1 155 225 A 100 100 0 0 1 111 214.79883 z " />
<path fill="#c5dbfb" d="M 110 214.30078 L 110 247 C 110 248.64501 111.35499 250 113 250 L 197 250 C 198.64501 250 200 248.64501 200 247 L 200 214.30078 A 100 100 0 0 1 198 215.2832 L 198 247 C 198 247.56413 197.56413 248 197 248 L 113 248 C 112.43587 248 112 247.56413 112 247 L 112 215.2832 A 100 100 0 0 1 110 214.30078 z " />
<path class="cls-2" d="M263,197V183h40a2,2,0,0,1,2,2v12Z" />
<path fill="white" d="M 237.84766 181 A 100 100 0 0 1 221 200.12695 L 221 247 C 221 248.10457 221.89543 249 223 249 L 307 249 C 308.10457 249 309 248.10457 309 247 L 309 199 C 309 197.89543 308.10457 197 307 197 L 276 197 L 260 181 L 237.84766 181 z " />
<path fill="#c5dbfb" d="M 238.51562 180 A 100 100 0 0 1 237.16406 182 L 259.58594 182 L 275.58594 198 L 307 198 C 307.56413 198 308 198.43587 308 199 L 308 247 C 308 247.56413 307.56413 248 307 248 L 223 248 C 222.43587 248 222 247.56413 222 247 L 222 199.23438 A 100 100 0 0 1 220 200.99219 L 220 247 C 220 248.64501 221.35499 250 223 250 L 307 250 C 308.64501 250 310 248.64501 310 247 L 310 199 C 310 197.35499 308.64501 196 307 196 L 276.41406 196 L 260.41406 180 L 238.51562 180 z " />
<path class="cls-6" d="M351.12,322.62h20a10,10,0,0,1,10,10v7a0,0,0,0,1,0,0h-40a0,0,0,0,1,0,0v-7A10,10,0,0,1,351.12,322.62Z" transform="translate(850.61 309.91) rotate(135)" />
<rect class="cls-7" x="293.75" y="225.25" width="40" height="117" transform="translate(-108.74 304.96) rotate(-45)" />
<polyline class="cls-7" points="213.69 199.25 252.58 238.14 267.43 223.29 228.54 184.4" />
<path fill="white" stroke="#061884" stroke-width="2px" d="M 154.94141 30 A 95 95 0 0 0 60 125 A 95 95 0 0 0 155 220 A 95 95 0 0 0 250 125 A 95 95 0 0 0 155 30 A 95 95 0 0 0 154.94141 30 z M 154.82812 40 A 85 85 0 0 1 155 40 A 85 85 0 0 1 240 125 A 85 85 0 0 1 155 210 A 85 85 0 0 1 70 125 A 85 85 0 0 1 154.82812 40 z " />
<path class="cls-6" d="M256.37,227.87h20a10,10,0,0,1,10,10v7a0,0,0,0,1,0,0h-40a0,0,0,0,1,0,0v-7a10,10,0,0,1,10-10Z" transform="translate(-89.12 257.58) rotate(-45)" />
<path fill="white" d="M 154.94141 45 A 80 80 0 0 0 111 58.185547 L 111 67 C 111 68.104569 111.89543 69 113 69 L 197 69 C 198.10457 69 199 68.104569 199 67 L 199 58.1875 A 80 80 0 0 0 155 45 A 80 80 0 0 0 154.94141 45 z " />
<path fill="#061884" d="M 112 57.539062 A 80 80 0 0 0 110 58.857422 L 110 67 C 110 68.64501 111.35499 70 113 70 L 197 70 C 198.64501 70 200 68.64501 200 67 L 200 58.857422 A 80 80 0 0 0 198 57.541016 L 198 67 C 198 67.564129 197.56413 68 197 68 L 113 68 C 112.43587 68 112 67.564129 112 67 L 112 57.539062 z " />
<path fill="#196ef0" d="M 81.679688 93 A 80 80 0 0 0 77.050781 107 L 85 107 L 85 95 A 2 2 0 0 0 83 93 L 81.679688 93 z " />
<path fill="white" d="M 77.050781 107 A 80 80 0 0 0 75 125 A 80 80 0 0 0 82.585938 159 L 87 159 C 88.104569 159 89 158.10457 89 157 L 89 109 C 89 107.89543 88.104569 107 87 107 L 77.050781 107 z " />
<path fill="#061884" d="M 77.289062 106 A 80 80 0 0 0 76.828125 108 L 87 108 C 87.564129 108 88 108.43587 88 109 L 88 157 C 88 157.56413 87.564129 158 87 158 L 82.125 158 A 80 80 0 0 0 83.0625 160 L 87 160 C 88.64501 160 90 158.64501 90 157 L 90 109 C 90 107.35499 88.64501 106 87 106 L 77.289062 106 z " />
<path fill="white" d="M 223 91 C 221.89543 91 221 91.895431 221 93 L 221 157 C 221 158.10457 221.89543 159 223 159 L 227.41406 159 A 80 80 0 0 0 235 125 A 80 80 0 0 0 227.41406 91 L 223 91 z " />
<path fill="#061884" d="M 223 90 C 221.35499 90 220 91.35499 220 93 L 220 157 C 220 158.64501 221.35499 160 223 160 L 226.9375 160 A 80 80 0 0 0 227.87695 158 L 223 158 C 222.43587 158 222 157.56413 222 157 L 222 93 C 222 92.435871 222.43587 92 223 92 L 227.875 92 A 80 80 0 0 0 226.9375 90 L 223 90 z " />
<path fill="#196ef0" d="M 153 183 L 153 197 L 189.86914 197 A 80 80 0 0 0 195 194.28125 L 195 185 A 2 2 0 0 0 193 183 L 153 183 z "/>
<path fill="white" d="M 113 181 C 111.89543 181 111 181.89543 111 183 L 111 191.8125 A 80 80 0 0 0 155 205 A 80 80 0 0 0 189.86914 197 L 166 197 L 150 181 L 113 181 z " />
<path fill="#061884" d="M 113 180 C 111.35499 180 110 181.35499 110 183 L 110 191.14258 A 80 80 0 0 0 112 192.45898 L 112 183 C 112 182.43587 112.43587 182 113 182 L 149.58594 182 L 165.58594 198 L 187.72461 198 A 80 80 0 0 0 191.86328 196 L 166.41406 196 L 150.41406 180 L 113 180 z " />
<path fill="#061884" d="m 149.18,133.69 v -3.48 a 14.36,14.36 0 0 1 1.74,-7.25 20.17,20.17 0 0 1 6.4,-6.17 25.87,25.87 0 0 0 5.68,-4.79 7,7 0 0 0 1.48,-4.34 4.13,4.13 0 0 0 -1.93,-3.62 9,9 0 0 0 -5.14,-1.3 24.94,24.94 0 0 0 -7.34,1.16 45.2,45.2 0 0 0 -7.78,3.31 l -5.37,-10.64 a 48.41,48.41 0 0 1 9.89,-4.21 40.25,40.25 0 0 1 11.67,-1.61 q 9.57,0 14.9,4.43 a 14.16,14.16 0 0 1 5.32,11.41 15.41,15.41 0 0 1 -2.55,9 30.38,30.38 0 0 1 -7.92,7.34 32.11,32.11 0 0 0 -5.23,4.37 5.91,5.91 0 0 0 -1.34,4 v 2.41 z m -1.61,15.12 q 0,-4.38 2.46,-6.12 a 10,10 0 0 1 5.95,-1.75 9.69,9.69 0 0 1 5.77,1.75 q 2.46,1.74 2.46,6.12 0,4.22 -2.46,6 a 9.42,9.42 0 0 1 -5.77,1.84 9.69,9.69 0 0 1 -5.95,-1.84 q -2.46,-1.81 -2.46,-6 z" />
</svg>

After

Width:  |  Height:  |  Size: 9.6 KiB

View File

@ -93,7 +93,7 @@ Popup
}
validator: RegExpValidator
{
regExp: /^[^\\\/\*\?\|\[\]]{0,96}$/
regExp: /^[^\\\/\*\?\|\[\]]{0,99}$/
}
text: PrintInformation.jobName
@ -148,7 +148,7 @@ Popup
anchors.bottom: parent.bottom
anchors.right: parent.right
text: "Create"
enabled: newProjectNameTextField.text != "" && !busy
enabled: newProjectNameTextField.text.length >= 2 && !busy
onClicked:
{

View File

@ -63,7 +63,7 @@ Item
anchors.topMargin: UM.Theme.getSize("thin_margin").height
validator: RegExpValidator
{
regExp: /^[^\\\/\*\?\|\[\]]{0,96}$/
regExp: /^[\w\-\. ()]{0,255}$/
}
text: PrintInformation.jobName
@ -200,7 +200,7 @@ Item
anchors.bottom: parent.bottom
anchors.right: parent.right
text: "Save"
enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text != ""
enabled: (asProjectCheckbox.checked || asSlicedCheckbox.checked) && dfFilenameTextfield.text.length >= 1
onClicked:
{

View File

@ -1,10 +1,12 @@
// Copyright (C) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Window 2.2
import QtQuick.Controls 1.4 as OldControls // TableView doesn't exist in the QtQuick Controls 2.x in 5.10, so use the old one
import QtQuick.Controls 2.3
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.1
import UM 1.2 as UM
import Cura 1.6 as Cura
@ -18,7 +20,7 @@ Item
width: parent.width
height: parent.height
property alias createNewProjectButtonVisible: createNewProjectButton.visible
property bool createNewProjectButtonVisible: true
anchors
{
@ -29,31 +31,58 @@ Item
margins: UM.Theme.getSize("default_margin").width
}
Label
RowLayout
{
id: selectProjectLabel
id: headerRow
text: "Select Project"
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("small_button_text")
anchors.top: parent.top
anchors.left: parent.left
visible: projectListContainer.visible
}
Cura.SecondaryButton
{
id: createNewProjectButton
anchors.verticalCenter: selectProjectLabel.verticalCenter
anchors.right: parent.right
text: "New Library project"
onClicked:
anchors
{
createNewProjectPopup.open()
top: parent.top
left: parent.left
right: parent.right
}
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
Cura.TextField
{
id: searchBar
Layout.fillWidth: true
implicitHeight: createNewProjectButton.height
onTextEdited: manager.projectFilter = text //Update the search filter when editing this text field.
leftIcon: UM.Theme.getIcon("Magnifier")
placeholderText: "Search"
}
Cura.SecondaryButton
{
id: createNewProjectButton
text: "New Library project"
visible: createNewProjectButtonVisible && manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed)
onClicked:
{
createNewProjectPopup.open()
}
busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress
}
Cura.SecondaryButton
{
id: upgradePlanButton
text: "Upgrade plan"
iconSource: UM.Theme.getIcon("LinkExternal")
visible: createNewProjectButtonVisible && !manager.userAccountCanCreateNewLibraryProject && (manager.retrievingProjectsStatus == DF.RetrievalStatus.Success || manager.retrievingProjectsStatus == DF.RetrievalStatus.Failed)
tooltip: "You have reached the maximum number of projects allowed by your subscription. Please upgrade to the Professional subscription to create more projects."
tooltipWidth: parent.width * 0.5
onClicked: Qt.openUrlExternally("https://ultimaker.com/software/ultimaker-essentials/sign-up-cura?utm_source=cura&utm_medium=software&utm_campaign=lib-max")
}
busy: manager.creatingNewProjectStatus == DF.RetrievalStatus.InProgress
}
Item
@ -76,19 +105,18 @@ Item
{
id: digitalFactoryImage
anchors.horizontalCenter: parent.horizontalCenter
source: "../images/digital_factory.svg"
source: searchBar.text === "" ? "../images/digital_factory.svg" : "../images/projects_not_found.svg"
fillMode: Image.PreserveAspectFit
width: parent.width - 2 * UM.Theme.getSize("thick_margin").width
sourceSize.width: width
sourceSize.height: height
}
Label
{
id: noLibraryProjectsLabel
anchors.horizontalCenter: parent.horizontalCenter
text: "It appears that you don't have any projects in the Library yet."
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
@ -97,6 +125,7 @@ Item
anchors.horizontalCenter: parent.horizontalCenter
text: "Visit Digital Library"
onClicked: Qt.openUrlExternally(CuraApplication.ultimakerDigitalFactoryUrl + "/app/library")
visible: searchBar.text === "" //Show the link to Digital Library when there are no projects in the user's Library.
}
}
}
@ -106,7 +135,7 @@ Item
id: projectListContainer
anchors
{
top: selectProjectLabel.bottom
top: headerRow.bottom
topMargin: UM.Theme.getSize("default_margin").height
bottom: parent.bottom
left: parent.left

View File

@ -22,6 +22,7 @@ from .DFFileUploader import DFFileUploader
from .DFLibraryFileUploadRequest import DFLibraryFileUploadRequest
from .DFLibraryFileUploadResponse import DFLibraryFileUploadResponse
from .DFPrintJobUploadRequest import DFPrintJobUploadRequest
from .DigitalFactoryFeatureBudgetResponse import DigitalFactoryFeatureBudgetResponse
from .DigitalFactoryFileResponse import DigitalFactoryFileResponse
from .DigitalFactoryProjectResponse import DigitalFactoryProjectResponse
from .PaginationLinks import PaginationLinks
@ -54,9 +55,67 @@ class DigitalFactoryApiClient:
self._http = HttpRequestManager.getInstance()
self._on_error = on_error
self._file_uploader = None # type: Optional[DFFileUploader]
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]
def checkUserHasAccess(self, callback: Callable) -> None:
"""Checks if the user has any sort of access to the digital library.
A user is considered to have access if the max-# of private projects is greater then 0 (or -1 for unlimited).
"""
def callbackWrap(response: Optional[Any] = None, *args, **kwargs) -> None:
if (response is not None and isinstance(response, DigitalFactoryFeatureBudgetResponse) and
response.library_max_private_projects is not None):
callback(
response.library_max_private_projects == -1 or # Note: -1 is unlimited
response.library_max_private_projects > 0)
self._library_max_private_projects = response.library_max_private_projects
else:
Logger.warning(f"Digital Factory: Response is not a feature budget, likely an error: {str(response)}")
callback(False)
self._http.get(f"{self.CURA_API_ROOT}/feature_budgets",
scope = self._scope,
callback = self._parseCallback(callbackWrap, DigitalFactoryFeatureBudgetResponse, callbackWrap),
error_callback = callbackWrap,
timeout = self.DEFAULT_REQUEST_TIMEOUT)
def checkUserCanCreateNewLibraryProject(self, callback: Callable) -> None:
"""
Checks if the user is allowed to create new library projects.
A user is allowed to create new library projects if the haven't reached their maximum allowed private projects.
"""
def callbackWrap(response: Optional[Any] = None, *args, **kwargs) -> None:
if response is not None:
if isinstance(response, DigitalFactoryProjectResponse): # The user has only one private project
callback(True)
elif isinstance(response, list) and all(isinstance(r, DigitalFactoryProjectResponse) for r in response):
callback(len(response) < cast(int, self._library_max_private_projects))
else:
Logger.warning(f"Digital Factory: Incorrect response type received when requesting private projects: {str(response)}")
callback(False)
else:
Logger.warning(f"Digital Factory: Response is empty, likely an error: {str(response)}")
callback(False)
if self._library_max_private_projects is not None and self._library_max_private_projects > 0:
# The user has a limit in the number of private projects they can create. Check whether they have already
# reached that limit.
# Note: Set the pagination manager to None when doing this get request, or else the next/previous links
# of the pagination will become corrupted
url = f"{self.CURA_API_ROOT}/projects?shared=false&limit={self._library_max_private_projects}"
self._http.get(url,
scope = self._scope,
callback = self._parseCallback(callbackWrap, DigitalFactoryProjectResponse, callbackWrap, pagination_manager = None),
error_callback = callbackWrap,
timeout = self.DEFAULT_REQUEST_TIMEOUT)
else:
# If the limit is -1, then the user is allowed unlimited projects. If its 0 then they are not allowed to
# create any projects
callback(self._library_max_private_projects == -1)
def getProject(self, library_project_id: str, on_finished: Callable[[DigitalFactoryProjectResponse], Any], failed: Callable) -> None:
"""
Retrieves a digital factory project by its library project id.
@ -73,7 +132,7 @@ class DigitalFactoryApiClient:
error_callback = failed,
timeout = self.DEFAULT_REQUEST_TIMEOUT)
def getProjectsFirstPage(self, on_finished: Callable[[List[DigitalFactoryProjectResponse]], Any], failed: Callable) -> None:
def getProjectsFirstPage(self, search_filter: str, on_finished: Callable[[List[DigitalFactoryProjectResponse]], Any], failed: Callable) -> None:
"""
Retrieves digital factory projects for the user that is currently logged in.
@ -81,13 +140,18 @@ class DigitalFactoryApiClient:
according to the limit set in the pagination manager. If there is no projects pagination manager, this function
leaves the project limit to the default set on the server side (999999).
:param search_filter: Text to filter the search results. If given an empty string, results are not filtered.
:param on_finished: The function to be called after the result is parsed.
:param failed: The function to be called if the request fails.
"""
url = "{}/projects".format(self.CURA_API_ROOT)
url = f"{self.CURA_API_ROOT}/projects"
query_character = "?"
if self._projects_pagination_mgr:
self._projects_pagination_mgr.reset() # reset to clear all the links and response metadata
url += "?limit={}".format(self._projects_pagination_mgr.limit)
url += f"{query_character}limit={self._projects_pagination_mgr.limit}"
query_character = "&"
if search_filter != "":
url += f"{query_character}search={search_filter}"
self._http.get(url,
scope = self._scope,
@ -301,12 +365,10 @@ class DigitalFactoryApiClient:
:param on_finished: The function to be called after the result is parsed.
:param on_error: The function to be called if anything goes wrong.
"""
display_name = re.sub(r"[^a-zA-Z0-9- ./™®ö+']", " ", project_name)
Logger.log("i", "Attempt to create new DF project '{}'.".format(display_name))
Logger.log("i", "Attempt to create new DF project '{}'.".format(project_name))
url = "{}/projects".format(self.CURA_API_ROOT)
data = json.dumps({"data": {"display_name": display_name}}).encode()
data = json.dumps({"data": {"display_name": project_name}}).encode()
self._http.put(url,
scope = self._scope,
data = data,

View File

@ -1,4 +1,6 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
import math
import os
@ -8,7 +10,7 @@ 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, QUrl
from PyQt5.QtCore import pyqtSignal, QObject, pyqtSlot, pyqtProperty, Q_ENUMS, QTimer, QUrl
from PyQt5.QtNetwork import QNetworkReply
from PyQt5.QtQml import qmlRegisterType, qmlRegisterUncreatableType
@ -89,6 +91,12 @@ class DigitalFactoryController(QObject):
uploadFileError = Signal()
uploadFileFinished = Signal()
"""Signal to inform about the state of user access."""
userAccessStateChanged = pyqtSignal(bool)
"""Signal to inform whether the user is allowed to create more Library projects."""
userCanCreateNewLibraryProjectChanged = pyqtSignal(bool)
def __init__(self, application: CuraApplication) -> None:
super().__init__(parent = None)
@ -106,12 +114,18 @@ class DigitalFactoryController(QObject):
self._has_more_projects_to_load = False
self._account = self._application.getInstance().getCuraAPI().account # type: Account
self._account.loginStateChanged.connect(self._onLoginStateChanged)
self._current_workspace_information = CuraApplication.getInstance().getCurrentWorkspaceInformation()
# Initialize the project model
self._project_model = DigitalFactoryProjectModel()
self._selected_project_idx = -1
self._project_creation_error_text = "Something went wrong while creating a new project. Please try again."
self._project_filter = ""
self._project_filter_change_timer = QTimer()
self._project_filter_change_timer.setInterval(200)
self._project_filter_change_timer.setSingleShot(True)
self._project_filter_change_timer.timeout.connect(self._applyProjectFilter)
# Initialize the file model
self._file_model = DigitalFactoryFileModel()
@ -131,6 +145,9 @@ class DigitalFactoryController(QObject):
self._application.engineCreatedSignal.connect(self._onEngineCreated)
self._application.initializationFinished.connect(self._applicationInitializationFinished)
self._user_has_access = False
self._user_account_can_create_new_project = False
def clear(self) -> None:
self._project_model.clearProjects()
self._api.clear()
@ -143,16 +160,22 @@ class DigitalFactoryController(QObject):
self.setSelectedProjectIndex(-1)
def _onLoginStateChanged(self, logged_in: bool) -> None:
def callback(has_access, **kwargs):
self._user_has_access = has_access
self.userAccessStateChanged.emit(logged_in)
self._api.checkUserHasAccess(callback)
def userAccountHasLibraryAccess(self) -> bool:
"""
Checks whether the currently logged in user account has access to the Digital Library
:return: True if the user account has Digital Library access, else False
"""
subscriptions = [] # type: List[Dict[str, Any]]
if self._account.userProfile:
subscriptions = self._account.userProfile.get("subscriptions", [])
return len(subscriptions) > 0
if self._user_has_access:
self._api.checkUserCanCreateNewLibraryProject(callback = self.setCanCreateNewLibraryProject)
return self._user_has_access
def initialize(self, preselected_project_id: Optional[str] = None) -> None:
self.clear()
@ -162,7 +185,7 @@ class DigitalFactoryController(QObject):
if preselected_project_id:
self._api.getProject(preselected_project_id, on_finished = self.setProjectAsPreselected, failed = self._onGetProjectFailed)
else:
self._api.getProjectsFirstPage(on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
def setProjectAsPreselected(self, df_project: DigitalFactoryProjectResponse) -> None:
"""
@ -288,6 +311,38 @@ class DigitalFactoryController(QObject):
self._selected_file_indices = file_indices
self.selectedFileIndicesChanged.emit(file_indices)
def setProjectFilter(self, new_filter: str) -> None:
"""
Called when the user wants to change the search filter for projects.
The filter is not immediately applied. There is some delay to allow the user to finish typing.
:param new_filter: The new filter that the user wants to apply.
"""
self._project_filter = new_filter
self._project_filter_change_timer.start()
"""
Signal to notify Qt that the applied filter has changed.
"""
projectFilterChanged = pyqtSignal()
@pyqtProperty(str, notify = projectFilterChanged, fset = setProjectFilter)
def projectFilter(self) -> str:
"""
The current search filter being applied to the project list.
:return: The current search filter being applied to the project list.
"""
return self._project_filter
def _applyProjectFilter(self) -> None:
"""
Actually apply the current filter to search for projects with the user-defined search string.
:return:
"""
self.clear()
self.projectFilterChanged.emit()
self._api.getProjectsFirstPage(search_filter = self._project_filter, on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
@pyqtProperty(QObject, constant = True)
def digitalFactoryProjectModel(self) -> "DigitalFactoryProjectModel":
return self._project_model
@ -502,7 +557,8 @@ class DigitalFactoryController(QObject):
# false, we also need to clean it from the projects model
self._project_model.clearProjects()
self.setSelectedProjectIndex(-1)
self._api.getProjectsFirstPage(on_finished = self._onGetProjectsFirstPageFinished, failed = self._onGetProjectsFailed)
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._has_preselected_project = new_has_preselected_project
self.preselectedProjectChanged.emit()
@ -511,6 +567,14 @@ class DigitalFactoryController(QObject):
def hasPreselectedProject(self) -> bool:
return self._has_preselected_project
def setCanCreateNewLibraryProject(self, can_create_new_library_project: bool) -> None:
self._user_account_can_create_new_project = can_create_new_library_project
self.userCanCreateNewLibraryProjectChanged.emit(self._user_account_can_create_new_project)
@pyqtProperty(bool, fset = setCanCreateNewLibraryProject, notify = userCanCreateNewLibraryProjectChanged)
def userAccountCanCreateNewLibraryProject(self) -> bool:
return self._user_account_can_create_new_project
@pyqtSlot(str, "QStringList")
def saveFileToSelectedProject(self, filename: str, formats: List[str]) -> None:
"""

View File

@ -0,0 +1,43 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from .BaseModel import BaseModel
from typing import Optional
class DigitalFactoryFeatureBudgetResponse(BaseModel):
"""Class representing the capabilities of a user account for Digital Library.
NOTE: For each max_..._projects fields, '-1' means unlimited!
"""
def __init__(self,
library_can_use_business_value: Optional[bool] = False,
library_can_use_comments: Optional[bool] = False,
library_can_use_status: Optional[bool] = False,
library_can_use_tags: Optional[bool] = False,
library_can_use_technical_requirements: Optional[bool] = False,
library_max_organization_shared_projects: Optional[int] = None, # -1 means unlimited
library_max_private_projects: Optional[int] = None, # -1 means unlimited
library_max_team_shared_projects: Optional[int] = None, # -1 means unlimited
**kwargs) -> None:
self.library_can_use_business_value = library_can_use_business_value
self.library_can_use_comments = library_can_use_comments
self.library_can_use_status = library_can_use_status
self.library_can_use_tags = library_can_use_tags
self.library_can_use_technical_requirements = library_can_use_technical_requirements
self.library_max_organization_shared_projects = library_max_organization_shared_projects # -1 means unlimited
self.library_max_private_projects = library_max_private_projects # -1 means unlimited
self.library_max_team_shared_projects = library_max_team_shared_projects # -1 means unlimited
super().__init__(**kwargs)
def __repr__(self) -> str:
return "max private: {}, max org: {}, max team: {}".format(
self.library_max_private_projects,
self.library_max_organization_shared_projects,
self.library_max_team_shared_projects)
# Validates the model, raising an exception if the model is invalid.
def validate(self) -> None:
super().validate()
# No validation for now, as the response can be "data: []", which should be interpreted as all False and 0's

View File

@ -22,7 +22,7 @@ class DigitalFactoryFileProvider(FileProvider):
self._dialog = None
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account
self._account.loginStateChanged.connect(self._onLoginStateChanged)
self._controller.userAccessStateChanged.connect(self._onUserAccessStateChanged)
self.enabled = self._account.isLoggedIn and self._controller.userAccountHasLibraryAccess()
self.priority = 10
@ -53,7 +53,7 @@ class DigitalFactoryFileProvider(FileProvider):
if not self._dialog:
Logger.log("e", "Unable to create the Digital Library Open dialog.")
def _onLoginStateChanged(self, logged_in: bool) -> None:
def _onUserAccessStateChanged(self, logged_in: bool) -> None:
"""
Sets the enabled status of the DigitalFactoryFileProvider according to the account's login status
:param logged_in: The new login status

View File

@ -45,7 +45,7 @@ class DigitalFactoryOutputDevice(ProjectOutputDevice):
self._writing = False
self._account = CuraApplication.getInstance().getCuraAPI().account # type: Account
self._account.loginStateChanged.connect(self._onLoginStateChanged)
self._controller.userAccessStateChanged.connect(self._onUserAccessStateChanged)
self.enabled = self._account.isLoggedIn and self._controller.userAccountHasLibraryAccess()
self._current_workspace_information = CuraApplication.getInstance().getCurrentWorkspaceInformation()
@ -97,7 +97,7 @@ class DigitalFactoryOutputDevice(ProjectOutputDevice):
if not self._dialog:
Logger.log("e", "Unable to create the Digital Library Save dialog.")
def _onLoginStateChanged(self, logged_in: bool) -> None:
def _onUserAccessStateChanged(self, logged_in: bool) -> None:
"""
Sets the enabled status of the DigitalFactoryOutputDevice according to the account's login status
:param logged_in: The new login status

View File

@ -1,3 +1,6 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from unittest.mock import MagicMock
import pytest
@ -37,7 +40,7 @@ def test_getProjectsFirstPage(api_client):
failed_callback = MagicMock()
# Call
api_client.getProjectsFirstPage(on_finished = finished_callback, failed = failed_callback)
api_client.getProjectsFirstPage(search_filter = "filter", on_finished = finished_callback, failed = failed_callback)
# Asserts
pagination_manager.reset.assert_called_once() # Should be called since we asked for new set of projects
@ -45,16 +48,16 @@ def test_getProjectsFirstPage(api_client):
args = http_manager.get.call_args_list[0]
# Ensure that it's called with the right limit
assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=20"
assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=20&search=filter"
# Change the limit & try again
http_manager.get.reset_mock()
pagination_manager.limit = 80
api_client.getProjectsFirstPage(on_finished = finished_callback, failed = failed_callback)
api_client.getProjectsFirstPage(search_filter = "filter", on_finished = finished_callback, failed = failed_callback)
args = http_manager.get.call_args_list[0]
# Ensure that it's called with the right limit
assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=80"
assert args[0][0] == "https://api.ultimaker.com/cura/v1/projects?limit=80&search=filter"
def test_getMoreProjects_noNewProjects(api_client):

View File

@ -1,4 +1,4 @@
# Copyright (c) 2020 Ultimaker B.V.
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
@ -103,20 +103,27 @@ class PerObjectSettingsTool(Tool):
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
for property_key in ["top_bottom_thickness", "wall_thickness", "wall_line_count"]:
# Override some settings to ensure that the infill mesh by default adds no skin or walls. Or remove them if not an infill mesh.
specialized_settings = {
"top_bottom_thickness": 0,
"top_thickness": "=top_bottom_thickness",
"bottom_thickness": "=top_bottom_thickness",
"top_layers": "=0 if infill_sparse_density == 100 else math.ceil(round(top_thickness / resolveOrValue('layer_height'), 4))",
"bottom_layers": "=0 if infill_sparse_density == 100 else math.ceil(round(bottom_thickness / resolveOrValue('layer_height'), 4))",
"wall_thickness": 0,
"wall_line_count": "=max(1, round((wall_thickness - wall_line_width_0) / wall_line_width_x) + 1) if wall_thickness != 0 else 0"
}
for property_key in specialized_settings:
if mesh_type == "infill_mesh":
if settings.getInstance(property_key) is None:
definition = stack.getSettingDefinition(property_key)
new_instance = SettingInstance(definition, settings)
# We just want the wall_line count to be there in case it was overriden in the global stack.
# as such, we don't need to set a value.
if property_key != "wall_line_count":
new_instance.setProperty("value", 0)
new_instance.setProperty("value", specialized_settings[property_key])
new_instance.resetState() # Ensure that the state is not seen as a user state.
settings.addInstance(new_instance)
settings_visibility_changed = True
elif old_mesh_type == "infill_mesh" and settings.getInstance(property_key) and (settings.getProperty(property_key, "value") == 0 or property_key == "wall_line_count"):
elif old_mesh_type == "infill_mesh" and settings.getInstance(property_key) and property_key in specialized_settings:
settings.removeInstance(property_key)
settings_visibility_changed = True

View File

@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.9
import QtQuick.Layouts 1.1
import QtQuick.Controls 2.3
@ -13,6 +13,8 @@ Item
{
id: prepareMenu
property var fileProviderModel: CuraApplication.getFileProviderModel()
UM.I18nCatalog
{
id: catalog
@ -36,9 +38,9 @@ Item
{
id: itemRow
anchors.left: openFileButton.right
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.leftMargin: UM.Theme.getSize("default_margin").width + openFileButton.width + openFileMenu.width
property int machineSelectorWidth: Math.round((width - printSetupSelectorItem.width) / 3)
height: parent.height
@ -74,22 +76,116 @@ Item
}
}
//Pop-up shown when there are multiple items to select from.
Cura.ExpandablePopup
{
id: openFileMenu
visible: prepareMenu.fileProviderModel.count > 1
contentAlignment: Cura.ExpandablePopup.ContentAlignment.AlignLeft
headerCornerSide: Cura.RoundedRectangle.Direction.All
headerPadding: Math.round((parent.height - UM.Theme.getSize("button_icon").height) / 2)
contentPadding: UM.Theme.getSize("default_lining").width
enabled: visible
height: parent.height
width: visible ? (headerPadding * 3 + UM.Theme.getSize("button_icon").height + iconSize) : 0
headerItem: UM.RecolorImage
{
id: menuIcon
source: UM.Theme.getIcon("Folder", "medium")
color: UM.Theme.getColor("icon")
sourceSize.height: height
}
contentItem: Item
{
id: popup
Column
{
id: openProviderColumn
//The column doesn't automatically listen to its children rect if the children change internally, so we need to explicitly update the size.
onChildrenRectChanged:
{
popup.height = childrenRect.height
popup.width = childrenRect.width
}
onPositioningComplete:
{
popup.height = childrenRect.height
popup.width = childrenRect.width
}
Repeater
{
model: prepareMenu.fileProviderModel
delegate: Button
{
leftPadding: UM.Theme.getSize("default_margin").width
rightPadding: UM.Theme.getSize("default_margin").width
width: contentItem.width + leftPadding + rightPadding
height: UM.Theme.getSize("action_button").height
hoverEnabled: true
contentItem: Label
{
text: model.displayText
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
verticalAlignment: Text.AlignVCenter
width: contentWidth
height: parent.height
}
onClicked:
{
if(model.index == 0) //The 0th element is the "From Disk" option, which should activate the open local file dialog.
{
Cura.Actions.open.trigger();
}
else
{
prepareMenu.fileProviderModel.trigger(model.name);
}
}
background: Rectangle
{
color: parent.hovered ? UM.Theme.getColor("action_button_hovered") : "transparent"
radius: UM.Theme.getSize("action_button_radius").width
width: popup.width
}
}
}
}
}
}
//If there is just a single item, show a button instead that directly chooses the one option.
Button
{
id: openFileButton
height: UM.Theme.getSize("stage_menu").height
width: UM.Theme.getSize("stage_menu").height
visible: prepareMenu.fileProviderModel.count <= 1
height: parent.height
width: visible ? height : 0 //Square button (and don't take up space if invisible).
onClicked: Cura.Actions.open.trigger()
enabled: visible && prepareMenu.fileProviderModel.count > 0
hoverEnabled: true
contentItem: Item
{
anchors.fill: parent
UM.RecolorImage
{
id: buttonIcon
anchors.centerIn: parent
source: UM.Theme.getIcon("Folder", "medium")
anchors.centerIn: parent
width: UM.Theme.getSize("button_icon").width
height: UM.Theme.getSize("button_icon").height
color: UM.Theme.getColor("icon")
@ -101,8 +197,8 @@ Item
background: Rectangle
{
id: background
height: UM.Theme.getSize("stage_menu").height
width: UM.Theme.getSize("stage_menu").height
height: parent.height
width: parent.width
border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width

View File

@ -203,16 +203,16 @@ Cura.ExpandableComponent
style: UM.Theme.styles.checkbox
UM.RecolorImage
Rectangle
{
id: swatch
anchors.verticalCenter: parent.verticalCenter
anchors.right: extrudersModelCheckBox.right
width: UM.Theme.getSize("layerview_legend_size").width
height: UM.Theme.getSize("layerview_legend_size").height
source: UM.Theme.getIcon("Extruder", "medium")
color: model.color
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
}
Label

View File

@ -256,7 +256,16 @@ class CloudOutputDevice(UltimakerNetworkedPrinterOutputDevice):
"""
self._uploaded_print_job = self._pre_upload_print_job
self._progress.hide()
PrintJobUploadSuccessMessage().show()
message = PrintJobUploadSuccessMessage()
message.addAction("monitor print",
name=I18N_CATALOG.i18nc("@action:button", "Monitor print"),
icon="",
description=I18N_CATALOG.i18nc("@action:tooltip", "Track the print in Ultimaker Digital Factory"),
button_align=message.ActionButtonAlignment.ALIGN_RIGHT)
df_url = f"https://digitalfactory.ultimaker.com/app/jobs/{self._cluster.cluster_id}?utm_source=cura&utm_medium=software&utm_campaign=monitor-button"
message.pyQtActionTriggered.connect(lambda message, action: (QDesktopServices.openUrl(QUrl(df_url)), message.hide()))
message.show()
self.writeFinished.emit()
def _onPrintUploadSpecificError(self, reply: "QNetworkReply", _: "QNetworkReply.NetworkError"):

View File

@ -13,6 +13,5 @@ class PrintJobUploadSuccessMessage(Message):
def __init__(self) -> None:
super().__init__(
text = I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."),
title = I18N_CATALOG.i18nc("@info:title", "Data Sent"),
lifetime = 5
title = I18N_CATALOG.i18nc("@info:title", "Data Sent")
)

View File

@ -115,6 +115,7 @@
"skirt_gap": { "value": 5.0 },
"skirt_line_count": { "value": 4 },
"meshfix_maximum_deviation": { "value": 0.05 },
"support_angle": { "value": "math.floor(math.degrees(math.atan(line_width / 2.0 / layer_height)))" },
"support_pattern": { "value": "'zigzag'" },

View File

@ -5,6 +5,7 @@
"metadata": {
"visible": true,
"platform": "kossel_pro_build_platform.3mf",
"platform_offset": [0, -0.25, 0],
"machine_extruder_trains": {
"0": "anycubic_kossel_extruder_0"
}

View File

@ -0,0 +1,35 @@
{
"name": "Atom 2",
"version": 2,
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "Victor (Yu Chieh) Lin",
"manufacturer": "Layer One",
"file_formats": "text/x-gcode",
"platform_offset": [0,0,0],
"machine_extruder_trains": { "0": "atom2_extruder_0"
}
},
"overrides": {
"machine_name": { "default_value": "Atom 2" },
"machine_shape": { "default_value": "elliptic" },
"machine_width": { "default_value": 210 },
"machine_depth": { "default_value": 210 },
"machine_height": { "default_value": 320 },
"machine_extruder_count": { "default_value": 1 },
"machine_heated_bed": { "default_value": false },
"machine_center_is_zero": { "default_value": true },
"machine_start_gcode": { "default_value": "G21\nG90 \nM107\nG28\nG92 E0\nG1 F200 E3\nG92 E0" },
"machine_end_gcode": { "default_value": "M104 S0\nG28\nG91\nG1 E-6 F300\nM84\nG90" },
"layer_height": { "default_value": 0.2 },
"default_material_print_temperature": { "default_value": 210 },
"speed_print": { "default_value": 32 },
"optimize_wall_printing_order": { "value": "True" },
"infill_sparse_density": { "default_value": 10 },
"brim_width": { "default_value": 4 }
}
}

View File

@ -85,8 +85,8 @@
"material_flow": { "value": 100 },
"travel_compensate_overlapping_walls_0_enabled": { "value": "False" },
"z_seam_type": { "value": "'back'" },
"z_seam_corner": { "value": "'z_seam_corner_weighted'" },
"z_seam_type": { "value": "'sharpest_corner'" },
"z_seam_corner": { "value": "'z_seam_corner_inner'" },
"infill_line_width": { "value": "line_width * 1.2" },
"infill_sparse_density": { "value": "20" },
@ -156,7 +156,7 @@
"support_interface_enable": { "value": true },
"support_interface_height": { "value": "layer_height * 4" },
"support_interface_density": { "value": 33.333 },
"support_interface_density": { "value": 75 },
"support_interface_pattern": { "value": "'grid'" },
"support_interface_skip_height": { "value": 0.2 },
"minimum_support_area": { "value": 2 },

View File

@ -0,0 +1,51 @@
{
"name": "Biqu BX",
"version": 2,
"inherits": "biqu_base",
"metadata": {
"quality_definition": "biqu_base",
"visible": true,
"has_machine_materials": true,
"platform": "BIQU_BX_PLATE.stl",
"platform_offset": [
7,
-7.4,
-3
]
},
"overrides": {
"coasting_enable": { "value": false },
"retraction_amount": { "value": 1 },
"retraction_speed": { "value": 40 },
"retraction_extrusion_window": { "value": 1 },
"retract_at_layer_change": { "value": true },
"support_enable": { "value": false },
"support_structure": { "value": "'normal'" },
"support_type": { "value": "'buildplate'" },
"support_angle": { "value": 45 },
"support_infill_rate": { "value": 15 },
"infill_overlap": { "value": 15.0 },
"skin_overlap": { "value": 20.0 },
"fill_outline_gaps": { "value": true },
"filter_out_tiny_gaps": { "value": true },
"roofing_layer_count": { "value": 2 },
"xy_offset_layer_0": { "value": -0.1 },
"speed_print": { "value": 50 },
"machine_name": { "default_value": "Biqu BX" },
"machine_width": { "value": 250 },
"machine_depth": { "value": 250 },
"machine_height": { "value": 250 },
"machine_head_with_fans_polygon": { "default_value": [
[-33, 35],
[-33, -23],
[33, -23],
[33, 35]
]
},
"machine_start_gcode": {
"default_value": "; BIQU BX Start G-code\r\n; For inforation on how to tune this profile and get the\r\n; most out of your BX visit: https:\/\/github.com\/looxonline\/Marlin\r\n; For the official github site visit: https:\/\/github.com\/bigtreetech\/BIQU-BX\r\n\r\nM117 Initial homing sequence. ; Home so that the probe is positioned to heat\r\nG28\r\n\r\nM117 Probe heating position\r\nG0 X65 Y5 Z1 ; Move the probe to the heating position.\r\n\r\nM117 Getting the heaters up to temp!\r\nM104 S140 ; Set Extruder temperature, no wait\r\nM140 S60 ; Set Heat Bed temperature\r\nM190 S60 ; Wait for Heat Bed temperature\r\n\r\nM117 Waiting for probe to warm! ; Wait another 90s for the probe to absorb heat.\r\nG4 S90 \r\n\r\nM117 Post warming re-home\r\nG28 ; Home all axes again after warming\r\n\r\nM117 Z-Dance of my people\r\nG34\r\n\r\nM117 ABL Probing\r\nG29\r\n\r\nM900 K0 L0 T0 ;Edit the K and L values if you have calibrated a k factor for your filament\r\nM900 T0 S0\r\n\r\nG1 Z2.0 F3000 ; Move Z Axis up little to prevent scratching of Heat Bed\r\nG1 X4.1 Y10 Z0.3 F5000.0 ; Move to start position\r\n\r\nM117 Getting the extruder up to temp\r\nM140 S{material_bed_temperature_layer_0} ; Set Heat Bed temperature\r\nM104 S{material_print_temperature_layer_0} ; Set Extruder temperature\r\nM109 S{material_print_temperature_layer_0} ; Wait for Extruder temperature\r\nM190 S{material_bed_temperature_layer_0} ; Wait for Heat Bed temperature\r\n\r\nG92 E0 ; Reset Extruder\r\nM117 Purging\r\nG1 X4.1 Y200.0 Z0.3 F1500.0 E15 ; Draw the first line\r\nG1 X4.4 Y200.0 Z0.3 F5000.0 ; Move to side a little\r\nG1 X4.4 Y20 Z0.3 F1500.0 E30 ; Draw the second line\r\nG92 E0 ; Reset Extruder\r\nM117 Lets make\r\nG1 X8 Y20 Z0.3 F5000.0 ; Move over to prevent blob squish"
},
"gantry_height": { "value": 27.5 }
}
}

View File

@ -0,0 +1,76 @@
{
"version": 2,
"name": "FlSun SuperRacer",
"inherits": "fdmprinter",
"metadata": {
"visible": true,
"author": "Thushan Fernando",
"manufacturer": "Flsun",
"platform": "flsun_sr.3mf",
"file_formats": "text/x-gcode",
"has_materials": true,
"has_machine_quality": false,
"preferred_quality_type": "fast",
"machine_extruder_trains": {
"0": "flsun_sr_extruder_0"
}
},
"overrides": {
"machine_extruder_count": {
"default_value": 1
},
"retraction_enable": {
"default_value": true
},
"machine_heated_bed": {
"default_value": true
},
"machine_width": {
"default_value": 260
},
"machine_depth": {
"default_value": 260
},
"machine_height": {
"default_value": 330
},
"machine_center_is_zero": {
"default_value": true
},
"machine_head_with_fans_polygon": {
"default_value": [
[0, 0],
[0, 0],
[0, 0],
[0, 0]
]
},
"z_seam_type": {
"value": "'back'"
},
"gantry_height": {
"value": "0"
},
"machine_shape": {
"default_value": "elliptic"
},
"machine_nozzle_size": {
"default_value": 0.4
},
"material_diameter": {
"default_value": 1.75
},
"infill_sparse_density": {
"default_value": 15
},
"machine_start_gcode": {
"default_value": "G21\nG90\nM82\nM107 T0\nM140 S{material_bed_temperature}\nM104 S{material_print_temperature} T0\nM190 S{material_bed_temperature}\nM109 S{material_print_temperature} T0\nG28\nG92 E0\n"
},
"machine_end_gcode": {
"default_value": "M107 T0\nM104 S0\nM104 S0 T1\nM140 S0\nG92 E0\nG91\nG1 E-1 F300 \nG1 Z+0.5 E-5 F6000\nG28 X0 Y0\nG90 ;absolute positioning\n"
},
"machine_gcode_flavor": {
"default_value": "RepRap (Marlin/Sprinter)"
}
}
}

View File

@ -4,7 +4,7 @@
"inherits": "fdmprinter",
"metadata": {
"author": "William & Cataldo URSO",
"manufacturer": "Shenzhen Geeetech Technology",
"manufacturer": "Geeetech",
"file_formats": "text/x-gcode",
"visible": true,
"has_materials": true,

View File

@ -3,7 +3,7 @@
"name": "MP Mini Delta",
"inherits": "fdmprinter",
"metadata": {
"author": "MPMD Facebook Group",
"author": "MPMD V1 Facebook Group",
"manufacturer": "Monoprice",
"file_formats": "text/x-gcode",
"platform": "mp_mini_delta_platform.3mf",
@ -25,7 +25,7 @@
"overrides": {
"machine_start_gcode":
{
"default_value": ";MPMD Basic Calibration Tutorial: \n; https://www.thingiverse.com/thing:3892011 \n; \n; If you want to put calibration values in your \n; Start Gcode, put them here. \n; \n;If on stock firmware, at minimum, consider adding \n;M665 R here since there is a firmware bug. \n; \n; Calibration part ends here \n; \nG90 ; switch to absolute positioning \nG92 E0 ; reset extrusion distance \nG1 E20 F200 ; purge 20mm of filament to prime nozzle. \nG92 E0 ; reset extrusion distance \nG4 S5 ; Pause for 5 seconds to allow time for removing extruded filament \nG28 ; start from home position \nG1 E-6 F900 ; retract 6mm of filament before starting the bed leveling process \nG92 E0 ; reset extrusion distance \nG4 S5 ; pause for 5 seconds to allow time for removing extruded filament \nG29 P2 Z0.28 ; Auto-level ; ADJUST Z higher or lower to set first layer height. Start with 0.02 adjustments. \nG1 Z30 ; raise Z 30mm to prepare for priming the nozzle \nG1 E5 F200 ; extrude 5mm of filament to help prime the nozzle just prior to the start of the print \nG92 E0 ; reset extrusion distance \nG4 S5 ; pause for 5 seconds to allow time for cleaning the nozzle and build plate if needed "
"default_value": ";MPMD V1 Basic Calibration Tutorial: \n; https://www.thingiverse.com/thing:3892011 \n; \n; If you want to put calibration values in your \n; Start Gcode, put them here. \n; \n;If on stock firmware, at minimum, consider adding \n;M665 R here since there is a firmware bug. \n; \n; Calibration part ends here \n; \nG90 ; switch to absolute positioning \nG92 E0 ; reset extrusion distance \nG1 E20 F200 ; purge 20mm of filament to prime nozzle. \nG92 E0 ; reset extrusion distance \nG4 S5 ; Pause for 5 seconds to allow time for removing extruded filament \nG28 ; start from home position \nG1 E-6 F900 ; retract 6mm of filament before starting the bed leveling process \nG92 E0 ; reset extrusion distance \nG4 S5 ; pause for 5 seconds to allow time for removing extruded filament \nG29 P2 Z0.28 ; Auto-level ; ADJUST Z higher or lower to set first layer height. Start with 0.02 adjustments. \nG1 Z30 ; raise Z 30mm to prepare for priming the nozzle \nG1 E5 F200 ; extrude 5mm of filament to help prime the nozzle just prior to the start of the print \nG92 E0 ; reset extrusion distance \nG4 S5 ; pause for 5 seconds to allow time for cleaning the nozzle and build plate if needed "
},
"machine_end_gcode":
{
@ -47,9 +47,9 @@
"default_value": 0.21
},
"material_bed_temperature": { "value": 40 },
"line_width": { "value": "round(machine_nozzle_size * 0.875, 2)" },
"material_print_temperature_layer_0": { "value": "material_print_temperature + 5" },
"material_bed_temperature_layer_0": { "value": "material_bed_temperature + 5" },
"line_width": { "value": "round(machine_nozzle_size, 2)" },
"material_print_temperature_layer_0": { "value": "material_print_temperature" },
"material_bed_temperature_layer_0": { "value": "material_bed_temperature" },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"machine_max_feedrate_x": { "default_value": 150 },
"machine_max_feedrate_y": { "default_value": 150 },

View File

@ -0,0 +1,51 @@
{
"version": 2,
"name": "MP Mini Delta V2",
"inherits": "fdmprinter",
"metadata": {
"author": "mpminidelta subreddit",
"manufacturer": "Monoprice",
"file_formats": "text/x-gcode",
"platform": "mp_mini_delta_platform.3mf",
"supports_usb_connection": true,
"has_machine_quality": false,
"visible": true,
"platform_offset": [0, 0, 0],
"has_materials": true,
"has_variants": false,
"has_machine_materials": false,
"has_variant_materials": false,
"preferred_quality_type": "normal",
"machine_extruder_trains":
{
"0": "mp_mini_delta_v2_extruder_0"
}
},
"overrides": {
"machine_start_gcode":
{
"default_value": ";(**** start.gcode for MP Mini Delta V2****)\nG21\nG90\nM82\nM107\nM104 S170\nG28 X0 Y0\nG28 Z0\nG29 Z0.4\nG1 Z15 F300\nM109 S{material_print_temperature_layer_0}\nG92 E0\nG1 F200 E3\nG92 E0\nG1 F2000\n"
},
"machine_end_gcode": {
"default_value": ";(**** end.gcode for MP Mini Delta V2****)\nG28;(Stick out the part)\nM190 S0;(Turn off heat bed, don't wait.)\nG92 E10;(Set extruder to 10)\nG1 E7 F200;(retract 3mm)\nM104 S0;(Turn off nozzle, don't wait)\nG4 S300;(Delay 5 minutes)\nM107;(Turn off part fan)\nM84;(Turn off stepper motors.)"
},
"material_print_temp_prepend":{"default_value":false},
"material_bed_temperature": { "value": 40 },
"machine_width": { "default_value": 110 },
"machine_depth": { "default_value": 110 },
"machine_height": { "default_value": 120 },
"machine_heated_bed": { "default_value": true },
"machine_shape": { "default_value": "elliptic" },
"machine_center_is_zero": { "default_value": true },
"machine_nozzle_size": {
"default_value": 0.4
},
"line_width": { "value": "round(machine_nozzle_size, 2)" },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
"retraction_amount": { "default_value": 5 },
"retraction_speed": { "default_value": 28 },
"retraction_hop_enabled": { "default_value": false },
"retract_at_layer_change": { "default_value": true }
}
}

View File

@ -22,7 +22,8 @@
"0": "ultimaker2_plus_connect_extruder_0"
},
"supports_usb_connection": false,
"supports_network_connection": true
"supports_network_connection": true,
"supports_material_export": true
},
"overrides": {

View File

@ -27,6 +27,7 @@
"first_start_actions": [ "DiscoverUM3Action" ],
"supported_actions": [ "DiscoverUM3Action" ],
"supports_usb_connection": false,
"supports_material_export": true,
"weight": -1,
"firmware_update_info": {
"id": 213482,

View File

@ -28,6 +28,7 @@
"supported_actions": [ "DiscoverUM3Action" ],
"supports_usb_connection": false,
"supports_network_connection": true,
"supports_material_export": true,
"weight": -2,
"firmware_update_info": {
"id": 9051,

View File

@ -0,0 +1,15 @@
{
"version": 2,
"name": "Extruder 1",
"inherits": "fdmextruder",
"metadata": {
"machine": "atom2",
"position": "0"
},
"overrides": {
"extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 1.75 }
}
}

View File

@ -0,0 +1,20 @@
{
"version": 2,
"name": "Extruder 1",
"inherits": "fdmextruder",
"metadata": {
"machine": "flsun_sr",
"position": "0"
},
"overrides": {
"extruder_nr": {
"default_value": 0
},
"machine_nozzle_size": {
"default_value": 0.4
},
"material_diameter": {
"default_value": 1.75
}
}
}

View File

@ -0,0 +1,15 @@
{
"version": 2,
"name": "Extruder 0",
"inherits": "fdmextruder",
"metadata": {
"machine": "mp_mini_delta_v2",
"position": "0"
},
"overrides": {
"extruder_nr": { "default_value": 0 },
"machine_nozzle_size": { "default_value": 0.4 },
"material_diameter": { "default_value": 1.75 }
}
}

View File

@ -1177,7 +1177,7 @@ msgstr "Hinten links"
#: fdmprinter.def.json
msgctxt "z_seam_position option back"
msgid "Back"
msgstr "Zurück"
msgstr "Hinten"
#: fdmprinter.def.json
msgctxt "z_seam_position option backright"

Binary file not shown.

Binary file not shown.

View File

@ -18,6 +18,7 @@ Button
property alias textFont: buttonText.font
property alias cornerRadius: backgroundRect.radius
property alias tooltip: tooltip.tooltipText
property alias tooltipWidth: tooltip.width
property color color: UM.Theme.getColor("primary")
property color hoverColor: UM.Theme.getColor("primary_hover")

View File

@ -1,4 +1,4 @@
// Copyright (c) 2018 Ultimaker B.V.
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
pragma Singleton
@ -122,7 +122,15 @@ Item
Action
{
id: quitAction
text: catalog.i18nc("@action:inmenu menubar:file","&Quit")
//On MacOS, don't translate the "Quit" word.
//Qt moves the "quit" entry to a different place, and if it got renamed can't find it again when it attempts to
//delete the item upon closing the application, causing a crash.
//In the new location, these items are translated automatically according to the system's language.
//For more information, see:
//- https://doc.qt.io/qt-5/macos-issues.html#menu-bar
//- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar
text: (Qt.platform.os == "osx") ? "&Quit" : catalog.i18nc("@action:inmenu menubar:file", "&Quit")
iconName: "application-exit"
shortcut: StandardKey.Quit
}
@ -172,7 +180,14 @@ Item
Action
{
id: preferencesAction
text: catalog.i18nc("@action:inmenu", "Configure Cura...")
//On MacOS, don't translate the "Configure" word.
//Qt moves the "configure" entry to a different place, and if it got renamed can't find it again when it
//attempts to delete the item upon closing the application, causing a crash.
//In the new location, these items are translated automatically according to the system's language.
//For more information, see:
//- https://doc.qt.io/qt-5/macos-issues.html#menu-bar
//- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar
text: (Qt.platform.os == "osx") ? "Configure Cura..." : catalog.i18nc("@action:inmenu", "Configure Cura...")
iconName: "configure"
}
@ -263,7 +278,15 @@ Item
Action
{
id: aboutAction;
text: catalog.i18nc("@action:inmenu menubar:help", "About...");
//On MacOS, don't translate the "About" word.
//Qt moves the "about" entry to a different place, and if it got renamed can't find it again when it
//attempts to delete the item upon closing the application, causing a crash.
//In the new location, these items are translated automatically according to the system's language.
//For more information, see:
//- https://doc.qt.io/qt-5/macos-issues.html#menu-bar
//- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar
text: (Qt.platform.os == "osx") ? "About..." : catalog.i18nc("@action:inmenu menubar:help", "About...");
iconName: "help-about";
}

View File

@ -417,6 +417,7 @@ UM.MainWindow
Cura.PrimaryButton
{
text: model.name
iconSource: UM.Theme.getIcon(model.icon)
height: UM.Theme.getSize("message_action_button").height
}
}
@ -426,6 +427,7 @@ UM.MainWindow
Cura.SecondaryButton
{
text: model.name
iconSource: UM.Theme.getIcon(model.icon)
height: UM.Theme.getSize("message_action_button").height
}
}
@ -434,6 +436,14 @@ UM.MainWindow
Cura.TertiaryButton
{
text: model.name
iconSource:
{
if (model.icon == null || model.icon == "")
{
return UM.Theme.getIcon("LinkExternal")
}
return UM.Theme.getIcon(model.icon)
}
height: UM.Theme.getSize("message_action_button").height
}
}

View File

@ -167,7 +167,7 @@ Item
verticalCenter: parent.verticalCenter
margins: background.padding
}
source: expanded ? UM.Theme.getIcon("ChevronSingleDown") : UM.Theme.getIcon("ChevronSingleLeft")
source: UM.Theme.getIcon("ChevronSingleDown")
visible: source != ""
width: UM.Theme.getSize("standard_arrow").width
height: UM.Theme.getSize("standard_arrow").height

View File

@ -180,7 +180,7 @@ Item
verticalCenter: parent.verticalCenter
margins: background.padding
}
source: expanded ? UM.Theme.getIcon("ChevronSingleDown") : UM.Theme.getIcon("ChevronSingleLeft")
source: UM.Theme.getIcon("ChevronSingleDown")
visible: source != ""
width: UM.Theme.getSize("standard_arrow").width
height: UM.Theme.getSize("standard_arrow").height

View File

@ -1,4 +1,4 @@
// Copyright (c) 2018 Ultimaker B.V.
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
@ -48,7 +48,17 @@ Item
ViewMenu { title: catalog.i18nc("@title:menu menubar:toplevel", "&View") }
SettingsMenu { title: catalog.i18nc("@title:menu menubar:toplevel", "&Settings") }
SettingsMenu
{
//On MacOS, don't translate the "Settings" word.
//Qt moves the "settings" entry to a different place, and if it got renamed can't find it again when it
//attempts to delete the item upon closing the application, causing a crash.
//In the new location, these items are translated automatically according to the system's language.
//For more information, see:
//- https://doc.qt.io/qt-5/macos-issues.html#menu-bar
//- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar
title: (Qt.platform.os == "osx") ? "&Settings" : catalog.i18nc("@title:menu menubar:toplevel", "&Settings")
}
Menu
{
@ -91,7 +101,15 @@ Item
Menu
{
id: preferencesMenu
title: catalog.i18nc("@title:menu menubar:toplevel", "P&references")
//On MacOS, don't translate the "Preferences" word.
//Qt moves the "preferences" entry to a different place, and if it got renamed can't find it again when it
//attempts to delete the item upon closing the application, causing a crash.
//In the new location, these items are translated automatically according to the system's language.
//For more information, see:
//- https://doc.qt.io/qt-5/macos-issues.html#menu-bar
//- https://doc.qt.io/qt-5/qmenubar.html#qmenubar-as-a-global-menu-bar
title: (Qt.platform.os == "osx") ? "&Preferences" : catalog.i18nc("@title:menu menubar:toplevel", "P&references")
MenuItem { action: Cura.Actions.preferences }
}

View File

@ -1,5 +1,5 @@
// Copyright (c) 2018 Ultimaker B.V.
// Uranium is released under the terms of the LGPLv3 or higher.
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 1.4
@ -191,6 +191,21 @@ Item
}
enabled: base.hasCurrentItem
}
//Sync button.
Button
{
id: syncMaterialsButton
text: catalog.i18nc("@action:button Sending materials to printers", "Sync with Printers")
iconName: "sync-synchronizing"
onClicked:
{
forceActiveFocus();
exportAllMaterialsDialog.folder = base.materialManagementModel.getPreferredExportAllPath();
exportAllMaterialsDialog.open();
}
visible: Cura.MachineManager.activeMachine.supportsMaterialExport
}
}
Item {
@ -368,6 +383,19 @@ Item
}
}
FileDialog
{
id: exportAllMaterialsDialog
title: catalog.i18nc("@title:window", "Export All Materials")
selectExisting: false
nameFilters: ["Material archives (*.umm)", "All files (*)"]
onAccepted:
{
base.materialManagementModel.exportAll(fileUrl);
CuraApplication.setDefaultPath("dialog_material_path", folder);
}
}
MessageDialog
{
id: messageDialog

View File

@ -95,7 +95,7 @@ UM.PreferencesPage
placeholderText: catalog.i18nc("@label:textbox", "Filter...")
onTextChanged: definitionsModel.filter = {"i18n_label": "*" + text}
onTextChanged: definitionsModel.filter = {"i18n_label|i18n_description": "*" + text}
}
NewControls.ComboBox

View File

@ -54,7 +54,7 @@ Item
{
id: networkPrinterScrollView
maxItemCountAtOnce: 10 // show at max 10 items at once, otherwise you need to scroll.
maxItemCountAtOnce: 9 // show at max 9 items at once, otherwise you need to scroll.
onRefreshButtonClicked:
{

View File

@ -1,4 +1,4 @@
// Copyright (c) 2019 Ultimaker B.V.
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
@ -214,16 +214,16 @@ Item
id: troubleshootingButton
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.rightMargin: UM.Theme.getSize("thin_margin").width
anchors.verticalCenter: parent.verticalCenter
height: troubleshootingLinkIcon.height
width: troubleshootingLinkIcon.width + troubleshootingLabel.width + UM.Theme.getSize("default_margin").width
width: troubleshootingLinkIcon.width + troubleshootingLabel.width + UM.Theme.getSize("thin_margin").width
UM.RecolorImage
{
id: troubleshootingLinkIcon
anchors.right: troubleshootingLabel.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.rightMargin: UM.Theme.getSize("thin_margin").width
anchors.verticalCenter: parent.verticalCenter
height: troubleshootingLabel.height
width: height

View File

@ -1,4 +1,4 @@
// Copyright (c) 2019 Ultimaker B.V.
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
@ -15,6 +15,8 @@ TextField
{
id: textField
property alias leftIcon: iconLeft.source
UM.I18nCatalog { id: catalog; name: "cura" }
hoverEnabled: true
@ -22,6 +24,7 @@ TextField
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
leftPadding: iconLeft.visible ? iconLeft.width + UM.Theme.getSize("default_margin").width * 2 : UM.Theme.getSize("thin_margin").width
states: [
State
@ -52,7 +55,6 @@ TextField
color: UM.Theme.getColor("main_background")
anchors.margins: Math.round(UM.Theme.getSize("default_lining").width)
radius: UM.Theme.getSize("setting_control_radius").width
border.color:
@ -67,5 +69,23 @@ TextField
}
return UM.Theme.getColor("setting_control_border")
}
//Optional icon added on the left hand side.
UM.RecolorImage
{
id: iconLeft
anchors
{
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
}
visible: source != ""
height: UM.Theme.getSize("small_button_icon").height
width: visible ? height : 0
color: textField.color
}
}
}

View File

@ -16,7 +16,7 @@
"primary_text": [255, 255, 255, 204],
"secondary": [95, 95, 95, 255],
"secondary_button": [0, 0, 0, 0],
"secondary_button": [39, 44, 48, 255],
"secondary_button_hover": [85, 85, 87, 255],
"secondary_button_text": [255, 255, 255, 255],

View File

@ -6,8 +6,8 @@
.st0{fill:#231F20;}
</style>
<g id="Layer_1_1_">
<path class="st0" d="M15,26C8.9,26,4,21.1,4,15S8.9,4,15,4s11,4.9,11,11S21.1,26,15,26z M15,7c-4.4,0-8,3.6-8,8s3.6,8,8,8
s8-3.6,8-8S19.4,7,15,7z"/>
<path class="st0" d="M15,26C8.9,26,4,21.1,4,15S8.9,4,15,4s11,4.9,11,11S21.1,26,15,26z M15,8c-3.8,0-7,3.2-7,7s3.2,7,7,7
s7-3.1,7-7S18.9,8,15,8z"/>
</g>
<g id="Comments">
</g>

Before

Width:  |  Height:  |  Size: 603 B

After

Width:  |  Height:  |  Size: 603 B

View File

@ -193,7 +193,7 @@
"primary_button_hover": [16, 70, 156, 255],
"primary_button_text": [255, 255, 255, 255],
"secondary_button": [255, 255, 255, 0],
"secondary_button": [255, 255, 255, 255],
"secondary_button_shadow": [216, 216, 216, 255],
"secondary_button_hover": [232, 240, 253, 255],
"secondary_button_text": [25, 110, 240, 255],

View File

@ -0,0 +1,12 @@
[general]
name = 0.2mm Nozzle
version = 4
definition = biqu_bx_abl
[metadata]
setting_version = 17
type = variant
hardware_type = nozzle
[values]
machine_nozzle_size = 0.2

View File

@ -0,0 +1,12 @@
[general]
name = 0.3mm Nozzle
version = 4
definition = biqu_bx_abl
[metadata]
setting_version = 17
type = variant
hardware_type = nozzle
[values]
machine_nozzle_size = 0.3

View File

@ -0,0 +1,12 @@
[general]
name = 0.4mm Nozzle
version = 4
definition = biqu_bx_abl
[metadata]
setting_version = 17
type = variant
hardware_type = nozzle
[values]
machine_nozzle_size = 0.4

View File

@ -0,0 +1,12 @@
[general]
name = 0.5mm Nozzle
version = 4
definition = biqu_bx_abl
[metadata]
setting_version = 17
type = variant
hardware_type = nozzle
[values]
machine_nozzle_size = 0.5

View File

@ -0,0 +1,12 @@
[general]
name = 0.6mm Nozzle
version = 4
definition = biqu_bx_abl
[metadata]
setting_version = 17
type = variant
hardware_type = nozzle
[values]
machine_nozzle_size = 0.6

View File

@ -0,0 +1,12 @@
[general]
name = 0.8mm Nozzle
version = 4
definition = biqu_bx_abl
[metadata]
setting_version = 17
type = variant
hardware_type = nozzle
[values]
machine_nozzle_size = 0.8