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 70881000a3..5fab3e4a06 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 CuraConstants
from cura.OAuth2.AuthorizationService import AuthorizationService
from cura.OAuth2.Models import OAuth2Settings
@@ -37,7 +38,7 @@ class Account(QObject):
self._logged_in = False
self._callback_port = 32118
- self._oauth_root = "https://account-staging.ultimaker.com"
+ self._oauth_root = CuraConstants.CuraCloudAccountAPIRoot
self._oauth_settings = OAuth2Settings(
OAUTH_SERVER_URL= self._oauth_root,
diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py
index 7e11fd4d59..e59c4e2895 100755
--- a/cura/CuraApplication.py
+++ b/cura/CuraApplication.py
@@ -115,6 +115,8 @@ from cura.ObjectsModel import ObjectsModel
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
+from cura import CuraConstants
+
from UM.FlameProfiler import pyqtSlot
from UM.Decorators import override
@@ -127,15 +129,6 @@ if TYPE_CHECKING:
numpy.seterr(all = "ignore")
-try:
- from cura.CuraVersion import CuraAppDisplayName, CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion # type: ignore
-except ImportError:
- CuraAppDisplayName = "Ultimaker Cura"
- CuraVersion = "master" # [CodeStyle: Reflecting imported value]
- CuraBuildType = ""
- CuraDebugMode = False
- CuraSDKVersion = "5.0.0"
-
class CuraApplication(QtApplication):
# SettingVersion represents the set of settings available in the machine/extruder definitions.
@@ -162,11 +155,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 = CuraConstants.CuraAppDisplayName,
+ version = CuraConstants.CuraVersion,
+ api_version = CuraConstants.CuraSDKVersion,
+ buildtype = CuraConstants.CuraBuildType,
+ is_debug_mode = CuraConstants.CuraDebugMode,
tray_icon_name = "cura-icon-32.png",
**kwargs)
@@ -936,7 +929,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", CuraConstants.CuraSDKVersion)
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
diff --git a/cura/CuraConstants.py b/cura/CuraConstants.py
new file mode 100644
index 0000000000..7ca8ea865b
--- /dev/null
+++ b/cura/CuraConstants.py
@@ -0,0 +1,60 @@
+#
+# 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
diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py
index f75ad9c9f9..762d0db069 100644
--- a/cura/OAuth2/AuthorizationHelpers.py
+++ b/cura/OAuth2/AuthorizationHelpers.py
@@ -81,9 +81,14 @@ class AuthorizationHelpers:
# \param access_token: The encoded JWT token.
# \return: Dict containing some profile data.
def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
- token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
- "Authorization": "Bearer {}".format(access_token)
- })
+ try:
+ token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
+ "Authorization": "Bearer {}".format(access_token)
+ })
+ except ConnectionError:
+ # Connection was suddenly dropped. Nothing we can do about that.
+ Logger.logException("e", "Something failed while attempting to parse the JWT token")
+ return None
if token_request.status_code not in (200, 201):
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
return None
diff --git a/plugins/CuraDrive/__init__.py b/plugins/CuraDrive/__init__.py
new file mode 100644
index 0000000000..6612a5d614
--- /dev/null
+++ b/plugins/CuraDrive/__init__.py
@@ -0,0 +1,14 @@
+# Copyright (c) 2017 Ultimaker B.V.
+import os
+
+is_testing = os.getenv('ENV_NAME', "development") == "testing"
+
+# 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 register(app):
+ return {"extension": DrivePluginExtension(app)}
diff --git a/plugins/CuraDrive/plugin.json b/plugins/CuraDrive/plugin.json
new file mode 100644
index 0000000000..6cf1fa273c
--- /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": 5,
+ "i18n-catalog": "cura"
+}
diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py
new file mode 100644
index 0000000000..98199c91cf
--- /dev/null
+++ b/plugins/CuraDrive/src/DriveApiService.py
@@ -0,0 +1,185 @@
+# Copyright (c) 2017 Ultimaker B.V.
+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
+
+from .UploadBackupJob import UploadBackupJob
+from .Settings import Settings
+
+
+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)
+
+ # Emit signal when restoring backup started or finished.
+ onRestoringStateChanged = Signal()
+
+ # Emit signal when creating backup started or finished.
+ onCreatingStateChanged = 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 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={
+ "Authorization": "Bearer {}".format(access_token)
+ })
+ if backup_list_request.status_code > 299:
+ 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()
+ 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)
+
+ # 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.")
+ 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.onCreatingStateChanged.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:
+ """
+ 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)
+ else:
+ self.onCreatingStateChanged.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)
+ 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:
+ # 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("data"))
+ self.onRestoringStateChanged.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
+ )
+
+ @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")
+ 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 = {
+ "Authorization": "Bearer {}".format(access_token)
+ })
+ if delete_backup.status_code > 299:
+ Logger.log("w", "Could not delete backup: %s", delete_backup.text)
+ return False
+ return True
+
+ 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={
+ "data": {
+ "backup_size": backup_size,
+ "metadata": backup_metadata
+ }
+ }, headers={
+ "Authorization": "Bearer {}".format(access_token)
+ })
+
+ if backup_upload_request.status_code > 299:
+ 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..7e1472b988
--- /dev/null
+++ b/plugins/CuraDrive/src/DrivePluginExtension.py
@@ -0,0 +1,201 @@
+# Copyright (c) 2017 Ultimaker B.V.
+import os
+from datetime import datetime
+from typing import Optional
+
+from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
+
+from UM.Extension import Extension
+from UM.Message import Message
+
+from .Settings import Settings
+from .DriveApiService import DriveApiService
+from .models.BackupListModel import BackupListModel
+
+
+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()
+
+ # 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, application):
+ super(DrivePluginExtension, self).__init__()
+
+ # Re-usable instance of application.
+ self._application = application
+
+ # Local data caching for the UI.
+ self._drive_window = None # type: Optional[QObject]
+ self._backups_list_model = BackupListModel()
+ 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)
+
+ # 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)
+
+ # 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()
+
+ # Make auto-backup on boot if required.
+ self._application.engineCreatedSignal.connect(self._autoBackup)
+
+ def showDriveWindow(self) -> None:
+ """Show the Drive UI popup window."""
+ if not self._drive_window:
+ self._drive_window = self.createDriveWindow()
+ 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():
+ self.createBackup()
+
+ def _lastBackupTooLongAgo(self) -> bool:
+ """Check if the last backup was longer than 1 day ago."""
+ 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)
+ 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)
+
+ 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()
+
+ 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()
+ else:
+ self._storeBackupDate()
+ if not is_creating:
+ # 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()
+
+ @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))
+
+ @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
+
+ @pyqtSlot(name = "refreshBackups")
+ def refreshBackups(self) -> None:
+ """
+ Forcefully refresh the backups list.
+ """
+ self._backups_list_model.loadBackups(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)
+
+ @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
new file mode 100644
index 0000000000..c0df66b950
--- /dev/null
+++ b/plugins/CuraDrive/src/Settings.py
@@ -0,0 +1,37 @@
+# Copyright (c) 2018 Ultimaker B.V.
+from UM import i18nCatalog
+
+from cura import CuraConstants
+
+
+class Settings:
+ """
+ Keeps the application settings.
+ """
+ DRIVE_API_VERSION = 1
+ DRIVE_API_URL = "{}/cura-drive/v{}".format(CuraConstants.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
new file mode 100644
index 0000000000..bcecce554a
--- /dev/null
+++ b/plugins/CuraDrive/src/UploadBackupJob.py
@@ -0,0 +1,39 @@
+# Copyright (c) 2018 Ultimaker B.V.
+import requests
+
+from UM.Job import Job
+from UM.Logger import Logger
+from UM.Message import Message
+
+from .Settings import Settings
+
+
+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.
+ """
+
+ 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:
+ Message(Settings.translatable_messages["uploading_backup"], title = Settings.MESSAGE_TITLE,
+ lifetime = 10).show()
+
+ backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip)
+ if backup_upload.status_code not in (200, 201):
+ 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()
+ else:
+ self._upload_success = True
+ Message(Settings.translatable_messages["uploading_backup_success"], title = Settings.MESSAGE_TITLE,
+ lifetime = 10).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/models/BackupListModel.py b/plugins/CuraDrive/src/models/BackupListModel.py
new file mode 100644
index 0000000000..93b0c4c48c
--- /dev/null
+++ b/plugins/CuraDrive/src/models/BackupListModel.py
@@ -0,0 +1,38 @@
+# 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
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/plugins/CuraDrive/src/qml/components/ActionButton.qml b/plugins/CuraDrive/src/qml/components/ActionButton.qml
new file mode 100644
index 0000000000..843079ed88
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/ActionButton.qml
@@ -0,0 +1,67 @@
+// 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
new file mode 100644
index 0000000000..71f5e6035d
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/ActionCheckBox.qml
@@ -0,0 +1,49 @@
+// 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
new file mode 100644
index 0000000000..93b92bc2df
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/ActionToolTip.qml
@@ -0,0 +1,29 @@
+// 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
new file mode 100644
index 0000000000..231f25afc8
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupList.qml
@@ -0,0 +1,31 @@
+// 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
+
+ListView
+{
+ id: backupList
+ width: parent.width
+ clip: true
+ delegate: Item
+ {
+ width: parent.width
+ height: childrenRect.height
+
+ BackupListItem
+ {
+ id: backupListItem
+ width: parent.width
+ }
+
+ Divider
+ {
+ width: parent.width
+ anchors.top: backupListItem.bottom
+ }
+ }
+ ScrollBar.vertical: RightSideScrollBar {}
+}
diff --git a/plugins/CuraDrive/src/qml/components/BackupListFooter.qml b/plugins/CuraDrive/src/qml/components/BackupListFooter.qml
new file mode 100644
index 0000000000..80f47d6cba
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupListFooter.qml
@@ -0,0 +1,42 @@
+// 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
+
+import "../components"
+
+RowLayout
+{
+ id: backupListFooter
+ width: parent.width
+ property bool showInfoButton: false
+
+ ActionButton
+ {
+ id: infoButton
+ text: catalog.i18nc("@button", "Want more?")
+ iconSource: "../images/info.svg"
+ onClicked: Qt.openUrlExternally("https://goo.gl/forms/QACEP8pP3RV60QYG2")
+ visible: backupListFooter.showInfoButton
+ }
+
+ ActionButton
+ {
+ id: createBackupButton
+ text: catalog.i18nc("@button", "Backup Now")
+ iconSource: "../images/backup.svg"
+ enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
+ onClicked: CuraDrive.createBackup()
+ busy: CuraDrive.isCreatingBackup
+ }
+
+ ActionCheckBox
+ {
+ 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.")
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/components/BackupListItem.qml b/plugins/CuraDrive/src/qml/components/BackupListItem.qml
new file mode 100644
index 0000000000..abe9a1acf9
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupListItem.qml
@@ -0,0 +1,112 @@
+// Copyright (c) 2018 Ultimaker B.V.
+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
+
+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("default_margin").width * 2
+ width: parent.width
+ height: 50 * screenScaleFactor
+
+ ActionButton
+ {
+ color: "transparent"
+ hoverColor: "transparent"
+ textColor: UM.Theme.getColor("text")
+ textHoverColor: UM.Theme.getColor("primary")
+ iconSource: "../images/info.svg"
+ onClicked: backupListItem.showDetails = !backupListItem.showDetails
+ }
+
+ Label
+ {
+ text: new Date(model["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
+ renderType: Text.NativeRendering
+ }
+
+ Label
+ {
+ text: model["data"]["description"]
+ color: UM.Theme.getColor("text")
+ elide: Text.ElideRight
+ Layout.minimumWidth: 100 * screenScaleFactor
+ Layout.maximumWidth: 500 * screenScaleFactor
+ Layout.fillWidth: true
+ renderType: Text.NativeRendering
+ }
+
+ ActionButton
+ {
+ 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
+ {
+ color: "transparent"
+ hoverColor: "transparent"
+ textColor: UM.Theme.getColor("setting_validation_error")
+ textHoverColor: UM.Theme.getColor("setting_validation_error")
+ iconSource: "../images/delete.svg"
+ onClicked: confirmDeleteDialog.visible = true
+ }
+ }
+
+ BackupListItemDetails
+ {
+ id: backupDetails
+ backupDetailsData: model
+ 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(model["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(model["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..74d4c5ab57
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml
@@ -0,0 +1,61 @@
+// 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
+
+ColumnLayout
+{
+ id: backupDetails
+ width: parent.width
+ spacing: 10 * screenScaleFactor
+ property var backupDetailsData
+
+ // Cura version
+ BackupListItemDetailsRow
+ {
+ iconSource: "../images/cura.svg"
+ label: catalog.i18nc("@backuplist:label", "Cura Version")
+ value: backupDetailsData["data"]["cura_release"]
+ }
+
+ // Machine count.
+ BackupListItemDetailsRow
+ {
+ iconSource: "../images/printer.svg"
+ label: catalog.i18nc("@backuplist:label", "Machines")
+ value: backupDetailsData["data"]["machine_count"]
+ }
+
+ // Meterial count.
+ BackupListItemDetailsRow
+ {
+ iconSource: "../images/material.svg"
+ label: catalog.i18nc("@backuplist:label", "Materials")
+ value: backupDetailsData["data"]["material_count"]
+ }
+
+ // Meterial count.
+ BackupListItemDetailsRow
+ {
+ iconSource: "../images/profile.svg"
+ label: catalog.i18nc("@backuplist:label", "Profiles")
+ value: backupDetailsData["data"]["profile_count"]
+ }
+
+ // Meterial count.
+ BackupListItemDetailsRow
+ {
+ iconSource: "../images/plugin.svg"
+ label: catalog.i18nc("@backuplist:label", "Plugins")
+ value: backupDetailsData["data"]["plugin_count"]
+ }
+
+ // Spacer.
+ Item
+ {
+ width: parent.width
+ height: 10 * screenScaleFactor
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml b/plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml
new file mode 100644
index 0000000000..dad1674fe7
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml
@@ -0,0 +1,52 @@
+// 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
+
+RowLayout
+{
+ id: detailsRow
+ width: parent.width
+ height: 40 * screenScaleFactor
+
+ property var iconSource
+ property var label
+ property var value
+
+ // Spacing.
+ Item
+ {
+ width: 40 * screenScaleFactor
+ }
+
+ Icon
+ {
+ width: 18 * screenScaleFactor
+ iconSource: detailsRow.iconSource
+ color: UM.Theme.getColor("text")
+ }
+
+ Label
+ {
+ text: detailsRow.label
+ color: UM.Theme.getColor("text")
+ elide: Text.ElideRight
+ Layout.minimumWidth: 50 * screenScaleFactor
+ Layout.maximumWidth: 100 * screenScaleFactor
+ Layout.fillWidth: true
+ renderType: Text.NativeRendering
+ }
+
+ Label
+ {
+ text: detailsRow.value
+ color: UM.Theme.getColor("text")
+ elide: Text.ElideRight
+ Layout.minimumWidth: 50 * screenScaleFactor
+ Layout.maximumWidth: 100 * screenScaleFactor
+ Layout.fillWidth: true
+ renderType: Text.NativeRendering
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/components/Divider.qml b/plugins/CuraDrive/src/qml/components/Divider.qml
new file mode 100644
index 0000000000..bba2f2f29c
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/Divider.qml
@@ -0,0 +1,11 @@
+// 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
new file mode 100644
index 0000000000..3cb822bf82
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/Icon.qml
@@ -0,0 +1,56 @@
+// 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
new file mode 100644
index 0000000000..5ac5df15ff
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/RightSideScrollBar.qml
@@ -0,0 +1,13 @@
+// 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
new file mode 100644
index 0000000000..0c306680f7
Binary files /dev/null and b/plugins/CuraDrive/src/qml/images/avatar_default.png differ
diff --git a/plugins/CuraDrive/src/qml/images/background.svg b/plugins/CuraDrive/src/qml/images/background.svg
new file mode 100644
index 0000000000..cbcfdbaa2d
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/images/background.svg
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/plugins/CuraDrive/src/qml/images/backup.svg b/plugins/CuraDrive/src/qml/images/backup.svg
new file mode 100644
index 0000000000..51f6be4cba
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/images/backup.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/plugins/CuraDrive/src/qml/images/cura.svg b/plugins/CuraDrive/src/qml/images/cura.svg
new file mode 100644
index 0000000000..6b1b6c0c79
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/images/cura.svg
@@ -0,0 +1,7 @@
+
\ 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
new file mode 100644
index 0000000000..621c03f035
Binary files /dev/null and b/plugins/CuraDrive/src/qml/images/cura_logo.jpg differ
diff --git a/plugins/CuraDrive/src/qml/images/cura_logo.png b/plugins/CuraDrive/src/qml/images/cura_logo.png
new file mode 100644
index 0000000000..f846f2a0f0
Binary files /dev/null and b/plugins/CuraDrive/src/qml/images/cura_logo.png differ
diff --git a/plugins/CuraDrive/src/qml/images/delete.svg b/plugins/CuraDrive/src/qml/images/delete.svg
new file mode 100644
index 0000000000..2f6190ad43
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/images/delete.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/plugins/CuraDrive/src/qml/images/folder.svg b/plugins/CuraDrive/src/qml/images/folder.svg
new file mode 100644
index 0000000000..f66f83a888
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/images/folder.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/plugins/CuraDrive/src/qml/images/home.svg b/plugins/CuraDrive/src/qml/images/home.svg
new file mode 100644
index 0000000000..9d0e4d802c
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/images/home.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
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/info.svg b/plugins/CuraDrive/src/qml/images/info.svg
new file mode 100644
index 0000000000..36154d6729
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/images/info.svg
@@ -0,0 +1,4 @@
+
+
\ 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
new file mode 100644
index 0000000000..3612b37d4d
Binary files /dev/null and b/plugins/CuraDrive/src/qml/images/inverted_circle.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/images/material.svg b/plugins/CuraDrive/src/qml/images/material.svg
new file mode 100644
index 0000000000..eac724e471
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/images/material.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/plugins/CuraDrive/src/qml/images/plugin.svg b/plugins/CuraDrive/src/qml/images/plugin.svg
new file mode 100644
index 0000000000..674eb99a54
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/images/plugin.svg
@@ -0,0 +1,7 @@
+
\ 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
new file mode 100644
index 0000000000..414019531b
Binary files /dev/null and b/plugins/CuraDrive/src/qml/images/preview_banner.png differ
diff --git a/plugins/CuraDrive/src/qml/images/printer.svg b/plugins/CuraDrive/src/qml/images/printer.svg
new file mode 100644
index 0000000000..f7dc83987d
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/images/printer.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/plugins/CuraDrive/src/qml/images/profile.svg b/plugins/CuraDrive/src/qml/images/profile.svg
new file mode 100644
index 0000000000..ec2130f3d6
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/images/profile.svg
@@ -0,0 +1,3 @@
+
\ No newline at end of file
diff --git a/plugins/CuraDrive/src/qml/images/restore.svg b/plugins/CuraDrive/src/qml/images/restore.svg
new file mode 100644
index 0000000000..803215eada
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/images/restore.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file
diff --git a/plugins/CuraDrive/src/qml/main.qml b/plugins/CuraDrive/src/qml/main.qml
new file mode 100644
index 0000000000..4a2219cf1f
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/main.qml
@@ -0,0 +1,42 @@
+// Copyright (c) 2018 Ultimaker B.V.
+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: minimumWidth * 1.2
+ maximumHeight: minimumHeight * 1.2
+ width: minimumWidth
+ height: minimumHeight
+ color: UM.Theme.getColor("sidebar")
+ title: catalog.i18nc("@title:window", "Cura Backups")
+
+ // Globally available.
+ UM.I18nCatalog
+ {
+ id: catalog
+ name: "cura_drive"
+ }
+
+ 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..88ce766383
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/pages/BackupsPage.qml
@@ -0,0 +1,73 @@
+// 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
+import Cura 1.1 as Cura
+
+import "../components"
+
+Item
+{
+ id: backupsPage
+ anchors.fill: parent
+ anchors.margins: UM.Theme.getSize("default_margin").width * 3
+
+ ColumnLayout
+ {
+ spacing: UM.Theme.getSize("default_margin").height * 2
+ 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..882656dc4a
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/pages/WelcomePage.qml
@@ -0,0 +1,48 @@
+// Copyright (c) 2018 Ultimaker B.V.
+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
+ topPadding: 150 * screenScaleFactor
+
+ 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
+ }
+
+ ActionButton
+ {
+ id: loginButton
+ onClicked: Cura.API.account.login()
+ text: catalog.i18nc("@button", "Sign In")
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+}
diff --git a/plugins/Toolbox/resources/qml/Toolbox.qml b/plugins/Toolbox/resources/qml/Toolbox.qml
index 853cec399d..9ede2a6bda 100644
--- a/plugins/Toolbox/resources/qml/Toolbox.qml
+++ b/plugins/Toolbox/resources/qml/Toolbox.qml
@@ -14,8 +14,8 @@ Window
modality: Qt.ApplicationModal
flags: Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint
- width: 720 * screenScaleFactor
- height: 640 * screenScaleFactor
+ width: Math.floor(720 * screenScaleFactor)
+ height: Math.floor(640 * screenScaleFactor)
minimumWidth: width
maximumWidth: minimumWidth
minimumHeight: height
@@ -95,6 +95,7 @@ Window
licenseDialog.show();
}
}
+
ToolboxLicenseDialog
{
id: licenseDialog
diff --git a/plugins/Toolbox/resources/qml/ToolboxAuthorPage.qml b/plugins/Toolbox/resources/qml/ToolboxAuthorPage.qml
index 9c1df0c49e..7b026566c3 100644
--- a/plugins/Toolbox/resources/qml/ToolboxAuthorPage.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxAuthorPage.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.3
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@@ -59,6 +59,7 @@ Item
wrapMode: Text.WordWrap
width: parent.width
height: UM.Theme.getSize("toolbox_property_label").height
+ renderType: Text.NativeRendering
}
Label
{
@@ -70,6 +71,7 @@ Item
left: title.left
topMargin: UM.Theme.getSize("default_margin").height
}
+ renderType: Text.NativeRendering
}
Column
{
@@ -88,12 +90,14 @@ Item
text: catalog.i18nc("@label", "Website") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
+ renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Email") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
+ renderType: Text.NativeRendering
}
}
Column
@@ -122,6 +126,7 @@ Item
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
+ renderType: Text.NativeRendering
}
Label
@@ -138,6 +143,7 @@ Item
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
+ renderType: Text.NativeRendering
}
}
Rectangle
diff --git a/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml b/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml
index 8524b7d1e5..edb1967fee 100644
--- a/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.2
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@@ -64,6 +64,7 @@ Item
font: UM.Theme.getFont("default_bold")
horizontalAlignment: Text.AlignRight
width: control.width
+ renderType: Text.NativeRendering
}
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml b/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml
index d4c0ae14eb..db4e8c628f 100644
--- a/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@@ -67,6 +67,7 @@ Item
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
+ renderType: Text.NativeRendering
}
TableView
@@ -99,6 +100,7 @@ Item
text: styleData.value || ""
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default_bold")
+ renderType: Text.NativeRendering
}
Rectangle
{
@@ -118,6 +120,7 @@ Item
text: styleData.value || ""
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
}
}
itemDelegate: Item
@@ -130,6 +133,7 @@ Item
text: styleData.value || ""
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
}
}
@@ -144,6 +148,7 @@ Item
elide: Text.ElideRight
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
}
}
@@ -232,5 +237,6 @@ Item
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
+ renderType: Text.NativeRendering
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxConfirmUninstallResetDialog.qml b/plugins/Toolbox/resources/qml/ToolboxConfirmUninstallResetDialog.qml
index 2c5d08aa72..e238132680 100644
--- a/plugins/Toolbox/resources/qml/ToolboxConfirmUninstallResetDialog.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxConfirmUninstallResetDialog.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.2
+import QtQuick 2.10
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
@@ -66,6 +66,7 @@ UM.Dialog
anchors.right: parent.right
font: UM.Theme.getFont("default")
wrapMode: Text.WordWrap
+ renderType: Text.NativeRendering
}
// Buttons
diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailList.qml b/plugins/Toolbox/resources/qml/ToolboxDetailList.qml
index 2e5eae098c..4e44ea7d0b 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDetailList.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDetailList.qml
@@ -26,10 +26,19 @@ Item
}
height: childrenRect.height + 2 * UM.Theme.getSize("wide_margin").height
spacing: UM.Theme.getSize("default_margin").height
+
Repeater
{
model: toolbox.packagesModel
- delegate: ToolboxDetailTile {}
+ delegate: Loader
+ {
+ // FIXME: When using asynchronous loading, on Mac and Windows, the tile may fail to load complete,
+ // leaving an empty space below the title part. We turn it off for now to make it work on Mac and
+ // Windows.
+ // Can be related to this QT bug: https://bugreports.qt.io/browse/QTBUG-50992
+ asynchronous: false
+ source: "ToolboxDetailTile.qml"
+ }
}
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml b/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml
index 9e2e178b71..7983be8aef 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.3
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@@ -65,6 +65,7 @@ Item
wrapMode: Text.WordWrap
width: parent.width
height: UM.Theme.getSize("toolbox_property_label").height
+ renderType: Text.NativeRendering
}
Column
@@ -84,24 +85,28 @@ Item
text: catalog.i18nc("@label", "Version") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
+ renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Last updated") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
+ renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Author") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
+ renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Downloads") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
+ renderType: Text.NativeRendering
}
}
Column
@@ -121,6 +126,7 @@ Item
text: details === null ? "" : (details.version || catalog.i18nc("@label", "Unknown"))
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
+ renderType: Text.NativeRendering
}
Label
{
@@ -135,6 +141,7 @@ Item
}
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
+ renderType: Text.NativeRendering
}
Label
{
@@ -153,12 +160,14 @@ Item
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
+ renderType: Text.NativeRendering
}
Label
{
text: details === null ? "" : (details.download_count || catalog.i18nc("@label", "Unknown"))
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
+ renderType: Text.NativeRendering
}
}
Rectangle
diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml
index 1d701543ce..43f97baf3f 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@@ -31,6 +31,7 @@ Item
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("medium_bold")
+ renderType: Text.NativeRendering
}
Label
{
@@ -42,6 +43,7 @@ Item
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml
index cd1e4cdbda..7160dafa2d 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml
@@ -1,40 +1,69 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
+import Cura 1.1 as Cura
Column
{
property bool installed: toolbox.isInstalled(model.id)
property bool canUpdate: toolbox.canUpdate(model.id)
+ property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
+
width: UM.Theme.getSize("toolbox_action_button").width
spacing: UM.Theme.getSize("narrow_margin").height
- ToolboxProgressButton
+ Item
{
- id: installButton
- active: toolbox.isDownloading && toolbox.activePackage == model
- complete: installed
- readyAction: function()
+ width: installButton.width
+ height: installButton.height
+ ToolboxProgressButton
{
- toolbox.activePackage = model
- toolbox.startDownload(model.download_url)
+ id: installButton
+ active: toolbox.isDownloading && toolbox.activePackage == model
+ onReadyAction:
+ {
+ toolbox.activePackage = model
+ toolbox.startDownload(model.download_url)
+ }
+ onActiveAction: toolbox.cancelDownload()
+
+ // Don't allow installing while another download is running
+ enabled: installed || (!(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired)
+ opacity: enabled ? 1.0 : 0.5
+ visible: !updateButton.visible && !installed// Don't show when the update button is visible
}
- activeAction: function()
+
+ Cura.SecondaryButton
{
- toolbox.cancelDownload()
+ visible: installed
+ onClicked: toolbox.viewCategory = "installed"
+ text: catalog.i18nc("@action:button", "Installed")
+ fixedWidthMode: true
+ width: installButton.width
+ height: installButton.height
}
- completeAction: function()
+ }
+
+ Label
+ {
+ wrapMode: Text.WordWrap
+ text: catalog.i18nc("@label:The string between and is the highlighted link", "Log in is required to install or update")
+ font: UM.Theme.getFont("default")
+ color: UM.Theme.getColor("text")
+ linkColor: UM.Theme.getColor("text_link")
+ visible: loginRequired
+ width: installButton.width
+ renderType: Text.NativeRendering
+
+ MouseArea
{
- toolbox.viewCategory = "installed"
+ anchors.fill: parent
+ onClicked: Cura.API.account.login()
}
- // Don't allow installing while another download is running
- enabled: installed || !(toolbox.isDownloading && toolbox.activePackage != model)
- opacity: enabled ? 1.0 : 0.5
- visible: !updateButton.visible // Don't show when the update button is visible
}
ToolboxProgressButton
@@ -44,20 +73,19 @@ Column
readyLabel: catalog.i18nc("@action:button", "Update")
activeLabel: catalog.i18nc("@action:button", "Updating")
completeLabel: catalog.i18nc("@action:button", "Updated")
- readyAction: function()
+
+ onReadyAction:
{
toolbox.activePackage = model
toolbox.update(model.id)
}
- activeAction: function()
- {
- toolbox.cancelDownload()
- }
+ onActiveAction: toolbox.cancelDownload()
// Don't allow installing while another download is running
- enabled: !(toolbox.isDownloading && toolbox.activePackage != model)
+ enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired
opacity: enabled ? 1.0 : 0.5
visible: canUpdate
}
+
Connections
{
target: toolbox
diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml
index c586828969..85f0ff8be4 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.3
@@ -23,8 +23,9 @@ Column
width: parent.width
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
+ renderType: Text.NativeRendering
}
- GridLayout
+ Grid
{
id: grid
width: parent.width - 2 * parent.padding
@@ -34,10 +35,12 @@ Column
Repeater
{
model: gridArea.model
- delegate: ToolboxDownloadsGridTile
+ delegate: Loader
{
- Layout.preferredWidth: (grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns
- Layout.preferredHeight: UM.Theme.getSize("toolbox_thumbnail_small").height
+ asynchronous: true
+ width: Math.round((grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns)
+ height: UM.Theme.getSize("toolbox_thumbnail_small").height
+ source: "ToolboxDownloadsGridTile.qml"
}
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml
index 61374f9d99..357e9e9a72 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.3
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.3
@@ -62,6 +62,8 @@ Item
{
width: parent.width - thumbnail.width - parent.spacing
spacing: Math.floor(UM.Theme.getSize("narrow_margin").width)
+ anchors.top: parent.top
+ anchors.topMargin: UM.Theme.getSize("default_margin").height
Label
{
id: name
@@ -70,6 +72,7 @@ Item
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default_bold")
+ renderType: Text.NativeRendering
}
Label
{
@@ -81,6 +84,7 @@ Item
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
}
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml
index 46f5debfdd..820b74554a 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@@ -24,29 +24,33 @@ Rectangle
width: parent.width
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
+ renderType: Text.NativeRendering
}
Grid
{
height: childrenRect.height
spacing: UM.Theme.getSize("wide_margin").width
columns: 3
- anchors
- {
- horizontalCenter: parent.horizontalCenter
- }
+ anchors.horizontalCenter: parent.horizontalCenter
+
Repeater
{
- model: {
- if ( toolbox.viewCategory == "plugin" )
+ model:
+ {
+ if (toolbox.viewCategory == "plugin")
{
return toolbox.pluginsShowcaseModel
}
- if ( toolbox.viewCategory == "material" )
+ if (toolbox.viewCategory == "material")
{
return toolbox.materialsShowcaseModel
}
}
- delegate: ToolboxDownloadsShowcaseTile {}
+ delegate: Loader
+ {
+ asynchronous: true
+ source: "ToolboxDownloadsShowcaseTile.qml"
+ }
}
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml
index 8a2fdc8bc8..d1130cf63f 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtGraphicalEffects 1.0
@@ -79,6 +79,7 @@ Rectangle
wrapMode: Text.WordWrap
color: UM.Theme.getColor("button_text")
font: UM.Theme.getFont("medium_bold")
+ renderType: Text.NativeRendering
}
}
MouseArea
diff --git a/plugins/Toolbox/resources/qml/ToolboxErrorPage.qml b/plugins/Toolbox/resources/qml/ToolboxErrorPage.qml
index 600ae2b39f..e57e63dbb9 100644
--- a/plugins/Toolbox/resources/qml/ToolboxErrorPage.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxErrorPage.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
@@ -18,5 +18,6 @@ Rectangle
{
centerIn: parent
}
+ renderType: Text.NativeRendering
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxFooter.qml b/plugins/Toolbox/resources/qml/ToolboxFooter.qml
index 5c2a6577ad..2d42ca7269 100644
--- a/plugins/Toolbox/resources/qml/ToolboxFooter.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxFooter.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.2
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@@ -26,7 +26,7 @@ Item
right: restartButton.right
rightMargin: UM.Theme.getSize("default_margin").width
}
-
+ renderType: Text.NativeRendering
}
Button
{
@@ -56,6 +56,7 @@ Item
text: control.text
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
+ renderType: Text.NativeRendering
}
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml
index e683f89823..e1d01db59a 100644
--- a/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
@@ -21,44 +21,40 @@ ScrollView
Column
{
spacing: UM.Theme.getSize("default_margin").height
+ visible: toolbox.pluginsInstalledModel.items.length > 0
+ height: childrenRect.height + 4 * UM.Theme.getSize("default_margin").height
+
anchors
{
right: parent.right
left: parent.left
- leftMargin: UM.Theme.getSize("wide_margin").width
- topMargin: UM.Theme.getSize("wide_margin").height
- bottomMargin: UM.Theme.getSize("wide_margin").height
+ margins: UM.Theme.getSize("default_margin").width
top: parent.top
}
- height: childrenRect.height + 4 * UM.Theme.getSize("default_margin").height
+
Label
{
- visible: toolbox.pluginsInstalledModel.items.length > 0
- width: parent.width
+ width: page.width
text: catalog.i18nc("@title:tab", "Plugins")
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
+ renderType: Text.NativeRendering
}
Rectangle
{
- visible: toolbox.pluginsInstalledModel.items.length > 0
color: "transparent"
width: parent.width
- height: childrenRect.height + 1 * UM.Theme.getSize("default_lining").width
+ height: childrenRect.height + UM.Theme.getSize("default_margin").width
border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width
Column
{
- height: childrenRect.height
anchors
{
top: parent.top
right: parent.right
left: parent.left
- leftMargin: UM.Theme.getSize("default_margin").width
- rightMargin: UM.Theme.getSize("default_margin").width
- topMargin: UM.Theme.getSize("default_lining").width
- bottomMargin: UM.Theme.getSize("default_lining").width
+ margins: UM.Theme.getSize("default_margin").width
}
Repeater
{
@@ -70,32 +66,27 @@ ScrollView
}
Label
{
- visible: toolbox.materialsInstalledModel.items.length > 0
- width: page.width
text: catalog.i18nc("@title:tab", "Materials")
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
+ renderType: Text.NativeRendering
}
+
Rectangle
{
- visible: toolbox.materialsInstalledModel.items.length > 0
color: "transparent"
width: parent.width
- height: childrenRect.height + 1 * UM.Theme.getSize("default_lining").width
+ height: childrenRect.height + UM.Theme.getSize("default_margin").width
border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width
Column
{
- height: Math.max( UM.Theme.getSize("wide_margin").height, childrenRect.height)
anchors
{
top: parent.top
right: parent.right
left: parent.left
- leftMargin: UM.Theme.getSize("default_margin").width
- rightMargin: UM.Theme.getSize("default_margin").width
- topMargin: UM.Theme.getSize("default_lining").width
- bottomMargin: UM.Theme.getSize("default_lining").width
+ margins: UM.Theme.getSize("default_margin").width
}
Repeater
{
diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml
index b16564fdd2..593e024309 100644
--- a/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@@ -51,6 +51,7 @@ Item
wrapMode: Text.WordWrap
font: UM.Theme.getFont("default_bold")
color: pluginInfo.color
+ renderType: Text.NativeRendering
}
Label
{
@@ -60,6 +61,7 @@ Item
width: parent.width
wrapMode: Text.WordWrap
color: pluginInfo.color
+ renderType: Text.NativeRendering
}
}
Column
@@ -88,6 +90,7 @@ Item
onLinkActivated: Qt.openUrlExternally("mailto:" + model.author_email + "?Subject=Cura: " + model.name + " Plugin")
color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining")
linkColor: UM.Theme.getColor("text_link")
+ renderType: Text.NativeRendering
}
Label
@@ -98,6 +101,7 @@ Item
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
+ renderType: Text.NativeRendering
}
}
ToolboxInstalledTileActions
diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml
index 8fd88b1cfd..61af84fbe5 100644
--- a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml
@@ -1,15 +1,18 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
+import Cura 1.1 as Cura
+
Column
{
property bool canUpdate: false
property bool canDowngrade: false
+ property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
width: UM.Theme.getSize("toolbox_action_button").width
spacing: UM.Theme.getSize("narrow_margin").height
@@ -21,6 +24,7 @@ Column
font: UM.Theme.getFont("default")
wrapMode: Text.WordWrap
width: parent.width
+ renderType: Text.NativeRendering
}
ToolboxProgressButton
@@ -30,59 +34,49 @@ Column
readyLabel: catalog.i18nc("@action:button", "Update")
activeLabel: catalog.i18nc("@action:button", "Updating")
completeLabel: catalog.i18nc("@action:button", "Updated")
- readyAction: function()
+ onReadyAction:
{
toolbox.activePackage = model
toolbox.update(model.id)
}
- activeAction: function()
- {
- toolbox.cancelDownload()
- }
+ onActiveAction: toolbox.cancelDownload()
+
// Don't allow installing while another download is running
- enabled: !(toolbox.isDownloading && toolbox.activePackage != model)
+ enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired
opacity: enabled ? 1.0 : 0.5
visible: canUpdate
}
- Button
+ Label
+ {
+ wrapMode: Text.WordWrap
+ text: catalog.i18nc("@label:The string between and is the highlighted link", "Log in is required to update")
+ font: UM.Theme.getFont("default")
+ color: UM.Theme.getColor("text")
+ linkColor: UM.Theme.getColor("text_link")
+ visible: loginRequired
+ width: updateButton.width
+ renderType: Text.NativeRendering
+
+ MouseArea
+ {
+ anchors.fill: parent
+ onClicked: Cura.API.account.login()
+ }
+ }
+
+ Cura.SecondaryButton
{
id: removeButton
text: canDowngrade ? catalog.i18nc("@action:button", "Downgrade") : catalog.i18nc("@action:button", "Uninstall")
visible: !model.is_bundled && model.is_installed
enabled: !toolbox.isDownloading
- style: ButtonStyle
- {
- background: Rectangle
- {
- implicitWidth: UM.Theme.getSize("toolbox_action_button").width
- implicitHeight: UM.Theme.getSize("toolbox_action_button").height
- color: "transparent"
- border
- {
- width: UM.Theme.getSize("default_lining").width
- color:
- {
- if (control.hovered)
- {
- return UM.Theme.getColor("primary_hover")
- }
- else
- {
- return UM.Theme.getColor("lining")
- }
- }
- }
- }
- label: Label
- {
- text: control.text
- color: UM.Theme.getColor("text")
- verticalAlignment: Text.AlignVCenter
- horizontalAlignment: Text.AlignHCenter
- font: UM.Theme.getFont("default")
- }
- }
+
+ width: UM.Theme.getSize("toolbox_action_button").width
+ height: UM.Theme.getSize("toolbox_action_button").height
+
+ fixedWidthMode: true
+
onClicked: toolbox.checkPackageUsageAndUninstall(model.id)
Connections
{
diff --git a/plugins/Toolbox/resources/qml/ToolboxLicenseDialog.qml b/plugins/Toolbox/resources/qml/ToolboxLicenseDialog.qml
index b8baf7bc83..40b22c268d 100644
--- a/plugins/Toolbox/resources/qml/ToolboxLicenseDialog.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxLicenseDialog.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.2
+import QtQuick 2.10
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
@@ -32,6 +32,7 @@ UM.Dialog
anchors.right: parent.right
text: licenseDialog.pluginName + catalog.i18nc("@label", "This plugin contains a license.\nYou need to accept this license to install this plugin.\nDo you agree with the terms below?")
wrapMode: Text.Wrap
+ renderType: Text.NativeRendering
}
TextArea
{
diff --git a/plugins/Toolbox/resources/qml/ToolboxLoadingPage.qml b/plugins/Toolbox/resources/qml/ToolboxLoadingPage.qml
index 1ba271dcab..025239bd43 100644
--- a/plugins/Toolbox/resources/qml/ToolboxLoadingPage.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxLoadingPage.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
@@ -18,5 +18,6 @@ Rectangle
{
centerIn: parent
}
+ renderType: Text.NativeRendering
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml b/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml
index 2744e40ec9..933e3a5900 100644
--- a/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml
@@ -5,6 +5,7 @@ import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
+import Cura 1.0 as Cura
Item
@@ -18,16 +19,19 @@ Item
property var activeLabel: catalog.i18nc("@action:button", "Cancel")
property var completeLabel: catalog.i18nc("@action:button", "Installed")
- property var readyAction: null // Action when button is ready and clicked (likely install)
- property var activeAction: null // Action when button is active and clicked (likely cancel)
- property var completeAction: null // Action when button is complete and clicked (likely go to installed)
+ signal readyAction() // Action when button is ready and clicked (likely install)
+ signal activeAction() // Action when button is active and clicked (likely cancel)
+ signal completeAction() // Action when button is complete and clicked (likely go to installed)
width: UM.Theme.getSize("toolbox_action_button").width
height: UM.Theme.getSize("toolbox_action_button").height
- Button
+ Cura.PrimaryButton
{
id: button
+ width: UM.Theme.getSize("toolbox_action_button").width
+ height: UM.Theme.getSize("toolbox_action_button").height
+ fixedWidthMode: true
text:
{
if (complete)
@@ -47,101 +51,15 @@ Item
{
if (complete)
{
- return completeAction()
+ completeAction()
}
else if (active)
{
- return activeAction()
+ activeAction()
}
else
{
- return readyAction()
- }
- }
- style: ButtonStyle
- {
- background: Rectangle
- {
- implicitWidth: UM.Theme.getSize("toolbox_action_button").width
- implicitHeight: UM.Theme.getSize("toolbox_action_button").height
- color:
- {
- if (base.complete)
- {
- return "transparent"
- }
- else
- {
- if (control.hovered)
- {
- return UM.Theme.getColor("primary_hover")
- }
- else
- {
- return UM.Theme.getColor("primary")
- }
- }
- }
- border
- {
- width:
- {
- if (base.complete)
- {
- UM.Theme.getSize("default_lining").width
- }
- else
- {
- return 0
- }
- }
- color:
- {
- if (control.hovered)
- {
- return UM.Theme.getColor("primary_hover")
- }
- else
- {
- return UM.Theme.getColor("lining")
- }
- }
- }
- }
- label: Label
- {
- text: control.text
- color:
- {
- if (base.complete)
- {
- return UM.Theme.getColor("text")
- }
- else
- {
- if (control.hovered)
- {
- return UM.Theme.getColor("button_text_hover")
- }
- else
- {
- return UM.Theme.getColor("button_text")
- }
- }
- }
- verticalAlignment: Text.AlignVCenter
- horizontalAlignment: Text.AlignHCenter
- font:
- {
- if (base.complete)
- {
- return UM.Theme.getFont("default")
- }
- else
- {
- return UM.Theme.getFont("default_bold")
- }
- }
+ readyAction()
}
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxTabButton.qml b/plugins/Toolbox/resources/qml/ToolboxTabButton.qml
index b671d779f8..5e1aeaa636 100644
--- a/plugins/Toolbox/resources/qml/ToolboxTabButton.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxTabButton.qml
@@ -1,51 +1,51 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.2
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
+import QtQuick 2.10
+import QtQuick.Controls 2.3
import UM 1.1 as UM
Button
{
+ id: control
property bool active: false
- style: ButtonStyle
+ hoverEnabled: true
+
+ background: Item
{
- background: Rectangle
+ implicitWidth: UM.Theme.getSize("toolbox_header_tab").width
+ implicitHeight: UM.Theme.getSize("toolbox_header_tab").height
+ Rectangle
{
- color: "transparent"
- implicitWidth: UM.Theme.getSize("toolbox_header_tab").width
- implicitHeight: UM.Theme.getSize("toolbox_header_tab").height
- Rectangle
- {
- visible: control.active
- color: UM.Theme.getColor("toolbox_header_highlight_hover")
- anchors.bottom: parent.bottom
- width: parent.width
- height: UM.Theme.getSize("toolbox_header_highlight").height
- }
- }
- label: Label
- {
- text: control.text
- color:
- {
- if(control.hovered)
- {
- return UM.Theme.getColor("toolbox_header_button_text_hovered");
- }
- if(control.active)
- {
- return UM.Theme.getColor("toolbox_header_button_text_active");
- }
- else
- {
- return UM.Theme.getColor("toolbox_header_button_text_inactive");
- }
- }
- font: control.enabled ? (control.active ? UM.Theme.getFont("medium_bold") : UM.Theme.getFont("medium")) : UM.Theme.getFont("default_italic")
- verticalAlignment: Text.AlignVCenter
- horizontalAlignment: Text.AlignHCenter
+ visible: control.active
+ color: UM.Theme.getColor("primary")
+ anchors.bottom: parent.bottom
+ width: parent.width
+ height: UM.Theme.getSize("toolbox_header_highlight").height
}
}
-}
+ contentItem: Label
+ {
+ id: label
+ text: control.text
+ color:
+ {
+ if(control.hovered)
+ {
+ return UM.Theme.getColor("toolbox_header_button_text_hovered");
+ }
+ if(control.active)
+ {
+ return UM.Theme.getColor("toolbox_header_button_text_active");
+ }
+ else
+ {
+ return UM.Theme.getColor("toolbox_header_button_text_inactive");
+ }
+ }
+ font: control.enabled ? (control.active ? UM.Theme.getFont("medium_bold") : UM.Theme.getFont("medium")) : UM.Theme.getFont("default_italic")
+ verticalAlignment: Text.AlignVCenter
+ horizontalAlignment: Text.AlignHCenter
+ renderType: Text.NativeRendering
+ }
+}
\ No newline at end of file
diff --git a/plugins/Toolbox/src/AuthorsModel.py b/plugins/Toolbox/src/AuthorsModel.py
index bea3893504..877f8256ee 100644
--- a/plugins/Toolbox/src/AuthorsModel.py
+++ b/plugins/Toolbox/src/AuthorsModel.py
@@ -2,18 +2,19 @@
# Cura is released under the terms of the LGPLv3 or higher.
import re
-from typing import Dict
+from typing import Dict, List, Optional, Union
from PyQt5.QtCore import Qt, pyqtProperty, pyqtSignal
from UM.Qt.ListModel import ListModel
+
## Model that holds cura packages. By setting the filter property the instances held by this model can be changed.
class AuthorsModel(ListModel):
- def __init__(self, parent = None):
+ def __init__(self, parent = None) -> None:
super().__init__(parent)
- self._metadata = None
+ self._metadata = None # type: Optional[List[Dict[str, Union[str, List[str], int]]]]
self.addRoleName(Qt.UserRole + 1, "id")
self.addRoleName(Qt.UserRole + 2, "name")
@@ -25,39 +26,40 @@ class AuthorsModel(ListModel):
self.addRoleName(Qt.UserRole + 8, "description")
# List of filters for queries. The result is the union of the each list of results.
- self._filter = {} # type: Dict[str,str]
+ self._filter = {} # type: Dict[str, str]
- def setMetadata(self, data):
- self._metadata = data
- self._update()
+ def setMetadata(self, data: List[Dict[str, Union[str, List[str], int]]]):
+ if self._metadata != data:
+ self._metadata = data
+ self._update()
- def _update(self):
- items = []
+ def _update(self) -> None:
+ items = [] # type: List[Dict[str, Union[str, List[str], int, None]]]
if not self._metadata:
- self.setItems([])
+ self.setItems(items)
return
for author in self._metadata:
items.append({
- "id": author["author_id"],
- "name": author["display_name"],
- "email": author["email"] if "email" in author else None,
- "website": author["website"],
- "package_count": author["package_count"] if "package_count" in author else 0,
- "package_types": author["package_types"] if "package_types" in author else [],
- "icon_url": author["icon_url"] if "icon_url" in author else None,
- "description": "Material and quality profiles from {author_name}".format(author_name = author["display_name"])
+ "id": author.get("author_id"),
+ "name": author.get("display_name"),
+ "email": author.get("email"),
+ "website": author.get("website"),
+ "package_count": author.get("package_count", 0),
+ "package_types": author.get("package_types", []),
+ "icon_url": author.get("icon_url"),
+ "description": "Material and quality profiles from {author_name}".format(author_name = author.get("display_name", ""))
})
# Filter on all the key-word arguments.
for key, value in self._filter.items():
if key is "package_types":
- key_filter = lambda item, value = value: value in item["package_types"]
+ key_filter = lambda item, value = value: value in item["package_types"] # type: ignore
elif "*" in value:
- key_filter = lambda item, key = key, value = value: self._matchRegExp(item, key, value)
+ key_filter = lambda item, key = key, value = value: self._matchRegExp(item, key, value) # type: ignore
else:
- key_filter = lambda item, key = key, value = value: self._matchString(item, key, value)
- items = filter(key_filter, items)
+ key_filter = lambda item, key = key, value = value: self._matchString(item, key, value) # type: ignore
+ items = filter(key_filter, items) # type: ignore
# Execute all filters.
filtered_items = list(items)
diff --git a/plugins/Toolbox/src/PackagesModel.py b/plugins/Toolbox/src/PackagesModel.py
index a31facf75a..bcc02955a2 100644
--- a/plugins/Toolbox/src/PackagesModel.py
+++ b/plugins/Toolbox/src/PackagesModel.py
@@ -33,20 +33,22 @@ class PackagesModel(ListModel):
self.addRoleName(Qt.UserRole + 12, "last_updated")
self.addRoleName(Qt.UserRole + 13, "is_bundled")
self.addRoleName(Qt.UserRole + 14, "is_active")
- self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed
+ self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed
self.addRoleName(Qt.UserRole + 16, "has_configs")
self.addRoleName(Qt.UserRole + 17, "supported_configs")
self.addRoleName(Qt.UserRole + 18, "download_count")
self.addRoleName(Qt.UserRole + 19, "tags")
self.addRoleName(Qt.UserRole + 20, "links")
self.addRoleName(Qt.UserRole + 21, "website")
+ self.addRoleName(Qt.UserRole + 22, "login_required")
# List of filters for queries. The result is the union of the each list of results.
self._filter = {} # type: Dict[str, str]
def setMetadata(self, data):
- self._metadata = data
- self._update()
+ if self._metadata != data:
+ self._metadata = data
+ self._update()
def _update(self):
items = []
@@ -99,6 +101,7 @@ class PackagesModel(ListModel):
"tags": package["tags"] if "tags" in package else [],
"links": links_dict,
"website": package["website"] if "website" in package else None,
+ "login_required": "login-required" in package.get("tags", [])
})
# Filter on all the key-word arguments.
diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py
index 562a964f01..b3c0277658 100644
--- a/plugins/Toolbox/src/Toolbox.py
+++ b/plugins/Toolbox/src/Toolbox.py
@@ -18,6 +18,7 @@ from UM.i18n import i18nCatalog
from UM.Version import Version
import cura
+from cura import CuraConstants
from cura.CuraApplication import CuraApplication
from .AuthorsModel import AuthorsModel
@@ -31,17 +32,17 @@ 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
+ 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 = CuraConstants.CuraSDKVersion # type: Union[str, int]
+ self._cloud_api_version = CuraConstants.CuraCloudAPIVersion # type: int
+ self._cloud_api_root = CuraConstants.CuraCloudAPIRoot # type: str
self._api_url = None # type: Optional[str]
# Network:
@@ -66,31 +67,26 @@ class Toolbox(QObject, Extension):
self._old_plugin_ids = set() # type: Set[str]
self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]]
- # Data:
- self._metadata = {
+ # The responses as given by the server parsed to a list.
+ self._server_response_data = {
"authors": [],
- "packages": [],
- "plugins_showcase": [],
- "plugins_available": [],
- "plugins_installed": [],
- "materials_showcase": [],
- "materials_available": [],
- "materials_installed": [],
- "materials_generic": []
+ "packages": []
} # type: Dict[str, List[Any]]
# Models:
self._models = {
"authors": AuthorsModel(self),
"packages": PackagesModel(self),
- "plugins_showcase": PackagesModel(self),
- "plugins_available": PackagesModel(self),
- "plugins_installed": PackagesModel(self),
- "materials_showcase": AuthorsModel(self),
- "materials_available": AuthorsModel(self),
- "materials_installed": PackagesModel(self),
- "materials_generic": PackagesModel(self)
- } # type: Dict[str, ListModel]
+ } # type: Dict[str, Union[AuthorsModel, PackagesModel]]
+
+ self._plugins_showcase_model = PackagesModel(self)
+ self._plugins_available_model = PackagesModel(self)
+ self._plugins_installed_model = PackagesModel(self)
+
+ self._materials_showcase_model = AuthorsModel(self)
+ self._materials_available_model = AuthorsModel(self)
+ self._materials_installed_model = PackagesModel(self)
+ self._materials_generic_model = PackagesModel(self)
# These properties are for keeping track of the UI state:
# ----------------------------------------------------------------------
@@ -168,9 +164,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,
@@ -178,44 +171,9 @@ class Toolbox(QObject, Extension):
)
self._request_urls = {
"authors": QUrl("{base_url}/authors".format(base_url = self._api_url)),
- "packages": QUrl("{base_url}/packages".format(base_url = self._api_url)),
- "plugins_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)),
- "plugins_available": QUrl("{base_url}/packages?package_type=plugin".format(base_url = self._api_url)),
- "materials_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)),
- "materials_available": QUrl("{base_url}/packages?package_type=material".format(base_url = self._api_url)),
- "materials_generic": QUrl("{base_url}/packages?package_type=material&tags=generic".format(base_url = self._api_url))
+ "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:
@@ -231,12 +189,6 @@ class Toolbox(QObject, Extension):
# Make remote requests:
self._makeRequestByType("packages")
self._makeRequestByType("authors")
- # TODO: Uncomment in the future when the tag-filtered api calls work in the cloud server
- # self._makeRequestByType("plugins_showcase")
- # self._makeRequestByType("plugins_available")
- # self._makeRequestByType("materials_showcase")
- # self._makeRequestByType("materials_available")
- # self._makeRequestByType("materials_generic")
# Gather installed packages:
self._updateInstalledModels()
@@ -281,7 +233,7 @@ class Toolbox(QObject, Extension):
"description": plugin_data["plugin"]["description"]
}
return formatted
- except:
+ except KeyError:
Logger.log("w", "Unable to convert plugin meta data %s", str(plugin_data))
return None
@@ -319,13 +271,10 @@ class Toolbox(QObject, Extension):
if plugin_id not in all_plugin_package_ids)
self._old_plugin_metadata = {k: v for k, v in self._old_plugin_metadata.items() if k in self._old_plugin_ids}
- self._metadata["plugins_installed"] = all_packages["plugin"] + list(self._old_plugin_metadata.values())
- self._models["plugins_installed"].setMetadata(self._metadata["plugins_installed"])
+ self._plugins_installed_model.setMetadata(all_packages["plugin"] + list(self._old_plugin_metadata.values()))
self.metadataChanged.emit()
if "material" in all_packages:
- self._metadata["materials_installed"] = all_packages["material"]
- # TODO: ADD MATERIALS HERE ONCE MATERIALS PORTION OF TOOLBOX IS LIVE
- self._models["materials_installed"].setMetadata(self._metadata["materials_installed"])
+ self._materials_installed_model.setMetadata(all_packages["material"])
self.metadataChanged.emit()
@pyqtSlot(str)
@@ -479,7 +428,7 @@ class Toolbox(QObject, Extension):
def getRemotePackage(self, package_id: str) -> Optional[Dict]:
# TODO: make the lookup in a dict, not a loop. canUpdate is called for every item.
remote_package = None
- for package in self._metadata["packages"]:
+ for package in self._server_response_data["packages"]:
if package["package_id"] == package_id:
remote_package = package
break
@@ -491,11 +440,8 @@ class Toolbox(QObject, Extension):
def canUpdate(self, package_id: str) -> bool:
local_package = self._package_manager.getInstalledPackageInfo(package_id)
if local_package is None:
- Logger.log("i", "Could not find package [%s] as installed in the package manager, fall back to check the old plugins",
- package_id)
local_package = self.getOldPluginPackageMetadata(package_id)
if local_package is None:
- Logger.log("i", "Could not find package [%s] in the old plugins", package_id)
return False
remote_package = self.getRemotePackage(package_id)
@@ -545,8 +491,8 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, result = int)
def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int:
count = 0
- for package in self._metadata["materials_installed"]:
- if package["author"]["author_id"] == author_id:
+ for package in self._materials_installed_model.items:
+ if package["author_id"] == author_id:
count += 1
return count
@@ -554,7 +500,7 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, result = int)
def getTotalNumberOfMaterialPackagesByAuthor(self, author_id: str) -> int:
count = 0
- for package in self._metadata["packages"]:
+ for package in self._server_response_data["packages"]:
if package["package_type"] == "material":
if package["author"]["author_id"] == author_id:
count += 1
@@ -568,34 +514,30 @@ class Toolbox(QObject, Extension):
# Check for plugins that were installed with the old plugin browser
def isOldPlugin(self, plugin_id: str) -> bool:
- if plugin_id in self._old_plugin_ids:
- return True
- return False
+ return plugin_id in self._old_plugin_ids
def getOldPluginPackageMetadata(self, plugin_id: str) -> Optional[Dict[str, Any]]:
return self._old_plugin_metadata.get(plugin_id)
- def loadingComplete(self) -> bool:
+ def isLoadingComplete(self) -> bool:
populated = 0
- for list in self._metadata.items():
- if len(list) > 0:
+ for metadata_list in self._server_response_data.items():
+ if metadata_list:
populated += 1
- if populated == len(self._metadata.items()):
- return True
- return False
+ return populated == len(self._server_response_data.items())
# Make API Calls
# --------------------------------------------------------------------------
- def _makeRequestByType(self, type: str) -> None:
- Logger.log("i", "Marketplace: Requesting %s metadata from server.", type)
- request = QNetworkRequest(self._request_urls[type])
+ def _makeRequestByType(self, request_type: str) -> None:
+ Logger.log("i", "Requesting %s metadata from server.", request_type)
+ request = QNetworkRequest(self._request_urls[request_type])
request.setRawHeader(*self._request_header)
if self._network_manager:
self._network_manager.get(request)
@pyqtSlot(str)
def startDownload(self, url: str) -> None:
- Logger.log("i", "Marketplace: Attempting to download & install package from %s.", url)
+ Logger.log("i", "Attempting to download & install package from %s.", url)
url = QUrl(url)
self._download_request = QNetworkRequest(url)
if hasattr(QNetworkRequest, "FollowRedirectsAttribute"):
@@ -612,15 +554,15 @@ class Toolbox(QObject, Extension):
@pyqtSlot()
def cancelDownload(self) -> None:
- Logger.log("i", "Marketplace: User cancelled the download of a package.")
+ Logger.log("i", "User cancelled the download of a package.")
self.resetDownload()
def resetDownload(self) -> None:
if self._download_reply:
try:
self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
- except TypeError: #Raised when the method is not connected to the signal yet.
- pass #Don't need to disconnect.
+ except TypeError: # Raised when the method is not connected to the signal yet.
+ pass # Don't need to disconnect.
self._download_reply.abort()
self._download_reply = None
self._download_request = None
@@ -646,22 +588,8 @@ class Toolbox(QObject, Extension):
self.resetDownload()
return
- # HACK: These request are not handled independently at this moment, but together from the "packages" call
- do_not_handle = [
- "materials_available",
- "materials_showcase",
- "materials_generic",
- "plugins_available",
- "plugins_showcase",
- ]
-
if reply.operation() == QNetworkAccessManager.GetOperation:
- for type, url in self._request_urls.items():
-
- # HACK: Do nothing because we'll handle these from the "packages" call
- if type in do_not_handle:
- continue
-
+ for response_type, url in self._request_urls.items():
if reply.url() == url:
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200:
try:
@@ -674,38 +602,32 @@ class Toolbox(QObject, Extension):
return
# Create model and apply metadata:
- if not self._models[type]:
- Logger.log("e", "Could not find the %s model.", type)
+ if not self._models[response_type]:
+ Logger.log("e", "Could not find the %s model.", response_type)
break
- self._metadata[type] = json_data["data"]
- self._models[type].setMetadata(self._metadata[type])
+ self._server_response_data[response_type] = json_data["data"]
+ self._models[response_type].setMetadata(self._server_response_data[response_type])
- # Do some auto filtering
- # TODO: Make multiple API calls in the future to handle this
- if type is "packages":
- self._models[type].setFilter({"type": "plugin"})
- self.buildMaterialsModels()
- self.buildPluginsModels()
- if type is "authors":
- self._models[type].setFilter({"package_types": "material"})
- if type is "materials_generic":
- self._models[type].setFilter({"tags": "generic"})
+ if response_type is "packages":
+ self._models[response_type].setFilter({"type": "plugin"})
+ self.reBuildMaterialsModels()
+ self.reBuildPluginsModels()
+ elif response_type is "authors":
+ self._models[response_type].setFilter({"package_types": "material"})
+ self._models[response_type].setFilter({"tags": "generic"})
self.metadataChanged.emit()
- if self.loadingComplete() is True:
+ if self.isLoadingComplete():
self.setViewPage("overview")
- return
except json.decoder.JSONDecodeError:
- Logger.log("w", "Marketplace: Received invalid JSON for %s.", type)
+ Logger.log("w", "Received invalid JSON for %s.", response_type)
break
else:
self.setViewPage("errored")
self.resetDownload()
- return
-
else:
# Ignore any operation that is not a get operation
pass
@@ -716,7 +638,13 @@ class Toolbox(QObject, Extension):
self.setDownloadProgress(new_progress)
if bytes_sent == bytes_total:
self.setIsDownloading(False)
- cast(QNetworkReply, self._download_reply).downloadProgress.disconnect(self._onDownloadProgress)
+ self._download_reply = cast(QNetworkReply, self._download_reply)
+ self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
+
+ # Check if the download was sucessfull
+ if self._download_reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
+ Logger.log("w", "Failed to download package. The following error was returned: %s", json.loads(bytes(self._download_reply.readAll()).decode("utf-8")))
+ return
# Must not delete the temporary file on Windows
self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False)
file_path = self._temp_plugin_file.name
@@ -726,10 +654,10 @@ class Toolbox(QObject, Extension):
self._onDownloadComplete(file_path)
def _onDownloadComplete(self, file_path: str) -> None:
- Logger.log("i", "Marketplace: Download complete.")
+ Logger.log("i", "Download complete.")
package_info = self._package_manager.getPackageInfo(file_path)
if not package_info:
- Logger.log("w", "Marketplace: Package file [%s] was not a valid CuraPackage.", file_path)
+ Logger.log("w", "Package file [%s] was not a valid CuraPackage.", file_path)
return
license_content = self._package_manager.getPackageLicense(file_path)
@@ -738,7 +666,6 @@ class Toolbox(QObject, Extension):
return
self.install(file_path)
- return
# Getter & Setters for Properties:
# --------------------------------------------------------------------------
@@ -761,8 +688,9 @@ class Toolbox(QObject, Extension):
return self._is_downloading
def setActivePackage(self, package: Dict[str, Any]) -> None:
- self._active_package = package
- self.activePackageChanged.emit()
+ if self._active_package != package:
+ self._active_package = package
+ self.activePackageChanged.emit()
## The active package is the package that is currently being downloaded
@pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged)
@@ -770,16 +698,18 @@ class Toolbox(QObject, Extension):
return self._active_package
def setViewCategory(self, category: str = "plugin") -> None:
- self._view_category = category
- self.viewChanged.emit()
+ if self._view_category != category:
+ self._view_category = category
+ self.viewChanged.emit()
@pyqtProperty(str, fset = setViewCategory, notify = viewChanged)
def viewCategory(self) -> str:
return self._view_category
def setViewPage(self, page: str = "overview") -> None:
- self._view_page = page
- self.viewChanged.emit()
+ if self._view_page != page:
+ self._view_page = page
+ self.viewChanged.emit()
@pyqtProperty(str, fset = setViewPage, notify = viewChanged)
def viewPage(self) -> str:
@@ -787,48 +717,48 @@ class Toolbox(QObject, Extension):
# Exposed Models:
# --------------------------------------------------------------------------
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def authorsModel(self) -> AuthorsModel:
return cast(AuthorsModel, self._models["authors"])
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def packagesModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["packages"])
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def pluginsShowcaseModel(self) -> PackagesModel:
- return cast(PackagesModel, self._models["plugins_showcase"])
+ return self._plugins_showcase_model
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def pluginsAvailableModel(self) -> PackagesModel:
- return cast(PackagesModel, self._models["plugins_available"])
+ return self._plugins_available_model
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def pluginsInstalledModel(self) -> PackagesModel:
- return cast(PackagesModel, self._models["plugins_installed"])
+ return self._plugins_installed_model
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def materialsShowcaseModel(self) -> AuthorsModel:
- return cast(AuthorsModel, self._models["materials_showcase"])
+ return self._materials_showcase_model
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def materialsAvailableModel(self) -> AuthorsModel:
- return cast(AuthorsModel, self._models["materials_available"])
+ return self._materials_available_model
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def materialsInstalledModel(self) -> PackagesModel:
- return cast(PackagesModel, self._models["materials_installed"])
+ return self._materials_installed_model
- @pyqtProperty(QObject, notify=metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def materialsGenericModel(self) -> PackagesModel:
- return cast(PackagesModel, self._models["materials_generic"])
+ return self._materials_generic_model
# Filter Models:
# --------------------------------------------------------------------------
@pyqtSlot(str, str, str)
def filterModelByProp(self, model_type: str, filter_type: str, parameter: str) -> None:
if not self._models[model_type]:
- Logger.log("w", "Marketplace: Couldn't filter %s model because it doesn't exist.", model_type)
+ Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type)
return
self._models[model_type].setFilter({filter_type: parameter})
self.filterChanged.emit()
@@ -836,7 +766,7 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, "QVariantMap")
def setFilters(self, model_type: str, filter_dict: dict) -> None:
if not self._models[model_type]:
- Logger.log("w", "Marketplace: Couldn't filter %s model because it doesn't exist.", model_type)
+ Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type)
return
self._models[model_type].setFilter(filter_dict)
self.filterChanged.emit()
@@ -844,21 +774,21 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str)
def removeFilters(self, model_type: str) -> None:
if not self._models[model_type]:
- Logger.log("w", "Marketplace: Couldn't remove filters on %s model because it doesn't exist.", model_type)
+ Logger.log("w", "Couldn't remove filters on %s model because it doesn't exist.", model_type)
return
self._models[model_type].setFilter({})
self.filterChanged.emit()
# HACK(S):
# --------------------------------------------------------------------------
- def buildMaterialsModels(self) -> None:
- self._metadata["materials_showcase"] = []
- self._metadata["materials_available"] = []
- self._metadata["materials_generic"] = []
+ def reBuildMaterialsModels(self) -> None:
+ materials_showcase_metadata = []
+ materials_available_metadata = []
+ materials_generic_metadata = []
- processed_authors = [] # type: List[str]
+ processed_authors = [] # type: List[str]
- for item in self._metadata["packages"]:
+ for item in self._server_response_data["packages"]:
if item["package_type"] == "material":
author = item["author"]
@@ -867,30 +797,29 @@ class Toolbox(QObject, Extension):
# Generic materials to be in the same section
if "generic" in item["tags"]:
- self._metadata["materials_generic"].append(item)
+ materials_generic_metadata.append(item)
else:
if "showcase" in item["tags"]:
- self._metadata["materials_showcase"].append(author)
+ materials_showcase_metadata.append(author)
else:
- self._metadata["materials_available"].append(author)
+ materials_available_metadata.append(author)
processed_authors.append(author["author_id"])
- self._models["materials_showcase"].setMetadata(self._metadata["materials_showcase"])
- self._models["materials_available"].setMetadata(self._metadata["materials_available"])
- self._models["materials_generic"].setMetadata(self._metadata["materials_generic"])
+ self._materials_showcase_model.setMetadata(materials_showcase_metadata)
+ self._materials_available_model.setMetadata(materials_available_metadata)
+ self._materials_generic_model.setMetadata(materials_generic_metadata)
- def buildPluginsModels(self) -> None:
- self._metadata["plugins_showcase"] = []
- self._metadata["plugins_available"] = []
+ def reBuildPluginsModels(self) -> None:
+ plugins_showcase_metadata = []
+ plugins_available_metadata = []
- for item in self._metadata["packages"]:
+ for item in self._server_response_data["packages"]:
if item["package_type"] == "plugin":
-
if "showcase" in item["tags"]:
- self._metadata["plugins_showcase"].append(item)
+ plugins_showcase_metadata.append(item)
else:
- self._metadata["plugins_available"].append(item)
+ plugins_available_metadata.append(item)
- self._models["plugins_showcase"].setMetadata(self._metadata["plugins_showcase"])
- self._models["plugins_available"].setMetadata(self._metadata["plugins_available"])
+ self._plugins_showcase_model.setMetadata(plugins_showcase_metadata)
+ self._plugins_available_model.setMetadata(plugins_available_metadata)
diff --git a/resources/bundled_packages/cura.json b/resources/bundled_packages/cura.json
index 384b7d0412..e1c55992f9 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": 5,
+ "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",