diff --git a/cura/API/Account.py b/cura/API/Account.py index 5fab3e4a06..a04f97ef1c 100644 --- a/cura/API/Account.py +++ b/cura/API/Account.py @@ -6,7 +6,7 @@ from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty from UM.i18n import i18nCatalog from UM.Message import Message -from cura import CuraConstants +from cura import UltimakerCloudAuthentication from cura.OAuth2.AuthorizationService import AuthorizationService from cura.OAuth2.Models import OAuth2Settings @@ -38,7 +38,7 @@ class Account(QObject): self._logged_in = False self._callback_port = 32118 - self._oauth_root = CuraConstants.CuraCloudAccountAPIRoot + self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot self._oauth_settings = OAuth2Settings( OAUTH_SERVER_URL= 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/CuraConstants.py b/cura/ApplicationMetadata.py similarity index 50% rename from cura/CuraConstants.py rename to cura/ApplicationMetadata.py index 7ca8ea865b..e2ac4453eb 100644 --- a/cura/CuraConstants.py +++ b/cura/ApplicationMetadata.py @@ -1,60 +1,36 @@ -# -# This file contains all constant values in Cura -# - -# ------------- -# Cura Versions -# ------------- -DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura" -DEFAULT_CURA_VERSION = "master" -DEFAULT_CURA_BUILD_TYPE = "" -DEFAULT_CURA_DEBUG_MODE = False -DEFAULT_CURA_SDK_VERSION = "5.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 - - -# --------- -# Cloud API -# --------- -DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str -DEFAULT_CLOUD_API_VERSION = "1" # type: str -DEFAULT_CLOUD_ACCOUNT_API_ROOT = "https://account.ultimaker.com" # type: str - -try: - from cura.CuraVersion import CuraCloudAPIRoot # type: ignore -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 -except ImportError: - CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT +# 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 ee1df5fb2e..cf4e85f850 100755 --- a/cura/CuraApplication.py +++ b/cura/CuraApplication.py @@ -117,7 +117,7 @@ from cura.ObjectsModel import ObjectsModel from cura.PrinterOutputDevice import PrinterOutputDevice from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage -from cura import CuraConstants +from cura import ApplicationMetadata from UM.FlameProfiler import pyqtSlot from UM.Decorators import override @@ -167,11 +167,11 @@ class CuraApplication(QtApplication): def __init__(self, *args, **kwargs): super().__init__(name = "cura", - app_display_name = CuraConstants.CuraAppDisplayName, - version = CuraConstants.CuraVersion, - api_version = CuraConstants.CuraSDKVersion, - buildtype = CuraConstants.CuraBuildType, - is_debug_mode = CuraConstants.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) @@ -957,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", CuraConstants.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/UltimakerCloudAuthentication.py b/cura/UltimakerCloudAuthentication.py new file mode 100644 index 0000000000..ac752231b9 --- /dev/null +++ b/cura/UltimakerCloudAuthentication.py @@ -0,0 +1,24 @@ +# 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 +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 +except ImportError: + CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT diff --git a/plugins/CuraDrive/__init__.py b/plugins/CuraDrive/__init__.py index 6612a5d614..eeb6b78689 100644 --- a/plugins/CuraDrive/__init__.py +++ b/plugins/CuraDrive/__init__.py @@ -1,14 +1,12 @@ -# Copyright (c) 2017 Ultimaker B.V. -import os +# Copyright (c) 2018 Ultimaker B.V. +# Cura is released under the terms of the LGPLv3 or higher. -is_testing = os.getenv('ENV_NAME', "development") == "testing" +from .src.DrivePluginExtension import DrivePluginExtension -# Only load the whole plugin when not running tests as __init__.py is automatically loaded by PyTest -if not is_testing: - from .src.DrivePluginExtension import DrivePluginExtension - def getMetaData(): - return {} +def getMetaData(): + return {} - def register(app): - return {"extension": DrivePluginExtension(app)} + +def register(app): + return {"extension": DrivePluginExtension()} diff --git a/plugins/CuraDrive/plugin.json b/plugins/CuraDrive/plugin.json index 6cf1fa273c..d1cab39ca5 100644 --- a/plugins/CuraDrive/plugin.json +++ b/plugins/CuraDrive/plugin.json @@ -3,6 +3,6 @@ "author": "Ultimaker B.V.", "description": "Backup and restore your configuration.", "version": "1.2.0", - "api": 5, + "api": 6, "i18n-catalog": "cura" } diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py index 98199c91cf..7c1f8faa83 100644 --- a/plugins/CuraDrive/src/DriveApiService.py +++ b/plugins/CuraDrive/src/DriveApiService.py @@ -1,4 +1,6 @@ -# Copyright (c) 2017 Ultimaker B.V. +# 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 @@ -9,56 +11,55 @@ import requests from UM.Logger import Logger from UM.Message import Message -from UM.Signal import Signal +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: - """ - The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling. - """ - - GET_BACKUPS_URL = "{}/backups".format(Settings.DRIVE_API_URL) - PUT_BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL) - DELETE_BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL) + BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL) # Emit signal when restoring backup started or finished. - onRestoringStateChanged = Signal() + restoringStateChanged = Signal() # Emit signal when creating backup started or finished. - onCreatingStateChanged = Signal() + creatingStateChanged = Signal() - def __init__(self, cura_api) -> None: - """Create a new instance of the Drive API service and set the cura_api object.""" - self._cura_api = cura_api + def __init__(self) -> None: + self._cura_api = CuraApplication.getInstance().getCuraAPI() def getBackups(self) -> List[Dict[str, Any]]: - """Get all backups from the API.""" 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.GET_BACKUPS_URL, headers={ + backup_list_request = requests.get(self.BACKUP_URL, headers = { "Authorization": "Bearer {}".format(access_token) }) - if backup_list_request.status_code > 299: + + # 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(Settings.translatable_messages["get_backups_error"], title = Settings.MESSAGE_TITLE, - lifetime = 10).show() + 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: - """Create a backup and upload it to CuraDrive cloud storage.""" - self.onCreatingStateChanged.emit(is_creating=True) + 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.onCreatingStateChanged.emit(is_creating=False, error_message="Could not create backup.") + self.creatingStateChanged.emit(is_creating = False, error_message ="Could not create backup.") return # Create an upload entry for the backup. @@ -66,7 +67,7 @@ class DriveApiService: 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.onCreatingStateChanged.emit(is_creating=False, error_message="Could not upload backup.") + self.creatingStateChanged.emit(is_creating = False, error_message ="Could not upload backup.") return # Upload the backup to storage. @@ -75,35 +76,27 @@ class DriveApiService: upload_backup_job.start() def _onUploadFinished(self, job: "UploadBackupJob") -> None: - """ - Callback handler for the upload job. - :param job: The executed job. - """ if job.backup_upload_error_message != "": # If the job contains an error message we pass it along so the UI can display it. - self.onCreatingStateChanged.emit(is_creating=False, error_message=job.backup_upload_error_message) + self.creatingStateChanged.emit(is_creating = False, error_message = job.backup_upload_error_message) else: - self.onCreatingStateChanged.emit(is_creating=False) + self.creatingStateChanged.emit(is_creating = False) def restoreBackup(self, backup: Dict[str, Any]) -> None: - """ - Restore a previously exported backup from cloud storage. - :param backup: A dict containing an entry from the API list response. - """ - self.onRestoringStateChanged.emit(is_restoring=True) + 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 != 200: + 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) + 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) @@ -116,69 +109,59 @@ class DriveApiService: # 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("data")) - self.onRestoringStateChanged.emit(is_restoring=False) + self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("metadata", {})) + self.restoringStateChanged.emit(is_restoring = False) - def _emitRestoreError(self, error_message: str = Settings.translatable_messages["backup_restore_error_message"]): - """Helper method for emitting a signal when restoring failed.""" - self.onRestoringStateChanged.emit( - is_restoring=False, - error_message=error_message - ) + 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: - """ - 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. - """ with open(file_path, "rb") as read_backup: - local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars=b"_-").decode("utf-8") + 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: - """ - Delete a backup from the server by ID. - :param backup_id: The ID of the backup to delete. - :return: Success 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.DELETE_BACKUP_URL, backup_id), headers = { + delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = { "Authorization": "Bearer {}".format(access_token) }) - if delete_backup.status_code > 299: + 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]: - """ - 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. - """ 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.PUT_BACKUP_URL, json={ + backup_upload_request = requests.put(self.BACKUP_URL, json = { "data": { "backup_size": backup_size, "metadata": backup_metadata } - }, headers={ + }, headers = { "Authorization": "Bearer {}".format(access_token) }) - - if backup_upload_request.status_code > 299: + + # 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 diff --git a/plugins/CuraDrive/src/DrivePluginExtension.py b/plugins/CuraDrive/src/DrivePluginExtension.py index 7e1472b988..060f1496f1 100644 --- a/plugins/CuraDrive/src/DrivePluginExtension.py +++ b/plugins/CuraDrive/src/DrivePluginExtension.py @@ -1,22 +1,26 @@ -# Copyright (c) 2017 Ultimaker B.V. +# 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 +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 .models.BackupListModel import BackupListModel + +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): - """ - The DivePluginExtension provides functionality to backup and restore your Cura configuration to Ultimaker's cloud. - """ # Signal emitted when the list of backups changed. backupsChanged = pyqtSignal() @@ -29,173 +33,130 @@ class DrivePluginExtension(QObject, Extension): # Signal emitted when preferences changed (like auto-backup). preferencesChanged = pyqtSignal() - + DATE_FORMAT = "%d/%m/%Y %H:%M:%S" - def __init__(self, application): - super(DrivePluginExtension, self).__init__() - - # Re-usable instance of application. - self._application = application + 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_list_model = BackupListModel() + self._backups = [] # type: List[Dict[str, Any]] self._is_restoring_backup = False self._is_creating_backup = False # Initialize services. - self._preferences = self._application.getPreferences() - self._cura_api = self._application.getCuraAPI() - self._drive_api_service = DriveApiService(self._cura_api) + preferences = CuraApplication.getInstance().getPreferences() + self._drive_api_service = DriveApiService() # Attach signals. - self._cura_api.account.loginStateChanged.connect(self._onLoginStateChanged) - self._drive_api_service.onRestoringStateChanged.connect(self._onRestoringStateChanged) - self._drive_api_service.onCreatingStateChanged.connect(self._onCreatingStateChanged) + 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. - self._preferences.addPreference(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, False) - self._preferences.addPreference(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, datetime.now() - .strftime(self.DATE_FORMAT)) - - # Register menu items. - self._updateMenuItems() + 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. - self._application.engineCreatedSignal.connect(self._autoBackup) + CuraApplication.getInstance().engineCreatedSignal.connect(self._autoBackup) def showDriveWindow(self) -> None: - """Show the Drive UI popup window.""" if not self._drive_window: - self._drive_window = self.createDriveWindow() + 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 createDriveWindow(self) -> Optional["QObject"]: - """ - Create an instance of the Drive UI popup window. - :return: The popup window object. - """ - path = os.path.join(os.path.dirname(__file__), "qml", "main.qml") - return self._application.createQmlComponent(path, {"CuraDrive": self}) - - def _updateMenuItems(self) -> None: - """Update the menu items.""" - self.addMenuItem(Settings.translatable_messages["extension_menu_entry"], self.showDriveWindow) - def _autoBackup(self) -> None: - """Automatically make a backup on boot if enabled.""" - if self._preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._lastBackupTooLongAgo(): + preferences = CuraApplication.getInstance().getPreferences() + if preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._isLastBackupTooLongAgo(): self.createBackup() - - def _lastBackupTooLongAgo(self) -> bool: - """Check if the last backup was longer than 1 day ago.""" + + 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": - """Get the last backup date as datetime object.""" - last_backup_date = self._preferences.getValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY) + 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: - """Store the current date as last backup date.""" backup_date = datetime.now().strftime(self.DATE_FORMAT) - self._preferences.setValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, backup_date) + preferences = CuraApplication.getInstance().getPreferences() + preferences.setValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, backup_date) def _onLoginStateChanged(self, logged_in: bool = False) -> None: - """Callback handler for changes in the login state.""" if logged_in: self.refreshBackups() def _onRestoringStateChanged(self, is_restoring: bool = False, error_message: str = None) -> None: - """Callback handler for changes in the restoring state.""" self._is_restoring_backup = is_restoring self.restoringStateChanged.emit() if error_message: - Message(error_message, title = Settings.MESSAGE_TITLE, lifetime = 5).show() + Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show() def _onCreatingStateChanged(self, is_creating: bool = False, error_message: str = None) -> None: - """Callback handler for changes in the creation state.""" self._is_creating_backup = is_creating self.creatingStateChanged.emit() if error_message: - Message(error_message, title = Settings.MESSAGE_TITLE, lifetime = 5).show() + Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show() else: self._storeBackupDate() - if not is_creating: + 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: - """Enable or disable the auto-backup feature.""" - self._preferences.setValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, enabled) - self.preferencesChanged.emit() + preferences = CuraApplication.getInstance().getPreferences() + preferences.setValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, enabled) @pyqtProperty(bool, notify = preferencesChanged) def autoBackupEnabled(self) -> bool: - """Check if auto-backup is enabled or not.""" - return bool(self._preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY)) + preferences = CuraApplication.getInstance().getPreferences() + return bool(preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY)) - @pyqtProperty(QObject, notify = backupsChanged) - def backups(self) -> BackupListModel: - """ - Get a list of the backups. - :return: The backups as Qt List Model. - """ - return self._backups_list_model + @pyqtProperty("QVariantList", notify = backupsChanged) + def backups(self) -> List[Dict[str, Any]]: + return self._backups @pyqtSlot(name = "refreshBackups") def refreshBackups(self) -> None: - """ - Forcefully refresh the backups list. - """ - self._backups_list_model.loadBackups(self._drive_api_service.getBackups()) + self._backups = self._drive_api_service.getBackups() self.backupsChanged.emit() @pyqtProperty(bool, notify = restoringStateChanged) def isRestoringBackup(self) -> bool: - """ - Get the current restoring state. - :return: Boolean if we are restoring or not. - """ return self._is_restoring_backup @pyqtProperty(bool, notify = creatingStateChanged) def isCreatingBackup(self) -> bool: - """ - Get the current creating state. - :return: Boolean if we are creating or not. - """ return self._is_creating_backup @pyqtSlot(str, name = "restoreBackup") def restoreBackup(self, backup_id: str) -> None: - """ - Download and restore a backup by ID. - :param backup_id: The ID of the backup. - """ - index = self._backups_list_model.find("backup_id", backup_id) - backup = self._backups_list_model.getItem(index) - self._drive_api_service.restoreBackup(backup) + 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: - """ - Create a new backup. - """ self._drive_api_service.createBackup() @pyqtSlot(str, name = "deleteBackup") def deleteBackup(self, backup_id: str) -> None: - """ - Delete a backup by ID. - :param backup_id: The ID of the backup. - """ self._drive_api_service.deleteBackup(backup_id) self.refreshBackups() diff --git a/plugins/CuraDrive/src/Settings.py b/plugins/CuraDrive/src/Settings.py index c0df66b950..abe64e0acd 100644 --- a/plugins/CuraDrive/src/Settings.py +++ b/plugins/CuraDrive/src/Settings.py @@ -1,37 +1,13 @@ # Copyright (c) 2018 Ultimaker B.V. -from UM import i18nCatalog +# Cura is released under the terms of the LGPLv3 or higher. -from cura import CuraConstants +from cura import UltimakerCloudAuthentication class Settings: - """ - Keeps the application settings. - """ + # Keeps the plugin settings. DRIVE_API_VERSION = 1 - DRIVE_API_URL = "{}/cura-drive/v{}".format(CuraConstants.CuraCloudAPIRoot, str(DRIVE_API_VERSION)) + 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" - - I18N_CATALOG_ID = "cura" - I18N_CATALOG = i18nCatalog(I18N_CATALOG_ID) - - MESSAGE_TITLE = I18N_CATALOG.i18nc("@info:title", "Backups"), - - # Translatable messages for the entire plugin. - translatable_messages = { - - # Menu items. - "extension_menu_entry": I18N_CATALOG.i18nc("@item:inmenu", "Manage backups"), - - # Notification messages. - "backup_failed": I18N_CATALOG.i18nc("@info:backup_status", "There was an error while creating your backup."), - "uploading_backup": I18N_CATALOG.i18nc("@info:backup_status", "Uploading your backup..."), - "uploading_backup_success": I18N_CATALOG.i18nc("@info:backup_status", "Your backup has finished uploading."), - "uploading_backup_error": I18N_CATALOG.i18nc("@info:backup_status", - "There was an error while uploading your backup."), - "get_backups_error": I18N_CATALOG.i18nc("@info:backup_status", "There was an error listing your backups."), - "backup_restore_error_message": I18N_CATALOG.i18nc("@info:backup_status", - "There was an error trying to restore your backup.") - } diff --git a/plugins/CuraDrive/src/UploadBackupJob.py b/plugins/CuraDrive/src/UploadBackupJob.py index bcecce554a..2e76ed9b4b 100644 --- a/plugins/CuraDrive/src/UploadBackupJob.py +++ b/plugins/CuraDrive/src/UploadBackupJob.py @@ -1,19 +1,21 @@ # 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 .Settings import Settings +from UM.i18n import i18nCatalog +catalog = i18nCatalog("cura") class UploadBackupJob(Job): - """ - 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. - """ + 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 @@ -22,18 +24,18 @@ class UploadBackupJob(Job): self.backup_upload_error_message = "" def run(self) -> None: - Message(Settings.translatable_messages["uploading_backup"], title = Settings.MESSAGE_TITLE, - lifetime = 10).show() + 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) - if backup_upload.status_code not in (200, 201): + 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(Settings.translatable_messages["uploading_backup_error"], title = Settings.MESSAGE_TITLE, - lifetime = 10).show() + 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(Settings.translatable_messages["uploading_backup_success"], title = Settings.MESSAGE_TITLE, - lifetime = 10).show() + 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/models/BackupListModel.py b/plugins/CuraDrive/src/models/BackupListModel.py deleted file mode 100644 index 93b0c4c48c..0000000000 --- a/plugins/CuraDrive/src/models/BackupListModel.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (c) 2018 Ultimaker B.V. -from typing import Any, List, Dict - -from UM.Qt.ListModel import ListModel - -from PyQt5.QtCore import Qt - - -class BackupListModel(ListModel): - """ - The BackupListModel transforms the backups data that came from the server so it can be served to the Qt UI. - """ - - def __init__(self, parent = None) -> None: - super().__init__(parent) - self.addRoleName(Qt.UserRole + 1, "backup_id") - self.addRoleName(Qt.UserRole + 2, "download_url") - self.addRoleName(Qt.UserRole + 3, "generated_time") - self.addRoleName(Qt.UserRole + 4, "md5_hash") - self.addRoleName(Qt.UserRole + 5, "data") - - def loadBackups(self, data: List[Dict[str, Any]]) -> None: - """ - Populate the model with server data. - :param data: - """ - items = [] - for backup in data: - # We do this loop because we only want to append these specific fields. - # Without this, ListModel will break. - items.append({ - "backup_id": backup["backup_id"], - "download_url": backup["download_url"], - "generated_time": backup["generated_time"], - "md5_hash": backup["md5_hash"], - "data": backup["metadata"] - }) - self.setItems(items) diff --git a/plugins/CuraDrive/src/models/__init__.py b/plugins/CuraDrive/src/models/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/plugins/CuraDrive/src/qml/components/ActionButton.qml b/plugins/CuraDrive/src/qml/components/ActionButton.qml deleted file mode 100644 index 843079ed88..0000000000 --- a/plugins/CuraDrive/src/qml/components/ActionButton.qml +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -import QtQuick 2.7 -import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.3 - -import UM 1.1 as UM - -Button -{ - id: button - property alias cursorShape: mouseArea.cursorShape - property var iconSource: "" - property var busy: false - property var color: UM.Theme.getColor("primary") - property var hoverColor: UM.Theme.getColor("primary_hover") - property var disabledColor: color - property var textColor: UM.Theme.getColor("button_text") - property var textHoverColor: UM.Theme.getColor("button_text_hover") - property var textDisabledColor: textColor - property var textFont: UM.Theme.getFont("action_button") - - contentItem: RowLayout - { - Icon - { - id: buttonIcon - iconSource: button.iconSource - width: 16 * screenScaleFactor - color: button.hovered ? button.textHoverColor : button.textColor - visible: button.iconSource != "" && !loader.visible - } - - Icon - { - id: loader - iconSource: "../images/loading.gif" - width: 16 * screenScaleFactor - color: button.hovered ? button.textHoverColor : button.textColor - visible: button.busy - animated: true - } - - Label - { - id: buttonText - text: button.text - color: button.enabled ? (button.hovered ? button.textHoverColor : button.textColor): button.textDisabledColor - font: button.textFont - visible: button.text != "" - renderType: Text.NativeRendering - } - } - - background: Rectangle - { - color: button.enabled ? (button.hovered ? button.hoverColor : button.color) : button.disabledColor - } - - MouseArea - { - id: mouseArea - anchors.fill: parent - onPressed: mouse.accepted = false - hoverEnabled: true - cursorShape: button.enabled ? (hovered ? Qt.PointingHandCursor : Qt.ArrowCursor) : Qt.ForbiddenCursor - } -} diff --git a/plugins/CuraDrive/src/qml/components/ActionCheckBox.qml b/plugins/CuraDrive/src/qml/components/ActionCheckBox.qml deleted file mode 100644 index 71f5e6035d..0000000000 --- a/plugins/CuraDrive/src/qml/components/ActionCheckBox.qml +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -import QtQuick 2.7 -import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.3 - -import UM 1.3 as UM - -CheckBox -{ - id: checkbox - hoverEnabled: true - - property var label: "" - - indicator: Rectangle { - implicitWidth: 30 * screenScaleFactor - implicitHeight: 30 * screenScaleFactor - x: 0 - y: Math.round(parent.height / 2 - height / 2) - color: UM.Theme.getColor("sidebar") - border.color: UM.Theme.getColor("text") - - Rectangle { - width: 14 * screenScaleFactor - height: 14 * screenScaleFactor - x: 8 * screenScaleFactor - y: 8 * screenScaleFactor - color: UM.Theme.getColor("primary") - visible: checkbox.checked - } - } - - contentItem: Label { - anchors - { - left: checkbox.indicator.right - leftMargin: 5 * screenScaleFactor - } - text: catalog.i18nc("@checkbox:description", "Auto Backup") - color: UM.Theme.getColor("text") - renderType: Text.NativeRendering - verticalAlignment: Text.AlignVCenter - } - - ActionToolTip - { - text: checkbox.label - } -} diff --git a/plugins/CuraDrive/src/qml/components/ActionToolTip.qml b/plugins/CuraDrive/src/qml/components/ActionToolTip.qml deleted file mode 100644 index 93b92bc2df..0000000000 --- a/plugins/CuraDrive/src/qml/components/ActionToolTip.qml +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -import QtQuick 2.7 -import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.3 - -import UM 1.1 as UM - -ToolTip -{ - id: tooltip - visible: parent.hovered - opacity: 0.9 - delay: 500 - - background: Rectangle - { - color: UM.Theme.getColor("sidebar") - border.color: UM.Theme.getColor("primary") - border.width: 1 * screenScaleFactor - } - - contentItem: Label - { - text: tooltip.text - color: UM.Theme.getColor("text") - font: UM.Theme.getFont("very_small") - renderType: Text.NativeRendering - } -} diff --git a/plugins/CuraDrive/src/qml/components/BackupList.qml b/plugins/CuraDrive/src/qml/components/BackupList.qml index 231f25afc8..afa9538486 100644 --- a/plugins/CuraDrive/src/qml/components/BackupList.qml +++ b/plugins/CuraDrive/src/qml/components/BackupList.qml @@ -1,31 +1,37 @@ // 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.Controls 2.2 import QtQuick.Layouts 1.3 import UM 1.1 as UM -ListView +ScrollView { - id: backupList + property alias model: backupList.model width: parent.width - clip: true - delegate: Item + ListView { + id: backupList width: parent.width - height: childrenRect.height - - BackupListItem - { - id: backupListItem - width: parent.width - } - - Divider + delegate: Item { width: parent.width - anchors.top: backupListItem.bottom + height: childrenRect.height + + BackupListItem + { + id: backupListItem + width: parent.width + } + + Rectangle + { + id: divider + color: UM.Theme.getColor("lining") + height: UM.Theme.getSize("default_lining").height + } } } - ScrollBar.vertical: RightSideScrollBar {} } diff --git a/plugins/CuraDrive/src/qml/components/BackupListFooter.qml b/plugins/CuraDrive/src/qml/components/BackupListFooter.qml index 80f47d6cba..56706b9990 100644 --- a/plugins/CuraDrive/src/qml/components/BackupListFooter.qml +++ b/plugins/CuraDrive/src/qml/components/BackupListFooter.qml @@ -1,9 +1,12 @@ // 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" @@ -13,30 +16,31 @@ RowLayout width: parent.width property bool showInfoButton: false - ActionButton + Cura.PrimaryButton { id: infoButton text: catalog.i18nc("@button", "Want more?") - iconSource: "../images/info.svg" + iconSource: UM.Theme.getIcon("info") onClicked: Qt.openUrlExternally("https://goo.gl/forms/QACEP8pP3RV60QYG2") visible: backupListFooter.showInfoButton } - ActionButton + Cura.PrimaryButton { id: createBackupButton text: catalog.i18nc("@button", "Backup Now") - iconSource: "../images/backup.svg" - enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup + iconSource: UM.Theme.getIcon("plus") + enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup && !backupListFooter.showInfoButton onClicked: CuraDrive.createBackup() busy: CuraDrive.isCreatingBackup } - ActionCheckBox + Cura.CheckBoxWithTooltip { id: autoBackupEnabled checked: CuraDrive.autoBackupEnabled onClicked: CuraDrive.toggleAutoBackup(autoBackupEnabled.checked) - label: catalog.i18nc("@checkbox:description", "Automatically create a backup each day that Cura is started.") + 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 index abe9a1acf9..5cdb500b4e 100644 --- a/plugins/CuraDrive/src/qml/components/BackupListItem.qml +++ b/plugins/CuraDrive/src/qml/components/BackupListItem.qml @@ -1,10 +1,13 @@ // 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 { @@ -25,60 +28,58 @@ Item RowLayout { id: dataRow - spacing: UM.Theme.getSize("default_margin").width * 2 + spacing: UM.Theme.getSize("wide_margin").width width: parent.width height: 50 * screenScaleFactor - ActionButton + UM.SimpleButton { - color: "transparent" - hoverColor: "transparent" - textColor: UM.Theme.getColor("text") - textHoverColor: UM.Theme.getColor("primary") - iconSource: "../images/info.svg" + 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(model["generated_time"]).toLocaleString(UM.Preferences.getValue("general/language")) + 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: model["data"]["description"] + 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 } - ActionButton + Cura.SecondaryButton { text: catalog.i18nc("@button", "Restore") - color: "transparent" - hoverColor: "transparent" - textColor: UM.Theme.getColor("text") - textHoverColor: UM.Theme.getColor("text_link") enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup onClicked: confirmRestoreDialog.visible = true } - ActionButton + UM.SimpleButton { - color: "transparent" - hoverColor: "transparent" - textColor: UM.Theme.getColor("setting_validation_error") - textHoverColor: UM.Theme.getColor("setting_validation_error") - iconSource: "../images/delete.svg" + 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 } } @@ -86,7 +87,7 @@ Item BackupListItemDetails { id: backupDetails - backupDetailsData: model + backupDetailsData: modelData width: parent.width visible: parent.showDetails anchors.top: dataRow.bottom @@ -98,7 +99,7 @@ Item 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(model["backup_id"]) + onYes: CuraDrive.deleteBackup(modelData.backup_id) } MessageDialog @@ -107,6 +108,6 @@ Item 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(model["backup_id"]) + onYes: CuraDrive.restoreBackup(modelData.backup_id) } } diff --git a/plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml b/plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml index 74d4c5ab57..4da15c6f16 100644 --- a/plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml +++ b/plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml @@ -1,4 +1,6 @@ // 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 @@ -9,53 +11,53 @@ ColumnLayout { id: backupDetails width: parent.width - spacing: 10 * screenScaleFactor + spacing: UM.Theme.getSize("default_margin").width property var backupDetailsData // Cura version BackupListItemDetailsRow { - iconSource: "../images/cura.svg" + iconSource: UM.Theme.getIcon("application") label: catalog.i18nc("@backuplist:label", "Cura Version") - value: backupDetailsData["data"]["cura_release"] + value: backupDetailsData.metadata.cura_release } // Machine count. BackupListItemDetailsRow { - iconSource: "../images/printer.svg" + iconSource: UM.Theme.getIcon("printer_single") label: catalog.i18nc("@backuplist:label", "Machines") - value: backupDetailsData["data"]["machine_count"] + value: backupDetailsData.metadata.machine_count } - // Meterial count. + // Material count BackupListItemDetailsRow { - iconSource: "../images/material.svg" + iconSource: UM.Theme.getIcon("category_material") label: catalog.i18nc("@backuplist:label", "Materials") - value: backupDetailsData["data"]["material_count"] + value: backupDetailsData.metadata.material_count } - // Meterial count. + // Profile count. BackupListItemDetailsRow { - iconSource: "../images/profile.svg" + iconSource: UM.Theme.getIcon("settings") label: catalog.i18nc("@backuplist:label", "Profiles") - value: backupDetailsData["data"]["profile_count"] + value: backupDetailsData.metadata.profile_count } - // Meterial count. + // Plugin count. BackupListItemDetailsRow { - iconSource: "../images/plugin.svg" + iconSource: UM.Theme.getIcon("plugin") label: catalog.i18nc("@backuplist:label", "Plugins") - value: backupDetailsData["data"]["plugin_count"] + value: backupDetailsData.metadata.plugin_count } // Spacer. Item { width: parent.width - height: 10 * screenScaleFactor + 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 index dad1674fe7..9e4612fcf8 100644 --- a/plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml +++ b/plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml @@ -1,4 +1,6 @@ // 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 @@ -11,42 +13,40 @@ RowLayout width: parent.width height: 40 * screenScaleFactor - property var iconSource - property var label - property var value + property alias iconSource: icon.source + property alias label: detailName.text + property alias value: detailValue.text - // Spacing. - Item - { - width: 40 * screenScaleFactor - } - - Icon + UM.RecolorImage { + id: icon width: 18 * screenScaleFactor - iconSource: detailsRow.iconSource + height: width + source: "" color: UM.Theme.getColor("text") } Label { - text: detailsRow.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 { - text: detailsRow.value + 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/components/Divider.qml b/plugins/CuraDrive/src/qml/components/Divider.qml deleted file mode 100644 index bba2f2f29c..0000000000 --- a/plugins/CuraDrive/src/qml/components/Divider.qml +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -import QtQuick 2.7 - -import UM 1.3 as UM - -Rectangle -{ - id: divider - color: UM.Theme.getColor("lining") - height: UM.Theme.getSize("default_lining").height -} diff --git a/plugins/CuraDrive/src/qml/components/Icon.qml b/plugins/CuraDrive/src/qml/components/Icon.qml deleted file mode 100644 index 3cb822bf82..0000000000 --- a/plugins/CuraDrive/src/qml/components/Icon.qml +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -import QtQuick 2.7 -import QtQuick.Controls 2.1 -import QtGraphicalEffects 1.0 - -Item -{ - id: icon - width: parent.height - height: width - property var color: "transparent" - property var iconSource - property bool animated: false - - Image - { - id: iconImage - width: parent.height - height: width - smooth: true - source: icon.iconSource - sourceSize.width: width - sourceSize.height: height - antialiasing: true - visible: !icon.animated - } - - AnimatedImage - { - id: animatedIconImage - width: parent.height - height: width - smooth: true - antialiasing: true - source: "../images/loading.gif" - visible: icon.animated - } - - ColorOverlay - { - anchors.fill: iconImage - source: iconImage - color: icon.color - antialiasing: true - visible: !icon.animated - } - - ColorOverlay - { - anchors.fill: animatedIconImage - source: animatedIconImage - color: icon.color - antialiasing: true - visible: icon.animated - } -} diff --git a/plugins/CuraDrive/src/qml/components/RightSideScrollBar.qml b/plugins/CuraDrive/src/qml/components/RightSideScrollBar.qml deleted file mode 100644 index 5ac5df15ff..0000000000 --- a/plugins/CuraDrive/src/qml/components/RightSideScrollBar.qml +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) 2018 Ultimaker B.V. -import QtQuick 2.7 -import QtQuick.Controls 2.1 -import QtQuick.Layouts 1.3 - -ScrollBar -{ - active: true - size: parent.height - anchors.top: parent.top - anchors.right: parent.right - anchors.bottom: parent.bottom -} diff --git a/plugins/CuraDrive/src/qml/images/avatar_default.png b/plugins/CuraDrive/src/qml/images/avatar_default.png deleted file mode 100644 index 0c306680f7..0000000000 Binary files a/plugins/CuraDrive/src/qml/images/avatar_default.png and /dev/null differ diff --git a/plugins/CuraDrive/src/qml/images/background.svg b/plugins/CuraDrive/src/qml/images/background.svg deleted file mode 100644 index cbcfdbaa2d..0000000000 --- a/plugins/CuraDrive/src/qml/images/background.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - Polygon 18 - Created with Sketch. - - - - - - - \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/backup.svg b/plugins/CuraDrive/src/qml/images/backup.svg deleted file mode 100644 index 51f6be4cba..0000000000 --- a/plugins/CuraDrive/src/qml/images/backup.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/cura.svg b/plugins/CuraDrive/src/qml/images/cura.svg deleted file mode 100644 index 6b1b6c0c79..0000000000 --- a/plugins/CuraDrive/src/qml/images/cura.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/cura_logo.jpg b/plugins/CuraDrive/src/qml/images/cura_logo.jpg deleted file mode 100644 index 621c03f035..0000000000 Binary files a/plugins/CuraDrive/src/qml/images/cura_logo.jpg and /dev/null differ diff --git a/plugins/CuraDrive/src/qml/images/cura_logo.png b/plugins/CuraDrive/src/qml/images/cura_logo.png deleted file mode 100644 index f846f2a0f0..0000000000 Binary files a/plugins/CuraDrive/src/qml/images/cura_logo.png and /dev/null differ diff --git a/plugins/CuraDrive/src/qml/images/delete.svg b/plugins/CuraDrive/src/qml/images/delete.svg deleted file mode 100644 index 2f6190ad43..0000000000 --- a/plugins/CuraDrive/src/qml/images/delete.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/folder.svg b/plugins/CuraDrive/src/qml/images/folder.svg deleted file mode 100644 index f66f83a888..0000000000 --- a/plugins/CuraDrive/src/qml/images/folder.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/home.svg b/plugins/CuraDrive/src/qml/images/home.svg deleted file mode 100644 index 9d0e4d802c..0000000000 --- a/plugins/CuraDrive/src/qml/images/home.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/info.svg b/plugins/CuraDrive/src/qml/images/info.svg deleted file mode 100644 index 36154d6729..0000000000 --- a/plugins/CuraDrive/src/qml/images/info.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/inverted_circle.png b/plugins/CuraDrive/src/qml/images/inverted_circle.png deleted file mode 100644 index 3612b37d4d..0000000000 Binary files a/plugins/CuraDrive/src/qml/images/inverted_circle.png and /dev/null differ diff --git a/plugins/CuraDrive/src/qml/images/material.svg b/plugins/CuraDrive/src/qml/images/material.svg deleted file mode 100644 index eac724e471..0000000000 --- a/plugins/CuraDrive/src/qml/images/material.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/plugin.svg b/plugins/CuraDrive/src/qml/images/plugin.svg deleted file mode 100644 index 674eb99a54..0000000000 --- a/plugins/CuraDrive/src/qml/images/plugin.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/preview_banner.png b/plugins/CuraDrive/src/qml/images/preview_banner.png deleted file mode 100644 index 414019531b..0000000000 Binary files a/plugins/CuraDrive/src/qml/images/preview_banner.png and /dev/null differ diff --git a/plugins/CuraDrive/src/qml/images/printer.svg b/plugins/CuraDrive/src/qml/images/printer.svg deleted file mode 100644 index f7dc83987d..0000000000 --- a/plugins/CuraDrive/src/qml/images/printer.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - icn_singlePrinter - Created with Sketch. - - - - - - - - - \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/profile.svg b/plugins/CuraDrive/src/qml/images/profile.svg deleted file mode 100644 index ec2130f3d6..0000000000 --- a/plugins/CuraDrive/src/qml/images/profile.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/images/restore.svg b/plugins/CuraDrive/src/qml/images/restore.svg deleted file mode 100644 index 803215eada..0000000000 --- a/plugins/CuraDrive/src/qml/images/restore.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/plugins/CuraDrive/src/qml/main.qml b/plugins/CuraDrive/src/qml/main.qml index 4a2219cf1f..48bf3b6ea4 100644 --- a/plugins/CuraDrive/src/qml/main.qml +++ b/plugins/CuraDrive/src/qml/main.qml @@ -1,4 +1,6 @@ // 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 @@ -14,18 +16,18 @@ Window id: curaDriveDialog minimumWidth: Math.round(UM.Theme.getSize("modal_window_minimum").width) minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height) - maximumWidth: minimumWidth * 1.2 - maximumHeight: minimumHeight * 1.2 + maximumWidth: Math.round(minimumWidth * 1.2) + maximumHeight: Math.round(minimumHeight * 1.2) width: minimumWidth height: minimumHeight - color: UM.Theme.getColor("sidebar") + color: UM.Theme.getColor("main_background") title: catalog.i18nc("@title:window", "Cura Backups") // Globally available. UM.I18nCatalog { id: catalog - name: "cura_drive" + name: "cura" } WelcomePage diff --git a/plugins/CuraDrive/src/qml/pages/BackupsPage.qml b/plugins/CuraDrive/src/qml/pages/BackupsPage.qml index 88ce766383..0ba0cae09b 100644 --- a/plugins/CuraDrive/src/qml/pages/BackupsPage.qml +++ b/plugins/CuraDrive/src/qml/pages/BackupsPage.qml @@ -1,4 +1,6 @@ // 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 @@ -12,11 +14,11 @@ Item { id: backupsPage anchors.fill: parent - anchors.margins: UM.Theme.getSize("default_margin").width * 3 + anchors.margins: UM.Theme.getSize("wide_margin").width ColumnLayout { - spacing: UM.Theme.getSize("default_margin").height * 2 + spacing: UM.Theme.getSize("wide_margin").height width: parent.width anchors.fill: parent diff --git a/plugins/CuraDrive/src/qml/pages/WelcomePage.qml b/plugins/CuraDrive/src/qml/pages/WelcomePage.qml index 882656dc4a..0b207bc170 100644 --- a/plugins/CuraDrive/src/qml/pages/WelcomePage.qml +++ b/plugins/CuraDrive/src/qml/pages/WelcomePage.qml @@ -1,4 +1,6 @@ // 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 @@ -8,12 +10,14 @@ import Cura 1.1 as Cura import "../components" + Column { id: welcomePage spacing: UM.Theme.getSize("wide_margin").height width: parent.width - topPadding: 150 * screenScaleFactor + height: childrenRect.height + anchors.centerIn: parent Image { @@ -38,11 +42,15 @@ Column renderType: Text.NativeRendering } - ActionButton + Cura.PrimaryButton { id: loginButton - onClicked: Cura.API.account.login() - text: catalog.i18nc("@button", "Sign In") + 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/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py index 2229ab1f67..192471a357 100644 --- a/plugins/Toolbox/src/Toolbox.py +++ b/plugins/Toolbox/src/Toolbox.py @@ -16,8 +16,8 @@ from UM.Extension import Extension from UM.i18n import i18nCatalog from UM.Version import Version -import cura -from cura import CuraConstants +from cura import ApplicationMetadata +from cura import UltimakerCloudAuthentication from cura.CuraApplication import CuraApplication from .AuthorsModel import AuthorsModel @@ -31,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 = CuraConstants.CuraSDKVersion # type: Union[str, int] - self._cloud_api_version = CuraConstants.CuraCloudAPIVersion # type: int - self._cloud_api_root = CuraConstants.CuraCloudAPIRoot # type: 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: diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py index 8f10d02802..2ff323555a 100644 --- a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py +++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py @@ -9,7 +9,7 @@ from PyQt5.QtCore import QUrl from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager from UM.Logger import Logger -from cura import CuraConstants +from cura import UltimakerCloudAuthentication from cura.API import Account from .MeshUploader import MeshUploader from ..Models import BaseModel @@ -30,7 +30,7 @@ CloudApiClientModel = TypeVar("Model", bound = BaseModel) class CloudApiClient: # The cloud URL to use for this remote cluster. - ROOT_PATH = CuraConstants.CuraCloudAPIRoot + ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH) CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH) diff --git a/resources/bundled_packages/cura.json b/resources/bundled_packages/cura.json index dee436f584..172b27b452 100644 --- a/resources/bundled_packages/cura.json +++ b/resources/bundled_packages/cura.json @@ -57,7 +57,7 @@ "display_name": "Cura Backups", "description": "Backup and restore your configuration.", "package_version": "1.2.0", - "sdk_version": 5, + "sdk_version": 6, "website": "https://ultimaker.com", "author": { "author_id": "UltimakerPackages", 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/SliceProcessWidget.qml b/resources/qml/ActionPanel/SliceProcessWidget.qml index 1695be8748..0f415a6a2d 100644 --- a/resources/qml/ActionPanel/SliceProcessWidget.qml +++ b/resources/qml/ActionPanel/SliceProcessWidget.qml @@ -194,7 +194,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 a78295e7fa..4a031e33fa 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 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/resources/themes/cura-light/styles.qml b/resources/themes/cura-light/styles.qml index b314190e24..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")