Merge branch 'master' of github.com:Ultimaker/Cura into CURA-5734-rework-and-unit-test-setting-visibility-preset

This commit is contained in:
Jaime van Kessel 2018-10-17 10:49:29 +02:00
commit 4c5bf3297c
194 changed files with 4821 additions and 3118 deletions

117
cura/API/Account.py Normal file
View File

@ -0,0 +1,117 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Dict, TYPE_CHECKING
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
from UM.i18n import i18nCatalog
from UM.Message import Message
from cura.OAuth2.AuthorizationService import AuthorizationService
from cura.OAuth2.Models import OAuth2Settings
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
i18n_catalog = i18nCatalog("cura")
## The account API provides a version-proof bridge to use Ultimaker Accounts
#
# Usage:
# ``from cura.API import CuraAPI
# api = CuraAPI()
# api.account.login()
# api.account.logout()
# api.account.userProfile # Who is logged in``
#
class Account(QObject):
# Signal emitted when user logged in or out.
loginStateChanged = pyqtSignal(bool)
def __init__(self, application: "CuraApplication", parent = None) -> None:
super().__init__(parent)
self._application = application
self._error_message = None # type: Optional[Message]
self._logged_in = False
self._callback_port = 32118
self._oauth_root = "https://account.ultimaker.com"
self._cloud_api_root = "https://api.ultimaker.com"
self._oauth_settings = OAuth2Settings(
OAUTH_SERVER_URL= self._oauth_root,
CALLBACK_PORT=self._callback_port,
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
CLIENT_ID="um---------------ultimaker_cura_drive_plugin",
CLIENT_SCOPES="user.read drive.backups.read drive.backups.write",
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
)
self._authorization_service = AuthorizationService(self._oauth_settings)
def initialize(self) -> None:
self._authorization_service.initialize(self._application.getPreferences())
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
self._authorization_service.loadAuthDataFromPreferences()
@pyqtProperty(bool, notify=loginStateChanged)
def isLoggedIn(self) -> bool:
return self._logged_in
def _onLoginStateChanged(self, logged_in: bool = False, error_message: Optional[str] = None) -> None:
if error_message:
if self._error_message:
self._error_message.hide()
self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed"))
self._error_message.show()
if self._logged_in != logged_in:
self._logged_in = logged_in
self.loginStateChanged.emit(logged_in)
@pyqtSlot()
def login(self) -> None:
if self._logged_in:
# Nothing to do, user already logged in.
return
self._authorization_service.startAuthorizationFlow()
@pyqtProperty(str, notify=loginStateChanged)
def userName(self):
user_profile = self._authorization_service.getUserProfile()
if not user_profile:
return None
return user_profile.username
@pyqtProperty(str, notify = loginStateChanged)
def profileImageUrl(self):
user_profile = self._authorization_service.getUserProfile()
if not user_profile:
return None
return user_profile.profile_image_url
@pyqtProperty(str, notify=loginStateChanged)
def accessToken(self) -> Optional[str]:
return self._authorization_service.getAccessToken()
# Get the profile of the logged in user
# @returns None if no user is logged in, a dict containing user_id, username and profile_image_url
@pyqtProperty("QVariantMap", notify = loginStateChanged)
def userProfile(self) -> Optional[Dict[str, Optional[str]]]:
user_profile = self._authorization_service.getUserProfile()
if not user_profile:
return None
return user_profile.__dict__
@pyqtSlot()
def logout(self) -> None:
if not self._logged_in:
return # Nothing to do, user isn't logged in.
self._authorization_service.deleteAuthData()

View File

@ -1,9 +1,12 @@
# 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 from typing import Tuple, Optional, TYPE_CHECKING
from cura.Backups.BackupsManager import BackupsManager from cura.Backups.BackupsManager import BackupsManager
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
## The back-ups API provides a version-proof bridge between Cura's ## The back-ups API provides a version-proof bridge between Cura's
# BackupManager and plug-ins that hook into it. # BackupManager and plug-ins that hook into it.
@ -13,9 +16,10 @@ from cura.Backups.BackupsManager import BackupsManager
# api = CuraAPI() # api = CuraAPI()
# api.backups.createBackup() # api.backups.createBackup()
# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})`` # api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
class Backups: class Backups:
manager = BackupsManager() # Re-used instance of the backups manager.
def __init__(self, application: "CuraApplication") -> None:
self.manager = BackupsManager(application)
## 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

View File

@ -1,7 +1,11 @@
# 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 cura.CuraApplication import CuraApplication from typing import TYPE_CHECKING
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
## The Interface.Settings API provides a version-proof bridge between Cura's ## The Interface.Settings API provides a version-proof bridge between Cura's
# (currently) sidebar UI and plug-ins that hook into it. # (currently) sidebar UI and plug-ins that hook into it.
@ -19,8 +23,9 @@ from cura.CuraApplication import CuraApplication
# api.interface.settings.addContextMenuItem(data)`` # api.interface.settings.addContextMenuItem(data)``
class Settings: class Settings:
# Re-used instance of Cura:
application = CuraApplication.getInstance() # type: CuraApplication def __init__(self, application: "CuraApplication") -> None:
self.application = application
## Add items to the sidebar context menu. ## Add items to the sidebar context menu.
# \param menu_item dict containing the menu item to add. # \param menu_item dict containing the menu item to add.

View File

@ -1,9 +1,15 @@
# 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 TYPE_CHECKING
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from cura.API.Interface.Settings import Settings from cura.API.Interface.Settings import Settings
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
## The Interface class serves as a common root for the specific API ## The Interface class serves as a common root for the specific API
# methods for each interface element. # methods for each interface element.
# #
@ -20,5 +26,6 @@ class Interface:
# For now we use the same API version to be consistent. # For now we use the same API version to be consistent.
VERSION = PluginRegistry.APIVersion VERSION = PluginRegistry.APIVersion
# API methods specific to the settings portion of the UI def __init__(self, application: "CuraApplication") -> None:
settings = Settings() # API methods specific to the settings portion of the UI
self.settings = Settings(application)

View File

@ -1,8 +1,17 @@
# 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 Optional, TYPE_CHECKING
from PyQt5.QtCore import QObject, pyqtProperty
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from cura.API.Backups import Backups from cura.API.Backups import Backups
from cura.API.Interface import Interface from cura.API.Interface import Interface
from cura.API.Account import Account
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
## The official Cura API that plug-ins can use to interact with Cura. ## The official Cura API that plug-ins can use to interact with Cura.
# #
@ -10,14 +19,47 @@ from cura.API.Interface import Interface
# this API provides a version-safe interface with proper deprecation warnings # this API provides a version-safe interface with proper deprecation warnings
# etc. Usage of any other methods than the ones provided in this API can cause # etc. Usage of any other methods than the ones provided in this API can cause
# plug-ins to be unstable. # plug-ins to be unstable.
class CuraAPI(QObject):
class CuraAPI:
# For now we use the same API version to be consistent. # For now we use the same API version to be consistent.
VERSION = PluginRegistry.APIVersion VERSION = PluginRegistry.APIVersion
__instance = None # type: "CuraAPI"
_application = None # type: CuraApplication
# Backups API # This is done to ensure that the first time an instance is created, it's forced that the application is set.
backups = Backups() # The main reason for this is that we want to prevent consumers of API to have a dependency on CuraApplication.
# Since the API is intended to be used by plugins, the cura application should have already created this.
def __new__(cls, application: Optional["CuraApplication"] = None):
if cls.__instance is None:
if application is None:
raise Exception("Upon first time creation, the application must be set.")
cls.__instance = super(CuraAPI, cls).__new__(cls)
cls._application = application
return cls.__instance
# Interface API def __init__(self, application: Optional["CuraApplication"] = None) -> None:
interface = Interface() super().__init__(parent = CuraAPI._application)
# Accounts API
self._account = Account(self._application)
# Backups API
self._backups = Backups(self._application)
# Interface API
self._interface = Interface(self._application)
def initialize(self) -> None:
self._account.initialize()
@pyqtProperty(QObject, constant = True)
def account(self) -> "Account":
return self._account
@property
def backups(self) -> "Backups":
return self._backups
@property
def interface(self) -> "Interface":
return self._interface

View File

@ -4,18 +4,18 @@
import io import io
import os import os
import re import re
import shutil import shutil
from typing import Dict, Optional
from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
from typing import Dict, Optional, TYPE_CHECKING
from UM import i18nCatalog from UM import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message from UM.Message import Message
from UM.Platform import Platform from UM.Platform import Platform
from UM.Resources import Resources from UM.Resources import Resources
from cura.CuraApplication import CuraApplication
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
## The back-up class holds all data about a back-up. ## The back-up class holds all data about a back-up.
@ -29,24 +29,25 @@ class Backup:
# Re-use translation catalog. # Re-use translation catalog.
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
def __init__(self, zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None: def __init__(self, application: "CuraApplication", zip_file: bytes = None, meta_data: Dict[str, str] = None) -> None:
self._application = application
self.zip_file = zip_file # type: Optional[bytes] self.zip_file = zip_file # type: Optional[bytes]
self.meta_data = meta_data # type: Optional[Dict[str, str]] self.meta_data = meta_data # type: Optional[Dict[str, str]]
## Create a back-up from the current user config folder. ## Create a back-up from the current user config folder.
def makeFromCurrent(self) -> None: def makeFromCurrent(self) -> None:
cura_release = CuraApplication.getInstance().getVersion() cura_release = self._application.getVersion()
version_data_dir = Resources.getDataStoragePath() version_data_dir = Resources.getDataStoragePath()
Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir) Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
# Ensure all current settings are saved. # Ensure all current settings are saved.
CuraApplication.getInstance().saveSettings() self._application.saveSettings()
# We copy the preferences file to the user data directory in Linux as it's in a different location there. # We copy the preferences file to the user data directory in Linux as it's in a different location there.
# When restoring a backup on Linux, we move it back. # When restoring a backup on Linux, we move it back.
if Platform.isLinux(): if Platform.isLinux():
preferences_file_name = CuraApplication.getInstance().getApplicationName() preferences_file_name = self._application.getApplicationName()
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name)) backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file) Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
@ -112,7 +113,7 @@ class Backup:
"Tried to restore a Cura backup without having proper data or meta data.")) "Tried to restore a Cura backup without having proper data or meta data."))
return False return False
current_version = CuraApplication.getInstance().getVersion() current_version = self._application.getVersion()
version_to_restore = self.meta_data.get("cura_release", "master") version_to_restore = self.meta_data.get("cura_release", "master")
if current_version != version_to_restore: if current_version != version_to_restore:
# Cannot restore version older or newer than current because settings might have changed. # Cannot restore version older or newer than current because settings might have changed.
@ -128,7 +129,7 @@ class Backup:
# Under Linux, preferences are stored elsewhere, so we copy the file to there. # Under Linux, preferences are stored elsewhere, so we copy the file to there.
if Platform.isLinux(): if Platform.isLinux():
preferences_file_name = CuraApplication.getInstance().getApplicationName() preferences_file_name = self._application.getApplicationName()
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name)) backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file) Logger.log("d", "Moving preferences file from %s to %s", backup_preferences_file, preferences_file)

View File

@ -1,11 +1,13 @@
# 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 Dict, Optional, Tuple from typing import Dict, Optional, Tuple, TYPE_CHECKING
from UM.Logger import Logger from UM.Logger import Logger
from cura.Backups.Backup import Backup from cura.Backups.Backup import Backup
from cura.CuraApplication import CuraApplication
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
## The BackupsManager is responsible for managing the creating and restoring of ## The BackupsManager is responsible for managing the creating and restoring of
@ -13,15 +15,15 @@ from cura.CuraApplication import CuraApplication
# #
# Back-ups themselves are represented in a different class. # Back-ups themselves are represented in a different class.
class BackupsManager: class BackupsManager:
def __init__(self): def __init__(self, application: "CuraApplication") -> None:
self._application = CuraApplication.getInstance() self._application = application
## Get a back-up of the current configuration. ## Get a back-up of the current configuration.
# \return A tuple containing a ZipFile (the actual back-up) and a dict # \return A tuple containing a ZipFile (the actual back-up) and a dict
# containing some metadata (like version). # containing some metadata (like version).
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]: def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]:
self._disableAutoSave() self._disableAutoSave()
backup = Backup() backup = Backup(self._application)
backup.makeFromCurrent() backup.makeFromCurrent()
self._enableAutoSave() self._enableAutoSave()
# We don't return a Backup here because we want plugins only to interact with our API and not full objects. # We don't return a Backup here because we want plugins only to interact with our API and not full objects.
@ -39,7 +41,7 @@ class BackupsManager:
self._disableAutoSave() self._disableAutoSave()
backup = Backup(zip_file = zip_file, meta_data = meta_data) backup = Backup(self._application, zip_file = zip_file, meta_data = meta_data)
restored = backup.restore() restored = backup.restore()
if restored: if restored:
# At this point, Cura will need to restart for the changes to take effect. # At this point, Cura will need to restart for the changes to take effect.

View File

@ -718,21 +718,23 @@ class BuildVolume(SceneNode):
# Add prime tower location as disallowed area. # Add prime tower location as disallowed area.
if len(used_extruders) > 1: #No prime tower in single-extrusion. if len(used_extruders) > 1: #No prime tower in single-extrusion.
prime_tower_collision = False
prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders) if len([x for x in used_extruders if x.isEnabled == True]) > 1: #No prime tower if only one extruder is enabled
for extruder_id in prime_tower_areas: prime_tower_collision = False
for prime_tower_area in prime_tower_areas[extruder_id]: prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
for area in result_areas[extruder_id]: for extruder_id in prime_tower_areas:
if prime_tower_area.intersectsPolygon(area) is not None: for prime_tower_area in prime_tower_areas[extruder_id]:
prime_tower_collision = True for area in result_areas[extruder_id]:
if prime_tower_area.intersectsPolygon(area) is not None:
prime_tower_collision = True
break
if prime_tower_collision: #Already found a collision.
break break
if prime_tower_collision: #Already found a collision. if not prime_tower_collision:
break result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
if not prime_tower_collision: result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
result_areas[extruder_id].extend(prime_tower_areas[extruder_id]) else:
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id]) self._error_areas.extend(prime_tower_areas[extruder_id])
else:
self._error_areas.extend(prime_tower_areas[extruder_id])
self._has_errors = len(self._error_areas) > 0 self._has_errors = len(self._error_areas) > 0

View File

@ -1,15 +1,26 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtGui import QImage from PyQt5.QtGui import QImage
from PyQt5.QtQuick import QQuickImageProvider from PyQt5.QtQuick import QQuickImageProvider
from PyQt5.QtCore import QSize from PyQt5.QtCore import QSize
from UM.Application import Application from UM.Application import Application
## Creates screenshots of the current scene.
class CameraImageProvider(QQuickImageProvider): class CameraImageProvider(QQuickImageProvider):
def __init__(self): def __init__(self):
super().__init__(QQuickImageProvider.Image) super().__init__(QQuickImageProvider.Image)
## Request a new image. ## Request a new image.
#
# The image will be taken using the current camera position.
# Only the actual objects in the scene will get rendered. Not the build
# plate and such!
# \param id The ID for the image to create. This is the requested image
# source, with the "image:" scheme and provider identifier removed. It's
# a Qt thing, they'll provide this parameter.
# \param size The dimensions of the image to scale to.
def requestImage(self, id, size): def requestImage(self, id, size):
for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices(): for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
try: try:

View File

@ -4,7 +4,7 @@
import os import os
import sys import sys
import time import time
from typing import cast, TYPE_CHECKING from typing import cast, TYPE_CHECKING, Optional
import numpy import numpy
@ -44,6 +44,7 @@ from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.SetTransformOperation import SetTransformOperation from UM.Operations.SetTransformOperation import SetTransformOperation
from cura.API import CuraAPI
from cura.Arranging.Arrange import Arrange from cura.Arranging.Arrange import Arrange
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
@ -61,6 +62,7 @@ from cura.Scene.CuraSceneController import CuraSceneController
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.SettingFunction import SettingFunction from UM.Settings.SettingFunction import SettingFunction
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.MachineNameValidator import MachineNameValidator from cura.Settings.MachineNameValidator import MachineNameValidator
from cura.Machines.Models.BuildPlateModel import BuildPlateModel from cura.Machines.Models.BuildPlateModel import BuildPlateModel
@ -107,6 +109,7 @@ from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisi
from cura.Settings.ContainerManager import ContainerManager from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
import cura.Settings.cura_empty_instance_containers import cura.Settings.cura_empty_instance_containers
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
from cura.ObjectsModel import ObjectsModel from cura.ObjectsModel import ObjectsModel
@ -174,6 +177,8 @@ class CuraApplication(QtApplication):
self._single_instance = None self._single_instance = None
self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions]
self._cura_package_manager = None self._cura_package_manager = None
self._machine_action_manager = None self._machine_action_manager = None
@ -203,6 +208,7 @@ class CuraApplication(QtApplication):
self._quality_profile_drop_down_menu_model = None self._quality_profile_drop_down_menu_model = None
self._custom_quality_profile_drop_down_menu_model = None self._custom_quality_profile_drop_down_menu_model = None
self._cura_API = CuraAPI(self)
self._physics = None self._physics = None
self._volume = None self._volume = None
@ -241,6 +247,8 @@ class CuraApplication(QtApplication):
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
self._container_registry_class = CuraContainerRegistry self._container_registry_class = CuraContainerRegistry
# Redefined here in order to please the typing.
self._container_registry = None # type: CuraContainerRegistry
from cura.CuraPackageManager import CuraPackageManager from cura.CuraPackageManager import CuraPackageManager
self._package_manager_class = CuraPackageManager self._package_manager_class = CuraPackageManager
@ -265,6 +273,9 @@ class CuraApplication(QtApplication):
help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog.") help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog.")
self._cli_parser.add_argument("file", nargs = "*", help = "Files to load after starting the application.") self._cli_parser.add_argument("file", nargs = "*", help = "Files to load after starting the application.")
def getContainerRegistry(self) -> "CuraContainerRegistry":
return self._container_registry
def parseCliOptions(self): def parseCliOptions(self):
super().parseCliOptions() super().parseCliOptions()
@ -317,6 +328,8 @@ class CuraApplication(QtApplication):
# Adds custom property types, settings types, and extra operators (functions) that need to be registered in # Adds custom property types, settings types, and extra operators (functions) that need to be registered in
# SettingDefinition and SettingFunction. # SettingDefinition and SettingFunction.
def __initializeSettingDefinitionsAndFunctions(self): def __initializeSettingDefinitionsAndFunctions(self):
self._cura_formula_functions = CuraFormulaFunctions(self)
# Need to do this before ContainerRegistry tries to load the machines # Need to do this before ContainerRegistry tries to load the machines
SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True, read_only = True) SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True, read_only = True)
SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True, read_only = True) SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True, read_only = True)
@ -337,10 +350,10 @@ class CuraApplication(QtApplication):
SettingDefinition.addSettingType("optional_extruder", None, str, None) SettingDefinition.addSettingType("optional_extruder", None, str, None)
SettingDefinition.addSettingType("[int]", None, str, None) SettingDefinition.addSettingType("[int]", None, str, None)
SettingFunction.registerOperator("extruderValues", ExtruderManager.getExtruderValues) SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder)
SettingFunction.registerOperator("extruderValue", ExtruderManager.getExtruderValue) SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
SettingFunction.registerOperator("resolveOrValue", ExtruderManager.getResolveOrValue) SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
SettingFunction.registerOperator("defaultExtruderPosition", ExtruderManager.getDefaultExtruderPosition) SettingFunction.registerOperator("defaultExtruderPosition", self._cura_formula_functions.getDefaultExtruderPosition)
# Adds all resources and container related resources. # Adds all resources and container related resources.
def __addAllResourcesAndContainerResources(self) -> None: def __addAllResourcesAndContainerResources(self) -> None:
@ -674,7 +687,7 @@ class CuraApplication(QtApplication):
Logger.log("i", "Initializing quality manager") Logger.log("i", "Initializing quality manager")
from cura.Machines.QualityManager import QualityManager from cura.Machines.QualityManager import QualityManager
self._quality_manager = QualityManager(container_registry, parent = self) self._quality_manager = QualityManager(self, parent = self)
self._quality_manager.initialize() self._quality_manager.initialize()
Logger.log("i", "Initializing machine manager") Logger.log("i", "Initializing machine manager")
@ -704,6 +717,9 @@ class CuraApplication(QtApplication):
# Initialize setting visibility presets model. # Initialize setting visibility presets model.
self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self.getPreferences(), parent = self) self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self.getPreferences(), parent = self)
# Initialize Cura API
self._cura_API.initialize()
# Detect in which mode to run and execute that mode # Detect in which mode to run and execute that mode
if self._is_headless: if self._is_headless:
self.runWithoutGUI() self.runWithoutGUI()
@ -802,6 +818,11 @@ class CuraApplication(QtApplication):
def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel: def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
return self._setting_visibility_presets_model return self._setting_visibility_presets_model
def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
if self._cura_formula_functions is None:
self._cura_formula_functions = CuraFormulaFunctions(self)
return self._cura_formula_functions
def getMachineErrorChecker(self, *args) -> MachineErrorChecker: def getMachineErrorChecker(self, *args) -> MachineErrorChecker:
return self._machine_error_checker return self._machine_error_checker
@ -891,6 +912,9 @@ class CuraApplication(QtApplication):
self._custom_quality_profile_drop_down_menu_model = CustomQualityProfilesDropDownMenuModel(self) self._custom_quality_profile_drop_down_menu_model = CustomQualityProfilesDropDownMenuModel(self)
return self._custom_quality_profile_drop_down_menu_model return self._custom_quality_profile_drop_down_menu_model
def getCuraAPI(self, *args, **kwargs) -> "CuraAPI":
return self._cura_API
## Registers objects for the QML engine to use. ## Registers objects for the QML engine to use.
# #
# \param engine The QML engine. # \param engine The QML engine.
@ -939,6 +963,9 @@ class CuraApplication(QtApplication):
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance) qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel") qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
from cura.API import CuraAPI
qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
# As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work. # As of Qt5.7, it is necessary to get rid of any ".." in the path for the singleton to work.
actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml"))) actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
qmlRegisterSingletonType(actions_url, "Cura", 1, 0, "Actions") qmlRegisterSingletonType(actions_url, "Cura", 1, 0, "Actions")
@ -1578,6 +1605,11 @@ class CuraApplication(QtApplication):
job.start() job.start()
def _readMeshFinished(self, job): def _readMeshFinished(self, job):
global_container_stack = self.getGlobalContainerStack()
if not global_container_stack:
Logger.log("w", "Can't load meshes before a printer is added.")
return
nodes = job.getResult() nodes = job.getResult()
file_name = job.getFileName() file_name = job.getFileName()
file_name_lower = file_name.lower() file_name_lower = file_name.lower()
@ -1592,7 +1624,6 @@ class CuraApplication(QtApplication):
for node_ in DepthFirstIterator(root): for node_ in DepthFirstIterator(root):
if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate: if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
fixed_nodes.append(node_) fixed_nodes.append(node_)
global_container_stack = self.getGlobalContainerStack()
machine_width = global_container_stack.getProperty("machine_width", "value") machine_width = global_container_stack.getProperty("machine_width", "value")
machine_depth = global_container_stack.getProperty("machine_depth", "value") machine_depth = global_container_stack.getProperty("machine_depth", "value")
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = fixed_nodes) arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = fixed_nodes)

View File

@ -5,7 +5,6 @@ from typing import TYPE_CHECKING, Optional, cast, Dict, List
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
from UM.Application import Application
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Logger import Logger from UM.Logger import Logger
from UM.Util import parseBool from UM.Util import parseBool
@ -21,7 +20,6 @@ if TYPE_CHECKING:
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from .QualityChangesGroup import QualityChangesGroup from .QualityChangesGroup import QualityChangesGroup
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from UM.Settings.ContainerRegistry import ContainerRegistry
# #
@ -38,11 +36,11 @@ class QualityManager(QObject):
qualitiesUpdated = pyqtSignal() qualitiesUpdated = pyqtSignal()
def __init__(self, container_registry: "ContainerRegistry", parent = None) -> None: def __init__(self, application: "CuraApplication", parent = None) -> None:
super().__init__(parent) super().__init__(parent)
self._application = Application.getInstance() # type: CuraApplication self._application = application
self._material_manager = self._application.getMaterialManager() self._material_manager = self._application.getMaterialManager()
self._container_registry = container_registry self._container_registry = self._application.getContainerRegistry()
self._empty_quality_container = self._application.empty_quality_container self._empty_quality_container = self._application.empty_quality_container
self._empty_quality_changes_container = self._application.empty_quality_changes_container self._empty_quality_changes_container = self._application.empty_quality_changes_container
@ -458,7 +456,7 @@ class QualityManager(QObject):
# stack and clear the user settings. # stack and clear the user settings.
@pyqtSlot(str) @pyqtSlot(str)
def createQualityChanges(self, base_name: str) -> None: def createQualityChanges(self, base_name: str) -> None:
machine_manager = Application.getInstance().getMachineManager() machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine global_stack = machine_manager.activeMachine
if not global_stack: if not global_stack:

View File

@ -115,17 +115,24 @@ class VariantManager:
# #
# Gets the default variant for the given machine definition. # Gets the default variant for the given machine definition.
# If the optional GlobalStack is given, the metadata information will be fetched from the GlobalStack instead of
# the DefinitionContainer. Because for machines such as UM2, you can enable Olsson Block, which will set
# "has_variants" to True in the GlobalStack. In those cases, we need to fetch metadata from the GlobalStack or
# it may not be correct.
# #
def getDefaultVariantNode(self, machine_definition: "DefinitionContainer", def getDefaultVariantNode(self, machine_definition: "DefinitionContainer",
variant_type: VariantType) -> Optional["ContainerNode"]: variant_type: "VariantType",
global_stack: Optional["GlobalStack"] = None) -> Optional["ContainerNode"]:
machine_definition_id = machine_definition.getId() machine_definition_id = machine_definition.getId()
container_for_metadata_fetching = global_stack if global_stack is not None else machine_definition
preferred_variant_name = None preferred_variant_name = None
if variant_type == VariantType.BUILD_PLATE: if variant_type == VariantType.BUILD_PLATE:
if parseBool(machine_definition.getMetaDataEntry("has_variant_buildplates", False)): if parseBool(container_for_metadata_fetching.getMetaDataEntry("has_variant_buildplates", False)):
preferred_variant_name = machine_definition.getMetaDataEntry("preferred_variant_buildplate_name") preferred_variant_name = container_for_metadata_fetching.getMetaDataEntry("preferred_variant_buildplate_name")
else: else:
if parseBool(machine_definition.getMetaDataEntry("has_variants", False)): if parseBool(container_for_metadata_fetching.getMetaDataEntry("has_variants", False)):
preferred_variant_name = machine_definition.getMetaDataEntry("preferred_variant_name") preferred_variant_name = container_for_metadata_fetching.getMetaDataEntry("preferred_variant_name")
node = None node = None
if preferred_variant_name: if preferred_variant_name:

View File

@ -0,0 +1,112 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
import random
from hashlib import sha512
from base64 import b64encode
from typing import Dict, Optional
import requests
from UM.Logger import Logger
from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
# Class containing several helpers to deal with the authorization flow.
class AuthorizationHelpers:
def __init__(self, settings: "OAuth2Settings") -> None:
self._settings = settings
self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
@property
# The OAuth2 settings object.
def settings(self) -> "OAuth2Settings":
return self._settings
# Request the access token from the authorization server.
# \param authorization_code: The authorization code from the 1st step.
# \param verification_code: The verification code needed for the PKCE extension.
# \return: An AuthenticationResponse object.
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse":
data = {
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
"grant_type": "authorization_code",
"code": authorization_code,
"code_verifier": verification_code,
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
}
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
# Request the access token from the authorization server using a refresh token.
# \param refresh_token:
# \return: An AuthenticationResponse object.
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
data = {
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
"grant_type": "refresh_token",
"refresh_token": refresh_token,
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
}
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
@staticmethod
# Parse the token response from the authorization server into an AuthenticationResponse object.
# \param token_response: The JSON string data response from the authorization server.
# \return: An AuthenticationResponse object.
def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse":
token_data = None
try:
token_data = json.loads(token_response.text)
except ValueError:
Logger.log("w", "Could not parse token response data: %s", token_response.text)
if not token_data:
return AuthenticationResponse(success=False, err_message="Could not read response.")
if token_response.status_code not in (200, 201):
return AuthenticationResponse(success=False, err_message=token_data["error_description"])
return AuthenticationResponse(success=True,
token_type=token_data["token_type"],
access_token=token_data["access_token"],
refresh_token=token_data["refresh_token"],
expires_in=token_data["expires_in"],
scope=token_data["scope"])
# Calls the authentication API endpoint to get the token data.
# \param access_token: The encoded JWT token.
# \return: Dict containing some profile data.
def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
"Authorization": "Bearer {}".format(access_token)
})
if token_request.status_code not in (200, 201):
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
return None
user_data = token_request.json().get("data")
if not user_data or not isinstance(user_data, dict):
Logger.log("w", "Could not parse user data from token: %s", user_data)
return None
return UserProfile(
user_id = user_data["user_id"],
username = user_data["username"],
profile_image_url = user_data.get("profile_image_url", "")
)
@staticmethod
# Generate a 16-character verification code.
# \param code_length: How long should the code be?
def generateVerificationCode(code_length: int = 16) -> str:
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
@staticmethod
# Generates a base64 encoded sha512 encrypted version of a given string.
# \param verification_code:
# \return: The encrypted code in base64 format.
def generateVerificationCodeChallenge(verification_code: str) -> str:
encoded = sha512(verification_code.encode()).digest()
return b64encode(encoded, altchars = b"_-").decode()

View File

@ -0,0 +1,101 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Callable, Tuple, Dict, Any, List, TYPE_CHECKING
from http.server import BaseHTTPRequestHandler
from urllib.parse import parse_qs, urlparse
from cura.OAuth2.Models import AuthenticationResponse, ResponseData, HTTP_STATUS
if TYPE_CHECKING:
from cura.OAuth2.Models import ResponseStatus
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
# This handler handles all HTTP requests on the local web server.
# It also requests the access token for the 2nd stage of the OAuth flow.
class AuthorizationRequestHandler(BaseHTTPRequestHandler):
def __init__(self, request, client_address, server) -> None:
super().__init__(request, client_address, server)
# These values will be injected by the HTTPServer that this handler belongs to.
self.authorization_helpers = None # type: Optional["AuthorizationHelpers"]
self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]]
self.verification_code = None # type: Optional[str]
def do_GET(self) -> None:
# Extract values from the query string.
parsed_url = urlparse(self.path)
query = parse_qs(parsed_url.query)
# Handle the possible requests
if parsed_url.path == "/callback":
server_response, token_response = self._handleCallback(query)
else:
server_response = self._handleNotFound()
token_response = None
# Send the data to the browser.
self._sendHeaders(server_response.status, server_response.content_type, server_response.redirect_uri)
if server_response.data_stream:
# If there is data in the response, we send it.
self._sendData(server_response.data_stream)
if token_response and self.authorization_callback is not None:
# Trigger the callback if we got a response.
# This will cause the server to shut down, so we do it at the very end of the request handling.
self.authorization_callback(token_response)
# Handler for the callback URL redirect.
# \param query: Dict containing the HTTP query parameters.
# \return: HTTP ResponseData containing a success page to show to the user.
def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]:
code = self._queryGet(query, "code")
if code and self.authorization_helpers is not None and self.verification_code is not None:
# If the code was returned we get the access token.
token_response = self.authorization_helpers.getAccessTokenUsingAuthorizationCode(
code, self.verification_code)
elif self._queryGet(query, "error_code") == "user_denied":
# Otherwise we show an error message (probably the user clicked "Deny" in the auth dialog).
token_response = AuthenticationResponse(
success=False,
err_message="Please give the required permissions when authorizing this application."
)
else:
# We don't know what went wrong here, so instruct the user to check the logs.
token_response = AuthenticationResponse(
success=False,
error_message="Something unexpected happened when trying to log in, please try again."
)
if self.authorization_helpers is None:
return ResponseData(), token_response
return ResponseData(
status=HTTP_STATUS["REDIRECT"],
data_stream=b"Redirecting...",
redirect_uri=self.authorization_helpers.settings.AUTH_SUCCESS_REDIRECT if token_response.success else
self.authorization_helpers.settings.AUTH_FAILED_REDIRECT
), token_response
@staticmethod
# Handle all other non-existing server calls.
def _handleNotFound() -> ResponseData:
return ResponseData(status=HTTP_STATUS["NOT_FOUND"], content_type="text/html", data_stream=b"Not found.")
def _sendHeaders(self, status: "ResponseStatus", content_type: str, redirect_uri: str = None) -> None:
self.send_response(status.code, status.message)
self.send_header("Content-type", content_type)
if redirect_uri:
self.send_header("Location", redirect_uri)
self.end_headers()
def _sendData(self, data: bytes) -> None:
self.wfile.write(data)
@staticmethod
# Convenience Helper for getting values from a pre-parsed query string
def _queryGet(query_data: Dict[Any, List], key: str, default: Optional[str]=None) -> Optional[str]:
return query_data.get(key, [default])[0]

