Merge branch '4.0'

This commit is contained in:
Diego Prado Gesto 2019-01-14 08:56:33 +01:00
commit c6da824203
72 changed files with 1910 additions and 555 deletions

1
.gitignore vendored
View file

@ -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

View file

@ -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)

View file

@ -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)

View 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

View file

@ -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)
@ -500,7 +502,7 @@ class CuraApplication(QtApplication):
preferences.addPreference("cura/choice_on_profile_override", "always_ask") preferences.addPreference("cura/choice_on_profile_override", "always_ask")
preferences.addPreference("cura/choice_on_open_project", "always_ask") preferences.addPreference("cura/choice_on_open_project", "always_ask")
preferences.addPreference("cura/use_multi_build_plate", False) preferences.addPreference("cura/use_multi_build_plate", False)
preferences.addPreference("view/settings_list_height", 600) preferences.addPreference("view/settings_list_height", 400)
preferences.addPreference("view/settings_visible", False) preferences.addPreference("view/settings_visible", False)
preferences.addPreference("cura/currency", "") preferences.addPreference("cura/currency", "")
preferences.addPreference("cura/material_settings", "{}") preferences.addPreference("cura/material_settings", "{}")
@ -955,7 +957,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")

View file

@ -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@"

View file

@ -302,6 +302,10 @@ class MaterialManager(QObject):
def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]: def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]:
return self._guid_material_groups_map.get(guid) return self._guid_material_groups_map.get(guid)
# Returns a dict of all material groups organized by root_material_id.
def getAllMaterialGroups(self) -> Dict[str, "MaterialGroup"]:
return self._material_group_map
# #
# Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup. # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
# #
@ -679,7 +683,11 @@ class MaterialManager(QObject):
@pyqtSlot(str) @pyqtSlot(str)
def removeFavorite(self, root_material_id: str) -> None: def removeFavorite(self, root_material_id: str) -> None:
self._favorites.remove(root_material_id) try:
self._favorites.remove(root_material_id)
except KeyError:
Logger.log("w", "Could not delete material %s from favorites as it was already deleted", root_material_id)
return
self.materialsUpdated.emit() self.materialsUpdated.emit()
# Ensure all settings are saved. # Ensure all settings are saved.
@ -688,4 +696,4 @@ class MaterialManager(QObject):
@pyqtSlot() @pyqtSlot()
def getFavorites(self): def getFavorites(self):
return self._favorites return self._favorites

View file

@ -0,0 +1,28 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# ---------
# Constants used for the Cloud API
# ---------
DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str
DEFAULT_CLOUD_API_VERSION = 1 # type: int
DEFAULT_CLOUD_ACCOUNT_API_ROOT = "https://account.ultimaker.com" # type: str
try:
from cura.CuraVersion import CuraCloudAPIRoot # type: ignore
if CuraCloudAPIRoot == "":
CuraCloudAPIRoot = DEFAULT_CLOUD_API_ROOT
except ImportError:
CuraCloudAPIRoot = DEFAULT_CLOUD_API_ROOT
try:
from cura.CuraVersion import CuraCloudAPIVersion # type: ignore
except ImportError:
CuraCloudAPIVersion = DEFAULT_CLOUD_API_VERSION
try:
from cura.CuraVersion import CuraCloudAccountAPIRoot # type: ignore
if CuraCloudAccountAPIRoot == "":
CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT
except ImportError:
CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT

View 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()}

View 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"
}

View 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"]

View 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()

View 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"

View 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)

View file

View file

@ -0,0 +1,39 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 2.2
import QtQuick.Layouts 1.3
import UM 1.1 as UM
ScrollView
{
property alias model: backupList.model
width: parent.width
clip: true
ListView
{
id: backupList
width: parent.width
delegate: Item
{
// Add a margin, otherwise the scrollbar is on top of the right most component
width: parent.width - UM.Theme.getSize("default_margin").width
height: childrenRect.height
BackupListItem
{
id: backupListItem
width: parent.width
}
Rectangle
{
id: divider
color: UM.Theme.getColor("lining")
height: UM.Theme.getSize("default_lining").height
}
}
}
}

View 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.")
}
}

View 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)
}
}

View 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 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
}
}

View file

@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View 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
}
}

View 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
}
}
}

View 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
}
}

View file

@ -833,7 +833,10 @@ class CuraEngineBackend(QObject, Backend):
self._onChanged() self._onChanged()
def _onProcessLayersFinished(self, job: ProcessSlicedLayersJob) -> None: def _onProcessLayersFinished(self, job: ProcessSlicedLayersJob) -> None:
del self._stored_optimized_layer_data[job.getBuildPlate()] if job.getBuildPlate() in self._stored_optimized_layer_data:
del self._stored_optimized_layer_data[job.getBuildPlate()]
else:
Logger.log("w", "The optimized layer data was already deleted for buildplate %s", job.getBuildPlate())
self._process_layers_job = None self._process_layers_job = None
Logger.log("d", "See if there is more to slice(2)...") Logger.log("d", "See if there is more to slice(2)...")
self._invokeSlice() self._invokeSlice()

View file

@ -57,7 +57,7 @@ class FirmwareUpdaterMachineAction(MachineAction):
outputDeviceCanUpdateFirmwareChanged = pyqtSignal() outputDeviceCanUpdateFirmwareChanged = pyqtSignal()
@pyqtProperty(QObject, notify = outputDeviceCanUpdateFirmwareChanged) @pyqtProperty(QObject, notify = outputDeviceCanUpdateFirmwareChanged)
def firmwareUpdater(self) -> Optional["FirmwareUpdater"]: def firmwareUpdater(self) -> Optional["FirmwareUpdater"]:
if self._active_output_device and self._active_output_device.activePrinter.getController().can_update_firmware: if self._active_output_device and self._active_output_device.activePrinter and self._active_output_device.activePrinter.getController().can_update_firmware:
self._active_firmware_updater = self._active_output_device.getFirmwareUpdater() self._active_firmware_updater = self._active_output_device.getFirmwareUpdater()
return self._active_firmware_updater return self._active_firmware_updater

View file

@ -1,4 +1,5 @@
// Copyright (c) 2017 Ultimaker B.V. // Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10 import QtQuick 2.10
import QtQuick.Controls 1.4 import QtQuick.Controls 1.4
@ -7,31 +8,27 @@ import UM 1.3 as UM
import Cura 1.0 as Cura import Cura 1.0 as Cura
Item // We show a nice overlay on the 3D viewer when the current output device has no monitor view
Rectangle
{ {
// We show a nice overlay on the 3D viewer when the current output device has no monitor view id: viewportOverlay
Rectangle
color: UM.Theme.getColor("viewport_overlay")
anchors.fill: parent
// This mouse area is to prevent mouse clicks to be passed onto the scene.
MouseArea
{ {
id: viewportOverlay
color: UM.Theme.getColor("viewport_overlay")
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.AllButtons
// This mouse area is to prevent mouse clicks to be passed onto the scene. onWheel: wheel.accepted = true
MouseArea
{
anchors.fill: parent
acceptedButtons: Qt.AllButtons
onWheel: wheel.accepted = true
}
// Disable dropping files into Cura when the monitor page is active
DropArea
{
anchors.fill: parent
}
} }
// Disable dropping files into Cura when the monitor page is active
DropArea
{
anchors.fill: parent
}
Loader Loader
{ {
id: monitorViewComponent id: monitorViewComponent
@ -45,4 +42,4 @@ Item
sourceComponent: Cura.MachineManager.printerOutputDevices.length > 0 ? Cura.MachineManager.printerOutputDevices[0].monitorItem : null sourceComponent: Cura.MachineManager.printerOutputDevices.length > 0 ? Cura.MachineManager.printerOutputDevices[0].monitorItem : null
} }
} }

View file

