diff --git a/.gitignore b/.gitignore
index 0a66b6eb33..60b59e6829 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,7 +42,6 @@ plugins/cura-siemensnx-plugin
plugins/CuraBlenderPlugin
plugins/CuraCloudPlugin
plugins/CuraDrivePlugin
-plugins/CuraDrive
plugins/CuraLiveScriptingPlugin
plugins/CuraOpenSCADPlugin
plugins/CuraPrintProfileCreator
diff --git a/cura/API/Account.py b/cura/API/Account.py
index be77a6307b..47f67af8ce 100644
--- a/cura/API/Account.py
+++ b/cura/API/Account.py
@@ -6,6 +6,7 @@ from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
from UM.i18n import i18nCatalog
from UM.Message import Message
+from cura import UltimakerCloudAuthentication
from cura.OAuth2.AuthorizationService import AuthorizationService
from cura.OAuth2.Models import OAuth2Settings
@@ -37,15 +38,16 @@ class Account(QObject):
self._logged_in = False
self._callback_port = 32118
- self._oauth_root = "https://account.ultimaker.com"
- self._cloud_api_root = "https://api.ultimaker.com"
+ self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
self._oauth_settings = OAuth2Settings(
OAUTH_SERVER_URL= self._oauth_root,
CALLBACK_PORT=self._callback_port,
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
CLIENT_ID="um----------------------------ultimaker_cura",
- CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.write",
+ CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download "
+ "packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write "
+ "cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write",
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
diff --git a/cura/API/Backups.py b/cura/API/Backups.py
index 8e5cd7b83a..ef74e74be0 100644
--- a/cura/API/Backups.py
+++ b/cura/API/Backups.py
@@ -1,6 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
-from typing import Tuple, Optional, TYPE_CHECKING
+from typing import Tuple, Optional, TYPE_CHECKING, Dict, Any
from cura.Backups.BackupsManager import BackupsManager
@@ -24,12 +24,12 @@ class Backups:
## Create a new back-up using the BackupsManager.
# \return Tuple containing a ZIP file with the back-up data and a dict
# with metadata about the back-up.
- def createBackup(self) -> Tuple[Optional[bytes], Optional[dict]]:
+ def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]:
return self.manager.createBackup()
## Restore a back-up using the BackupsManager.
# \param zip_file A ZIP file containing the actual back-up data.
# \param meta_data Some metadata needed for restoring a back-up, like the
# Cura version number.
- def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
+ def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None:
return self.manager.restoreBackup(zip_file, meta_data)
diff --git a/cura/ApplicationMetadata.py b/cura/ApplicationMetadata.py
new file mode 100644
index 0000000000..e2ac4453eb
--- /dev/null
+++ b/cura/ApplicationMetadata.py
@@ -0,0 +1,36 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+# ---------
+# Genearl constants used in Cura
+# ---------
+DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
+DEFAULT_CURA_VERSION = "master"
+DEFAULT_CURA_BUILD_TYPE = ""
+DEFAULT_CURA_DEBUG_MODE = False
+DEFAULT_CURA_SDK_VERSION = "6.0.0"
+
+try:
+ from cura.CuraVersion import CuraAppDisplayName # type: ignore
+except ImportError:
+ CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
+
+try:
+ from cura.CuraVersion import CuraVersion # type: ignore
+except ImportError:
+ CuraVersion = DEFAULT_CURA_VERSION # [CodeStyle: Reflecting imported value]
+
+try:
+ from cura.CuraVersion import CuraBuildType # type: ignore
+except ImportError:
+ CuraBuildType = DEFAULT_CURA_BUILD_TYPE
+
+try:
+ from cura.CuraVersion import CuraDebugMode # type: ignore
+except ImportError:
+ CuraDebugMode = DEFAULT_CURA_DEBUG_MODE
+
+try:
+ from cura.CuraVersion import CuraSDKVersion # type: ignore
+except ImportError:
+ CuraSDKVersion = DEFAULT_CURA_SDK_VERSION
diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py
index 9855bbe7de..0d71dff106 100755
--- a/cura/CuraApplication.py
+++ b/cura/CuraApplication.py
@@ -117,6 +117,8 @@ from cura.ObjectsModel import ObjectsModel
from cura.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
+from cura import ApplicationMetadata
+
from UM.FlameProfiler import pyqtSlot
from UM.Decorators import override
@@ -164,11 +166,11 @@ class CuraApplication(QtApplication):
def __init__(self, *args, **kwargs):
super().__init__(name = "cura",
- app_display_name = CuraAppDisplayName,
- version = CuraVersion,
- api_version = CuraSDKVersion,
- buildtype = CuraBuildType,
- is_debug_mode = CuraDebugMode,
+ app_display_name = ApplicationMetadata.CuraAppDisplayName,
+ version = ApplicationMetadata.CuraVersion,
+ api_version = ApplicationMetadata.CuraSDKVersion,
+ buildtype = ApplicationMetadata.CuraBuildType,
+ is_debug_mode = ApplicationMetadata.CuraDebugMode,
tray_icon_name = "cura-icon-32.png",
**kwargs)
@@ -500,7 +502,7 @@ class CuraApplication(QtApplication):
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
preferences.addPreference("cura/choice_on_open_project", "always_ask")
preferences.addPreference("cura/use_multi_build_plate", False)
- preferences.addPreference("view/settings_list_height", 600)
+ preferences.addPreference("view/settings_list_height", 400)
preferences.addPreference("view/settings_visible", False)
preferences.addPreference("cura/currency", "€")
preferences.addPreference("cura/material_settings", "{}")
@@ -955,7 +957,7 @@ class CuraApplication(QtApplication):
engine.rootContext().setContextProperty("CuraApplication", self)
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
- engine.rootContext().setContextProperty("CuraSDKVersion", CuraSDKVersion)
+ engine.rootContext().setContextProperty("CuraSDKVersion", ApplicationMetadata.CuraSDKVersion)
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
diff --git a/cura/CuraVersion.py.in b/cura/CuraVersion.py.in
index 7c6304231d..770a0efd7b 100644
--- a/cura/CuraVersion.py.in
+++ b/cura/CuraVersion.py.in
@@ -8,3 +8,4 @@ CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
CuraSDKVersion = "@CURA_SDK_VERSION@"
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
+CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
diff --git a/cura/Machines/MaterialManager.py b/cura/Machines/MaterialManager.py
index aee96f3153..160508e7a6 100644
--- a/cura/Machines/MaterialManager.py
+++ b/cura/Machines/MaterialManager.py
@@ -302,6 +302,10 @@ class MaterialManager(QObject):
def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]:
return self._guid_material_groups_map.get(guid)
+ # Returns a dict of all material groups organized by root_material_id.
+ def getAllMaterialGroups(self) -> Dict[str, "MaterialGroup"]:
+ return self._material_group_map
+
#
# Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
#
@@ -679,7 +683,11 @@ class MaterialManager(QObject):
@pyqtSlot(str)
def removeFavorite(self, root_material_id: str) -> None:
- self._favorites.remove(root_material_id)
+ try:
+ self._favorites.remove(root_material_id)
+ except KeyError:
+ Logger.log("w", "Could not delete material %s from favorites as it was already deleted", root_material_id)
+ return
self.materialsUpdated.emit()
# Ensure all settings are saved.
@@ -688,4 +696,4 @@ class MaterialManager(QObject):
@pyqtSlot()
def getFavorites(self):
- return self._favorites
\ No newline at end of file
+ return self._favorites
diff --git a/cura/UltimakerCloudAuthentication.py b/cura/UltimakerCloudAuthentication.py
new file mode 100644
index 0000000000..5f69329dbb
--- /dev/null
+++ b/cura/UltimakerCloudAuthentication.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+# ---------
+# Constants used for the Cloud API
+# ---------
+DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str
+DEFAULT_CLOUD_API_VERSION = 1 # type: int
+DEFAULT_CLOUD_ACCOUNT_API_ROOT = "https://account.ultimaker.com" # type: str
+
+try:
+ from cura.CuraVersion import CuraCloudAPIRoot # type: ignore
+ if CuraCloudAPIRoot == "":
+ CuraCloudAPIRoot = DEFAULT_CLOUD_API_ROOT
+except ImportError:
+ CuraCloudAPIRoot = DEFAULT_CLOUD_API_ROOT
+
+try:
+ from cura.CuraVersion import CuraCloudAPIVersion # type: ignore
+except ImportError:
+ CuraCloudAPIVersion = DEFAULT_CLOUD_API_VERSION
+
+try:
+ from cura.CuraVersion import CuraCloudAccountAPIRoot # type: ignore
+ if CuraCloudAccountAPIRoot == "":
+ CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT
+except ImportError:
+ CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT
diff --git a/plugins/CuraDrive/__init__.py b/plugins/CuraDrive/__init__.py
new file mode 100644
index 0000000000..eeb6b78689
--- /dev/null
+++ b/plugins/CuraDrive/__init__.py
@@ -0,0 +1,12 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from .src.DrivePluginExtension import DrivePluginExtension
+
+
+def getMetaData():
+ return {}
+
+
+def register(app):
+ return {"extension": DrivePluginExtension()}
diff --git a/plugins/CuraDrive/plugin.json b/plugins/CuraDrive/plugin.json
new file mode 100644
index 0000000000..d1cab39ca5
--- /dev/null
+++ b/plugins/CuraDrive/plugin.json
@@ -0,0 +1,8 @@
+{
+ "name": "Cura Backups",
+ "author": "Ultimaker B.V.",
+ "description": "Backup and restore your configuration.",
+ "version": "1.2.0",
+ "api": 6,
+ "i18n-catalog": "cura"
+}
diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py
new file mode 100644
index 0000000000..7c1f8faa83
--- /dev/null
+++ b/plugins/CuraDrive/src/DriveApiService.py
@@ -0,0 +1,168 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+import base64
+import hashlib
+from datetime import datetime
+from tempfile import NamedTemporaryFile
+from typing import Any, Optional, List, Dict
+
+import requests
+
+from UM.Logger import Logger
+from UM.Message import Message
+from UM.Signal import Signal, signalemitter
+from cura.CuraApplication import CuraApplication
+
+from .UploadBackupJob import UploadBackupJob
+from .Settings import Settings
+
+from UM.i18n import i18nCatalog
+catalog = i18nCatalog("cura")
+
+
+## The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling.
+@signalemitter
+class DriveApiService:
+ BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL)
+
+ # Emit signal when restoring backup started or finished.
+ restoringStateChanged = Signal()
+
+ # Emit signal when creating backup started or finished.
+ creatingStateChanged = Signal()
+
+ def __init__(self) -> None:
+ self._cura_api = CuraApplication.getInstance().getCuraAPI()
+
+ def getBackups(self) -> List[Dict[str, Any]]:
+ access_token = self._cura_api.account.accessToken
+ if not access_token:
+ Logger.log("w", "Could not get access token.")
+ return []
+
+ backup_list_request = requests.get(self.BACKUP_URL, headers = {
+ "Authorization": "Bearer {}".format(access_token)
+ })
+
+ # HTTP status 300s mean redirection. 400s and 500s are errors.
+ # Technically 300s are not errors, but the use case here relies on "requests" to handle redirects automatically.
+ if backup_list_request.status_code >= 300:
+ Logger.log("w", "Could not get backups list from remote: %s", backup_list_request.text)
+ Message(catalog.i18nc("@info:backup_status", "There was an error listing your backups."), title = catalog.i18nc("@info:title", "Backup")).show()
+ return []
+ return backup_list_request.json()["data"]
+
+ def createBackup(self) -> None:
+ self.creatingStateChanged.emit(is_creating = True)
+
+ # Create the backup.
+ backup_zip_file, backup_meta_data = self._cura_api.backups.createBackup()
+ if not backup_zip_file or not backup_meta_data:
+ self.creatingStateChanged.emit(is_creating = False, error_message ="Could not create backup.")
+ return
+
+ # Create an upload entry for the backup.
+ timestamp = datetime.now().isoformat()
+ backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
+ backup_upload_url = self._requestBackupUpload(backup_meta_data, len(backup_zip_file))
+ if not backup_upload_url:
+ self.creatingStateChanged.emit(is_creating = False, error_message ="Could not upload backup.")
+ return
+
+ # Upload the backup to storage.
+ upload_backup_job = UploadBackupJob(backup_upload_url, backup_zip_file)
+ upload_backup_job.finished.connect(self._onUploadFinished)
+ upload_backup_job.start()
+
+ def _onUploadFinished(self, job: "UploadBackupJob") -> None:
+ if job.backup_upload_error_message != "":
+ # If the job contains an error message we pass it along so the UI can display it.
+ self.creatingStateChanged.emit(is_creating = False, error_message = job.backup_upload_error_message)
+ else:
+ self.creatingStateChanged.emit(is_creating = False)
+
+ def restoreBackup(self, backup: Dict[str, Any]) -> None:
+ self.restoringStateChanged.emit(is_restoring = True)
+ download_url = backup.get("download_url")
+ if not download_url:
+ # If there is no download URL, we can't restore the backup.
+ return self._emitRestoreError()
+
+ download_package = requests.get(download_url, stream = True)
+ if download_package.status_code >= 300:
+ # Something went wrong when attempting to download the backup.
+ Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text)
+ return self._emitRestoreError()
+
+ # We store the file in a temporary path fist to ensure integrity.
+ temporary_backup_file = NamedTemporaryFile(delete = False)
+ with open(temporary_backup_file.name, "wb") as write_backup:
+ for chunk in download_package:
+ write_backup.write(chunk)
+
+ if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash", "")):
+ # Don't restore the backup if the MD5 hashes do not match.
+ # This can happen if the download was interrupted.
+ Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.")
+ return self._emitRestoreError()
+
+ # Tell Cura to place the backup back in the user data folder.
+ with open(temporary_backup_file.name, "rb") as read_backup:
+ self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("metadata", {}))
+ self.restoringStateChanged.emit(is_restoring = False)
+
+ def _emitRestoreError(self) -> None:
+ self.restoringStateChanged.emit(is_restoring = False,
+ error_message = catalog.i18nc("@info:backup_status",
+ "There was an error trying to restore your backup."))
+
+ # Verify the MD5 hash of a file.
+ # \param file_path Full path to the file.
+ # \param known_hash The known MD5 hash of the file.
+ # \return: Success or not.
+ @staticmethod
+ def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
+ with open(file_path, "rb") as read_backup:
+ local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8")
+ return known_hash == local_md5_hash
+
+ def deleteBackup(self, backup_id: str) -> bool:
+ access_token = self._cura_api.account.accessToken
+ if not access_token:
+ Logger.log("w", "Could not get access token.")
+ return False
+
+ delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
+ "Authorization": "Bearer {}".format(access_token)
+ })
+ if delete_backup.status_code >= 300:
+ Logger.log("w", "Could not delete backup: %s", delete_backup.text)
+ return False
+ return True
+
+ # Request a backup upload slot from the API.
+ # \param backup_metadata: A dict containing some meta data about the backup.
+ # \param backup_size The size of the backup file in bytes.
+ # \return: The upload URL for the actual backup file if successful, otherwise None.
+ def _requestBackupUpload(self, backup_metadata: Dict[str, Any], backup_size: int) -> Optional[str]:
+ access_token = self._cura_api.account.accessToken
+ if not access_token:
+ Logger.log("w", "Could not get access token.")
+ return None
+
+ backup_upload_request = requests.put(self.BACKUP_URL, json = {
+ "data": {
+ "backup_size": backup_size,
+ "metadata": backup_metadata
+ }
+ }, headers = {
+ "Authorization": "Bearer {}".format(access_token)
+ })
+
+ # Any status code of 300 or above indicates an error.
+ if backup_upload_request.status_code >= 300:
+ Logger.log("w", "Could not request backup upload: %s", backup_upload_request.text)
+ return None
+
+ return backup_upload_request.json()["data"]["upload_url"]
diff --git a/plugins/CuraDrive/src/DrivePluginExtension.py b/plugins/CuraDrive/src/DrivePluginExtension.py
new file mode 100644
index 0000000000..060f1496f1
--- /dev/null
+++ b/plugins/CuraDrive/src/DrivePluginExtension.py
@@ -0,0 +1,162 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+import os
+from datetime import datetime
+from typing import Optional, List, Dict, Any
+
+from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
+
+from UM.Extension import Extension
+from UM.Logger import Logger
+from UM.Message import Message
+from cura.CuraApplication import CuraApplication
+
+from .Settings import Settings
+from .DriveApiService import DriveApiService
+
+from UM.i18n import i18nCatalog
+catalog = i18nCatalog("cura")
+
+
+# The DivePluginExtension provides functionality to backup and restore your Cura configuration to Ultimaker's cloud.
+class DrivePluginExtension(QObject, Extension):
+
+ # Signal emitted when the list of backups changed.
+ backupsChanged = pyqtSignal()
+
+ # Signal emitted when restoring has started. Needed to prevent parallel restoring.
+ restoringStateChanged = pyqtSignal()
+
+ # Signal emitted when creating has started. Needed to prevent parallel creation of backups.
+ creatingStateChanged = pyqtSignal()
+
+ # Signal emitted when preferences changed (like auto-backup).
+ preferencesChanged = pyqtSignal()
+
+ DATE_FORMAT = "%d/%m/%Y %H:%M:%S"
+
+ def __init__(self) -> None:
+ QObject.__init__(self, None)
+ Extension.__init__(self)
+
+ # Local data caching for the UI.
+ self._drive_window = None # type: Optional[QObject]
+ self._backups = [] # type: List[Dict[str, Any]]
+ self._is_restoring_backup = False
+ self._is_creating_backup = False
+
+ # Initialize services.
+ preferences = CuraApplication.getInstance().getPreferences()
+ self._drive_api_service = DriveApiService()
+
+ # Attach signals.
+ CuraApplication.getInstance().getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged)
+ self._drive_api_service.restoringStateChanged.connect(self._onRestoringStateChanged)
+ self._drive_api_service.creatingStateChanged.connect(self._onCreatingStateChanged)
+
+ # Register preferences.
+ preferences.addPreference(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, False)
+ preferences.addPreference(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY,
+ datetime.now().strftime(self.DATE_FORMAT))
+
+ # Register the menu item
+ self.addMenuItem(catalog.i18nc("@item:inmenu", "Manage backups"), self.showDriveWindow)
+
+ # Make auto-backup on boot if required.
+ CuraApplication.getInstance().engineCreatedSignal.connect(self._autoBackup)
+
+ def showDriveWindow(self) -> None:
+ if not self._drive_window:
+ plugin_dir_path = CuraApplication.getInstance().getPluginRegistry().getPluginPath("CuraDrive")
+ path = os.path.join(plugin_dir_path, "src", "qml", "main.qml")
+ self._drive_window = CuraApplication.getInstance().createQmlComponent(path, {"CuraDrive": self})
+ self.refreshBackups()
+ if self._drive_window:
+ self._drive_window.show()
+
+ def _autoBackup(self) -> None:
+ preferences = CuraApplication.getInstance().getPreferences()
+ if preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._isLastBackupTooLongAgo():
+ self.createBackup()
+
+ def _isLastBackupTooLongAgo(self) -> bool:
+ current_date = datetime.now()
+ last_backup_date = self._getLastBackupDate()
+ date_diff = current_date - last_backup_date
+ return date_diff.days > 1
+
+ def _getLastBackupDate(self) -> "datetime":
+ preferences = CuraApplication.getInstance().getPreferences()
+ last_backup_date = preferences.getValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY)
+ return datetime.strptime(last_backup_date, self.DATE_FORMAT)
+
+ def _storeBackupDate(self) -> None:
+ backup_date = datetime.now().strftime(self.DATE_FORMAT)
+ preferences = CuraApplication.getInstance().getPreferences()
+ preferences.setValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, backup_date)
+
+ def _onLoginStateChanged(self, logged_in: bool = False) -> None:
+ if logged_in:
+ self.refreshBackups()
+
+ def _onRestoringStateChanged(self, is_restoring: bool = False, error_message: str = None) -> None:
+ self._is_restoring_backup = is_restoring
+ self.restoringStateChanged.emit()
+ if error_message:
+ Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show()
+
+ def _onCreatingStateChanged(self, is_creating: bool = False, error_message: str = None) -> None:
+ self._is_creating_backup = is_creating
+ self.creatingStateChanged.emit()
+ if error_message:
+ Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show()
+ else:
+ self._storeBackupDate()
+ if not is_creating and not error_message:
+ # We've finished creating a new backup, to the list has to be updated.
+ self.refreshBackups()
+
+ @pyqtSlot(bool, name = "toggleAutoBackup")
+ def toggleAutoBackup(self, enabled: bool) -> None:
+ preferences = CuraApplication.getInstance().getPreferences()
+ preferences.setValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, enabled)
+
+ @pyqtProperty(bool, notify = preferencesChanged)
+ def autoBackupEnabled(self) -> bool:
+ preferences = CuraApplication.getInstance().getPreferences()
+ return bool(preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY))
+
+ @pyqtProperty("QVariantList", notify = backupsChanged)
+ def backups(self) -> List[Dict[str, Any]]:
+ return self._backups
+
+ @pyqtSlot(name = "refreshBackups")
+ def refreshBackups(self) -> None:
+ self._backups = self._drive_api_service.getBackups()
+ self.backupsChanged.emit()
+
+ @pyqtProperty(bool, notify = restoringStateChanged)
+ def isRestoringBackup(self) -> bool:
+ return self._is_restoring_backup
+
+ @pyqtProperty(bool, notify = creatingStateChanged)
+ def isCreatingBackup(self) -> bool:
+ return self._is_creating_backup
+
+ @pyqtSlot(str, name = "restoreBackup")
+ def restoreBackup(self, backup_id: str) -> None:
+ for backup in self._backups:
+ if backup.get("backup_id") == backup_id:
+ self._drive_api_service.restoreBackup(backup)
+ return
+ Logger.log("w", "Unable to find backup with the ID %s", backup_id)
+
+ @pyqtSlot(name = "createBackup")
+ def createBackup(self) -> None:
+ self._drive_api_service.createBackup()
+
+ @pyqtSlot(str, name = "deleteBackup")
+ def deleteBackup(self, backup_id: str) -> None:
+ self._drive_api_service.deleteBackup(backup_id)
+ self.refreshBackups()
diff --git a/plugins/CuraDrive/src/Settings.py b/plugins/CuraDrive/src/Settings.py
new file mode 100644
index 0000000000..abe64e0acd
--- /dev/null
+++ b/plugins/CuraDrive/src/Settings.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from cura import UltimakerCloudAuthentication
+
+
+class Settings:
+ # Keeps the plugin settings.
+ DRIVE_API_VERSION = 1
+ DRIVE_API_URL = "{}/cura-drive/v{}".format(UltimakerCloudAuthentication.CuraCloudAPIRoot, str(DRIVE_API_VERSION))
+
+ AUTO_BACKUP_ENABLED_PREFERENCE_KEY = "cura_drive/auto_backup_enabled"
+ AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY = "cura_drive/auto_backup_date"
diff --git a/plugins/CuraDrive/src/UploadBackupJob.py b/plugins/CuraDrive/src/UploadBackupJob.py
new file mode 100644
index 0000000000..2e76ed9b4b
--- /dev/null
+++ b/plugins/CuraDrive/src/UploadBackupJob.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+import requests
+
+from UM.Job import Job
+from UM.Logger import Logger
+from UM.Message import Message
+
+from UM.i18n import i18nCatalog
+catalog = i18nCatalog("cura")
+
+
+class UploadBackupJob(Job):
+ MESSAGE_TITLE = catalog.i18nc("@info:title", "Backups")
+
+ # This job is responsible for uploading the backup file to cloud storage.
+ # As it can take longer than some other tasks, we schedule this using a Cura Job.
+ def __init__(self, signed_upload_url: str, backup_zip: bytes) -> None:
+ super().__init__()
+ self._signed_upload_url = signed_upload_url
+ self._backup_zip = backup_zip
+ self._upload_success = False
+ self.backup_upload_error_message = ""
+
+ def run(self) -> None:
+ upload_message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1)
+ upload_message.show()
+
+ backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip)
+ upload_message.hide()
+
+ if backup_upload.status_code >= 300:
+ self.backup_upload_error_message = backup_upload.text
+ Logger.log("w", "Could not upload backup file: %s", backup_upload.text)
+ Message(catalog.i18nc("@info:backup_status", "There was an error while uploading your backup."), title = self.MESSAGE_TITLE).show()
+ else:
+ self._upload_success = True
+ Message(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."), title = self.MESSAGE_TITLE).show()
+
+ self.finished.emit(self)
diff --git a/plugins/CuraDrive/src/__init__.py b/plugins/CuraDrive/src/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/plugins/CuraDrive/src/qml/components/BackupList.qml b/plugins/CuraDrive/src/qml/components/BackupList.qml
new file mode 100644
index 0000000000..a4a460a885
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupList.qml
@@ -0,0 +1,39 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 2.2
+import QtQuick.Layouts 1.3
+
+import UM 1.1 as UM
+
+ScrollView
+{
+ property alias model: backupList.model
+ width: parent.width
+ clip: true
+ ListView
+ {
+ id: backupList
+ width: parent.width
+ delegate: Item
+ {
+ // Add a margin, otherwise the scrollbar is on top of the right most component
+ width: parent.width - UM.Theme.getSize("default_margin").width
+ height: childrenRect.height
+
+ BackupListItem
+ {
+ id: backupListItem
+ width: parent.width
+ }
+
+ Rectangle
+ {
+ id: divider
+ color: UM.Theme.getColor("lining")
+ height: UM.Theme.getSize("default_lining").height
+ }
+ }
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/components/BackupListFooter.qml b/plugins/CuraDrive/src/qml/components/BackupListFooter.qml
new file mode 100644
index 0000000000..56706b9990
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupListFooter.qml
@@ -0,0 +1,46 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.3
+
+import UM 1.3 as UM
+import Cura 1.0 as Cura
+
+import "../components"
+
+RowLayout
+{
+ id: backupListFooter
+ width: parent.width
+ property bool showInfoButton: false
+
+ Cura.PrimaryButton
+ {
+ id: infoButton
+ text: catalog.i18nc("@button", "Want more?")
+ iconSource: UM.Theme.getIcon("info")
+ onClicked: Qt.openUrlExternally("https://goo.gl/forms/QACEP8pP3RV60QYG2")
+ visible: backupListFooter.showInfoButton
+ }
+
+ Cura.PrimaryButton
+ {
+ id: createBackupButton
+ text: catalog.i18nc("@button", "Backup Now")
+ iconSource: UM.Theme.getIcon("plus")
+ enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup && !backupListFooter.showInfoButton
+ onClicked: CuraDrive.createBackup()
+ busy: CuraDrive.isCreatingBackup
+ }
+
+ Cura.CheckBoxWithTooltip
+ {
+ id: autoBackupEnabled
+ checked: CuraDrive.autoBackupEnabled
+ onClicked: CuraDrive.toggleAutoBackup(autoBackupEnabled.checked)
+ text: catalog.i18nc("@checkbox:description", "Auto Backup")
+ tooltip: catalog.i18nc("@checkbox:description", "Automatically create a backup each day that Cura is started.")
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/components/BackupListItem.qml b/plugins/CuraDrive/src/qml/components/BackupListItem.qml
new file mode 100644
index 0000000000..5cdb500b4e
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupListItem.qml
@@ -0,0 +1,113 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.3
+import QtQuick.Dialogs 1.1
+
+import UM 1.1 as UM
+import Cura 1.0 as Cura
+
+Item
+{
+ id: backupListItem
+ width: parent.width
+ height: showDetails ? dataRow.height + backupDetails.height : dataRow.height
+ property bool showDetails: false
+
+ // Backup details toggle animation.
+ Behavior on height
+ {
+ PropertyAnimation
+ {
+ duration: 70
+ }
+ }
+
+ RowLayout
+ {
+ id: dataRow
+ spacing: UM.Theme.getSize("wide_margin").width
+ width: parent.width
+ height: 50 * screenScaleFactor
+
+ UM.SimpleButton
+ {
+ width: UM.Theme.getSize("section_icon").width
+ height: UM.Theme.getSize("section_icon").height
+ color: UM.Theme.getColor("small_button_text")
+ hoverColor: UM.Theme.getColor("small_button_text_hover")
+ iconSource: UM.Theme.getIcon("info")
+ onClicked: backupListItem.showDetails = !backupListItem.showDetails
+ }
+
+ Label
+ {
+ text: new Date(modelData.generated_time).toLocaleString(UM.Preferences.getValue("general/language"))
+ color: UM.Theme.getColor("text")
+ elide: Text.ElideRight
+ Layout.minimumWidth: 100 * screenScaleFactor
+ Layout.maximumWidth: 500 * screenScaleFactor
+ Layout.fillWidth: true
+ font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
+ }
+
+ Label
+ {
+ text: modelData.metadata.description
+ color: UM.Theme.getColor("text")
+ elide: Text.ElideRight
+ Layout.minimumWidth: 100 * screenScaleFactor
+ Layout.maximumWidth: 500 * screenScaleFactor
+ Layout.fillWidth: true
+ font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
+ }
+
+ Cura.SecondaryButton
+ {
+ text: catalog.i18nc("@button", "Restore")
+ enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
+ onClicked: confirmRestoreDialog.visible = true
+ }
+
+ UM.SimpleButton
+ {
+ width: UM.Theme.getSize("message_close").width
+ height: UM.Theme.getSize("message_close").height
+ color: UM.Theme.getColor("small_button_text")
+ hoverColor: UM.Theme.getColor("small_button_text_hover")
+ iconSource: UM.Theme.getIcon("cross1")
+ onClicked: confirmDeleteDialog.visible = true
+ }
+ }
+
+ BackupListItemDetails
+ {
+ id: backupDetails
+ backupDetailsData: modelData
+ width: parent.width
+ visible: parent.showDetails
+ anchors.top: dataRow.bottom
+ }
+
+ MessageDialog
+ {
+ id: confirmDeleteDialog
+ title: catalog.i18nc("@dialog:title", "Delete Backup")
+ text: catalog.i18nc("@dialog:info", "Are you sure you want to delete this backup? This cannot be undone.")
+ standardButtons: StandardButton.Yes | StandardButton.No
+ onYes: CuraDrive.deleteBackup(modelData.backup_id)
+ }
+
+ MessageDialog
+ {
+ id: confirmRestoreDialog
+ title: catalog.i18nc("@dialog:title", "Restore Backup")
+ text: catalog.i18nc("@dialog:info", "You will need to restart Cura before your backup is restored. Do you want to close Cura now?")
+ standardButtons: StandardButton.Yes | StandardButton.No
+ onYes: CuraDrive.restoreBackup(modelData.backup_id)
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml b/plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml
new file mode 100644
index 0000000000..4da15c6f16
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml
@@ -0,0 +1,63 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.3
+
+import UM 1.1 as UM
+
+ColumnLayout
+{
+ id: backupDetails
+ width: parent.width
+ spacing: UM.Theme.getSize("default_margin").width
+ property var backupDetailsData
+
+ // Cura version
+ BackupListItemDetailsRow
+ {
+ iconSource: UM.Theme.getIcon("application")
+ label: catalog.i18nc("@backuplist:label", "Cura Version")
+ value: backupDetailsData.metadata.cura_release
+ }
+
+ // Machine count.
+ BackupListItemDetailsRow
+ {
+ iconSource: UM.Theme.getIcon("printer_single")
+ label: catalog.i18nc("@backuplist:label", "Machines")
+ value: backupDetailsData.metadata.machine_count
+ }
+
+ // Material count
+ BackupListItemDetailsRow
+ {
+ iconSource: UM.Theme.getIcon("category_material")
+ label: catalog.i18nc("@backuplist:label", "Materials")
+ value: backupDetailsData.metadata.material_count
+ }
+
+ // Profile count.
+ BackupListItemDetailsRow
+ {
+ iconSource: UM.Theme.getIcon("settings")
+ label: catalog.i18nc("@backuplist:label", "Profiles")
+ value: backupDetailsData.metadata.profile_count
+ }
+
+ // Plugin count.
+ BackupListItemDetailsRow
+ {
+ iconSource: UM.Theme.getIcon("plugin")
+ label: catalog.i18nc("@backuplist:label", "Plugins")
+ value: backupDetailsData.metadata.plugin_count
+ }
+
+ // Spacer.
+ Item
+ {
+ width: parent.width
+ height: UM.Theme.getSize("default_margin").height
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml b/plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml
new file mode 100644
index 0000000000..9e4612fcf8
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml
@@ -0,0 +1,52 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.3
+
+import UM 1.3 as UM
+
+RowLayout
+{
+ id: detailsRow
+ width: parent.width
+ height: 40 * screenScaleFactor
+
+ property alias iconSource: icon.source
+ property alias label: detailName.text
+ property alias value: detailValue.text
+
+ UM.RecolorImage
+ {
+ id: icon
+ width: 18 * screenScaleFactor
+ height: width
+ source: ""
+ color: UM.Theme.getColor("text")
+ }
+
+ Label
+ {
+ id: detailName
+ color: UM.Theme.getColor("text")
+ elide: Text.ElideRight
+ Layout.minimumWidth: 50 * screenScaleFactor
+ Layout.maximumWidth: 100 * screenScaleFactor
+ Layout.fillWidth: true
+ font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
+ }
+
+ Label
+ {
+ id: detailValue
+ color: UM.Theme.getColor("text")
+ elide: Text.ElideRight
+ Layout.minimumWidth: 50 * screenScaleFactor
+ Layout.maximumWidth: 100 * screenScaleFactor
+ Layout.fillWidth: true
+ font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/images/icon.png b/plugins/CuraDrive/src/qml/images/icon.png
new file mode 100644
index 0000000000..3f75491786
Binary files /dev/null and b/plugins/CuraDrive/src/qml/images/icon.png differ
diff --git a/plugins/CuraDrive/src/qml/images/loading.gif b/plugins/CuraDrive/src/qml/images/loading.gif
new file mode 100644
index 0000000000..791dcaa0c9
Binary files /dev/null and b/plugins/CuraDrive/src/qml/images/loading.gif differ
diff --git a/plugins/CuraDrive/src/qml/main.qml b/plugins/CuraDrive/src/qml/main.qml
new file mode 100644
index 0000000000..48bf3b6ea4
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/main.qml
@@ -0,0 +1,44 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 2.1
+import QtQuick.Window 2.2
+
+import UM 1.3 as UM
+import Cura 1.1 as Cura
+
+import "components"
+import "pages"
+
+Window
+{
+ id: curaDriveDialog
+ minimumWidth: Math.round(UM.Theme.getSize("modal_window_minimum").width)
+ minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height)
+ maximumWidth: Math.round(minimumWidth * 1.2)
+ maximumHeight: Math.round(minimumHeight * 1.2)
+ width: minimumWidth
+ height: minimumHeight
+ color: UM.Theme.getColor("main_background")
+ title: catalog.i18nc("@title:window", "Cura Backups")
+
+ // Globally available.
+ UM.I18nCatalog
+ {
+ id: catalog
+ name: "cura"
+ }
+
+ WelcomePage
+ {
+ id: welcomePage
+ visible: !Cura.API.account.isLoggedIn
+ }
+
+ BackupsPage
+ {
+ id: backupsPage
+ visible: Cura.API.account.isLoggedIn
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/pages/BackupsPage.qml b/plugins/CuraDrive/src/qml/pages/BackupsPage.qml
new file mode 100644
index 0000000000..0ba0cae09b
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/pages/BackupsPage.qml
@@ -0,0 +1,75 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 2.1
+import QtQuick.Layouts 1.3
+
+import UM 1.3 as UM
+import Cura 1.1 as Cura
+
+import "../components"
+
+Item
+{
+ id: backupsPage
+ anchors.fill: parent
+ anchors.margins: UM.Theme.getSize("wide_margin").width
+
+ ColumnLayout
+ {
+ spacing: UM.Theme.getSize("wide_margin").height
+ width: parent.width
+ anchors.fill: parent
+
+ 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
+ {
+ 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.count == 0
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ renderType: Text.NativeRendering
+ }
+
+ BackupList
+ {
+ id: backupList
+ model: CuraDrive.backups
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ 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.count > 4
+ renderType: Text.NativeRendering
+ }
+
+ BackupListFooter
+ {
+ id: backupListFooter
+ showInfoButton: backupList.count > 4
+ }
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/pages/WelcomePage.qml b/plugins/CuraDrive/src/qml/pages/WelcomePage.qml
new file mode 100644
index 0000000000..0b207bc170
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/pages/WelcomePage.qml
@@ -0,0 +1,56 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 2.1
+import QtQuick.Window 2.2
+
+import UM 1.3 as UM
+import Cura 1.1 as Cura
+
+import "../components"
+
+
+Column
+{
+ id: welcomePage
+ spacing: UM.Theme.getSize("wide_margin").height
+ width: parent.width
+ height: childrenRect.height
+ anchors.centerIn: parent
+
+ Image
+ {
+ id: profileImage
+ fillMode: Image.PreserveAspectFit
+ source: "../images/icon.png"
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: Math.round(parent.width / 4)
+ }
+
+ Label
+ {
+ id: welcomeTextLabel
+ text: catalog.i18nc("@description", "Backup and synchronize your Cura settings.")
+ width: Math.round(parent.width / 2)
+ font: UM.Theme.getFont("default")
+ color: UM.Theme.getColor("text")
+ verticalAlignment: Text.AlignVCenter
+ horizontalAlignment: Text.AlignHCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ wrapMode: Label.WordWrap
+ renderType: Text.NativeRendering
+ }
+
+ Cura.PrimaryButton
+ {
+ id: loginButton
+ width: UM.Theme.getSize("account_button").width
+ height: UM.Theme.getSize("account_button").height
+ anchors.horizontalCenter: parent.horizontalCenter
+ text: catalog.i18nc("@button", "Sign in")
+ onClicked: Cura.API.account.login()
+ fixedWidthMode: true
+ }
+}
+
diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py
index f12a5b1222..ef0898bb04 100755
--- a/plugins/CuraEngineBackend/CuraEngineBackend.py
+++ b/plugins/CuraEngineBackend/CuraEngineBackend.py
@@ -833,7 +833,10 @@ class CuraEngineBackend(QObject, Backend):
self._onChanged()
def _onProcessLayersFinished(self, job: ProcessSlicedLayersJob) -> None:
- del self._stored_optimized_layer_data[job.getBuildPlate()]
+ if job.getBuildPlate() in self._stored_optimized_layer_data:
+ del self._stored_optimized_layer_data[job.getBuildPlate()]
+ else:
+ Logger.log("w", "The optimized layer data was already deleted for buildplate %s", job.getBuildPlate())
self._process_layers_job = None
Logger.log("d", "See if there is more to slice(2)...")
self._invokeSlice()
diff --git a/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py b/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py
index 0a3e3a0ff0..59552775b6 100644
--- a/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py
+++ b/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py
@@ -57,7 +57,7 @@ class FirmwareUpdaterMachineAction(MachineAction):
outputDeviceCanUpdateFirmwareChanged = pyqtSignal()
@pyqtProperty(QObject, notify = outputDeviceCanUpdateFirmwareChanged)
def firmwareUpdater(self) -> Optional["FirmwareUpdater"]:
- if self._active_output_device and self._active_output_device.activePrinter.getController().can_update_firmware:
+ if self._active_output_device and self._active_output_device.activePrinter and self._active_output_device.activePrinter.getController().can_update_firmware:
self._active_firmware_updater = self._active_output_device.getFirmwareUpdater()
return self._active_firmware_updater
diff --git a/plugins/MonitorStage/MonitorMain.qml b/plugins/MonitorStage/MonitorMain.qml
index 34cf4ad801..5fda32db9e 100644
--- a/plugins/MonitorStage/MonitorMain.qml
+++ b/plugins/MonitorStage/MonitorMain.qml
@@ -1,4 +1,5 @@
-// Copyright (c) 2017 Ultimaker B.V.
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
@@ -7,31 +8,27 @@ import UM 1.3 as UM
import Cura 1.0 as Cura
-Item
+// We show a nice overlay on the 3D viewer when the current output device has no monitor view
+Rectangle
{
- // We show a nice overlay on the 3D viewer when the current output device has no monitor view
- Rectangle
+ id: viewportOverlay
+
+ color: UM.Theme.getColor("viewport_overlay")
+ anchors.fill: parent
+
+ // This mouse area is to prevent mouse clicks to be passed onto the scene.
+ MouseArea
{
- id: viewportOverlay
-
- color: UM.Theme.getColor("viewport_overlay")
anchors.fill: parent
-
- // This mouse area is to prevent mouse clicks to be passed onto the scene.
- MouseArea
- {
- anchors.fill: parent
- acceptedButtons: Qt.AllButtons
- onWheel: wheel.accepted = true
- }
-
- // Disable dropping files into Cura when the monitor page is active
- DropArea
- {
- anchors.fill: parent
- }
+ acceptedButtons: Qt.AllButtons
+ onWheel: wheel.accepted = true
}
+ // Disable dropping files into Cura when the monitor page is active
+ DropArea
+ {
+ anchors.fill: parent
+ }
Loader
{
id: monitorViewComponent
@@ -45,4 +42,4 @@ Item
sourceComponent: Cura.MachineManager.printerOutputDevices.length > 0 ? Cura.MachineManager.printerOutputDevices[0].monitorItem : null
}
-}
+}
\ No newline at end of file
diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.qml b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml
index d5fe618b2d..cd8303d1d3 100644
--- a/plugins/PostProcessingPlugin/PostProcessingPlugin.qml
+++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml
@@ -488,7 +488,7 @@ UM.Dialog
{
objectName: "postProcessingSaveAreaButton"
visible: activeScriptsList.count > 0
- height: UM.Theme.getSize("save_button_save_to_button").height
+ height: UM.Theme.getSize("action_button").height
width: height
tooltip: catalog.i18nc("@info:tooltip", "Change active post-processing scripts")
onClicked: dialog.show()
diff --git a/plugins/Toolbox/resources/qml/Toolbox.qml b/plugins/Toolbox/resources/qml/Toolbox.qml
index 9ede2a6bda..d15d98eed7 100644
--- a/plugins/Toolbox/resources/qml/Toolbox.qml
+++ b/plugins/Toolbox/resources/qml/Toolbox.qml
@@ -38,7 +38,7 @@ Window
{
id: mainView
width: parent.width
- z: -1
+ z: parent.z - 1
anchors
{
top: header.bottom
diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml
index 7160dafa2d..87fc5d6955 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml
@@ -91,5 +91,10 @@ Column
target: toolbox
onInstallChanged: installed = toolbox.isInstalled(model.id)
onMetadataChanged: canUpdate = toolbox.canUpdate(model.id)
+ onFilterChanged:
+ {
+ installed = toolbox.isInstalled(model.id)
+ canUpdate = toolbox.canUpdate(model.id)
+ }
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml
index 333d4dd50a..f50c3f3ac6 100644
--- a/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml
@@ -30,6 +30,7 @@ Item
CheckBox
{
id: disableButton
+ anchors.verticalCenter: pluginInfo.verticalCenter
checked: isEnabled
visible: model.type == "plugin"
width: visible ? UM.Theme.getSize("checkbox").width : 0
diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py
index 05669e55d8..192471a357 100644
--- a/plugins/Toolbox/src/Toolbox.py
+++ b/plugins/Toolbox/src/Toolbox.py
@@ -16,7 +16,8 @@ from UM.Extension import Extension
from UM.i18n import i18nCatalog
from UM.Version import Version
-import cura
+from cura import ApplicationMetadata
+from cura import UltimakerCloudAuthentication
from cura.CuraApplication import CuraApplication
from .AuthorsModel import AuthorsModel
@@ -30,17 +31,14 @@ i18n_catalog = i18nCatalog("cura")
## The Toolbox class is responsible of communicating with the server through the API
class Toolbox(QObject, Extension):
- DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str
- DEFAULT_CLOUD_API_VERSION = 1 # type: int
-
def __init__(self, application: CuraApplication) -> None:
super().__init__()
self._application = application # type: CuraApplication
- self._sdk_version = None # type: Optional[Union[str, int]]
- self._cloud_api_version = None # type: Optional[int]
- self._cloud_api_root = None # type: Optional[str]
+ self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
+ self._cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion # type: int
+ self._cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot # type: str
self._api_url = None # type: Optional[str]
# Network:
@@ -182,9 +180,6 @@ class Toolbox(QObject, Extension):
def _onAppInitialized(self) -> None:
self._plugin_registry = self._application.getPluginRegistry()
self._package_manager = self._application.getPackageManager()
- self._sdk_version = self._getSDKVersion()
- self._cloud_api_version = self._getCloudAPIVersion()
- self._cloud_api_root = self._getCloudAPIRoot()
self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
cloud_api_root = self._cloud_api_root,
cloud_api_version = self._cloud_api_version,
@@ -195,36 +190,6 @@ class Toolbox(QObject, Extension):
"packages": QUrl("{base_url}/packages".format(base_url = self._api_url))
}
- # Get the API root for the packages API depending on Cura version settings.
- def _getCloudAPIRoot(self) -> str:
- if not hasattr(cura, "CuraVersion"):
- return self.DEFAULT_CLOUD_API_ROOT
- if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): # type: ignore
- return self.DEFAULT_CLOUD_API_ROOT
- if not cura.CuraVersion.CuraCloudAPIRoot: # type: ignore
- return self.DEFAULT_CLOUD_API_ROOT
- return cura.CuraVersion.CuraCloudAPIRoot # type: ignore
-
- # Get the cloud API version from CuraVersion
- def _getCloudAPIVersion(self) -> int:
- if not hasattr(cura, "CuraVersion"):
- return self.DEFAULT_CLOUD_API_VERSION
- if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): # type: ignore
- return self.DEFAULT_CLOUD_API_VERSION
- if not cura.CuraVersion.CuraCloudAPIVersion: # type: ignore
- return self.DEFAULT_CLOUD_API_VERSION
- return cura.CuraVersion.CuraCloudAPIVersion # type: ignore
-
- # Get the packages version depending on Cura version settings.
- def _getSDKVersion(self) -> Union[int, str]:
- if not hasattr(cura, "CuraVersion"):
- return self._application.getAPIVersion().getMajor()
- if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
- return self._application.getAPIVersion().getMajor()
- if not cura.CuraVersion.CuraSDKVersion: # type: ignore
- return self._application.getAPIVersion().getMajor()
- return cura.CuraVersion.CuraSDKVersion # type: ignore
-
@pyqtSlot()
def browsePackages(self) -> None:
# Create the network manager:
diff --git a/plugins/UM3NetworkPrinting/resources/qml/ExpandableCard.qml b/plugins/UM3NetworkPrinting/resources/qml/ExpandableCard.qml
index f86135ae62..d4c123652d 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/ExpandableCard.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/ExpandableCard.qml
@@ -15,6 +15,7 @@ Item
id: base
property bool expanded: false
+ property bool enabled: true
property var borderWidth: 1
property color borderColor: "#CCCCCC"
property color headerBackgroundColor: "white"
@@ -34,7 +35,7 @@ Item
color: borderColor
width: borderWidth
}
- color: headerMouseArea.containsMouse ? headerHoverColor : headerBackgroundColor
+ color: base.enabled && headerMouseArea.containsMouse ? headerHoverColor : headerBackgroundColor
height: childrenRect.height
width: parent.width
Behavior on color
@@ -50,8 +51,12 @@ Item
{
id: headerMouseArea
anchors.fill: header
- onClicked: base.expanded = !base.expanded
- hoverEnabled: true
+ onClicked:
+ {
+ if (!base.enabled) return
+ base.expanded = !base.expanded
+ }
+ hoverEnabled: base.enabled
}
Rectangle
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorBuildplateConfiguration.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorBuildplateConfiguration.qml
index 44bd47f904..192a5a7f76 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorBuildplateConfiguration.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorBuildplateConfiguration.qml
@@ -18,7 +18,7 @@ import UM 1.3 as UM
Item
{
// The buildplate name
- property alias buildplate: buildplateLabel.text
+ property var buildplate: null
// Height is one 18px label/icon
height: 18 * screenScaleFactor // TODO: Theme!
@@ -34,7 +34,16 @@ Item
Item
{
height: parent.height
- width: 32 * screenScaleFactor // TODO: Theme! (Should be same as extruder icon width)
+ width: 32 * screenScaleFactor // Ensure the icon is centered under the extruder icon (same width)
+
+ Rectangle
+ {
+ anchors.centerIn: parent
+ height: parent.height
+ width: height
+ color: buildplateIcon.visible > 0 ? "transparent" : "#eeeeee" // TODO: Theme!
+ radius: Math.floor(height / 2)
+ }
UM.RecolorImage
{
@@ -44,6 +53,7 @@ Item
height: parent.height
source: "../svg/icons/buildplate.svg"
width: height
+ visible: buildplate
}
}
@@ -53,7 +63,8 @@ Item
color: "#191919" // TODO: Theme!
elide: Text.ElideRight
font: UM.Theme.getFont("default") // 12pt, regular
- text: ""
+ text: buildplate ? buildplate : ""
+ visible: text !== ""
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme!
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml
index eccd93c578..de24ee5a8c 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml
@@ -14,7 +14,12 @@ Item
property var tileWidth: 834 * screenScaleFactor // TODO: Theme!
property var tileHeight: 216 * screenScaleFactor // TODO: Theme!
property var tileSpacing: 60 * screenScaleFactor // TODO: Theme!
- property var maxOffset: (OutputDevice.printers.length - 1) * (tileWidth + tileSpacing)
+
+ // Array/model of printers to populate the carousel with
+ property var printers: []
+
+ // Maximum distance the carousel can be shifted
+ property var maxOffset: (printers.length - 1) * (tileWidth + tileSpacing)
height: centerSection.height
width: maximumWidth
@@ -129,7 +134,7 @@ Item
Repeater
{
- model: OutputDevice.printers
+ model: printers
MonitorPrinterCard
{
printer: modelData
@@ -151,7 +156,7 @@ Item
width: 36 * screenScaleFactor // TODO: Theme!
height: 72 * screenScaleFactor // TODO: Theme!
z: 10
- visible: currentIndex < OutputDevice.printers.length - 1
+ visible: currentIndex < printers.length - 1
onClicked: navigateTo(currentIndex + 1)
hoverEnabled: true
background: Rectangle
@@ -225,9 +230,10 @@ Item
topMargin: 36 * screenScaleFactor // TODO: Theme!
}
spacing: 8 * screenScaleFactor // TODO: Theme!
+ visible: printers.length > 1
Repeater
{
- model: OutputDevice.printers
+ model: printers
Button
{
background: Rectangle
@@ -243,7 +249,7 @@ Item
}
function navigateTo( i ) {
- if (i >= 0 && i < OutputDevice.printers.length)
+ if (i >= 0 && i < printers.length)
{
tiles.x = -1 * i * (tileWidth + tileSpacing)
currentIndex = i
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml
index 6a32310dd5..1718994d83 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml
@@ -54,7 +54,7 @@ UM.Dialog
wrapMode: Text.WordWrap
text:
{
- if (!printer.activePrintJob)
+ if (!printer || !printer.activePrintJob)
{
return ""
}
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorExtruderConfiguration.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorExtruderConfiguration.qml
index 1e53191d8c..17c0fa8651 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorExtruderConfiguration.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorExtruderConfiguration.qml
@@ -39,38 +39,62 @@ Item
color: "#eeeeee" // TODO: Theme!
position: 0
}
- Label
+
+ Rectangle
{
- id: materialLabel
+ id: materialLabelWrapper
anchors
{
left: extruderIcon.right
leftMargin: 12 * screenScaleFactor // TODO: Theme!
}
- color: "#191919" // TODO: Theme!
- elide: Text.ElideRight
- font: UM.Theme.getFont("default") // 12pt, regular
- text: ""
-
- // FIXED-LINE-HEIGHT:
+ color: materialLabel.visible > 0 ? "transparent" : "#eeeeee" // TODO: Theme!
height: 18 * screenScaleFactor // TODO: Theme!
- verticalAlignment: Text.AlignVCenter
+ width: Math.max(materialLabel.contentWidth, 60 * screenScaleFactor) // TODO: Theme!
+ radius: 2 * screenScaleFactor // TODO: Theme!
+
+ Label
+ {
+ id: materialLabel
+
+ color: "#191919" // TODO: Theme!
+ elide: Text.ElideRight
+ font: UM.Theme.getFont("default") // 12pt, regular
+ text: ""
+ visible: text !== ""
+
+ // FIXED-LINE-HEIGHT:
+ height: parent.height
+ verticalAlignment: Text.AlignVCenter
+ }
}
- Label
+
+ Rectangle
{
- id: printCoreLabel
+ id: printCoreLabelWrapper
anchors
{
- left: materialLabel.left
+ left: materialLabelWrapper.left
bottom: parent.bottom
}
- color: "#191919" // TODO: Theme!
- elide: Text.ElideRight
- font: UM.Theme.getFont("default_bold") // 12pt, bold
- text: ""
-
- // FIXED-LINE-HEIGHT:
+ color: printCoreLabel.visible > 0 ? "transparent" : "#eeeeee" // TODO: Theme!
height: 18 * screenScaleFactor // TODO: Theme!
- verticalAlignment: Text.AlignVCenter
+ width: Math.max(printCoreLabel.contentWidth, 36 * screenScaleFactor) // TODO: Theme!
+ radius: 2 * screenScaleFactor // TODO: Theme!
+
+ Label
+ {
+ id: printCoreLabel
+
+ color: "#191919" // TODO: Theme!
+ elide: Text.ElideRight
+ font: UM.Theme.getFont("default_bold") // 12pt, bold
+ text: ""
+ visible: text !== ""
+
+ // FIXED-LINE-HEIGHT:
+ height: parent.height
+ verticalAlignment: Text.AlignVCenter
+ }
}
}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorIconExtruder.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorIconExtruder.qml
index 971c6b2251..93dbebc8c6 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorIconExtruder.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorIconExtruder.qml
@@ -56,5 +56,6 @@ Item
x: Math.round(size * 0.25) * screenScaleFactor
y: Math.round(size * 0.15625) * screenScaleFactor
// TODO: Once 'size' is themed, screenScaleFactor won't be needed
+ visible: position >= 0
}
}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml
index f431ef1c52..f2b9c3cff7 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobCard.qml
@@ -26,6 +26,7 @@ Item
ExpandableCard
{
+ enabled: printJob != null
borderColor: printJob.configurationChanges.length !== 0 ? "#f5a623" : "#CCCCCC" // TODO: Theme!
headerItem: Row
{
@@ -41,32 +42,56 @@ Item
anchors.verticalCenter: parent.verticalCenter
}
- Label
+ Item
{
- text: printJob && printJob.name ? printJob.name : ""
- color: "#374355"
- elide: Text.ElideRight
- font: UM.Theme.getFont("medium") // 14pt, regular
anchors.verticalCenter: parent.verticalCenter
- width: 216 * screenScaleFactor // TODO: Theme! (Should match column size)
-
- // FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme!
- verticalAlignment: Text.AlignVCenter
+ width: 216 * screenScaleFactor // TODO: Theme! (Should match column size)
+ Rectangle
+ {
+ color: "#eeeeee"
+ width: Math.round(parent.width / 2)
+ height: parent.height
+ visible: !printJob
+ }
+ Label
+ {
+ text: printJob && printJob.name ? printJob.name : ""
+ color: "#374355"
+ elide: Text.ElideRight
+ font: UM.Theme.getFont("medium") // 14pt, regular
+ visible: printJob
+
+ // FIXED-LINE-HEIGHT:
+ height: parent.height
+ verticalAlignment: Text.AlignVCenter
+ }
}
-
- Label
- {
- text: printJob ? OutputDevice.formatDuration(printJob.timeTotal) : ""
- color: "#374355"
- elide: Text.ElideRight
- font: UM.Theme.getFont("medium") // 14pt, regular
- anchors.verticalCenter: parent.verticalCenter
- width: 216 * screenScaleFactor // TODO: Theme! (Should match column size)
- // FIXED-LINE-HEIGHT:
+ Item
+ {
+ anchors.verticalCenter: parent.verticalCenter
height: 18 * screenScaleFactor // TODO: Theme!
- verticalAlignment: Text.AlignVCenter
+ width: 216 * screenScaleFactor // TODO: Theme! (Should match column size)
+ Rectangle
+ {
+ color: "#eeeeee"
+ width: Math.round(parent.width / 3)
+ height: parent.height
+ visible: !printJob
+ }
+ Label
+ {
+ text: printJob ? OutputDevice.formatDuration(printJob.timeTotal) : ""
+ color: "#374355"
+ elide: Text.ElideRight
+ font: UM.Theme.getFont("medium") // 14pt, regular
+ visible: printJob
+
+ // FIXED-LINE-HEIGHT:
+ height: 18 * screenScaleFactor // TODO: Theme!
+ verticalAlignment: Text.AlignVCenter
+ }
}
Item
@@ -75,6 +100,14 @@ Item
height: 18 * screenScaleFactor // TODO: This should be childrenRect.height but QML throws warnings
width: childrenRect.width
+ Rectangle
+ {
+ color: "#eeeeee"
+ width: 72 * screenScaleFactor // TODO: Theme!
+ height: parent.height
+ visible: !printJob
+ }
+
Label
{
id: printerAssignmentLabel
@@ -100,7 +133,7 @@ Item
width: 120 * screenScaleFactor // TODO: Theme!
// FIXED-LINE-HEIGHT:
- height: 18 * screenScaleFactor // TODO: Theme!
+ height: parent.height
verticalAlignment: Text.AlignVCenter
}
@@ -115,6 +148,7 @@ Item
}
height: childrenRect.height
spacing: 6 // TODO: Theme!
+ visible: printJob
Repeater
{
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobPreview.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobPreview.qml
index 2f17db0c65..d0bad63258 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobPreview.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobPreview.qml
@@ -16,23 +16,28 @@ Item
width: size
height: size
- // Actual content
- Image
+ Rectangle
{
- id: previewImage
anchors.fill: parent
- opacity:
+ color: printJob ? "transparent" : "#eeeeee" // TODO: Theme!
+ radius: 8 // TODO: Theme!
+ Image
{
- if (printJob && (printJob.state == "error" || printJob.configurationChanges.length > 0 || !printJob.isActive))
+ id: previewImage
+ anchors.fill: parent
+ opacity:
{
- return 0.5
+ if (printJob && (printJob.state == "error" || printJob.configurationChanges.length > 0 || !printJob.isActive))
+ {
+ return 0.5
+ }
+ return 1.0
}
- return 1.0
+ source: printJob ? printJob.previewImageUrl : ""
}
- source: printJob ? printJob.previewImageUrl : ""
- visible: printJob
}
+
UM.RecolorImage
{
id: ultiBotImage
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobProgressBar.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobProgressBar.qml
index cfb7aba84d..d5d4705a36 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobProgressBar.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrintJobProgressBar.qml
@@ -34,16 +34,16 @@ Item
{
background: Rectangle
{
- color: printJob && printJob.isActive ? "#e4e4f2" : "#f3f3f9" // TODO: Theme!
+ color: "#f5f5f5" // TODO: Theme!
implicitHeight: visible ? 8 * screenScaleFactor : 0 // TODO: Theme!
implicitWidth: 180 * screenScaleFactor // TODO: Theme!
- radius: 4 * screenScaleFactor // TODO: Theme!
+ radius: 2 * screenScaleFactor // TODO: Theme!
}
progress: Rectangle
{
id: progressItem;
- color: printJob && printJob.isActive ? "#0a0850" : "#9392b2" // TODO: Theme!
- radius: 4 * screenScaleFactor // TODO: Theme!
+ color: printJob && printJob.isActive ? "#3282ff" : "#CCCCCC" // TODO: Theme!
+ radius: 2 * screenScaleFactor // TODO: Theme!
}
}
}
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml
index b8c4353811..facfaaaaaf 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterCard.qml
@@ -33,16 +33,24 @@ Item
width: 834 * screenScaleFactor // TODO: Theme!
height: childrenRect.height
- // Printer portion
Rectangle
{
- id: printerInfo
+ id: background
+ anchors.fill: parent
+ color: "#FFFFFF" // TODO: Theme!
border
{
color: "#CCCCCC" // TODO: Theme!
width: borderSize // TODO: Remove once themed
}
- color: "white" // TODO: Theme!
+ radius: 2 * screenScaleFactor // TODO: Theme!
+ }
+
+ // Printer portion
+ Item
+ {
+ id: printerInfo
+
width: parent.width
height: 144 * screenScaleFactor // TODO: Theme!
@@ -56,15 +64,22 @@ Item
}
spacing: 18 * screenScaleFactor // TODO: Theme!
- Image
+ Rectangle
{
id: printerImage
width: 108 * screenScaleFactor // TODO: Theme!
height: 108 * screenScaleFactor // TODO: Theme!
- fillMode: Image.PreserveAspectFit
- source: "../png/" + printer.type + ".png"
- mipmap: true
+ color: printer ? "transparent" : "#eeeeee" // TODO: Theme!
+ radius: 8 // TODO: Theme!
+ Image
+ {
+ anchors.fill: parent
+ fillMode: Image.PreserveAspectFit
+ source: printer ? "../png/" + printer.type + ".png" : ""
+ mipmap: true
+ }
}
+
Item
{
@@ -75,20 +90,38 @@ Item
width: 180 * screenScaleFactor // TODO: Theme!
height: printerNameLabel.height + printerFamilyPill.height + 6 * screenScaleFactor // TODO: Theme!
- Label
+ Rectangle
{
id: printerNameLabel
- text: printer && printer.name ? printer.name : ""
- color: "#414054" // TODO: Theme!
- elide: Text.ElideRight
- font: UM.Theme.getFont("large_bold") // 16pt, bold
- width: parent.width
-
- // FIXED-LINE-HEIGHT:
+ // color: "#414054" // TODO: Theme!
+ color: printer ? "transparent" : "#eeeeee" // TODO: Theme!
height: 18 * screenScaleFactor // TODO: Theme!
- verticalAlignment: Text.AlignVCenter
+ width: parent.width
+ radius: 2 * screenScaleFactor // TODO: Theme!
+
+ Label
+ {
+ text: printer && printer.name ? printer.name : ""
+ color: "#414054" // TODO: Theme!
+ elide: Text.ElideRight
+ font: UM.Theme.getFont("large") // 16pt, bold
+ width: parent.width
+ visible: printer
+
+ // FIXED-LINE-HEIGHT:
+ height: parent.height
+ verticalAlignment: Text.AlignVCenter
+ }
}
+ Rectangle
+ {
+ color: "#eeeeee" // TODO: Theme!
+ height: 18 * screenScaleFactor // TODO: Theme!
+ radius: 2 * screenScaleFactor // TODO: Theme!
+ visible: !printer
+ width: 48 * screenScaleFactor // TODO: Theme!
+ }
MonitorPrinterPill
{
id: printerFamilyPill
@@ -98,7 +131,7 @@ Item
topMargin: 6 * screenScaleFactor // TODO: Theme!
left: printerNameLabel.left
}
- text: printer.type
+ text: printer ? printer.type : ""
}
}
@@ -106,16 +139,30 @@ Item
{
id: printerConfiguration
anchors.verticalCenter: parent.verticalCenter
- buildplate: "Glass"
+ buildplate: printer ? "Glass" : null // 'Glass' as a default
configurations:
- [
- base.printer.printerConfiguration.extruderConfigurations[0],
- base.printer.printerConfiguration.extruderConfigurations[1]
- ]
- height: 72 * screenScaleFactor // TODO: Theme!
+ {
+ var configs = []
+ if (printer)
+ {
+ configs.push(printer.printerConfiguration.extruderConfigurations[0])
+ configs.push(printer.printerConfiguration.extruderConfigurations[1])
+ }
+ else
+ {
+ configs.push(null, null)
+ }
+ return configs
+ }
+ height: 72 * screenScaleFactor // TODO: Theme!te theRect's x property
}
+
+ // TODO: Make this work.
+ PropertyAnimation { target: printerConfiguration; property: "visible"; to: 0; loops: Animation.Infinite; duration: 500 }
}
+
+
PrintJobContextMenu
{
id: contextButton
@@ -126,10 +173,11 @@ Item
top: parent.top
topMargin: 12 * screenScaleFactor // TODO: Theme!
}
- printJob: printer.activePrintJob
+ printJob: printer ? printer.activePrintJob : null
width: 36 * screenScaleFactor // TODO: Theme!
height: 36 * screenScaleFactor // TODO: Theme!
enabled: base.enabled
+ visible: printer
}
CameraButton
{
@@ -143,10 +191,24 @@ Item
}
iconSource: "../svg/icons/camera.svg"
enabled: base.enabled
+ visible: printer
}
}
+ // Divider
+ Rectangle
+ {
+ anchors
+ {
+ top: printJobInfo.top
+ left: printJobInfo.left
+ right: printJobInfo.right
+ }
+ height: borderSize // Remove once themed
+ color: background.border.color
+ }
+
// Print job portion
Rectangle
{
@@ -158,10 +220,10 @@ Item
}
border
{
- color: printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 ? "#f5a623" : "#CCCCCC" // TODO: Theme!
+ color: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 ? "#f5a623" : "transparent" // TODO: Theme!
width: borderSize // TODO: Remove once themed
}
- color: "white" // TODO: Theme!
+ color: "transparent" // TODO: Theme!
height: 84 * screenScaleFactor + borderSize // TODO: Remove once themed
width: parent.width
@@ -184,9 +246,12 @@ Item
{
verticalCenter: parent.verticalCenter
}
- color: "#414054" // TODO: Theme!
+ color: printer ? "#414054" : "#aaaaaa" // TODO: Theme!
font: UM.Theme.getFont("large_bold") // 16pt, bold
text: {
+ if (!printer) {
+ return catalog.i18nc("@label:status", "Loading...")
+ }
if (printer && printer.state == "disabled")
{
return catalog.i18nc("@label:status", "Unavailable")
@@ -215,10 +280,10 @@ Item
MonitorPrintJobPreview
{
anchors.centerIn: parent
- printJob: base.printer.activePrintJob
+ printJob: printer ? printer.activePrintJob : null
size: parent.height
}
- visible: printer.activePrintJob
+ visible: printer && printer.activePrintJob && !printerStatus.visible
}
Item
@@ -229,15 +294,15 @@ Item
}
width: 180 * screenScaleFactor // TODO: Theme!
height: printerNameLabel.height + printerFamilyPill.height + 6 * screenScaleFactor // TODO: Theme!
- visible: printer.activePrintJob
+ visible: printer && printer.activePrintJob && !printerStatus.visible
Label
{
id: printerJobNameLabel
- color: printer.activePrintJob && printer.activePrintJob.isActive ? "#414054" : "#babac1" // TODO: Theme!
+ color: printer && printer.activePrintJob && printer.activePrintJob.isActive ? "#414054" : "#babac1" // TODO: Theme!
elide: Text.ElideRight
- font: UM.Theme.getFont("large_bold") // 16pt, bold
- text: base.printer.activePrintJob ? base.printer.activePrintJob.name : "Untitled" // TODO: I18N
+ font: UM.Theme.getFont("large") // 16pt, bold
+ text: printer && printer.activePrintJob ? printer.activePrintJob.name : "Untitled" // TODO: I18N
width: parent.width
// FIXED-LINE-HEIGHT:
@@ -254,10 +319,10 @@ Item
topMargin: 6 * screenScaleFactor // TODO: Theme!
left: printerJobNameLabel.left
}
- color: printer.activePrintJob && printer.activePrintJob.isActive ? "#53657d" : "#babac1" // TODO: Theme!
+ color: printer && printer.activePrintJob && printer.activePrintJob.isActive ? "#53657d" : "#babac1" // TODO: Theme!
elide: Text.ElideRight
font: UM.Theme.getFont("default") // 12pt, regular
- text: printer.activePrintJob ? printer.activePrintJob.owner : "Anonymous" // TODO: I18N
+ text: printer && printer.activePrintJob ? printer.activePrintJob.owner : "Anonymous" // TODO: I18N
width: parent.width
// FIXED-LINE-HEIGHT:
@@ -272,8 +337,8 @@ Item
{
verticalCenter: parent.verticalCenter
}
- printJob: printer.activePrintJob
- visible: printer.activePrintJob && printer.activePrintJob.configurationChanges.length === 0
+ printJob: printer && printer.activePrintJob
+ visible: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length === 0 && !printerStatus.visible
}
Label
@@ -284,7 +349,7 @@ Item
}
font: UM.Theme.getFont("default")
text: "Requires configuration changes"
- visible: printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0
+ visible: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 && !printerStatus.visible
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme!
@@ -326,7 +391,7 @@ Item
}
implicitHeight: 32 * screenScaleFactor // TODO: Theme!
implicitWidth: 96 * screenScaleFactor // TODO: Theme!
- visible: printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0
+ visible: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 && !printerStatus.visible
onClicked: base.enabled ? overrideConfirmationDialog.open() : {}
}
}
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterConfiguration.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterConfiguration.qml
index 6aa11528de..debc8b7959 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterConfiguration.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterConfiguration.qml
@@ -19,7 +19,7 @@ Item
property alias buildplate: buildplateConfig.buildplate
// Array of extracted extruder configurations
- property var configurations: null
+ property var configurations: [null,null]
// Default size, but should be stretched to fill parent
height: 72 * parent.height
@@ -37,10 +37,10 @@ Item
MonitorExtruderConfiguration
{
- color: modelData.activeMaterial ? modelData.activeMaterial.color : "#eeeeee" // TODO: Theme!
- material: modelData.activeMaterial ? modelData.activeMaterial.name : ""
- position: modelData.position
- printCore: modelData.hotendID
+ color: modelData && modelData.activeMaterial ? modelData.activeMaterial.color : "#eeeeee" // TODO: Theme!
+ material: modelData && modelData.activeMaterial ? modelData.activeMaterial.name : ""
+ position: modelData && typeof(modelData.position) === "number" ? modelData.position : -1 // Use negative one to create empty extruder number
+ printCore: modelData ? modelData.hotendID : ""
// Keep things responsive!
width: Math.floor((base.width - (configurations.length - 1) * extruderConfigurationRow.spacing) / configurations.length)
@@ -53,6 +53,6 @@ Item
{
id: buildplateConfig
anchors.bottom: parent.bottom
- buildplate: "Glass" // 'Glass' as a default
+ buildplate: null
}
}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml
index 80a089cc2a..2408089e1e 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorPrinterPill.qml
@@ -27,12 +27,12 @@ Item
}
implicitHeight: 18 * screenScaleFactor // TODO: Theme!
- implicitWidth: printerNameLabel.contentWidth + 12 // TODO: Theme!
+ implicitWidth: Math.max(printerNameLabel.contentWidth + 12 * screenScaleFactor, 36 * screenScaleFactor) // TODO: Theme!
Rectangle {
id: background
anchors.fill: parent
- color: "#e4e4f2" // TODO: Theme!
+ color: printerNameLabel.visible ? "#e4e4f2" : "#eeeeee"// TODO: Theme!
radius: 2 * screenScaleFactor // TODO: Theme!
}
@@ -41,6 +41,7 @@ Item
anchors.centerIn: parent
color: "#535369" // TODO: Theme!
text: tagText
- font.pointSize: 10
+ font.pointSize: 10 // TODO: Theme!
+ visible: text !== ""
}
}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml
index f2a0e785b8..f2dc09de95 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorQueue.qml
@@ -42,8 +42,8 @@ Item
{
id: externalLinkIcon
anchors.verticalCenter: manageQueueLabel.verticalCenter
- color: UM.Theme.getColor("primary")
- source: "../svg/icons/external_link.svg"
+ color: UM.Theme.getColor("text_link")
+ source: UM.Theme.getIcon("external_link")
width: 16 * screenScaleFactor // TODO: Theme! (Y U NO USE 18 LIKE ALL OTHER ICONS?!)
height: 16 * screenScaleFactor // TODO: Theme! (Y U NO USE 18 LIKE ALL OTHER ICONS?!)
}
@@ -56,10 +56,11 @@ Item
leftMargin: 6 * screenScaleFactor // TODO: Theme!
verticalCenter: externalLinkIcon.verticalCenter
}
- color: UM.Theme.getColor("primary")
+ color: UM.Theme.getColor("text_link")
font: UM.Theme.getFont("default") // 12pt, regular
- linkColor: UM.Theme.getColor("primary")
+ linkColor: UM.Theme.getColor("text_link")
text: catalog.i18nc("@label link to connect manager", "Manage queue in Cura Connect")
+ renderType: Text.NativeRendering
}
}
@@ -144,7 +145,6 @@ Item
topMargin: 12 * screenScaleFactor // TODO: Theme!
}
style: UM.Theme.styles.scrollview
- visible: OutputDevice.receivedPrintJobs
width: parent.width
ListView
@@ -160,7 +160,7 @@ Item
}
printJob: modelData
}
- model: OutputDevice.queuedPrintJobs
+ model: OutputDevice.receivedPrintJobs ? OutputDevice.queuedPrintJobs : [null,null]
spacing: 6 // TODO: Theme!
}
}
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml
index 8b1a11cb4d..8723e6f46e 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorStage.qml
@@ -64,8 +64,10 @@ Component
}
width: parent.width
height: 264 * screenScaleFactor // TODO: Theme!
- MonitorCarousel {
+ MonitorCarousel
+ {
id: carousel
+ printers: OutputDevice.receivedPrintJobs ? OutputDevice.printers : [null]
}
}
diff --git a/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml b/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml
index 320201e165..c99ed1688e 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml
@@ -90,67 +90,4 @@ Item {
source: "DiscoverUM3Action.qml";
}
}
-
- Column {
- anchors.fill: parent;
- objectName: "networkPrinterConnectionInfo";
- spacing: UM.Theme.getSize("default_margin").width;
- visible: isUM3;
-
- Button {
- onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication();
- text: catalog.i18nc("@action:button", "Request Access");
- tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer");
- visible: printerConnected && !printerAcceptsCommands && !authenticationRequested;
- }
-
- Row {
- anchors {
- left: parent.left;
- right: parent.right;
- }
- height: childrenRect.height;
- spacing: UM.Theme.getSize("default_margin").width;
- visible: printerConnected;
-
- Column {
- Repeater {
- model: CuraApplication.getExtrudersModel()
-
- Label {
- text: model.name;
- }
- }
- }
-
- Column {
- Repeater {
- id: nozzleColumn;
- model: hotendIds
-
- Label {
- text: nozzleColumn.model[index];
- }
- }
- }
-
- Column {
- Repeater {
- id: materialColumn;
- model: materialNames
-
- Label {
- text: materialColumn.model[index];
- }
- }
- }
- }
-
- Button {
- onClicked: manager.loadConfigurationFromPrinter();
- text: catalog.i18nc("@action:button", "Activate Configuration");
- tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura");
- visible: false; // printerConnected && !isClusterPrinter()
- }
- }
}
diff --git a/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py
index 6ce99e4891..68af2bd575 100644
--- a/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py
+++ b/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py
@@ -193,4 +193,3 @@ class DiscoverUM3Action(MachineAction):
# Create extra components
CuraApplication.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton"))
- CuraApplication.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo"))
diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py
index f536fad49a..9d0d3dbbad 100644
--- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py
+++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py
@@ -2,7 +2,6 @@
# Cura is released under the terms of the LGPLv3 or higher.
import json
import os
-import urllib.parse
from typing import Dict, TYPE_CHECKING, Set
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
@@ -10,9 +9,7 @@ from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from UM.Application import Application
from UM.Job import Job
from UM.Logger import Logger
-from UM.MimeTypeDatabase import MimeTypeDatabase
-from UM.Resources import Resources
-from cura.CuraApplication import CuraApplication
+
# Absolute imports don't work in plugins
from .Models import ClusterMaterial, LocalMaterial
@@ -37,7 +34,6 @@ class SendMaterialJob(Job):
#
# \param reply The reply from the printer, a json file.
def _onGetRemoteMaterials(self, reply: QNetworkReply) -> None:
-
# Got an error from the HTTP request. If we did not receive a 200 something happened.
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
Logger.log("e", "Error fetching materials from printer: %s", reply.errorString())
@@ -52,7 +48,6 @@ class SendMaterialJob(Job):
#
# \param remote_materials_by_guid The remote materials by GUID.
def _sendMissingMaterials(self, remote_materials_by_guid: Dict[str, ClusterMaterial]) -> None:
-
# Collect local materials
local_materials_by_guid = self._getLocalMaterials()
if len(local_materials_by_guid) == 0:
@@ -91,22 +86,22 @@ class SendMaterialJob(Job):
#
# \param materials_to_send A set with id's of materials that must be sent.
def _sendMaterials(self, materials_to_send: Set[str]) -> None:
- file_paths = Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.MaterialInstanceContainer)
+ container_registry = Application.getInstance().getContainerRegistry()
+ material_manager = Application.getInstance().getMaterialManager()
+ material_group_dict = material_manager.getAllMaterialGroups()
- # Find all local material files and send them if needed.
- for file_path in file_paths:
- try:
- mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
- except MimeTypeDatabase.MimeTypeNotFoundError:
- continue
-
- file_name = os.path.basename(file_path)
- material_id = urllib.parse.unquote_plus(mime_type.stripExtension(file_name))
- if material_id not in materials_to_send:
+ for root_material_id in material_group_dict:
+ if root_material_id not in materials_to_send:
# If the material does not have to be sent we skip it.
continue
- self._sendMaterialFile(file_path, file_name, material_id)
+ file_path = container_registry.getContainerFilePathById(root_material_id)
+ if not file_path:
+ Logger.log("w", "Cannot get file path for material container [%s]", root_material_id)
+ continue
+
+ file_name = os.path.basename(file_path)
+ self._sendMaterialFile(file_path, file_name, root_material_id)
## Send a single material file to the printer.
#
@@ -116,7 +111,6 @@ class SendMaterialJob(Job):
# \param file_name The name of the material file.
# \param material_id The ID of the material in the file.
def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None:
-
parts = []
# Add the material file.
@@ -171,27 +165,31 @@ class SendMaterialJob(Job):
# \return a dictionary of LocalMaterial objects by GUID
def _getLocalMaterials(self) -> Dict[str, LocalMaterial]:
result = {} # type: Dict[str, LocalMaterial]
- container_registry = Application.getInstance().getContainerRegistry()
- material_containers = container_registry.findContainersMetadata(type = "material")
+ material_manager = Application.getInstance().getMaterialManager()
+
+ material_group_dict = material_manager.getAllMaterialGroups()
# Find the latest version of all material containers in the registry.
- for material in material_containers:
+ for root_material_id, material_group in material_group_dict.items():
+ material_metadata = material_group.root_material_node.getMetadata()
+
try:
# material version must be an int
- material["version"] = int(material["version"])
+ material_metadata["version"] = int(material_metadata["version"])
# Create a new local material
- local_material = LocalMaterial(**material)
+ local_material = LocalMaterial(**material_metadata)
+ local_material.id = root_material_id
if local_material.GUID not in result or \
local_material.version > result.get(local_material.GUID).version:
result[local_material.GUID] = local_material
except KeyError:
- Logger.logException("w", "Local material {} has missing values.".format(material["id"]))
+ Logger.logException("w", "Local material {} has missing values.".format(material_metadata["id"]))
except ValueError:
- Logger.logException("w", "Local material {} has invalid values.".format(material["id"]))
+ Logger.logException("w", "Local material {} has invalid values.".format(material_metadata["id"]))
except TypeError:
- Logger.logException("w", "Local material {} has invalid values.".format(material["id"]))
+ Logger.logException("w", "Local material {} has invalid values.".format(material_metadata["id"]))
return result
diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py
index 80212fcf00..57bc96b0e0 100644
--- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py
+++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py
@@ -114,6 +114,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
if key == um_network_key:
if not self._discovered_devices[key].isConnected():
Logger.log("d", "Attempting to connect with [%s]" % key)
+ active_machine.setMetaDataEntry("connection_type", self._discovered_devices[key].getConnectionType().value)
self._discovered_devices[key].connect()
self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
else:
diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py
index b669eb192a..6eac892af6 100644
--- a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py
+++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py
@@ -1,26 +1,29 @@
# Copyright (c) 2018 Ultimaker B.V.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+import copy
import io
import json
from unittest import TestCase, mock
-from unittest.mock import patch, call
+from unittest.mock import patch, call, MagicMock
from PyQt5.QtCore import QByteArray
-from UM.MimeTypeDatabase import MimeType
from UM.Application import Application
+
+from cura.Machines.MaterialGroup import MaterialGroup
+from cura.Machines.MaterialNode import MaterialNode
+
from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob
+_FILES_MAP = {"generic_pla_white": "/materials/generic_pla_white.xml.fdm_material",
+ "generic_pla_black": "/materials/generic_pla_black.xml.fdm_material",
+ }
+
@patch("builtins.open", lambda _, __: io.StringIO(""))
-@patch("UM.MimeTypeDatabase.MimeTypeDatabase.getMimeTypeForFile",
- lambda _: MimeType(name = "application/x-ultimaker-material-profile", comment = "Ultimaker Material Profile",
- suffixes = ["xml.fdm_material"]))
-@patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: ["/materials/generic_pla_white.xml.fdm_material"])
-@patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice")
-@patch("PyQt5.QtNetwork.QNetworkReply")
class TestSendMaterialJob(TestCase):
+ # version 1
_LOCAL_MATERIAL_WHITE = {"type": "material", "status": "unknown", "id": "generic_pla_white",
"base_file": "generic_pla_white", "setting_version": "5", "name": "White PLA",
"brand": "Generic", "material": "PLA", "color_name": "White",
@@ -29,6 +32,37 @@ class TestSendMaterialJob(TestCase):
"properties": {"density": "1.00", "diameter": "2.85", "weight": "750"},
"definition": "fdmprinter", "compatible": True}
+ # version 2
+ _LOCAL_MATERIAL_WHITE_NEWER = {"type": "material", "status": "unknown", "id": "generic_pla_white",
+ "base_file": "generic_pla_white", "setting_version": "5", "name": "White PLA",
+ "brand": "Generic", "material": "PLA", "color_name": "White",
+ "GUID": "badb0ee7-87c8-4f3f-9398-938587b67dce", "version": "2",
+ "color_code": "#ffffff",
+ "description": "Test PLA White", "adhesion_info": "Use glue.",
+ "approximate_diameter": "3",
+ "properties": {"density": "1.00", "diameter": "2.85", "weight": "750"},
+ "definition": "fdmprinter", "compatible": True}
+
+ # invalid version: "one"
+ _LOCAL_MATERIAL_WHITE_INVALID_VERSION = {"type": "material", "status": "unknown", "id": "generic_pla_white",
+ "base_file": "generic_pla_white", "setting_version": "5", "name": "White PLA",
+ "brand": "Generic", "material": "PLA", "color_name": "White",
+ "GUID": "badb0ee7-87c8-4f3f-9398-938587b67dce", "version": "one",
+ "color_code": "#ffffff",
+ "description": "Test PLA White", "adhesion_info": "Use glue.",
+ "approximate_diameter": "3",
+ "properties": {"density": "1.00", "diameter": "2.85", "weight": "750"},
+ "definition": "fdmprinter", "compatible": True}
+
+ _LOCAL_MATERIAL_WHITE_ALL_RESULT = {"generic_pla_white": MaterialGroup("generic_pla_white",
+ MaterialNode(_LOCAL_MATERIAL_WHITE))}
+
+ _LOCAL_MATERIAL_WHITE_NEWER_ALL_RESULT = {"generic_pla_white": MaterialGroup("generic_pla_white",
+ MaterialNode(_LOCAL_MATERIAL_WHITE_NEWER))}
+
+ _LOCAL_MATERIAL_WHITE_INVALID_VERSION_ALL_RESULT = {"generic_pla_white": MaterialGroup("generic_pla_white",
+ MaterialNode(_LOCAL_MATERIAL_WHITE_INVALID_VERSION))}
+
_LOCAL_MATERIAL_BLACK = {"type": "material", "status": "unknown", "id": "generic_pla_black",
"base_file": "generic_pla_black", "setting_version": "5", "name": "Yellow CPE",
"brand": "Ultimaker", "material": "CPE", "color_name": "Black",
@@ -37,6 +71,9 @@ class TestSendMaterialJob(TestCase):
"properties": {"density": "1.01", "diameter": "2.85", "weight": "750"},
"definition": "fdmprinter", "compatible": True}
+ _LOCAL_MATERIAL_BLACK_ALL_RESULT = {"generic_pla_black": MaterialGroup("generic_pla_black",
+ MaterialNode(_LOCAL_MATERIAL_BLACK))}
+
_REMOTE_MATERIAL_WHITE = {
"guid": "badb0ee7-87c8-4f3f-9398-938587b67dce",
"material": "PLA",
@@ -55,14 +92,17 @@ class TestSendMaterialJob(TestCase):
"density": 1.00
}
- def test_run(self, device_mock, reply_mock):
+ def test_run(self):
+ device_mock = MagicMock()
job = SendMaterialJob(device_mock)
job.run()
# We expect the materials endpoint to be called when the job runs.
device_mock.get.assert_called_with("materials/", on_finished = job._onGetRemoteMaterials)
- def test__onGetRemoteMaterials_withFailedRequest(self, reply_mock, device_mock):
+ def test__onGetRemoteMaterials_withFailedRequest(self):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
reply_mock.attribute.return_value = 404
job = SendMaterialJob(device_mock)
job._onGetRemoteMaterials(reply_mock)
@@ -70,7 +110,9 @@ class TestSendMaterialJob(TestCase):
# We expect the device not to be called for any follow up.
self.assertEqual(0, device_mock.createFormPart.call_count)
- def test__onGetRemoteMaterials_withWrongEncoding(self, reply_mock, device_mock):
+ def test__onGetRemoteMaterials_withWrongEncoding(self):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("cp500"))
job = SendMaterialJob(device_mock)
@@ -79,7 +121,9 @@ class TestSendMaterialJob(TestCase):
# Given that the parsing fails we do no expect the device to be called for any follow up.
self.assertEqual(0, device_mock.createFormPart.call_count)
- def test__onGetRemoteMaterials_withBadJsonAnswer(self, reply_mock, device_mock):
+ def test__onGetRemoteMaterials_withBadJsonAnswer(self):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(b"Six sick hicks nick six slick bricks with picks and sticks.")
job = SendMaterialJob(device_mock)
@@ -88,7 +132,9 @@ class TestSendMaterialJob(TestCase):
# Given that the parsing fails we do no expect the device to be called for any follow up.
self.assertEqual(0, device_mock.createFormPart.call_count)
- def test__onGetRemoteMaterials_withMissingGuidInRemoteMaterial(self, reply_mock, device_mock):
+ def test__onGetRemoteMaterials_withMissingGuidInRemoteMaterial(self):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
reply_mock.attribute.return_value = 200
remote_material_without_guid = self._REMOTE_MATERIAL_WHITE.copy()
del remote_material_without_guid["guid"]
@@ -99,18 +145,20 @@ class TestSendMaterialJob(TestCase):
# Given that parsing fails we do not expect the device to be called for any follow up.
self.assertEqual(0, device_mock.createFormPart.call_count)
+ @patch("cura.Machines.MaterialManager.MaterialManager")
@patch("cura.Settings.CuraContainerRegistry")
@patch("UM.Application")
def test__onGetRemoteMaterials_withInvalidVersionInLocalMaterial(self, application_mock, container_registry_mock,
- reply_mock, device_mock):
+ material_manager_mock):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
+ application_mock.getContainerRegistry.return_value = container_registry_mock
+ application_mock.getMaterialManager.return_value = material_manager_mock
+
reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
- localMaterialWhiteWithInvalidVersion = self._LOCAL_MATERIAL_WHITE.copy()
- localMaterialWhiteWithInvalidVersion["version"] = "one"
- container_registry_mock.findContainersMetadata.return_value = [localMaterialWhiteWithInvalidVersion]
-
- application_mock.getContainerRegistry.return_value = container_registry_mock
+ material_manager_mock.getAllMaterialGroups.return_value = self._LOCAL_MATERIAL_WHITE_INVALID_VERSION_ALL_RESULT.copy()
with mock.patch.object(Application, "getInstance", new = lambda: application_mock):
job = SendMaterialJob(device_mock)
@@ -118,15 +166,16 @@ class TestSendMaterialJob(TestCase):
self.assertEqual(0, device_mock.createFormPart.call_count)
- @patch("cura.Settings.CuraContainerRegistry")
- @patch("UM.Application")
- def test__onGetRemoteMaterials_withNoUpdate(self, application_mock, container_registry_mock, reply_mock,
- device_mock):
- application_mock.getContainerRegistry.return_value = container_registry_mock
+ @patch("UM.Application.Application.getInstance")
+ def test__onGetRemoteMaterials_withNoUpdate(self, application_mock):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
+ container_registry_mock = application_mock.getContainerRegistry.return_value
+ material_manager_mock = application_mock.getMaterialManager.return_value
device_mock.createFormPart.return_value = "_xXx_"
- container_registry_mock.findContainersMetadata.return_value = [self._LOCAL_MATERIAL_WHITE]
+ material_manager_mock.getAllMaterialGroups.return_value = self._LOCAL_MATERIAL_WHITE_ALL_RESULT.copy()
reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
@@ -138,24 +187,25 @@ class TestSendMaterialJob(TestCase):
self.assertEqual(0, device_mock.createFormPart.call_count)
self.assertEqual(0, device_mock.postFormWithParts.call_count)
- @patch("cura.Settings.CuraContainerRegistry")
- @patch("UM.Application")
- def test__onGetRemoteMaterials_withUpdatedMaterial(self, application_mock, container_registry_mock, reply_mock,
- device_mock):
- application_mock.getContainerRegistry.return_value = container_registry_mock
+ @patch("UM.Application.Application.getInstance")
+ def test__onGetRemoteMaterials_withUpdatedMaterial(self, get_instance_mock):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
+ application_mock = get_instance_mock.return_value
+ container_registry_mock = application_mock.getContainerRegistry.return_value
+ material_manager_mock = application_mock.getMaterialManager.return_value
+
+ container_registry_mock.getContainerFilePathById = lambda x: _FILES_MAP.get(x)
device_mock.createFormPart.return_value = "_xXx_"
- localMaterialWhiteWithHigherVersion = self._LOCAL_MATERIAL_WHITE.copy()
- localMaterialWhiteWithHigherVersion["version"] = "2"
- container_registry_mock.findContainersMetadata.return_value = [localMaterialWhiteWithHigherVersion]
+ material_manager_mock.getAllMaterialGroups.return_value = self._LOCAL_MATERIAL_WHITE_NEWER_ALL_RESULT.copy()
reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
- with mock.patch.object(Application, "getInstance", new = lambda: application_mock):
- job = SendMaterialJob(device_mock)
- job._onGetRemoteMaterials(reply_mock)
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
self.assertEqual(1, device_mock.createFormPart.call_count)
self.assertEqual(1, device_mock.postFormWithParts.call_count)
@@ -164,16 +214,21 @@ class TestSendMaterialJob(TestCase):
call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)],
device_mock.method_calls)
- @patch("cura.Settings.CuraContainerRegistry")
- @patch("UM.Application")
- def test__onGetRemoteMaterials_withNewMaterial(self, application_mock, container_registry_mock, reply_mock,
- device_mock):
- application_mock.getContainerRegistry.return_value = container_registry_mock
+ @patch("UM.Application.Application.getInstance")
+ def test__onGetRemoteMaterials_withNewMaterial(self, application_mock):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
+ container_registry_mock = application_mock.getContainerRegistry.return_value
+ material_manager_mock = application_mock.getMaterialManager.return_value
+
+ container_registry_mock.getContainerFilePathById = lambda x: _FILES_MAP.get(x)
device_mock.createFormPart.return_value = "_xXx_"
- container_registry_mock.findContainersMetadata.return_value = [self._LOCAL_MATERIAL_WHITE,
- self._LOCAL_MATERIAL_BLACK]
+ all_results = self._LOCAL_MATERIAL_WHITE_ALL_RESULT.copy()
+ for key, value in self._LOCAL_MATERIAL_BLACK_ALL_RESULT.items():
+ all_results[key] = value
+ material_manager_mock.getAllMaterialGroups.return_value = all_results
reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_BLACK]).encode("ascii"))
diff --git a/plugins/VersionUpgrade/VersionUpgrade35to40/VersionUpgrade35to40.py b/plugins/VersionUpgrade/VersionUpgrade35to40/VersionUpgrade35to40.py
new file mode 100644
index 0000000000..52cd7cf3cb
--- /dev/null
+++ b/plugins/VersionUpgrade/VersionUpgrade35to40/VersionUpgrade35to40.py
@@ -0,0 +1,68 @@
+import configparser
+from typing import Tuple, List, Set
+import io
+from UM.VersionUpgrade import VersionUpgrade
+from cura.PrinterOutputDevice import ConnectionType
+deleted_settings = {"bridge_wall_max_overhang"} # type: Set[str]
+
+
+class VersionUpgrade35to40(VersionUpgrade):
+ # Upgrades stacks to have the new version number.
+ def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
+ parser = configparser.ConfigParser(interpolation=None)
+ parser.read_string(serialized)
+
+ # Update version number.
+ parser["general"]["version"] = "4"
+ parser["metadata"]["setting_version"] = "6"
+
+ if parser["metadata"].get("um_network_key") is not None or parser["metadata"].get("octoprint_api_key") is not None:
+ # Set the connection type if um_network_key or the octoprint key is set.
+ parser["metadata"]["connection_type"] = str(ConnectionType.NetworkConnection.value)
+
+ result = io.StringIO()
+ parser.write(result)
+ return [filename], [result.getvalue()]
+ pass
+
+ def getCfgVersion(self, serialised: str) -> int:
+ parser = configparser.ConfigParser(interpolation = None)
+ parser.read_string(serialised)
+ format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
+ setting_version = int(parser.get("metadata", "setting_version", fallback = "0"))
+ return format_version * 1000000 + setting_version
+
+ ## Upgrades Preferences to have the new version number.
+ def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
+ parser = configparser.ConfigParser(interpolation=None)
+ parser.read_string(serialized)
+
+ if "metadata" not in parser:
+ parser["metadata"] = {}
+ parser["general"]["version"] = "6"
+ parser["metadata"]["setting_version"] = "6"
+
+ result = io.StringIO()
+ parser.write(result)
+ return [filename], [result.getvalue()]
+
+ ## Upgrades instance containers to have the new version
+ # number.
+ def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
+ parser = configparser.ConfigParser(interpolation=None)
+ parser.read_string(serialized)
+
+ # Update version number.
+ parser["general"]["version"] = "4"
+ parser["metadata"]["setting_version"] = "6"
+
+ #self._resetConcentric3DInfillPattern(parser)
+ if "values" in parser:
+ for deleted_setting in deleted_settings:
+ if deleted_setting not in parser["values"]:
+ continue
+ del parser["values"][deleted_setting]
+
+ result = io.StringIO()
+ parser.write(result)
+ return [filename], [result.getvalue()]
\ No newline at end of file
diff --git a/plugins/VersionUpgrade/VersionUpgrade35to40/__init__.py b/plugins/VersionUpgrade/VersionUpgrade35to40/__init__.py
new file mode 100644
index 0000000000..2ad1dddbf2
--- /dev/null
+++ b/plugins/VersionUpgrade/VersionUpgrade35to40/__init__.py
@@ -0,0 +1,56 @@
+from typing import Dict, Any
+
+from . import VersionUpgrade35to40
+
+upgrade = VersionUpgrade35to40.VersionUpgrade35to40()
+
+
+def getMetaData() -> Dict[str, Any]:
+ return {
+ "version_upgrade": {
+ # From To Upgrade function
+ ("preferences", 6000005): ("preferences", 6000006, upgrade.upgradePreferences),
+
+ ("definition_changes", 4000005): ("definition_changes", 4000006, upgrade.upgradeInstanceContainer),
+ ("quality_changes", 4000005): ("quality_changes", 4000006, upgrade.upgradeInstanceContainer),
+ ("quality", 4000005): ("quality", 4000006, upgrade.upgradeInstanceContainer),
+ ("user", 4000005): ("user", 4000006, upgrade.upgradeInstanceContainer),
+
+ ("machine_stack", 4000005): ("machine_stack", 4000006, upgrade.upgradeStack),
+ ("extruder_train", 4000005): ("extruder_train", 4000006, upgrade.upgradeStack),
+ },
+ "sources": {
+ "preferences": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"."}
+ },
+ "machine_stack": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"./machine_instances"}
+ },
+ "extruder_train": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"./extruders"}
+ },
+ "definition_changes": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"./definition_changes"}
+ },
+ "quality_changes": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"./quality_changes"}
+ },
+ "quality": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"./quality"}
+ },
+ "user": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"./user"}
+ }
+ }
+ }
+
+
+def register(app) -> Dict[str, Any]:
+ return {"version_upgrade": upgrade}
\ No newline at end of file
diff --git a/plugins/VersionUpgrade/VersionUpgrade35to40/plugin.json b/plugins/VersionUpgrade/VersionUpgrade35to40/plugin.json
new file mode 100644
index 0000000000..578594fb6d
--- /dev/null
+++ b/plugins/VersionUpgrade/VersionUpgrade35to40/plugin.json
@@ -0,0 +1,8 @@
+ {
+ "name": "Version Upgrade 3.5 to 4.0",
+ "author": "Ultimaker B.V.",
+ "version": "1.0.0",
+ "description": "Upgrades configurations from Cura 3.5 to Cura 4.0.",
+ "api": "6.0",
+ "i18n-catalog": "cura"
+}
diff --git a/plugins/XmlMaterialProfile/__init__.py b/plugins/XmlMaterialProfile/__init__.py
index e8bde78424..c50df69516 100644
--- a/plugins/XmlMaterialProfile/__init__.py
+++ b/plugins/XmlMaterialProfile/__init__.py
@@ -16,7 +16,7 @@ def getMetaData():
"mimetype": "application/x-ultimaker-material-profile"
},
"version_upgrade": {
- ("materials", 1000000): ("materials", 1000004, upgrader.upgradeMaterial),
+ ("materials", 1000000): ("materials", 1000006, upgrader.upgradeMaterial),
},
"sources": {
"materials": {
diff --git a/resources/bundled_packages/cura.json b/resources/bundled_packages/cura.json
index 99b8cd35a0..5ad18c9883 100644
--- a/resources/bundled_packages/cura.json
+++ b/resources/bundled_packages/cura.json
@@ -50,6 +50,23 @@
}
}
},
+ "CuraDrive": {
+ "package_info": {
+ "package_id": "CuraDrive",
+ "package_type": "plugin",
+ "display_name": "Cura Backups",
+ "description": "Backup and restore your configuration.",
+ "package_version": "1.2.0",
+ "sdk_version": 6,
+ "website": "https://ultimaker.com",
+ "author": {
+ "author_id": "UltimakerPackages",
+ "display_name": "Ultimaker B.V.",
+ "email": "plugins@ultimaker.com",
+ "website": "https://ultimaker.com"
+ }
+ }
+ },
"CuraEngineBackend": {
"package_info": {
"package_id": "CuraEngineBackend",
@@ -1585,4 +1602,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/resources/qml/ActionButton.qml b/resources/qml/ActionButton.qml
index 6cab04e5ec..fabdcebc64 100644
--- a/resources/qml/ActionButton.qml
+++ b/resources/qml/ActionButton.qml
@@ -4,6 +4,7 @@
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtGraphicalEffects 1.0 // For the dropshadow
+
import UM 1.1 as UM
import Cura 1.0 as Cura
@@ -30,6 +31,7 @@ Button
property color outlineDisabledColor: outlineColor
property alias shadowColor: shadow.color
property alias shadowEnabled: shadow.visible
+ property alias busy: busyIndicator.visible
property alias toolTipContentAlignment: tooltip.contentAlignment
@@ -55,7 +57,7 @@ Button
width: visible ? height : 0
sourceSize.width: width
sourceSize.height: height
- color: button.hovered ? button.textHoverColor : button.textColor
+ color: button.enabled ? (button.hovered ? button.textHoverColor : button.textColor) : button.textDisabledColor
visible: source != "" && !button.isIconOnRightSide
anchors.verticalCenter: parent.verticalCenter
}
@@ -117,4 +119,16 @@ Button
id: tooltip
visible: button.hovered
}
+
+ BusyIndicator
+ {
+ id: busyIndicator
+
+ anchors.centerIn: parent
+
+ width: height
+ height: parent.height
+
+ visible: false
+ }
}
\ No newline at end of file
diff --git a/resources/qml/ActionPanel/OutputProcessWidget.qml b/resources/qml/ActionPanel/OutputProcessWidget.qml
index 15214f212c..63974d7f34 100644
--- a/resources/qml/ActionPanel/OutputProcessWidget.qml
+++ b/resources/qml/ActionPanel/OutputProcessWidget.qml
@@ -31,6 +31,13 @@ Column
id: information
width: parent.width
height: childrenRect.height
+
+ PrintInformationWidget
+ {
+ id: printInformationPanel
+ visible: !preSlicedData
+ anchors.right: parent.right
+ }
Column
{
@@ -50,15 +57,7 @@ Column
text: preSlicedData ? catalog.i18nc("@label", "No time estimation available") : PrintInformation.currentPrintTime.getDisplayString(UM.DurationFormat.Long)
source: UM.Theme.getIcon("clock")
- font: UM.Theme.getFont("large_bold")
-
- PrintInformationWidget
- {
- id: printInformationPanel
- visible: !preSlicedData
- anchors.left: parent.left
- anchors.leftMargin: parent.contentWidth + UM.Theme.getSize("default_margin").width
- }
+ font: UM.Theme.getFont("medium_bold")
}
Cura.IconWithText
@@ -91,43 +90,8 @@ Column
return totalWeights + "g · " + totalLengths.toFixed(2) + "m"
}
source: UM.Theme.getIcon("spool")
-
- Item
- {
- id: additionalComponents
- width: childrenRect.width
- anchors.right: parent.right
- height: parent.height
- Row
- {
- id: additionalComponentsRow
- anchors.right: parent.right
- anchors.bottom: parent.bottom
- spacing: UM.Theme.getSize("default_margin").width
- }
- }
- Component.onCompleted: addAdditionalComponents("saveButton")
-
- Connections
- {
- target: CuraApplication
- onAdditionalComponentsChanged: addAdditionalComponents("saveButton")
- }
-
- function addAdditionalComponents (areaId)
- {
- if(areaId == "saveButton")
- {
- for (var component in CuraApplication.additionalComponents["saveButton"])
- {
- CuraApplication.additionalComponents["saveButton"][component].parent = additionalComponentsRow
- }
- }
- }
}
}
-
-
}
Item
diff --git a/resources/qml/ActionPanel/SliceProcessWidget.qml b/resources/qml/ActionPanel/SliceProcessWidget.qml
index 1695be8748..08966ce82c 100644
--- a/resources/qml/ActionPanel/SliceProcessWidget.qml
+++ b/resources/qml/ActionPanel/SliceProcessWidget.qml
@@ -110,8 +110,7 @@ Column
height: parent.height
- anchors.right: additionalComponents.left
- anchors.rightMargin: additionalComponents.width != 0 ? UM.Theme.getSize("default_margin").width : 0
+ anchors.right: parent.right
anchors.left: parent.left
text: catalog.i18nc("@button", "Slice")
@@ -128,45 +127,12 @@ Column
height: parent.height
anchors.left: parent.left
- anchors.right: additionalComponents.left
- anchors.rightMargin: additionalComponents.width != 0 ? UM.Theme.getSize("default_margin").width : 0
+ anchors.right: parent.right
text: catalog.i18nc("@button", "Cancel")
enabled: sliceButton.enabled
visible: !sliceButton.visible
onClicked: sliceOrStopSlicing()
}
-
- Item
- {
- id: additionalComponents
- width: childrenRect.width
- anchors.right: parent.right
- height: parent.height
- Row
- {
- id: additionalComponentsRow
- anchors.verticalCenter: parent.verticalCenter
- spacing: UM.Theme.getSize("default_margin").width
- }
- }
- Component.onCompleted: prepareButtons.addAdditionalComponents("saveButton")
-
- Connections
- {
- target: CuraApplication
- onAdditionalComponentsChanged: prepareButtons.addAdditionalComponents("saveButton")
- }
-
- function addAdditionalComponents (areaId)
- {
- if(areaId == "saveButton")
- {
- for (var component in CuraApplication.additionalComponents["saveButton"])
- {
- CuraApplication.additionalComponents["saveButton"][component].parent = additionalComponentsRow
- }
- }
- }
}
@@ -194,7 +160,7 @@ Column
shortcut: "Ctrl+P"
onTriggered:
{
- if (prepareButton.enabled)
+ if (sliceButton.enabled)
{
sliceOrStopSlicing()
}
diff --git a/resources/qml/CheckBoxWithTooltip.qml b/resources/qml/CheckBoxWithTooltip.qml
new file mode 100644
index 0000000000..403efb4d7b
--- /dev/null
+++ b/resources/qml/CheckBoxWithTooltip.qml
@@ -0,0 +1,63 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 2.1
+
+import UM 1.3 as UM
+
+CheckBox
+{
+ id: checkbox
+ hoverEnabled: true
+
+ property alias tooltip: tooltip.text
+
+ indicator: Rectangle
+ {
+ implicitWidth: UM.Theme.getSize("checkbox").width
+ implicitHeight: UM.Theme.getSize("checkbox").height
+ x: 0
+ anchors.verticalCenter: parent.verticalCenter
+ color: UM.Theme.getColor("main_background")
+ radius: UM.Theme.getSize("checkbox_radius").width
+ border.width: UM.Theme.getSize("default_lining").width
+ border.color: checkbox.hovered ? UM.Theme.getColor("checkbox_border_hover") : UM.Theme.getColor("checkbox_border")
+
+ UM.RecolorImage
+ {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: Math.round(parent.width / 2.5)
+ height: Math.round(parent.height / 2.5)
+ sourceSize.height: width
+ color: UM.Theme.getColor("checkbox_mark")
+ source: UM.Theme.getIcon("check")
+ opacity: checkbox.checked
+ Behavior on opacity { NumberAnimation { duration: 100; } }
+ }
+ }
+
+ contentItem: Label
+ {
+ anchors
+ {
+ left: checkbox.indicator.right
+ leftMargin: UM.Theme.getSize("narrow_margin").width
+ }
+ text: checkbox.text
+ color: UM.Theme.getColor("checkbox_text")
+ font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
+ elide: Text.ElideRight
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ ToolTip
+ {
+ id: tooltip
+ text: ""
+ delay: 500
+ visible: text != "" && checkbox.hovered
+ }
+}
diff --git a/resources/qml/Cura.qml b/resources/qml/Cura.qml
index ca1c2e38c1..a522e3ffa0 100644
--- a/resources/qml/Cura.qml
+++ b/resources/qml/Cura.qml
@@ -124,16 +124,16 @@ UM.MainWindow
}
}
- // This is a placehoder for adding a pattern in the header
- Image
- {
- id: backgroundPattern
- anchors.fill: parent
- fillMode: Image.Tile
- source: UM.Theme.getImage("header_pattern")
- horizontalAlignment: Image.AlignLeft
- verticalAlignment: Image.AlignTop
- }
+ // This is a placehoder for adding a pattern in the header
+ Image
+ {
+ id: backgroundPattern
+ anchors.fill: parent
+ fillMode: Image.Tile
+ source: UM.Theme.getImage("header_pattern")
+ horizontalAlignment: Image.AlignLeft
+ verticalAlignment: Image.AlignTop
+ }
}
MainWindowHeader
@@ -248,11 +248,59 @@ UM.MainWindow
Cura.ActionPanelWidget
{
+ id: actionPanelWidget
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: UM.Theme.getSize("thick_margin").width
anchors.bottomMargin: UM.Theme.getSize("thick_margin").height
- visible: CuraApplication.platformActivity
+
+ /*
+ Show this panel only if there is something on the build plate, and there is NOT an opaque item in front of the build plate.
+ This cannot be solved by Z indexing! If you want to try solving this, please increase this counter when you're done:
+ Number of people having tried to fix this by z-indexing: 2
+ The problem arises from the following render order requirements:
+ - The stage menu must be rendered above the stage main.
+ - The stage main must be rendered above the action panel (because the monitor page must be rendered above the action panel).
+ - The action panel must be rendered above the expandable components drop-down.
+ However since the expandable components drop-downs are child elements of the stage menu,
+ they can't be rendered lower than elements that are lower than the stage menu.
+ Therefore we opted to forego the second requirement and hide the action panel instead when something obscures it (except the expandable components).
+ We assume that QQuickRectangles are always opaque and any other item is not.
+ */
+ visible: CuraApplication.platformActivity && (main.item == null || !qmlTypeOf(main.item, "QQuickRectangle"))
+ }
+
+ Item
+ {
+ id: additionalComponents
+ width: childrenRect.width
+ anchors.right: actionPanelWidget.left
+ anchors.rightMargin: UM.Theme.getSize("default_margin").width
+ anchors.bottom: actionPanelWidget.bottom
+ anchors.bottomMargin: UM.Theme.getSize("thick_margin").height * 2
+ visible: actionPanelWidget.visible
+ Row
+ {
+ id: additionalComponentsRow
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: UM.Theme.getSize("default_margin").width
+ }
+ }
+
+ Component.onCompleted: contentItem.addAdditionalComponents()
+
+ Connections
+ {
+ target: CuraApplication
+ onAdditionalComponentsChanged: contentItem.addAdditionalComponents("saveButton")
+ }
+
+ function addAdditionalComponents()
+ {
+ for (var component in CuraApplication.additionalComponents["saveButton"])
+ {
+ CuraApplication.additionalComponents["saveButton"][component].parent = additionalComponentsRow
+ }
}
Loader
@@ -815,4 +863,21 @@ UM.MainWindow
}
}
}
+
+ /**
+ * Function to check whether a QML object has a certain type.
+ * Taken from StackOverflow: https://stackoverflow.com/a/28384228 and
+ * adapted to our code style.
+ * Licensed under CC BY-SA 3.0.
+ * \param obj The QtObject to get the name of.
+ * \param class_name (str) The name of the class to check against. Has to be
+ * the QtObject class name, not the QML entity name.
+ */
+ function qmlTypeOf(obj, class_name)
+ {
+ //className plus "(" is the class instance without modification.
+ //className plus "_QML" is the class instance with user-defined properties.
+ var str = obj.toString();
+ return str.indexOf(class_name + "(") == 0 || str.indexOf(class_name + "_QML") == 0;
+ }
}
diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml
index e57b21cb78..15d882fdf5 100644
--- a/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml
+++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationListView.qml
@@ -23,7 +23,7 @@ Item
}
}
- // This component will appear when there is no configurations (e.g. when losing connection)
+ // This component will appear when there are no configurations (e.g. when losing connection or when they are being loaded)
Item
{
width: parent.width
@@ -51,7 +51,11 @@ Item
anchors.left: icon.right
anchors.right: parent.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
- text: catalog.i18nc("@label", "Downloading the configurations from the remote printer")
+ // There are two cases that we want to diferenciate, one is when Cura is loading the configurations and the
+ // other when the connection was lost
+ text: Cura.MachineManager.printerConnected ?
+ catalog.i18nc("@label", "Loading available configurations from the printer...") :
+ catalog.i18nc("@label", "The configurations are not available because the printer is disconnected.")
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
diff --git a/resources/qml/Menus/ConfigurationMenu/ConfigurationMenu.qml b/resources/qml/Menus/ConfigurationMenu/ConfigurationMenu.qml
index 3001efac54..7d09f4be38 100644
--- a/resources/qml/Menus/ConfigurationMenu/ConfigurationMenu.qml
+++ b/resources/qml/Menus/ConfigurationMenu/ConfigurationMenu.qml
@@ -173,6 +173,59 @@ Cura.ExpandablePopup
}
}
+ Item
+ {
+ height: visible ? childrenRect.height: 0
+ anchors.right: parent.right
+ anchors.rightMargin: UM.Theme.getSize("default_margin").width
+ width: childrenRect.width + UM.Theme.getSize("default_margin").width
+ visible: popupItem.configuration_method == ConfigurationMenu.ConfigurationMethod.Custom
+ UM.RecolorImage
+ {
+ id: externalLinkIcon
+ anchors.left: parent.left
+ anchors.leftMargin: UM.Theme.getSize("default_margin").width
+ height: materialInfoLabel.height
+ width: height
+ sourceSize.height: width
+ color: UM.Theme.getColor("text_link")
+ source: UM.Theme.getIcon("external_link")
+ }
+
+ Label
+ {
+ id: materialInfoLabel
+ wrapMode: Text.WordWrap
+ text: catalog.i18nc("@label", "See the material compatibility chart")
+ font: UM.Theme.getFont("default")
+ color: UM.Theme.getColor("text_link")
+ linkColor: UM.Theme.getColor("text_link")
+ anchors.left: externalLinkIcon.right
+ anchors.leftMargin: UM.Theme.getSize("narrow_margin").width
+ renderType: Text.NativeRendering
+
+ MouseArea
+ {
+ anchors.fill: parent
+ hoverEnabled: true
+ onClicked:
+ {
+ // open the material URL with web browser
+ var url = "https://ultimaker.com/incoming-links/cura/material-compatibilty"
+ Qt.openUrlExternally(url)
+ }
+ onEntered:
+ {
+ materialInfoLabel.font.underline = true
+ }
+ onExited:
+ {
+ materialInfoLabel.font.underline = false
+ }
+ }
+ }
+ }
+
Rectangle
{
id: separator
diff --git a/resources/qml/Preferences/MachinesPage.qml b/resources/qml/Preferences/MachinesPage.qml
index d5ecb10658..f9c1a9b0a0 100644
--- a/resources/qml/Preferences/MachinesPage.qml
+++ b/resources/qml/Preferences/MachinesPage.qml
@@ -126,132 +126,15 @@ UM.ManagementPage
}
}
- Grid
- {
- id: machineInfo
-
- anchors.top: machineActions.visible ? machineActions.bottom : machineActions.anchors.top
- anchors.topMargin: UM.Theme.getSize("default_margin").height
- anchors.left: parent.left
- anchors.right: parent.right
- spacing: UM.Theme.getSize("default_margin").height
- rowSpacing: UM.Theme.getSize("default_lining").height
- columns: 2
-
- visible: base.currentItem
-
- property bool printerConnected: Cura.MachineManager.printerConnected
- property var connectedPrinter: printerConnected ? Cura.MachineManager.printerOutputDevices[0] : null
- property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
- property var printJob: connectedPrinter != null ? connectedPrinter.activePrintJob: null
- Label
- {
- text: catalog.i18nc("@label", "Printer type:")
- visible: base.currentItem && "definition_name" in base.currentItem.metadata
- }
- Label
- {
- text: (base.currentItem && "definition_name" in base.currentItem.metadata) ? base.currentItem.metadata.definition_name : ""
- }
- Label
- {
- text: catalog.i18nc("@label", "Connection:")
- visible: base.currentItem && base.currentItem.id == Cura.MachineManager.activeMachineId
- }
- Label
- {
- width: (parent.width * 0.7) | 0
- text: machineInfo.printerConnected ? machineInfo.connectedPrinter.connectionText : catalog.i18nc("@info:status", "The printer is not connected.")
- visible: base.currentItem && base.currentItem.id == Cura.MachineManager.activeMachineId
- wrapMode: Text.WordWrap
- }
- Label
- {
- text: catalog.i18nc("@label", "State:")
- visible: base.currentItem && base.currentItem.id == Cura.MachineManager.activeMachineId && machineInfo.printerAcceptsCommands
- }
- Label {
- width: (parent.width * 0.7) | 0
- text:
- {
- if(!machineInfo.printerConnected || !machineInfo.printerAcceptsCommands) {
- return "";
- }
-
- if (machineInfo.printJob == null)
- {
- return catalog.i18nc("@label:MonitorStatus", "Waiting for a printjob");
- }
-
- switch(machineInfo.printJob.state)
- {
- case "printing":
- return catalog.i18nc("@label:MonitorStatus", "Printing...");
- case "paused":
- return catalog.i18nc("@label:MonitorStatus", "Paused");
- case "pre_print":
- return catalog.i18nc("@label:MonitorStatus", "Preparing...");
- case "wait_cleanup":
- return catalog.i18nc("@label:MonitorStatus", "Waiting for someone to clear the build plate");
- case "error":
- return printerOutputDevice.errorText;
- case "maintenance":
- return catalog.i18nc("@label:MonitorStatus", "In maintenance. Please check the printer");
- case "abort": // note sure if this jobState actually occurs in the wild
- return catalog.i18nc("@label:MonitorStatus", "Aborting print...");
-
- }
- return ""
- }
- visible: base.currentItem && base.currentItem.id == Cura.MachineManager.activeMachineId && machineInfo.printerAcceptsCommands
- wrapMode: Text.WordWrap
- }
- }
-
- Column {
- id: additionalComponentsColumn
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.top: machineInfo.visible ? machineInfo.bottom : machineInfo.anchors.top
- anchors.topMargin: UM.Theme.getSize("default_margin").width
-
- spacing: UM.Theme.getSize("default_margin").width
- visible: base.currentItem && base.currentItem.id == Cura.MachineManager.activeMachineId
-
- Component.onCompleted:
- {
- for (var component in CuraApplication.additionalComponents["machinesDetailPane"]) {
- CuraApplication.additionalComponents["machinesDetailPane"][component].parent = additionalComponentsColumn
- }
- }
- }
-
- Component.onCompleted: {
- addAdditionalComponents("machinesDetailPane")
- }
-
- Connections {
- target: CuraApplication
- onAdditionalComponentsChanged: addAdditionalComponents
- }
-
- function addAdditionalComponents (areaId) {
- if(areaId == "machinesDetailPane") {
- for (var component in CuraApplication.additionalComponents["machinesDetailPane"]) {
- CuraApplication.additionalComponents["machinesDetailPane"][component].parent = additionalComponentsColumn
- }
- }
- }
-
UM.I18nCatalog { id: catalog; name: "cura"; }
UM.ConfirmRemoveDialog
{
- id: confirmDialog;
- object: base.currentItem && base.currentItem.name ? base.currentItem.name : "";
+ id: confirmDialog
+ object: base.currentItem && base.currentItem.name ? base.currentItem.name : ""
onYes:
{
- Cura.MachineManager.removeMachine(base.currentItem.id);
+ Cura.MachineManager.removeMachine(base.currentItem.id)
if(!base.currentItem)
{
objectList.currentIndex = activeMachineIndex()
diff --git a/resources/qml/Preferences/ProfilesPage.qml b/resources/qml/Preferences/ProfilesPage.qml
index d9b679e344..f23a04d800 100644
--- a/resources/qml/Preferences/ProfilesPage.qml
+++ b/resources/qml/Preferences/ProfilesPage.qml
@@ -376,6 +376,7 @@ Item
width: true ? (parent.width * 0.4) | 0 : parent.width
frameVisible: true
+ clip: true
ListView
{
diff --git a/resources/qml/PrintSetupSelector/PrintSetupSelectorContents.qml b/resources/qml/PrintSetupSelector/PrintSetupSelectorContents.qml
index 35c5f008b6..0b8fb89311 100644
--- a/resources/qml/PrintSetupSelector/PrintSetupSelectorContents.qml
+++ b/resources/qml/PrintSetupSelector/PrintSetupSelectorContents.qml
@@ -176,6 +176,17 @@ Item
UM.Preferences.setValue("view/settings_list_height", h);
}
}
+
+ UM.RecolorImage
+ {
+ width: parent.width * 0.05
+ height: parent.height * 0.3
+
+ anchors.centerIn: parent
+
+ source: UM.Theme.getIcon("grip_lines")
+ color: UM.Theme.getColor("lining")
+ }
}
}
}
\ No newline at end of file
diff --git a/resources/qml/qmldir b/resources/qml/qmldir
index 80e0f8be46..62997cc27a 100644
--- a/resources/qml/qmldir
+++ b/resources/qml/qmldir
@@ -15,4 +15,5 @@ ViewsSelector 1.0 ViewsSelector.qml
ToolbarButton 1.0 ToolbarButton.qml
SettingView 1.0 SettingView.qml
ProfileMenu 1.0 ProfileMenu.qml
-ToolTip 1.0 ToolTip.qml
\ No newline at end of file
+CheckBoxWithTooltip 1.0 CheckBoxWithTooltip.qml
+ToolTip 1.0 ToolTip.qml
diff --git a/plugins/UM3NetworkPrinting/resources/svg/icons/external_link.svg b/resources/themes/cura-light/icons/external_link.svg
similarity index 100%
rename from plugins/UM3NetworkPrinting/resources/svg/icons/external_link.svg
rename to resources/themes/cura-light/icons/external_link.svg
diff --git a/resources/themes/cura-light/icons/grip_lines.svg b/resources/themes/cura-light/icons/grip_lines.svg
new file mode 100644
index 0000000000..253d1fb486
--- /dev/null
+++ b/resources/themes/cura-light/icons/grip_lines.svg
@@ -0,0 +1,4 @@
+
\ No newline at end of file
diff --git a/resources/themes/cura-light/styles.qml b/resources/themes/cura-light/styles.qml
index 4361c3ae2b..121f604362 100755
--- a/resources/themes/cura-light/styles.qml
+++ b/resources/themes/cura-light/styles.qml
@@ -478,7 +478,7 @@ QtObject
color: (control.hovered || control._hovered) ? Theme.getColor("checkbox_hover") : (control.enabled ? Theme.getColor("checkbox") : Theme.getColor("checkbox_disabled"))
Behavior on color { ColorAnimation { duration: 50; } }
- radius: control.exclusiveGroup ? Math.round(Theme.getSize("checkbox").width / 2) : UM.Theme.getSize("checkbox_radius").width
+ radius: control.exclusiveGroup ? Math.round(Theme.getSize("checkbox").width / 2) : Theme.getSize("checkbox_radius").width
border.width: Theme.getSize("default_lining").width
border.color: (control.hovered || control._hovered) ? Theme.getColor("checkbox_border_hover") : Theme.getColor("checkbox_border")
@@ -585,6 +585,7 @@ QtObject
text: control.unit ? control.unit : ""
color: Theme.getColor("setting_unit");
font: Theme.getFont("default");
+ renderType: Text.NativeRendering
}
}
}
diff --git a/resources/themes/cura-light/theme.json b/resources/themes/cura-light/theme.json
index 1cf56039e2..4938bd1aae 100644
--- a/resources/themes/cura-light/theme.json
+++ b/resources/themes/cura-light/theme.json
@@ -9,6 +9,21 @@
"weight": 40,
"family": "Noto Sans"
},
+ "large_ja_JP": {
+ "size": 1.35,
+ "weight": 50,
+ "family": "Noto Sans"
+ },
+ "large_zh_CN": {
+ "size": 1.35,
+ "weight": 50,
+ "family": "Noto Sans"
+ },
+ "large_zh_TW": {
+ "size": 1.35,
+ "weight": 50,
+ "family": "Noto Sans"
+ },
"large_bold": {
"size": 1.35,
"weight": 63,
@@ -19,6 +34,21 @@
"weight": 40,
"family": "Noto Sans"
},
+ "medium_ja_JP": {
+ "size": 1.16,
+ "weight": 50,
+ "family": "Noto Sans"
+ },
+ "medium_zh_CN": {
+ "size": 1.16,
+ "weight": 50,
+ "family": "Noto Sans"
+ },
+ "medium_zh_TW": {
+ "size": 1.16,
+ "weight": 50,
+ "family": "Noto Sans"
+ },
"medium_bold": {
"size": 1.16,
"weight": 63,
@@ -29,21 +59,84 @@
"weight": 40,
"family": "Noto Sans"
},
+ "default_ja_JP": {
+ "size": 1.0,
+ "weight": 50,
+ "family": "Noto Sans"
+ },
+ "default_zh_CN": {
+ "size": 1.0,
+ "weight": 50,
+ "family": "Noto Sans"
+ },
+ "default_zh_TW": {
+ "size": 1.0,
+ "weight": 50,
+ "family": "Noto Sans"
+ },
"default_bold": {
"size": 0.95,
"weight": 63,
"family": "Noto Sans"
},
+ "default_bold_ja_JP": {
+ "size": 1.0,
+ "weight": 63,
+ "family": "Noto Sans"
+ },
+ "default_bold_zh_CN": {
+ "size": 1.0,
+ "weight": 63,
+ "family": "Noto Sans"
+ },
+ "default_bold_zh_TW": {
+ "size": 1.0,
+ "weight": 63,
+ "family": "Noto Sans"
+ },
"default_italic": {
"size": 0.95,
"weight": 40,
"italic": true,
"family": "Noto Sans"
},
+ "default_italic_ja_JP": {
+ "size": 1.0,
+ "weight": 50,
+ "italic": true,
+ "family": "Noto Sans"
+ },
+ "default_italic_zh_CN": {
+ "size": 1.0,
+ "weight": 50,
+ "italic": true,
+ "family": "Noto Sans"
+ },
+ "default_italic_zh_TW": {
+ "size": 1.0,
+ "weight": 50,
+ "italic": true,
+ "family": "Noto Sans"
+ },
"small": {
"size": 0.7,
"weight": 40,
"family": "Noto Sans"
+ },
+ "small_ja_JP": {
+ "size": 0.7,
+ "weight": 50,
+ "family": "Noto Sans"
+ },
+ "small_zh_CN": {
+ "size": 0.7,
+ "weight": 50,
+ "family": "Noto Sans"
+ },
+ "small_zh_TW": {
+ "size": 0.7,
+ "weight": 50,
+ "family": "Noto Sans"
}
},