View File

@ -0,0 +1,26 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from http.server import HTTPServer
from typing import Callable, Any, TYPE_CHECKING
if TYPE_CHECKING:
from cura.OAuth2.Models import AuthenticationResponse
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
# The authorization request callback handler server.
# This subclass is needed to be able to pass some data to the request handler.
# This cannot be done on the request handler directly as the HTTPServer creates an instance of the handler after
# init.
class AuthorizationRequestServer(HTTPServer):
# Set the authorization helpers instance on the request handler.
def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None:
self.RequestHandlerClass.authorization_helpers = authorization_helpers # type: ignore
# Set the authorization callback on the request handler.
def setAuthorizationCallback(self, authorization_callback: Callable[["AuthenticationResponse"], Any]) -> None:
self.RequestHandlerClass.authorization_callback = authorization_callback # type: ignore
# Set the verification code on the request handler.
def setVerificationCode(self, verification_code: str) -> None:
self.RequestHandlerClass.verification_code = verification_code # type: ignore

View File

@ -0,0 +1,168 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
import webbrowser
from typing import Optional, TYPE_CHECKING
from urllib.parse import urlencode
from UM.Logger import Logger
from UM.Signal import Signal
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
from cura.OAuth2.Models import AuthenticationResponse
if TYPE_CHECKING:
from cura.OAuth2.Models import UserProfile, OAuth2Settings
from UM.Preferences import Preferences
class AuthorizationService:
"""
The authorization service is responsible for handling the login flow,
storing user credentials and providing account information.
"""
# Emit signal when authentication is completed.
onAuthStateChanged = Signal()
# Emit signal when authentication failed.
onAuthenticationError = Signal()
def __init__(self, settings: "OAuth2Settings", preferences: Optional["Preferences"] = None) -> None:
self._settings = settings
self._auth_helpers = AuthorizationHelpers(settings)
self._auth_url = "{}/authorize".format(self._settings.OAUTH_SERVER_URL)
self._auth_data = None # type: Optional[AuthenticationResponse]
self._user_profile = None # type: Optional["UserProfile"]
self._preferences = preferences
self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True)
def initialize(self, preferences: Optional["Preferences"] = None) -> None:
if preferences is not None:
self._preferences = preferences
if self._preferences:
self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}")
# Get the user profile as obtained from the JWT (JSON Web Token).
# If the JWT is not yet parsed, calling this will take care of that.
# \return UserProfile if a user is logged in, None otherwise.
# \sa _parseJWT
def getUserProfile(self) -> Optional["UserProfile"]:
if not self._user_profile:
# If no user profile was stored locally, we try to get it from JWT.
self._user_profile = self._parseJWT()
if not self._user_profile:
# If there is still no user profile from the JWT, we have to log in again.
return None
return self._user_profile
# Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there.
# \return UserProfile if it was able to parse, None otherwise.
def _parseJWT(self) -> Optional["UserProfile"]:
if not self._auth_data or self._auth_data.access_token is None:
# If no auth data exists, we should always log in again.
return None
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
if user_data:
# If the profile was found, we return it immediately.
return user_data
# The JWT was expired or invalid and we should request a new one.
if self._auth_data.refresh_token is None:
return None
self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
if not self._auth_data or self._auth_data.access_token is None:
# The token could not be refreshed using the refresh token. We should login again.
return None
return self._auth_helpers.parseJWT(self._auth_data.access_token)
# Get the access token as provided by the repsonse data.
def getAccessToken(self) -> Optional[str]:
if not self.getUserProfile():
# We check if we can get the user profile.
# If we can't get it, that means the access token (JWT) was invalid or expired.
return None
if self._auth_data is None:
return None
return self._auth_data.access_token
# Try to refresh the access token. This should be used when it has expired.
def refreshAccessToken(self) -> None:
if self._auth_data is None or self._auth_data.refresh_token is None:
Logger.log("w", "Unable to refresh access token, since there is no refresh token.")
return
self._storeAuthData(self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token))
self.onAuthStateChanged.emit(logged_in=True)
# Delete the authentication data that we have stored locally (eg; logout)
def deleteAuthData(self) -> None:
if self._auth_data is not None:
self._storeAuthData()
self.onAuthStateChanged.emit(logged_in=False)
# Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login.
def startAuthorizationFlow(self) -> None:
Logger.log("d", "Starting new OAuth2 flow...")
# Create the tokens needed for the code challenge (PKCE) extension for OAuth2.
# This is needed because the CuraDrivePlugin is a untrusted (open source) client.
# More details can be found at https://tools.ietf.org/html/rfc7636.
verification_code = self._auth_helpers.generateVerificationCode()
challenge_code = self._auth_helpers.generateVerificationCodeChallenge(verification_code)
# Create the query string needed for the OAuth2 flow.
query_string = urlencode({
"client_id": self._settings.CLIENT_ID,
"redirect_uri": self._settings.CALLBACK_URL,
"scope": self._settings.CLIENT_SCOPES,
"response_type": "code",
"state": "CuraDriveIsAwesome",
"code_challenge": challenge_code,
"code_challenge_method": "S512"
})
# Open the authorization page in a new browser window.
webbrowser.open_new("{}?{}".format(self._auth_url, query_string))
# Start a local web server to receive the callback URL on.
self._server.start(verification_code)
# Callback method for the authentication flow.
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
if auth_response.success:
self._storeAuthData(auth_response)
self.onAuthStateChanged.emit(logged_in=True)
else:
self.onAuthenticationError.emit(logged_in=False, error_message=auth_response.err_message)
self._server.stop() # Stop the web server at all times.
# Load authentication data from preferences.
def loadAuthDataFromPreferences(self) -> None:
if self._preferences is None:
Logger.log("e", "Unable to load authentication data, since no preference has been set!")
return
try:
preferences_data = json.loads(self._preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY))
if preferences_data:
self._auth_data = AuthenticationResponse(**preferences_data)
self.onAuthStateChanged.emit(logged_in=True)
except ValueError:
Logger.logException("w", "Could not load auth data from preferences")
# Store authentication data in preferences.
def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
if self._preferences is None:
Logger.log("e", "Unable to save authentication data, since no preference has been set!")
return
self._auth_data = auth_data
if auth_data:
self._user_profile = self.getUserProfile()
self._preferences.setValue(self._settings.AUTH_DATA_PREFERENCE_KEY, json.dumps(vars(auth_data)))
else:
self._user_profile = None
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)

View File

@ -0,0 +1,64 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import threading
from typing import Optional, Callable, Any, TYPE_CHECKING
from UM.Logger import Logger
from cura.OAuth2.AuthorizationRequestServer import AuthorizationRequestServer
from cura.OAuth2.AuthorizationRequestHandler import AuthorizationRequestHandler
if TYPE_CHECKING:
from cura.OAuth2.Models import AuthenticationResponse
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
class LocalAuthorizationServer:
# The local LocalAuthorizationServer takes care of the oauth2 callbacks.
# Once the flow is completed, this server should be closed down again by calling stop()
# \param auth_helpers: An instance of the authorization helpers class.
# \param auth_state_changed_callback: A callback function to be called when the authorization state changes.
# \param daemon: Whether the server thread should be run in daemon mode. Note: Daemon threads are abruptly stopped
# at shutdown. Their resources (e.g. open files) may never be released.
def __init__(self, auth_helpers: "AuthorizationHelpers",
auth_state_changed_callback: Callable[["AuthenticationResponse"], Any],
daemon: bool) -> None:
self._web_server = None # type: Optional[AuthorizationRequestServer]
self._web_server_thread = None # type: Optional[threading.Thread]
self._web_server_port = auth_helpers.settings.CALLBACK_PORT
self._auth_helpers = auth_helpers
self._auth_state_changed_callback = auth_state_changed_callback
self._daemon = daemon
# Starts the local web server to handle the authorization callback.
# \param verification_code: The verification code part of the OAuth2 client identification.
def start(self, verification_code: str) -> None:
if self._web_server:
# If the server is already running (because of a previously aborted auth flow), we don't have to start it.
# We still inject the new verification code though.
self._web_server.setVerificationCode(verification_code)
return
if self._web_server_port is None:
raise Exception("Unable to start server without specifying the port.")
Logger.log("d", "Starting local web server to handle authorization callback on port %s", self._web_server_port)
# Create the server and inject the callback and code.
self._web_server = AuthorizationRequestServer(("0.0.0.0", self._web_server_port), AuthorizationRequestHandler)
self._web_server.setAuthorizationHelpers(self._auth_helpers)
self._web_server.setAuthorizationCallback(self._auth_state_changed_callback)
self._web_server.setVerificationCode(verification_code)
# Start the server on a new thread.
self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon)
self._web_server_thread.start()
# Stops the web server if it was running. It also does some cleanup.
def stop(self) -> None:
Logger.log("d", "Stopping local oauth2 web server...")
if self._web_server:
self._web_server.server_close()
self._web_server = None
self._web_server_thread = None

60
cura/OAuth2/Models.py Normal file
View File

@ -0,0 +1,60 @@
# Copyright (c) 2018 Ultimaker B.V.
from typing import Optional
class BaseModel:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
# OAuth OAuth2Settings data template.
class OAuth2Settings(BaseModel):
CALLBACK_PORT = None # type: Optional[int]
OAUTH_SERVER_URL = None # type: Optional[str]
CLIENT_ID = None # type: Optional[str]
CLIENT_SCOPES = None # type: Optional[str]
CALLBACK_URL = None # type: Optional[str]
AUTH_DATA_PREFERENCE_KEY = "" # type: str
AUTH_SUCCESS_REDIRECT = "https://ultimaker.com" # type: str
AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str
# User profile data template.
class UserProfile(BaseModel):
user_id = None # type: Optional[str]
username = None # type: Optional[str]
profile_image_url = None # type: Optional[str]
# Authentication data template.
class AuthenticationResponse(BaseModel):
"""Data comes from the token response with success flag and error message added."""
success = True # type: bool
token_type = None # type: Optional[str]
access_token = None # type: Optional[str]
refresh_token = None # type: Optional[str]
expires_in = None # type: Optional[str]
scope = None # type: Optional[str]
err_message = None # type: Optional[str]
# Response status template.
class ResponseStatus(BaseModel):
code = 200 # type: int
message = "" # type str
# Response data template.
class ResponseData(BaseModel):
status = None # type: ResponseStatus
data_stream = None # type: Optional[bytes]
redirect_uri = None # type: Optional[str]
content_type = "text/html" # type: str
# Possible HTTP responses.
HTTP_STATUS = {
"OK": ResponseStatus(code=200, message="OK"),
"NOT_FOUND": ResponseStatus(code=404, message="NOT FOUND"),
"REDIRECT": ResponseStatus(code=302, message="REDIRECT")
}

2
cura/OAuth2/__init__.py Normal file
View File

@ -0,0 +1,2 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.

View File

@ -10,7 +10,6 @@ from typing import Dict
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
from UM.i18n import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
from UM.Qt.Duration import Duration from UM.Qt.Duration import Duration
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
@ -52,6 +51,8 @@ class PrintInformation(QObject):
super().__init__(parent) super().__init__(parent)
self._application = application self._application = application
self.UNTITLED_JOB_NAME = "Untitled"
self.initializeCuraMessagePrintTimeProperties() self.initializeCuraMessagePrintTimeProperties()
self._material_lengths = {} # indexed by build plate number self._material_lengths = {} # indexed by build plate number
@ -70,12 +71,13 @@ class PrintInformation(QObject):
self._base_name = "" self._base_name = ""
self._abbr_machine = "" self._abbr_machine = ""
self._job_name = "" self._job_name = ""
self._project_name = ""
self._active_build_plate = 0 self._active_build_plate = 0
self._initVariablesWithBuildPlate(self._active_build_plate) self._initVariablesWithBuildPlate(self._active_build_plate)
self._multi_build_plate_model = self._application.getMultiBuildPlateModel() self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
ss = self._multi_build_plate_model.maxBuildPlate
self._application.globalContainerStackChanged.connect(self._updateJobName) self._application.globalContainerStackChanged.connect(self._updateJobName)
self._application.globalContainerStackChanged.connect(self.setToZeroPrintInformation) self._application.globalContainerStackChanged.connect(self.setToZeroPrintInformation)
self._application.fileLoaded.connect(self.setBaseName) self._application.fileLoaded.connect(self.setBaseName)
@ -300,13 +302,13 @@ class PrintInformation(QObject):
def _updateJobName(self): def _updateJobName(self):
if self._base_name == "": if self._base_name == "":
self._job_name = "Untitled" self._job_name = self.UNTITLED_JOB_NAME
self._is_user_specified_job_name = False self._is_user_specified_job_name = False
self.jobNameChanged.emit() self.jobNameChanged.emit()
return return
base_name = self._stripAccents(self._base_name) base_name = self._stripAccents(self._base_name)
self._setAbbreviatedMachineName() self._defineAbbreviatedMachineName()
# Only update the job name when it's not user-specified. # Only update the job name when it's not user-specified.
if not self._is_user_specified_job_name: if not self._is_user_specified_job_name:
@ -382,7 +384,7 @@ class PrintInformation(QObject):
## Created an acronym-like abbreviated machine name from the currently ## Created an acronym-like abbreviated machine name from the currently
# active machine name. # active machine name.
# Called each time the global stack is switched. # Called each time the global stack is switched.
def _setAbbreviatedMachineName(self): def _defineAbbreviatedMachineName(self):
global_container_stack = self._application.getGlobalContainerStack() global_container_stack = self._application.getGlobalContainerStack()
if not global_container_stack: if not global_container_stack:
self._abbr_machine = "" self._abbr_machine = ""

View File

@ -0,0 +1,78 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import QObject, QUrl, pyqtSignal, pyqtProperty
from enum import IntEnum
from threading import Thread
from typing import Union
MYPY = False
if MYPY:
from cura.PrinterOutputDevice import PrinterOutputDevice
class FirmwareUpdater(QObject):
firmwareProgressChanged = pyqtSignal()
firmwareUpdateStateChanged = pyqtSignal()
def __init__(self, output_device: "PrinterOutputDevice") -> None:
super().__init__()
self._output_device = output_device
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True)
self._firmware_file = ""
self._firmware_progress = 0
self._firmware_update_state = FirmwareUpdateState.idle
def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None:
# the file path could be url-encoded.
if firmware_file.startswith("file://"):
self._firmware_file = QUrl(firmware_file).toLocalFile()
else:
self._firmware_file = firmware_file
self._setFirmwareUpdateState(FirmwareUpdateState.updating)
self._update_firmware_thread.start()
def _updateFirmware(self) -> None:
raise NotImplementedError("_updateFirmware needs to be implemented")
## Cleanup after a succesful update
def _cleanupAfterUpdate(self) -> None:
# Clean up for next attempt.
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True)
self._firmware_file = ""
self._onFirmwareProgress(100)
self._setFirmwareUpdateState(FirmwareUpdateState.completed)
@pyqtProperty(int, notify = firmwareProgressChanged)
def firmwareProgress(self) -> int:
return self._firmware_progress
@pyqtProperty(int, notify=firmwareUpdateStateChanged)
def firmwareUpdateState(self) -> "FirmwareUpdateState":
return self._firmware_update_state
def _setFirmwareUpdateState(self, state: "FirmwareUpdateState") -> None:
if self._firmware_update_state != state:
self._firmware_update_state = state
self.firmwareUpdateStateChanged.emit()
# Callback function for firmware update progress.
def _onFirmwareProgress(self, progress: int, max_progress: int = 100) -> None:
self._firmware_progress = int(progress * 100 / max_progress) # Convert to scale of 0-100
self.firmwareProgressChanged.emit()
class FirmwareUpdateState(IntEnum):
idle = 0
updating = 1
completed = 2
unknown_error = 3
communication_error = 4
io_error = 5
firmware_not_found_error = 6

View File

@ -1,7 +1,7 @@
# 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 TYPE_CHECKING from typing import TYPE_CHECKING, Set, Union, Optional
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from PyQt5.QtCore import QTimer from PyQt5.QtCore import QTimer
@ -9,27 +9,28 @@ from PyQt5.QtCore import QTimer
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
class GenericOutputController(PrinterOutputController): class GenericOutputController(PrinterOutputController):
def __init__(self, output_device): def __init__(self, output_device: "PrinterOutputDevice") -> None:
super().__init__(output_device) super().__init__(output_device)
self._preheat_bed_timer = QTimer() self._preheat_bed_timer = QTimer()
self._preheat_bed_timer.setSingleShot(True) self._preheat_bed_timer.setSingleShot(True)
self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished) self._preheat_bed_timer.timeout.connect(self._onPreheatBedTimerFinished)
self._preheat_printer = None self._preheat_printer = None # type: Optional[PrinterOutputModel]
self._preheat_hotends_timer = QTimer() self._preheat_hotends_timer = QTimer()
self._preheat_hotends_timer.setSingleShot(True) self._preheat_hotends_timer.setSingleShot(True)
self._preheat_hotends_timer.timeout.connect(self._onPreheatHotendsTimerFinished) self._preheat_hotends_timer.timeout.connect(self._onPreheatHotendsTimerFinished)
self._preheat_hotends = set() self._preheat_hotends = set() # type: Set[ExtruderOutputModel]
self._output_device.printersChanged.connect(self._onPrintersChanged) self._output_device.printersChanged.connect(self._onPrintersChanged)
self._active_printer = None self._active_printer = None # type: Optional[PrinterOutputModel]
def _onPrintersChanged(self): def _onPrintersChanged(self) -> None:
if self._active_printer: if self._active_printer:
self._active_printer.stateChanged.disconnect(self._onPrinterStateChanged) self._active_printer.stateChanged.disconnect(self._onPrinterStateChanged)
self._active_printer.targetBedTemperatureChanged.disconnect(self._onTargetBedTemperatureChanged) self._active_printer.targetBedTemperatureChanged.disconnect(self._onTargetBedTemperatureChanged)
@ -43,32 +44,33 @@ class GenericOutputController(PrinterOutputController):
for extruder in self._active_printer.extruders: for extruder in self._active_printer.extruders:
extruder.targetHotendTemperatureChanged.connect(self._onTargetHotendTemperatureChanged) extruder.targetHotendTemperatureChanged.connect(self._onTargetHotendTemperatureChanged)
def _onPrinterStateChanged(self): def _onPrinterStateChanged(self) -> None:
if self._active_printer.state != "idle": if self._active_printer and self._active_printer.state != "idle":
if self._preheat_bed_timer.isActive(): if self._preheat_bed_timer.isActive():
self._preheat_bed_timer.stop() self._preheat_bed_timer.stop()
self._preheat_printer.updateIsPreheating(False) if self._preheat_printer:
self._preheat_printer.updateIsPreheating(False)
if self._preheat_hotends_timer.isActive(): if self._preheat_hotends_timer.isActive():
self._preheat_hotends_timer.stop() self._preheat_hotends_timer.stop()
for extruder in self._preheat_hotends: for extruder in self._preheat_hotends:
extruder.updateIsPreheating(False) extruder.updateIsPreheating(False)
self._preheat_hotends = set() self._preheat_hotends = set() # type: Set[ExtruderOutputModel]
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed): def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
self._output_device.sendCommand("G91") self._output_device.sendCommand("G91")
self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed)) self._output_device.sendCommand("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
self._output_device.sendCommand("G90") self._output_device.sendCommand("G90")
def homeHead(self, printer): def homeHead(self, printer: "PrinterOutputModel") -> None:
self._output_device.sendCommand("G28 X Y") self._output_device.sendCommand("G28 X Y")
def homeBed(self, printer): def homeBed(self, printer: "PrinterOutputModel") -> None:
self._output_device.sendCommand("G28 Z") self._output_device.sendCommand("G28 Z")
def sendRawCommand(self, printer: "PrinterOutputModel", command: str): def sendRawCommand(self, printer: "PrinterOutputModel", command: str) -> None:
self._output_device.sendCommand(command.upper()) #Most printers only understand uppercase g-code commands. self._output_device.sendCommand(command.upper()) #Most printers only understand uppercase g-code commands.
def setJobState(self, job: "PrintJobOutputModel", state: str): def setJobState(self, job: "PrintJobOutputModel", state: str) -> None:
if state == "pause": if state == "pause":
self._output_device.pausePrint() self._output_device.pausePrint()
job.updateState("paused") job.updateState("paused")
@ -79,15 +81,15 @@ class GenericOutputController(PrinterOutputController):
self._output_device.cancelPrint() self._output_device.cancelPrint()
pass pass
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int) -> None:
self._output_device.sendCommand("M140 S%s" % temperature) self._output_device.sendCommand("M140 S%s" % temperature)
def _onTargetBedTemperatureChanged(self): def _onTargetBedTemperatureChanged(self) -> None:
if self._preheat_bed_timer.isActive() and self._preheat_printer.targetBedTemperature == 0: if self._preheat_bed_timer.isActive() and self._preheat_printer and self._preheat_printer.targetBedTemperature == 0:
self._preheat_bed_timer.stop() self._preheat_bed_timer.stop()
self._preheat_printer.updateIsPreheating(False) self._preheat_printer.updateIsPreheating(False)
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration): def preheatBed(self, printer: "PrinterOutputModel", temperature, duration) -> None:
try: try:
temperature = round(temperature) # The API doesn't allow floating point. temperature = round(temperature) # The API doesn't allow floating point.
duration = round(duration) duration = round(duration)
@ -100,21 +102,25 @@ class GenericOutputController(PrinterOutputController):
self._preheat_printer = printer self._preheat_printer = printer
printer.updateIsPreheating(True) printer.updateIsPreheating(True)
def cancelPreheatBed(self, printer: "PrinterOutputModel"): def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
self.setTargetBedTemperature(printer, temperature=0) self.setTargetBedTemperature(printer, temperature=0)
self._preheat_bed_timer.stop() self._preheat_bed_timer.stop()
printer.updateIsPreheating(False) printer.updateIsPreheating(False)
def _onPreheatBedTimerFinished(self): def _onPreheatBedTimerFinished(self) -> None:
if not self._preheat_printer:
return
self.setTargetBedTemperature(self._preheat_printer, 0) self.setTargetBedTemperature(self._preheat_printer, 0)
self._preheat_printer.updateIsPreheating(False) self._preheat_printer.updateIsPreheating(False)
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: int): def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: Union[int, float]) -> None:
self._output_device.sendCommand("M104 S%s T%s" % (temperature, position)) self._output_device.sendCommand("M104 S%s T%s" % (temperature, position))
def _onTargetHotendTemperatureChanged(self): def _onTargetHotendTemperatureChanged(self) -> None:
if not self._preheat_hotends_timer.isActive(): if not self._preheat_hotends_timer.isActive():
return return
if not self._active_printer:
return
for extruder in self._active_printer.extruders: for extruder in self._active_printer.extruders:
if extruder in self._preheat_hotends and extruder.targetHotendTemperature == 0: if extruder in self._preheat_hotends and extruder.targetHotendTemperature == 0:
@ -123,7 +129,7 @@ class GenericOutputController(PrinterOutputController):
if not self._preheat_hotends: if not self._preheat_hotends:
self._preheat_hotends_timer.stop() self._preheat_hotends_timer.stop()
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration): def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration) -> None:
position = extruder.getPosition() position = extruder.getPosition()
number_of_extruders = len(extruder.getPrinter().extruders) number_of_extruders = len(extruder.getPrinter().extruders)
if position >= number_of_extruders: if position >= number_of_extruders:
@ -141,7 +147,7 @@ class GenericOutputController(PrinterOutputController):
self._preheat_hotends.add(extruder) self._preheat_hotends.add(extruder)
extruder.updateIsPreheating(True) extruder.updateIsPreheating(True)
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"): def cancelPreheatHotend(self, extruder: "ExtruderOutputModel") -> None:
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), temperature=0) self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), temperature=0)
if extruder in self._preheat_hotends: if extruder in self._preheat_hotends:
extruder.updateIsPreheating(False) extruder.updateIsPreheating(False)
@ -149,21 +155,22 @@ class GenericOutputController(PrinterOutputController):
if not self._preheat_hotends and self._preheat_hotends_timer.isActive(): if not self._preheat_hotends and self._preheat_hotends_timer.isActive():
self._preheat_hotends_timer.stop() self._preheat_hotends_timer.stop()
def _onPreheatHotendsTimerFinished(self): def _onPreheatHotendsTimerFinished(self) -> None:
for extruder in self._preheat_hotends: for extruder in self._preheat_hotends:
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0) self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0)
self._preheat_hotends = set() self._preheat_hotends = set() #type: Set[ExtruderOutputModel]
# Cancel any ongoing preheating timers, without setting back the temperature to 0 # Cancel any ongoing preheating timers, without setting back the temperature to 0
# This can be used eg at the start of a print # This can be used eg at the start of a print
def stopPreheatTimers(self): def stopPreheatTimers(self) -> None:
if self._preheat_hotends_timer.isActive(): if self._preheat_hotends_timer.isActive():
for extruder in self._preheat_hotends: for extruder in self._preheat_hotends:
extruder.updateIsPreheating(False) extruder.updateIsPreheating(False)
self._preheat_hotends = set() self._preheat_hotends = set() #type: Set[ExtruderOutputModel]
self._preheat_hotends_timer.stop() self._preheat_hotends_timer.stop()
if self._preheat_bed_timer.isActive(): if self._preheat_bed_timer.isActive():
self._preheat_printer.updateIsPreheating(False) if self._preheat_printer:
self._preheat_printer.updateIsPreheating(False)
self._preheat_bed_timer.stop() self._preheat_bed_timer.stop()

View File

@ -130,9 +130,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
# We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to # We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to
# sleep. # sleep.
if time_since_last_response > self._recreate_network_manager_time: if time_since_last_response > self._recreate_network_manager_time:
if self._last_manager_create_time is None: if self._last_manager_create_time is None or time() - self._last_manager_create_time > self._recreate_network_manager_time:
self._createNetworkManager()
elif time() - self._last_manager_create_time > self._recreate_network_manager_time:
self._createNetworkManager() self._createNetworkManager()
assert(self._manager is not None) assert(self._manager is not None)
elif self._connection_state == ConnectionState.closed: elif self._connection_state == ConnectionState.closed:

View File

@ -91,7 +91,7 @@ class PrintJobOutputModel(QObject):
def assignedPrinter(self): def assignedPrinter(self):
return self._assigned_printer return self._assigned_printer
def updateAssignedPrinter(self, assigned_printer: "PrinterOutputModel"): def updateAssignedPrinter(self, assigned_printer: Optional["PrinterOutputModel"]) -> None:
if self._assigned_printer != assigned_printer: if self._assigned_printer != assigned_printer:
old_printer = self._assigned_printer old_printer = self._assigned_printer
self._assigned_printer = assigned_printer self._assigned_printer = assigned_printer

View File

@ -1,57 +1,68 @@
# Copyright (c) 2017 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 UM.Logger import Logger from UM.Logger import Logger
from UM.Signal import Signal
from typing import Union
MYPY = False MYPY = False
if MYPY: if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
class PrinterOutputController: class PrinterOutputController:
def __init__(self, output_device): def __init__(self, output_device: "PrinterOutputDevice") -> None:
self.can_pause = True self.can_pause = True
self.can_abort = True self.can_abort = True
self.can_pre_heat_bed = True self.can_pre_heat_bed = True
self.can_pre_heat_hotends = True self.can_pre_heat_hotends = True
self.can_send_raw_gcode = True self.can_send_raw_gcode = True
self.can_control_manually = True self.can_control_manually = True
self.can_update_firmware = False
self._output_device = output_device self._output_device = output_device
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", extruder: "ExtruderOutputModel", temperature: int): def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: Union[int, float]) -> None:
Logger.log("w", "Set target hotend temperature not implemented in controller") Logger.log("w", "Set target hotend temperature not implemented in controller")
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int): def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int) -> None:
Logger.log("w", "Set target bed temperature not implemented in controller") Logger.log("w", "Set target bed temperature not implemented in controller")
def setJobState(self, job: "PrintJobOutputModel", state: str): def setJobState(self, job: "PrintJobOutputModel", state: str) -> None:
Logger.log("w", "Set job state not implemented in controller") Logger.log("w", "Set job state not implemented in controller")
def cancelPreheatBed(self, printer: "PrinterOutputModel"): def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
Logger.log("w", "Cancel preheat bed not implemented in controller") Logger.log("w", "Cancel preheat bed not implemented in controller")
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration): def preheatBed(self, printer: "PrinterOutputModel", temperature, duration) -> None:
Logger.log("w", "Preheat bed not implemented in controller") Logger.log("w", "Preheat bed not implemented in controller")
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"): def cancelPreheatHotend(self, extruder: "ExtruderOutputModel") -> None:
Logger.log("w", "Cancel preheat hotend not implemented in controller") Logger.log("w", "Cancel preheat hotend not implemented in controller")
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration): def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration) -> None:
Logger.log("w", "Preheat hotend not implemented in controller") Logger.log("w", "Preheat hotend not implemented in controller")
def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed): def setHeadPosition(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
Logger.log("w", "Set head position not implemented in controller") Logger.log("w", "Set head position not implemented in controller")
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed): def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
Logger.log("w", "Move head not implemented in controller") Logger.log("w", "Move head not implemented in controller")
def homeBed(self, printer: "PrinterOutputModel"): def homeBed(self, printer: "PrinterOutputModel") -> None:
Logger.log("w", "Home bed not implemented in controller") Logger.log("w", "Home bed not implemented in controller")
def homeHead(self, printer: "PrinterOutputModel"): def homeHead(self, printer: "PrinterOutputModel") -> None:
Logger.log("w", "Home head not implemented in controller") Logger.log("w", "Home head not implemented in controller")
def sendRawCommand(self, printer: "PrinterOutputModel", command: str): def sendRawCommand(self, printer: "PrinterOutputModel", command: str) -> None:
Logger.log("w", "Custom command not implemented in controller") Logger.log("w", "Custom command not implemented in controller")
canUpdateFirmwareChanged = Signal()
def setCanUpdateFirmware(self, can_update_firmware: bool) -> None:
if can_update_firmware != self.can_update_firmware:
self.can_update_firmware = can_update_firmware
self.canUpdateFirmwareChanged.emit()

View File

