mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-19 01:25:55 +08:00
Merge pull request #4960 from Ultimaker/CURA-6005_cura_drive_plugin
[4.0] CURA-6005 Added CuraDrive plugin to Cura build
This commit is contained in:
commit
980953b00c
1
.gitignore
vendored
1
.gitignore
vendored
@ -42,7 +42,6 @@ plugins/cura-siemensnx-plugin
|
|||||||
plugins/CuraBlenderPlugin
|
plugins/CuraBlenderPlugin
|
||||||
plugins/CuraCloudPlugin
|
plugins/CuraCloudPlugin
|
||||||
plugins/CuraDrivePlugin
|
plugins/CuraDrivePlugin
|
||||||
plugins/CuraDrive
|
|
||||||
plugins/CuraLiveScriptingPlugin
|
plugins/CuraLiveScriptingPlugin
|
||||||
plugins/CuraOpenSCADPlugin
|
plugins/CuraOpenSCADPlugin
|
||||||
plugins/CuraPrintProfileCreator
|
plugins/CuraPrintProfileCreator
|
||||||
|
@ -6,6 +6,7 @@ from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
|
|||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
|
from cura import UltimakerCloudAuthentication
|
||||||
|
|
||||||
from cura.OAuth2.AuthorizationService import AuthorizationService
|
from cura.OAuth2.AuthorizationService import AuthorizationService
|
||||||
from cura.OAuth2.Models import OAuth2Settings
|
from cura.OAuth2.Models import OAuth2Settings
|
||||||
@ -37,15 +38,16 @@ class Account(QObject):
|
|||||||
self._logged_in = False
|
self._logged_in = False
|
||||||
|
|
||||||
self._callback_port = 32118
|
self._callback_port = 32118
|
||||||
self._oauth_root = "https://account.ultimaker.com"
|
self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
|
||||||
self._cloud_api_root = "https://api.ultimaker.com"
|
|
||||||
|
|
||||||
self._oauth_settings = OAuth2Settings(
|
self._oauth_settings = OAuth2Settings(
|
||||||
OAUTH_SERVER_URL= self._oauth_root,
|
OAUTH_SERVER_URL= self._oauth_root,
|
||||||
CALLBACK_PORT=self._callback_port,
|
CALLBACK_PORT=self._callback_port,
|
||||||
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
|
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
|
||||||
CLIENT_ID="um----------------------------ultimaker_cura",
|
CLIENT_ID="um----------------------------ultimaker_cura",
|
||||||
CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.write",
|
CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download "
|
||||||
|
"packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write "
|
||||||
|
"cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write",
|
||||||
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
|
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
|
||||||
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
|
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
|
||||||
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
|
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from typing import Tuple, Optional, TYPE_CHECKING
|
from typing import Tuple, Optional, TYPE_CHECKING, Dict, Any
|
||||||
|
|
||||||
from cura.Backups.BackupsManager import BackupsManager
|
from cura.Backups.BackupsManager import BackupsManager
|
||||||
|
|
||||||
@ -24,12 +24,12 @@ class Backups:
|
|||||||
## Create a new back-up using the BackupsManager.
|
## Create a new back-up using the BackupsManager.
|
||||||
# \return Tuple containing a ZIP file with the back-up data and a dict
|
# \return Tuple containing a ZIP file with the back-up data and a dict
|
||||||
# with metadata about the back-up.
|
# with metadata about the back-up.
|
||||||
def createBackup(self) -> Tuple[Optional[bytes], Optional[dict]]:
|
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]:
|
||||||
return self.manager.createBackup()
|
return self.manager.createBackup()
|
||||||
|
|
||||||
## Restore a back-up using the BackupsManager.
|
## Restore a back-up using the BackupsManager.
|
||||||
# \param zip_file A ZIP file containing the actual back-up data.
|
# \param zip_file A ZIP file containing the actual back-up data.
|
||||||
# \param meta_data Some metadata needed for restoring a back-up, like the
|
# \param meta_data Some metadata needed for restoring a back-up, like the
|
||||||
# Cura version number.
|
# Cura version number.
|
||||||
def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
|
def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None:
|
||||||
return self.manager.restoreBackup(zip_file, meta_data)
|
return self.manager.restoreBackup(zip_file, meta_data)
|
||||||
|
36
cura/ApplicationMetadata.py
Normal file
36
cura/ApplicationMetadata.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
# ---------
|
||||||
|
# Genearl constants used in Cura
|
||||||
|
# ---------
|
||||||
|
DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
|
||||||
|
DEFAULT_CURA_VERSION = "master"
|
||||||
|
DEFAULT_CURA_BUILD_TYPE = ""
|
||||||
|
DEFAULT_CURA_DEBUG_MODE = False
|
||||||
|
DEFAULT_CURA_SDK_VERSION = "6.0.0"
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraAppDisplayName # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraVersion # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
CuraVersion = DEFAULT_CURA_VERSION # [CodeStyle: Reflecting imported value]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraBuildType # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
CuraBuildType = DEFAULT_CURA_BUILD_TYPE
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraDebugMode # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
CuraDebugMode = DEFAULT_CURA_DEBUG_MODE
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraSDKVersion # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION
|
@ -117,6 +117,8 @@ from cura.ObjectsModel import ObjectsModel
|
|||||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||||
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
|
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
|
||||||
|
|
||||||
|
from cura import ApplicationMetadata
|
||||||
|
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
|
|
||||||
@ -164,11 +166,11 @@ class CuraApplication(QtApplication):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(name = "cura",
|
super().__init__(name = "cura",
|
||||||
app_display_name = CuraAppDisplayName,
|
app_display_name = ApplicationMetadata.CuraAppDisplayName,
|
||||||
version = CuraVersion,
|
version = ApplicationMetadata.CuraVersion,
|
||||||
api_version = CuraSDKVersion,
|
api_version = ApplicationMetadata.CuraSDKVersion,
|
||||||
buildtype = CuraBuildType,
|
buildtype = ApplicationMetadata.CuraBuildType,
|
||||||
is_debug_mode = CuraDebugMode,
|
is_debug_mode = ApplicationMetadata.CuraDebugMode,
|
||||||
tray_icon_name = "cura-icon-32.png",
|
tray_icon_name = "cura-icon-32.png",
|
||||||
**kwargs)
|
**kwargs)
|
||||||
|
|
||||||
@ -954,7 +956,7 @@ class CuraApplication(QtApplication):
|
|||||||
engine.rootContext().setContextProperty("CuraApplication", self)
|
engine.rootContext().setContextProperty("CuraApplication", self)
|
||||||
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
|
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
|
||||||
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
|
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
|
||||||
engine.rootContext().setContextProperty("CuraSDKVersion", CuraSDKVersion)
|
engine.rootContext().setContextProperty("CuraSDKVersion", ApplicationMetadata.CuraSDKVersion)
|
||||||
|
|
||||||
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
|
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
|
||||||
|
|
||||||
|
@ -8,3 +8,4 @@ CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
|||||||
CuraSDKVersion = "@CURA_SDK_VERSION@"
|
CuraSDKVersion = "@CURA_SDK_VERSION@"
|
||||||
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
||||||
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
||||||
|
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
|
||||||
|
24
cura/UltimakerCloudAuthentication.py
Normal file
24
cura/UltimakerCloudAuthentication.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
# ---------
|
||||||
|
# Constants used for the Cloud API
|
||||||
|
# ---------
|
||||||
|
DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str
|
||||||
|
DEFAULT_CLOUD_API_VERSION = 1 # type: int
|
||||||
|
DEFAULT_CLOUD_ACCOUNT_API_ROOT = "https://account.ultimaker.com" # type: str
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraCloudAPIRoot # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
CuraCloudAPIRoot = DEFAULT_CLOUD_API_ROOT
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraCloudAPIVersion # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
CuraCloudAPIVersion = DEFAULT_CLOUD_API_VERSION
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraCloudAccountAPIRoot # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT
|
12
plugins/CuraDrive/__init__.py
Normal file
12
plugins/CuraDrive/__init__.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from .src.DrivePluginExtension import DrivePluginExtension
|
||||||
|
|
||||||
|
|
||||||
|
def getMetaData():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
return {"extension": DrivePluginExtension()}
|
8
plugins/CuraDrive/plugin.json
Normal file
8
plugins/CuraDrive/plugin.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "Cura Backups",
|
||||||
|
"author": "Ultimaker B.V.",
|
||||||
|
"description": "Backup and restore your configuration.",
|
||||||
|
"version": "1.2.0",
|
||||||
|
"api": 6,
|
||||||
|
"i18n-catalog": "cura"
|
||||||
|
}
|
168
plugins/CuraDrive/src/DriveApiService.py
Normal file
168
plugins/CuraDrive/src/DriveApiService.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
from typing import Any, Optional, List, Dict
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
|
from UM.Signal import Signal, signalemitter
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
from .UploadBackupJob import UploadBackupJob
|
||||||
|
from .Settings import Settings
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
## The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling.
|
||||||
|
@signalemitter
|
||||||
|
class DriveApiService:
|
||||||
|
BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL)
|
||||||
|
|
||||||
|
# Emit signal when restoring backup started or finished.
|
||||||
|
restoringStateChanged = Signal()
|
||||||
|
|
||||||
|
# Emit signal when creating backup started or finished.
|
||||||
|
creatingStateChanged = Signal()
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._cura_api = CuraApplication.getInstance().getCuraAPI()
|
||||||
|
|
||||||
|
def getBackups(self) -> List[Dict[str, Any]]:
|
||||||
|
access_token = self._cura_api.account.accessToken
|
||||||
|
if not access_token:
|
||||||
|
Logger.log("w", "Could not get access token.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
backup_list_request = requests.get(self.BACKUP_URL, headers = {
|
||||||
|
"Authorization": "Bearer {}".format(access_token)
|
||||||
|
})
|
||||||
|
|
||||||
|
# HTTP status 300s mean redirection. 400s and 500s are errors.
|
||||||
|
# Technically 300s are not errors, but the use case here relies on "requests" to handle redirects automatically.
|
||||||
|
if backup_list_request.status_code >= 300:
|
||||||
|
Logger.log("w", "Could not get backups list from remote: %s", backup_list_request.text)
|
||||||
|
Message(catalog.i18nc("@info:backup_status", "There was an error listing your backups."), title = catalog.i18nc("@info:title", "Backup")).show()
|
||||||
|
return []
|
||||||
|
return backup_list_request.json()["data"]
|
||||||
|
|
||||||
|
def createBackup(self) -> None:
|
||||||
|
self.creatingStateChanged.emit(is_creating = True)
|
||||||
|
|
||||||
|
# Create the backup.
|
||||||
|
backup_zip_file, backup_meta_data = self._cura_api.backups.createBackup()
|
||||||
|
if not backup_zip_file or not backup_meta_data:
|
||||||
|
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not create backup.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create an upload entry for the backup.
|
||||||
|
timestamp = datetime.now().isoformat()
|
||||||
|
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
|
||||||
|
backup_upload_url = self._requestBackupUpload(backup_meta_data, len(backup_zip_file))
|
||||||
|
if not backup_upload_url:
|
||||||
|
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not upload backup.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Upload the backup to storage.
|
||||||
|
upload_backup_job = UploadBackupJob(backup_upload_url, backup_zip_file)
|
||||||
|
upload_backup_job.finished.connect(self._onUploadFinished)
|
||||||
|
upload_backup_job.start()
|
||||||
|
|
||||||
|
def _onUploadFinished(self, job: "UploadBackupJob") -> None:
|
||||||
|
if job.backup_upload_error_message != "":
|
||||||
|
# If the job contains an error message we pass it along so the UI can display it.
|
||||||
|
self.creatingStateChanged.emit(is_creating = False, error_message = job.backup_upload_error_message)
|
||||||
|
else:
|
||||||
|
self.creatingStateChanged.emit(is_creating = False)
|
||||||
|
|
||||||
|
def restoreBackup(self, backup: Dict[str, Any]) -> None:
|
||||||
|
self.restoringStateChanged.emit(is_restoring = True)
|
||||||
|
download_url = backup.get("download_url")
|
||||||
|
if not download_url:
|
||||||
|
# If there is no download URL, we can't restore the backup.
|
||||||
|
return self._emitRestoreError()
|
||||||
|
|
||||||
|
download_package = requests.get(download_url, stream = True)
|
||||||
|
if download_package.status_code >= 300:
|
||||||
|
# Something went wrong when attempting to download the backup.
|
||||||
|
Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text)
|
||||||
|
return self._emitRestoreError()
|
||||||
|
|
||||||
|
# We store the file in a temporary path fist to ensure integrity.
|
||||||
|
temporary_backup_file = NamedTemporaryFile(delete = False)
|
||||||
|
with open(temporary_backup_file.name, "wb") as write_backup:
|
||||||
|
for chunk in download_package:
|
||||||
|
write_backup.write(chunk)
|
||||||
|
|
||||||
|
if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash", "")):
|
||||||
|
# Don't restore the backup if the MD5 hashes do not match.
|
||||||
|
# This can happen if the download was interrupted.
|
||||||
|
Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.")
|
||||||
|
return self._emitRestoreError()
|
||||||
|
|
||||||
|
# Tell Cura to place the backup back in the user data folder.
|
||||||
|
with open(temporary_backup_file.name, "rb") as read_backup:
|
||||||
|
self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("metadata", {}))
|
||||||
|
self.restoringStateChanged.emit(is_restoring = False)
|
||||||
|
|
||||||
|
def _emitRestoreError(self) -> None:
|
||||||
|
self.restoringStateChanged.emit(is_restoring = False,
|
||||||
|
error_message = catalog.i18nc("@info:backup_status",
|
||||||
|
"There was an error trying to restore your backup."))
|
||||||
|
|
||||||
|
# Verify the MD5 hash of a file.
|
||||||
|
# \param file_path Full path to the file.
|
||||||
|
# \param known_hash The known MD5 hash of the file.
|
||||||
|
# \return: Success or not.
|
||||||
|
@staticmethod
|
||||||
|
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
|
||||||
|
with open(file_path, "rb") as read_backup:
|
||||||
|
local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8")
|
||||||
|
return known_hash == local_md5_hash
|
||||||
|
|
||||||
|
def deleteBackup(self, backup_id: str) -> bool:
|
||||||
|
access_token = self._cura_api.account.accessToken
|
||||||
|
if not access_token:
|
||||||
|
Logger.log("w", "Could not get access token.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
|
||||||
|
"Authorization": "Bearer {}".format(access_token)
|
||||||
|
})
|
||||||
|
if delete_backup.status_code >= 300:
|
||||||
|
Logger.log("w", "Could not delete backup: %s", delete_backup.text)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Request a backup upload slot from the API.
|
||||||
|
# \param backup_metadata: A dict containing some meta data about the backup.
|
||||||
|
# \param backup_size The size of the backup file in bytes.
|
||||||
|
# \return: The upload URL for the actual backup file if successful, otherwise None.
|
||||||
|
def _requestBackupUpload(self, backup_metadata: Dict[str, Any], backup_size: int) -> Optional[str]:
|
||||||
|
access_token = self._cura_api.account.accessToken
|
||||||
|
if not access_token:
|
||||||
|
Logger.log("w", "Could not get access token.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
backup_upload_request = requests.put(self.BACKUP_URL, json = {
|
||||||
|
"data": {
|
||||||
|
"backup_size": backup_size,
|
||||||
|
"metadata": backup_metadata
|
||||||
|
}
|
||||||
|
}, headers = {
|
||||||
|
"Authorization": "Bearer {}".format(access_token)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Any status code of 300 or above indicates an error.
|
||||||
|
if backup_upload_request.status_code >= 300:
|
||||||
|
Logger.log("w", "Could not request backup upload: %s", backup_upload_request.text)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return backup_upload_request.json()["data"]["upload_url"]
|
162
plugins/CuraDrive/src/DrivePluginExtension.py
Normal file
162
plugins/CuraDrive/src/DrivePluginExtension.py
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||||
|
|
||||||
|
from UM.Extension import Extension
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
from .Settings import Settings
|
||||||
|
from .DriveApiService import DriveApiService
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
# The DivePluginExtension provides functionality to backup and restore your Cura configuration to Ultimaker's cloud.
|
||||||
|
class DrivePluginExtension(QObject, Extension):
|
||||||
|
|
||||||
|
# Signal emitted when the list of backups changed.
|
||||||
|
backupsChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# Signal emitted when restoring has started. Needed to prevent parallel restoring.
|
||||||
|
restoringStateChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# Signal emitted when creating has started. Needed to prevent parallel creation of backups.
|
||||||
|
creatingStateChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# Signal emitted when preferences changed (like auto-backup).
|
||||||
|
preferencesChanged = pyqtSignal()
|
||||||
|
|
||||||
|
DATE_FORMAT = "%d/%m/%Y %H:%M:%S"
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
QObject.__init__(self, None)
|
||||||
|
Extension.__init__(self)
|
||||||
|
|
||||||
|
# Local data caching for the UI.
|
||||||
|
self._drive_window = None # type: Optional[QObject]
|
||||||
|
self._backups = [] # type: List[Dict[str, Any]]
|
||||||
|
self._is_restoring_backup = False
|
||||||
|
self._is_creating_backup = False
|
||||||
|
|
||||||
|
# Initialize services.
|
||||||
|
preferences = CuraApplication.getInstance().getPreferences()
|
||||||
|
self._drive_api_service = DriveApiService()
|
||||||
|
|
||||||
|
# Attach signals.
|
||||||
|
CuraApplication.getInstance().getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged)
|
||||||
|
self._drive_api_service.restoringStateChanged.connect(self._onRestoringStateChanged)
|
||||||
|
self._drive_api_service.creatingStateChanged.connect(self._onCreatingStateChanged)
|
||||||
|
|
||||||
|
# Register preferences.
|
||||||
|
preferences.addPreference(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, False)
|
||||||
|
preferences.addPreference(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY,
|
||||||
|
datetime.now().strftime(self.DATE_FORMAT))
|
||||||
|
|
||||||
|
# Register the menu item
|
||||||
|
self.addMenuItem(catalog.i18nc("@item:inmenu", "Manage backups"), self.showDriveWindow)
|
||||||
|
|
||||||
|
# Make auto-backup on boot if required.
|
||||||
|
CuraApplication.getInstance().engineCreatedSignal.connect(self._autoBackup)
|
||||||
|
|
||||||
|
def showDriveWindow(self) -> None:
|
||||||
|
if not self._drive_window:
|
||||||
|
plugin_dir_path = CuraApplication.getInstance().getPluginRegistry().getPluginPath("CuraDrive")
|
||||||
|
path = os.path.join(plugin_dir_path, "src", "qml", "main.qml")
|
||||||
|
self._drive_window = CuraApplication.getInstance().createQmlComponent(path, {"CuraDrive": self})
|
||||||
|
self.refreshBackups()
|
||||||
|
if self._drive_window:
|
||||||
|
self._drive_window.show()
|
||||||
|
|
||||||
|
def _autoBackup(self) -> None:
|
||||||
|
preferences = CuraApplication.getInstance().getPreferences()
|
||||||
|
if preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._isLastBackupTooLongAgo():
|
||||||
|
self.createBackup()
|
||||||
|
|
||||||
|
def _isLastBackupTooLongAgo(self) -> bool:
|
||||||
|
current_date = datetime.now()
|
||||||
|
last_backup_date = self._getLastBackupDate()
|
||||||
|
date_diff = current_date - last_backup_date
|
||||||
|
return date_diff.days > 1
|
||||||
|
|
||||||
|
def _getLastBackupDate(self) -> "datetime":
|
||||||
|
preferences = CuraApplication.getInstance().getPreferences()
|
||||||
|
last_backup_date = preferences.getValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY)
|
||||||
|
return datetime.strptime(last_backup_date, self.DATE_FORMAT)
|
||||||
|
|
||||||
|
def _storeBackupDate(self) -> None:
|
||||||
|
backup_date = datetime.now().strftime(self.DATE_FORMAT)
|
||||||
|
preferences = CuraApplication.getInstance().getPreferences()
|
||||||
|
preferences.setValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, backup_date)
|
||||||
|
|
||||||
|
def _onLoginStateChanged(self, logged_in: bool = False) -> None:
|
||||||
|
if logged_in:
|
||||||
|
self.refreshBackups()
|
||||||
|
|
||||||
|
def _onRestoringStateChanged(self, is_restoring: bool = False, error_message: str = None) -> None:
|
||||||
|
self._is_restoring_backup = is_restoring
|
||||||
|
self.restoringStateChanged.emit()
|
||||||
|
if error_message:
|
||||||
|
Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show()
|
||||||
|
|
||||||
|
def _onCreatingStateChanged(self, is_creating: bool = False, error_message: str = None) -> None:
|
||||||
|
self._is_creating_backup = is_creating
|
||||||
|
self.creatingStateChanged.emit()
|
||||||
|
if error_message:
|
||||||
|
Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show()
|
||||||
|
else:
|
||||||
|
self._storeBackupDate()
|
||||||
|
if not is_creating and not error_message:
|
||||||
|
# We've finished creating a new backup, to the list has to be updated.
|
||||||
|
self.refreshBackups()
|
||||||
|
|
||||||
|
@pyqtSlot(bool, name = "toggleAutoBackup")
|
||||||
|
def toggleAutoBackup(self, enabled: bool) -> None:
|
||||||
|
preferences = CuraApplication.getInstance().getPreferences()
|
||||||
|
preferences.setValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, enabled)
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = preferencesChanged)
|
||||||
|
def autoBackupEnabled(self) -> bool:
|
||||||
|
preferences = CuraApplication.getInstance().getPreferences()
|
||||||
|
return bool(preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY))
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify = backupsChanged)
|
||||||
|
def backups(self) -> List[Dict[str, Any]]:
|
||||||
|
return self._backups
|
||||||
|
|
||||||
|
@pyqtSlot(name = "refreshBackups")
|
||||||
|
def refreshBackups(self) -> None:
|
||||||
|
self._backups = self._drive_api_service.getBackups()
|
||||||
|
self.backupsChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = restoringStateChanged)
|
||||||
|
def isRestoringBackup(self) -> bool:
|
||||||
|
return self._is_restoring_backup
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = creatingStateChanged)
|
||||||
|
def isCreatingBackup(self) -> bool:
|
||||||
|
return self._is_creating_backup
|
||||||
|
|
||||||
|
@pyqtSlot(str, name = "restoreBackup")
|
||||||
|
def restoreBackup(self, backup_id: str) -> None:
|
||||||
|
for backup in self._backups:
|
||||||
|
if backup.get("backup_id") == backup_id:
|
||||||
|
self._drive_api_service.restoreBackup(backup)
|
||||||
|
return
|
||||||
|
Logger.log("w", "Unable to find backup with the ID %s", backup_id)
|
||||||
|
|
||||||
|
@pyqtSlot(name = "createBackup")
|
||||||
|
def createBackup(self) -> None:
|
||||||
|
self._drive_api_service.createBackup()
|
||||||
|
|
||||||
|
@pyqtSlot(str, name = "deleteBackup")
|
||||||
|
def deleteBackup(self, backup_id: str) -> None:
|
||||||
|
self._drive_api_service.deleteBackup(backup_id)
|
||||||
|
self.refreshBackups()
|
13
plugins/CuraDrive/src/Settings.py
Normal file
13
plugins/CuraDrive/src/Settings.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from cura import UltimakerCloudAuthentication
|
||||||
|
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
# Keeps the plugin settings.
|
||||||
|
DRIVE_API_VERSION = 1
|
||||||
|
DRIVE_API_URL = "{}/cura-drive/v{}".format(UltimakerCloudAuthentication.CuraCloudAPIRoot, str(DRIVE_API_VERSION))
|
||||||
|
|
||||||
|
AUTO_BACKUP_ENABLED_PREFERENCE_KEY = "cura_drive/auto_backup_enabled"
|
||||||
|
AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY = "cura_drive/auto_backup_date"
|
41
plugins/CuraDrive/src/UploadBackupJob.py
Normal file
41
plugins/CuraDrive/src/UploadBackupJob.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from UM.Job import Job
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
class UploadBackupJob(Job):
|
||||||
|
MESSAGE_TITLE = catalog.i18nc("@info:title", "Backups")
|
||||||
|
|
||||||
|
# This job is responsible for uploading the backup file to cloud storage.
|
||||||
|
# As it can take longer than some other tasks, we schedule this using a Cura Job.
|
||||||
|
def __init__(self, signed_upload_url: str, backup_zip: bytes) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._signed_upload_url = signed_upload_url
|
||||||
|
self._backup_zip = backup_zip
|
||||||
|
self._upload_success = False
|
||||||
|
self.backup_upload_error_message = ""
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
upload_message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1)
|
||||||
|
upload_message.show()
|
||||||
|
|
||||||
|
backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip)
|
||||||
|
upload_message.hide()
|
||||||
|
|
||||||
|
if backup_upload.status_code >= 300:
|
||||||
|
self.backup_upload_error_message = backup_upload.text
|
||||||
|
Logger.log("w", "Could not upload backup file: %s", backup_upload.text)
|
||||||
|
Message(catalog.i18nc("@info:backup_status", "There was an error while uploading your backup."), title = self.MESSAGE_TITLE).show()
|
||||||
|
else:
|
||||||
|
self._upload_success = True
|
||||||
|
Message(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."), title = self.MESSAGE_TITLE).show()
|
||||||
|
|
||||||
|
self.finished.emit(self)
|
0
plugins/CuraDrive/src/__init__.py
Normal file
0
plugins/CuraDrive/src/__init__.py
Normal file
37
plugins/CuraDrive/src/qml/components/BackupList.qml
Normal file
37
plugins/CuraDrive/src/qml/components/BackupList.qml
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
|
import UM 1.1 as UM
|
||||||
|
|
||||||
|
ScrollView
|
||||||
|
{
|
||||||
|
property alias model: backupList.model
|
||||||
|
width: parent.width
|
||||||
|
ListView
|
||||||
|
{
|
||||||
|
id: backupList
|
||||||
|
width: parent.width
|
||||||
|
delegate: Item
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
|
||||||
|
BackupListItem
|
||||||
|
{
|
||||||
|
id: backupListItem
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle
|
||||||
|
{
|
||||||
|
id: divider
|
||||||
|
color: UM.Theme.getColor("lining")
|
||||||
|
height: UM.Theme.getSize("default_lining").height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
plugins/CuraDrive/src/qml/components/BackupListFooter.qml
Normal file
46
plugins/CuraDrive/src/qml/components/BackupListFooter.qml
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
|
import UM 1.3 as UM
|
||||||
|
import Cura 1.0 as Cura
|
||||||
|
|
||||||
|
import "../components"
|
||||||
|
|
||||||
|
RowLayout
|
||||||
|
{
|
||||||
|
id: backupListFooter
|
||||||
|
width: parent.width
|
||||||
|
property bool showInfoButton: false
|
||||||
|
|
||||||
|
Cura.PrimaryButton
|
||||||
|
{
|
||||||
|
id: infoButton
|
||||||
|
text: catalog.i18nc("@button", "Want more?")
|
||||||
|
iconSource: UM.Theme.getIcon("info")
|
||||||
|
onClicked: Qt.openUrlExternally("https://goo.gl/forms/QACEP8pP3RV60QYG2")
|
||||||
|
visible: backupListFooter.showInfoButton
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.PrimaryButton
|
||||||
|
{
|
||||||
|
id: createBackupButton
|
||||||
|
text: catalog.i18nc("@button", "Backup Now")
|
||||||
|
iconSource: UM.Theme.getIcon("plus")
|
||||||
|
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup && !backupListFooter.showInfoButton
|
||||||
|
onClicked: CuraDrive.createBackup()
|
||||||
|
busy: CuraDrive.isCreatingBackup
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.CheckBoxWithTooltip
|
||||||
|
{
|
||||||
|
id: autoBackupEnabled
|
||||||
|
checked: CuraDrive.autoBackupEnabled
|
||||||
|
onClicked: CuraDrive.toggleAutoBackup(autoBackupEnabled.checked)
|
||||||
|
text: catalog.i18nc("@checkbox:description", "Auto Backup")
|
||||||
|
tooltip: catalog.i18nc("@checkbox:description", "Automatically create a backup each day that Cura is started.")
|
||||||
|
}
|
||||||
|
}
|
113
plugins/CuraDrive/src/qml/components/BackupListItem.qml
Normal file
113
plugins/CuraDrive/src/qml/components/BackupListItem.qml
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
import QtQuick.Dialogs 1.1
|
||||||
|
|
||||||
|
import UM 1.1 as UM
|
||||||
|
import Cura 1.0 as Cura
|
||||||
|
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
id: backupListItem
|
||||||
|
width: parent.width
|
||||||
|
height: showDetails ? dataRow.height + backupDetails.height : dataRow.height
|
||||||
|
property bool showDetails: false
|
||||||
|
|
||||||
|
// Backup details toggle animation.
|
||||||
|
Behavior on height
|
||||||
|
{
|
||||||
|
PropertyAnimation
|
||||||
|
{
|
||||||
|
duration: 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout
|
||||||
|
{
|
||||||
|
id: dataRow
|
||||||
|
spacing: UM.Theme.getSize("wide_margin").width
|
||||||
|
width: parent.width
|
||||||
|
height: 50 * screenScaleFactor
|
||||||
|
|
||||||
|
UM.SimpleButton
|
||||||
|
{
|
||||||
|
width: UM.Theme.getSize("section_icon").width
|
||||||
|
height: UM.Theme.getSize("section_icon").height
|
||||||
|
color: UM.Theme.getColor("small_button_text")
|
||||||
|
hoverColor: UM.Theme.getColor("small_button_text_hover")
|
||||||
|
iconSource: UM.Theme.getIcon("info")
|
||||||
|
onClicked: backupListItem.showDetails = !backupListItem.showDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: new Date(modelData.generated_time).toLocaleString(UM.Preferences.getValue("general/language"))
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.minimumWidth: 100 * screenScaleFactor
|
||||||
|
Layout.maximumWidth: 500 * screenScaleFactor
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: modelData.metadata.description
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.minimumWidth: 100 * screenScaleFactor
|
||||||
|
Layout.maximumWidth: 500 * screenScaleFactor
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.SecondaryButton
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@button", "Restore")
|
||||||
|
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
|
||||||
|
onClicked: confirmRestoreDialog.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.SimpleButton
|
||||||
|
{
|
||||||
|
width: UM.Theme.getSize("message_close").width
|
||||||
|
height: UM.Theme.getSize("message_close").height
|
||||||
|
color: UM.Theme.getColor("small_button_text")
|
||||||
|
hoverColor: UM.Theme.getColor("small_button_text_hover")
|
||||||
|
iconSource: UM.Theme.getIcon("cross1")
|
||||||
|
onClicked: confirmDeleteDialog.visible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackupListItemDetails
|
||||||
|
{
|
||||||
|
id: backupDetails
|
||||||
|
backupDetailsData: modelData
|
||||||
|
width: parent.width
|
||||||
|
visible: parent.showDetails
|
||||||
|
anchors.top: dataRow.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog
|
||||||
|
{
|
||||||
|
id: confirmDeleteDialog
|
||||||
|
title: catalog.i18nc("@dialog:title", "Delete Backup")
|
||||||
|
text: catalog.i18nc("@dialog:info", "Are you sure you want to delete this backup? This cannot be undone.")
|
||||||
|
standardButtons: StandardButton.Yes | StandardButton.No
|
||||||
|
onYes: CuraDrive.deleteBackup(modelData.backup_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog
|
||||||
|
{
|
||||||
|
id: confirmRestoreDialog
|
||||||
|
title: catalog.i18nc("@dialog:title", "Restore Backup")
|
||||||
|
text: catalog.i18nc("@dialog:info", "You will need to restart Cura before your backup is restored. Do you want to close Cura now?")
|
||||||
|
standardButtons: StandardButton.Yes | StandardButton.No
|
||||||
|
onYes: CuraDrive.restoreBackup(modelData.backup_id)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
|
import UM 1.1 as UM
|
||||||
|
|
||||||
|
ColumnLayout
|
||||||
|
{
|
||||||
|
id: backupDetails
|
||||||
|
width: parent.width
|
||||||
|
spacing: UM.Theme.getSize("default_margin").width
|
||||||
|
property var backupDetailsData
|
||||||
|
|
||||||
|
// Cura version
|
||||||
|
BackupListItemDetailsRow
|
||||||
|
{
|
||||||
|
iconSource: UM.Theme.getIcon("application")
|
||||||
|
label: catalog.i18nc("@backuplist:label", "Cura Version")
|
||||||
|
value: backupDetailsData.metadata.cura_release
|
||||||
|
}
|
||||||
|
|
||||||
|
// Machine count.
|
||||||
|
BackupListItemDetailsRow
|
||||||
|
{
|
||||||
|
iconSource: UM.Theme.getIcon("printer_single")
|
||||||
|
label: catalog.i18nc("@backuplist:label", "Machines")
|
||||||
|
value: backupDetailsData.metadata.machine_count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Material count
|
||||||
|
BackupListItemDetailsRow
|
||||||
|
{
|
||||||
|
iconSource: UM.Theme.getIcon("category_material")
|
||||||
|
label: catalog.i18nc("@backuplist:label", "Materials")
|
||||||
|
value: backupDetailsData.metadata.material_count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profile count.
|
||||||
|
BackupListItemDetailsRow
|
||||||
|
{
|
||||||
|
iconSource: UM.Theme.getIcon("settings")
|
||||||
|
label: catalog.i18nc("@backuplist:label", "Profiles")
|
||||||
|
value: backupDetailsData.metadata.profile_count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin count.
|
||||||
|
BackupListItemDetailsRow
|
||||||
|
{
|
||||||
|
iconSource: UM.Theme.getIcon("plugin")
|
||||||
|
label: catalog.i18nc("@backuplist:label", "Plugins")
|
||||||
|
value: backupDetailsData.metadata.plugin_count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spacer.
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: UM.Theme.getSize("default_margin").height
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,52 @@
|
|||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
|
import UM 1.3 as UM
|
||||||
|
|
||||||
|
RowLayout
|
||||||
|
{
|
||||||
|
id: detailsRow
|
||||||
|
width: parent.width
|
||||||
|
height: 40 * screenScaleFactor
|
||||||
|
|
||||||
|
property alias iconSource: icon.source
|
||||||
|
property alias label: detailName.text
|
||||||
|
property alias value: detailValue.text
|
||||||
|
|
||||||
|
UM.RecolorImage
|
||||||
|
{
|
||||||
|
id: icon
|
||||||
|
width: 18 * screenScaleFactor
|
||||||
|
height: width
|
||||||
|
source: ""
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: detailName
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.minimumWidth: 50 * screenScaleFactor
|
||||||
|
Layout.maximumWidth: 100 * screenScaleFactor
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: detailValue
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.minimumWidth: 50 * screenScaleFactor
|
||||||
|
Layout.maximumWidth: 100 * screenScaleFactor
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
}
|
BIN
plugins/CuraDrive/src/qml/images/icon.png
Normal file
BIN
plugins/CuraDrive/src/qml/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
plugins/CuraDrive/src/qml/images/loading.gif
Normal file
BIN
plugins/CuraDrive/src/qml/images/loading.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.6 KiB |
44
plugins/CuraDrive/src/qml/main.qml
Normal file
44
plugins/CuraDrive/src/qml/main.qml
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
|
import UM 1.3 as UM
|
||||||
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
|
import "components"
|
||||||
|
import "pages"
|
||||||
|
|
||||||
|
Window
|
||||||
|
{
|
||||||
|
id: curaDriveDialog
|
||||||
|
minimumWidth: Math.round(UM.Theme.getSize("modal_window_minimum").width)
|
||||||
|
minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height)
|
||||||
|
maximumWidth: Math.round(minimumWidth * 1.2)
|
||||||
|
maximumHeight: Math.round(minimumHeight * 1.2)
|
||||||
|
width: minimumWidth
|
||||||
|
height: minimumHeight
|
||||||
|
color: UM.Theme.getColor("main_background")
|
||||||
|
title: catalog.i18nc("@title:window", "Cura Backups")
|
||||||
|
|
||||||
|
// Globally available.
|
||||||
|
UM.I18nCatalog
|
||||||
|
{
|
||||||
|
id: catalog
|
||||||
|
name: "cura"
|
||||||
|
}
|
||||||
|
|
||||||
|
WelcomePage
|
||||||
|
{
|
||||||
|
id: welcomePage
|
||||||
|
visible: !Cura.API.account.isLoggedIn
|
||||||
|
}
|
||||||
|
|
||||||
|
BackupsPage
|
||||||
|
{
|
||||||
|
id: backupsPage
|
||||||
|
visible: Cura.API.account.isLoggedIn
|
||||||
|
}
|
||||||
|
}
|
75
plugins/CuraDrive/src/qml/pages/BackupsPage.qml
Normal file
75
plugins/CuraDrive/src/qml/pages/BackupsPage.qml
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
|
import UM 1.3 as UM
|
||||||
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
|
import "../components"
|
||||||
|
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
id: backupsPage
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: UM.Theme.getSize("wide_margin").width
|
||||||
|
|
||||||
|
ColumnLayout
|
||||||
|
{
|
||||||
|
spacing: UM.Theme.getSize("wide_margin").height
|
||||||
|
width: parent.width
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: backupTitle
|
||||||
|
text: catalog.i18nc("@title", "My Backups")
|
||||||
|
font: UM.Theme.getFont("large")
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
Layout.fillWidth: true
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@empty_state",
|
||||||
|
"You don't have any backups currently. Use the 'Backup Now' button to create one.")
|
||||||
|
width: parent.width
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
wrapMode: Label.WordWrap
|
||||||
|
visible: backupList.count == 0
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
BackupList
|
||||||
|
{
|
||||||
|
id: backupList
|
||||||
|
model: CuraDrive.backups
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@backup_limit_info",
|
||||||
|
"During the preview phase, you'll be limited to 5 visible backups. Remove a backup to see older ones.")
|
||||||
|
width: parent.width
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
wrapMode: Label.WordWrap
|
||||||
|
visible: backupList.count > 4
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
BackupListFooter
|
||||||
|
{
|
||||||
|
id: backupListFooter
|
||||||
|
showInfoButton: backupList.count > 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
plugins/CuraDrive/src/qml/pages/WelcomePage.qml
Normal file
56
plugins/CuraDrive/src/qml/pages/WelcomePage.qml
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
|
import UM 1.3 as UM
|
||||||
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
|
import "../components"
|
||||||
|
|
||||||
|
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
id: welcomePage
|
||||||
|
spacing: UM.Theme.getSize("wide_margin").height
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
Image
|
||||||
|
{
|
||||||
|
id: profileImage
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
source: "../images/icon.png"
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: Math.round(parent.width / 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: welcomeTextLabel
|
||||||
|
text: catalog.i18nc("@description", "Backup and synchronize your Cura settings.")
|
||||||
|
width: Math.round(parent.width / 2)
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
wrapMode: Label.WordWrap
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.PrimaryButton
|
||||||
|
{
|
||||||
|
id: loginButton
|
||||||
|
width: UM.Theme.getSize("account_button").width
|
||||||
|
height: UM.Theme.getSize("account_button").height
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: catalog.i18nc("@button", "Sign in")
|
||||||
|
onClicked: Cura.API.account.login()
|
||||||
|
fixedWidthMode: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,8 @@ from UM.Extension import Extension
|
|||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Version import Version
|
from UM.Version import Version
|
||||||
|
|
||||||
import cura
|
from cura import ApplicationMetadata
|
||||||
|
from cura import UltimakerCloudAuthentication
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
from .AuthorsModel import AuthorsModel
|
from .AuthorsModel import AuthorsModel
|
||||||
@ -30,17 +31,14 @@ i18n_catalog = i18nCatalog("cura")
|
|||||||
|
|
||||||
## The Toolbox class is responsible of communicating with the server through the API
|
## The Toolbox class is responsible of communicating with the server through the API
|
||||||
class Toolbox(QObject, Extension):
|
class Toolbox(QObject, Extension):
|
||||||
DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str
|
|
||||||
DEFAULT_CLOUD_API_VERSION = 1 # type: int
|
|
||||||
|
|
||||||
def __init__(self, application: CuraApplication) -> None:
|
def __init__(self, application: CuraApplication) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self._application = application # type: CuraApplication
|
self._application = application # type: CuraApplication
|
||||||
|
|
||||||
self._sdk_version = None # type: Optional[Union[str, int]]
|
self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
|
||||||
self._cloud_api_version = None # type: Optional[int]
|
self._cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion # type: int
|
||||||
self._cloud_api_root = None # type: Optional[str]
|
self._cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot # type: str
|
||||||
self._api_url = None # type: Optional[str]
|
self._api_url = None # type: Optional[str]
|
||||||
|
|
||||||
# Network:
|
# Network:
|
||||||
@ -182,9 +180,6 @@ class Toolbox(QObject, Extension):
|
|||||||
def _onAppInitialized(self) -> None:
|
def _onAppInitialized(self) -> None:
|
||||||
self._plugin_registry = self._application.getPluginRegistry()
|
self._plugin_registry = self._application.getPluginRegistry()
|
||||||
self._package_manager = self._application.getPackageManager()
|
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(
|
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_root = self._cloud_api_root,
|
||||||
cloud_api_version = self._cloud_api_version,
|
cloud_api_version = self._cloud_api_version,
|
||||||
@ -195,36 +190,6 @@ class Toolbox(QObject, Extension):
|
|||||||
"packages": QUrl("{base_url}/packages".format(base_url = self._api_url))
|
"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()
|
@pyqtSlot()
|
||||||
def browsePackages(self) -> None:
|
def browsePackages(self) -> None:
|
||||||
# Create the network manager:
|
# Create the network manager:
|
||||||
|
@ -50,6 +50,23 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"CuraDrive": {
|
||||||
|
"package_info": {
|
||||||
|
"package_id": "CuraDrive",
|
||||||
|
"package_type": "plugin",
|
||||||
|
"display_name": "Cura Backups",
|
||||||
|
"description": "Backup and restore your configuration.",
|
||||||
|
"package_version": "1.2.0",
|
||||||
|
"sdk_version": 6,
|
||||||
|
"website": "https://ultimaker.com",
|
||||||
|
"author": {
|
||||||
|
"author_id": "UltimakerPackages",
|
||||||
|
"display_name": "Ultimaker B.V.",
|
||||||
|
"email": "plugins@ultimaker.com",
|
||||||
|
"website": "https://ultimaker.com"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"CuraEngineBackend": {
|
"CuraEngineBackend": {
|
||||||
"package_info": {
|
"package_info": {
|
||||||
"package_id": "CuraEngineBackend",
|
"package_id": "CuraEngineBackend",
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import QtQuick 2.7
|
import QtQuick 2.7
|
||||||
import QtQuick.Controls 2.1
|
import QtQuick.Controls 2.1
|
||||||
import QtGraphicalEffects 1.0 // For the dropshadow
|
import QtGraphicalEffects 1.0 // For the dropshadow
|
||||||
|
|
||||||
import UM 1.1 as UM
|
import UM 1.1 as UM
|
||||||
import Cura 1.0 as Cura
|
import Cura 1.0 as Cura
|
||||||
|
|
||||||
@ -30,6 +31,7 @@ Button
|
|||||||
property color outlineDisabledColor: outlineColor
|
property color outlineDisabledColor: outlineColor
|
||||||
property alias shadowColor: shadow.color
|
property alias shadowColor: shadow.color
|
||||||
property alias shadowEnabled: shadow.visible
|
property alias shadowEnabled: shadow.visible
|
||||||
|
property alias busy: busyIndicator.visible
|
||||||
|
|
||||||
property alias toolTipContentAlignment: tooltip.contentAlignment
|
property alias toolTipContentAlignment: tooltip.contentAlignment
|
||||||
|
|
||||||
@ -55,7 +57,7 @@ Button
|
|||||||
width: visible ? height : 0
|
width: visible ? height : 0
|
||||||
sourceSize.width: width
|
sourceSize.width: width
|
||||||
sourceSize.height: height
|
sourceSize.height: height
|
||||||
color: button.hovered ? button.textHoverColor : button.textColor
|
color: button.enabled ? (button.hovered ? button.textHoverColor : button.textColor) : button.textDisabledColor
|
||||||
visible: source != "" && !button.isIconOnRightSide
|
visible: source != "" && !button.isIconOnRightSide
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
@ -117,4 +119,16 @@ Button
|
|||||||
id: tooltip
|
id: tooltip
|
||||||
visible: button.hovered
|
visible: button.hovered
|
||||||
}
|
}
|
||||||
|
|
||||||
|
BusyIndicator
|
||||||
|
{
|
||||||
|
id: busyIndicator
|
||||||
|
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
width: height
|
||||||
|
height: parent.height
|
||||||
|
|
||||||
|
visible: false
|
||||||
|
}
|
||||||
}
|
}
|
63
resources/qml/CheckBoxWithTooltip.qml
Normal file
63
resources/qml/CheckBoxWithTooltip.qml
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
|
||||||
|
import UM 1.3 as UM
|
||||||
|
|
||||||
|
CheckBox
|
||||||
|
{
|
||||||
|
id: checkbox
|
||||||
|
hoverEnabled: true
|
||||||
|
|
||||||
|
property alias tooltip: tooltip.text
|
||||||
|
|
||||||
|
indicator: Rectangle
|
||||||
|
{
|
||||||
|
implicitWidth: UM.Theme.getSize("checkbox").width
|
||||||
|
implicitHeight: UM.Theme.getSize("checkbox").height
|
||||||
|
x: 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: UM.Theme.getColor("main_background")
|
||||||
|
radius: UM.Theme.getSize("checkbox_radius").width
|
||||||
|
border.width: UM.Theme.getSize("default_lining").width
|
||||||
|
border.color: checkbox.hovered ? UM.Theme.getColor("checkbox_border_hover") : UM.Theme.getColor("checkbox_border")
|
||||||
|
|
||||||
|
UM.RecolorImage
|
||||||
|
{
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: Math.round(parent.width / 2.5)
|
||||||
|
height: Math.round(parent.height / 2.5)
|
||||||
|
sourceSize.height: width
|
||||||
|
color: UM.Theme.getColor("checkbox_mark")
|
||||||
|
source: UM.Theme.getIcon("check")
|
||||||
|
opacity: checkbox.checked
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 100; } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: Label
|
||||||
|
{
|
||||||
|
anchors
|
||||||
|
{
|
||||||
|
left: checkbox.indicator.right
|
||||||
|
leftMargin: UM.Theme.getSize("narrow_margin").width
|
||||||
|
}
|
||||||
|
text: checkbox.text
|
||||||
|
color: UM.Theme.getColor("checkbox_text")
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
elide: Text.ElideRight
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
ToolTip
|
||||||
|
{
|
||||||
|
id: tooltip
|
||||||
|
text: ""
|
||||||
|
delay: 500
|
||||||
|
visible: text != "" && checkbox.hovered
|
||||||
|
}
|
||||||
|
}
|
@ -124,16 +124,16 @@ UM.MainWindow
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a placehoder for adding a pattern in the header
|
// This is a placehoder for adding a pattern in the header
|
||||||
Image
|
Image
|
||||||
{
|
{
|
||||||
id: backgroundPattern
|
id: backgroundPattern
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
fillMode: Image.Tile
|
fillMode: Image.Tile
|
||||||
source: UM.Theme.getImage("header_pattern")
|
source: UM.Theme.getImage("header_pattern")
|
||||||
horizontalAlignment: Image.AlignLeft
|
horizontalAlignment: Image.AlignLeft
|
||||||
verticalAlignment: Image.AlignTop
|
verticalAlignment: Image.AlignTop
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MainWindowHeader
|
MainWindowHeader
|
||||||
|
@ -15,4 +15,5 @@ ViewsSelector 1.0 ViewsSelector.qml
|
|||||||
ToolbarButton 1.0 ToolbarButton.qml
|
ToolbarButton 1.0 ToolbarButton.qml
|
||||||
SettingView 1.0 SettingView.qml
|
SettingView 1.0 SettingView.qml
|
||||||
ProfileMenu 1.0 ProfileMenu.qml
|
ProfileMenu 1.0 ProfileMenu.qml
|
||||||
|
CheckBoxWithTooltip 1.0 CheckBoxWithTooltip.qml
|
||||||
ToolTip 1.0 ToolTip.qml
|
ToolTip 1.0 ToolTip.qml
|
@ -478,7 +478,7 @@ QtObject
|
|||||||
color: (control.hovered || control._hovered) ? Theme.getColor("checkbox_hover") : (control.enabled ? Theme.getColor("checkbox") : Theme.getColor("checkbox_disabled"))
|
color: (control.hovered || control._hovered) ? Theme.getColor("checkbox_hover") : (control.enabled ? Theme.getColor("checkbox") : Theme.getColor("checkbox_disabled"))
|
||||||
Behavior on color { ColorAnimation { duration: 50; } }
|
Behavior on color { ColorAnimation { duration: 50; } }
|
||||||
|
|
||||||
radius: control.exclusiveGroup ? Math.round(Theme.getSize("checkbox").width / 2) : UM.Theme.getSize("checkbox_radius").width
|
radius: control.exclusiveGroup ? Math.round(Theme.getSize("checkbox").width / 2) : Theme.getSize("checkbox_radius").width
|
||||||
|
|
||||||
border.width: Theme.getSize("default_lining").width
|
border.width: Theme.getSize("default_lining").width
|
||||||
border.color: (control.hovered || control._hovered) ? Theme.getColor("checkbox_border_hover") : Theme.getColor("checkbox_border")
|
border.color: (control.hovered || control._hovered) ? Theme.getColor("checkbox_border_hover") : Theme.getColor("checkbox_border")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user