@ -488,7 +488,7 @@ UM.Dialog
{ {
objectName: "postProcessingSaveAreaButton" objectName: "postProcessingSaveAreaButton"
visible: activeScriptsList.count > 0 visible: activeScriptsList.count > 0
height: UM.Theme.getSize("save_button_save_to_button").height height: UM.Theme.getSize("action_button").height
width: height width: height
tooltip: catalog.i18nc("@info:tooltip", "Change active post-processing scripts") tooltip: catalog.i18nc("@info:tooltip", "Change active post-processing scripts")
onClicked: dialog.show() onClicked: dialog.show()

View file

@ -38,7 +38,7 @@ Window
{ {
id: mainView id: mainView
width: parent.width width: parent.width
z: -1 z: parent.z - 1
anchors anchors
{ {
top: header.bottom top: header.bottom

View file

@ -91,5 +91,10 @@ Column
target: toolbox target: toolbox
onInstallChanged: installed = toolbox.isInstalled(model.id) onInstallChanged: installed = toolbox.isInstalled(model.id)
onMetadataChanged: canUpdate = toolbox.canUpdate(model.id) onMetadataChanged: canUpdate = toolbox.canUpdate(model.id)
onFilterChanged:
{
installed = toolbox.isInstalled(model.id)
canUpdate = toolbox.canUpdate(model.id)
}
} }
} }

View file

@ -30,6 +30,7 @@ Item
CheckBox CheckBox
{ {
id: disableButton id: disableButton
anchors.verticalCenter: pluginInfo.verticalCenter
checked: isEnabled checked: isEnabled
visible: model.type == "plugin" visible: model.type == "plugin"
width: visible ? UM.Theme.getSize("checkbox").width : 0 width: visible ? UM.Theme.getSize("checkbox").width : 0

View file

@ -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:

View file

@ -15,6 +15,7 @@ Item
id: base id: base
property bool expanded: false property bool expanded: false
property bool enabled: true
property var borderWidth: 1 property var borderWidth: 1
property color borderColor: "#CCCCCC" property color borderColor: "#CCCCCC"
property color headerBackgroundColor: "white" property color headerBackgroundColor: "white"
@ -34,7 +35,7 @@ Item
color: borderColor color: borderColor
width: borderWidth width: borderWidth
} }
color: headerMouseArea.containsMouse ? headerHoverColor : headerBackgroundColor color: base.enabled && headerMouseArea.containsMouse ? headerHoverColor : headerBackgroundColor
height: childrenRect.height height: childrenRect.height
width: parent.width width: parent.width
Behavior on color Behavior on color
@ -50,8 +51,12 @@ Item
{ {
id: headerMouseArea id: headerMouseArea
anchors.fill: header anchors.fill: header
onClicked: base.expanded = !base.expanded onClicked:
hoverEnabled: true {
if (!base.enabled) return
base.expanded = !base.expanded
}
hoverEnabled: base.enabled
} }
Rectangle Rectangle

View file

@ -18,7 +18,7 @@ import UM 1.3 as UM
Item Item
{ {
// The buildplate name // The buildplate name
property alias buildplate: buildplateLabel.text property var buildplate: null
// Height is one 18px label/icon // Height is one 18px label/icon
height: 18 * screenScaleFactor // TODO: Theme! height: 18 * screenScaleFactor // TODO: Theme!
@ -34,7 +34,16 @@ Item
Item Item
{ {
height: parent.height height: parent.height
width: 32 * screenScaleFactor // TODO: Theme! (Should be same as extruder icon width) width: 32 * screenScaleFactor // Ensure the icon is centered under the extruder icon (same width)
Rectangle
{
anchors.centerIn: parent
height: parent.height
width: height
color: buildplateIcon.visible > 0 ? "transparent" : "#eeeeee" // TODO: Theme!
radius: Math.floor(height / 2)
}
UM.RecolorImage UM.RecolorImage
{ {
@ -44,6 +53,7 @@ Item
height: parent.height height: parent.height
source: "../svg/icons/buildplate.svg" source: "../svg/icons/buildplate.svg"
width: height width: height
visible: buildplate
} }
} }
@ -53,7 +63,8 @@ Item
color: "#191919" // TODO: Theme! color: "#191919" // TODO: Theme!
elide: Text.ElideRight elide: Text.ElideRight
font: UM.Theme.getFont("default") // 12pt, regular font: UM.Theme.getFont("default") // 12pt, regular
text: "" text: buildplate ? buildplate : ""
visible: text !== ""
// FIXED-LINE-HEIGHT: // FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme! height: 18 * screenScaleFactor // TODO: Theme!

View file

@ -14,7 +14,12 @@ Item
property var tileWidth: 834 * screenScaleFactor // TODO: Theme! property var tileWidth: 834 * screenScaleFactor // TODO: Theme!
property var tileHeight: 216 * screenScaleFactor // TODO: Theme! property var tileHeight: 216 * screenScaleFactor // TODO: Theme!
property var tileSpacing: 60 * screenScaleFactor // TODO: Theme! property var tileSpacing: 60 * screenScaleFactor // TODO: Theme!
property var maxOffset: (OutputDevice.printers.length - 1) * (tileWidth + tileSpacing)
// Array/model of printers to populate the carousel with
property var printers: []
// Maximum distance the carousel can be shifted
property var maxOffset: (printers.length - 1) * (tileWidth + tileSpacing)
height: centerSection.height height: centerSection.height
width: maximumWidth width: maximumWidth
@ -129,7 +134,7 @@ Item
Repeater Repeater
{ {
model: OutputDevice.printers model: printers
MonitorPrinterCard MonitorPrinterCard
{ {
printer: modelData printer: modelData
@ -151,7 +156,7 @@ Item
width: 36 * screenScaleFactor // TODO: Theme! width: 36 * screenScaleFactor // TODO: Theme!
height: 72 * screenScaleFactor // TODO: Theme! height: 72 * screenScaleFactor // TODO: Theme!
z: 10 z: 10
visible: currentIndex < OutputDevice.printers.length - 1 visible: currentIndex < printers.length - 1
onClicked: navigateTo(currentIndex + 1) onClicked: navigateTo(currentIndex + 1)
hoverEnabled: true hoverEnabled: true
background: Rectangle background: Rectangle
@ -225,9 +230,10 @@ Item
topMargin: 36 * screenScaleFactor // TODO: Theme! topMargin: 36 * screenScaleFactor // TODO: Theme!
} }
spacing: 8 * screenScaleFactor // TODO: Theme! spacing: 8 * screenScaleFactor // TODO: Theme!
visible: printers.length > 1
Repeater Repeater
{ {
model: OutputDevice.printers model: printers
Button Button
{ {
background: Rectangle background: Rectangle
@ -243,7 +249,7 @@ Item
} }
function navigateTo( i ) { function navigateTo( i ) {
if (i >= 0 && i < OutputDevice.printers.length) if (i >= 0 && i < printers.length)
{ {
tiles.x = -1 * i * (tileWidth + tileSpacing) tiles.x = -1 * i * (tileWidth + tileSpacing)
currentIndex = i currentIndex = i

View file

@ -54,7 +54,7 @@ UM.Dialog
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: text:
{ {
if (!printer.activePrintJob) if (!printer || !printer.activePrintJob)
{ {
return "" return ""
} }

View file

@ -39,38 +39,62 @@ Item
color: "#eeeeee" // TODO: Theme! color: "#eeeeee" // TODO: Theme!
position: 0 position: 0
} }
Label
Rectangle
{ {
id: materialLabel id: materialLabelWrapper
anchors anchors
{ {
left: extruderIcon.right left: extruderIcon.right
leftMargin: 12 * screenScaleFactor // TODO: Theme! leftMargin: 12 * screenScaleFactor // TODO: Theme!
} }
color: "#191919" // TODO: Theme! color: materialLabel.visible > 0 ? "transparent" : "#eeeeee" // TODO: Theme!
elide: Text.ElideRight
font: UM.Theme.getFont("default") // 12pt, regular
text: ""
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme! height: 18 * screenScaleFactor // TODO: Theme!
verticalAlignment: Text.AlignVCenter width: Math.max(materialLabel.contentWidth, 60 * screenScaleFactor) // TODO: Theme!
radius: 2 * screenScaleFactor // TODO: Theme!
Label
{
id: materialLabel
color: "#191919" // TODO: Theme!
elide: Text.ElideRight
font: UM.Theme.getFont("default") // 12pt, regular
text: ""
visible: text !== ""
// FIXED-LINE-HEIGHT:
height: parent.height
verticalAlignment: Text.AlignVCenter
}
} }
Label
Rectangle
{ {
id: printCoreLabel id: printCoreLabelWrapper
anchors anchors
{ {
left: materialLabel.left left: materialLabelWrapper.left
bottom: parent.bottom bottom: parent.bottom
} }
color: "#191919" // TODO: Theme! color: printCoreLabel.visible > 0 ? "transparent" : "#eeeeee" // TODO: Theme!
elide: Text.ElideRight
font: UM.Theme.getFont("default_bold") // 12pt, bold
text: ""
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme! height: 18 * screenScaleFactor // TODO: Theme!
verticalAlignment: Text.AlignVCenter width: Math.max(printCoreLabel.contentWidth, 36 * screenScaleFactor) // TODO: Theme!
radius: 2 * screenScaleFactor // TODO: Theme!
Label
{
id: printCoreLabel
color: "#191919" // TODO: Theme!
elide: Text.ElideRight
font: UM.Theme.getFont("default_bold") // 12pt, bold
text: ""
visible: text !== ""
// FIXED-LINE-HEIGHT:
height: parent.height
verticalAlignment: Text.AlignVCenter
}
} }
} }

View file

@ -56,5 +56,6 @@ Item
x: Math.round(size * 0.25) * screenScaleFactor x: Math.round(size * 0.25) * screenScaleFactor
y: Math.round(size * 0.15625) * screenScaleFactor y: Math.round(size * 0.15625) * screenScaleFactor
// TODO: Once 'size' is themed, screenScaleFactor won't be needed // TODO: Once 'size' is themed, screenScaleFactor won't be needed
visible: position >= 0
} }
} }

View file

@ -26,6 +26,7 @@ Item
ExpandableCard ExpandableCard
{ {
enabled: printJob != null
borderColor: printJob.configurationChanges.length !== 0 ? "#f5a623" : "#CCCCCC" // TODO: Theme! borderColor: printJob.configurationChanges.length !== 0 ? "#f5a623" : "#CCCCCC" // TODO: Theme!
headerItem: Row headerItem: Row
{ {
@ -41,32 +42,56 @@ Item
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
Label Item
{ {
text: printJob && printJob.name ? printJob.name : ""
color: "#374355"
elide: Text.ElideRight
font: UM.Theme.getFont("medium") // 14pt, regular
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 216 * screenScaleFactor // TODO: Theme! (Should match column size)
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme! height: 18 * screenScaleFactor // TODO: Theme!
verticalAlignment: Text.AlignVCenter width: 216 * screenScaleFactor // TODO: Theme! (Should match column size)
Rectangle
{
color: "#eeeeee"
width: Math.round(parent.width / 2)
height: parent.height
visible: !printJob
}
Label
{
text: printJob && printJob.name ? printJob.name : ""
color: "#374355"
elide: Text.ElideRight
font: UM.Theme.getFont("medium") // 14pt, regular
visible: printJob
// FIXED-LINE-HEIGHT:
height: parent.height
verticalAlignment: Text.AlignVCenter
}
} }
Label
{
text: printJob ? OutputDevice.formatDuration(printJob.timeTotal) : ""
color: "#374355"
elide: Text.ElideRight
font: UM.Theme.getFont("medium") // 14pt, regular
anchors.verticalCenter: parent.verticalCenter
width: 216 * screenScaleFactor // TODO: Theme! (Should match column size)
// FIXED-LINE-HEIGHT: Item
{
anchors.verticalCenter: parent.verticalCenter
height: 18 * screenScaleFactor // TODO: Theme! height: 18 * screenScaleFactor // TODO: Theme!
verticalAlignment: Text.AlignVCenter width: 216 * screenScaleFactor // TODO: Theme! (Should match column size)
Rectangle
{
color: "#eeeeee"
width: Math.round(parent.width / 3)
height: parent.height
visible: !printJob
}
Label
{
text: printJob ? OutputDevice.formatDuration(printJob.timeTotal) : ""
color: "#374355"
elide: Text.ElideRight
font: UM.Theme.getFont("medium") // 14pt, regular
visible: printJob
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme!
verticalAlignment: Text.AlignVCenter
}
} }
Item Item
@ -75,6 +100,14 @@ Item
height: 18 * screenScaleFactor // TODO: This should be childrenRect.height but QML throws warnings height: 18 * screenScaleFactor // TODO: This should be childrenRect.height but QML throws warnings
width: childrenRect.width width: childrenRect.width
Rectangle
{
color: "#eeeeee"
width: 72 * screenScaleFactor // TODO: Theme!
height: parent.height
visible: !printJob
}
Label Label
{ {
id: printerAssignmentLabel id: printerAssignmentLabel
@ -100,7 +133,7 @@ Item
width: 120 * screenScaleFactor // TODO: Theme! width: 120 * screenScaleFactor // TODO: Theme!
// FIXED-LINE-HEIGHT: // FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme! height: parent.height
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
} }
@ -115,6 +148,7 @@ Item
} }
height: childrenRect.height height: childrenRect.height
spacing: 6 // TODO: Theme! spacing: 6 // TODO: Theme!
visible: printJob
Repeater Repeater
{ {

View file

@ -16,23 +16,28 @@ Item
width: size width: size
height: size height: size
// Actual content Rectangle
Image
{ {
id: previewImage
anchors.fill: parent anchors.fill: parent
opacity: color: printJob ? "transparent" : "#eeeeee" // TODO: Theme!
radius: 8 // TODO: Theme!
Image
{ {
if (printJob && (printJob.state == "error" || printJob.configurationChanges.length > 0 || !printJob.isActive)) id: previewImage
anchors.fill: parent
opacity:
{ {
return 0.5 if (printJob && (printJob.state == "error" || printJob.configurationChanges.length > 0 || !printJob.isActive))
{
return 0.5
}
return 1.0
} }
return 1.0 source: printJob ? printJob.previewImageUrl : ""
} }
source: printJob ? printJob.previewImageUrl : ""
visible: printJob
} }
UM.RecolorImage UM.RecolorImage
{ {
id: ultiBotImage id: ultiBotImage

View file

@ -34,16 +34,16 @@ Item
{ {
background: Rectangle background: Rectangle
{ {
color: printJob && printJob.isActive ? "#e4e4f2" : "#f3f3f9" // TODO: Theme! color: "#f5f5f5" // TODO: Theme!
implicitHeight: visible ? 8 * screenScaleFactor : 0 // TODO: Theme! implicitHeight: visible ? 8 * screenScaleFactor : 0 // TODO: Theme!
implicitWidth: 180 * screenScaleFactor // TODO: Theme! implicitWidth: 180 * screenScaleFactor // TODO: Theme!
radius: 4 * screenScaleFactor // TODO: Theme! radius: 2 * screenScaleFactor // TODO: Theme!
} }
progress: Rectangle progress: Rectangle
{ {
id: progressItem; id: progressItem;
color: printJob && printJob.isActive ? "#0a0850" : "#9392b2" // TODO: Theme! color: printJob && printJob.isActive ? "#3282ff" : "#CCCCCC" // TODO: Theme!
radius: 4 * screenScaleFactor // TODO: Theme! radius: 2 * screenScaleFactor // TODO: Theme!
} }
} }
} }

View file

@ -33,16 +33,24 @@ Item
width: 834 * screenScaleFactor // TODO: Theme! width: 834 * screenScaleFactor // TODO: Theme!
height: childrenRect.height height: childrenRect.height
// Printer portion
Rectangle Rectangle
{ {
id: printerInfo id: background
anchors.fill: parent
color: "#FFFFFF" // TODO: Theme!
border border
{ {
color: "#CCCCCC" // TODO: Theme! color: "#CCCCCC" // TODO: Theme!
width: borderSize // TODO: Remove once themed width: borderSize // TODO: Remove once themed
} }
color: "white" // TODO: Theme! radius: 2 * screenScaleFactor // TODO: Theme!
}
// Printer portion
Item
{
id: printerInfo
width: parent.width width: parent.width
height: 144 * screenScaleFactor // TODO: Theme! height: 144 * screenScaleFactor // TODO: Theme!
@ -56,15 +64,22 @@ Item
} }
spacing: 18 * screenScaleFactor // TODO: Theme! spacing: 18 * screenScaleFactor // TODO: Theme!
Image Rectangle
{ {
id: printerImage id: printerImage
width: 108 * screenScaleFactor // TODO: Theme! width: 108 * screenScaleFactor // TODO: Theme!
height: 108 * screenScaleFactor // TODO: Theme! height: 108 * screenScaleFactor // TODO: Theme!
fillMode: Image.PreserveAspectFit color: printer ? "transparent" : "#eeeeee" // TODO: Theme!
source: "../png/" + printer.type + ".png" radius: 8 // TODO: Theme!
mipmap: true Image
{
anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: printer ? "../png/" + printer.type + ".png" : ""
mipmap: true
}
} }
Item Item
{ {
@ -75,20 +90,38 @@ Item
width: 180 * screenScaleFactor // TODO: Theme! width: 180 * screenScaleFactor // TODO: Theme!
height: printerNameLabel.height + printerFamilyPill.height + 6 * screenScaleFactor // TODO: Theme! height: printerNameLabel.height + printerFamilyPill.height + 6 * screenScaleFactor // TODO: Theme!
Label Rectangle
{ {
id: printerNameLabel id: printerNameLabel
text: printer && printer.name ? printer.name : "" // color: "#414054" // TODO: Theme!
color: "#414054" // TODO: Theme! color: printer ? "transparent" : "#eeeeee" // TODO: Theme!
elide: Text.ElideRight
font: UM.Theme.getFont("large_bold") // 16pt, bold
width: parent.width
// FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme! height: 18 * screenScaleFactor // TODO: Theme!
verticalAlignment: Text.AlignVCenter width: parent.width
radius: 2 * screenScaleFactor // TODO: Theme!
Label
{
text: printer && printer.name ? printer.name : ""
color: "#414054" // TODO: Theme!
elide: Text.ElideRight
font: UM.Theme.getFont("large") // 16pt, bold
width: parent.width
visible: printer
// FIXED-LINE-HEIGHT:
height: parent.height
verticalAlignment: Text.AlignVCenter
}
} }
Rectangle
{
color: "#eeeeee" // TODO: Theme!
height: 18 * screenScaleFactor // TODO: Theme!
radius: 2 * screenScaleFactor // TODO: Theme!
visible: !printer
width: 48 * screenScaleFactor // TODO: Theme!
}
MonitorPrinterPill MonitorPrinterPill
{ {
id: printerFamilyPill id: printerFamilyPill
@ -98,7 +131,7 @@ Item
topMargin: 6 * screenScaleFactor // TODO: Theme! topMargin: 6 * screenScaleFactor // TODO: Theme!
left: printerNameLabel.left left: printerNameLabel.left
} }
text: printer.type text: printer ? printer.type : ""
} }
} }
@ -106,16 +139,30 @@ Item
{ {
id: printerConfiguration id: printerConfiguration
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
buildplate: "Glass" buildplate: printer ? "Glass" : null // 'Glass' as a default
configurations: configurations:
[ {
base.printer.printerConfiguration.extruderConfigurations[0], var configs = []
base.printer.printerConfiguration.extruderConfigurations[1] if (printer)
] {
height: 72 * screenScaleFactor // TODO: Theme! configs.push(printer.printerConfiguration.extruderConfigurations[0])
configs.push(printer.printerConfiguration.extruderConfigurations[1])
}
else
{
configs.push(null, null)
}
return configs
}
height: 72 * screenScaleFactor // TODO: Theme!te theRect's x property
} }
// TODO: Make this work.
PropertyAnimation { target: printerConfiguration; property: "visible"; to: 0; loops: Animation.Infinite; duration: 500 }
} }
PrintJobContextMenu PrintJobContextMenu
{ {
id: contextButton id: contextButton
@ -126,10 +173,11 @@ Item
top: parent.top top: parent.top
topMargin: 12 * screenScaleFactor // TODO: Theme! topMargin: 12 * screenScaleFactor // TODO: Theme!
} }
printJob: printer.activePrintJob printJob: printer ? printer.activePrintJob : null
width: 36 * screenScaleFactor // TODO: Theme! width: 36 * screenScaleFactor // TODO: Theme!
height: 36 * screenScaleFactor // TODO: Theme! height: 36 * screenScaleFactor // TODO: Theme!
enabled: base.enabled enabled: base.enabled
visible: printer
} }
CameraButton CameraButton
{ {
@ -143,10 +191,24 @@ Item
} }
iconSource: "../svg/icons/camera.svg" iconSource: "../svg/icons/camera.svg"
enabled: base.enabled enabled: base.enabled
visible: printer
} }
} }
// Divider
Rectangle
{
anchors
{
top: printJobInfo.top
left: printJobInfo.left
right: printJobInfo.right
}
height: borderSize // Remove once themed
color: background.border.color
}
// Print job portion // Print job portion
Rectangle Rectangle
{ {
@ -158,10 +220,10 @@ Item
} }
border border
{ {
color: printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 ? "#f5a623" : "#CCCCCC" // TODO: Theme! color: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 ? "#f5a623" : "transparent" // TODO: Theme!
width: borderSize // TODO: Remove once themed width: borderSize // TODO: Remove once themed
} }
color: "white" // TODO: Theme! color: "transparent" // TODO: Theme!
height: 84 * screenScaleFactor + borderSize // TODO: Remove once themed height: 84 * screenScaleFactor + borderSize // TODO: Remove once themed
width: parent.width width: parent.width
@ -184,9 +246,12 @@ Item
{ {
verticalCenter: parent.verticalCenter verticalCenter: parent.verticalCenter
} }
color: "#414054" // TODO: Theme! color: printer ? "#414054" : "#aaaaaa" // TODO: Theme!
font: UM.Theme.getFont("large_bold") // 16pt, bold font: UM.Theme.getFont("large_bold") // 16pt, bold
text: { text: {
if (!printer) {
return catalog.i18nc("@label:status", "Loading...")
}
if (printer && printer.state == "disabled") if (printer && printer.state == "disabled")
{ {
return catalog.i18nc("@label:status", "Unavailable") return catalog.i18nc("@label:status", "Unavailable")
@ -215,10 +280,10 @@ Item
MonitorPrintJobPreview MonitorPrintJobPreview
{ {
anchors.centerIn: parent anchors.centerIn: parent
printJob: base.printer.activePrintJob printJob: printer ? printer.activePrintJob : null
size: parent.height size: parent.height
} }
visible: printer.activePrintJob visible: printer && printer.activePrintJob && !printerStatus.visible
} }
Item Item
@ -229,15 +294,15 @@ Item
} }
width: 180 * screenScaleFactor // TODO: Theme! width: 180 * screenScaleFactor // TODO: Theme!
height: printerNameLabel.height + printerFamilyPill.height + 6 * screenScaleFactor // TODO: Theme! height: printerNameLabel.height + printerFamilyPill.height + 6 * screenScaleFactor // TODO: Theme!
visible: printer.activePrintJob visible: printer && printer.activePrintJob && !printerStatus.visible
Label Label
{ {
id: printerJobNameLabel id: printerJobNameLabel
color: printer.activePrintJob && printer.activePrintJob.isActive ? "#414054" : "#babac1" // TODO: Theme! color: printer && printer.activePrintJob && printer.activePrintJob.isActive ? "#414054" : "#babac1" // TODO: Theme!
elide: Text.ElideRight elide: Text.ElideRight
font: UM.Theme.getFont("large_bold") // 16pt, bold font: UM.Theme.getFont("large") // 16pt, bold
text: base.printer.activePrintJob ? base.printer.activePrintJob.name : "Untitled" // TODO: I18N text: printer && printer.activePrintJob ? printer.activePrintJob.name : "Untitled" // TODO: I18N
width: parent.width width: parent.width
// FIXED-LINE-HEIGHT: // FIXED-LINE-HEIGHT:
@ -254,10 +319,10 @@ Item
topMargin: 6 * screenScaleFactor // TODO: Theme! topMargin: 6 * screenScaleFactor // TODO: Theme!
left: printerJobNameLabel.left left: printerJobNameLabel.left
} }
color: printer.activePrintJob && printer.activePrintJob.isActive ? "#53657d" : "#babac1" // TODO: Theme! color: printer && printer.activePrintJob && printer.activePrintJob.isActive ? "#53657d" : "#babac1" // TODO: Theme!
elide: Text.ElideRight elide: Text.ElideRight
font: UM.Theme.getFont("default") // 12pt, regular font: UM.Theme.getFont("default") // 12pt, regular
text: printer.activePrintJob ? printer.activePrintJob.owner : "Anonymous" // TODO: I18N text: printer && printer.activePrintJob ? printer.activePrintJob.owner : "Anonymous" // TODO: I18N
width: parent.width width: parent.width
// FIXED-LINE-HEIGHT: // FIXED-LINE-HEIGHT:
@ -272,8 +337,8 @@ Item
{ {
verticalCenter: parent.verticalCenter verticalCenter: parent.verticalCenter
} }
printJob: printer.activePrintJob printJob: printer && printer.activePrintJob
visible: printer.activePrintJob && printer.activePrintJob.configurationChanges.length === 0 visible: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length === 0 && !printerStatus.visible
} }
Label Label
@ -284,7 +349,7 @@ Item
} }
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
text: "Requires configuration changes" text: "Requires configuration changes"
visible: printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 visible: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 && !printerStatus.visible
// FIXED-LINE-HEIGHT: // FIXED-LINE-HEIGHT:
height: 18 * screenScaleFactor // TODO: Theme! height: 18 * screenScaleFactor // TODO: Theme!
@ -326,7 +391,7 @@ Item
} }
implicitHeight: 32 * screenScaleFactor // TODO: Theme! implicitHeight: 32 * screenScaleFactor // TODO: Theme!
implicitWidth: 96 * screenScaleFactor // TODO: Theme! implicitWidth: 96 * screenScaleFactor // TODO: Theme!
visible: printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 visible: printer && printer.activePrintJob && printer.activePrintJob.configurationChanges.length > 0 && !printerStatus.visible
onClicked: base.enabled ? overrideConfirmationDialog.open() : {} onClicked: base.enabled ? overrideConfirmationDialog.open() : {}
} }
} }