@ -2,7 +2,7 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
from typing import Optional from typing import List, Dict, Optional
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
@ -11,6 +11,7 @@ MYPY = False
if MYPY: if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from cura.PrinterOutput.NetworkCamera import NetworkCamera
class PrinterOutputModel(QObject): class PrinterOutputModel(QObject):
@ -26,6 +27,7 @@ class PrinterOutputModel(QObject):
buildplateChanged = pyqtSignal() buildplateChanged = pyqtSignal()
cameraChanged = pyqtSignal() cameraChanged = pyqtSignal()
configurationChanged = pyqtSignal() configurationChanged = pyqtSignal()
canUpdateFirmwareChanged = pyqtSignal()
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None: def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None:
super().__init__(parent) super().__init__(parent)
@ -34,6 +36,7 @@ class PrinterOutputModel(QObject):
self._name = "" self._name = ""
self._key = "" # Unique identifier self._key = "" # Unique identifier
self._controller = output_controller self._controller = output_controller
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)] self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
self._head_position = Vector(0, 0, 0) self._head_position = Vector(0, 0, 0)
@ -42,7 +45,7 @@ class PrinterOutputModel(QObject):
self._printer_state = "unknown" self._printer_state = "unknown"
self._is_preheating = False self._is_preheating = False
self._printer_type = "" self._printer_type = ""
self._buildplate_name = None self._buildplate_name = ""
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
self._extruders] self._extruders]
@ -50,32 +53,32 @@ class PrinterOutputModel(QObject):
self._camera = None self._camera = None
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def firmwareVersion(self): def firmwareVersion(self) -> str:
return self._firmware_version return self._firmware_version
def setCamera(self, camera): def setCamera(self, camera: Optional["NetworkCamera"]) -> None:
if self._camera is not camera: if self._camera is not camera:
self._camera = camera self._camera = camera
self.cameraChanged.emit() self.cameraChanged.emit()
def updateIsPreheating(self, pre_heating): def updateIsPreheating(self, pre_heating: bool) -> None:
if self._is_preheating != pre_heating: if self._is_preheating != pre_heating:
self._is_preheating = pre_heating self._is_preheating = pre_heating
self.isPreheatingChanged.emit() self.isPreheatingChanged.emit()
@pyqtProperty(bool, notify=isPreheatingChanged) @pyqtProperty(bool, notify=isPreheatingChanged)
def isPreheating(self): def isPreheating(self) -> bool:
return self._is_preheating return self._is_preheating
@pyqtProperty(QObject, notify=cameraChanged) @pyqtProperty(QObject, notify=cameraChanged)
def camera(self): def camera(self) -> Optional["NetworkCamera"]:
return self._camera return self._camera
@pyqtProperty(str, notify = printerTypeChanged) @pyqtProperty(str, notify = printerTypeChanged)
def type(self): def type(self) -> str:
return self._printer_type return self._printer_type
def updateType(self, printer_type): def updateType(self, printer_type: str) -> None:
if self._printer_type != printer_type: if self._printer_type != printer_type:
self._printer_type = printer_type self._printer_type = printer_type
self._printer_configuration.printerType = self._printer_type self._printer_configuration.printerType = self._printer_type
@ -83,10 +86,10 @@ class PrinterOutputModel(QObject):
self.configurationChanged.emit() self.configurationChanged.emit()
@pyqtProperty(str, notify = buildplateChanged) @pyqtProperty(str, notify = buildplateChanged)
def buildplate(self): def buildplate(self) -> str:
return self._buildplate_name return self._buildplate_name
def updateBuildplateName(self, buildplate_name): def updateBuildplateName(self, buildplate_name: str) -> None:
if self._buildplate_name != buildplate_name: if self._buildplate_name != buildplate_name:
self._buildplate_name = buildplate_name self._buildplate_name = buildplate_name
self._printer_configuration.buildplateConfiguration = self._buildplate_name self._printer_configuration.buildplateConfiguration = self._buildplate_name
@ -94,66 +97,66 @@ class PrinterOutputModel(QObject):
self.configurationChanged.emit() self.configurationChanged.emit()
@pyqtProperty(str, notify=keyChanged) @pyqtProperty(str, notify=keyChanged)
def key(self): def key(self) -> str:
return self._key return self._key
def updateKey(self, key: str): def updateKey(self, key: str) -> None:
if self._key != key: if self._key != key:
self._key = key self._key = key
self.keyChanged.emit() self.keyChanged.emit()
@pyqtSlot() @pyqtSlot()
def homeHead(self): def homeHead(self) -> None:
self._controller.homeHead(self) self._controller.homeHead(self)
@pyqtSlot() @pyqtSlot()
def homeBed(self): def homeBed(self) -> None:
self._controller.homeBed(self) self._controller.homeBed(self)
@pyqtSlot(str) @pyqtSlot(str)
def sendRawCommand(self, command: str): def sendRawCommand(self, command: str) -> None:
self._controller.sendRawCommand(self, command) self._controller.sendRawCommand(self, command)
@pyqtProperty("QVariantList", constant = True) @pyqtProperty("QVariantList", constant = True)
def extruders(self): def extruders(self) -> List["ExtruderOutputModel"]:
return self._extruders return self._extruders
@pyqtProperty(QVariant, notify = headPositionChanged) @pyqtProperty(QVariant, notify = headPositionChanged)
def headPosition(self): def headPosition(self) -> Dict[str, float]:
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position.z} return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position.z}
def updateHeadPosition(self, x, y, z): def updateHeadPosition(self, x: float, y: float, z: float) -> None:
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z: if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:
self._head_position = Vector(x, y, z) self._head_position = Vector(x, y, z)
self.headPositionChanged.emit() self.headPositionChanged.emit()
@pyqtProperty(float, float, float) @pyqtProperty(float, float, float)
@pyqtProperty(float, float, float, float) @pyqtProperty(float, float, float, float)
def setHeadPosition(self, x, y, z, speed = 3000): def setHeadPosition(self, x: float, y: float, z: float, speed: float = 3000) -> None:
self.updateHeadPosition(x, y, z) self.updateHeadPosition(x, y, z)
self._controller.setHeadPosition(self, x, y, z, speed) self._controller.setHeadPosition(self, x, y, z, speed)
@pyqtProperty(float) @pyqtProperty(float)
@pyqtProperty(float, float) @pyqtProperty(float, float)
def setHeadX(self, x, speed = 3000): def setHeadX(self, x: float, speed: float = 3000) -> None:
self.updateHeadPosition(x, self._head_position.y, self._head_position.z) self.updateHeadPosition(x, self._head_position.y, self._head_position.z)
self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed) self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed)
@pyqtProperty(float) @pyqtProperty(float)
@pyqtProperty(float, float) @pyqtProperty(float, float)
def setHeadY(self, y, speed = 3000): def setHeadY(self, y: float, speed: float = 3000) -> None:
self.updateHeadPosition(self._head_position.x, y, self._head_position.z) self.updateHeadPosition(self._head_position.x, y, self._head_position.z)
self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed) self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed)
@pyqtProperty(float) @pyqtProperty(float)
@pyqtProperty(float, float) @pyqtProperty(float, float)
def setHeadZ(self, z, speed = 3000): def setHeadZ(self, z: float, speed:float = 3000) -> None:
self.updateHeadPosition(self._head_position.x, self._head_position.y, z) self.updateHeadPosition(self._head_position.x, self._head_position.y, z)
self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed) self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed)
@pyqtSlot(float, float, float) @pyqtSlot(float, float, float)
@pyqtSlot(float, float, float, float) @pyqtSlot(float, float, float, float)
def moveHead(self, x = 0, y = 0, z = 0, speed = 3000): def moveHead(self, x: float = 0, y: float = 0, z: float = 0, speed: float = 3000) -> None:
self._controller.moveHead(self, x, y, z, speed) self._controller.moveHead(self, x, y, z, speed)
## Pre-heats the heated bed of the printer. ## Pre-heats the heated bed of the printer.
@ -162,47 +165,47 @@ class PrinterOutputModel(QObject):
# Celsius. # Celsius.
# \param duration How long the bed should stay warm, in seconds. # \param duration How long the bed should stay warm, in seconds.
@pyqtSlot(float, float) @pyqtSlot(float, float)
def preheatBed(self, temperature, duration): def preheatBed(self, temperature: float, duration: float) -> None:
self._controller.preheatBed(self, temperature, duration) self._controller.preheatBed(self, temperature, duration)
@pyqtSlot() @pyqtSlot()
def cancelPreheatBed(self): def cancelPreheatBed(self) -> None:
self._controller.cancelPreheatBed(self) self._controller.cancelPreheatBed(self)
def getController(self): def getController(self) -> "PrinterOutputController":
return self._controller return self._controller
@pyqtProperty(str, notify=nameChanged) @pyqtProperty(str, notify = nameChanged)
def name(self): def name(self) -> str:
return self._name return self._name
def setName(self, name): def setName(self, name: str) -> None:
self._setName(name) self._setName(name)
self.updateName(name) self.updateName(name)
def updateName(self, name): def updateName(self, name: str) -> None:
if self._name != name: if self._name != name:
self._name = name self._name = name
self.nameChanged.emit() self.nameChanged.emit()
## Update the bed temperature. This only changes it locally. ## Update the bed temperature. This only changes it locally.
def updateBedTemperature(self, temperature): def updateBedTemperature(self, temperature: int) -> None:
if self._bed_temperature != temperature: if self._bed_temperature != temperature:
self._bed_temperature = temperature self._bed_temperature = temperature
self.bedTemperatureChanged.emit() self.bedTemperatureChanged.emit()
def updateTargetBedTemperature(self, temperature): def updateTargetBedTemperature(self, temperature: int) -> None:
if self._target_bed_temperature != temperature: if self._target_bed_temperature != temperature:
self._target_bed_temperature = temperature self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit() self.targetBedTemperatureChanged.emit()
## Set the target bed temperature. This ensures that it's actually sent to the remote. ## Set the target bed temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(int) @pyqtSlot(int)
def setTargetBedTemperature(self, temperature): def setTargetBedTemperature(self, temperature: int) -> None:
self._controller.setTargetBedTemperature(self, temperature) self._controller.setTargetBedTemperature(self, temperature)
self.updateTargetBedTemperature(temperature) self.updateTargetBedTemperature(temperature)
def updateActivePrintJob(self, print_job): def updateActivePrintJob(self, print_job: Optional["PrintJobOutputModel"]) -> None:
if self._active_print_job != print_job: if self._active_print_job != print_job:
old_print_job = self._active_print_job old_print_job = self._active_print_job
@ -214,72 +217,83 @@ class PrinterOutputModel(QObject):
old_print_job.updateAssignedPrinter(None) old_print_job.updateAssignedPrinter(None)
self.activePrintJobChanged.emit() self.activePrintJobChanged.emit()
def updateState(self, printer_state): def updateState(self, printer_state: str) -> None:
if self._printer_state != printer_state: if self._printer_state != printer_state:
self._printer_state = printer_state self._printer_state = printer_state
self.stateChanged.emit() self.stateChanged.emit()
@pyqtProperty(QObject, notify = activePrintJobChanged) @pyqtProperty(QObject, notify = activePrintJobChanged)
def activePrintJob(self): def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
return self._active_print_job return self._active_print_job
@pyqtProperty(str, notify=stateChanged) @pyqtProperty(str, notify=stateChanged)
def state(self): def state(self) -> str:
return self._printer_state return self._printer_state
@pyqtProperty(int, notify = bedTemperatureChanged) @pyqtProperty(int, notify=bedTemperatureChanged)
def bedTemperature(self): def bedTemperature(self) -> int:
return self._bed_temperature return self._bed_temperature
@pyqtProperty(int, notify=targetBedTemperatureChanged) @pyqtProperty(int, notify=targetBedTemperatureChanged)
def targetBedTemperature(self): def targetBedTemperature(self) -> int:
return self._target_bed_temperature return self._target_bed_temperature
# Does the printer support pre-heating the bed at all # Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def canPreHeatBed(self): def canPreHeatBed(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_pre_heat_bed return self._controller.can_pre_heat_bed
return False return False
# Does the printer support pre-heating the bed at all # Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def canPreHeatHotends(self): def canPreHeatHotends(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_pre_heat_hotends return self._controller.can_pre_heat_hotends
return False return False
# Does the printer support sending raw G-code at all # Does the printer support sending raw G-code at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def canSendRawGcode(self): def canSendRawGcode(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_send_raw_gcode return self._controller.can_send_raw_gcode
return False return False
# Does the printer support pause at all # Does the printer support pause at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def canPause(self): def canPause(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_pause return self._controller.can_pause
return False return False
# Does the printer support abort at all # Does the printer support abort at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def canAbort(self): def canAbort(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_abort return self._controller.can_abort
return False return False
# Does the printer support manual control at all # Does the printer support manual control at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant=True)
def canControlManually(self): def canControlManually(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_control_manually return self._controller.can_control_manually
return False return False
# Does the printer support upgrading firmware
@pyqtProperty(bool, notify = canUpdateFirmwareChanged)
def canUpdateFirmware(self) -> bool:
if self._controller:
return self._controller.can_update_firmware
return False
# Stub to connect UM.Signal to pyqtSignal
def _onControllerCanUpdateFirmwareChanged(self) -> None:
self.canUpdateFirmwareChanged.emit()
# Returns the configuration (material, variant and buildplate) of the current printer # Returns the configuration (material, variant and buildplate) of the current printer
@pyqtProperty(QObject, notify = configurationChanged) @pyqtProperty(QObject, notify = configurationChanged)
def printerConfiguration(self): def printerConfiguration(self) -> Optional[ConfigurationModel]:
if self._printer_configuration.isValid(): if self._printer_configuration.isValid():
return self._printer_configuration return self._printer_configuration
return None return None

View File

@ -4,22 +4,24 @@
from UM.Decorators import deprecated from UM.Decorators import deprecated
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.OutputDevice.OutputDevice import OutputDevice from UM.OutputDevice.OutputDevice import OutputDevice
from PyQt5.QtCore import pyqtProperty, QObject, QTimer, pyqtSignal from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from UM.Logger import Logger from UM.Logger import Logger
from UM.FileHandler.FileHandler import FileHandler #For typing.
from UM.Scene.SceneNode import SceneNode #For typing.
from UM.Signal import signalemitter from UM.Signal import signalemitter
from UM.Qt.QtApplication import QtApplication from UM.Qt.QtApplication import QtApplication
from UM.FlameProfiler import pyqtSlot
from enum import IntEnum # For the connection state tracking. from enum import IntEnum # For the connection state tracking.
from typing import Callable, List, Optional from typing import Callable, List, Optional, Union
MYPY = False MYPY = False
if MYPY: if MYPY:
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdater
from UM.FileHandler.FileHandler import FileHandler
from UM.Scene.SceneNode import SceneNode
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -83,6 +85,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
self._connection_state = ConnectionState.closed #type: ConnectionState self._connection_state = ConnectionState.closed #type: ConnectionState
self._firmware_updater = None #type: Optional[FirmwareUpdater]
self._firmware_name = None #type: Optional[str] self._firmware_name = None #type: Optional[str]
self._address = "" #type: str self._address = "" #type: str
self._connection_text = "" #type: str self._connection_text = "" #type: str
@ -128,7 +131,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
return None return None
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None:
raise NotImplementedError("requestWrite needs to be implemented") raise NotImplementedError("requestWrite needs to be implemented")
@pyqtProperty(QObject, notify = printersChanged) @pyqtProperty(QObject, notify = printersChanged)
@ -226,3 +229,13 @@ class PrinterOutputDevice(QObject, OutputDevice):
# This name can be used to define device type # This name can be used to define device type
def getFirmwareName(self) -> Optional[str]: def getFirmwareName(self) -> Optional[str]:
return self._firmware_name return self._firmware_name
def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]:
return self._firmware_updater
@pyqtSlot(str)
def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None:
if not self._firmware_updater:
return
self._firmware_updater.updateFirmware(firmware_file)

View File

@ -5,6 +5,7 @@ from PyQt5.QtCore import QTimer
from UM.Application import Application from UM.Application import Application
from UM.Math.Polygon import Polygon from UM.Math.Polygon import Polygon
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
@ -18,6 +19,8 @@ from typing import TYPE_CHECKING, Any, Optional
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from UM.Mesh.MeshData import MeshData
from UM.Math.Matrix import Matrix
## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node. ## The convex hull decorator is a scene node decorator that adds the convex hull functionality to a scene node.
@ -33,17 +36,17 @@ class ConvexHullDecorator(SceneNodeDecorator):
# Make sure the timer is created on the main thread # Make sure the timer is created on the main thread
self._recompute_convex_hull_timer = None # type: Optional[QTimer] self._recompute_convex_hull_timer = None # type: Optional[QTimer]
from cura.CuraApplication import CuraApplication
if Application.getInstance() is not None: if CuraApplication.getInstance() is not None:
Application.getInstance().callLater(self.createRecomputeConvexHullTimer) CuraApplication.getInstance().callLater(self.createRecomputeConvexHullTimer)
self._raft_thickness = 0.0 self._raft_thickness = 0.0
self._build_volume = Application.getInstance().getBuildVolume() self._build_volume = CuraApplication.getInstance().getBuildVolume()
self._build_volume.raftThicknessChanged.connect(self._onChanged) self._build_volume.raftThicknessChanged.connect(self._onChanged)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged) CuraApplication.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
Application.getInstance().getController().toolOperationStarted.connect(self._onChanged) CuraApplication.getInstance().getController().toolOperationStarted.connect(self._onChanged)
Application.getInstance().getController().toolOperationStopped.connect(self._onChanged) CuraApplication.getInstance().getController().toolOperationStopped.connect(self._onChanged)
self._onGlobalStackChanged() self._onGlobalStackChanged()
@ -61,9 +64,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
previous_node.parentChanged.disconnect(self._onChanged) previous_node.parentChanged.disconnect(self._onChanged)
super().setNode(node) super().setNode(node)
# Mypy doesn't understand that self._node is no longer optional, so just use the node.
self._node.transformationChanged.connect(self._onChanged) node.transformationChanged.connect(self._onChanged)
self._node.parentChanged.connect(self._onChanged) node.parentChanged.connect(self._onChanged)
self._onChanged() self._onChanged()
@ -78,9 +81,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
hull = self._compute2DConvexHull() hull = self._compute2DConvexHull()
if self._global_stack and self._node and hull is not None: if self._global_stack and self._node is not None and hull is not None:
# Parent can be None if node is just loaded. # Parent can be None if node is just loaded.
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")): if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32))) hull = hull.getMinkowskiHull(Polygon(numpy.array(self._global_stack.getProperty("machine_head_polygon", "value"), numpy.float32)))
hull = self._add2DAdhesionMargin(hull) hull = self._add2DAdhesionMargin(hull)
return hull return hull
@ -92,6 +95,13 @@ class ConvexHullDecorator(SceneNodeDecorator):
return self._compute2DConvexHeadFull() return self._compute2DConvexHeadFull()
@staticmethod
def hasGroupAsParent(node: "SceneNode") -> bool:
parent = node.getParent()
if parent is None:
return False
return bool(parent.callDecoration("isGroup"))
## Get convex hull of the object + head size ## Get convex hull of the object + head size
# In case of printing all at once this is the same as the convex hull. # In case of printing all at once this is the same as the convex hull.
# For one at the time this is area with intersection of mirrored head # For one at the time this is area with intersection of mirrored head
@ -100,8 +110,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
return None return None
if self._global_stack: if self._global_stack:
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")): if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
head_with_fans = self._compute2DConvexHeadMin() head_with_fans = self._compute2DConvexHeadMin()
if head_with_fans is None:
return None
head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans) head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans)
return head_with_fans_with_adhesion_margin return head_with_fans_with_adhesion_margin
return None return None
@ -114,7 +126,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
return None return None
if self._global_stack: if self._global_stack:
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and (self._node.getParent() is None or not self._node.getParent().callDecoration("isGroup")): if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
# Printing one at a time and it's not an object in a group # Printing one at a time and it's not an object in a group
return self._compute2DConvexHull() return self._compute2DConvexHull()
return None return None
@ -153,15 +165,17 @@ class ConvexHullDecorator(SceneNodeDecorator):
def _init2DConvexHullCache(self) -> None: def _init2DConvexHullCache(self) -> None:
# Cache for the group code path in _compute2DConvexHull() # Cache for the group code path in _compute2DConvexHull()
self._2d_convex_hull_group_child_polygon = None self._2d_convex_hull_group_child_polygon = None # type: Optional[Polygon]
self._2d_convex_hull_group_result = None self._2d_convex_hull_group_result = None # type: Optional[Polygon]
# Cache for the mesh code path in _compute2DConvexHull() # Cache for the mesh code path in _compute2DConvexHull()
self._2d_convex_hull_mesh = None self._2d_convex_hull_mesh = None # type: Optional[MeshData]
self._2d_convex_hull_mesh_world_transform = None self._2d_convex_hull_mesh_world_transform = None # type: Optional[Matrix]
self._2d_convex_hull_mesh_result = None self._2d_convex_hull_mesh_result = None # type: Optional[Polygon]
def _compute2DConvexHull(self) -> Optional[Polygon]: def _compute2DConvexHull(self) -> Optional[Polygon]:
if self._node is None:
return None
if self._node.callDecoration("isGroup"): if self._node.callDecoration("isGroup"):
points = numpy.zeros((0, 2), dtype=numpy.int32) points = numpy.zeros((0, 2), dtype=numpy.int32)
for child in self._node.getChildren(): for child in self._node.getChildren():
@ -187,47 +201,47 @@ class ConvexHullDecorator(SceneNodeDecorator):
return offset_hull return offset_hull
else: else:
offset_hull = None offset_hull = Polygon([])
if self._node.getMeshData(): mesh = self._node.getMeshData()
mesh = self._node.getMeshData() if mesh is None:
world_transform = self._node.getWorldTransformation()
# Check the cache
if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform:
return self._2d_convex_hull_mesh_result
vertex_data = mesh.getConvexHullTransformedVertices(world_transform)
# Don't use data below 0.
# TODO; We need a better check for this as this gives poor results for meshes with long edges.
# Do not throw away vertices: the convex hull may be too small and objects can collide.
# vertex_data = vertex_data[vertex_data[:,1] >= -0.01]
if len(vertex_data) >= 4:
# Round the vertex data to 1/10th of a mm, then remove all duplicate vertices
# This is done to greatly speed up further convex hull calculations as the convex hull
# becomes much less complex when dealing with highly detailed models.
vertex_data = numpy.round(vertex_data, 1)
vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D.
# Grab the set of unique points.
#
# This basically finds the unique rows in the array by treating them as opaque groups of bytes
# which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch.
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
_, idx = numpy.unique(vertex_byte_view, return_index=True)
vertex_data = vertex_data[idx] # Select the unique rows by index.
hull = Polygon(vertex_data)
if len(vertex_data) >= 3:
convex_hull = hull.getConvexHull()
offset_hull = self._offsetHull(convex_hull)
else:
return Polygon([]) # Node has no mesh data, so just return an empty Polygon. return Polygon([]) # Node has no mesh data, so just return an empty Polygon.
world_transform = self._node.getWorldTransformation()
# Check the cache
if mesh is self._2d_convex_hull_mesh and world_transform == self._2d_convex_hull_mesh_world_transform:
return self._2d_convex_hull_mesh_result
vertex_data = mesh.getConvexHullTransformedVertices(world_transform)
# Don't use data below 0.
# TODO; We need a better check for this as this gives poor results for meshes with long edges.
# Do not throw away vertices: the convex hull may be too small and objects can collide.
# vertex_data = vertex_data[vertex_data[:,1] >= -0.01]
if len(vertex_data) >= 4: # type: ignore # mypy and numpy don't play along well just yet.
# Round the vertex data to 1/10th of a mm, then remove all duplicate vertices
# This is done to greatly speed up further convex hull calculations as the convex hull
# becomes much less complex when dealing with highly detailed models.
vertex_data = numpy.round(vertex_data, 1)
vertex_data = vertex_data[:, [0, 2]] # Drop the Y components to project to 2D.
# Grab the set of unique points.
#
# This basically finds the unique rows in the array by treating them as opaque groups of bytes
# which are as long as the 2 float64s in each row, and giving this view to numpy.unique() to munch.
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
_, idx = numpy.unique(vertex_byte_view, return_index=True)
vertex_data = vertex_data[idx] # Select the unique rows by index.
hull = Polygon(vertex_data)
if len(vertex_data) >= 3:
convex_hull = hull.getConvexHull()
offset_hull = self._offsetHull(convex_hull)
# Store the result in the cache # Store the result in the cache
self._2d_convex_hull_mesh = mesh self._2d_convex_hull_mesh = mesh
self._2d_convex_hull_mesh_world_transform = world_transform self._2d_convex_hull_mesh_world_transform = world_transform
@ -338,7 +352,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property). ## Private convenience function to get a setting from the correct extruder (as defined by limit_to_extruder property).
def _getSettingProperty(self, setting_key: str, prop: str = "value") -> Any: def _getSettingProperty(self, setting_key: str, prop: str = "value") -> Any:
if not self._global_stack: if self._global_stack is None or self._node is None:
return None return None
per_mesh_stack = self._node.callDecoration("getStack") per_mesh_stack = self._node.callDecoration("getStack")
if per_mesh_stack: if per_mesh_stack:
@ -358,7 +372,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
return self._global_stack.getProperty(setting_key, prop) return self._global_stack.getProperty(setting_key, prop)
## Returns True if node is a descendant or the same as the root node. ## Returns True if node is a descendant or the same as the root node.
def __isDescendant(self, root: "SceneNode", node: "SceneNode") -> bool: def __isDescendant(self, root: "SceneNode", node: Optional["SceneNode"]) -> bool:
if node is None: if node is None:
return False return False
if root is node: if root is node:

View File

@ -28,10 +28,10 @@ if TYPE_CHECKING:
from cura.Machines.MaterialNode import MaterialNode from cura.Machines.MaterialNode import MaterialNode
from cura.Machines.QualityChangesGroup import QualityChangesGroup from cura.Machines.QualityChangesGroup import QualityChangesGroup
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.MachineManager import MachineManager from cura.Settings.MachineManager import MachineManager
from cura.Machines.MaterialManager import MaterialManager from cura.Machines.MaterialManager import MaterialManager
from cura.Machines.QualityManager import QualityManager from cura.Machines.QualityManager import QualityManager
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -52,7 +52,7 @@ class ContainerManager(QObject):
self._application = application # type: CuraApplication self._application = application # type: CuraApplication
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
self._container_registry = self._application.getContainerRegistry() # type: ContainerRegistry self._container_registry = self._application.getContainerRegistry() # type: CuraContainerRegistry
self._machine_manager = self._application.getMachineManager() # type: MachineManager self._machine_manager = self._application.getMachineManager() # type: MachineManager
self._material_manager = self._application.getMaterialManager() # type: MaterialManager self._material_manager = self._application.getMaterialManager() # type: MaterialManager
self._quality_manager = self._application.getQualityManager() # type: QualityManager self._quality_manager = self._application.getQualityManager() # type: QualityManager
@ -391,7 +391,8 @@ class ContainerManager(QObject):
continue continue
mime_type = self._container_registry.getMimeTypeForContainer(container_type) mime_type = self._container_registry.getMimeTypeForContainer(container_type)
if mime_type is None:
continue
entry = { entry = {
"type": serialize_type, "type": serialize_type,
"mime": mime_type, "mime": mime_type,

View File

@ -187,11 +187,11 @@ class CuraContainerRegistry(ContainerRegistry):
try: try:
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader. profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
except NoProfileException: except NoProfileException:
return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "No custom profile to import in file <filename>{0}</filename>", file_name)} return { "status": "ok", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "No custom profile to import in file <filename>{0}</filename>", file_name)}
except Exception as e: except Exception as e:
# Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None. # Note that this will fail quickly. That is, if any profile reader throws an exception, it will stop reading. It will only continue reading if the reader returned None.
Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name, profile_reader.getPluginId(), str(e)) Logger.log("e", "Failed to import profile from %s: %s while using profile reader. Got exception %s", file_name, profile_reader.getPluginId(), str(e))
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "\n" + str(e))} return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>:", file_name) + "\n<message>" + str(e) + "</message>"}
if profile_or_list: if profile_or_list:
# Ensure it is always a list of profiles # Ensure it is always a list of profiles
@ -215,7 +215,7 @@ class CuraContainerRegistry(ContainerRegistry):
if not global_profile: if not global_profile:
Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name) Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name)
return { "status": "error", return { "status": "error",
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)} "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)}
profile_definition = global_profile.getMetaDataEntry("definition") profile_definition = global_profile.getMetaDataEntry("definition")
# Make sure we have a profile_definition in the file: # Make sure we have a profile_definition in the file:
@ -225,7 +225,7 @@ class CuraContainerRegistry(ContainerRegistry):
if not machine_definition: if not machine_definition:
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition) Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
return {"status": "error", return {"status": "error",
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name) "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)
} }
machine_definition = machine_definition[0] machine_definition = machine_definition[0]
@ -238,7 +238,7 @@ class CuraContainerRegistry(ContainerRegistry):
if profile_definition != expected_machine_definition: if profile_definition != expected_machine_definition:
Logger.log("e", "Profile [%s] is for machine [%s] but the current active machine is [%s]. Will not import the profile", file_name, profile_definition, expected_machine_definition) Logger.log("e", "Profile [%s] is for machine [%s] but the current active machine is [%s]. Will not import the profile", file_name, profile_definition, expected_machine_definition)
return { "status": "error", return { "status": "error",
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "The machine defined in profile <filename>{0}</filename> ({1}) doesn't match with your current machine ({2}), could not import it.", file_name, profile_definition, expected_machine_definition)} "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "The machine defined in profile <filename>{0}</filename> ({1}) doesn't match with your current machine ({2}), could not import it.", file_name, profile_definition, expected_machine_definition)}
# Fix the global quality profile's definition field in case it's not correct # Fix the global quality profile's definition field in case it's not correct
global_profile.setMetaDataEntry("definition", expected_machine_definition) global_profile.setMetaDataEntry("definition", expected_machine_definition)
@ -269,8 +269,7 @@ class CuraContainerRegistry(ContainerRegistry):
if idx == 0: if idx == 0:
# move all per-extruder settings to the first extruder's quality_changes # move all per-extruder settings to the first extruder's quality_changes
for qc_setting_key in global_profile.getAllKeys(): for qc_setting_key in global_profile.getAllKeys():
settable_per_extruder = global_stack.getProperty(qc_setting_key, settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder")
"settable_per_extruder")
if settable_per_extruder: if settable_per_extruder:
setting_value = global_profile.getProperty(qc_setting_key, "value") setting_value = global_profile.getProperty(qc_setting_key, "value")
@ -310,8 +309,8 @@ class CuraContainerRegistry(ContainerRegistry):
if result is not None: if result is not None:
return {"status": "error", "message": catalog.i18nc( return {"status": "error", "message": catalog.i18nc(
"@info:status Don't translate the XML tags <filename> or <message>!", "@info:status Don't translate the XML tags <filename> or <message>!",
"Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", "Failed to import profile from <filename>{0}</filename>:",
file_name, result)} file_name) + " <message>" + result + "</message>"}
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())} return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
@ -686,7 +685,7 @@ class CuraContainerRegistry(ContainerRegistry):
if not os.path.isfile(file_path): if not os.path.isfile(file_path):
continue continue
parser = configparser.ConfigParser(interpolation=None) parser = configparser.ConfigParser(interpolation = None)
try: try:
parser.read([file_path]) parser.read([file_path])
except: except:

View File

