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

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()
def _onProcessLayersFinished(self, job: ProcessSlicedLayersJob) -> None:
del self._stored_optimized_layer_data[job.getBuildPlate()]
if job.getBuildPlate() in self._stored_optimized_layer_data:
del self._stored_optimized_layer_data[job.getBuildPlate()]
else:
Logger.log("w", "The optimized layer data was already deleted for buildplate %s", job.getBuildPlate())
self._process_layers_job = None
Logger.log("d", "See if there is more to slice(2)...")
self._invokeSlice()

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,7 +16,8 @@ from UM.Extension import Extension
from UM.i18n import i18nCatalog
from UM.Version import Version
import cura
from cura import ApplicationMetadata
from cura import UltimakerCloudAuthentication
from cura.CuraApplication import CuraApplication
from .AuthorsModel import AuthorsModel
@ -30,17 +31,14 @@ i18n_catalog = i18nCatalog("cura")
## The Toolbox class is responsible of communicating with the server through the API
class Toolbox(QObject, Extension):
DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str
DEFAULT_CLOUD_API_VERSION = 1 # type: int
def __init__(self, application: CuraApplication) -> None:
super().__init__()
self._application = application # type: CuraApplication
self._sdk_version = None # type: Optional[Union[str, int]]
self._cloud_api_version = None # type: Optional[int]
self._cloud_api_root = None # type: Optional[str]
self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
self._cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion # type: int
self._cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot # type: str
self._api_url = None # type: Optional[str]
# Network:
@ -182,9 +180,6 @@ class Toolbox(QObject, Extension):
def _onAppInitialized(self) -> None:
self._plugin_registry = self._application.getPluginRegistry()
self._package_manager = self._application.getPackageManager()
self._sdk_version = self._getSDKVersion()
self._cloud_api_version = self._getCloudAPIVersion()
self._cloud_api_root = self._getCloudAPIRoot()
self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
cloud_api_root = self._cloud_api_root,
cloud_api_version = self._cloud_api_version,
@ -195,36 +190,6 @@ class Toolbox(QObject, Extension):
"packages": QUrl("{base_url}/packages".format(base_url = self._api_url))
}
# Get the API root for the packages API depending on Cura version settings.
def _getCloudAPIRoot(self) -> str:
if not hasattr(cura, "CuraVersion"):
return self.DEFAULT_CLOUD_API_ROOT
if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): # type: ignore
return self.DEFAULT_CLOUD_API_ROOT
if not cura.CuraVersion.CuraCloudAPIRoot: # type: ignore
return self.DEFAULT_CLOUD_API_ROOT
return cura.CuraVersion.CuraCloudAPIRoot # type: ignore
# Get the cloud API version from CuraVersion
def _getCloudAPIVersion(self) -> int:
if not hasattr(cura, "CuraVersion"):
return self.DEFAULT_CLOUD_API_VERSION
if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): # type: ignore
return self.DEFAULT_CLOUD_API_VERSION
if not cura.CuraVersion.CuraCloudAPIVersion: # type: ignore
return self.DEFAULT_CLOUD_API_VERSION
return cura.CuraVersion.CuraCloudAPIVersion # type: ignore
# Get the packages version depending on Cura version settings.
def _getSDKVersion(self) -> Union[int, str]:
if not hasattr(cura, "CuraVersion"):
return self._application.getAPIVersion().getMajor()
if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
return self._application.getAPIVersion().getMajor()
if not cura.CuraVersion.CuraSDKVersion: # type: ignore
return self._application.getAPIVersion().getMajor()
return cura.CuraVersion.CuraSDKVersion # type: ignore
@pyqtSlot()
def browsePackages(self) -> None:
# Create the network manager:

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +0,0 @@
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="External-site" fill="#2671E7">
<path d="M15.2542746,0 L10.117115,0 C9.70521103,0 9.37138968,0.331077601 9.37138968,0.7395961 C9.37138968,1.1481146 9.70521103,1.4791922 10.117115,1.4791922 L13.5368139,1.4791922 L6.21853299,8.73732261 C5.92715567,9.02612743 5.92715567,9.49450277 6.21853299,9.78330758 C6.36413212,9.92770999 6.55504068,10 6.74577014,10 C6.93649962,10 7.12740818,9.92770999 7.2730073,9.78330758 L14.5085492,2.60705849 L14.5085492,5.83435463 C14.5085492,6.24287313 14.8423706,6.57395074 15.2542746,6.57395074 C15.6661786,6.57395074 16,6.24287313 16,5.83435463 L16,0.7395961 C16,0.331077601 15.6661786,0 15.2542746,0 Z" id="Path"></path>
<path d="M12.2810964,7.69220962 C11.8840075,7.69220962 11.5621929,8.01402427 11.5621929,8.41111317 L11.5621929,14.0829238 C11.5621929,14.3427581 11.3427581,14.5621929 11.0829238,14.5621929 L1.91707615,14.5621929 C1.65724189,14.5621929 1.43780712,14.3427581 1.43780712,14.0829238 L1.43780712,4.91707615 C1.43780712,4.65724189 1.65724189,4.43780712 1.91707615,4.43780712 L7.60891391,4.43780712 C8.00600281,4.43780712 8.32781747,4.11599245 8.32781747,3.71890356 C8.32781747,3.32181465 8.00600281,3 7.60891391,3 L1.91707615,3 C0.859956439,3 0,3.85995644 0,4.91707615 L0,14.0829238 C0,15.1400436 0.859956439,16 1.91707615,16 L11.0829238,16 C12.1400436,16 13,15.1400436 13,14.0829238 L13,8.41111317 C13,8.01419692 12.6781853,7.69220962 12.2810964,7.69220962 Z" id="Path"></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View file

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

View file

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

View file

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

View file

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

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"
},
"version_upgrade": {
("materials", 1000000): ("materials", 1000004, upgrader.upgradeMaterial),
("materials", 1000000): ("materials", 1000006, upgrader.upgradeMaterial),
},
"sources": {
"materials": {