View file

@ -19,7 +19,7 @@ Item
property alias buildplate: buildplateConfig.buildplate property alias buildplate: buildplateConfig.buildplate
// Array of extracted extruder configurations // Array of extracted extruder configurations
property var configurations: null property var configurations: [null,null]
// Default size, but should be stretched to fill parent // Default size, but should be stretched to fill parent
height: 72 * parent.height height: 72 * parent.height
@ -37,10 +37,10 @@ Item
MonitorExtruderConfiguration MonitorExtruderConfiguration
{ {
color: modelData.activeMaterial ? modelData.activeMaterial.color : "#eeeeee" // TODO: Theme! color: modelData && modelData.activeMaterial ? modelData.activeMaterial.color : "#eeeeee" // TODO: Theme!
material: modelData.activeMaterial ? modelData.activeMaterial.name : "" material: modelData && modelData.activeMaterial ? modelData.activeMaterial.name : ""
position: modelData.position position: modelData && typeof(modelData.position) === "number" ? modelData.position : -1 // Use negative one to create empty extruder number
printCore: modelData.hotendID printCore: modelData ? modelData.hotendID : ""
// Keep things responsive! // Keep things responsive!
width: Math.floor((base.width - (configurations.length - 1) * extruderConfigurationRow.spacing) / configurations.length) width: Math.floor((base.width - (configurations.length - 1) * extruderConfigurationRow.spacing) / configurations.length)
@ -53,6 +53,6 @@ Item
{ {
id: buildplateConfig id: buildplateConfig
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
buildplate: "Glass" // 'Glass' as a default buildplate: null
} }
} }