@ -0,0 +1,130 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, List, Optional, TYPE_CHECKING
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from UM.Settings.SettingFunction import SettingFunction
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
from cura.Settings.CuraContainerStack import CuraContainerStack
#
# This class contains all Cura-related custom functions that can be used in formulas. Some functions requires
# information such as the currently active machine, so this is made into a class instead of standalone functions.
#
class CuraFormulaFunctions:
def __init__(self, application: "CuraApplication") -> None:
self._application = application
# ================
# Custom Functions
# ================
# Gets the default extruder position of the currently active machine.
def getDefaultExtruderPosition(self) -> str:
machine_manager = self._application.getMachineManager()
return machine_manager.defaultExtruderPosition
# Gets the given setting key from the given extruder position.
def getValueInExtruder(self, extruder_position: int, property_key: str,
context: Optional["PropertyEvaluationContext"] = None) -> Any:
machine_manager = self._application.getMachineManager()
if extruder_position == -1:
extruder_position = int(machine_manager.defaultExtruderPosition)
global_stack = machine_manager.activeMachine
extruder_stack = global_stack.extruders[str(extruder_position)]
value = extruder_stack.getRawProperty(property_key, "value", context = context)
if isinstance(value, SettingFunction):
value = value(extruder_stack, context = context)
return value
# Gets all extruder values as a list for the given property.
def getValuesInAllExtruders(self, property_key: str,
context: Optional["PropertyEvaluationContext"] = None) -> List[Any]:
machine_manager = self._application.getMachineManager()
extruder_manager = self._application.getExtruderManager()
global_stack = machine_manager.activeMachine
result = []
for extruder in extruder_manager.getActiveExtruderStacks():
if not extruder.isEnabled:
continue
# only include values from extruders that are "active" for the current machine instance
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value", context = context):
continue
value = extruder.getRawProperty(property_key, "value", context = context)
if value is None:
continue
if isinstance(value, SettingFunction):
value = value(extruder, context = context)
result.append(value)
if not result:
result.append(global_stack.getProperty(property_key, "value", context = context))
return result
# Get the resolve value or value for a given key.
def getResolveOrValue(self, property_key: str, context: Optional["PropertyEvaluationContext"] = None) -> Any:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
resolved_value = global_stack.getProperty(property_key, "value", context = context)
return resolved_value
# Gets the default setting value from given extruder position. The default value is what excludes the values in
# the user_changes container.
def getDefaultValueInExtruder(self, extruder_position: int, property_key: str) -> Any:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
extruder_stack = global_stack.extruders[str(extruder_position)]
context = self.createContextForDefaultValueEvaluation(extruder_stack)
return self.getValueInExtruder(extruder_position, property_key, context = context)
# Gets all default setting values as a list from all extruders of the currently active machine.
# The default values are those excluding the values in the user_changes container.
def getDefaultValuesInAllExtruders(self, property_key: str) -> List[Any]:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
context = self.createContextForDefaultValueEvaluation(global_stack)
return self.getValuesInAllExtruders(property_key, context = context)
# Gets the resolve value or value for a given key without looking the first container (user container).
def getDefaultResolveOrValue(self, property_key: str) -> Any:
machine_manager = self._application.getMachineManager()
global_stack = machine_manager.activeMachine
context = self.createContextForDefaultValueEvaluation(global_stack)
return self.getResolveOrValue(property_key, context = context)
# Creates a context for evaluating default values (skip the user_changes container).
def createContextForDefaultValueEvaluation(self, source_stack: "CuraContainerStack") -> "PropertyEvaluationContext":
context = PropertyEvaluationContext(source_stack)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
context.context["override_operators"] = {
"extruderValue": self.getDefaultValueInExtruder,
"extruderValues": self.getDefaultValuesInAllExtruders,
"resolveOrValue": self.getDefaultResolveOrValue,
}
return context

View File

@ -114,7 +114,8 @@ class CuraStackBuilder:
# get variant container for extruders # get variant container for extruders
extruder_variant_container = application.empty_variant_container extruder_variant_container = application.empty_variant_container
extruder_variant_node = variant_manager.getDefaultVariantNode(global_stack.definition, VariantType.NOZZLE) extruder_variant_node = variant_manager.getDefaultVariantNode(global_stack.definition, VariantType.NOZZLE,
global_stack = global_stack)
extruder_variant_name = None extruder_variant_name = None
if extruder_variant_node: if extruder_variant_node:
extruder_variant_container = extruder_variant_node.getContainer() extruder_variant_container = extruder_variant_node.getContainer()

View File

@ -12,9 +12,7 @@ from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID. from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
@ -69,16 +67,6 @@ class ExtruderManager(QObject):
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong. except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
return None return None
## Return extruder count according to extruder trains.
@pyqtProperty(int, notify = extrudersChanged)
def extruderCount(self) -> int:
if not self._application.getGlobalContainerStack():
return 0 # No active machine, so no extruders.
try:
return len(self._extruder_trains[self._application.getGlobalContainerStack().getId()])
except KeyError:
return 0
## Gets a dict with the extruder stack ids with the extruder number as the key. ## Gets a dict with the extruder stack ids with the extruder number as the key.
@pyqtProperty("QVariantMap", notify = extrudersChanged) @pyqtProperty("QVariantMap", notify = extrudersChanged)
def extruderIds(self) -> Dict[str, str]: def extruderIds(self) -> Dict[str, str]:
@ -360,8 +348,19 @@ class ExtruderManager(QObject):
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing # After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this. # "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
def _fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None: def _fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
container_registry = ContainerRegistry.getInstance()
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"] expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
extruder_stack_0 = global_stack.extruders.get("0") extruder_stack_0 = global_stack.extruders.get("0")
# At this point, extruder stacks for this machine may not have been loaded yet. In this case, need to look in
# the container registry as well.
if not global_stack.extruders:
extruder_trains = container_registry.findContainerStacks(type = "extruder_train",
machine = global_stack.getId())
if extruder_trains:
for extruder in extruder_trains:
if extruder.getMetaDataEntry("position") == "0":
extruder_stack_0 = extruder
break
if extruder_stack_0 is None: if extruder_stack_0 is None:
Logger.log("i", "No extruder stack for global stack [%s], create one", global_stack.getId()) Logger.log("i", "No extruder stack for global stack [%s], create one", global_stack.getId())
@ -372,89 +371,10 @@ class ExtruderManager(QObject):
elif extruder_stack_0.definition.getId() != expected_extruder_definition_0_id: elif extruder_stack_0.definition.getId() != expected_extruder_definition_0_id:
Logger.log("e", "Single extruder printer [{printer}] expected extruder [{expected}], but got [{got}]. I'm making it [{expected}].".format( Logger.log("e", "Single extruder printer [{printer}] expected extruder [{expected}], but got [{got}]. I'm making it [{expected}].".format(
printer = global_stack.getId(), expected = expected_extruder_definition_0_id, got = extruder_stack_0.definition.getId())) printer = global_stack.getId(), expected = expected_extruder_definition_0_id, got = extruder_stack_0.definition.getId()))
container_registry = ContainerRegistry.getInstance()
extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0] extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0]
extruder_stack_0.definition = extruder_definition extruder_stack_0.definition = extruder_definition
## Get all extruder values for a certain setting. extruder_stack_0.setNextStack(global_stack)
#
# This is exposed to SettingFunction so it can be used in value functions.
#
# \param key The key of the setting to retrieve values for.
#
# \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list.
# If no extruder has the value, the list will contain the global value.
@staticmethod
def getExtruderValues(key: str) -> List[Any]:
global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()) #We know that there must be a global stack by the time you're requesting setting values.
result = []
for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
if not extruder.isEnabled:
continue
# only include values from extruders that are "active" for the current machine instance
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value"):
continue
value = extruder.getRawProperty(key, "value")
if value is None:
continue
if isinstance(value, SettingFunction):
value = value(extruder)
result.append(value)
if not result:
result.append(global_stack.getProperty(key, "value"))
return result
## Get all extruder values for a certain setting. This function will skip the user settings container.
#
# This is exposed to SettingFunction so it can be used in value functions.
#
# \param key The key of the setting to retrieve values for.
#
# \return A list of values for all extruders. If an extruder does not have a value, it will not be in the list.
# If no extruder has the value, the list will contain the global value.
@staticmethod
def getDefaultExtruderValues(key: str) -> List[Any]:
global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()) #We know that there must be a global stack by the time you're requesting setting values.
context = PropertyEvaluationContext(global_stack)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
context.context["override_operators"] = {
"extruderValue": ExtruderManager.getDefaultExtruderValue,
"extruderValues": ExtruderManager.getDefaultExtruderValues,
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
}
result = []
for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
# only include values from extruders that are "active" for the current machine instance
if int(extruder.getMetaDataEntry("position")) >= global_stack.getProperty("machine_extruder_count", "value", context = context):
continue
value = extruder.getRawProperty(key, "value", context = context)
if value is None:
continue
if isinstance(value, SettingFunction):
value = value(extruder, context = context)
result.append(value)
if not result:
result.append(global_stack.getProperty(key, "value", context = context))
return result
## Return the default extruder position from the machine manager
@staticmethod
def getDefaultExtruderPosition() -> str:
return cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition
## Get all extruder values for a certain setting. ## Get all extruder values for a certain setting.
# #
@ -464,62 +384,8 @@ class ExtruderManager(QObject):
# #
# \return String representing the extruder values # \return String representing the extruder values
@pyqtSlot(str, result="QVariant") @pyqtSlot(str, result="QVariant")
def getInstanceExtruderValues(self, key) -> List: def getInstanceExtruderValues(self, key: str) -> List:
return ExtruderManager.getExtruderValues(key) return self._application.getCuraFormulaFunctions().getValuesInAllExtruders(key)
## Get the value for a setting from a specific extruder.
#
# This is exposed to SettingFunction to use in value functions.
#
# \param extruder_index The index of the extruder to get the value from.
# \param key The key of the setting to get the value of.
#
# \return The value of the setting for the specified extruder or for the
# global stack if not found.
@staticmethod
def getExtruderValue(extruder_index: int, key: str) -> Any:
if extruder_index == -1:
extruder_index = int(cura.CuraApplication.CuraApplication.getInstance().getMachineManager().defaultExtruderPosition)
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
if extruder:
value = extruder.getRawProperty(key, "value")
if isinstance(value, SettingFunction):
value = value(extruder)
else:
# Just a value from global.
value = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()).getProperty(key, "value")
return value
## Get the default value from the given extruder. This function will skip the user settings container.
#
# This is exposed to SettingFunction to use in value functions.
#
# \param extruder_index The index of the extruder to get the value from.
# \param key The key of the setting to get the value of.
#
# \return The value of the setting for the specified extruder or for the
# global stack if not found.
@staticmethod
def getDefaultExtruderValue(extruder_index: int, key: str) -> Any:
extruder = ExtruderManager.getInstance().getExtruderStack(extruder_index)
context = PropertyEvaluationContext(extruder)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
context.context["override_operators"] = {
"extruderValue": ExtruderManager.getDefaultExtruderValue,
"extruderValues": ExtruderManager.getDefaultExtruderValues,
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
}
if extruder:
value = extruder.getRawProperty(key, "value", context = context)
if isinstance(value, SettingFunction):
value = value(extruder, context = context)
else: # Just a value from global.
value = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack()).getProperty(key, "value", context = context)
return value
## Get the resolve value or value for a given key ## Get the resolve value or value for a given key
# #
@ -535,28 +401,6 @@ class ExtruderManager(QObject):
return resolved_value return resolved_value
## Get the resolve value or value for a given key without looking the first container (user container)
#
# This is the effective value for a given key, it is used for values in the global stack.
# This is exposed to SettingFunction to use in value functions.
# \param key The key of the setting to get the value of.
#
# \return The effective value
@staticmethod
def getDefaultResolveOrValue(key: str) -> Any:
global_stack = cast(GlobalStack, cura.CuraApplication.CuraApplication.getInstance().getGlobalContainerStack())
context = PropertyEvaluationContext(global_stack)
context.context["evaluate_from_container_index"] = 1 # skip the user settings container
context.context["override_operators"] = {
"extruderValue": ExtruderManager.getDefaultExtruderValue,
"extruderValues": ExtruderManager.getDefaultExtruderValues,
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
}
resolved_value = global_stack.getProperty(key, "value", context = context)
return resolved_value
__instance = None # type: ExtruderManager __instance = None # type: ExtruderManager
@classmethod @classmethod

View File

@ -4,7 +4,7 @@
from collections import defaultdict from collections import defaultdict
import threading import threading
from typing import Any, Dict, Optional, Set, TYPE_CHECKING from typing import Any, Dict, Optional, Set, TYPE_CHECKING
from PyQt5.QtCore import pyqtProperty from PyQt5.QtCore import pyqtProperty, pyqtSlot
from UM.Decorators import override from UM.Decorators import override
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
@ -13,6 +13,8 @@ from UM.Settings.SettingInstance import InstanceState
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.Interfaces import PropertyEvaluationContext from UM.Settings.Interfaces import PropertyEvaluationContext
from UM.Logger import Logger from UM.Logger import Logger
from UM.Resources import Resources
from UM.Platform import Platform
from UM.Util import parseBool from UM.Util import parseBool
import cura.CuraApplication import cura.CuraApplication
@ -200,6 +202,31 @@ class GlobalStack(CuraContainerStack):
def getHasMachineQuality(self) -> bool: def getHasMachineQuality(self) -> bool:
return parseBool(self.getMetaDataEntry("has_machine_quality", False)) return parseBool(self.getMetaDataEntry("has_machine_quality", False))
## Get default firmware file name if one is specified in the firmware
@pyqtSlot(result = str)
def getDefaultFirmwareName(self) -> str:
machine_has_heated_bed = self.getProperty("machine_heated_bed", "value")
baudrate = 250000
if Platform.isLinux():
# Linux prefers a baudrate of 115200 here because older versions of
# pySerial did not support a baudrate of 250000
baudrate = 115200
# If a firmware file is available, it should be specified in the definition for the printer
hex_file = self.getMetaDataEntry("firmware_file", None)
if machine_has_heated_bed:
hex_file = self.getMetaDataEntry("firmware_hbk_file", hex_file)
if not hex_file:
Logger.log("w", "There is no firmware for machine %s.", self.getBottom().id)
return ""
try:
return Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
except FileNotFoundError:
Logger.log("w", "Firmware file %s not found.", hex_file)
return ""
## private: ## private:
global_stack_mime = MimeType( global_stack_mime = MimeType(

View File

@ -1148,7 +1148,7 @@ class MachineManager(QObject):
self._fixQualityChangesGroupToNotSupported(quality_changes_group) self._fixQualityChangesGroupToNotSupported(quality_changes_group)
quality_changes_container = self._empty_quality_changes_container quality_changes_container = self._empty_quality_changes_container
quality_container = self._empty_quality_container quality_container = self._empty_quality_container # type: Optional[InstanceContainer]
if quality_changes_group.node_for_global and quality_changes_group.node_for_global.getContainer(): if quality_changes_group.node_for_global and quality_changes_group.node_for_global.getContainer():
quality_changes_container = cast(InstanceContainer, quality_changes_group.node_for_global.getContainer()) quality_changes_container = cast(InstanceContainer, quality_changes_group.node_for_global.getContainer())
if quality_group is not None and quality_group.node_for_global and quality_group.node_for_global.getContainer(): if quality_group is not None and quality_group.node_for_global and quality_group.node_for_global.getContainer():

View File

@ -1,6 +1,6 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 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 List from typing import List, Optional, TYPE_CHECKING
from PyQt5.QtCore import QObject, QTimer, pyqtProperty, pyqtSignal from PyQt5.QtCore import QObject, QTimer, pyqtProperty, pyqtSignal
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
@ -20,13 +20,18 @@ from UM.Settings.SettingInstance import InstanceState
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
if TYPE_CHECKING:
from cura.Settings.ExtruderStack import ExtruderStack
from UM.Settings.SettingDefinition import SettingDefinition
class SettingInheritanceManager(QObject): class SettingInheritanceManager(QObject):
def __init__(self, parent = None): def __init__(self, parent = None) -> None:
super().__init__(parent) super().__init__(parent)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged) Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._global_container_stack = None self._global_container_stack = None # type: Optional[ContainerStack]
self._settings_with_inheritance_warning = [] self._settings_with_inheritance_warning = [] # type: List[str]
self._active_container_stack = None self._active_container_stack = None # type: Optional[ExtruderStack]
self._onGlobalContainerChanged() self._onGlobalContainerChanged()
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged) ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
@ -41,7 +46,9 @@ class SettingInheritanceManager(QObject):
## Get the keys of all children settings with an override. ## Get the keys of all children settings with an override.
@pyqtSlot(str, result = "QStringList") @pyqtSlot(str, result = "QStringList")
def getChildrenKeysWithOverride(self, key): def getChildrenKeysWithOverride(self, key: str) -> List[str]:
if self._global_container_stack is None:
return []
definitions = self._global_container_stack.definition.findDefinitions(key=key) definitions = self._global_container_stack.definition.findDefinitions(key=key)
if not definitions: if not definitions:
Logger.log("w", "Could not find definition for key [%s]", key) Logger.log("w", "Could not find definition for key [%s]", key)
@ -53,9 +60,11 @@ class SettingInheritanceManager(QObject):
return result return result
@pyqtSlot(str, str, result = "QStringList") @pyqtSlot(str, str, result = "QStringList")
def getOverridesForExtruder(self, key, extruder_index): def getOverridesForExtruder(self, key: str, extruder_index: str) -> List[str]:
result = [] if self._global_container_stack is None:
return []
result = [] # type: List[str]
extruder_stack = ExtruderManager.getInstance().getExtruderStack(extruder_index) extruder_stack = ExtruderManager.getInstance().getExtruderStack(extruder_index)
if not extruder_stack: if not extruder_stack:
Logger.log("w", "Unable to find extruder for current machine with index %s", extruder_index) Logger.log("w", "Unable to find extruder for current machine with index %s", extruder_index)
@ -73,16 +82,16 @@ class SettingInheritanceManager(QObject):
return result return result
@pyqtSlot(str) @pyqtSlot(str)
def manualRemoveOverride(self, key): def manualRemoveOverride(self, key: str) -> None:
if key in self._settings_with_inheritance_warning: if key in self._settings_with_inheritance_warning:
self._settings_with_inheritance_warning.remove(key) self._settings_with_inheritance_warning.remove(key)
self.settingsWithIntheritanceChanged.emit() self.settingsWithIntheritanceChanged.emit()
@pyqtSlot() @pyqtSlot()
def forceUpdate(self): def forceUpdate(self) -> None:
self._update() self._update()
def _onActiveExtruderChanged(self): def _onActiveExtruderChanged(self) -> None:
new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack() new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack()
if not new_active_stack: if not new_active_stack:
self._active_container_stack = None self._active_container_stack = None
@ -94,13 +103,14 @@ class SettingInheritanceManager(QObject):
self._active_container_stack.containersChanged.disconnect(self._onContainersChanged) self._active_container_stack.containersChanged.disconnect(self._onContainersChanged)
self._active_container_stack = new_active_stack self._active_container_stack = new_active_stack
self._active_container_stack.propertyChanged.connect(self._onPropertyChanged) if self._active_container_stack is not None:
self._active_container_stack.containersChanged.connect(self._onContainersChanged) self._active_container_stack.propertyChanged.connect(self._onPropertyChanged)
self._active_container_stack.containersChanged.connect(self._onContainersChanged)
self._update() # Ensure that the settings_with_inheritance_warning list is populated. self._update() # Ensure that the settings_with_inheritance_warning list is populated.
def _onPropertyChanged(self, key, property_name): def _onPropertyChanged(self, key: str, property_name: str) -> None:
if (property_name == "value" or property_name == "enabled") and self._global_container_stack: if (property_name == "value" or property_name == "enabled") and self._global_container_stack:
definitions = self._global_container_stack.definition.findDefinitions(key = key) definitions = self._global_container_stack.definition.findDefinitions(key = key) # type: List["SettingDefinition"]
if not definitions: if not definitions:
return return
@ -139,7 +149,7 @@ class SettingInheritanceManager(QObject):
if settings_with_inheritance_warning_changed: if settings_with_inheritance_warning_changed:
self.settingsWithIntheritanceChanged.emit() self.settingsWithIntheritanceChanged.emit()
def _recursiveCheck(self, definition): def _recursiveCheck(self, definition: "SettingDefinition") -> bool:
for child in definition.children: for child in definition.children:
if child.key in self._settings_with_inheritance_warning: if child.key in self._settings_with_inheritance_warning:
return True return True
@ -149,7 +159,7 @@ class SettingInheritanceManager(QObject):
return False return False
@pyqtProperty("QVariantList", notify = settingsWithIntheritanceChanged) @pyqtProperty("QVariantList", notify = settingsWithIntheritanceChanged)
def settingsWithInheritanceWarning(self): def settingsWithInheritanceWarning(self) -> List[str]:
return self._settings_with_inheritance_warning return self._settings_with_inheritance_warning
## Check if a setting has an inheritance function that is overwritten ## Check if a setting has an inheritance function that is overwritten
@ -157,9 +167,14 @@ class SettingInheritanceManager(QObject):
has_setting_function = False has_setting_function = False
if not stack: if not stack:
stack = self._active_container_stack stack = self._active_container_stack
if not stack: #No active container stack yet! if not stack: # No active container stack yet!
return False return False
containers = [] # type: List[ContainerInterface]
if self._active_container_stack is None:
return False
all_keys = self._active_container_stack.getAllKeys()
containers = [] # type: List[ContainerInterface]
## Check if the setting has a user state. If not, it is never overwritten. ## Check if the setting has a user state. If not, it is never overwritten.
has_user_state = stack.getProperty(key, "state") == InstanceState.User has_user_state = stack.getProperty(key, "state") == InstanceState.User
@ -190,8 +205,8 @@ class SettingInheritanceManager(QObject):
has_setting_function = isinstance(value, SettingFunction) has_setting_function = isinstance(value, SettingFunction)
if has_setting_function: if has_setting_function:
for setting_key in value.getUsedSettingKeys(): for setting_key in value.getUsedSettingKeys():
if setting_key in self._active_container_stack.getAllKeys(): if setting_key in all_keys:
break # We found an actual setting. So has_setting_function can remain true break # We found an actual setting. So has_setting_function can remain true
else: else:
# All of the setting_keys turned out to not be setting keys at all! # All of the setting_keys turned out to not be setting keys at all!
# This can happen due enum keys also being marked as settings. # This can happen due enum keys also being marked as settings.
@ -205,7 +220,7 @@ class SettingInheritanceManager(QObject):
break # There is a setting function somewhere, stop looking deeper. break # There is a setting function somewhere, stop looking deeper.
return has_setting_function and has_non_function_value return has_setting_function and has_non_function_value
def _update(self): def _update(self) -> None:
self._settings_with_inheritance_warning = [] # Reset previous data. self._settings_with_inheritance_warning = [] # Reset previous data.
# Make sure that the GlobalStack is not None. sometimes the globalContainerChanged signal gets here late. # Make sure that the GlobalStack is not None. sometimes the globalContainerChanged signal gets here late.
@ -226,7 +241,7 @@ class SettingInheritanceManager(QObject):
# Notify others that things have changed. # Notify others that things have changed.
self.settingsWithIntheritanceChanged.emit() self.settingsWithIntheritanceChanged.emit()
def _onGlobalContainerChanged(self): def _onGlobalContainerChanged(self) -> None:
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged) self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
self._global_container_stack.containersChanged.disconnect(self._onContainersChanged) self._global_container_stack.containersChanged.disconnect(self._onContainersChanged)

View File

@ -1,15 +1,17 @@
from UM.Qt.ListModel import ListModel # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from collections import OrderedDict
from PyQt5.QtCore import pyqtSlot, Qt from PyQt5.QtCore import pyqtSlot, Qt
from UM.Application import Application from UM.Application import Application
from cura.Settings.ExtruderManager import ExtruderManager
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Settings.SettingFunction import SettingFunction from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from collections import OrderedDict from UM.Qt.ListModel import ListModel
import os
class UserChangesModel(ListModel): class UserChangesModel(ListModel):
@ -38,9 +40,13 @@ class UserChangesModel(ListModel):
self._update() self._update()
def _update(self): def _update(self):
application = Application.getInstance()
machine_manager = application.getMachineManager()
cura_formula_functions = application.getCuraFormulaFunctions()
item_dict = OrderedDict() item_dict = OrderedDict()
item_list = [] item_list = []
global_stack = Application.getInstance().getGlobalContainerStack() global_stack = machine_manager.activeMachine
if not global_stack: if not global_stack:
return return
@ -71,13 +77,7 @@ class UserChangesModel(ListModel):
# Override "getExtruderValue" with "getDefaultExtruderValue" so we can get the default values # Override "getExtruderValue" with "getDefaultExtruderValue" so we can get the default values
user_changes = containers.pop(0) user_changes = containers.pop(0)
default_value_resolve_context = PropertyEvaluationContext(stack) default_value_resolve_context = cura_formula_functions.createContextForDefaultValueEvaluation(stack)
default_value_resolve_context.context["evaluate_from_container_index"] = 1 # skip the user settings container
default_value_resolve_context.context["override_operators"] = {
"extruderValue": ExtruderManager.getDefaultExtruderValue,
"extruderValues": ExtruderManager.getDefaultExtruderValues,
"resolveOrValue": ExtruderManager.getDefaultResolveOrValue
}
for setting_key in user_changes.getAllKeys(): for setting_key in user_changes.getAllKeys():
original_value = None original_value = None

View File

@ -1012,7 +1012,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data. ## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data.
def _getContainerIdListFromSerialized(self, serialized): def _getContainerIdListFromSerialized(self, serialized):
parser = ConfigParser(interpolation=None, empty_lines_in_values=False) parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
parser.read_string(serialized) parser.read_string(serialized)
container_ids = [] container_ids = []
@ -1033,7 +1033,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
return container_ids return container_ids
def _getMachineNameFromSerializedStack(self, serialized): def _getMachineNameFromSerializedStack(self, serialized):
parser = ConfigParser(interpolation=None, empty_lines_in_values=False) parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
parser.read_string(serialized) parser.read_string(serialized)
return parser["general"].get("name", "") return parser["general"].get("name", "")

View File

@ -12,7 +12,7 @@ from . import ThreeMFWorkspaceWriter
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Platform import Platform from UM.Platform import Platform
i18n_catalog = i18nCatalog("uranium") i18n_catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
workspace_extension = "3mf" workspace_extension = "3mf"

View File

@ -3,8 +3,6 @@
from . import ChangeLog from . import ChangeLog
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return {} return {}

View File

@ -2,6 +2,7 @@
#Cura is released under the terms of the LGPLv3 or higher. #Cura is released under the terms of the LGPLv3 or higher.
import gc import gc
import sys
from UM.Job import Job from UM.Job import Job
from UM.Application import Application from UM.Application import Application
@ -95,23 +96,35 @@ class ProcessSlicedLayersJob(Job):
layer_count = len(self._layers) layer_count = len(self._layers)
# Find the minimum layer number # Find the minimum layer number
# When disabling the remove empty first layers setting, the minimum layer number will be a positive
# value. In that case the first empty layers will be discarded and start processing layers from the
# first layer with data.
# When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we # When using a raft, the raft layers are sent as layers < 0. Instead of allowing layers < 0, we
# instead simply offset all other layers so the lowest layer is always 0. It could happens that # simply offset all other layers so the lowest layer is always 0. It could happens that the first
# the first raft layer has value -8 but there are just 4 raft (negative) layers. # raft layer has value -8 but there are just 4 raft (negative) layers.
min_layer_number = 0 min_layer_number = sys.maxsize
negative_layers = 0 negative_layers = 0
for layer in self._layers: for layer in self._layers:
if layer.id < min_layer_number: if layer.repeatedMessageCount("path_segment") > 0:
min_layer_number = layer.id if layer.id < min_layer_number:
if layer.id < 0: min_layer_number = layer.id
negative_layers += 1 if layer.id < 0:
negative_layers += 1
current_layer = 0 current_layer = 0
for layer in self._layers: for layer in self._layers:
# Negative layers are offset by the minimum layer number, but the positive layers are just # If the layer is below the minimum, it means that there is no data, so that we don't create a layer
# offset by the number of negative layers so there is no layer gap between raft and model # data. However, if there are empty layers in between, we compute them.
abs_layer_number = layer.id + abs(min_layer_number) if layer.id < 0 else layer.id + negative_layers if layer.id < min_layer_number:
continue
# Layers are offset by the minimum layer number. In case the raft (negative layers) is being used,
# then the absolute layer number is adjusted by removing the empty layers that can be in between raft
# and the model
abs_layer_number = layer.id - min_layer_number
if layer.id >= 0 and negative_layers != 0:
abs_layer_number += (min_layer_number + negative_layers)
layer_data.addLayer(abs_layer_number) layer_data.addLayer(abs_layer_number)
this_layer = layer_data.getLayer(abs_layer_number) this_layer = layer_data.getLayer(abs_layer_number)

View File

@ -41,11 +41,15 @@ class StartJobResult(IntEnum):
## Formatter class that handles token expansion in start/end gcode ## Formatter class that handles token expansion in start/end gcode
class GcodeStartEndFormatter(Formatter): class GcodeStartEndFormatter(Formatter):
def get_value(self, key: str, args: str, kwargs: dict, default_extruder_nr: str = "-1") -> str: #type: ignore # [CodeStyle: get_value is an overridden function from the Formatter class] def __init__(self, default_extruder_nr: int = -1) -> None:
super().__init__()
self._default_extruder_nr = default_extruder_nr
def get_value(self, key: str, args: str, kwargs: dict) -> str: #type: ignore # [CodeStyle: get_value is an overridden function from the Formatter class]
# The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key), # The kwargs dictionary contains a dictionary for each stack (with a string of the extruder_nr as their key),
# and a default_extruder_nr to use when no extruder_nr is specified # and a default_extruder_nr to use when no extruder_nr is specified
extruder_nr = int(default_extruder_nr) extruder_nr = self._default_extruder_nr
key_fragments = [fragment.strip() for fragment in key.split(",")] key_fragments = [fragment.strip() for fragment in key.split(",")]
if len(key_fragments) == 2: if len(key_fragments) == 2:
@ -247,7 +251,10 @@ class StartSliceJob(Job):
self._buildGlobalInheritsStackMessage(stack) self._buildGlobalInheritsStackMessage(stack)
# Build messages for extruder stacks # Build messages for extruder stacks
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(stack.getId()): # Send the extruder settings in the order of extruder positions. Somehow, if you send e.g. extruder 3 first,
# then CuraEngine can slice with the wrong settings. This I think should be fixed in CuraEngine as well.
extruder_stack_list = sorted(list(global_stack.extruders.items()), key = lambda item: int(item[0]))
for _, extruder_stack in extruder_stack_list:
self._buildExtruderMessage(extruder_stack) self._buildExtruderMessage(extruder_stack)
for group in filtered_object_groups: for group in filtered_object_groups:
@ -339,7 +346,7 @@ class StartSliceJob(Job):
try: try:
# any setting can be used as a token # any setting can be used as a token
fmt = GcodeStartEndFormatter() fmt = GcodeStartEndFormatter(default_extruder_nr = default_extruder_nr)
settings = self._all_extruders_settings.copy() settings = self._all_extruders_settings.copy()
settings["default_extruder_nr"] = default_extruder_nr settings["default_extruder_nr"] = default_extruder_nr
return str(fmt.format(value, **settings)) return str(fmt.format(value, **settings))

View File

@ -50,7 +50,7 @@ class CuraProfileReader(ProfileReader):
# \param profile_id \type{str} The name of the profile. # \param profile_id \type{str} The name of the profile.
# \return \type{List[Tuple[str,str]]} List of serialized profile strings and matching profile names. # \return \type{List[Tuple[str,str]]} List of serialized profile strings and matching profile names.
def _upgradeProfile(self, serialized, profile_id): def _upgradeProfile(self, serialized, profile_id):
parser = configparser.ConfigParser(interpolation=None) parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized) parser.read_string(serialized)
if "general" not in parser: if "general" not in parser:

View File