View file

@ -27,12 +27,12 @@ Item
} }
implicitHeight: 18 * screenScaleFactor // TODO: Theme! implicitHeight: 18 * screenScaleFactor // TODO: Theme!
implicitWidth: printerNameLabel.contentWidth + 12 // TODO: Theme! implicitWidth: Math.max(printerNameLabel.contentWidth + 12 * screenScaleFactor, 36 * screenScaleFactor) // TODO: Theme!
Rectangle { Rectangle {
id: background id: background
anchors.fill: parent anchors.fill: parent
color: "#e4e4f2" // TODO: Theme! color: printerNameLabel.visible ? "#e4e4f2" : "#eeeeee"// TODO: Theme!
radius: 2 * screenScaleFactor // TODO: Theme! radius: 2 * screenScaleFactor // TODO: Theme!
} }
@ -41,6 +41,7 @@ Item
anchors.centerIn: parent anchors.centerIn: parent
color: "#535369" // TODO: Theme! color: "#535369" // TODO: Theme!
text: tagText text: tagText
font.pointSize: 10 font.pointSize: 10 // TODO: Theme!
visible: text !== ""
} }
} }

View file

@ -42,8 +42,8 @@ Item
{ {
id: externalLinkIcon id: externalLinkIcon
anchors.verticalCenter: manageQueueLabel.verticalCenter anchors.verticalCenter: manageQueueLabel.verticalCenter
color: UM.Theme.getColor("primary") color: UM.Theme.getColor("text_link")
source: "../svg/icons/external_link.svg" source: UM.Theme.getIcon("external_link")
width: 16 * screenScaleFactor // TODO: Theme! (Y U NO USE 18 LIKE ALL OTHER ICONS?!) width: 16 * screenScaleFactor // TODO: Theme! (Y U NO USE 18 LIKE ALL OTHER ICONS?!)
height: 16 * screenScaleFactor // TODO: Theme! (Y U NO USE 18 LIKE ALL OTHER ICONS?!) height: 16 * screenScaleFactor // TODO: Theme! (Y U NO USE 18 LIKE ALL OTHER ICONS?!)
} }
@ -56,10 +56,11 @@ Item
leftMargin: 6 * screenScaleFactor // TODO: Theme! leftMargin: 6 * screenScaleFactor // TODO: Theme!
verticalCenter: externalLinkIcon.verticalCenter verticalCenter: externalLinkIcon.verticalCenter
} }
color: UM.Theme.getColor("primary") color: UM.Theme.getColor("text_link")
font: UM.Theme.getFont("default") // 12pt, regular font: UM.Theme.getFont("default") // 12pt, regular
linkColor: UM.Theme.getColor("primary") linkColor: UM.Theme.getColor("text_link")
text: catalog.i18nc("@label link to connect manager", "Manage queue in Cura Connect") text: catalog.i18nc("@label link to connect manager", "Manage queue in Cura Connect")
renderType: Text.NativeRendering
} }
} }
@ -144,7 +145,6 @@ Item
topMargin: 12 * screenScaleFactor // TODO: Theme! topMargin: 12 * screenScaleFactor // TODO: Theme!
} }
style: UM.Theme.styles.scrollview style: UM.Theme.styles.scrollview
visible: OutputDevice.receivedPrintJobs
width: parent.width width: parent.width
ListView ListView
@ -160,7 +160,7 @@ Item
} }
printJob: modelData printJob: modelData
} }
model: OutputDevice.queuedPrintJobs model: OutputDevice.receivedPrintJobs ? OutputDevice.queuedPrintJobs : [null,null]
spacing: 6 // TODO: Theme! spacing: 6 // TODO: Theme!
} }
} }

View file

@ -64,8 +64,10 @@ Component
} }
width: parent.width width: parent.width
height: 264 * screenScaleFactor // TODO: Theme! height: 264 * screenScaleFactor // TODO: Theme!
MonitorCarousel { MonitorCarousel
{
id: carousel id: carousel
printers: OutputDevice.receivedPrintJobs ? OutputDevice.printers : [null]
} }
} }

View file

@ -90,67 +90,4 @@ Item {
source: "DiscoverUM3Action.qml"; source: "DiscoverUM3Action.qml";
} }
} }
Column {
anchors.fill: parent;
objectName: "networkPrinterConnectionInfo";
spacing: UM.Theme.getSize("default_margin").width;
visible: isUM3;
Button {
onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication();
text: catalog.i18nc("@action:button", "Request Access");
tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer");
visible: printerConnected && !printerAcceptsCommands && !authenticationRequested;
}
Row {
anchors {
left: parent.left;
right: parent.right;
}
height: childrenRect.height;
spacing: UM.Theme.getSize("default_margin").width;
visible: printerConnected;
Column {
Repeater {
model: CuraApplication.getExtrudersModel()
Label {
text: model.name;
}
}
}
Column {
Repeater {
id: nozzleColumn;
model: hotendIds
Label {
text: nozzleColumn.model[index];
}
}
}
Column {
Repeater {
id: materialColumn;
model: materialNames
Label {
text: materialColumn.model[index];
}
}
}
}
Button {
onClicked: manager.loadConfigurationFromPrinter();
text: catalog.i18nc("@action:button", "Activate Configuration");
tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura");
visible: false; // printerConnected && !isClusterPrinter()
}
}
} }

View file

@ -193,4 +193,3 @@ class DiscoverUM3Action(MachineAction):
# Create extra components # Create extra components
CuraApplication.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton")) CuraApplication.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton"))
CuraApplication.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo"))

View file

@ -2,7 +2,6 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import json import json
import os import os
import urllib.parse
from typing import Dict, TYPE_CHECKING, Set from typing import Dict, TYPE_CHECKING, Set
from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
@ -10,9 +9,7 @@ from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
from UM.Application import Application from UM.Application import Application
from UM.Job import Job from UM.Job import Job
from UM.Logger import Logger from UM.Logger import Logger
from UM.MimeTypeDatabase import MimeTypeDatabase
from UM.Resources import Resources
from cura.CuraApplication import CuraApplication
# Absolute imports don't work in plugins # Absolute imports don't work in plugins
from .Models import ClusterMaterial, LocalMaterial from .Models import ClusterMaterial, LocalMaterial
@ -37,7 +34,6 @@ class SendMaterialJob(Job):
# #
# \param reply The reply from the printer, a json file. # \param reply The reply from the printer, a json file.
def _onGetRemoteMaterials(self, reply: QNetworkReply) -> None: def _onGetRemoteMaterials(self, reply: QNetworkReply) -> None:
# Got an error from the HTTP request. If we did not receive a 200 something happened. # Got an error from the HTTP request. If we did not receive a 200 something happened.
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
Logger.log("e", "Error fetching materials from printer: %s", reply.errorString()) Logger.log("e", "Error fetching materials from printer: %s", reply.errorString())
@ -52,7 +48,6 @@ class SendMaterialJob(Job):
# #
# \param remote_materials_by_guid The remote materials by GUID. # \param remote_materials_by_guid The remote materials by GUID.
def _sendMissingMaterials(self, remote_materials_by_guid: Dict[str, ClusterMaterial]) -> None: def _sendMissingMaterials(self, remote_materials_by_guid: Dict[str, ClusterMaterial]) -> None:
# Collect local materials # Collect local materials
local_materials_by_guid = self._getLocalMaterials() local_materials_by_guid = self._getLocalMaterials()
if len(local_materials_by_guid) == 0: if len(local_materials_by_guid) == 0:
@ -91,22 +86,22 @@ class SendMaterialJob(Job):
# #
# \param materials_to_send A set with id's of materials that must be sent. # \param materials_to_send A set with id's of materials that must be sent.
def _sendMaterials(self, materials_to_send: Set[str]) -> None: def _sendMaterials(self, materials_to_send: Set[str]) -> None:
file_paths = Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.MaterialInstanceContainer) container_registry = Application.getInstance().getContainerRegistry()
material_manager = Application.getInstance().getMaterialManager()
material_group_dict = material_manager.getAllMaterialGroups()
# Find all local material files and send them if needed. for root_material_id in material_group_dict:
for file_path in file_paths: if root_material_id not in materials_to_send:
try:
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
except MimeTypeDatabase.MimeTypeNotFoundError:
continue
file_name = os.path.basename(file_path)
material_id = urllib.parse.unquote_plus(mime_type.stripExtension(file_name))
if material_id not in materials_to_send:
# If the material does not have to be sent we skip it. # If the material does not have to be sent we skip it.
continue continue
self._sendMaterialFile(file_path, file_name, material_id) file_path = container_registry.getContainerFilePathById(root_material_id)
if not file_path:
Logger.log("w", "Cannot get file path for material container [%s]", root_material_id)
continue
file_name = os.path.basename(file_path)
self._sendMaterialFile(file_path, file_name, root_material_id)
## Send a single material file to the printer. ## Send a single material file to the printer.
# #
@ -116,7 +111,6 @@ class SendMaterialJob(Job):
# \param file_name The name of the material file. # \param file_name The name of the material file.
# \param material_id The ID of the material in the file. # \param material_id The ID of the material in the file.
def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None: def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None:
parts = [] parts = []
# Add the material file. # Add the material file.
@ -171,27 +165,31 @@ class SendMaterialJob(Job):
# \return a dictionary of LocalMaterial objects by GUID # \return a dictionary of LocalMaterial objects by GUID
def _getLocalMaterials(self) -> Dict[str, LocalMaterial]: def _getLocalMaterials(self) -> Dict[str, LocalMaterial]:
result = {} # type: Dict[str, LocalMaterial] result = {} # type: Dict[str, LocalMaterial]
container_registry = Application.getInstance().getContainerRegistry() material_manager = Application.getInstance().getMaterialManager()
material_containers = container_registry.findContainersMetadata(type = "material")
material_group_dict = material_manager.getAllMaterialGroups()
# Find the latest version of all material containers in the registry. # Find the latest version of all material containers in the registry.
for material in material_containers: for root_material_id, material_group in material_group_dict.items():
material_metadata = material_group.root_material_node.getMetadata()
try: try:
# material version must be an int # material version must be an int
material["version"] = int(material["version"]) material_metadata["version"] = int(material_metadata["version"])
# Create a new local material # Create a new local material
local_material = LocalMaterial(**material) local_material = LocalMaterial(**material_metadata)
local_material.id = root_material_id
if local_material.GUID not in result or \ if local_material.GUID not in result or \
local_material.version > result.get(local_material.GUID).version: local_material.version > result.get(local_material.GUID).version:
result[local_material.GUID] = local_material result[local_material.GUID] = local_material
except KeyError: except KeyError:
Logger.logException("w", "Local material {} has missing values.".format(material["id"])) Logger.logException("w", "Local material {} has missing values.".format(material_metadata["id"]))
except ValueError: except ValueError:
Logger.logException("w", "Local material {} has invalid values.".format(material["id"])) Logger.logException("w", "Local material {} has invalid values.".format(material_metadata["id"]))
except TypeError: except TypeError:
Logger.logException("w", "Local material {} has invalid values.".format(material["id"])) Logger.logException("w", "Local material {} has invalid values.".format(material_metadata["id"]))
return result return result

View file

@ -114,6 +114,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
if key == um_network_key: if key == um_network_key:
if not self._discovered_devices[key].isConnected(): if not self._discovered_devices[key].isConnected():
Logger.log("d", "Attempting to connect with [%s]" % key) Logger.log("d", "Attempting to connect with [%s]" % key)
active_machine.setMetaDataEntry("connection_type", self._discovered_devices[key].getConnectionType().value)
self._discovered_devices[key].connect() self._discovered_devices[key].connect()
self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged) self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
else: else:

View file