@ -1,9 +1,12 @@
# Copyright (c) 2017 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 os
from PyQt5.QtCore import QUrl from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from typing import Set
from UM.Extension import Extension from UM.Extension import Extension
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
@ -13,6 +16,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -21,32 +25,31 @@ i18n_catalog = i18nCatalog("cura")
# The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy # The plugin is currently only usable for applications maintained by Ultimaker. But it should be relatively easy
# to change it to work for other applications. # to change it to work for other applications.
class FirmwareUpdateChecker(Extension): class FirmwareUpdateChecker(Extension):
JEDI_VERSION_URL = "http://software.ultimaker.com/jedi/releases/latest.version?utm_source=cura&utm_medium=software&utm_campaign=resources"
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
# Initialize the Preference called `latest_checked_firmware` that stores the last version
# checked for the UM3. In the future if we need to check other printers' firmware
Application.getInstance().getPreferences().addPreference("info/latest_checked_firmware", "")
# Listen to a Signal that indicates a change in the list of printers, just if the user has enabled the # Listen to a Signal that indicates a change in the list of printers, just if the user has enabled the
# 'check for updates' option # "check for updates" option
Application.getInstance().getPreferences().addPreference("info/automatic_update_check", True) Application.getInstance().getPreferences().addPreference("info/automatic_update_check", True)
if Application.getInstance().getPreferences().getValue("info/automatic_update_check"): if Application.getInstance().getPreferences().getValue("info/automatic_update_check"):
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded) ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
self._download_url = None
self._check_job = None self._check_job = None
self._checked_printer_names = set() # type: Set[str]
## Callback for the message that is spawned when there is a new version. ## Callback for the message that is spawned when there is a new version.
def _onActionTriggered(self, message, action): def _onActionTriggered(self, message, action):
if action == "download": if action == FirmwareUpdateCheckerMessage.STR_ACTION_DOWNLOAD:
if self._download_url is not None: machine_id = message.getMachineId()
QDesktopServices.openUrl(QUrl(self._download_url)) download_url = message.getDownloadUrl()
if download_url is not None:
def _onSetDownloadUrl(self, download_url): if QDesktopServices.openUrl(QUrl(download_url)):
self._download_url = download_url Logger.log("i", "Redirected browser to {0} to show newly available firmware.".format(download_url))
else:
Logger.log("e", "Can't reach URL: {0}".format(download_url))
else:
Logger.log("e", "Can't find URL for {0}".format(machine_id))
def _onContainerAdded(self, container): def _onContainerAdded(self, container):
# Only take care when a new GlobalStack was added # Only take care when a new GlobalStack was added
@ -63,13 +66,18 @@ class FirmwareUpdateChecker(Extension):
# \param silent type(boolean) Suppresses messages other than "new version found" messages. # \param silent type(boolean) Suppresses messages other than "new version found" messages.
# This is used when checking for a new firmware version at startup. # This is used when checking for a new firmware version at startup.
def checkFirmwareVersion(self, container = None, silent = False): def checkFirmwareVersion(self, container = None, silent = False):
# Do not run multiple check jobs in parallel container_name = container.definition.getName()
if self._check_job is not None: if container_name in self._checked_printer_names:
Logger.log("i", "A firmware update check is already running, do nothing.") return
self._checked_printer_names.add(container_name)
metadata = container.definition.getMetaData().get("firmware_update_info")
if metadata is None:
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(container_name))
return return
self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent, url = self.JEDI_VERSION_URL, self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent,
callback = self._onActionTriggered, machine_name = container_name, metadata = metadata,
set_download_url_callback = self._onSetDownloadUrl) callback = self._onActionTriggered)
self._check_job.start() self._check_job.start()
self._check_job.finished.connect(self._onJobFinished) self._check_job.finished.connect(self._onJobFinished)

View File

@ -1,13 +1,18 @@
# Copyright (c) 2017 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 UM.Application import Application from UM.Application import Application
from UM.Message import Message from UM.Message import Message
from UM.Logger import Logger from UM.Logger import Logger
from UM.Job import Job from UM.Job import Job
from UM.Version import Version
import urllib.request import urllib.request
import codecs from urllib.error import URLError
from typing import Dict, Optional
from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
@ -15,46 +20,86 @@ i18n_catalog = i18nCatalog("cura")
## This job checks if there is an update available on the provided URL. ## This job checks if there is an update available on the provided URL.
class FirmwareUpdateCheckerJob(Job): class FirmwareUpdateCheckerJob(Job):
def __init__(self, container = None, silent = False, url = None, callback = None, set_download_url_callback = None): STRING_ZERO_VERSION = "0.0.0"
STRING_EPSILON_VERSION = "0.0.1"
ZERO_VERSION = Version(STRING_ZERO_VERSION)
EPSILON_VERSION = Version(STRING_EPSILON_VERSION)
def __init__(self, container, silent, machine_name, metadata, callback) -> None:
super().__init__() super().__init__()
self._container = container self._container = container
self.silent = silent self.silent = silent
self._url = url
self._callback = callback self._callback = callback
self._set_download_url_callback = set_download_url_callback
def run(self): self._machine_name = machine_name
if not self._url: self._metadata = metadata
Logger.log("e", "Can not check for a new release. URL not set!") self._lookups = None # type:Optional[FirmwareUpdateCheckerLookup]
return self._headers = {} # type:Dict[str, str] # Don't set headers yet.
def getUrlResponse(self, url: str) -> str:
result = self.STRING_ZERO_VERSION
try: try:
request = urllib.request.Request(url, headers = self._headers)
response = urllib.request.urlopen(request)
result = response.read().decode("utf-8")
except URLError:
Logger.log("w", "Could not reach '{0}', if this URL is old, consider removal.".format(url))
return result
def parseVersionResponse(self, response: str) -> Version:
raw_str = response.split("\n", 1)[0].rstrip()
return Version(raw_str)
def getCurrentVersion(self) -> Version:
max_version = self.ZERO_VERSION
if self._lookups is None:
return max_version
machine_urls = self._lookups.getCheckUrls()
if machine_urls is not None:
for url in machine_urls:
version = self.parseVersionResponse(self.getUrlResponse(url))
if version > max_version:
max_version = version
if max_version < self.EPSILON_VERSION:
Logger.log("w", "MachineID {0} not handled!".format(self._lookups.getMachineName()))
return max_version
def run(self):
if self._lookups is None:
self._lookups = FirmwareUpdateCheckerLookup(self._machine_name, self._metadata)
try:
# Initialize a Preference that stores the last version checked for this printer.
Application.getInstance().getPreferences().addPreference(
getSettingsKeyForMachine(self._lookups.getMachineId()), "")
# Get headers
application_name = Application.getInstance().getApplicationName() application_name = Application.getInstance().getApplicationName()
headers = {"User-Agent": "%s - %s" % (application_name, Application.getInstance().getVersion())} application_version = Application.getInstance().getVersion()
request = urllib.request.Request(self._url, headers = headers) self._headers = {"User-Agent": "%s - %s" % (application_name, application_version)}
current_version_file = urllib.request.urlopen(request)
reader = codecs.getreader("utf-8")
# get machine name from the definition container # get machine name from the definition container
machine_name = self._container.definition.getName() machine_name = self._container.definition.getName()
machine_name_parts = machine_name.lower().split(" ")
# If it is not None, then we compare between the checked_version and the current_version # If it is not None, then we compare between the checked_version and the current_version
# Now we just do that if the active printer is Ultimaker 3 or Ultimaker 3 Extended or any machine_id = self._lookups.getMachineId()
# other Ultimaker 3 that will come in the future if machine_id is not None:
if len(machine_name_parts) >= 2 and machine_name_parts[:2] == ["ultimaker", "3"]: Logger.log("i", "You have a(n) {0} in the printer list. Let's check the firmware!".format(machine_name))
Logger.log("i", "You have a UM3 in printer list. Let's check the firmware!")
# Nothing to parse, just get the string current_version = self.getCurrentVersion()
# TODO: In the future may be done by parsing a JSON file with diferent version for each printer model
current_version = reader(current_version_file).readline().rstrip()
# If it is the first time the version is checked, the checked_version is '' # If it is the first time the version is checked, the checked_version is ""
checked_version = Application.getInstance().getPreferences().getValue("info/latest_checked_firmware") setting_key_str = getSettingsKeyForMachine(machine_id)
checked_version = Version(Application.getInstance().getPreferences().getValue(setting_key_str))
# If the checked_version is '', it's because is the first time we check firmware and in this case # If the checked_version is "", it's because is the first time we check firmware and in this case
# we will not show the notification, but we will store it for the next time # we will not show the notification, but we will store it for the next time
Application.getInstance().getPreferences().setValue("info/latest_checked_firmware", current_version) Application.getInstance().getPreferences().setValue(setting_key_str, current_version)
Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s", machine_name, checked_version, current_version) Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s", machine_name, checked_version, current_version)
# The first time we want to store the current version, the notification will not be shown, # The first time we want to store the current version, the notification will not be shown,
@ -62,28 +107,11 @@ class FirmwareUpdateCheckerJob(Job):
# notify the user when no new firmware version is available. # notify the user when no new firmware version is available.
if (checked_version != "") and (checked_version != current_version): if (checked_version != "") and (checked_version != current_version):
Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE") Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE")
message = FirmwareUpdateCheckerMessage(machine_id, machine_name, self._lookups.getRedirectUserUrl())
message = Message(i18n_catalog.i18nc(
"@info Don't translate {machine_name}, since it gets replaced by a printer name!",
"New features are available for your {machine_name}! It is recommended to update the firmware on your printer.").format(
machine_name=machine_name),
title=i18n_catalog.i18nc(
"@info:title The %s gets replaced with the printer name.",
"New %s firmware available") % machine_name)
message.addAction("download",
i18n_catalog.i18nc("@action:button", "How to update"),
"[no_icon]",
"[no_description]",
button_style=Message.ActionButtonStyle.LINK,
button_align=Message.ActionButtonStyle.BUTTON_ALIGN_LEFT)
# If we do this in a cool way, the download url should be available in the JSON file
if self._set_download_url_callback:
self._set_download_url_callback("https://ultimaker.com/en/resources/20500-upgrade-firmware")
message.actionTriggered.connect(self._callback) message.actionTriggered.connect(self._callback)
message.show() message.show()
else:
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(machine_name))
except Exception as e: except Exception as e:
Logger.log("w", "Failed to check for new version: %s", e) Logger.log("w", "Failed to check for new version: %s", e)

View File

@ -0,0 +1,35 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import List, Optional
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getSettingsKeyForMachine(machine_id: int) -> str:
return "info/latest_checked_firmware_for_{0}".format(machine_id)
class FirmwareUpdateCheckerLookup:
def __init__(self, machine_name, machine_json) -> None:
# Parse all the needed lookup-tables from the ".json" file(s) in the resources folder.
self._machine_id = machine_json.get("id")
self._machine_name = machine_name.lower() # Lower in-case upper-case chars are added to the original json.
self._check_urls = [] # type:List[str]
for check_url in machine_json.get("check_urls"):
self._check_urls.append(check_url)
self._redirect_user = machine_json.get("update_url")
def getMachineId(self) -> Optional[int]:
return self._machine_id
def getMachineName(self) -> Optional[int]:
return self._machine_name
def getCheckUrls(self) -> Optional[List[str]]:
return self._check_urls
def getRedirectUserUrl(self) -> Optional[str]:
return self._redirect_user

View File

@ -0,0 +1,37 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.i18n import i18nCatalog
from UM.Message import Message
i18n_catalog = i18nCatalog("cura")
# Make a separate class, since we need an extra field: The machine-id that this message is about.
class FirmwareUpdateCheckerMessage(Message):
STR_ACTION_DOWNLOAD = "download"
def __init__(self, machine_id: int, machine_name: str, download_url: str) -> None:
super().__init__(i18n_catalog.i18nc(
"@info Don't translate {machine_name}, since it gets replaced by a printer name!",
"New features are available for your {machine_name}! It is recommended to update the firmware on your printer.").format(
machine_name = machine_name),
title = i18n_catalog.i18nc(
"@info:title The %s gets replaced with the printer name.",
"New %s firmware available") % machine_name)
self._machine_id = machine_id
self._download_url = download_url
self.addAction(self.STR_ACTION_DOWNLOAD,
i18n_catalog.i18nc("@action:button", "How to update"),
"[no_icon]",
"[no_description]",
button_style = Message.ActionButtonStyle.LINK,
button_align = Message.ActionButtonStyle.BUTTON_ALIGN_LEFT)
def getMachineId(self) -> int:
return self._machine_id
def getDownloadUrl(self) -> str:
return self._download_url

View File

@ -1,12 +1,8 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 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 UM.i18n import i18nCatalog
from . import FirmwareUpdateChecker from . import FirmwareUpdateChecker
i18n_catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return {} return {}

View File

@ -0,0 +1,69 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura.CuraApplication import CuraApplication
from UM.Settings.DefinitionContainer import DefinitionContainer
from cura.MachineAction import MachineAction
from UM.i18n import i18nCatalog
from UM.Settings.ContainerRegistry import ContainerRegistry
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdateState
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject
from typing import Optional
MYPY = False
if MYPY:
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdater
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from UM.Settings.ContainerInterface import ContainerInterface
catalog = i18nCatalog("cura")
## Upgrade the firmware of a machine by USB with this action.
class FirmwareUpdaterMachineAction(MachineAction):
def __init__(self) -> None:
super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Update Firmware"))
self._qml_url = "FirmwareUpdaterMachineAction.qml"
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
self._active_output_device = None # type: Optional[PrinterOutputDevice]
self._active_firmware_updater = None # type: Optional[FirmwareUpdater]
CuraApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
def _onEngineCreated(self) -> None:
CuraApplication.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
def _onContainerAdded(self, container: "ContainerInterface") -> None:
# Add this action as a supported action to all machine definitions if they support USB connection
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine" and container.getMetaDataEntry("supports_usb_connection"):
CuraApplication.getInstance().getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
def _onOutputDevicesChanged(self) -> None:
if self._active_output_device and self._active_output_device.activePrinter:
self._active_output_device.activePrinter.getController().canUpdateFirmwareChanged.disconnect(self._onControllerCanUpdateFirmwareChanged)
output_devices = CuraApplication.getInstance().getMachineManager().printerOutputDevices
self._active_output_device = output_devices[0] if output_devices else None
if self._active_output_device and self._active_output_device.activePrinter:
self._active_output_device.activePrinter.getController().canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
self.outputDeviceCanUpdateFirmwareChanged.emit()
def _onControllerCanUpdateFirmwareChanged(self) -> None:
self.outputDeviceCanUpdateFirmwareChanged.emit()
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:
self._active_firmware_updater = self._active_output_device.getFirmwareUpdater()
return self._active_firmware_updater
elif self._active_firmware_updater and self._active_firmware_updater.firmwareUpdateState not in [FirmwareUpdateState.idle, FirmwareUpdateState.completed]:
# During a firmware update, the PrinterOutputDevice is disconnected but the FirmwareUpdater is still there
return self._active_firmware_updater
self._active_firmware_updater = None
return None

View File

@ -0,0 +1,191 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import QtQuick.Dialogs 1.2 // For filedialog
import UM 1.2 as UM
import Cura 1.0 as Cura
Cura.MachineAction
{
anchors.fill: parent;
property bool printerConnected: Cura.MachineManager.printerConnected
property var activeOutputDevice: printerConnected ? Cura.MachineManager.printerOutputDevices[0] : null
property bool canUpdateFirmware: activeOutputDevice ? activeOutputDevice.activePrinter.canUpdateFirmware : false
Column
{
id: firmwareUpdaterMachineAction
anchors.fill: parent;
UM.I18nCatalog { id: catalog; name:"cura"}
spacing: UM.Theme.getSize("default_margin").height
Label
{
width: parent.width
text: catalog.i18nc("@title", "Update Firmware")
wrapMode: Text.WordWrap
font.pointSize: 18
}
Label
{
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Firmware is the piece of software running directly on your 3D printer. This firmware controls the step motors, regulates the temperature and ultimately makes your printer work.")
}
Label
{
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "The firmware shipping with new printers works, but new versions tend to have more features and improvements.");
}
Row
{
anchors.horizontalCenter: parent.horizontalCenter
width: childrenRect.width
spacing: UM.Theme.getSize("default_margin").width
property string firmwareName: Cura.MachineManager.activeMachine.getDefaultFirmwareName()
Button
{
id: autoUpgradeButton
text: catalog.i18nc("@action:button", "Automatically upgrade Firmware");
enabled: parent.firmwareName != "" && canUpdateFirmware
onClicked:
{
updateProgressDialog.visible = true;
activeOutputDevice.updateFirmware(parent.firmwareName);
}
}
Button
{
id: manualUpgradeButton
text: catalog.i18nc("@action:button", "Upload custom Firmware");
enabled: canUpdateFirmware
onClicked:
{
customFirmwareDialog.open()
}
}
}
Label
{
width: parent.width
wrapMode: Text.WordWrap
visible: !printerConnected && !updateProgressDialog.visible
text: catalog.i18nc("@label", "Firmware can not be updated because there is no connection with the printer.");
}
Label
{
width: parent.width
wrapMode: Text.WordWrap
visible: printerConnected && !canUpdateFirmware
text: catalog.i18nc("@label", "Firmware can not be updated because the connection with the printer does not support upgrading firmware.");
}
}
FileDialog
{
id: customFirmwareDialog
title: catalog.i18nc("@title:window", "Select custom firmware")
nameFilters: "Firmware image files (*.hex)"
selectExisting: true
onAccepted:
{
updateProgressDialog.visible = true;
activeOutputDevice.updateFirmware(fileUrl);
}
}
UM.Dialog
{
id: updateProgressDialog
width: minimumWidth
minimumWidth: 500 * screenScaleFactor
height: minimumHeight
minimumHeight: 100 * screenScaleFactor
modality: Qt.ApplicationModal
title: catalog.i18nc("@title:window","Firmware Update")
Column
{
anchors.fill: parent
Label
{
anchors
{
left: parent.left
right: parent.right
}
text: {
if(manager.firmwareUpdater == null)
{
return "";
}
switch (manager.firmwareUpdater.firmwareUpdateState)
{
case 0:
return ""; //Not doing anything (eg; idling)
case 1:
return catalog.i18nc("@label","Updating firmware.");
case 2:
return catalog.i18nc("@label","Firmware update completed.");
case 3:
return catalog.i18nc("@label","Firmware update failed due to an unknown error.");
case 4:
return catalog.i18nc("@label","Firmware update failed due to an communication error.");
case 5:
return catalog.i18nc("@label","Firmware update failed due to an input/output error.");
case 6:
return catalog.i18nc("@label","Firmware update failed due to missing firmware.");
}
}
wrapMode: Text.Wrap
}
ProgressBar
{
id: prog
value: (manager.firmwareUpdater != null) ? manager.firmwareUpdater.firmwareProgress : 0
minimumValue: 0
maximumValue: 100
indeterminate:
{
if(manager.firmwareUpdater == null)
{
return false;
}
return manager.firmwareUpdater.firmwareProgress < 1 && manager.firmwareUpdater.firmwareProgress > 0;
}
anchors
{
left: parent.left;
right: parent.right;
}
}
}
rightButtons: [
Button
{
text: catalog.i18nc("@action:button","Close");
enabled: (manager.firmwareUpdater != null) ? manager.firmwareUpdater.firmwareUpdateState != 1 : true;
onClicked: updateProgressDialog.visible = false;
}
]
}
}

View File

@ -0,0 +1,12 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import FirmwareUpdaterMachineAction
def getMetaData():
return {}
def register(app):
return { "machine_action": [
FirmwareUpdaterMachineAction.FirmwareUpdaterMachineAction()
]}

View File

@ -0,0 +1,8 @@
{
"name": "Firmware Updater",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides a machine actions for updating firmware.",
"api": 5,
"i18n-catalog": "cura"
}

View File

@ -44,6 +44,7 @@ class FlavorParser:
self._extruder_offsets = {} # type: Dict[int, List[float]] # Offsets for multi extruders. key is index, value is [x-offset, y-offset] self._extruder_offsets = {} # type: Dict[int, List[float]] # Offsets for multi extruders. key is index, value is [x-offset, y-offset]
self._current_layer_thickness = 0.2 # default self._current_layer_thickness = 0.2 # default
self._filament_diameter = 2.85 # default self._filament_diameter = 2.85 # default
self._previous_extrusion_value = 0.0 # keep track of the filament retractions
CuraApplication.getInstance().getPreferences().addPreference("gcodereader/show_caution", True) CuraApplication.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)
@ -182,6 +183,7 @@ class FlavorParser:
new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e new_extrusion_value = params.e if self._is_absolute_extrusion else e[self._extruder_number] + params.e
if new_extrusion_value > e[self._extruder_number]: if new_extrusion_value > e[self._extruder_number]:
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], self._layer_type]) # extrusion
self._previous_extrusion_value = new_extrusion_value
else: else:
path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction path.append([x, y, z, f, new_extrusion_value + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType]) # retraction
e[self._extruder_number] = new_extrusion_value e[self._extruder_number] = new_extrusion_value
@ -191,6 +193,12 @@ class FlavorParser:
if z > self._previous_z and (z - self._previous_z < 1.5): if z > self._previous_z and (z - self._previous_z < 1.5):
self._current_layer_thickness = z - self._previous_z # allow a tiny overlap self._current_layer_thickness = z - self._previous_z # allow a tiny overlap
self._previous_z = z self._previous_z = z
elif self._previous_extrusion_value > e[self._extruder_number]:
path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType])
# This case only for initial start, for the first coordinate in GCode
elif e[self._extruder_number] == 0 and self._previous_extrusion_value == 0:
path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveRetractionType])
else: else:
path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType]) path.append([x, y, z, f, e[self._extruder_number] + self._extrusion_length_offset[self._extruder_number], LayerPolygon.MoveCombingType])
return self._position(x, y, z, f, e) return self._position(x, y, z, f, e)
@ -235,6 +243,7 @@ class FlavorParser:
position.e) position.e)
def processGCode(self, G: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position: def processGCode(self, G: int, line: str, position: Position, path: List[List[Union[float, int]]]) -> Position:
self._previous_extrusion_value = 0.0
func = getattr(self, "_gCode%s" % G, None) func = getattr(self, "_gCode%s" % G, None)
line = line.split(";", 1)[0] # Remove comments (if any) line = line.split(";", 1)[0] # Remove comments (if any)
if func is not None: if func is not None:

View File

@ -35,7 +35,7 @@ UM.Dialog
width: parent.width width: parent.width
Label { Label {
text: catalog.i18nc("@action:label","Height (mm)") text: catalog.i18nc("@action:label", "Height (mm)")
width: 150 * screenScaleFactor width: 150 * screenScaleFactor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@ -58,7 +58,7 @@ UM.Dialog
width: parent.width width: parent.width
Label { Label {
text: catalog.i18nc("@action:label","Base (mm)") text: catalog.i18nc("@action:label", "Base (mm)")
width: 150 * screenScaleFactor width: 150 * screenScaleFactor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@ -81,7 +81,7 @@ UM.Dialog
width: parent.width width: parent.width
Label { Label {
text: catalog.i18nc("@action:label","Width (mm)") text: catalog.i18nc("@action:label", "Width (mm)")
width: 150 * screenScaleFactor width: 150 * screenScaleFactor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@ -105,7 +105,7 @@ UM.Dialog
width: parent.width width: parent.width
Label { Label {
text: catalog.i18nc("@action:label","Depth (mm)") text: catalog.i18nc("@action:label", "Depth (mm)")
width: 150 * screenScaleFactor width: 150 * screenScaleFactor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@ -151,7 +151,7 @@ UM.Dialog
width: parent.width width: parent.width
Label { Label {
text: catalog.i18nc("@action:label","Smoothing") text: catalog.i18nc("@action:label", "Smoothing")
width: 150 * screenScaleFactor width: 150 * screenScaleFactor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }

View File

@ -152,7 +152,7 @@ class LegacyProfileReader(ProfileReader):
profile.setDirty(True) profile.setDirty(True)
#Serialise and deserialise in order to perform the version upgrade. #Serialise and deserialise in order to perform the version upgrade.
parser = configparser.ConfigParser(interpolation=None) parser = configparser.ConfigParser(interpolation = None)
data = profile.serialize() data = profile.serialize()
parser.read_string(data) parser.read_string(data)
parser["general"]["version"] = "1" parser["general"]["version"] = "1"

View File

@ -435,6 +435,18 @@ Cura.MachineAction
property bool allowNegative: true property bool allowNegative: true
} }
Loader
{
id: extruderCoolingFanNumberField
sourceComponent: numericTextFieldWithUnit
property string settingKey: "machine_extruder_cooling_fan_number"
property string label: catalog.i18nc("@label", "Cooling Fan Number")
property string unit: catalog.i18nc("@label", "")
property bool isExtruderSetting: true
property bool forceUpdateOnChange: true
property bool allowNegative: false
}
Item { width: UM.Theme.getSize("default_margin").width; height: UM.Theme.getSize("default_margin").height } Item { width: UM.Theme.getSize("default_margin").width; height: UM.Theme.getSize("default_margin").height }
Row Row

View File

@ -3,8 +3,6 @@
from . import MachineSettingsAction from . import MachineSettingsAction
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return {} return {}

View File

@ -1,11 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# This example is released under the terms of the AGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from . import ModelChecker from . import ModelChecker
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return {} return {}

View File

@ -3,6 +3,7 @@
from . import MonitorStage from . import MonitorStage
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")

View File

@ -185,6 +185,12 @@ Item {
{ {
selectedObjectId: UM.ActiveTool.properties.getValue("SelectedObjectId") selectedObjectId: UM.ActiveTool.properties.getValue("SelectedObjectId")
} }
// For some reason the model object is updated after removing him from the memory and
// it happens only on Windows. For this reason, set the destroyed value manually.
Component.onDestruction: {
setDestroyed(true);
}
} }
delegate: Row delegate: Row

View File

@ -2,6 +2,7 @@
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from typing import Dict, Type, TYPE_CHECKING, List, Optional, cast
from UM.PluginRegistry import PluginRegistry from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources from UM.Resources import Resources
@ -9,55 +10,62 @@ from UM.Application import Application
from UM.Extension import Extension from UM.Extension import Extension
from UM.Logger import Logger from UM.Logger import Logger
import configparser #The script lists are stored in metadata as serialised config files. import configparser # The script lists are stored in metadata as serialised config files.
import io #To allow configparser to write to a string. import io # To allow configparser to write to a string.
import os.path import os.path
import pkgutil import pkgutil
import sys import sys
import importlib.util import importlib.util
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from cura.CuraApplication import CuraApplication
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
if TYPE_CHECKING:
from .Script import Script
## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated ## The post processing plugin is an Extension type plugin that enables pre-written scripts to post process generated
# g-code files. # g-code files.
class PostProcessingPlugin(QObject, Extension): class PostProcessingPlugin(QObject, Extension):
def __init__(self, parent = None): def __init__(self, parent = None) -> None:
super().__init__(parent) QObject.__init__(self, parent)
Extension.__init__(self)
self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup) self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup)
self._view = None self._view = None
# Loaded scripts are all scripts that can be used # Loaded scripts are all scripts that can be used
self._loaded_scripts = {} self._loaded_scripts = {} # type: Dict[str, Type[Script]]
self._script_labels = {} self._script_labels = {} # type: Dict[str, str]
# Script list contains instances of scripts in loaded_scripts. # Script list contains instances of scripts in loaded_scripts.
# There can be duplicates, which will be executed in sequence. # There can be duplicates, which will be executed in sequence.
self._script_list = [] self._script_list = [] # type: List[Script]
self._selected_script_index = -1 self._selected_script_index = -1
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute) Application.getInstance().getOutputDeviceManager().writeStarted.connect(self.execute)
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) #When the current printer changes, update the list of scripts. Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) # When the current printer changes, update the list of scripts.
Application.getInstance().mainWindowChanged.connect(self._createView) #When the main window is created, create the view so that we can display the post-processing icon if necessary. CuraApplication.getInstance().mainWindowChanged.connect(self._createView) # When the main window is created, create the view so that we can display the post-processing icon if necessary.
selectedIndexChanged = pyqtSignal() selectedIndexChanged = pyqtSignal()
@pyqtProperty("QVariant", notify = selectedIndexChanged)
def selectedScriptDefinitionId(self): @pyqtProperty(str, notify = selectedIndexChanged)
def selectedScriptDefinitionId(self) -> Optional[str]:
try: try:
return self._script_list[self._selected_script_index].getDefinitionId() return self._script_list[self._selected_script_index].getDefinitionId()
except: except:
return "" return ""
@pyqtProperty("QVariant", notify=selectedIndexChanged) @pyqtProperty(str, notify=selectedIndexChanged)
def selectedScriptStackId(self): def selectedScriptStackId(self) -> Optional[str]:
try: try:
return self._script_list[self._selected_script_index].getStackId() return self._script_list[self._selected_script_index].getStackId()
except: except:
return "" return ""
## Execute all post-processing scripts on the gcode. ## Execute all post-processing scripts on the gcode.
def execute(self, output_device): def execute(self, output_device) -> None:
scene = Application.getInstance().getController().getScene() scene = Application.getInstance().getController().getScene()
# If the scene does not have a gcode, do nothing # If the scene does not have a gcode, do nothing
if not hasattr(scene, "gcode_dict"): if not hasattr(scene, "gcode_dict"):
@ -67,7 +75,7 @@ class PostProcessingPlugin(QObject, Extension):
return return
# get gcode list for the active build plate # get gcode list for the active build plate
active_build_plate_id = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate active_build_plate_id = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
gcode_list = gcode_dict[active_build_plate_id] gcode_list = gcode_dict[active_build_plate_id]
if not gcode_list: if not gcode_list:
return return
@ -86,16 +94,17 @@ class PostProcessingPlugin(QObject, Extension):
Logger.log("e", "Already post processed") Logger.log("e", "Already post processed")
@pyqtSlot(int) @pyqtSlot(int)
def setSelectedScriptIndex(self, index): def setSelectedScriptIndex(self, index: int) -> None:
self._selected_script_index = index if self._selected_script_index != index:
self.selectedIndexChanged.emit() self._selected_script_index = index
self.selectedIndexChanged.emit()
@pyqtProperty(int, notify = selectedIndexChanged) @pyqtProperty(int, notify = selectedIndexChanged)
def selectedScriptIndex(self): def selectedScriptIndex(self) -> int:
return self._selected_script_index return self._selected_script_index
@pyqtSlot(int, int) @pyqtSlot(int, int)
def moveScript(self, index, new_index): def moveScript(self, index: int, new_index: int) -> None:
if new_index < 0 or new_index > len(self._script_list) - 1: if new_index < 0 or new_index > len(self._script_list) - 1:
return # nothing needs to be done return # nothing needs to be done
else: else:
@ -107,7 +116,7 @@ class PostProcessingPlugin(QObject, Extension):
## Remove a script from the active script list by index. ## Remove a script from the active script list by index.
@pyqtSlot(int) @pyqtSlot(int)
def removeScriptByIndex(self, index): def removeScriptByIndex(self, index: int) -> None:
self._script_list.pop(index) self._script_list.pop(index)
if len(self._script_list) - 1 < self._selected_script_index: if len(self._script_list) - 1 < self._selected_script_index:
self._selected_script_index = len(self._script_list) - 1 self._selected_script_index = len(self._script_list) - 1
@ -118,14 +127,16 @@ class PostProcessingPlugin(QObject, Extension):
## Load all scripts from all paths where scripts can be found. ## Load all scripts from all paths where scripts can be found.
# #
# This should probably only be done on init. # This should probably only be done on init.
def loadAllScripts(self): def loadAllScripts(self) -> None:
if self._loaded_scripts: #Already loaded. if self._loaded_scripts: # Already loaded.
return return
#The PostProcessingPlugin path is for built-in scripts. # The PostProcessingPlugin path is for built-in scripts.
#The Resources path is where the user should store custom scripts. # The Resources path is where the user should store custom scripts.
#The Preferences path is legacy, where the user may previously have stored scripts. # The Preferences path is legacy, where the user may previously have stored scripts.
for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Resources), Resources.getStoragePath(Resources.Preferences)]: for root in [PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), Resources.getStoragePath(Resources.Resources), Resources.getStoragePath(Resources.Preferences)]:
if root is None:
continue
path = os.path.join(root, "scripts") path = os.path.join(root, "scripts")
if not os.path.isdir(path): if not os.path.isdir(path):
try: try:
@ -139,7 +150,7 @@ class PostProcessingPlugin(QObject, Extension):
## Load all scripts from provided path. ## Load all scripts from provided path.
# This should probably only be done on init. # This should probably only be done on init.
# \param path Path to check for scripts. # \param path Path to check for scripts.
def loadScripts(self, path): def loadScripts(self, path: str) -> None:
## Load all scripts in the scripts folders ## Load all scripts in the scripts folders
scripts = pkgutil.iter_modules(path = [path]) scripts = pkgutil.iter_modules(path = [path])
for loader, script_name, ispkg in scripts: for loader, script_name, ispkg in scripts:
@ -148,6 +159,8 @@ class PostProcessingPlugin(QObject, Extension):
try: try:
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py")) spec = importlib.util.spec_from_file_location(__name__ + "." + script_name, os.path.join(path, script_name + ".py"))
loaded_script = importlib.util.module_from_spec(spec) loaded_script = importlib.util.module_from_spec(spec)
if spec.loader is None:
continue
spec.loader.exec_module(loaded_script) spec.loader.exec_module(loaded_script)
sys.modules[script_name] = loaded_script #TODO: This could be a security risk. Overwrite any module with a user-provided name? sys.modules[script_name] = loaded_script #TODO: This could be a security risk. Overwrite any module with a user-provided name?
@ -172,23 +185,24 @@ class PostProcessingPlugin(QObject, Extension):
loadedScriptListChanged = pyqtSignal() loadedScriptListChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = loadedScriptListChanged) @pyqtProperty("QVariantList", notify = loadedScriptListChanged)
def loadedScriptList(self): def loadedScriptList(self) -> List[str]:
return sorted(list(self._loaded_scripts.keys())) return sorted(list(self._loaded_scripts.keys()))
@pyqtSlot(str, result = str) @pyqtSlot(str, result = str)
def getScriptLabelByKey(self, key): def getScriptLabelByKey(self, key: str) -> Optional[str]:
return self._script_labels[key] return self._script_labels.get(key)
scriptListChanged = pyqtSignal() scriptListChanged = pyqtSignal()
@pyqtProperty("QVariantList", notify = scriptListChanged) @pyqtProperty("QStringList", notify = scriptListChanged)
def scriptList(self): def scriptList(self) -> List[str]:
script_list = [script.getSettingData()["key"] for script in self._script_list] script_list = [script.getSettingData()["key"] for script in self._script_list]
return script_list return script_list
@pyqtSlot(str) @pyqtSlot(str)
def addScriptToList(self, key): def addScriptToList(self, key: str) -> None:
Logger.log("d", "Adding script %s to list.", key) Logger.log("d", "Adding script %s to list.", key)
new_script = self._loaded_scripts[key]() new_script = self._loaded_scripts[key]()
new_script.initialize()
self._script_list.append(new_script) self._script_list.append(new_script)
self.setSelectedScriptIndex(len(self._script_list) - 1) self.setSelectedScriptIndex(len(self._script_list) - 1)
self.scriptListChanged.emit() self.scriptListChanged.emit()
@ -196,81 +210,89 @@ class PostProcessingPlugin(QObject, Extension):
## When the global container stack is changed, swap out the list of active ## When the global container stack is changed, swap out the list of active
# scripts. # scripts.
def _onGlobalContainerStackChanged(self): def _onGlobalContainerStackChanged(self) -> None:
self.loadAllScripts() self.loadAllScripts()
new_stack = Application.getInstance().getGlobalContainerStack() new_stack = Application.getInstance().getGlobalContainerStack()
if new_stack is None:
return
self._script_list.clear() self._script_list.clear()
if not new_stack.getMetaDataEntry("post_processing_scripts"): #Missing or empty. if not new_stack.getMetaDataEntry("post_processing_scripts"): # Missing or empty.
self.scriptListChanged.emit() #Even emit this if it didn't change. We want it to write the empty list to the stack's metadata. self.scriptListChanged.emit() # Even emit this if it didn't change. We want it to write the empty list to the stack's metadata.
return return
self._script_list.clear() self._script_list.clear()
scripts_list_strs = new_stack.getMetaDataEntry("post_processing_scripts") scripts_list_strs = new_stack.getMetaDataEntry("post_processing_scripts")
for script_str in scripts_list_strs.split("\n"): #Encoded config files should never contain three newlines in a row. At most 2, just before section headers. for script_str in scripts_list_strs.split("\n"): # Encoded config files should never contain three newlines in a row. At most 2, just before section headers.
if not script_str: #There were no scripts in this one (or a corrupt file caused more than 3 consecutive newlines here). if not script_str: # There were no scripts in this one (or a corrupt file caused more than 3 consecutive newlines here).
continue continue
script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") #Unescape escape sequences. script_str = script_str.replace(r"\\\n", "\n").replace(r"\\\\", "\\\\") # Unescape escape sequences.
script_parser = configparser.ConfigParser(interpolation = None) script_parser = configparser.ConfigParser(interpolation = None)
script_parser.optionxform = str #Don't transform the setting keys as they are case-sensitive. script_parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive.
script_parser.read_string(script_str) script_parser.read_string(script_str)
for script_name, settings in script_parser.items(): #There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script. for script_name, settings in script_parser.items(): # There should only be one, really! Otherwise we can't guarantee the order or allow multiple uses of the same script.
if script_name == "DEFAULT": #ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one. if script_name == "DEFAULT": # ConfigParser always has a DEFAULT section, but we don't fill it. Ignore this one.
continue continue
if script_name not in self._loaded_scripts: #Don't know this post-processing plug-in. if script_name not in self._loaded_scripts: # Don't know this post-processing plug-in.
Logger.log("e", "Unknown post-processing script {script_name} was encountered in this global stack.".format(script_name = script_name)) Logger.log("e", "Unknown post-processing script {script_name} was encountered in this global stack.".format(script_name = script_name))
continue continue
new_script = self._loaded_scripts[script_name]() new_script = self._loaded_scripts[script_name]()
for setting_key, setting_value in settings.items(): #Put all setting values into the script. new_script.initialize()
new_script._instance.setProperty(setting_key, "value", setting_value) for setting_key, setting_value in settings.items(): # Put all setting values into the script.
if new_script._instance is not None:
new_script._instance.setProperty(setting_key, "value", setting_value)
self._script_list.append(new_script) self._script_list.append(new_script)
self.setSelectedScriptIndex(0) self.setSelectedScriptIndex(0)
self.scriptListChanged.emit() self.scriptListChanged.emit()
@pyqtSlot() @pyqtSlot()
def writeScriptsToStack(self): def writeScriptsToStack(self) -> None:
script_list_strs = [] script_list_strs = [] # type: List[str]
for script in self._script_list: for script in self._script_list:
parser = configparser.ConfigParser(interpolation = None) #We'll encode the script as a config with one section. The section header is the key and its values are the settings. parser = configparser.ConfigParser(interpolation = None) # We'll encode the script as a config with one section. The section header is the key and its values are the settings.
parser.optionxform = str #Don't transform the setting keys as they are case-sensitive. parser.optionxform = str # type: ignore # Don't transform the setting keys as they are case-sensitive.
script_name = script.getSettingData()["key"] script_name = script.getSettingData()["key"]
parser.add_section(script_name) parser.add_section(script_name)
for key in script.getSettingData()["settings"]: for key in script.getSettingData()["settings"]:
value = script.getSettingValueByKey(key) value = script.getSettingValueByKey(key)
parser[script_name][key] = str(value) parser[script_name][key] = str(value)
serialized = io.StringIO() #ConfigParser can only write to streams. Fine. serialized = io.StringIO() # ConfigParser can only write to streams. Fine.
parser.write(serialized) parser.write(serialized)
serialized.seek(0) serialized.seek(0)
script_str = serialized.read() script_str = serialized.read()
script_str = script_str.replace("\\\\", r"\\\\").replace("\n", r"\\\n") #Escape newlines because configparser sees those as section delimiters. script_str = script_str.replace("\\\\", r"\\\\").replace("\n", r"\\\n") # Escape newlines because configparser sees those as section delimiters.
script_list_strs.append(script_str) script_list_strs.append(script_str)
script_list_strs = "\n".join(script_list_strs) #ConfigParser should never output three newlines in a row when serialised, so it's a safe delimiter. script_list_string = "\n".join(script_list_strs) # ConfigParser should never output three newlines in a row when serialised, so it's a safe delimiter.
global_stack = Application.getInstance().getGlobalContainerStack() global_stack = Application.getInstance().getGlobalContainerStack()
if global_stack is None:
return
if "post_processing_scripts" not in global_stack.getMetaData(): if "post_processing_scripts" not in global_stack.getMetaData():
global_stack.setMetaDataEntry("post_processing_scripts", "") global_stack.setMetaDataEntry("post_processing_scripts", "")
Application.getInstance().getGlobalContainerStack().setMetaDataEntry("post_processing_scripts", script_list_strs)
global_stack.setMetaDataEntry("post_processing_scripts", script_list_string)
## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection. ## Creates the view used by show popup. The view is saved because of the fairly aggressive garbage collection.
def _createView(self): def _createView(self) -> None:
Logger.log("d", "Creating post processing plugin view.") Logger.log("d", "Creating post processing plugin view.")
self.loadAllScripts() self.loadAllScripts()
# Create the plugin dialog component # Create the plugin dialog component
path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml") path = os.path.join(cast(str, PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin")), "PostProcessingPlugin.qml")
self._view = Application.getInstance().createQmlComponent(path, {"manager": self}) self._view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
if self._view is None: if self._view is None:
Logger.log("e", "Not creating PostProcessing button near save button because the QML component failed to be created.") Logger.log("e", "Not creating PostProcessing button near save button because the QML component failed to be created.")
return return
Logger.log("d", "Post processing view created.") Logger.log("d", "Post processing view created.")
# Create the save button component # Create the save button component
Application.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton")) CuraApplication.getInstance().addAdditionalComponent("saveButton", self._view.findChild(QObject, "postProcessingSaveAreaButton"))
## Show the (GUI) popup of the post processing plugin. ## Show the (GUI) popup of the post processing plugin.
def showPopup(self): def showPopup(self) -> None:
if self._view is None: if self._view is None:
self._createView() self._createView()
if self._view is None: if self._view is None:
@ -282,8 +304,9 @@ class PostProcessingPlugin(QObject, Extension):
# To do this we use the global container stack propertyChanged. # To do this we use the global container stack propertyChanged.
# Re-slicing is necessary for setting changes in this plugin, because the changes # Re-slicing is necessary for setting changes in this plugin, because the changes
# are applied only once per "fresh" gcode # are applied only once per "fresh" gcode
def _propertyChanged(self): def _propertyChanged(self) -> None:
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
global_container_stack.propertyChanged.emit("post_processing_plugin", "value") if global_container_stack is not None:
global_container_stack.propertyChanged.emit("post_processing_plugin", "value")

View File

@ -62,6 +62,7 @@ UM.Dialog
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: base.textMargin anchors.rightMargin: base.textMargin
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
elide: Text.ElideRight
} }
ListView ListView
{ {
@ -115,6 +116,7 @@ UM.Dialog
{ {
wrapMode: Text.Wrap wrapMode: Text.Wrap
text: control.text text: control.text
elide: Text.ElideRight
color: activeScriptButton.checked ? palette.highlightedText : palette.text color: activeScriptButton.checked ? palette.highlightedText : palette.text
} }
} }
@ -275,6 +277,7 @@ UM.Dialog
anchors.leftMargin: base.textMargin anchors.leftMargin: base.textMargin
anchors.right: parent.right anchors.right: parent.right
anchors.rightMargin: base.textMargin anchors.rightMargin: base.textMargin
elide: Text.ElideRight
height: 20 * screenScaleFactor height: 20 * screenScaleFactor
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")

View File

@ -1,6 +1,8 @@
# Copyright (c) 2015 Jaime van Kessel # Copyright (c) 2015 Jaime van Kessel
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from typing import Optional, Any, Dict, TYPE_CHECKING, List
from UM.Signal import Signal, signalemitter from UM.Signal import Signal, signalemitter
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
@ -17,23 +19,27 @@ import json
import collections import collections
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
if TYPE_CHECKING:
from UM.Settings.Interfaces import DefinitionContainerInterface
## Base class for scripts. All scripts should inherit the script class. ## Base class for scripts. All scripts should inherit the script class.
@signalemitter @signalemitter
class Script: class Script:
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
self._settings = None self._stack = None # type: Optional[ContainerStack]
self._stack = None self._definition = None # type: Optional[DefinitionContainerInterface]
self._instance = None # type: Optional[InstanceContainer]
def initialize(self) -> None:
setting_data = self.getSettingData() setting_data = self.getSettingData()
self._stack = ContainerStack(stack_id = str(id(self))) self._stack = ContainerStack(stack_id=str(id(self)))
self._stack.setDirty(False) # This stack does not need to be saved. self._stack.setDirty(False) # This stack does not need to be saved.
## Check if the definition of this script already exists. If not, add it to the registry. ## Check if the definition of this script already exists. If not, add it to the registry.
if "key" in setting_data: if "key" in setting_data:
definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = setting_data["key"]) definitions = ContainerRegistry.getInstance().findDefinitionContainers(id=setting_data["key"])
if definitions: if definitions:
# Definition was found # Definition was found
self._definition = definitions[0] self._definition = definitions[0]
@ -45,10 +51,13 @@ class Script:
except ContainerFormatError: except ContainerFormatError:
self._definition = None self._definition = None
return return
if self._definition is None:
return
self._stack.addContainer(self._definition) self._stack.addContainer(self._definition)
self._instance = InstanceContainer(container_id="ScriptInstanceContainer") self._instance = InstanceContainer(container_id="ScriptInstanceContainer")
self._instance.setDefinition(self._definition.getId()) self._instance.setDefinition(self._definition.getId())
self._instance.setMetaDataEntry("setting_version", self._definition.getMetaDataEntry("setting_version", default = 0)) self._instance.setMetaDataEntry("setting_version",
self._definition.getMetaDataEntry("setting_version", default=0))
self._stack.addContainer(self._instance) self._stack.addContainer(self._instance)
self._stack.propertyChanged.connect(self._onPropertyChanged) self._stack.propertyChanged.connect(self._onPropertyChanged)
@ -57,16 +66,17 @@ class Script:
settingsLoaded = Signal() settingsLoaded = Signal()
valueChanged = Signal() # Signal emitted whenever a value of a setting is changed valueChanged = Signal() # Signal emitted whenever a value of a setting is changed
def _onPropertyChanged(self, key, property_name): def _onPropertyChanged(self, key: str, property_name: str) -> None:
if property_name == "value": if property_name == "value":
self.valueChanged.emit() self.valueChanged.emit()
# Property changed: trigger reslice # Property changed: trigger reslice
# To do this we use the global container stack propertyChanged. # To do this we use the global container stack propertyChanged.
# Reslicing is necessary for setting changes in this plugin, because the changes # Re-slicing is necessary for setting changes in this plugin, because the changes
# are applied only once per "fresh" gcode # are applied only once per "fresh" gcode
global_container_stack = Application.getInstance().getGlobalContainerStack() global_container_stack = Application.getInstance().getGlobalContainerStack()
global_container_stack.propertyChanged.emit(key, property_name) if global_container_stack is not None:
global_container_stack.propertyChanged.emit(key, property_name)
## Needs to return a dict that can be used to construct a settingcategory file. ## Needs to return a dict that can be used to construct a settingcategory file.
# See the example script for an example. # See the example script for an example.
@ -74,30 +84,35 @@ class Script:
# Scripts can either override getSettingData directly, or use getSettingDataString # Scripts can either override getSettingData directly, or use getSettingDataString
# to return a string that will be parsed as json. The latter has the benefit over # to return a string that will be parsed as json. The latter has the benefit over
# returning a dict in that the order of settings is maintained. # returning a dict in that the order of settings is maintained.
def getSettingData(self): def getSettingData(self) -> Dict[str, Any]:
setting_data = self.getSettingDataString() setting_data_as_string = self.getSettingDataString()
if type(setting_data) == str: setting_data = json.loads(setting_data_as_string, object_pairs_hook = collections.OrderedDict)
setting_data = json.loads(setting_data, object_pairs_hook = collections.OrderedDict)
return setting_data return setting_data
def getSettingDataString(self): def getSettingDataString(self) -> str:
raise NotImplementedError() raise NotImplementedError()
def getDefinitionId(self): def getDefinitionId(self) -> Optional[str]:
if self._stack: if self._stack:
return self._stack.getBottom().getId() bottom = self._stack.getBottom()
if bottom is not None:
return bottom.getId()
return None
def getStackId(self): def getStackId(self) -> Optional[str]:
if self._stack: if self._stack:
return self._stack.getId() return self._stack.getId()
return None
## Convenience function that retrieves value of a setting from the stack. ## Convenience function that retrieves value of a setting from the stack.
def getSettingValueByKey(self, key): def getSettingValueByKey(self, key: str) -> Any:
return self._stack.getProperty(key, "value") if self._stack is not None:
return self._stack.getProperty(key, "value")
return None
## Convenience function that finds the value in a line of g-code. ## Convenience function that finds the value in a line of g-code.
# When requesting key = x from line "G1 X100" the value 100 is returned. # When requesting key = x from line "G1 X100" the value 100 is returned.
def getValue(self, line, key, default = None): def getValue(self, line: str, key: str, default = None) -> Any:
if not key in line or (';' in line and line.find(key) > line.find(';')): if not key in line or (';' in line and line.find(key) > line.find(';')):
return default return default
sub_part = line[line.find(key) + 1:] sub_part = line[line.find(key) + 1:]
@ -125,7 +140,7 @@ class Script:
# \param line The original g-code line that must be modified. If not # \param line The original g-code line that must be modified. If not
# provided, an entirely new g-code line will be produced. # provided, an entirely new g-code line will be produced.
# \return A line of g-code with the desired parameters filled in. # \return A line of g-code with the desired parameters filled in.
def putValue(self, line = "", **kwargs): def putValue(self, line: str = "", **kwargs) -> str:
#Strip the comment. #Strip the comment.
comment = "" comment = ""
if ";" in line: if ";" in line:
@ -166,5 +181,5 @@ class Script:
## This is called when the script is executed. ## This is called when the script is executed.
# It gets a list of g-code strings and needs to return a (modified) list. # It gets a list of g-code strings and needs to return a (modified) list.
def execute(self, data): def execute(self, data: List[str]) -> List[str]:
raise NotImplementedError() raise NotImplementedError()

View File

@ -2,8 +2,8 @@
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher. # The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from . import PostProcessingPlugin from . import PostProcessingPlugin
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return {} return {}

View File

@ -0,0 +1,53 @@
# Cura PostProcessingPlugin
# Author: Amanda de Castilho
# Date: August 28, 2018
# Description: This plugin inserts a line at the start of each layer,
# M117 - displays the filename and layer height to the LCD
# Alternatively, user can override the filename to display alt text + layer height
from ..Script import Script
from UM.Application import Application
class DisplayFilenameAndLayerOnLCD(Script):
def __init__(self):
super().__init__()
def getSettingDataString(self):
return """{
"name": "Display filename and layer on LCD",
"key": "DisplayFilenameAndLayerOnLCD",
"metadata": {},
"version": 2,
"settings":
{
"name":
{
"label": "text to display:",
"description": "By default the current filename will be displayed on the LCD. Enter text here to override the filename and display something else.",
"type": "str",
"default_value": ""
}
}
}"""
def execute(self, data):
if self.getSettingValueByKey("name") != "":
name = self.getSettingValueByKey("name")
else:
name = Application.getInstance().getPrintInformation().jobName
lcd_text = "M117 " + name + " layer: "
i = 0
for layer in data:
display_text = lcd_text + str(i)
layer_index = data.index(layer)
lines = layer.split("\n")
for line in lines:
if line.startswith(";LAYER:"):
line_index = lines.index(line)
lines.insert(line_index + 1, display_text)
i += 1
final_lines = "\n".join(lines)
data[layer_index] = final_lines
return data

View File

@ -3,12 +3,10 @@
from UM.Platform import Platform from UM.Platform import Platform
from UM.Logger import Logger from UM.Logger import Logger
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return { return {}
}
def register(app): def register(app):
if Platform.isWindows(): if Platform.isWindows():

View File

@ -234,6 +234,11 @@ Item
UM.SimulationView.setCurrentLayer(value) UM.SimulationView.setCurrentLayer(value)
var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue) var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue)
// In case there is only one layer, the diff value results in a NaN, so this is for catching this specific case
if (isNaN(diff))
{
diff = 0
}
var newUpperYPosition = Math.round(diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize))) var newUpperYPosition = Math.round(diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)))
y = newUpperYPosition y = newUpperYPosition
@ -339,6 +344,11 @@ Item
UM.SimulationView.setMinimumLayer(value) UM.SimulationView.setMinimumLayer(value)
var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue) var diff = (value - sliderRoot.maximumValue) / (sliderRoot.minimumValue - sliderRoot.maximumValue)
// In case there is only one layer, the diff value results in a NaN, so this is for catching this specific case
if (isNaN(diff))
{
diff = 0
}
var newLowerYPosition = Math.round((sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize) + diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize))) var newLowerYPosition = Math.round((sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize) + diff * (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)))
y = newLowerYPosition y = newLowerYPosition

View File

@ -21,9 +21,10 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Signal import Signal from UM.Signal import Signal
from UM.View.CompositePass import CompositePass
from UM.View.GL.OpenGL import OpenGL from UM.View.GL.OpenGL import OpenGL
from UM.View.GL.OpenGLContext import OpenGLContext from UM.View.GL.OpenGLContext import OpenGLContext
from UM.View.GL.ShaderProgram import ShaderProgram
from UM.View.View import View from UM.View.View import View
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
@ -36,13 +37,11 @@ from .SimulationViewProxy import SimulationViewProxy
import numpy import numpy
import os.path import os.path
from typing import Optional, TYPE_CHECKING, List from typing import Optional, TYPE_CHECKING, List, cast
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Scene.Scene import Scene from UM.Scene.Scene import Scene
from UM.View.GL.ShaderProgram import ShaderProgram
from UM.View.RenderPass import RenderPass
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -64,7 +63,7 @@ class SimulationView(View):
self._minimum_layer_num = 0 self._minimum_layer_num = 0
self._current_layer_mesh = None self._current_layer_mesh = None
self._current_layer_jumps = None self._current_layer_jumps = None
self._top_layers_job = None self._top_layers_job = None # type: Optional["_CreateTopLayersJob"]
self._activity = False self._activity = False
self._old_max_layers = 0 self._old_max_layers = 0
@ -78,10 +77,10 @@ class SimulationView(View):
self._ghost_shader = None # type: Optional["ShaderProgram"] self._ghost_shader = None # type: Optional["ShaderProgram"]
self._layer_pass = None # type: Optional[SimulationPass] self._layer_pass = None # type: Optional[SimulationPass]
self._composite_pass = None # type: Optional[RenderPass] self._composite_pass = None # type: Optional[CompositePass]
self._old_layer_bindings = None self._old_layer_bindings = None # type: Optional[List[str]]
self._simulationview_composite_shader = None # type: Optional["ShaderProgram"] self._simulationview_composite_shader = None # type: Optional["ShaderProgram"]
self._old_composite_shader = None self._old_composite_shader = None # type: Optional["ShaderProgram"]
self._global_container_stack = None # type: Optional[ContainerStack] self._global_container_stack = None # type: Optional[ContainerStack]
self._proxy = SimulationViewProxy() self._proxy = SimulationViewProxy()
@ -204,9 +203,11 @@ class SimulationView(View):
if not self._ghost_shader: if not self._ghost_shader:
self._ghost_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader")) self._ghost_shader = OpenGL.getInstance().createShaderProgram(Resources.getPath(Resources.Shaders, "color.shader"))
self._ghost_shader.setUniformValue("u_color", Color(*Application.getInstance().getTheme().getColor("layerview_ghost").getRgb())) theme = CuraApplication.getInstance().getTheme()
if theme is not None:
self._ghost_shader.setUniformValue("u_color", Color(*theme.getColor("layerview_ghost").getRgb()))
for node in DepthFirstIterator(scene.getRoot()): for node in DepthFirstIterator(scene.getRoot()): # type: ignore
# We do not want to render ConvexHullNode as it conflicts with the bottom layers. # We do not want to render ConvexHullNode as it conflicts with the bottom layers.
# However, it is somewhat relevant when the node is selected, so do render it then. # However, it is somewhat relevant when the node is selected, so do render it then.
if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()): if type(node) is ConvexHullNode and not Selection.isSelected(node.getWatchedNode()):
@ -346,8 +347,8 @@ class SimulationView(View):
self._old_max_layers = self._max_layers self._old_max_layers = self._max_layers
## Recalculate num max layers ## Recalculate num max layers
new_max_layers = 0 new_max_layers = -1
for node in DepthFirstIterator(scene.getRoot()): for node in DepthFirstIterator(scene.getRoot()): # type: ignore
layer_data = node.callDecoration("getLayerData") layer_data = node.callDecoration("getLayerData")
if not layer_data: if not layer_data:
continue continue
@ -381,7 +382,7 @@ class SimulationView(View):
if new_max_layers < layer_count: if new_max_layers < layer_count:
new_max_layers = layer_count new_max_layers = layer_count
if new_max_layers > 0 and new_max_layers != self._old_max_layers: if new_max_layers >= 0 and new_max_layers != self._old_max_layers:
self._max_layers = new_max_layers self._max_layers = new_max_layers
# The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first # The qt slider has a bit of weird behavior that if the maxvalue needs to be changed first
@ -398,7 +399,7 @@ class SimulationView(View):
def calculateMaxPathsOnLayer(self, layer_num: int) -> None: def calculateMaxPathsOnLayer(self, layer_num: int) -> None:
# Update the currentPath # Update the currentPath
scene = self.getController().getScene() scene = self.getController().getScene()
for node in DepthFirstIterator(scene.getRoot()): for node in DepthFirstIterator(scene.getRoot()): # type: ignore
layer_data = node.callDecoration("getLayerData") layer_data = node.callDecoration("getLayerData")
if not layer_data: if not layer_data:
continue continue
@ -474,15 +475,17 @@ class SimulationView(View):
self._onGlobalStackChanged() self._onGlobalStackChanged()
if not self._simulationview_composite_shader: if not self._simulationview_composite_shader:
self._simulationview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), "simulationview_composite.shader")) plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath("SimulationView"))
theme = Application.getInstance().getTheme() self._simulationview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(plugin_path, "simulationview_composite.shader"))
self._simulationview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb())) theme = CuraApplication.getInstance().getTheme()
self._simulationview_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb())) if theme is not None:
self._simulationview_composite_shader.setUniformValue("u_background_color", Color(*theme.getColor("viewport_background").getRgb()))
self._simulationview_composite_shader.setUniformValue("u_outline_color", Color(*theme.getColor("model_selection_outline").getRgb()))
if not self._composite_pass: if not self._composite_pass:
self._composite_pass = self.getRenderer().getRenderPass("composite") self._composite_pass = cast(CompositePass, self.getRenderer().getRenderPass("composite"))
self._old_layer_bindings = self._composite_pass.getLayerBindings()[:] # make a copy so we can restore to it later self._old_layer_bindings = self._composite_pass.getLayerBindings()[:] # make a copy so we can restore to it later
self._composite_pass.getLayerBindings().append("simulationview") self._composite_pass.getLayerBindings().append("simulationview")
self._old_composite_shader = self._composite_pass.getCompositeShader() self._old_composite_shader = self._composite_pass.getCompositeShader()
self._composite_pass.setCompositeShader(self._simulationview_composite_shader) self._composite_pass.setCompositeShader(self._simulationview_composite_shader)
@ -496,8 +499,8 @@ class SimulationView(View):
self._nozzle_node.setParent(None) self._nozzle_node.setParent(None)
self.getRenderer().removeRenderPass(self._layer_pass) self.getRenderer().removeRenderPass(self._layer_pass)
if self._composite_pass: if self._composite_pass:
self._composite_pass.setLayerBindings(self._old_layer_bindings) self._composite_pass.setLayerBindings(cast(List[str], self._old_layer_bindings))
self._composite_pass.setCompositeShader(self._old_composite_shader) self._composite_pass.setCompositeShader(cast(ShaderProgram, self._old_composite_shader))
return False return False
@ -606,7 +609,7 @@ class _CreateTopLayersJob(Job):
def run(self) -> None: def run(self) -> None:
layer_data = None layer_data = None
for node in DepthFirstIterator(self._scene.getRoot()): for node in DepthFirstIterator(self._scene.getRoot()): # type: ignore
layer_data = node.callDecoration("getLayerData") layer_data = node.callDecoration("getLayerData")
if layer_data: if layer_data:
break break

View File

@ -33,30 +33,35 @@ class SliceInfo(QObject, Extension):
def __init__(self, parent = None): def __init__(self, parent = None):
QObject.__init__(self, parent) QObject.__init__(self, parent)
Extension.__init__(self) Extension.__init__(self)
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._onWriteStarted)
Application.getInstance().getPreferences().addPreference("info/send_slice_info", True) self._application = Application.getInstance()
Application.getInstance().getPreferences().addPreference("info/asked_send_slice_info", False)
self._application.getOutputDeviceManager().writeStarted.connect(self._onWriteStarted)
self._application.getPreferences().addPreference("info/send_slice_info", True)
self._application.getPreferences().addPreference("info/asked_send_slice_info", False)
self._more_info_dialog = None self._more_info_dialog = None
self._example_data_content = None self._example_data_content = None
if not Application.getInstance().getPreferences().getValue("info/asked_send_slice_info"): self._application.initializationFinished.connect(self._onAppInitialized)
def _onAppInitialized(self):
# DO NOT read any preferences values in the constructor because at the time plugins are created, no version
# upgrade has been performed yet because version upgrades are plugins too!
if not self._application.getPreferences().getValue("info/asked_send_slice_info"):
self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymized usage statistics."), self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymized usage statistics."),
lifetime = 0, lifetime = 0,
dismissable = False, dismissable = False,
title = catalog.i18nc("@info:title", "Collecting Data")) title = catalog.i18nc("@info:title", "Collecting Data"))
self.send_slice_info_message.addAction("MoreInfo", name = catalog.i18nc("@action:button", "More info"), icon = None, self.send_slice_info_message.addAction("MoreInfo", name = catalog.i18nc("@action:button", "More info"), icon = None,
description = catalog.i18nc("@action:tooltip", "See more information on what data Cura sends."), button_style = Message.ActionButtonStyle.LINK) description = catalog.i18nc("@action:tooltip", "See more information on what data Cura sends."), button_style = Message.ActionButtonStyle.LINK)
self.send_slice_info_message.addAction("Dismiss", name = catalog.i18nc("@action:button", "Allow"), icon = None, self.send_slice_info_message.addAction("Dismiss", name = catalog.i18nc("@action:button", "Allow"), icon = None,
description = catalog.i18nc("@action:tooltip", "Allow Cura to send anonymized usage statistics to help prioritize future improvements to Cura. Some of your preferences and settings are sent, the Cura version and a hash of the models you're slicing.")) description = catalog.i18nc("@action:tooltip", "Allow Cura to send anonymized usage statistics to help prioritize future improvements to Cura. Some of your preferences and settings are sent, the Cura version and a hash of the models you're slicing."))
self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered) self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered)
self.send_slice_info_message.show() self.send_slice_info_message.show()
Application.getInstance().initializationFinished.connect(self._onAppInitialized)
def _onAppInitialized(self):
if self._more_info_dialog is None: if self._more_info_dialog is None:
self._more_info_dialog = self._createDialog("MoreInfoWindow.qml") self._more_info_dialog = self._createDialog("MoreInfoWindow.qml")