@ -1,26 +1,29 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# 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.
import copy
import io import io
import json import json
from unittest import TestCase, mock from unittest import TestCase, mock
from unittest.mock import patch, call from unittest.mock import patch, call, MagicMock
from PyQt5.QtCore import QByteArray from PyQt5.QtCore import QByteArray
from UM.MimeTypeDatabase import MimeType
from UM.Application import Application from UM.Application import Application
from cura.Machines.MaterialGroup import MaterialGroup
from cura.Machines.MaterialNode import MaterialNode
from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob
_FILES_MAP = {"generic_pla_white": "/materials/generic_pla_white.xml.fdm_material",
"generic_pla_black": "/materials/generic_pla_black.xml.fdm_material",
}
@patch("builtins.open", lambda _, __: io.StringIO("<xml></xml>")) @patch("builtins.open", lambda _, __: io.StringIO("<xml></xml>"))
@patch("UM.MimeTypeDatabase.MimeTypeDatabase.getMimeTypeForFile",
lambda _: MimeType(name = "application/x-ultimaker-material-profile", comment = "Ultimaker Material Profile",
suffixes = ["xml.fdm_material"]))
@patch("UM.Resources.Resources.getAllResourcesOfType", lambda _: ["/materials/generic_pla_white.xml.fdm_material"])
@patch("plugins.UM3NetworkPrinting.src.ClusterUM3OutputDevice")
@patch("PyQt5.QtNetwork.QNetworkReply")
class TestSendMaterialJob(TestCase): class TestSendMaterialJob(TestCase):
# version 1
_LOCAL_MATERIAL_WHITE = {"type": "material", "status": "unknown", "id": "generic_pla_white", _LOCAL_MATERIAL_WHITE = {"type": "material", "status": "unknown", "id": "generic_pla_white",
"base_file": "generic_pla_white", "setting_version": "5", "name": "White PLA", "base_file": "generic_pla_white", "setting_version": "5", "name": "White PLA",
"brand": "Generic", "material": "PLA", "color_name": "White", "brand": "Generic", "material": "PLA", "color_name": "White",
@ -29,6 +32,37 @@ class TestSendMaterialJob(TestCase):
"properties": {"density": "1.00", "diameter": "2.85", "weight": "750"}, "properties": {"density": "1.00", "diameter": "2.85", "weight": "750"},
"definition": "fdmprinter", "compatible": True} "definition": "fdmprinter", "compatible": True}
# version 2
_LOCAL_MATERIAL_WHITE_NEWER = {"type": "material", "status": "unknown", "id": "generic_pla_white",
"base_file": "generic_pla_white", "setting_version": "5", "name": "White PLA",
"brand": "Generic", "material": "PLA", "color_name": "White",
"GUID": "badb0ee7-87c8-4f3f-9398-938587b67dce", "version": "2",
"color_code": "#ffffff",
"description": "Test PLA White", "adhesion_info": "Use glue.",
"approximate_diameter": "3",
"properties": {"density": "1.00", "diameter": "2.85", "weight": "750"},
"definition": "fdmprinter", "compatible": True}
# invalid version: "one"
_LOCAL_MATERIAL_WHITE_INVALID_VERSION = {"type": "material", "status": "unknown", "id": "generic_pla_white",
"base_file": "generic_pla_white", "setting_version": "5", "name": "White PLA",
"brand": "Generic", "material": "PLA", "color_name": "White",
"GUID": "badb0ee7-87c8-4f3f-9398-938587b67dce", "version": "one",
"color_code": "#ffffff",
"description": "Test PLA White", "adhesion_info": "Use glue.",
"approximate_diameter": "3",
"properties": {"density": "1.00", "diameter": "2.85", "weight": "750"},
"definition": "fdmprinter", "compatible": True}
_LOCAL_MATERIAL_WHITE_ALL_RESULT = {"generic_pla_white": MaterialGroup("generic_pla_white",
MaterialNode(_LOCAL_MATERIAL_WHITE))}
_LOCAL_MATERIAL_WHITE_NEWER_ALL_RESULT = {"generic_pla_white": MaterialGroup("generic_pla_white",
MaterialNode(_LOCAL_MATERIAL_WHITE_NEWER))}
_LOCAL_MATERIAL_WHITE_INVALID_VERSION_ALL_RESULT = {"generic_pla_white": MaterialGroup("generic_pla_white",
MaterialNode(_LOCAL_MATERIAL_WHITE_INVALID_VERSION))}
_LOCAL_MATERIAL_BLACK = {"type": "material", "status": "unknown", "id": "generic_pla_black", _LOCAL_MATERIAL_BLACK = {"type": "material", "status": "unknown", "id": "generic_pla_black",
"base_file": "generic_pla_black", "setting_version": "5", "name": "Yellow CPE", "base_file": "generic_pla_black", "setting_version": "5", "name": "Yellow CPE",
"brand": "Ultimaker", "material": "CPE", "color_name": "Black", "brand": "Ultimaker", "material": "CPE", "color_name": "Black",
@ -37,6 +71,9 @@ class TestSendMaterialJob(TestCase):
"properties": {"density": "1.01", "diameter": "2.85", "weight": "750"}, "properties": {"density": "1.01", "diameter": "2.85", "weight": "750"},
"definition": "fdmprinter", "compatible": True} "definition": "fdmprinter", "compatible": True}
_LOCAL_MATERIAL_BLACK_ALL_RESULT = {"generic_pla_black": MaterialGroup("generic_pla_black",
MaterialNode(_LOCAL_MATERIAL_BLACK))}
_REMOTE_MATERIAL_WHITE = { _REMOTE_MATERIAL_WHITE = {
"guid": "badb0ee7-87c8-4f3f-9398-938587b67dce", "guid": "badb0ee7-87c8-4f3f-9398-938587b67dce",
"material": "PLA", "material": "PLA",
@ -55,14 +92,17 @@ class TestSendMaterialJob(TestCase):
"density": 1.00 "density": 1.00
} }
def test_run(self, device_mock, reply_mock): def test_run(self):
device_mock = MagicMock()
job = SendMaterialJob(device_mock) job = SendMaterialJob(device_mock)
job.run() job.run()
# We expect the materials endpoint to be called when the job runs. # We expect the materials endpoint to be called when the job runs.
device_mock.get.assert_called_with("materials/", on_finished = job._onGetRemoteMaterials) device_mock.get.assert_called_with("materials/", on_finished = job._onGetRemoteMaterials)
def test__onGetRemoteMaterials_withFailedRequest(self, reply_mock, device_mock): def test__onGetRemoteMaterials_withFailedRequest(self):
reply_mock = MagicMock()
device_mock = MagicMock()
reply_mock.attribute.return_value = 404 reply_mock.attribute.return_value = 404
job = SendMaterialJob(device_mock) job = SendMaterialJob(device_mock)
job._onGetRemoteMaterials(reply_mock) job._onGetRemoteMaterials(reply_mock)
@ -70,7 +110,9 @@ class TestSendMaterialJob(TestCase):
# We expect the device not to be called for any follow up. # We expect the device not to be called for any follow up.
self.assertEqual(0, device_mock.createFormPart.call_count) self.assertEqual(0, device_mock.createFormPart.call_count)
def test__onGetRemoteMaterials_withWrongEncoding(self, reply_mock, device_mock): def test__onGetRemoteMaterials_withWrongEncoding(self):
reply_mock = MagicMock()
device_mock = MagicMock()
reply_mock.attribute.return_value = 200 reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("cp500")) reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("cp500"))
job = SendMaterialJob(device_mock) job = SendMaterialJob(device_mock)
@ -79,7 +121,9 @@ class TestSendMaterialJob(TestCase):
# Given that the parsing fails we do no expect the device to be called for any follow up. # Given that the parsing fails we do no expect the device to be called for any follow up.
self.assertEqual(0, device_mock.createFormPart.call_count) self.assertEqual(0, device_mock.createFormPart.call_count)
def test__onGetRemoteMaterials_withBadJsonAnswer(self, reply_mock, device_mock): def test__onGetRemoteMaterials_withBadJsonAnswer(self):
reply_mock = MagicMock()
device_mock = MagicMock()
reply_mock.attribute.return_value = 200 reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(b"Six sick hicks nick six slick bricks with picks and sticks.") reply_mock.readAll.return_value = QByteArray(b"Six sick hicks nick six slick bricks with picks and sticks.")
job = SendMaterialJob(device_mock) job = SendMaterialJob(device_mock)
@ -88,7 +132,9 @@ class TestSendMaterialJob(TestCase):
# Given that the parsing fails we do no expect the device to be called for any follow up. # Given that the parsing fails we do no expect the device to be called for any follow up.
self.assertEqual(0, device_mock.createFormPart.call_count) self.assertEqual(0, device_mock.createFormPart.call_count)
def test__onGetRemoteMaterials_withMissingGuidInRemoteMaterial(self, reply_mock, device_mock): def test__onGetRemoteMaterials_withMissingGuidInRemoteMaterial(self):
reply_mock = MagicMock()
device_mock = MagicMock()
reply_mock.attribute.return_value = 200 reply_mock.attribute.return_value = 200
remote_material_without_guid = self._REMOTE_MATERIAL_WHITE.copy() remote_material_without_guid = self._REMOTE_MATERIAL_WHITE.copy()
del remote_material_without_guid["guid"] del remote_material_without_guid["guid"]
@ -99,18 +145,20 @@ class TestSendMaterialJob(TestCase):
# Given that parsing fails we do not expect the device to be called for any follow up. # Given that parsing fails we do not expect the device to be called for any follow up.
self.assertEqual(0, device_mock.createFormPart.call_count) self.assertEqual(0, device_mock.createFormPart.call_count)
@patch("cura.Machines.MaterialManager.MaterialManager")
@patch("cura.Settings.CuraContainerRegistry") @patch("cura.Settings.CuraContainerRegistry")
@patch("UM.Application") @patch("UM.Application")
def test__onGetRemoteMaterials_withInvalidVersionInLocalMaterial(self, application_mock, container_registry_mock, def test__onGetRemoteMaterials_withInvalidVersionInLocalMaterial(self, application_mock, container_registry_mock,
reply_mock, device_mock): material_manager_mock):
reply_mock = MagicMock()
device_mock = MagicMock()
application_mock.getContainerRegistry.return_value = container_registry_mock
application_mock.getMaterialManager.return_value = material_manager_mock
reply_mock.attribute.return_value = 200 reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii")) reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
localMaterialWhiteWithInvalidVersion = self._LOCAL_MATERIAL_WHITE.copy() material_manager_mock.getAllMaterialGroups.return_value = self._LOCAL_MATERIAL_WHITE_INVALID_VERSION_ALL_RESULT.copy()
localMaterialWhiteWithInvalidVersion["version"] = "one"
container_registry_mock.findContainersMetadata.return_value = [localMaterialWhiteWithInvalidVersion]
application_mock.getContainerRegistry.return_value = container_registry_mock
with mock.patch.object(Application, "getInstance", new = lambda: application_mock): with mock.patch.object(Application, "getInstance", new = lambda: application_mock):
job = SendMaterialJob(device_mock) job = SendMaterialJob(device_mock)
@ -118,15 +166,16 @@ class TestSendMaterialJob(TestCase):
self.assertEqual(0, device_mock.createFormPart.call_count) self.assertEqual(0, device_mock.createFormPart.call_count)
@patch("cura.Settings.CuraContainerRegistry") @patch("UM.Application.Application.getInstance")
@patch("UM.Application") def test__onGetRemoteMaterials_withNoUpdate(self, application_mock):
def test__onGetRemoteMaterials_withNoUpdate(self, application_mock, container_registry_mock, reply_mock, reply_mock = MagicMock()
device_mock): device_mock = MagicMock()
application_mock.getContainerRegistry.return_value = container_registry_mock container_registry_mock = application_mock.getContainerRegistry.return_value
material_manager_mock = application_mock.getMaterialManager.return_value
device_mock.createFormPart.return_value = "_xXx_" device_mock.createFormPart.return_value = "_xXx_"
container_registry_mock.findContainersMetadata.return_value = [self._LOCAL_MATERIAL_WHITE] material_manager_mock.getAllMaterialGroups.return_value = self._LOCAL_MATERIAL_WHITE_ALL_RESULT.copy()
reply_mock.attribute.return_value = 200 reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii")) reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
@ -138,24 +187,25 @@ class TestSendMaterialJob(TestCase):
self.assertEqual(0, device_mock.createFormPart.call_count) self.assertEqual(0, device_mock.createFormPart.call_count)
self.assertEqual(0, device_mock.postFormWithParts.call_count) self.assertEqual(0, device_mock.postFormWithParts.call_count)
@patch("cura.Settings.CuraContainerRegistry") @patch("UM.Application.Application.getInstance")
@patch("UM.Application") def test__onGetRemoteMaterials_withUpdatedMaterial(self, get_instance_mock):
def test__onGetRemoteMaterials_withUpdatedMaterial(self, application_mock, container_registry_mock, reply_mock, reply_mock = MagicMock()
device_mock): device_mock = MagicMock()
application_mock.getContainerRegistry.return_value = container_registry_mock application_mock = get_instance_mock.return_value
container_registry_mock = application_mock.getContainerRegistry.return_value
material_manager_mock = application_mock.getMaterialManager.return_value
container_registry_mock.getContainerFilePathById = lambda x: _FILES_MAP.get(x)
device_mock.createFormPart.return_value = "_xXx_" device_mock.createFormPart.return_value = "_xXx_"
localMaterialWhiteWithHigherVersion = self._LOCAL_MATERIAL_WHITE.copy() material_manager_mock.getAllMaterialGroups.return_value = self._LOCAL_MATERIAL_WHITE_NEWER_ALL_RESULT.copy()
localMaterialWhiteWithHigherVersion["version"] = "2"
container_registry_mock.findContainersMetadata.return_value = [localMaterialWhiteWithHigherVersion]
reply_mock.attribute.return_value = 200 reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii")) reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
with mock.patch.object(Application, "getInstance", new = lambda: application_mock): job = SendMaterialJob(device_mock)
job = SendMaterialJob(device_mock) job._onGetRemoteMaterials(reply_mock)
job._onGetRemoteMaterials(reply_mock)
self.assertEqual(1, device_mock.createFormPart.call_count) self.assertEqual(1, device_mock.createFormPart.call_count)
self.assertEqual(1, device_mock.postFormWithParts.call_count) self.assertEqual(1, device_mock.postFormWithParts.call_count)
@ -164,16 +214,21 @@ class TestSendMaterialJob(TestCase):
call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)], call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)],
device_mock.method_calls) device_mock.method_calls)
@patch("cura.Settings.CuraContainerRegistry") @patch("UM.Application.Application.getInstance")
@patch("UM.Application") def test__onGetRemoteMaterials_withNewMaterial(self, application_mock):
def test__onGetRemoteMaterials_withNewMaterial(self, application_mock, container_registry_mock, reply_mock, reply_mock = MagicMock()
device_mock): device_mock = MagicMock()
application_mock.getContainerRegistry.return_value = container_registry_mock container_registry_mock = application_mock.getContainerRegistry.return_value
material_manager_mock = application_mock.getMaterialManager.return_value
container_registry_mock.getContainerFilePathById = lambda x: _FILES_MAP.get(x)
device_mock.createFormPart.return_value = "_xXx_" device_mock.createFormPart.return_value = "_xXx_"
container_registry_mock.findContainersMetadata.return_value = [self._LOCAL_MATERIAL_WHITE, all_results = self._LOCAL_MATERIAL_WHITE_ALL_RESULT.copy()
self._LOCAL_MATERIAL_BLACK] for key, value in self._LOCAL_MATERIAL_BLACK_ALL_RESULT.items():
all_results[key] = value
material_manager_mock.getAllMaterialGroups.return_value = all_results
reply_mock.attribute.return_value = 200 reply_mock.attribute.return_value = 200
reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_BLACK]).encode("ascii")) reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_BLACK]).encode("ascii"))

View file

@ -0,0 +1,68 @@
import configparser
from typing import Tuple, List, Set
import io
from UM.VersionUpgrade import VersionUpgrade
from cura.PrinterOutputDevice import ConnectionType
deleted_settings = {"bridge_wall_max_overhang"} # type: Set[str]
class VersionUpgrade35to40(VersionUpgrade):
# Upgrades stacks to have the new version number.
def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation=None)
parser.read_string(serialized)
# Update version number.
parser["general"]["version"] = "4"
parser["metadata"]["setting_version"] = "6"
if parser["metadata"].get("um_network_key") is not None or parser["metadata"].get("octoprint_api_key") is not None:
# Set the connection type if um_network_key or the octoprint key is set.
parser["metadata"]["connection_type"] = str(ConnectionType.NetworkConnection.value)
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
pass
def getCfgVersion(self, serialised: str) -> int:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
setting_version = int(parser.get("metadata", "setting_version", fallback = "0"))
return format_version * 1000000 + setting_version
## Upgrades Preferences to have the new version number.
def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation=None)
parser.read_string(serialized)
if "metadata" not in parser:
parser["metadata"] = {}
parser["general"]["version"] = "6"
parser["metadata"]["setting_version"] = "6"
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]
## Upgrades instance containers to have the new version
# number.
def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation=None)
parser.read_string(serialized)
# Update version number.
parser["general"]["version"] = "4"
parser["metadata"]["setting_version"] = "6"
#self._resetConcentric3DInfillPattern(parser)
if "values" in parser:
for deleted_setting in deleted_settings:
if deleted_setting not in parser["values"]:
continue
del parser["values"][deleted_setting]
result = io.StringIO()
parser.write(result)
return [filename], [result.getvalue()]