View File

@ -1,12 +1,11 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2015 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 . import SliceInfo from . import SliceInfo
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return { return {}
}
def register(app): def register(app):
return { "extension": SliceInfo.SliceInfo()} return { "extension": SliceInfo.SliceInfo()}

View File

@ -15,7 +15,7 @@ Item
{ {
id: sidebar id: sidebar
} }
Rectangle Item
{ {
id: header id: header
anchors anchors

View File

@ -23,6 +23,7 @@ Item
{ {
id: button id: button
text: catalog.i18nc("@action:button", "Back") text: catalog.i18nc("@action:button", "Back")
enabled: !toolbox.isDownloading
UM.RecolorImage UM.RecolorImage
{ {
id: backArrow id: backArrow
@ -39,7 +40,7 @@ Item
width: width width: width
height: height height: height
} }
color: button.hovered ? UM.Theme.getColor("primary") : UM.Theme.getColor("text") color: button.enabled ? (button.hovered ? UM.Theme.getColor("primary") : UM.Theme.getColor("text")) : UM.Theme.getColor("text_inactive")
source: UM.Theme.getIcon("arrow_left") source: UM.Theme.getIcon("arrow_left")
} }
width: UM.Theme.getSize("toolbox_back_button").width width: UM.Theme.getSize("toolbox_back_button").width
@ -59,7 +60,7 @@ Item
{ {
id: labelStyle id: labelStyle
text: control.text text: control.text
color: control.hovered ? UM.Theme.getColor("primary") : UM.Theme.getColor("text") color: control.enabled ? (control.hovered ? UM.Theme.getColor("primary") : UM.Theme.getColor("text")) : UM.Theme.getColor("text_inactive")
font: UM.Theme.getFont("default_bold") font: UM.Theme.getFont("default_bold")
horizontalAlignment: Text.AlignRight horizontalAlignment: Text.AlignRight
width: control.width width: control.width

View File

@ -17,7 +17,7 @@ UM.Dialog
// This dialog asks the user whether he/she wants to open a project file as a project or import models. // This dialog asks the user whether he/she wants to open a project file as a project or import models.
id: base id: base
title: catalog.i18nc("@title:window", "Confirm uninstall ") + toolbox.pluginToUninstall title: catalog.i18nc("@title:window", "Confirm uninstall") + toolbox.pluginToUninstall
width: 450 * screenScaleFactor width: 450 * screenScaleFactor
height: 50 * screenScaleFactor + dialogText.height + buttonBar.height height: 50 * screenScaleFactor + dialogText.height + buttonBar.height

View File

@ -9,9 +9,8 @@ import UM 1.1 as UM
Item Item
{ {
id: page id: page
property var details: base.selection property var details: base.selection || {}
anchors.fill: parent anchors.fill: parent
width: parent.width
ToolboxBackColumn ToolboxBackColumn
{ {
id: sidebar id: sidebar

View File

@ -27,7 +27,7 @@ Item
id: pluginsTabButton id: pluginsTabButton
text: catalog.i18nc("@title:tab", "Plugins") text: catalog.i18nc("@title:tab", "Plugins")
active: toolbox.viewCategory == "plugin" && enabled active: toolbox.viewCategory == "plugin" && enabled
enabled: toolbox.viewPage != "loading" && toolbox.viewPage != "errored" enabled: !toolbox.isDownloading && toolbox.viewPage != "loading" && toolbox.viewPage != "errored"
onClicked: onClicked:
{ {
toolbox.filterModelByProp("packages", "type", "plugin") toolbox.filterModelByProp("packages", "type", "plugin")
@ -41,7 +41,7 @@ Item
id: materialsTabButton id: materialsTabButton
text: catalog.i18nc("@title:tab", "Materials") text: catalog.i18nc("@title:tab", "Materials")
active: toolbox.viewCategory == "material" && enabled active: toolbox.viewCategory == "material" && enabled
enabled: toolbox.viewPage != "loading" && toolbox.viewPage != "errored" enabled: !toolbox.isDownloading && toolbox.viewPage != "loading" && toolbox.viewPage != "errored"
onClicked: onClicked:
{ {
toolbox.filterModelByProp("authors", "package_types", "material") toolbox.filterModelByProp("authors", "package_types", "material")
@ -55,6 +55,7 @@ Item
id: installedTabButton id: installedTabButton
text: catalog.i18nc("@title:tab", "Installed") text: catalog.i18nc("@title:tab", "Installed")
active: toolbox.viewCategory == "installed" active: toolbox.viewCategory == "installed"
enabled: !toolbox.isDownloading
anchors anchors
{ {
right: parent.right right: parent.right

View File

@ -603,7 +603,7 @@ class Toolbox(QObject, Extension):
@pyqtSlot() @pyqtSlot()
def cancelDownload(self) -> None: def cancelDownload(self) -> None:
Logger.log("i", "Toolbox: User cancelled the download of a plugin.") Logger.log("i", "Toolbox: User cancelled the download of a package.")
self.resetDownload() self.resetDownload()
def resetDownload(self) -> None: def resetDownload(self) -> None:
@ -755,6 +755,7 @@ class Toolbox(QObject, Extension):
self._active_package = package self._active_package = package
self.activePackageChanged.emit() self.activePackageChanged.emit()
## The active package is the package that is currently being downloaded
@pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged) @pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged)
def activePackage(self) -> Optional[Dict[str, Any]]: def activePackage(self) -> Optional[Dict[str, Any]]:
return self._active_package return self._active_package

View File

@ -27,14 +27,13 @@ class UFPWriter(MeshWriter):
MimeTypeDatabase.addMimeType( MimeTypeDatabase.addMimeType(
MimeType( MimeType(
name = "application/x-cura-stl-file", name = "application/x-ufp",
comment = "Cura UFP File", comment = "Cura UFP File",
suffixes = ["ufp"] suffixes = ["ufp"]
) )
) )
self._snapshot = None self._snapshot = None
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._createSnapshot)
def _createSnapshot(self, *args): def _createSnapshot(self, *args):
# must be called from the main thread because of OpenGL # must be called from the main thread because of OpenGL
@ -62,6 +61,8 @@ class UFPWriter(MeshWriter):
gcode.write(gcode_textio.getvalue().encode("UTF-8")) gcode.write(gcode_textio.getvalue().encode("UTF-8"))
archive.addRelation(virtual_path = "/3D/model.gcode", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode") archive.addRelation(virtual_path = "/3D/model.gcode", relation_type = "http://schemas.ultimaker.org/package/2018/relationships/gcode")
self._createSnapshot()
#Store the thumbnail. #Store the thumbnail.
if self._snapshot: if self._snapshot:
archive.addContentType(extension = "png", mime_type = "image/png") archive.addContentType(extension = "png", mime_type = "image/png")

View File

@ -2,9 +2,6 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from .src import DiscoverUM3Action from .src import DiscoverUM3Action
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from .src import UM3OutputDevicePlugin from .src import UM3OutputDevicePlugin
def getMetaData(): def getMetaData():

View File

@ -0,0 +1,47 @@
import QtQuick 2.3
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.3
import QtQuick.Controls 2.0 as Controls2
import QtGraphicalEffects 1.0
import UM 1.3 as UM
import Cura 1.0 as Cura
Rectangle
{
property var iconSource: null
width: 36 * screenScaleFactor
height: width
radius: 0.5 * width
color: clickArea.containsMouse ? UM.Theme.getColor("primary_hover") : UM.Theme.getColor("primary")
UM.RecolorImage
{
id: icon
width: parent.width / 2
height: width
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
color: UM.Theme.getColor("primary_text")
source: iconSource
}
MouseArea
{
id: clickArea
anchors.fill:parent
hoverEnabled: true
onClicked:
{
if (OutputDevice.activeCamera !== null)
{
OutputDevice.setActiveCamera(null)
}
else
{
OutputDevice.setActiveCamera(modelData.camera)
}
}
}
}

View File

@ -16,7 +16,7 @@ Component
{ {
id: base id: base
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme. property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var shadowRadius: 5 * screenScaleFactor
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme. property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
visible: OutputDevice != null visible: OutputDevice != null
anchors.fill: parent anchors.fill: parent
@ -83,6 +83,8 @@ Component
ListView ListView
{ {
id: printer_list
property var current_index: -1
anchors anchors
{ {
top: parent.top top: parent.top
@ -105,18 +107,35 @@ Component
height: childrenRect.height + UM.Theme.getSize("default_margin").height height: childrenRect.height + UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
color:
{
if(modelData.state == "disabled")
{
return UM.Theme.getColor("monitor_background_inactive")
}
else
{
return UM.Theme.getColor("monitor_background_active")
}
}
id: base id: base
property var shadowRadius: 5 property var shadowRadius: 5 * screenScaleFactor
property var collapsed: true property var collapsed: true
layer.enabled: true layer.enabled: true
layer.effect: DropShadow layer.effect: DropShadow
{ {
radius: base.shadowRadius radius: 5 * screenScaleFactor
verticalOffset: 2 verticalOffset: 2
color: "#3F000000" // 25% shadow color: "#3F000000" // 25% shadow
} }
Connections
{
target: printer_list
onCurrent_indexChanged: { base.collapsed = printer_list.current_index != model.index }
}
Item Item
{ {
id: printerInfo id: printerInfo
@ -132,7 +151,16 @@ Component
MouseArea MouseArea
{ {
anchors.fill: parent anchors.fill: parent
onClicked: base.collapsed = !base.collapsed onClicked:
{
if (base.collapsed) {
printer_list.current_index = model.index
}
else
{
printer_list.current_index = -1
}
}
} }
Item Item
@ -168,7 +196,7 @@ Component
{ {
if(modelData.state == "disabled") if(modelData.state == "disabled")
{ {
return UM.Theme.getColor("setting_control_disabled") return UM.Theme.getColor("monitor_text_inactive")
} }
if(modelData.activePrintJob != undefined) if(modelData.activePrintJob != undefined)
@ -176,7 +204,7 @@ Component
return UM.Theme.getColor("primary") return UM.Theme.getColor("primary")
} }
return UM.Theme.getColor("setting_control_disabled") return UM.Theme.getColor("monitor_text_inactive")
} }
} }
} }
@ -224,7 +252,7 @@ Component
width: parent.width width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
opacity: 0.6 color: UM.Theme.getColor("monitor_text_inactive")
} }
} }
@ -257,8 +285,16 @@ Component
Rectangle Rectangle
{ {
id: topSpacer id: topSpacer
color: UM.Theme.getColor("viewport_background") color:
height: 2 {
if(modelData.state == "disabled")
{
return UM.Theme.getColor("monitor_lining_inactive")
}
return UM.Theme.getColor("viewport_background")
}
// UM.Theme.getColor("viewport_background")
height: 1
anchors anchors
{ {
left: parent.left left: parent.left
@ -271,7 +307,14 @@ Component
PrinterFamilyPill PrinterFamilyPill
{ {
id: printerFamilyPill id: printerFamilyPill
color: UM.Theme.getColor("viewport_background") color:
{
if(modelData.state == "disabled")
{
return "transparent"
}
return UM.Theme.getColor("viewport_background")
}
anchors.top: topSpacer.bottom anchors.top: topSpacer.bottom
anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height
text: modelData.type text: modelData.type
@ -357,21 +400,13 @@ Component
function switchPopupState() function switchPopupState()
{ {
if (popup.visible) popup.visible ? popup.close() : popup.open()
{
popup.close()
}
else
{
popup.open()
}
} }
Controls2.Button Controls2.Button
{ {
id: contextButton id: contextButton
text: "\u22EE" //Unicode; Three stacked points. text: "\u22EE" //Unicode; Three stacked points.
font.pixelSize: 25
width: 35 width: 35
height: width height: width
anchors anchors
@ -389,6 +424,14 @@ Component
radius: 0.5 * width radius: 0.5 * width
color: UM.Theme.getColor("viewport_background") color: UM.Theme.getColor("viewport_background")
} }
contentItem: Label
{
text: contextButton.text
color: UM.Theme.getColor("monitor_text_inactive")
font.pixelSize: 25
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
onClicked: parent.switchPopupState() onClicked: parent.switchPopupState()
} }
@ -398,18 +441,21 @@ Component
// TODO Change once updating to Qt5.10 - The 'opened' property is in 5.10 but the behavior is now implemented with the visible property // TODO Change once updating to Qt5.10 - The 'opened' property is in 5.10 but the behavior is now implemented with the visible property
id: popup id: popup
clip: true clip: true
closePolicy: Controls2.Popup.CloseOnPressOutsideParent closePolicy: Popup.CloseOnPressOutside
x: parent.width - width x: (parent.width - width) + 26 * screenScaleFactor
y: contextButton.height y: contextButton.height - 5 * screenScaleFactor // Because shadow
width: 160 width: 182 * screenScaleFactor
height: contentItem.height + 2 * padding height: contentItem.height + 2 * padding
visible: false visible: false
padding: 5 * screenScaleFactor // Because shadow
transformOrigin: Controls2.Popup.Top transformOrigin: Popup.Top
contentItem: Item contentItem: Item
{ {
width: popup.width - 2 * popup.padding width: popup.width
height: childrenRect.height + 15 height: childrenRect.height + 36 * screenScaleFactor
anchors.topMargin: 10 * screenScaleFactor
anchors.bottomMargin: 10 * screenScaleFactor
Controls2.Button Controls2.Button
{ {
id: pauseButton id: pauseButton
@ -428,14 +474,22 @@ Component
} }
width: parent.width width: parent.width
enabled: modelData.activePrintJob != null && ["paused", "printing"].indexOf(modelData.activePrintJob.state) >= 0 enabled: modelData.activePrintJob != null && ["paused", "printing"].indexOf(modelData.activePrintJob.state) >= 0
visible: enabled
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 10 anchors.topMargin: 18 * screenScaleFactor
height: visible ? 39 * screenScaleFactor : 0 * screenScaleFactor
hoverEnabled: true hoverEnabled: true
background: Rectangle background: Rectangle
{ {
opacity: pauseButton.down || pauseButton.hovered ? 1 : 0 opacity: pauseButton.down || pauseButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background") color: UM.Theme.getColor("viewport_background")
} }
contentItem: Label
{
text: pauseButton.text
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
}
} }
Controls2.Button Controls2.Button
@ -448,6 +502,7 @@ Component
popup.close(); popup.close();
} }
width: parent.width width: parent.width
height: 39 * screenScaleFactor
anchors.top: pauseButton.bottom anchors.top: pauseButton.bottom
hoverEnabled: true hoverEnabled: true
enabled: modelData.activePrintJob != null && ["paused", "printing", "pre_print"].indexOf(modelData.activePrintJob.state) >= 0 enabled: modelData.activePrintJob != null && ["paused", "printing", "pre_print"].indexOf(modelData.activePrintJob.state) >= 0
@ -456,6 +511,12 @@ Component
opacity: abortButton.down || abortButton.hovered ? 1 : 0 opacity: abortButton.down || abortButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background") color: UM.Theme.getColor("viewport_background")
} }
contentItem: Label
{
text: abortButton.text
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
}
} }
MessageDialog MessageDialog
@ -488,19 +549,20 @@ Component
Item Item
{ {
id: pointedRectangle id: pointedRectangle
width: parent.width -10 width: parent.width - 10 * screenScaleFactor // Because of the shadow
height: parent.height -10 height: parent.height - 10 * screenScaleFactor // Because of the shadow
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Rectangle Rectangle
{ {
id: point id: point
height: 13 height: 14 * screenScaleFactor
width: 13 width: 14 * screenScaleFactor
color: UM.Theme.getColor("setting_control") color: UM.Theme.getColor("setting_control")
transform: Rotation { angle: 45} transform: Rotation { angle: 45}
anchors.right: bloop.right anchors.right: bloop.right
anchors.rightMargin: 24
y: 1 y: 1
} }
@ -510,9 +572,9 @@ Component
color: UM.Theme.getColor("setting_control") color: UM.Theme.getColor("setting_control")
width: parent.width width: parent.width
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 10 anchors.topMargin: 8 * screenScaleFactor // Because of the shadow + point
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 5 anchors.bottomMargin: 8 * screenScaleFactor // Because of the shadow
} }
} }
} }
@ -595,30 +657,14 @@ Component
color: "black" color: "black"
} }
Rectangle CameraButton
{ {
id: showCameraIcon id: showCameraButton
width: 35 * screenScaleFactor iconSource: "../svg/camera-icon.svg"
height: width anchors
radius: 0.5 * width
anchors.left: parent.left
anchors.bottom: printJobPreview.bottom
color: UM.Theme.getColor("setting_control_border_highlight")
Image
{ {
width: parent.width left: parent.left
height: width bottom: printJobPreview.bottom
anchors.right: parent.right
anchors.rightMargin: parent.rightMargin
source: "../svg/camera-icon.svg"
}
MouseArea
{
anchors.fill:parent
onClicked:
{
OutputDevice.setActiveCamera(modelData.camera)
}
} }
} }
} }
@ -650,13 +696,24 @@ Component
style: ProgressBarStyle style: ProgressBarStyle
{ {
property var remainingTime:
{
if(modelData.activePrintJob == null)
{
return 0
}
/* Sometimes total minus elapsed is less than 0. Use Math.max() to prevent remaining
time from ever being less than 0. Negative durations cause strange behavior such
as displaying "-1h -1m". */
var activeJob = modelData.activePrintJob
return Math.max(activeJob.timeTotal - activeJob.timeElapsed, 0);
}
property var progressText: property var progressText:
{ {
if(modelData.activePrintJob == null) if(modelData.activePrintJob == null)
{ {
return "" return ""
} }
switch(modelData.activePrintJob.state) switch(modelData.activePrintJob.state)
{ {
case "wait_cleanup": case "wait_cleanup":
@ -669,18 +726,19 @@ Component
case "sent_to_printer": case "sent_to_printer":
return catalog.i18nc("@label:status", "Preparing") return catalog.i18nc("@label:status", "Preparing")
case "aborted": case "aborted":
return catalog.i18nc("@label:status", "Aborted")
case "wait_user_action": case "wait_user_action":
return catalog.i18nc("@label:status", "Aborted") return catalog.i18nc("@label:status", "Aborted")
case "pausing": case "pausing":
return catalog.i18nc("@label:status", "Pausing") return catalog.i18nc("@label:status", "Pausing")
case "paused": case "paused":
return catalog.i18nc("@label:status", "Paused") return OutputDevice.formatDuration( remainingTime )
case "resuming": case "resuming":
return catalog.i18nc("@label:status", "Resuming") return catalog.i18nc("@label:status", "Resuming")
case "queued": case "queued":
return catalog.i18nc("@label:status", "Action required") return catalog.i18nc("@label:status", "Action required")
default: default:
OutputDevice.formatDuration(modelData.activePrintJob.timeTotal - modelData.activePrintJob.timeElapsed) return OutputDevice.formatDuration( remainingTime )
} }
} }
@ -693,11 +751,28 @@ Component
progress: Rectangle progress: Rectangle
{ {
color: UM.Theme.getColor("primary") color:
{
var state = modelData.activePrintJob.state
var inactiveStates = [
"pausing",
"paused",
"resuming",
"wait_cleanup"
]
if(inactiveStates.indexOf(state) > -1 && remainingTime > 0)
{
return UM.Theme.getColor("monitor_text_inactive")
}
else
{
return UM.Theme.getColor("primary")
}
}
id: progressItem id: progressItem
function getTextOffset() function getTextOffset()
{ {
if(progressItem.width + progressLabel.width < control.width) if(progressItem.width + progressLabel.width + 16 < control.width)
{ {
return progressItem.width + UM.Theme.getSize("default_margin").width return progressItem.width + UM.Theme.getSize("default_margin").width
} }

View File

@ -26,7 +26,7 @@ Component
Label Label
{ {
id: manageQueueLabel id: manageQueueLabel
anchors.rightMargin: 4 * UM.Theme.getSize("default_margin").width anchors.rightMargin: 3 * UM.Theme.getSize("default_margin").width
anchors.right: queuedPrintJobs.right anchors.right: queuedPrintJobs.right
anchors.bottom: queuedLabel.bottom anchors.bottom: queuedLabel.bottom
text: catalog.i18nc("@label link to connect manager", "Manage queue") text: catalog.i18nc("@label link to connect manager", "Manage queue")
@ -50,7 +50,7 @@ Component
anchors.left: queuedPrintJobs.left anchors.left: queuedPrintJobs.left
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height
anchors.leftMargin: 3 * UM.Theme.getSize("default_margin").width anchors.leftMargin: 3 * UM.Theme.getSize("default_margin").width + 5
text: catalog.i18nc("@label", "Queued") text: catalog.i18nc("@label", "Queued")
font: UM.Theme.getFont("large") font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text") color: UM.Theme.getColor("text")

View File

@ -67,7 +67,7 @@ Item
} }
return "" return ""
} }
font: UM.Theme.getFont("default_bold") font: UM.Theme.getFont("default")
elide: Text.ElideRight elide: Text.ElideRight
width: parent.width width: parent.width
} }

View File

@ -11,12 +11,14 @@ Item
{ {
id: base id: base
property var printJob: null property var printJob: null
property var shadowRadius: 5 property var shadowRadius: 5 * screenScaleFactor
function getPrettyTime(time) function getPrettyTime(time)
{ {
return OutputDevice.formatDuration(time) return OutputDevice.formatDuration(time)
} }
width: parent.width
UM.I18nCatalog UM.I18nCatalog
{ {
id: catalog id: catalog
@ -29,7 +31,7 @@ Item
anchors anchors
{ {
top: parent.top top: parent.top
topMargin: 3 topMargin: 3 * screenScaleFactor
left: parent.left left: parent.left
leftMargin: base.shadowRadius leftMargin: base.shadowRadius
rightMargin: base.shadowRadius rightMargin: base.shadowRadius
@ -42,7 +44,7 @@ Item
layer.effect: DropShadow layer.effect: DropShadow
{ {
radius: base.shadowRadius radius: base.shadowRadius
verticalOffset: 2 verticalOffset: 2 * screenScaleFactor
color: "#3F000000" // 25% shadow color: "#3F000000" // 25% shadow
} }
@ -55,7 +57,7 @@ Item
bottom: parent.bottom bottom: parent.bottom
left: parent.left left: parent.left
right: parent.horizontalCenter right: parent.horizontalCenter
margins: 2 * UM.Theme.getSize("default_margin").width margins: UM.Theme.getSize("wide_margin").width
rightMargin: UM.Theme.getSize("default_margin").width rightMargin: UM.Theme.getSize("default_margin").width
} }
@ -106,7 +108,6 @@ Item
Label Label
{ {
id: totalTimeLabel id: totalTimeLabel
opacity: 0.6
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
font: UM.Theme.getFont("default") font: UM.Theme.getFont("default")
@ -126,6 +127,7 @@ Item
right: parent.right right: parent.right
margins: 2 * UM.Theme.getSize("default_margin").width margins: 2 * UM.Theme.getSize("default_margin").width
leftMargin: UM.Theme.getSize("default_margin").width leftMargin: UM.Theme.getSize("default_margin").width
rightMargin: UM.Theme.getSize("default_margin").width / 2
} }
Label Label
@ -168,7 +170,6 @@ Item
{ {
id: contextButton id: contextButton
text: "\u22EE" //Unicode; Three stacked points. text: "\u22EE" //Unicode; Three stacked points.
font.pixelSize: 25
width: 35 width: 35
height: width height: width
anchors anchors
@ -186,6 +187,14 @@ Item
radius: 0.5 * width radius: 0.5 * width
color: UM.Theme.getColor("viewport_background") color: UM.Theme.getColor("viewport_background")
} }
contentItem: Label
{
text: contextButton.text
color: UM.Theme.getColor("monitor_text_inactive")
font.pixelSize: 25
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
onClicked: parent.switchPopupState() onClicked: parent.switchPopupState()
} }
@ -195,18 +204,21 @@ Item
// TODO Change once updating to Qt5.10 - The 'opened' property is in 5.10 but the behavior is now implemented with the visible property // TODO Change once updating to Qt5.10 - The 'opened' property is in 5.10 but the behavior is now implemented with the visible property
id: popup id: popup
clip: true clip: true
closePolicy: Popup.CloseOnPressOutsideParent closePolicy: Popup.CloseOnPressOutside
x: parent.width - width x: (parent.width - width) + 26 * screenScaleFactor
y: contextButton.height y: contextButton.height - 5 * screenScaleFactor // Because shadow
width: 160 width: 182 * screenScaleFactor
height: contentItem.height + 2 * padding height: contentItem.height + 2 * padding
visible: false visible: false
padding: 5 * screenScaleFactor // Because shadow
transformOrigin: Popup.Top transformOrigin: Popup.Top
contentItem: Item contentItem: Item
{ {
width: popup.width - 2 * popup.padding width: popup.width
height: childrenRect.height + 15 height: childrenRect.height + 36 * screenScaleFactor
anchors.topMargin: 10 * screenScaleFactor
anchors.bottomMargin: 10 * screenScaleFactor
Button Button
{ {
id: sendToTopButton id: sendToTopButton
@ -218,14 +230,22 @@ Item
} }
width: parent.width width: parent.width
enabled: OutputDevice.queuedPrintJobs[0].key != printJob.key enabled: OutputDevice.queuedPrintJobs[0].key != printJob.key
visible: enabled
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 10 anchors.topMargin: 18 * screenScaleFactor
height: visible ? 39 * screenScaleFactor : 0 * screenScaleFactor
hoverEnabled: true hoverEnabled: true
background: Rectangle background: Rectangle
{ {
opacity: sendToTopButton.down || sendToTopButton.hovered ? 1 : 0 opacity: sendToTopButton.down || sendToTopButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background") color: UM.Theme.getColor("viewport_background")
} }
contentItem: Label
{
text: sendToTopButton.text
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
}
} }
MessageDialog MessageDialog
@ -249,6 +269,7 @@ Item
popup.close(); popup.close();
} }
width: parent.width width: parent.width
height: 39 * screenScaleFactor
anchors.top: sendToTopButton.bottom anchors.top: sendToTopButton.bottom
hoverEnabled: true hoverEnabled: true
background: Rectangle background: Rectangle
@ -256,6 +277,12 @@ Item
opacity: deleteButton.down || deleteButton.hovered ? 1 : 0 opacity: deleteButton.down || deleteButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background") color: UM.Theme.getColor("viewport_background")
} }
contentItem: Label
{
text: deleteButton.text
horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter
}
} }
MessageDialog MessageDialog
@ -288,19 +315,20 @@ Item
Item Item
{ {
id: pointedRectangle id: pointedRectangle
width: parent.width -10 width: parent.width - 10 * screenScaleFactor // Because of the shadow
height: parent.height -10 height: parent.height - 10 * screenScaleFactor // Because of the shadow
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
Rectangle Rectangle
{ {
id: point id: point
height: 13 height: 14 * screenScaleFactor
width: 13 width: 14 * screenScaleFactor
color: UM.Theme.getColor("setting_control") color: UM.Theme.getColor("setting_control")
transform: Rotation { angle: 45} transform: Rotation { angle: 45}
anchors.right: bloop.right anchors.right: bloop.right
anchors.rightMargin: 24
y: 1 y: 1
} }
@ -310,9 +338,9 @@ Item
color: UM.Theme.getColor("setting_control") color: UM.Theme.getColor("setting_control")
width: parent.width width: parent.width
anchors.top: parent.top anchors.top: parent.top
anchors.topMargin: 10 anchors.topMargin: 8 * screenScaleFactor // Because of the shadow + point
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 5 anchors.bottomMargin: 8 * screenScaleFactor // Because of the shadow
} }
} }
} }
@ -352,7 +380,7 @@ Item
{ {
text: modelData text: modelData
color: UM.Theme.getColor("viewport_background") color: UM.Theme.getColor("viewport_background")
padding: 3 padding: 3 * screenScaleFactor
} }
} }
} }
@ -374,14 +402,14 @@ Item
PrintCoreConfiguration PrintCoreConfiguration
{ {
id: leftExtruderInfo id: leftExtruderInfo
width: Math.round(parent.width / 2) width: Math.round(parent.width / 2) * screenScaleFactor
printCoreConfiguration: printJob.configuration.extruderConfigurations[0] printCoreConfiguration: printJob.configuration.extruderConfigurations[0]
} }
PrintCoreConfiguration PrintCoreConfiguration
{ {
id: rightExtruderInfo id: rightExtruderInfo
width: Math.round(parent.width / 2) width: Math.round(parent.width / 2) * screenScaleFactor
printCoreConfiguration: printJob.configuration.extruderConfigurations[1] printCoreConfiguration: printJob.configuration.extruderConfigurations[1]
} }
} }
@ -391,7 +419,7 @@ Item
Rectangle Rectangle
{ {
color: UM.Theme.getColor("viewport_background") color: UM.Theme.getColor("viewport_background")
width: 2 width: 2 * screenScaleFactor
anchors.top: parent.top anchors.top: parent.top
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.margins: UM.Theme.getSize("default_margin").height anchors.margins: UM.Theme.getSize("default_margin").height

View File

@ -23,36 +23,18 @@ Item
z: 0 z: 0
} }
Button CameraButton
{ {
id: backButton id: closeCameraButton
anchors.bottom: cameraImage.top iconSource: UM.Theme.getIcon("cross1")
anchors.bottomMargin: UM.Theme.getSize("default_margin").width anchors
anchors.right: cameraImage.right
// TODO: Hardcoded sizes
width: 20 * screenScaleFactor
height: 20 * screenScaleFactor
onClicked: OutputDevice.setActiveCamera(null)
style: ButtonStyle
{ {
label: Item top: cameraImage.top
{ topMargin: UM.Theme.getSize("default_margin").height
UM.RecolorImage right: cameraImage.right
{ rightMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: control.width
height: control.height
sourceSize.width: width
sourceSize.height: width
source: UM.Theme.getIcon("cross1")
}
}
background: Item {}
} }
z: 999
} }
Image Image

View File

@ -1,6 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48"> <svg width="48px" height="48px" viewBox="0 0 48 48" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g fill="none" fill-rule="evenodd"> <!-- Generator: Sketch 51.3 (57544) - http://www.bohemiancoding.com/sketch -->
<!-- <rect width="48" height="48" fill="#00A6EC" rx="24"/>--> <desc>Created with Sketch.</desc>
<path stroke="#FFF" stroke-width="2.5" d="M32.75 16.25h-19.5v15.5h19.5v-4.51l3.501 1.397c.181.072.405.113.638.113.333 0 .627-.081.81-.2.036-.024.048-.028.051-.011V18.487c-.26-.23-.976-.332-1.499-.124L32.75 19.76v-3.51z"/> <defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M39.0204082,32.810726 L39.0204082,40 L0,40 L0,8 L39.0204082,8 L39.0204082,13.382823 L42.565076,11.9601033 C44.1116852,11.3414006 46.2176038,11.5575311 47.3294911,12.5468926 L48,13.1435139 L48,32.1994839 C48,32.8444894 47.6431099,33.4236728 46.9293296,33.9370341 C45.8586592,34.707076 45.395355,34.5806452 44.4537143,34.5806452 C43.7935857,34.5806452 43.1386795,34.4629571 42.5629467,34.2325919 L39.0204082,32.810726 Z M35.0204082,12 L4,12 L4,36 L35.0204082,36 L35.0204082,26.8950804 L37.7653798,27.9968275 L44,30.4992132 L44,15.6943364 L35.0204082,19.298468 L35.0204082,12 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
</g> </g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 438 B

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -100,8 +100,7 @@ class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
title=i18n_catalog.i18nc("@info:title", title=i18n_catalog.i18nc("@info:title",
"Authentication status")) "Authentication status"))
self._authentication_failed_message = Message(i18n_catalog.i18nc("@info:status", ""), self._authentication_failed_message = Message("", title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
title=i18n_catalog.i18nc("@info:title", "Authentication Status"))
self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None, self._authentication_failed_message.addAction("Retry", i18n_catalog.i18nc("@action:button", "Retry"), None,
i18n_catalog.i18nc("@info:tooltip", "Re-send the access request")) i18n_catalog.i18nc("@info:tooltip", "Re-send the access request"))
self._authentication_failed_message.actionTriggered.connect(self._messageCallback) self._authentication_failed_message.actionTriggered.connect(self._messageCallback)