View file

@ -0,0 +1,56 @@
from typing import Dict, Any
from . import VersionUpgrade35to40
upgrade = VersionUpgrade35to40.VersionUpgrade35to40()
def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
("preferences", 6000005): ("preferences", 6000006, upgrade.upgradePreferences),
("definition_changes", 4000005): ("definition_changes", 4000006, upgrade.upgradeInstanceContainer),
("quality_changes", 4000005): ("quality_changes", 4000006, upgrade.upgradeInstanceContainer),
("quality", 4000005): ("quality", 4000006, upgrade.upgradeInstanceContainer),
("user", 4000005): ("user", 4000006, upgrade.upgradeInstanceContainer),
("machine_stack", 4000005): ("machine_stack", 4000006, upgrade.upgradeStack),
("extruder_train", 4000005): ("extruder_train", 4000006, upgrade.upgradeStack),
},
"sources": {
"preferences": {
"get_version": upgrade.getCfgVersion,
"location": {"."}
},
"machine_stack": {
"get_version": upgrade.getCfgVersion,
"location": {"./machine_instances"}
},
"extruder_train": {
"get_version": upgrade.getCfgVersion,
"location": {"./extruders"}
},
"definition_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./definition_changes"}
},
"quality_changes": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality_changes"}
},
"quality": {
"get_version": upgrade.getCfgVersion,
"location": {"./quality"}
},
"user": {
"get_version": upgrade.getCfgVersion,
"location": {"./user"}
}
}
}
def register(app) -> Dict[str, Any]:
return {"version_upgrade": upgrade}

View file

@ -0,0 +1,8 @@
{
"name": "Version Upgrade 3.5 to 4.0",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Upgrades configurations from Cura 3.5 to Cura 4.0.",
"api": "6.0",
"i18n-catalog": "cura"
}

View file

@ -16,7 +16,7 @@ def getMetaData():
"mimetype": "application/x-ultimaker-material-profile" "mimetype": "application/x-ultimaker-material-profile"
}, },
"version_upgrade": { "version_upgrade": {
("materials", 1000000): ("materials", 1000004, upgrader.upgradeMaterial), ("materials", 1000000): ("materials", 1000006, upgrader.upgradeMaterial),
}, },
"sources": { "sources": {
"materials": { "materials": {

View file

@ -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",
@ -1585,4 +1602,4 @@
} }
} }
} }
} }

View file

@ -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
}
} }

View file

@ -31,6 +31,13 @@ Column
id: information id: information
width: parent.width width: parent.width
height: childrenRect.height height: childrenRect.height
PrintInformationWidget
{
id: printInformationPanel
visible: !preSlicedData
anchors.right: parent.right
}
Column Column
{ {
@ -50,15 +57,7 @@ Column
text: preSlicedData ? catalog.i18nc("@label", "No time estimation available") : PrintInformation.currentPrintTime.getDisplayString(UM.DurationFormat.Long) text: preSlicedData ? catalog.i18nc("@label", "No time estimation available") : PrintInformation.currentPrintTime.getDisplayString(UM.DurationFormat.Long)
source: UM.Theme.getIcon("clock") source: UM.Theme.getIcon("clock")
font: UM.Theme.getFont("large_bold") font: UM.Theme.getFont("medium_bold")
PrintInformationWidget
{
id: printInformationPanel
visible: !preSlicedData
anchors.left: parent.left
anchors.leftMargin: parent.contentWidth + UM.Theme.getSize("default_margin").width
}
} }
Cura.IconWithText Cura.IconWithText
@ -91,43 +90,8 @@ Column
return totalWeights + "g · " + totalLengths.toFixed(2) + "m" return totalWeights + "g · " + totalLengths.toFixed(2) + "m"
} }
source: UM.Theme.getIcon("spool") source: UM.Theme.getIcon("spool")
Item
{
id: additionalComponents
width: childrenRect.width
anchors.right: parent.right
height: parent.height
Row
{
id: additionalComponentsRow
anchors.right: parent.right
anchors.bottom: parent.bottom
spacing: UM.Theme.getSize("default_margin").width
}
}
Component.onCompleted: addAdditionalComponents("saveButton")
Connections
{
target: CuraApplication
onAdditionalComponentsChanged: addAdditionalComponents("saveButton")
}
function addAdditionalComponents (areaId)
{
if(areaId == "saveButton")
{
for (var component in CuraApplication.additionalComponents["saveButton"])
{
CuraApplication.additionalComponents["saveButton"][component].parent = additionalComponentsRow
}
}
}
} }
} }
} }
Item Item

View file

@ -110,8 +110,7 @@ Column
height: parent.height height: parent.height
anchors.right: additionalComponents.left anchors.right: parent.right
anchors.rightMargin: additionalComponents.width != 0 ? UM.Theme.getSize("default_margin").width : 0
anchors.left: parent.left anchors.left: parent.left
text: catalog.i18nc("@button", "Slice") text: catalog.i18nc("@button", "Slice")
@ -128,45 +127,12 @@ Column
height: parent.height height: parent.height
anchors.left: parent.left anchors.left: parent.left
anchors.right: additionalComponents.left anchors.right: parent.right
anchors.rightMargin: additionalComponents.width != 0 ? UM.Theme.getSize("default_margin").width : 0
text: catalog.i18nc("@button", "Cancel") text: catalog.i18nc("@button", "Cancel")
enabled: sliceButton.enabled enabled: sliceButton.enabled
visible: !sliceButton.visible visible: !sliceButton.visible
onClicked: sliceOrStopSlicing() onClicked: sliceOrStopSlicing()
} }
Item
{
id: additionalComponents
width: childrenRect.width
anchors.right: parent.right
height: parent.height
Row
{
id: additionalComponentsRow
anchors.verticalCenter: parent.verticalCenter
spacing: UM.Theme.getSize("default_margin").width
}
}
Component.onCompleted: prepareButtons.addAdditionalComponents("saveButton")
Connections
{
target: CuraApplication
onAdditionalComponentsChanged: prepareButtons.addAdditionalComponents("saveButton")
}
function addAdditionalComponents (areaId)
{
if(areaId == "saveButton")
{
for (var component in CuraApplication.additionalComponents["saveButton"])
{
CuraApplication.additionalComponents["saveButton"][component].parent = additionalComponentsRow
}
}
}
} }
@ -194,7 +160,7 @@ Column
shortcut: "Ctrl+P" shortcut: "Ctrl+P"
onTriggered: onTriggered:
{ {
if (prepareButton.enabled) if (sliceButton.enabled)
{ {
sliceOrStopSlicing() sliceOrStopSlicing()
} }

View 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
}
}

View file

@ -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
@ -248,11 +248,59 @@ UM.MainWindow
Cura.ActionPanelWidget Cura.ActionPanelWidget
{ {
id: actionPanelWidget
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.rightMargin: UM.Theme.getSize("thick_margin").width anchors.rightMargin: UM.Theme.getSize("thick_margin").width
anchors.bottomMargin: UM.Theme.getSize("thick_margin").height anchors.bottomMargin: UM.Theme.getSize("thick_margin").height
visible: CuraApplication.platformActivity
/*
Show this panel only if there is something on the build plate, and there is NOT an opaque item in front of the build plate.
This cannot be solved by Z indexing! If you want to try solving this, please increase this counter when you're done:
Number of people having tried to fix this by z-indexing: 2
The problem arises from the following render order requirements:
- The stage menu must be rendered above the stage main.
- The stage main must be rendered above the action panel (because the monitor page must be rendered above the action panel).
- The action panel must be rendered above the expandable components drop-down.
However since the expandable components drop-downs are child elements of the stage menu,
they can't be rendered lower than elements that are lower than the stage menu.
Therefore we opted to forego the second requirement and hide the action panel instead when something obscures it (except the expandable components).
We assume that QQuickRectangles are always opaque and any other item is not.
*/
visible: CuraApplication.platformActivity && (main.item == null || !qmlTypeOf(main.item, "QQuickRectangle"))
}
Item
{
id: additionalComponents
width: childrenRect.width
anchors.right: actionPanelWidget.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.bottom: actionPanelWidget.bottom
anchors.bottomMargin: UM.Theme.getSize("thick_margin").height * 2
visible: actionPanelWidget.visible
Row
{
id: additionalComponentsRow
anchors.verticalCenter: parent.verticalCenter
spacing: UM.Theme.getSize("default_margin").width
}
}
Component.onCompleted: contentItem.addAdditionalComponents()
Connections
{
target: CuraApplication
onAdditionalComponentsChanged: contentItem.addAdditionalComponents("saveButton")
}
function addAdditionalComponents()
{
for (var component in CuraApplication.additionalComponents["saveButton"])
{
CuraApplication.additionalComponents["saveButton"][component].parent = additionalComponentsRow
}
} }
Loader Loader
@ -815,4 +863,21 @@ UM.MainWindow
} }
} }
} }
/**
* Function to check whether a QML object has a certain type.
* Taken from StackOverflow: https://stackoverflow.com/a/28384228 and
* adapted to our code style.
* Licensed under CC BY-SA 3.0.
* \param obj The QtObject to get the name of.
* \param class_name (str) The name of the class to check against. Has to be
* the QtObject class name, not the QML entity name.
*/
function qmlTypeOf(obj, class_name)
{
//className plus "(" is the class instance without modification.
//className plus "_QML" is the class instance with user-defined properties.
var str = obj.toString();
return str.indexOf(class_name + "(") == 0 || str.indexOf(class_name + "_QML") == 0;
}
} }

View file