View File

@ -0,0 +1,68 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdater, FirmwareUpdateState
from .avr_isp import stk500v2, intelHex
from serial import SerialException
from time import sleep
MYPY = False
if MYPY:
from cura.PrinterOutputDevice import PrinterOutputDevice
class AvrFirmwareUpdater(FirmwareUpdater):
def __init__(self, output_device: "PrinterOutputDevice") -> None:
super().__init__(output_device)
def _updateFirmware(self) -> None:
try:
hex_file = intelHex.readHex(self._firmware_file)
assert len(hex_file) > 0
except (FileNotFoundError, AssertionError):
Logger.log("e", "Unable to read provided hex file. Could not update firmware.")
self._setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error)
return
programmer = stk500v2.Stk500v2()
programmer.progress_callback = self._onFirmwareProgress
# Ensure that other connections are closed.
if self._output_device.isConnected():
self._output_device.close()
try:
programmer.connect(self._output_device._serial_port)
except:
programmer.close()
Logger.logException("e", "Failed to update firmware")
self._setFirmwareUpdateState(FirmwareUpdateState.communication_error)
return
# Give programmer some time to connect. Might need more in some cases, but this worked in all tested cases.
sleep(1)
if not programmer.isConnected():
Logger.log("e", "Unable to connect with serial. Could not update firmware")
self._setFirmwareUpdateState(FirmwareUpdateState.communication_error)
try:
programmer.programChip(hex_file)
except SerialException as e:
Logger.log("e", "A serial port exception occured during firmware update: %s" % e)
self._setFirmwareUpdateState(FirmwareUpdateState.io_error)
return
except Exception as e:
Logger.log("e", "An unknown exception occured during firmware update: %s" % e)
self._setFirmwareUpdateState(FirmwareUpdateState.unknown_error)
return
programmer.close()
# Try to re-connect with the machine again, which must be done on the Qt thread, so we use call later.
CuraApplication.getInstance().callLater(self._output_device.connect)
self._cleanupAfterUpdate()

View File

@ -1,89 +0,0 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Window 2.2
import QtQuick.Controls 1.2
import UM 1.1 as UM
UM.Dialog
{
id: base;
width: minimumWidth;
minimumWidth: 500 * screenScaleFactor;
height: minimumHeight;
minimumHeight: 100 * screenScaleFactor;
visible: true;
modality: Qt.ApplicationModal;
title: catalog.i18nc("@title:window","Firmware Update");
Column
{
anchors.fill: parent;
Label
{
anchors
{
left: parent.left;
right: parent.right;
}
text: {
switch (manager.firmwareUpdateState)
{
case 0:
return "" //Not doing anything (eg; idling)
case 1:
return catalog.i18nc("@label","Updating firmware.")
case 2:
return catalog.i18nc("@label","Firmware update completed.")
case 3:
return catalog.i18nc("@label","Firmware update failed due to an unknown error.")
case 4:
return catalog.i18nc("@label","Firmware update failed due to an communication error.")
case 5:
return catalog.i18nc("@label","Firmware update failed due to an input/output error.")
case 6:
return catalog.i18nc("@label","Firmware update failed due to missing firmware.")
}
}
wrapMode: Text.Wrap;
}
ProgressBar
{
id: prog
value: manager.firmwareProgress
minimumValue: 0
maximumValue: 100
indeterminate: manager.firmwareProgress < 1 && manager.firmwareProgress > 0
anchors
{
left: parent.left;
right: parent.right;
}
}
SystemPalette
{
id: palette;
}
UM.I18nCatalog { id: catalog; name: "cura"; }
}
rightButtons: [
Button
{
text: catalog.i18nc("@action:button","Close");
enabled: manager.firmwareUpdateCompleteStatus;
onClicked: base.visible = false;
}
]
}

View File

@ -4,7 +4,6 @@
from UM.Logger import Logger from UM.Logger import Logger
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Qt.Duration import DurationFormat from UM.Qt.Duration import DurationFormat
from UM.PluginRegistry import PluginRegistry
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
@ -13,28 +12,21 @@ from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.GenericOutputController import GenericOutputController from cura.PrinterOutput.GenericOutputController import GenericOutputController
from .AutoDetectBaudJob import AutoDetectBaudJob from .AutoDetectBaudJob import AutoDetectBaudJob
from .avr_isp import stk500v2, intelHex from .AvrFirmwareUpdater import AvrFirmwareUpdater
from PyQt5.QtCore import pyqtSlot, pyqtSignal, pyqtProperty, QUrl
from serial import Serial, SerialException, SerialTimeoutException from serial import Serial, SerialException, SerialTimeoutException
from threading import Thread, Event from threading import Thread, Event
from time import time, sleep from time import time
from queue import Queue from queue import Queue
from enum import IntEnum
from typing import Union, Optional, List, cast from typing import Union, Optional, List, cast
import re import re
import functools # Used for reduce import functools # Used for reduce
import os
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
class USBPrinterOutputDevice(PrinterOutputDevice): class USBPrinterOutputDevice(PrinterOutputDevice):
firmwareProgressChanged = pyqtSignal()
firmwareUpdateStateChanged = pyqtSignal()
def __init__(self, serial_port: str, baud_rate: Optional[int] = None) -> None: def __init__(self, serial_port: str, baud_rate: Optional[int] = None) -> None:
super().__init__(serial_port) super().__init__(serial_port)
self.setName(catalog.i18nc("@item:inmenu", "USB printing")) self.setName(catalog.i18nc("@item:inmenu", "USB printing"))
@ -59,9 +51,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600] self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600]
# Instead of using a timer, we really need the update to be as a thread, as reading from serial can block. # Instead of using a timer, we really need the update to be as a thread, as reading from serial can block.
self._update_thread = Thread(target=self._update, daemon = True) self._update_thread = Thread(target = self._update, daemon = True)
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon = True)
self._last_temperature_request = None # type: Optional[int] self._last_temperature_request = None # type: Optional[int]
@ -75,11 +65,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._paused = False self._paused = False
self._firmware_view = None
self._firmware_location = None
self._firmware_progress = 0
self._firmware_update_state = FirmwareUpdateState.idle
self.setConnectionText(catalog.i18nc("@info:status", "Connected via USB")) self.setConnectionText(catalog.i18nc("@info:status", "Connected via USB"))
# Queue for commands that need to be sent. # Queue for commands that need to be sent.
@ -88,6 +73,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._command_received = Event() self._command_received = Event()
self._command_received.set() self._command_received.set()
self._firmware_updater = AvrFirmwareUpdater(self)
CuraApplication.getInstance().getOnExitCallbackManager().addCallback(self._checkActivePrintingUponAppExit) CuraApplication.getInstance().getOnExitCallbackManager().addCallback(self._checkActivePrintingUponAppExit)
# This is a callback function that checks if there is any printing in progress via USB when the application tries # This is a callback function that checks if there is any printing in progress via USB when the application tries
@ -109,7 +96,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
## Reset USB device settings ## Reset USB device settings
# #
def resetDeviceSettings(self): def resetDeviceSettings(self) -> None:
self._firmware_name = None self._firmware_name = None
## Request the current scene to be sent to a USB-connected printer. ## Request the current scene to be sent to a USB-connected printer.
@ -135,93 +122,6 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._printGCode(gcode_list) self._printGCode(gcode_list)
## Show firmware interface.
# This will create the view if its not already created.
def showFirmwareInterface(self):
if self._firmware_view is None:
path = os.path.join(PluginRegistry.getInstance().getPluginPath("USBPrinting"), "FirmwareUpdateWindow.qml")
self._firmware_view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
self._firmware_view.show()
@pyqtSlot(str)
def updateFirmware(self, file):
# the file path could be url-encoded.
if file.startswith("file://"):
self._firmware_location = QUrl(file).toLocalFile()
else:
self._firmware_location = file
self.showFirmwareInterface()
self.setFirmwareUpdateState(FirmwareUpdateState.updating)
self._update_firmware_thread.start()
def _updateFirmware(self):
# Ensure that other connections are closed.
if self._connection_state != ConnectionState.closed:
self.close()
try:
hex_file = intelHex.readHex(self._firmware_location)
assert len(hex_file) > 0
except (FileNotFoundError, AssertionError):
Logger.log("e", "Unable to read provided hex file. Could not update firmware.")
self.setFirmwareUpdateState(FirmwareUpdateState.firmware_not_found_error)
return
programmer = stk500v2.Stk500v2()
programmer.progress_callback = self._onFirmwareProgress
try:
programmer.connect(self._serial_port)
except:
programmer.close()
Logger.logException("e", "Failed to update firmware")
self.setFirmwareUpdateState(FirmwareUpdateState.communication_error)
return
# Give programmer some time to connect. Might need more in some cases, but this worked in all tested cases.
sleep(1)
if not programmer.isConnected():
Logger.log("e", "Unable to connect with serial. Could not update firmware")
self.setFirmwareUpdateState(FirmwareUpdateState.communication_error)
try:
programmer.programChip(hex_file)
except SerialException:
self.setFirmwareUpdateState(FirmwareUpdateState.io_error)
return
except:
self.setFirmwareUpdateState(FirmwareUpdateState.unknown_error)
return
programmer.close()
# Clean up for next attempt.
self._update_firmware_thread = Thread(target=self._updateFirmware, daemon=True)
self._firmware_location = ""
self._onFirmwareProgress(100)
self.setFirmwareUpdateState(FirmwareUpdateState.completed)
# Try to re-connect with the machine again, which must be done on the Qt thread, so we use call later.
CuraApplication.getInstance().callLater(self.connect)
@pyqtProperty(float, notify = firmwareProgressChanged)
def firmwareProgress(self):
return self._firmware_progress
@pyqtProperty(int, notify=firmwareUpdateStateChanged)
def firmwareUpdateState(self):
return self._firmware_update_state
def setFirmwareUpdateState(self, state):
if self._firmware_update_state != state:
self._firmware_update_state = state
self.firmwareUpdateStateChanged.emit()
# Callback function for firmware update progress.
def _onFirmwareProgress(self, progress, max_progress = 100):
self._firmware_progress = (progress / max_progress) * 100 # Convert to scale of 0-100
self.firmwareProgressChanged.emit()
## Start a print based on a g-code. ## Start a print based on a g-code.
# \param gcode_list List with gcode (strings). # \param gcode_list List with gcode (strings).
def _printGCode(self, gcode_list: List[str]): def _printGCode(self, gcode_list: List[str]):
@ -258,7 +158,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._baud_rate = baud_rate self._baud_rate = baud_rate
def connect(self): def connect(self):
self._firmware_name = None # after each connection ensure that the firmware name is removed self._firmware_name = None # after each connection ensure that the firmware name is removed
if self._baud_rate is None: if self._baud_rate is None:
if self._use_auto_detect: if self._use_auto_detect:
@ -272,13 +172,19 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
except SerialException: except SerialException:
Logger.log("w", "An exception occured while trying to create serial connection") Logger.log("w", "An exception occured while trying to create serial connection")
return return
CuraApplication.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged)
self._onGlobalContainerStackChanged()
self.setConnectionState(ConnectionState.connected)
self._update_thread.start()
def _onGlobalContainerStackChanged(self):
container_stack = CuraApplication.getInstance().getGlobalContainerStack() container_stack = CuraApplication.getInstance().getGlobalContainerStack()
num_extruders = container_stack.getProperty("machine_extruder_count", "value") num_extruders = container_stack.getProperty("machine_extruder_count", "value")
# Ensure that a printer is created. # Ensure that a printer is created.
self._printers = [PrinterOutputModel(output_controller=GenericOutputController(self), number_of_extruders=num_extruders)] controller = GenericOutputController(self)
controller.setCanUpdateFirmware(True)
self._printers = [PrinterOutputModel(output_controller = controller, number_of_extruders = num_extruders)]
self._printers[0].updateName(container_stack.getName()) self._printers[0].updateName(container_stack.getName())
self.setConnectionState(ConnectionState.connected)
self._update_thread.start()
def close(self): def close(self):
super().close() super().close()
@ -295,6 +201,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._command_queue.put(command) self._command_queue.put(command)
else: else:
self._sendCommand(command) self._sendCommand(command)
def _sendCommand(self, command: Union[str, bytes]): def _sendCommand(self, command: Union[str, bytes]):
if self._serial is None or self._connection_state != ConnectionState.connected: if self._serial is None or self._connection_state != ConnectionState.connected:
return return
@ -326,8 +233,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if self._firmware_name is None: if self._firmware_name is None:
self.sendCommand("M115") self.sendCommand("M115")
if (b"ok " in line and b"T:" in line) or line.startswith(b"T:") or b"ok B:" in line or line.startswith(b"B:"): # Temperature message. 'T:' for extruder and 'B:' for bed if re.search(b"[B|T\d*]: ?\d+\.?\d*", line): # Temperature message. 'T:' for extruder and 'B:' for bed
extruder_temperature_matches = re.findall(b"T(\d*): ?([\d\.]+) ?\/?([\d\.]+)?", line) extruder_temperature_matches = re.findall(b"T(\d*): ?(\d+\.?\d*) ?\/?(\d+\.?\d*)?", line)
# Update all temperature values # Update all temperature values
matched_extruder_nrs = [] matched_extruder_nrs = []
for match in extruder_temperature_matches: for match in extruder_temperature_matches:
@ -349,7 +256,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
if match[2]: if match[2]:
extruder.updateTargetHotendTemperature(float(match[2])) extruder.updateTargetHotendTemperature(float(match[2]))
bed_temperature_matches = re.findall(b"B: ?([\d\.]+) ?\/?([\d\.]+)?", line) bed_temperature_matches = re.findall(b"B: ?(\d+\.?\d*) ?\/?(\d+\.?\d*) ?", line)
if bed_temperature_matches: if bed_temperature_matches:
match = bed_temperature_matches[0] match = bed_temperature_matches[0]
if match[0]: if match[0]:
@ -445,7 +352,9 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
elapsed_time = int(time() - self._print_start_time) elapsed_time = int(time() - self._print_start_time)
print_job = self._printers[0].activePrintJob print_job = self._printers[0].activePrintJob
if print_job is None: if print_job is None:
print_job = PrintJobOutputModel(output_controller = GenericOutputController(self), name= CuraApplication.getInstance().getPrintInformation().jobName) controller = GenericOutputController(self)
controller.setCanUpdateFirmware(True)
print_job = PrintJobOutputModel(output_controller=controller, name=CuraApplication.getInstance().getPrintInformation().jobName)
print_job.updateState("printing") print_job.updateState("printing")
self._printers[0].updateActivePrintJob(print_job) self._printers[0].updateActivePrintJob(print_job)
@ -456,13 +365,3 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
print_job.updateTimeTotal(estimated_time) print_job.updateTimeTotal(estimated_time)
self._gcode_position += 1 self._gcode_position += 1
class FirmwareUpdateState(IntEnum):
idle = 0
updating = 1
completed = 2
unknown_error = 3
communication_error = 4
io_error = 5
firmware_not_found_error = 6

View File

@ -2,14 +2,12 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import threading import threading
import platform
import time import time
import serial.tools.list_ports import serial.tools.list_ports
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
from UM.Logger import Logger from UM.Logger import Logger
from UM.Resources import Resources
from UM.Signal import Signal, signalemitter from UM.Signal import Signal, signalemitter
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
@ -87,65 +85,6 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
self._addRemovePorts(port_list) self._addRemovePorts(port_list)
time.sleep(5) time.sleep(5)
@pyqtSlot(result = str)
def getDefaultFirmwareName(self):
# Check if there is a valid global container stack
global_container_stack = self._application.getGlobalContainerStack()
if not global_container_stack:
Logger.log("e", "There is no global container stack. Can not update firmware.")
self._firmware_view.close()
return ""
# The bottom of the containerstack is the machine definition
machine_id = global_container_stack.getBottom().id
machine_has_heated_bed = global_container_stack.getProperty("machine_heated_bed", "value")
if platform.system() == "Linux":
baudrate = 115200
else:
baudrate = 250000
# NOTE: The keyword used here is the id of the machine. You can find the id of your machine in the *.json file, eg.
# https://github.com/Ultimaker/Cura/blob/master/resources/machines/ultimaker_original.json#L2
# The *.hex files are stored at a seperate repository:
# https://github.com/Ultimaker/cura-binary-data/tree/master/cura/resources/firmware
machine_without_extras = {"bq_witbox" : "MarlinWitbox.hex",
"bq_hephestos_2" : "MarlinHephestos2.hex",
"ultimaker_original" : "MarlinUltimaker-{baudrate}.hex",
"ultimaker_original_plus" : "MarlinUltimaker-UMOP-{baudrate}.hex",
"ultimaker_original_dual" : "MarlinUltimaker-{baudrate}-dual.hex",
"ultimaker2" : "MarlinUltimaker2.hex",
"ultimaker2_go" : "MarlinUltimaker2go.hex",
"ultimaker2_plus" : "MarlinUltimaker2plus.hex",
"ultimaker2_extended" : "MarlinUltimaker2extended.hex",
"ultimaker2_extended_plus" : "MarlinUltimaker2extended-plus.hex",
}
machine_with_heated_bed = {"ultimaker_original" : "MarlinUltimaker-HBK-{baudrate}.hex",
"ultimaker_original_dual" : "MarlinUltimaker-HBK-{baudrate}-dual.hex",
}
##TODO: Add check for multiple extruders
hex_file = None
if machine_id in machine_without_extras.keys(): # The machine needs to be defined here!
if machine_id in machine_with_heated_bed.keys() and machine_has_heated_bed:
Logger.log("d", "Choosing firmware with heated bed enabled for machine %s.", machine_id)
hex_file = machine_with_heated_bed[machine_id] # Return firmware with heated bed enabled
else:
Logger.log("d", "Choosing basic firmware for machine %s.", machine_id)
hex_file = machine_without_extras[machine_id] # Return "basic" firmware
else:
Logger.log("w", "There is no firmware for machine %s.", machine_id)
if hex_file:
try:
return Resources.getPath(CuraApplication.ResourceTypes.Firmware, hex_file.format(baudrate=baudrate))
except FileNotFoundError:
Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
return ""
else:
Logger.log("w", "Could not find any firmware for machine %s.", machine_id)
return ""
## Helper to identify serial ports (and scan for them) ## Helper to identify serial ports (and scan for them)
def _addRemovePorts(self, serial_ports): def _addRemovePorts(self, serial_ports):
# First, find and add all new or changed keys # First, find and add all new or changed keys

View File

@ -2,9 +2,6 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from . import USBPrinterOutputDeviceManager from . import USBPrinterOutputDeviceManager
from PyQt5.QtQml import qmlRegisterSingletonType
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
@ -14,5 +11,4 @@ def getMetaData():
def register(app): def register(app):
# We are violating the QT API here (as we use a factory, which is technically not allowed). # We are violating the QT API here (as we use a factory, which is technically not allowed).
# but we don't really have another means for doing this (and it seems to you know -work-) # but we don't really have another means for doing this (and it seems to you know -work-)
qmlRegisterSingletonType(USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager, "Cura", 1, 0, "USBPrinterManager", USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager.getInstance)
return {"output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager(app)} return {"output_device": USBPrinterOutputDeviceManager.USBPrinterOutputDeviceManager(app)}

View File

@ -17,7 +17,7 @@ Cura.MachineAction
property int rightRow: (checkupMachineAction.width * 0.60) | 0 property int rightRow: (checkupMachineAction.width * 0.60) | 0
property bool heatupHotendStarted: false property bool heatupHotendStarted: false
property bool heatupBedStarted: false property bool heatupBedStarted: false
property bool usbConnected: Cura.USBPrinterManager.connectedPrinterList.rowCount() > 0 property bool printerConnected: Cura.MachineManager.printerConnected
UM.I18nCatalog { id: catalog; name:"cura"} UM.I18nCatalog { id: catalog; name:"cura"}
Label Label
@ -86,7 +86,7 @@ Cura.MachineAction
anchors.left: connectionLabel.right anchors.left: connectionLabel.right
anchors.top: parent.top anchors.top: parent.top
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: checkupMachineAction.usbConnected ? catalog.i18nc("@info:status","Connected"): catalog.i18nc("@info:status","Not connected") text: checkupMachineAction.printerConnected ? catalog.i18nc("@info:status","Connected"): catalog.i18nc("@info:status","Not connected")
} }
////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////
Label Label
@ -97,7 +97,7 @@ Cura.MachineAction
anchors.top: connectionLabel.bottom anchors.top: connectionLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop X: ") text: catalog.i18nc("@label","Min endstop X: ")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
Label Label
{ {
@ -107,7 +107,7 @@ Cura.MachineAction
anchors.top: connectionLabel.bottom anchors.top: connectionLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: manager.xMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked") text: manager.xMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////
Label Label
@ -118,7 +118,7 @@ Cura.MachineAction
anchors.top: endstopXLabel.bottom anchors.top: endstopXLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop Y: ") text: catalog.i18nc("@label","Min endstop Y: ")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
Label Label
{ {
@ -128,7 +128,7 @@ Cura.MachineAction
anchors.top: endstopXLabel.bottom anchors.top: endstopXLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: manager.yMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked") text: manager.yMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////
Label Label
@ -139,7 +139,7 @@ Cura.MachineAction
anchors.top: endstopYLabel.bottom anchors.top: endstopYLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Min endstop Z: ") text: catalog.i18nc("@label","Min endstop Z: ")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
Label Label
{ {
@ -149,7 +149,7 @@ Cura.MachineAction
anchors.top: endstopYLabel.bottom anchors.top: endstopYLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: manager.zMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked") text: manager.zMinEndstopTestCompleted ? catalog.i18nc("@info:status","Works") : catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
//////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////
Label Label
@ -161,7 +161,7 @@ Cura.MachineAction
anchors.top: endstopZLabel.bottom anchors.top: endstopZLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Nozzle temperature check: ") text: catalog.i18nc("@label","Nozzle temperature check: ")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
Label Label
{ {
@ -171,7 +171,7 @@ Cura.MachineAction
anchors.left: nozzleTempLabel.right anchors.left: nozzleTempLabel.right
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@info:status","Not checked") text: catalog.i18nc("@info:status","Not checked")
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
Item Item
{ {
@ -181,7 +181,7 @@ Cura.MachineAction
anchors.top: nozzleTempLabel.top anchors.top: nozzleTempLabel.top
anchors.left: bedTempStatus.right anchors.left: bedTempStatus.right
anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").width/2) anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").width/2)
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
Button Button
{ {
text: checkupMachineAction.heatupHotendStarted ? catalog.i18nc("@action:button","Stop Heating") : catalog.i18nc("@action:button","Start Heating") text: checkupMachineAction.heatupHotendStarted ? catalog.i18nc("@action:button","Stop Heating") : catalog.i18nc("@action:button","Start Heating")
@ -209,7 +209,7 @@ Cura.MachineAction
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: manager.hotendTemperature + "°C" text: manager.hotendTemperature + "°C"
font.bold: true font.bold: true
visible: checkupMachineAction.usbConnected visible: checkupMachineAction.printerConnected
} }
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
Label Label
@ -221,7 +221,7 @@ Cura.MachineAction
anchors.top: nozzleTempLabel.bottom anchors.top: nozzleTempLabel.bottom
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: catalog.i18nc("@label","Build plate temperature check:") text: catalog.i18nc("@label","Build plate temperature check:")
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed visible: checkupMachineAction.printerConnected && manager.hasHeatedBed
} }
Label Label
@ -232,7 +232,7 @@ Cura.MachineAction
anchors.left: bedTempLabel.right anchors.left: bedTempLabel.right
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: manager.bedTestCompleted ? catalog.i18nc("@info:status","Not checked"): catalog.i18nc("@info:status","Checked") text: manager.bedTestCompleted ? catalog.i18nc("@info:status","Not checked"): catalog.i18nc("@info:status","Checked")
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed visible: checkupMachineAction.printerConnected && manager.hasHeatedBed
} }
Item Item
{ {
@ -242,7 +242,7 @@ Cura.MachineAction
anchors.top: bedTempLabel.top anchors.top: bedTempLabel.top
anchors.left: bedTempStatus.right anchors.left: bedTempStatus.right
anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").width/2) anchors.leftMargin: Math.round(UM.Theme.getSize("default_margin").width/2)
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed visible: checkupMachineAction.printerConnected && manager.hasHeatedBed
Button Button
{ {
text: checkupMachineAction.heatupBedStarted ?catalog.i18nc("@action:button","Stop Heating") : catalog.i18nc("@action:button","Start Heating") text: checkupMachineAction.heatupBedStarted ?catalog.i18nc("@action:button","Stop Heating") : catalog.i18nc("@action:button","Start Heating")
@ -270,7 +270,7 @@ Cura.MachineAction
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
text: manager.bedTemperature + "°C" text: manager.bedTemperature + "°C"
font.bold: true font.bold: true
visible: checkupMachineAction.usbConnected && manager.hasHeatedBed visible: checkupMachineAction.printerConnected && manager.hasHeatedBed
} }
Label Label
{ {

View File

@ -1,19 +0,0 @@
from UM.Application import Application
from UM.Settings.DefinitionContainer import DefinitionContainer
from cura.MachineAction import MachineAction
from UM.i18n import i18nCatalog
from UM.Settings.ContainerRegistry import ContainerRegistry
catalog = i18nCatalog("cura")
## Upgrade the firmware of a machine by USB with this action.
class UpgradeFirmwareMachineAction(MachineAction):
def __init__(self):
super().__init__("UpgradeFirmware", catalog.i18nc("@action", "Upgrade Firmware"))
self._qml_url = "UpgradeFirmwareMachineAction.qml"
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
def _onContainerAdded(self, container):
# Add this action as a supported action to all machine definitions if they support USB connection
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine" and container.getMetaDataEntry("supports_usb_connection"):
Application.getInstance().getMachineActionManager().addSupportedAction(container.getId(), self.getKey())

View File

@ -1,93 +0,0 @@
// Copyright (c) 2016 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Layouts 1.1
import QtQuick.Window 2.1
import QtQuick.Dialogs 1.2 // For filedialog
import UM 1.2 as UM
import Cura 1.0 as Cura
Cura.MachineAction
{
anchors.fill: parent;
property bool printerConnected: Cura.MachineManager.printerConnected
property var activeOutputDevice: printerConnected ? Cura.MachineManager.printerOutputDevices[0] : null
Item
{
id: upgradeFirmwareMachineAction
anchors.fill: parent;
UM.I18nCatalog { id: catalog; name:"cura"}
Label
{
id: pageTitle
width: parent.width
text: catalog.i18nc("@title", "Upgrade Firmware")
wrapMode: Text.WordWrap
font.pointSize: 18
}
Label
{
id: pageDescription
anchors.top: pageTitle.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "Firmware is the piece of software running directly on your 3D printer. This firmware controls the step motors, regulates the temperature and ultimately makes your printer work.")
}
Label
{
id: upgradeText1
anchors.top: pageDescription.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width
wrapMode: Text.WordWrap
text: catalog.i18nc("@label", "The firmware shipping with new printers works, but new versions tend to have more features and improvements.");
}
Row
{
anchors.top: upgradeText1.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter
width: childrenRect.width
spacing: UM.Theme.getSize("default_margin").width
property var firmwareName: Cura.USBPrinterManager.getDefaultFirmwareName()
Button
{
id: autoUpgradeButton
text: catalog.i18nc("@action:button", "Automatically upgrade Firmware");
enabled: parent.firmwareName != "" && activeOutputDevice
onClicked:
{
activeOutputDevice.updateFirmware(parent.firmwareName)
}
}
Button
{
id: manualUpgradeButton
text: catalog.i18nc("@action:button", "Upload custom Firmware");
enabled: activeOutputDevice != null
onClicked:
{
customFirmwareDialog.open()
}
}
}
FileDialog
{
id: customFirmwareDialog
title: catalog.i18nc("@title:window", "Select custom firmware")
nameFilters: "Firmware image files (*.hex)"
selectExisting: true
onAccepted: activeOutputDevice.updateFirmware(fileUrl)
}
}
}

View File

@ -1,22 +1,16 @@
# Copyright (c) 2016 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 . import BedLevelMachineAction from . import BedLevelMachineAction
from . import UpgradeFirmwareMachineAction
from . import UMOUpgradeSelection from . import UMOUpgradeSelection
from . import UM2UpgradeSelection from . import UM2UpgradeSelection
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
def getMetaData(): def getMetaData():
return { return {}
}
def register(app): def register(app):
return { "machine_action": [ return { "machine_action": [
BedLevelMachineAction.BedLevelMachineAction(), BedLevelMachineAction.BedLevelMachineAction(),
UpgradeFirmwareMachineAction.UpgradeFirmwareMachineAction(),
UMOUpgradeSelection.UMOUpgradeSelection(), UMOUpgradeSelection.UMOUpgradeSelection(),
UM2UpgradeSelection.UM2UpgradeSelection() UM2UpgradeSelection.UM2UpgradeSelection()
]} ]}

View File

@ -3,9 +3,6 @@
from . import VersionUpgrade21to22 from . import VersionUpgrade21to22
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
upgrade = VersionUpgrade21to22.VersionUpgrade21to22() upgrade = VersionUpgrade21to22.VersionUpgrade21to22()
def getMetaData(): def getMetaData():

View File

@ -73,7 +73,7 @@ class VersionUpgrade22to24(VersionUpgrade):
def __convertVariant(self, variant_path): def __convertVariant(self, variant_path):
# Copy the variant to the machine_instances/*_settings.inst.cfg # Copy the variant to the machine_instances/*_settings.inst.cfg
variant_config = configparser.ConfigParser(interpolation=None) variant_config = configparser.ConfigParser(interpolation = None)
with open(variant_path, "r", encoding = "utf-8") as fhandle: with open(variant_path, "r", encoding = "utf-8") as fhandle:
variant_config.read_file(fhandle) variant_config.read_file(fhandle)

View File

@ -3,9 +3,6 @@
from . import VersionUpgrade from . import VersionUpgrade
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
upgrade = VersionUpgrade.VersionUpgrade22to24() upgrade = VersionUpgrade.VersionUpgrade22to24()
def getMetaData(): def getMetaData():

View File

@ -117,7 +117,7 @@ class VersionUpgrade25to26(VersionUpgrade):
# \param serialised The serialised form of a quality profile. # \param serialised The serialised form of a quality profile.
# \param filename The name of the file to upgrade. # \param filename The name of the file to upgrade.
def upgradeMachineStack(self, serialised, filename): def upgradeMachineStack(self, serialised, filename):
parser = configparser.ConfigParser(interpolation=None) parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised) parser.read_string(serialised)
# NOTE: This is for Custom FDM printers # NOTE: This is for Custom FDM printers

View File

@ -3,9 +3,6 @@
from . import VersionUpgrade25to26 from . import VersionUpgrade25to26
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
upgrade = VersionUpgrade25to26.VersionUpgrade25to26() upgrade = VersionUpgrade25to26.VersionUpgrade25to26()
def getMetaData(): def getMetaData():

Some files were not shown because too many files have changed in this diff Show More