@ -23,7 +23,7 @@ Item
} }
} }
// This component will appear when there is no configurations (e.g. when losing connection) // This component will appear when there are no configurations (e.g. when losing connection or when they are being loaded)
Item Item
{ {
width: parent.width width: parent.width
@ -51,7 +51,11 @@ Item
anchors.left: icon.right anchors.left: icon.right
anchors.right: parent.right anchors.right: parent.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width anchors.leftMargin: UM.Theme.getSize("default_margin").width
text: catalog.i18nc("@label", "Downloading the configurations from the remote printer") // There are two cases that we want to diferenciate, one is when Cura is loading the configurations and the
// other when the connection was lost
text: Cura.MachineManager.printerConnected ?
catalog.i18nc("@label", "Loading available configurations from the printer...") :
catalog.i18nc("@label", "The configurations are not available because the printer is disconnected.")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
renderType: Text.NativeRendering renderType: Text.NativeRendering

View file

@ -173,6 +173,59 @@ Cura.ExpandablePopup
} }
} }
Item
{
height: visible ? childrenRect.height: 0
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
width: childrenRect.width + UM.Theme.getSize("default_margin").width
visible: popupItem.configuration_method == ConfigurationMenu.ConfigurationMethod.Custom
UM.RecolorImage
{
id: externalLinkIcon
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
height: materialInfoLabel.height
width: height
sourceSize.height: width
color: UM.Theme.getColor("text_link")
source: UM.Theme.getIcon("external_link")
}
Label
{
id: materialInfoLabel
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "See the material compatibility chart")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_link")
linkColor: UM.Theme.getColor("text_link")
anchors.left: externalLinkIcon.right
anchors.leftMargin: UM.Theme.getSize("narrow_margin").width
renderType: Text.NativeRendering
MouseArea
{
anchors.fill: parent
hoverEnabled: true
onClicked:
{
// open the material URL with web browser
var url = "https://ultimaker.com/incoming-links/cura/material-compatibilty"
Qt.openUrlExternally(url)
}
onEntered:
{
materialInfoLabel.font.underline = true
}
onExited:
{
materialInfoLabel.font.underline = false
}
}
}
}
Rectangle Rectangle
{ {
id: separator id: separator

View file

@ -126,132 +126,15 @@ UM.ManagementPage
} }
} }
Grid
{
id: machineInfo
anchors.top: machineActions.visible ? machineActions.bottom : machineActions.anchors.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.right: parent.right
spacing: UM.Theme.getSize("default_margin").height
rowSpacing: UM.Theme.getSize("default_lining").height
columns: 2
visible: base.currentItem
property bool printerConnected: Cura.MachineManager.printerConnected
property var connectedPrinter: printerConnected ? Cura.MachineManager.printerOutputDevices[0] : null
property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands
property var printJob: connectedPrinter != null ? connectedPrinter.activePrintJob: null
Label
{
text: catalog.i18nc("@label", "Printer type:")
visible: base.currentItem && "definition_name" in base.currentItem.metadata
}
Label
{
text: (base.currentItem && "definition_name" in base.currentItem.metadata) ? base.currentItem.metadata.definition_name : ""
}
Label
{
text: catalog.i18nc("@label", "Connection:")
visible: base.currentItem && base.currentItem.id == Cura.MachineManager.activeMachineId
}
Label
{
width: (parent.width * 0.7) | 0
text: machineInfo.printerConnected ? machineInfo.connectedPrinter.connectionText : catalog.i18nc("@info:status", "The printer is not connected.")
visible: base.currentItem && base.currentItem.id == Cura.MachineManager.activeMachineId
wrapMode: Text.WordWrap
}
Label
{
text: catalog.i18nc("@label", "State:")
visible: base.currentItem && base.currentItem.id == Cura.MachineManager.activeMachineId && machineInfo.printerAcceptsCommands
}
Label {
width: (parent.width * 0.7) | 0
text:
{
if(!machineInfo.printerConnected || !machineInfo.printerAcceptsCommands) {
return "";
}
if (machineInfo.printJob == null)
{
return catalog.i18nc("@label:MonitorStatus", "Waiting for a printjob");
}
switch(machineInfo.printJob.state)
{
case "printing":
return catalog.i18nc("@label:MonitorStatus", "Printing...");
case "paused":
return catalog.i18nc("@label:MonitorStatus", "Paused");
case "pre_print":
return catalog.i18nc("@label:MonitorStatus", "Preparing...");
case "wait_cleanup":
return catalog.i18nc("@label:MonitorStatus", "Waiting for someone to clear the build plate");
case "error":
return printerOutputDevice.errorText;
case "maintenance":
return catalog.i18nc("@label:MonitorStatus", "In maintenance. Please check the printer");
case "abort": // note sure if this jobState actually occurs in the wild
return catalog.i18nc("@label:MonitorStatus", "Aborting print...");
}
return ""
}
visible: base.currentItem && base.currentItem.id == Cura.MachineManager.activeMachineId && machineInfo.printerAcceptsCommands
wrapMode: Text.WordWrap
}
}
Column {
id: additionalComponentsColumn
anchors.left: parent.left
anchors.right: parent.right
anchors.top: machineInfo.visible ? machineInfo.bottom : machineInfo.anchors.top
anchors.topMargin: UM.Theme.getSize("default_margin").width
spacing: UM.Theme.getSize("default_margin").width
visible: base.currentItem && base.currentItem.id == Cura.MachineManager.activeMachineId
Component.onCompleted:
{
for (var component in CuraApplication.additionalComponents["machinesDetailPane"]) {
CuraApplication.additionalComponents["machinesDetailPane"][component].parent = additionalComponentsColumn
}
}
}
Component.onCompleted: {
addAdditionalComponents("machinesDetailPane")
}
Connections {
target: CuraApplication
onAdditionalComponentsChanged: addAdditionalComponents
}
function addAdditionalComponents (areaId) {
if(areaId == "machinesDetailPane") {
for (var component in CuraApplication.additionalComponents["machinesDetailPane"]) {
CuraApplication.additionalComponents["machinesDetailPane"][component].parent = additionalComponentsColumn
}
}
}
UM.I18nCatalog { id: catalog; name: "cura"; } UM.I18nCatalog { id: catalog; name: "cura"; }
UM.ConfirmRemoveDialog UM.ConfirmRemoveDialog
{ {
id: confirmDialog; id: confirmDialog
object: base.currentItem && base.currentItem.name ? base.currentItem.name : ""; object: base.currentItem && base.currentItem.name ? base.currentItem.name : ""
onYes: onYes:
{ {
Cura.MachineManager.removeMachine(base.currentItem.id); Cura.MachineManager.removeMachine(base.currentItem.id)
if(!base.currentItem) if(!base.currentItem)
{ {
objectList.currentIndex = activeMachineIndex() objectList.currentIndex = activeMachineIndex()

View file

@ -376,6 +376,7 @@ Item
width: true ? (parent.width * 0.4) | 0 : parent.width width: true ? (parent.width * 0.4) | 0 : parent.width
frameVisible: true frameVisible: true
clip: true
ListView ListView
{ {

View file

@ -176,6 +176,17 @@ Item
UM.Preferences.setValue("view/settings_list_height", h); UM.Preferences.setValue("view/settings_list_height", h);
} }
} }
UM.RecolorImage
{
width: parent.width * 0.05
height: parent.height * 0.3
anchors.centerIn: parent
source: UM.Theme.getIcon("grip_lines")
color: UM.Theme.getColor("lining")
}
} }
} }
} }

View file

@ -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
ToolTip 1.0 ToolTip.qml CheckBoxWithTooltip 1.0 CheckBoxWithTooltip.qml
ToolTip 1.0 ToolTip.qml

View file

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

Before After
Before After

View file

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 30 6">
<rect width="30" height="2" rx="1" ry="1" />
<rect width="30" height="2" rx="1" ry="1" y="4" />
</svg>

After

Width:  |  Height:  |  Size: 170 B

View file

@ -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")
@ -585,6 +585,7 @@ QtObject
text: control.unit ? control.unit : "" text: control.unit ? control.unit : ""
color: Theme.getColor("setting_unit"); color: Theme.getColor("setting_unit");
font: Theme.getFont("default"); font: Theme.getFont("default");
renderType: Text.NativeRendering
} }
} }
} }

View file

@ -9,6 +9,21 @@
"weight": 40, "weight": 40,
"family": "Noto Sans" "family": "Noto Sans"
}, },
"large_ja_JP": {
"size": 1.35,
"weight": 50,
"family": "Noto Sans"
},
"large_zh_CN": {
"size": 1.35,
"weight": 50,
"family": "Noto Sans"
},
"large_zh_TW": {
"size": 1.35,
"weight": 50,
"family": "Noto Sans"
},
"large_bold": { "large_bold": {
"size": 1.35, "size": 1.35,
"weight": 63, "weight": 63,
@ -19,6 +34,21 @@
"weight": 40, "weight": 40,
"family": "Noto Sans" "family": "Noto Sans"
}, },
"medium_ja_JP": {
"size": 1.16,
"weight": 50,
"family": "Noto Sans"
},
"medium_zh_CN": {
"size": 1.16,
"weight": 50,
"family": "Noto Sans"
},
"medium_zh_TW": {
"size": 1.16,
"weight": 50,
"family": "Noto Sans"
},
"medium_bold": { "medium_bold": {
"size": 1.16, "size": 1.16,
"weight": 63, "weight": 63,
@ -29,21 +59,84 @@
"weight": 40, "weight": 40,
"family": "Noto Sans" "family": "Noto Sans"
}, },
"default_ja_JP": {
"size": 1.0,
"weight": 50,
"family": "Noto Sans"
},
"default_zh_CN": {
"size": 1.0,
"weight": 50,
"family": "Noto Sans"
},
"default_zh_TW": {
"size": 1.0,
"weight": 50,
"family": "Noto Sans"
},
"default_bold": { "default_bold": {
"size": 0.95, "size": 0.95,
"weight": 63, "weight": 63,
"family": "Noto Sans" "family": "Noto Sans"
}, },
"default_bold_ja_JP": {
"size": 1.0,
"weight": 63,
"family": "Noto Sans"
},
"default_bold_zh_CN": {
"size": 1.0,
"weight": 63,
"family": "Noto Sans"
},
"default_bold_zh_TW": {
"size": 1.0,
"weight": 63,
"family": "Noto Sans"
},
"default_italic": { "default_italic": {
"size": 0.95, "size": 0.95,
"weight": 40, "weight": 40,
"italic": true, "italic": true,
"family": "Noto Sans" "family": "Noto Sans"
}, },
"default_italic_ja_JP": {
"size": 1.0,
"weight": 50,
"italic": true,
"family": "Noto Sans"
},
"default_italic_zh_CN": {
"size": 1.0,
"weight": 50,
"italic": true,
"family": "Noto Sans"
},
"default_italic_zh_TW": {
"size": 1.0,
"weight": 50,
"italic": true,
"family": "Noto Sans"
},
"small": { "small": {
"size": 0.7, "size": 0.7,
"weight": 40, "weight": 40,
"family": "Noto Sans" "family": "Noto Sans"
},
"small_ja_JP": {
"size": 0.7,
"weight": 50,
"family": "Noto Sans"
},
"small_zh_CN": {
"size": 0.7,
"weight": 50,
"family": "Noto Sans"
},
"small_zh_TW": {
"size": 0.7,
"weight": 50,
"family": "Noto Sans"
} }
}, },