mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-24 15:13:56 -06:00
Merge remote-tracking branch 'upstream/master' into feature_minimum_support_area
This commit is contained in:
commit
d8d92eb4b0
364 changed files with 42426 additions and 75595 deletions
|
@ -17,6 +17,7 @@ if(CURA_DEBUGMODE)
|
|||
set(_cura_debugmode "ON")
|
||||
endif()
|
||||
|
||||
set(CURA_APP_DISPLAY_NAME "Ultimaker Cura" CACHE STRING "Display name of Cura")
|
||||
set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
|
||||
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
|
||||
set(CURA_SDK_VERSION "" CACHE STRING "SDK version of Cura")
|
||||
|
|
88
Jenkinsfile
vendored
88
Jenkinsfile
vendored
|
@ -1,8 +1,11 @@
|
|||
parallel_nodes(['linux && cura', 'windows && cura']) {
|
||||
timeout(time: 2, unit: "HOURS") {
|
||||
parallel_nodes(['linux && cura', 'windows && cura'])
|
||||
{
|
||||
timeout(time: 2, unit: "HOURS")
|
||||
{
|
||||
|
||||
// Prepare building
|
||||
stage('Prepare') {
|
||||
stage('Prepare')
|
||||
{
|
||||
// Ensure we start with a clean build directory.
|
||||
step([$class: 'WsCleanup'])
|
||||
|
||||
|
@ -11,37 +14,17 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
|
|||
}
|
||||
|
||||
// If any error occurs during building, we want to catch it and continue with the "finale" stage.
|
||||
catchError {
|
||||
stage('Pre Checks') {
|
||||
if (isUnix()) {
|
||||
// Check shortcut keys
|
||||
try {
|
||||
sh """
|
||||
echo 'Check for duplicate shortcut keys in all translation files.'
|
||||
${env.CURA_ENVIRONMENT_PATH}/master/bin/python3 scripts/check_shortcut_keys.py
|
||||
"""
|
||||
} catch(e) {
|
||||
currentBuild.result = "UNSTABLE"
|
||||
}
|
||||
|
||||
// Check setting visibilities
|
||||
try {
|
||||
sh """
|
||||
echo 'Check for duplicate shortcut keys in all translation files.'
|
||||
${env.CURA_ENVIRONMENT_PATH}/master/bin/python3 scripts/check_setting_visibility.py
|
||||
"""
|
||||
} catch(e) {
|
||||
currentBuild.result = "UNSTABLE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
catchError
|
||||
{
|
||||
// Building and testing should happen in a subdirectory.
|
||||
dir('build') {
|
||||
dir('build')
|
||||
{
|
||||
// Perform the "build". Since Uranium is Python code, this basically only ensures CMake is setup.
|
||||
stage('Build') {
|
||||
stage('Build')
|
||||
{
|
||||
def branch = env.BRANCH_NAME
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
|
||||
{
|
||||
branch = "master"
|
||||
}
|
||||
|
||||
|
@ -51,11 +34,14 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
|
|||
}
|
||||
|
||||
// Try and run the unit tests. If this stage fails, we consider the build to be "unstable".
|
||||
stage('Unit Test') {
|
||||
if (isUnix()) {
|
||||
stage('Unit Test')
|
||||
{
|
||||
if (isUnix())
|
||||
{
|
||||
// For Linux to show everything
|
||||
def branch = env.BRANCH_NAME
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
|
||||
{
|
||||
branch = "master"
|
||||
}
|
||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
|
||||
|
@ -66,37 +52,48 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
|
|||
export PYTHONPATH=.:"${uranium_dir}"
|
||||
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/pytest -x --verbose --full-trace --capture=no ./tests
|
||||
"""
|
||||
} catch(e) {
|
||||
} catch(e)
|
||||
{
|
||||
currentBuild.result = "UNSTABLE"
|
||||
}
|
||||
}
|
||||
else {
|
||||
else
|
||||
{
|
||||
// For Windows
|
||||
try {
|
||||
try
|
||||
{
|
||||
// This also does code style checks.
|
||||
bat 'ctest -V'
|
||||
} catch(e) {
|
||||
} catch(e)
|
||||
{
|
||||
currentBuild.result = "UNSTABLE"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Code Style') {
|
||||
if (isUnix()) {
|
||||
// For Linux to show everything
|
||||
stage('Code Style')
|
||||
{
|
||||
if (isUnix())
|
||||
{
|
||||
// For Linux to show everything.
|
||||
// CMake also runs this test, but if it fails then the test just shows "failed" without details of what exactly failed.
|
||||
def branch = env.BRANCH_NAME
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}")) {
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
|
||||
{
|
||||
branch = "master"
|
||||
}
|
||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
|
||||
|
||||
try {
|
||||
try
|
||||
{
|
||||
sh """
|
||||
cd ..
|
||||
export PYTHONPATH=.:"${uranium_dir}"
|
||||
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/python3 run_mypy.py
|
||||
"""
|
||||
} catch(e) {
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
currentBuild.result = "UNSTABLE"
|
||||
}
|
||||
}
|
||||
|
@ -105,7 +102,8 @@ parallel_nodes(['linux && cura', 'windows && cura']) {
|
|||
}
|
||||
|
||||
// Perform any post-build actions like notification and publishing of unit tests.
|
||||
stage('Finalize') {
|
||||
stage('Finalize')
|
||||
{
|
||||
// Publish the test results to Jenkins.
|
||||
junit allowEmptyResults: true, testResults: 'build/junit*.xml'
|
||||
|
||||
|
|
|
@ -57,5 +57,13 @@ endforeach()
|
|||
#Add code style test.
|
||||
add_test(
|
||||
NAME "code-style"
|
||||
COMMAND ${PYTHON_EXECUTABLE} run_mypy.py WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
COMMAND ${PYTHON_EXECUTABLE} run_mypy.py
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
|
||||
#Add test for whether the shortcut alt-keys are unique in every translation.
|
||||
add_test(
|
||||
NAME "shortcut-keys"
|
||||
COMMAND ${PYTHON_EXECUTABLE} scripts/check_shortcut_keys.py
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
117
cura/API/Account.py
Normal file
117
cura/API/Account.py
Normal 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="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.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()
|
|
@ -1,9 +1,12 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
## The back-ups API provides a version-proof bridge between Cura's
|
||||
# BackupManager and plug-ins that hook into it.
|
||||
|
@ -13,9 +16,10 @@ from cura.Backups.BackupsManager import BackupsManager
|
|||
# api = CuraAPI()
|
||||
# api.backups.createBackup()
|
||||
# api.backups.restoreBackup(my_zip_file, {"cura_release": "3.1"})``
|
||||
|
||||
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.
|
||||
# \return Tuple containing a ZIP file with the back-up data and a dict
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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
|
||||
# (currently) sidebar UI and plug-ins that hook into it.
|
||||
|
@ -19,8 +23,9 @@ from cura.CuraApplication import CuraApplication
|
|||
# api.interface.settings.addContextMenuItem(data)``
|
||||
|
||||
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.
|
||||
# \param menu_item dict containing the menu item to add.
|
||||
|
@ -30,4 +35,4 @@ class Settings:
|
|||
## Get all custom items currently added to the sidebar context menu.
|
||||
# \return List containing all custom context menu items.
|
||||
def getContextMenuItems(self) -> list:
|
||||
return self.application.getSidebarCustomMenuItems()
|
||||
return self.application.getSidebarCustomMenuItems()
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
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
|
||||
# methods for each interface element.
|
||||
#
|
||||
|
@ -20,5 +26,6 @@ class Interface:
|
|||
# For now we use the same API version to be consistent.
|
||||
VERSION = PluginRegistry.APIVersion
|
||||
|
||||
# API methods specific to the settings portion of the UI
|
||||
settings = Settings()
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
# API methods specific to the settings portion of the UI
|
||||
self.settings = Settings(application)
|
||||
|
|
|
@ -1,8 +1,17 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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 cura.API.Backups import Backups
|
||||
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.
|
||||
#
|
||||
|
@ -10,14 +19,47 @@ from cura.API.Interface import Interface
|
|||
# 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
|
||||
# plug-ins to be unstable.
|
||||
|
||||
class CuraAPI:
|
||||
class CuraAPI(QObject):
|
||||
|
||||
# For now we use the same API version to be consistent.
|
||||
VERSION = PluginRegistry.APIVersion
|
||||
__instance = None # type: "CuraAPI"
|
||||
_application = None # type: CuraApplication
|
||||
|
||||
# Backups API
|
||||
backups = Backups()
|
||||
# This is done to ensure that the first time an instance is created, it's forced that the application is set.
|
||||
# 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
|
||||
interface = Interface()
|
||||
def __init__(self, application: Optional["CuraApplication"] = None) -> None:
|
||||
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
|
|
@ -4,18 +4,18 @@
|
|||
import io
|
||||
import os
|
||||
import re
|
||||
|
||||
import shutil
|
||||
|
||||
from typing import Dict, Optional
|
||||
from zipfile import ZipFile, ZIP_DEFLATED, BadZipfile
|
||||
from typing import Dict, Optional, TYPE_CHECKING
|
||||
|
||||
from UM import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Platform import Platform
|
||||
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.
|
||||
|
@ -29,24 +29,25 @@ class Backup:
|
|||
# Re-use translation catalog.
|
||||
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.meta_data = meta_data # type: Optional[Dict[str, str]]
|
||||
|
||||
## Create a back-up from the current user config folder.
|
||||
def makeFromCurrent(self) -> None:
|
||||
cura_release = CuraApplication.getInstance().getVersion()
|
||||
cura_release = self._application.getVersion()
|
||||
version_data_dir = Resources.getDataStoragePath()
|
||||
|
||||
Logger.log("d", "Creating backup for Cura %s, using folder %s", cura_release, version_data_dir)
|
||||
|
||||
# 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.
|
||||
# When restoring a backup on Linux, we move it back.
|
||||
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))
|
||||
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)
|
||||
|
@ -58,7 +59,7 @@ class Backup:
|
|||
if archive is None:
|
||||
return
|
||||
files = archive.namelist()
|
||||
|
||||
|
||||
# Count the metadata items. We do this in a rather naive way at the moment.
|
||||
machine_count = len([s for s in files if "machine_instances/" in s]) - 1
|
||||
material_count = len([s for s in files if "materials/" in s]) - 1
|
||||
|
@ -112,7 +113,7 @@ class Backup:
|
|||
"Tried to restore a Cura backup without having proper data or meta data."))
|
||||
return False
|
||||
|
||||
current_version = CuraApplication.getInstance().getVersion()
|
||||
current_version = self._application.getVersion()
|
||||
version_to_restore = self.meta_data.get("cura_release", "master")
|
||||
if current_version != version_to_restore:
|
||||
# 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.
|
||||
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))
|
||||
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)
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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 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
|
||||
|
@ -13,15 +15,15 @@ from cura.CuraApplication import CuraApplication
|
|||
#
|
||||
# Back-ups themselves are represented in a different class.
|
||||
class BackupsManager:
|
||||
def __init__(self):
|
||||
self._application = CuraApplication.getInstance()
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
self._application = application
|
||||
|
||||
## Get a back-up of the current configuration.
|
||||
# \return A tuple containing a ZipFile (the actual back-up) and a dict
|
||||
# containing some metadata (like version).
|
||||
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, str]]]:
|
||||
self._disableAutoSave()
|
||||
backup = Backup()
|
||||
backup = Backup(self._application)
|
||||
backup.makeFromCurrent()
|
||||
self._enableAutoSave()
|
||||
# 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()
|
||||
|
||||
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()
|
||||
if restored:
|
||||
# At this point, Cura will need to restart for the changes to take effect.
|
||||
|
|
|
@ -718,21 +718,23 @@ class BuildVolume(SceneNode):
|
|||
|
||||
# Add prime tower location as disallowed area.
|
||||
if len(used_extruders) > 1: #No prime tower in single-extrusion.
|
||||
prime_tower_collision = False
|
||||
prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
|
||||
for extruder_id in prime_tower_areas:
|
||||
for prime_tower_area in prime_tower_areas[extruder_id]:
|
||||
for area in result_areas[extruder_id]:
|
||||
if prime_tower_area.intersectsPolygon(area) is not None:
|
||||
prime_tower_collision = True
|
||||
|
||||
if len([x for x in used_extruders if x.isEnabled == True]) > 1: #No prime tower if only one extruder is enabled
|
||||
prime_tower_collision = False
|
||||
prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
|
||||
for extruder_id in prime_tower_areas:
|
||||
for prime_tower_area in prime_tower_areas[extruder_id]:
|
||||
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
|
||||
if prime_tower_collision: #Already found a collision.
|
||||
break
|
||||
if not prime_tower_collision:
|
||||
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||
else:
|
||||
self._error_areas.extend(prime_tower_areas[extruder_id])
|
||||
if not prime_tower_collision:
|
||||
result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
|
||||
else:
|
||||
self._error_areas.extend(prime_tower_areas[extruder_id])
|
||||
|
||||
self._has_errors = len(self._error_areas) > 0
|
||||
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
from PyQt5.QtGui import QImage
|
||||
from PyQt5.QtQuick import QQuickImageProvider
|
||||
from PyQt5.QtCore import QSize
|
||||
|
||||
from UM.Application import Application
|
||||
|
||||
|
||||
class CameraImageProvider(QQuickImageProvider):
|
||||
def __init__(self):
|
||||
super().__init__(QQuickImageProvider.Image)
|
||||
|
||||
## Request a new image.
|
||||
def requestImage(self, id, size):
|
||||
for output_device in Application.getInstance().getOutputDeviceManager().getOutputDevices():
|
||||
try:
|
||||
image = output_device.activePrinter.camera.getImage()
|
||||
if image.isNull():
|
||||
image = QImage()
|
||||
|
||||
return image, QSize(15, 15)
|
||||
except AttributeError:
|
||||
try:
|
||||
image = output_device.activeCamera.getImage()
|
||||
|
||||
return image, QSize(15, 15)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
return QImage(), QSize(15, 15)
|
|
@ -4,7 +4,7 @@
|
|||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import cast, TYPE_CHECKING
|
||||
from typing import cast, TYPE_CHECKING, Optional, Callable
|
||||
|
||||
import numpy
|
||||
|
||||
|
@ -13,6 +13,7 @@ from PyQt5.QtGui import QColor, QIcon
|
|||
from PyQt5.QtWidgets import QMessageBox
|
||||
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.PluginError import PluginNotFoundError
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Camera import Camera
|
||||
|
@ -44,6 +45,7 @@ from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
|||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Operations.SetTransformOperation import SetTransformOperation
|
||||
|
||||
from cura.API import CuraAPI
|
||||
from cura.Arranging.Arrange import Arrange
|
||||
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
||||
|
@ -61,6 +63,7 @@ from cura.Scene.CuraSceneController import CuraSceneController
|
|||
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
from cura.Settings.MachineNameValidator import MachineNameValidator
|
||||
|
||||
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
||||
|
@ -93,7 +96,6 @@ from . import PrintInformation
|
|||
from . import CuraActions
|
||||
from cura.Scene import ZOffsetDecorator
|
||||
from . import CuraSplashScreen
|
||||
from . import CameraImageProvider
|
||||
from . import PrintJobPreviewImageProvider
|
||||
from . import MachineActionManager
|
||||
|
||||
|
@ -107,23 +109,28 @@ from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisi
|
|||
from cura.Settings.ContainerManager import ContainerManager
|
||||
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
|
||||
import cura.Settings.cura_empty_instance_containers
|
||||
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
|
||||
|
||||
from cura.ObjectsModel import ObjectsModel
|
||||
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
|
||||
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Decorators import override
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Machines.MaterialManager import MaterialManager
|
||||
from cura.Machines.QualityManager import QualityManager
|
||||
from UM.Settings.EmptyInstanceContainer import EmptyInstanceContainer
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
|
||||
numpy.seterr(all = "ignore")
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion
|
||||
from cura.CuraVersion import CuraAppDisplayName, CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion # type: ignore
|
||||
except ImportError:
|
||||
CuraAppDisplayName = "Ultimaker Cura"
|
||||
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
|
||||
CuraBuildType = ""
|
||||
CuraDebugMode = False
|
||||
|
@ -155,6 +162,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(name = "cura",
|
||||
app_display_name = CuraAppDisplayName,
|
||||
version = CuraVersion,
|
||||
buildtype = CuraBuildType,
|
||||
is_debug_mode = CuraDebugMode,
|
||||
|
@ -163,6 +171,8 @@ class CuraApplication(QtApplication):
|
|||
|
||||
self.default_theme = "cura-light"
|
||||
|
||||
self.change_log_url = "https://ultimaker.com/ultimaker-cura-latest-features"
|
||||
|
||||
self._boot_loading_time = time.time()
|
||||
|
||||
self._on_exit_callback_manager = OnExitCallbackManager(self)
|
||||
|
@ -174,6 +184,8 @@ class CuraApplication(QtApplication):
|
|||
|
||||
self._single_instance = None
|
||||
|
||||
self._cura_formula_functions = None # type: Optional[CuraFormulaFunctions]
|
||||
|
||||
self._cura_package_manager = None
|
||||
|
||||
self._machine_action_manager = None
|
||||
|
@ -203,6 +215,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
self._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._volume = None
|
||||
|
@ -241,6 +254,8 @@ class CuraApplication(QtApplication):
|
|||
|
||||
from cura.Settings.CuraContainerRegistry import 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
|
||||
self._package_manager_class = CuraPackageManager
|
||||
|
||||
|
@ -265,6 +280,9 @@ class CuraApplication(QtApplication):
|
|||
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.")
|
||||
|
||||
def getContainerRegistry(self) -> "CuraContainerRegistry":
|
||||
return self._container_registry
|
||||
|
||||
def parseCliOptions(self):
|
||||
super().parseCliOptions()
|
||||
|
||||
|
@ -291,8 +309,6 @@ class CuraApplication(QtApplication):
|
|||
self._machine_action_manager = MachineActionManager.MachineActionManager(self)
|
||||
self._machine_action_manager.initialize()
|
||||
|
||||
self.change_log_url = "https://ultimaker.com/ultimaker-cura-latest-features"
|
||||
|
||||
def __sendCommandToSingleInstance(self):
|
||||
self._single_instance = SingleInstance(self, self._files_to_open)
|
||||
|
||||
|
@ -317,6 +333,8 @@ class CuraApplication(QtApplication):
|
|||
# Adds custom property types, settings types, and extra operators (functions) that need to be registered in
|
||||
# SettingDefinition and SettingFunction.
|
||||
def __initializeSettingDefinitionsAndFunctions(self):
|
||||
self._cura_formula_functions = CuraFormulaFunctions(self)
|
||||
|
||||
# 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_extruder", DefinitionPropertyType.Any, default = True, read_only = True)
|
||||
|
@ -337,10 +355,10 @@ class CuraApplication(QtApplication):
|
|||
SettingDefinition.addSettingType("optional_extruder", None, str, None)
|
||||
SettingDefinition.addSettingType("[int]", None, str, None)
|
||||
|
||||
SettingFunction.registerOperator("extruderValues", ExtruderManager.getExtruderValues)
|
||||
SettingFunction.registerOperator("extruderValue", ExtruderManager.getExtruderValue)
|
||||
SettingFunction.registerOperator("resolveOrValue", ExtruderManager.getResolveOrValue)
|
||||
SettingFunction.registerOperator("defaultExtruderPosition", ExtruderManager.getDefaultExtruderPosition)
|
||||
SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder)
|
||||
SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
|
||||
SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
|
||||
SettingFunction.registerOperator("defaultExtruderPosition", self._cura_formula_functions.getDefaultExtruderPosition)
|
||||
|
||||
# Adds all resources and container related resources.
|
||||
def __addAllResourcesAndContainerResources(self) -> None:
|
||||
|
@ -406,41 +424,37 @@ class CuraApplication(QtApplication):
|
|||
)
|
||||
|
||||
# Runs preparations that needs to be done before the starting process.
|
||||
def startSplashWindowPhase(self):
|
||||
def startSplashWindowPhase(self) -> None:
|
||||
super().startSplashWindowPhase()
|
||||
|
||||
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
|
||||
|
||||
self.setRequiredPlugins([
|
||||
# Misc.:
|
||||
"ConsoleLogger",
|
||||
"CuraEngineBackend",
|
||||
"UserAgreement",
|
||||
"FileLogger",
|
||||
"XmlMaterialProfile",
|
||||
"Toolbox",
|
||||
"PrepareStage",
|
||||
"MonitorStage",
|
||||
"LocalFileOutputDevice",
|
||||
"LocalContainerProvider",
|
||||
"ConsoleLogger", #You want to be able to read the log if something goes wrong.
|
||||
"CuraEngineBackend", #Cura is useless without this one since you can't slice.
|
||||
"UserAgreement", #Our lawyers want every user to see this at least once.
|
||||
"FileLogger", #You want to be able to read the log if something goes wrong.
|
||||
"XmlMaterialProfile", #Cura crashes without this one.
|
||||
"Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
|
||||
"PrepareStage", #Cura is useless without this one since you can't load models.
|
||||
"MonitorStage", #Major part of Cura's functionality.
|
||||
"LocalFileOutputDevice", #Major part of Cura's functionality.
|
||||
"LocalContainerProvider", #Cura is useless without any profiles or setting definitions.
|
||||
|
||||
# Views:
|
||||
"SimpleView",
|
||||
"SimulationView",
|
||||
"SolidView",
|
||||
"SimpleView", #Dependency of SolidView.
|
||||
"SolidView", #Displays models. Cura is useless without it.
|
||||
|
||||
# Readers & Writers:
|
||||
"GCodeWriter",
|
||||
"STLReader",
|
||||
"3MFWriter",
|
||||
"GCodeWriter", #Cura is useless if it can't write its output.
|
||||
"STLReader", #Most common model format, so disabling this makes Cura 90% useless.
|
||||
"3MFWriter", #Required for writing project files.
|
||||
|
||||
# Tools:
|
||||
"CameraTool",
|
||||
"MirrorTool",
|
||||
"RotateTool",
|
||||
"ScaleTool",
|
||||
"SelectionTool",
|
||||
"TranslateTool",
|
||||
"CameraTool", #Needed to see the scene. Cura is useless without it.
|
||||
"SelectionTool", #Dependency of the rest of the tools.
|
||||
"TranslateTool", #You'll need this for almost every print.
|
||||
])
|
||||
self._i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -508,19 +522,18 @@ class CuraApplication(QtApplication):
|
|||
CuraApplication.Created = True
|
||||
|
||||
def _onEngineCreated(self):
|
||||
self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
|
||||
self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider())
|
||||
|
||||
@pyqtProperty(bool)
|
||||
def needToShowUserAgreement(self):
|
||||
def needToShowUserAgreement(self) -> bool:
|
||||
return self._need_to_show_user_agreement
|
||||
|
||||
def setNeedToShowUserAgreement(self, set_value = True):
|
||||
def setNeedToShowUserAgreement(self, set_value = True) -> None:
|
||||
self._need_to_show_user_agreement = set_value
|
||||
|
||||
# DO NOT call this function to close the application, use checkAndExitApplication() instead which will perform
|
||||
# pre-exit checks such as checking for in-progress USB printing, etc.
|
||||
def closeApplication(self):
|
||||
def closeApplication(self) -> None:
|
||||
Logger.log("i", "Close application")
|
||||
main_window = self.getMainWindow()
|
||||
if main_window is not None:
|
||||
|
@ -547,11 +560,11 @@ class CuraApplication(QtApplication):
|
|||
|
||||
showConfirmExitDialog = pyqtSignal(str, arguments = ["message"])
|
||||
|
||||
def setConfirmExitDialogCallback(self, callback):
|
||||
def setConfirmExitDialogCallback(self, callback: Callable) -> None:
|
||||
self._confirm_exit_dialog_callback = callback
|
||||
|
||||
@pyqtSlot(bool)
|
||||
def callConfirmExitDialogCallback(self, yes_or_no: bool):
|
||||
def callConfirmExitDialogCallback(self, yes_or_no: bool) -> None:
|
||||
self._confirm_exit_dialog_callback(yes_or_no)
|
||||
|
||||
## Signal to connect preferences action in QML
|
||||
|
@ -559,9 +572,17 @@ class CuraApplication(QtApplication):
|
|||
|
||||
## Show the preferences window
|
||||
@pyqtSlot()
|
||||
def showPreferences(self):
|
||||
def showPreferences(self) -> None:
|
||||
self.showPreferencesWindow.emit()
|
||||
|
||||
@override(Application)
|
||||
def getGlobalContainerStack(self) -> Optional["GlobalStack"]:
|
||||
return self._global_container_stack
|
||||
|
||||
@override(Application)
|
||||
def setGlobalContainerStack(self, stack: "GlobalStack") -> None:
|
||||
super().setGlobalContainerStack(stack)
|
||||
|
||||
## A reusable dialogbox
|
||||
#
|
||||
showMessageBox = pyqtSignal(str, str, str, str, int, int, arguments = ["title", "text", "informativeText", "detailedText", "buttons", "icon"])
|
||||
|
@ -573,7 +594,7 @@ class CuraApplication(QtApplication):
|
|||
|
||||
showDiscardOrKeepProfileChanges = pyqtSignal()
|
||||
|
||||
def discardOrKeepProfileChanges(self):
|
||||
def discardOrKeepProfileChanges(self) -> bool:
|
||||
has_user_interaction = False
|
||||
choice = self.getPreferences().getValue("cura/choice_on_profile_override")
|
||||
if choice == "always_discard":
|
||||
|
@ -589,7 +610,7 @@ class CuraApplication(QtApplication):
|
|||
return has_user_interaction
|
||||
|
||||
@pyqtSlot(str)
|
||||
def discardOrKeepProfileChangesClosed(self, option):
|
||||
def discardOrKeepProfileChangesClosed(self, option: str) -> None:
|
||||
global_stack = self.getGlobalContainerStack()
|
||||
if option == "discard":
|
||||
for extruder in global_stack.extruders.values():
|
||||
|
@ -674,11 +695,11 @@ class CuraApplication(QtApplication):
|
|||
|
||||
Logger.log("i", "Initializing quality manager")
|
||||
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()
|
||||
|
||||
Logger.log("i", "Initializing machine manager")
|
||||
self._machine_manager = MachineManager(self)
|
||||
self._machine_manager = MachineManager(self, parent = self)
|
||||
|
||||
Logger.log("i", "Initializing container manager")
|
||||
self._container_manager = ContainerManager(self)
|
||||
|
@ -701,10 +722,11 @@ class CuraApplication(QtApplication):
|
|||
self._print_information = PrintInformation.PrintInformation(self)
|
||||
self._cura_actions = CuraActions.CuraActions(self)
|
||||
|
||||
# Initialize setting visibility presets model
|
||||
self._setting_visibility_presets_model = SettingVisibilityPresetsModel(self)
|
||||
default_visibility_profile = self._setting_visibility_presets_model.getItem(0)
|
||||
self.getPreferences().setDefault("general/visible_settings", ";".join(default_visibility_profile["settings"]))
|
||||
# Initialize setting visibility presets model.
|
||||
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
|
||||
if self._is_headless:
|
||||
|
@ -804,6 +826,11 @@ class CuraApplication(QtApplication):
|
|||
def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
|
||||
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:
|
||||
return self._machine_error_checker
|
||||
|
||||
|
@ -893,6 +920,9 @@ class CuraApplication(QtApplication):
|
|||
self._custom_quality_profile_drop_down_menu_model = CustomQualityProfilesDropDownMenuModel(self)
|
||||
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.
|
||||
#
|
||||
# \param engine The QML engine.
|
||||
|
@ -915,6 +945,8 @@ class CuraApplication(QtApplication):
|
|||
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
|
||||
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
|
||||
|
||||
qmlRegisterType(NetworkMJPGImage, "Cura", 1, 0, "NetworkMJPGImage")
|
||||
|
||||
qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 0, "ObjectsModel", self.getObjectsModel)
|
||||
qmlRegisterType(BuildPlateModel, "Cura", 1, 0, "BuildPlateModel")
|
||||
qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
|
||||
|
@ -941,6 +973,9 @@ class CuraApplication(QtApplication):
|
|||
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
|
||||
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.
|
||||
actions_url = QUrl.fromLocalFile(os.path.abspath(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles, "Actions.qml")))
|
||||
qmlRegisterSingletonType(actions_url, "Cura", 1, 0, "Actions")
|
||||
|
@ -1580,6 +1615,11 @@ class CuraApplication(QtApplication):
|
|||
job.start()
|
||||
|
||||
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()
|
||||
file_name = job.getFileName()
|
||||
file_name_lower = file_name.lower()
|
||||
|
@ -1594,7 +1634,6 @@ class CuraApplication(QtApplication):
|
|||
for node_ in DepthFirstIterator(root):
|
||||
if node_.callDecoration("isSliceable") and node_.callDecoration("getBuildPlateNumber") == target_build_plate:
|
||||
fixed_nodes.append(node_)
|
||||
global_container_stack = self.getGlobalContainerStack()
|
||||
machine_width = global_container_stack.getProperty("machine_width", "value")
|
||||
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
||||
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = fixed_nodes)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
CuraAppDisplayName = "@CURA_APP_DISPLAY_NAME@"
|
||||
CuraVersion = "@CURA_VERSION@"
|
||||
CuraBuildType = "@CURA_BUILDTYPE@"
|
||||
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.PluginObject import PluginObject
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Application import Application
|
||||
|
||||
import os
|
||||
|
||||
|
||||
## Machine actions are actions that are added to a specific machine type. Examples of such actions are
|
||||
|
@ -19,7 +19,7 @@ class MachineAction(QObject, PluginObject):
|
|||
## Create a new Machine action.
|
||||
# \param key unique key of the machine action
|
||||
# \param label Human readable label used to identify the machine action.
|
||||
def __init__(self, key, label = ""):
|
||||
def __init__(self, key: str, label: str = "") -> None:
|
||||
super().__init__()
|
||||
self._key = key
|
||||
self._label = label
|
||||
|
@ -30,14 +30,14 @@ class MachineAction(QObject, PluginObject):
|
|||
labelChanged = pyqtSignal()
|
||||
onFinished = pyqtSignal()
|
||||
|
||||
def getKey(self):
|
||||
def getKey(self) -> str:
|
||||
return self._key
|
||||
|
||||
@pyqtProperty(str, notify = labelChanged)
|
||||
def label(self):
|
||||
def label(self) -> str:
|
||||
return self._label
|
||||
|
||||
def setLabel(self, label):
|
||||
def setLabel(self, label: str) -> None:
|
||||
if self._label != label:
|
||||
self._label = label
|
||||
self.labelChanged.emit()
|
||||
|
@ -46,29 +46,35 @@ class MachineAction(QObject, PluginObject):
|
|||
# This should not be re-implemented by child classes, instead re-implement _reset.
|
||||
# /sa _reset
|
||||
@pyqtSlot()
|
||||
def reset(self):
|
||||
def reset(self) -> None:
|
||||
self._finished = False
|
||||
self._reset()
|
||||
|
||||
## Protected implementation of reset.
|
||||
# /sa reset()
|
||||
def _reset(self):
|
||||
def _reset(self) -> None:
|
||||
pass
|
||||
|
||||
@pyqtSlot()
|
||||
def setFinished(self):
|
||||
def setFinished(self) -> None:
|
||||
self._finished = True
|
||||
self._reset()
|
||||
self.onFinished.emit()
|
||||
|
||||
@pyqtProperty(bool, notify = onFinished)
|
||||
def finished(self):
|
||||
def finished(self) -> bool:
|
||||
return self._finished
|
||||
|
||||
## Protected helper to create a view object based on provided QML.
|
||||
def _createViewFromQML(self):
|
||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), self._qml_url)
|
||||
self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||
def _createViewFromQML(self) -> None:
|
||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||
if plugin_path is None:
|
||||
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
|
||||
return
|
||||
path = os.path.join(plugin_path, self._qml_url)
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
self._view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
|
||||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
def displayItem(self):
|
||||
|
|
|
@ -1,12 +1,18 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, List, Set, Dict
|
||||
|
||||
from PyQt5.QtCore import QObject
|
||||
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Logger import Logger
|
||||
from UM.PluginRegistry import PluginRegistry # So MachineAction can be added as plugin type
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from .MachineAction import MachineAction
|
||||
|
||||
|
||||
## Raised when trying to add an unknown machine action as a required action
|
||||
|
@ -20,46 +26,54 @@ class NotUniqueMachineActionError(Exception):
|
|||
|
||||
|
||||
class MachineActionManager(QObject):
|
||||
def __init__(self, application, parent = None):
|
||||
super().__init__(parent)
|
||||
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent = parent)
|
||||
self._application = application
|
||||
self._container_registry = self._application.getContainerRegistry()
|
||||
|
||||
self._machine_actions = {} # Dict of all known machine actions
|
||||
self._required_actions = {} # Dict of all required actions by definition ID
|
||||
self._supported_actions = {} # Dict of all supported actions by definition ID
|
||||
self._first_start_actions = {} # Dict of all actions that need to be done when first added by definition ID
|
||||
# Keeps track of which machines have already been processed so we don't do that again.
|
||||
self._definition_ids_with_default_actions_added = set() # type: Set[str]
|
||||
|
||||
# Dict of all known machine actions
|
||||
self._machine_actions = {} # type: Dict[str, MachineAction]
|
||||
# Dict of all required actions by definition ID
|
||||
self._required_actions = {} # type: Dict[str, List[MachineAction]]
|
||||
# Dict of all supported actions by definition ID
|
||||
self._supported_actions = {} # type: Dict[str, List[MachineAction]]
|
||||
# Dict of all actions that need to be done when first added by definition ID
|
||||
self._first_start_actions = {} # type: Dict[str, List[MachineAction]]
|
||||
|
||||
def initialize(self):
|
||||
container_registry = self._application.getContainerRegistry()
|
||||
|
||||
# Add machine_action as plugin type
|
||||
PluginRegistry.addType("machine_action", self.addMachineAction)
|
||||
|
||||
# Ensure that all containers that were registered before creation of this registry are also handled.
|
||||
# This should not have any effect, but it makes it safer if we ever refactor the order of things.
|
||||
for container in container_registry.findDefinitionContainers():
|
||||
self._onContainerAdded(container)
|
||||
# Adds all default machine actions that are defined in the machine definition for the given machine.
|
||||
def addDefaultMachineActions(self, global_stack: "GlobalStack") -> None:
|
||||
definition_id = global_stack.definition.getId()
|
||||
|
||||
container_registry.containerAdded.connect(self._onContainerAdded)
|
||||
if definition_id in self._definition_ids_with_default_actions_added:
|
||||
Logger.log("i", "Default machine actions have been added for machine definition [%s], do nothing.",
|
||||
definition_id)
|
||||
return
|
||||
|
||||
def _onContainerAdded(self, container):
|
||||
## Ensure that the actions are added to this manager
|
||||
if isinstance(container, DefinitionContainer):
|
||||
supported_actions = container.getMetaDataEntry("supported_actions", [])
|
||||
for action in supported_actions:
|
||||
self.addSupportedAction(container.getId(), action)
|
||||
supported_actions = global_stack.getMetaDataEntry("supported_actions", [])
|
||||
for action_key in supported_actions:
|
||||
self.addSupportedAction(definition_id, action_key)
|
||||
|
||||
required_actions = container.getMetaDataEntry("required_actions", [])
|
||||
for action in required_actions:
|
||||
self.addRequiredAction(container.getId(), action)
|
||||
required_actions = global_stack.getMetaDataEntry("required_actions", [])
|
||||
for action_key in required_actions:
|
||||
self.addRequiredAction(definition_id, action_key)
|
||||
|
||||
first_start_actions = container.getMetaDataEntry("first_start_actions", [])
|
||||
for action in first_start_actions:
|
||||
self.addFirstStartAction(container.getId(), action)
|
||||
first_start_actions = global_stack.getMetaDataEntry("first_start_actions", [])
|
||||
for action_key in first_start_actions:
|
||||
self.addFirstStartAction(definition_id, action_key)
|
||||
|
||||
self._definition_ids_with_default_actions_added.add(definition_id)
|
||||
Logger.log("i", "Default machine actions added for machine definition [%s]", definition_id)
|
||||
|
||||
## Add a required action to a machine
|
||||
# Raises an exception when the action is not recognised.
|
||||
def addRequiredAction(self, definition_id, action_key):
|
||||
def addRequiredAction(self, definition_id: str, action_key: str) -> None:
|
||||
if action_key in self._machine_actions:
|
||||
if definition_id in self._required_actions:
|
||||
if self._machine_actions[action_key] not in self._required_actions[definition_id]:
|
||||
|
@ -70,7 +84,7 @@ class MachineActionManager(QObject):
|
|||
raise UnknownMachineActionError("Action %s, which is required for %s is not known." % (action_key, definition_id))
|
||||
|
||||
## Add a supported action to a machine.
|
||||
def addSupportedAction(self, definition_id, action_key):
|
||||
def addSupportedAction(self, definition_id: str, action_key: str) -> None:
|
||||
if action_key in self._machine_actions:
|
||||
if definition_id in self._supported_actions:
|
||||
if self._machine_actions[action_key] not in self._supported_actions[definition_id]:
|
||||
|
@ -81,13 +95,10 @@ class MachineActionManager(QObject):
|
|||
Logger.log("w", "Unable to add %s to %s, as the action is not recognised", action_key, definition_id)
|
||||
|
||||
## Add an action to the first start list of a machine.
|
||||
def addFirstStartAction(self, definition_id, action_key, index = None):
|
||||
def addFirstStartAction(self, definition_id: str, action_key: str) -> None:
|
||||
if action_key in self._machine_actions:
|
||||
if definition_id in self._first_start_actions:
|
||||
if index is not None:
|
||||
self._first_start_actions[definition_id].insert(index, self._machine_actions[action_key])
|
||||
else:
|
||||
self._first_start_actions[definition_id].append(self._machine_actions[action_key])
|
||||
self._first_start_actions[definition_id].append(self._machine_actions[action_key])
|
||||
else:
|
||||
self._first_start_actions[definition_id] = [self._machine_actions[action_key]]
|
||||
else:
|
||||
|
@ -95,7 +106,7 @@ class MachineActionManager(QObject):
|
|||
|
||||
## Add a (unique) MachineAction
|
||||
# if the Key of the action is not unique, an exception is raised.
|
||||
def addMachineAction(self, action):
|
||||
def addMachineAction(self, action: "MachineAction") -> None:
|
||||
if action.getKey() not in self._machine_actions:
|
||||
self._machine_actions[action.getKey()] = action
|
||||
else:
|
||||
|
@ -105,7 +116,7 @@ class MachineActionManager(QObject):
|
|||
# \param definition_id The ID of the definition you want the supported actions of
|
||||
# \returns set of supported actions.
|
||||
@pyqtSlot(str, result = "QVariantList")
|
||||
def getSupportedActions(self, definition_id):
|
||||
def getSupportedActions(self, definition_id: str) -> List["MachineAction"]:
|
||||
if definition_id in self._supported_actions:
|
||||
return list(self._supported_actions[definition_id])
|
||||
else:
|
||||
|
@ -114,11 +125,11 @@ class MachineActionManager(QObject):
|
|||
## Get all actions required by given machine
|
||||
# \param definition_id The ID of the definition you want the required actions of
|
||||
# \returns set of required actions.
|
||||
def getRequiredActions(self, definition_id):
|
||||
def getRequiredActions(self, definition_id: str) -> List["MachineAction"]:
|
||||
if definition_id in self._required_actions:
|
||||
return self._required_actions[definition_id]
|
||||
else:
|
||||
return set()
|
||||
return list()
|
||||
|
||||
## Get all actions that need to be performed upon first start of a given machine.
|
||||
# Note that contrary to required / supported actions a list is returned (as it could be required to run the same
|
||||
|
@ -126,7 +137,7 @@ class MachineActionManager(QObject):
|
|||
# \param definition_id The ID of the definition that you want to get the "on added" actions for.
|
||||
# \returns List of actions.
|
||||
@pyqtSlot(str, result="QVariantList")
|
||||
def getFirstStartActions(self, definition_id):
|
||||
def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]:
|
||||
if definition_id in self._first_start_actions:
|
||||
return self._first_start_actions[definition_id]
|
||||
else:
|
||||
|
@ -134,7 +145,7 @@ class MachineActionManager(QObject):
|
|||
|
||||
## Remove Machine action from manager
|
||||
# \param action to remove
|
||||
def removeMachineAction(self, action):
|
||||
def removeMachineAction(self, action: "MachineAction") -> None:
|
||||
try:
|
||||
del self._machine_actions[action.getKey()]
|
||||
except KeyError:
|
||||
|
@ -143,7 +154,7 @@ class MachineActionManager(QObject):
|
|||
## Get MachineAction by key
|
||||
# \param key String of key to select
|
||||
# \return Machine action if found, None otherwise
|
||||
def getMachineAction(self, key):
|
||||
def getMachineAction(self, key: str) -> Optional["MachineAction"]:
|
||||
if key in self._machine_actions:
|
||||
return self._machine_actions[key]
|
||||
else:
|
||||
|
|
|
@ -21,6 +21,7 @@ from .VariantType import VariantType
|
|||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
|
||||
|
@ -298,7 +299,7 @@ class MaterialManager(QObject):
|
|||
def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
|
||||
return self._diameter_material_map.get(root_material_id, "")
|
||||
|
||||
def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]:
|
||||
def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]:
|
||||
return self._guid_material_groups_map.get(guid)
|
||||
|
||||
#
|
||||
|
@ -365,7 +366,7 @@ class MaterialManager(QObject):
|
|||
nozzle_name = None
|
||||
if extruder_stack.variant.getId() != "empty_variant":
|
||||
nozzle_name = extruder_stack.variant.getName()
|
||||
diameter = extruder_stack.approximateMaterialDiameter
|
||||
diameter = extruder_stack.getApproximateMaterialDiameter()
|
||||
|
||||
# Fetch the available materials (ContainerNode) for the current active machine and extruder setup.
|
||||
return self.getAvailableMaterials(machine.definition, nozzle_name, buildplate_name, diameter)
|
||||
|
@ -446,6 +447,28 @@ class MaterialManager(QObject):
|
|||
material_diameter, root_material_id)
|
||||
return node
|
||||
|
||||
# There are 2 ways to get fallback materials;
|
||||
# - A fallback by type (@sa getFallbackMaterialIdByMaterialType), which adds the generic version of this material
|
||||
# - A fallback by GUID; If a material has been duplicated, it should also check if the original materials do have
|
||||
# a GUID. This should only be done if the material itself does not have a quality just yet.
|
||||
def getFallBackMaterialIdsByMaterial(self, material: "InstanceContainer") -> List[str]:
|
||||
results = [] # type: List[str]
|
||||
|
||||
material_groups = self.getMaterialGroupListByGUID(material.getMetaDataEntry("GUID"))
|
||||
for material_group in material_groups: # type: ignore
|
||||
if material_group.name != material.getId():
|
||||
# If the material in the group is read only, put it at the front of the list (since that is the most
|
||||
# likely one to get a result)
|
||||
if material_group.is_read_only:
|
||||
results.insert(0, material_group.name)
|
||||
else:
|
||||
results.append(material_group.name)
|
||||
|
||||
fallback = self.getFallbackMaterialIdByMaterialType(material.getMetaDataEntry("material"))
|
||||
if fallback is not None:
|
||||
results.append(fallback)
|
||||
return results
|
||||
|
||||
#
|
||||
# Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla".
|
||||
# For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
|
||||
|
@ -478,12 +501,22 @@ class MaterialManager(QObject):
|
|||
|
||||
buildplate_name = global_stack.getBuildplateName()
|
||||
machine_definition = global_stack.definition
|
||||
if extruder_definition is None:
|
||||
extruder_definition = global_stack.extruders[position].definition
|
||||
|
||||
if extruder_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
|
||||
# At this point the extruder_definition is not None
|
||||
material_diameter = extruder_definition.getProperty("material_diameter", "value")
|
||||
# The extruder-compatible material diameter in the extruder definition may not be the correct value because
|
||||
# the user can change it in the definition_changes container.
|
||||
if extruder_definition is None:
|
||||
extruder_stack_or_definition = global_stack.extruders[position]
|
||||
is_extruder_stack = True
|
||||
else:
|
||||
extruder_stack_or_definition = extruder_definition
|
||||
is_extruder_stack = False
|
||||
|
||||
if extruder_stack_or_definition and parseBool(global_stack.getMetaDataEntry("has_materials", False)):
|
||||
if is_extruder_stack:
|
||||
material_diameter = extruder_stack_or_definition.getCompatibleMaterialDiameter()
|
||||
else:
|
||||
material_diameter = extruder_stack_or_definition.getProperty("material_diameter", "value")
|
||||
|
||||
if isinstance(material_diameter, SettingFunction):
|
||||
material_diameter = material_diameter(global_stack)
|
||||
approximate_material_diameter = str(round(material_diameter))
|
||||
|
@ -592,7 +625,6 @@ class MaterialManager(QObject):
|
|||
container_to_add.setDirty(True)
|
||||
self._container_registry.addContainer(container_to_add)
|
||||
|
||||
|
||||
# if the duplicated material was favorite then the new material should also be added to favorite.
|
||||
if root_material_id in self.getFavorites():
|
||||
self.addFavorite(new_base_id)
|
||||
|
@ -612,8 +644,11 @@ class MaterialManager(QObject):
|
|||
machine_manager = self._application.getMachineManager()
|
||||
extruder_stack = machine_manager.activeStack
|
||||
|
||||
machine_definition = self._application.getGlobalContainerStack().definition
|
||||
preferred_material = machine_definition.getMetaDataEntry("preferred_material")
|
||||
|
||||
approximate_diameter = str(extruder_stack.approximateMaterialDiameter)
|
||||
root_material_id = "generic_pla"
|
||||
root_material_id = preferred_material if preferred_material else "generic_pla"
|
||||
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
|
||||
material_group = self.getMaterialGroup(root_material_id)
|
||||
|
||||
|
|
|
@ -64,9 +64,11 @@ class BaseMaterialsModel(ListModel):
|
|||
|
||||
if self._extruder_stack is not None:
|
||||
self._extruder_stack.pyqtContainersChanged.disconnect(self._update)
|
||||
self._extruder_stack.approximateMaterialDiameterChanged.disconnect(self._update)
|
||||
self._extruder_stack = global_stack.extruders.get(str(self._extruder_position))
|
||||
if self._extruder_stack is not None:
|
||||
self._extruder_stack.pyqtContainersChanged.connect(self._update)
|
||||
self._extruder_stack.approximateMaterialDiameterChanged.connect(self._update)
|
||||
# Force update the model when the extruder stack changes
|
||||
self._update()
|
||||
|
||||
|
|
|
@ -1,135 +1,109 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional
|
||||
import os
|
||||
import urllib.parse
|
||||
from configparser import ConfigParser
|
||||
from typing import Optional, List
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, Qt, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Resources import Resources
|
||||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura.Settings.SettingVisibilityPreset import SettingVisibilityPreset
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class SettingVisibilityPresetsModel(ListModel):
|
||||
IdRole = Qt.UserRole + 1
|
||||
NameRole = Qt.UserRole + 2
|
||||
SettingsRole = Qt.UserRole + 3
|
||||
class SettingVisibilityPresetsModel(QObject):
|
||||
onItemsChanged = pyqtSignal()
|
||||
activePresetChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, parent = None):
|
||||
def __init__(self, preferences, parent = None):
|
||||
super().__init__(parent)
|
||||
self.addRoleName(self.IdRole, "id")
|
||||
self.addRoleName(self.NameRole, "name")
|
||||
self.addRoleName(self.SettingsRole, "settings")
|
||||
|
||||
self._items = [] # type: List[SettingVisibilityPreset]
|
||||
self._populate()
|
||||
basic_item = self.items[1]
|
||||
basic_visibile_settings = ";".join(basic_item["settings"])
|
||||
|
||||
self._preferences = Application.getInstance().getPreferences()
|
||||
basic_item = self.getVisibilityPresetById("basic")
|
||||
basic_visibile_settings = ";".join(basic_item.settings)
|
||||
|
||||
self._preferences = preferences
|
||||
|
||||
# Preference to store which preset is currently selected
|
||||
self._preferences.addPreference("cura/active_setting_visibility_preset", "basic")
|
||||
|
||||
# Preference that stores the "custom" set so it can always be restored (even after a restart)
|
||||
self._preferences.addPreference("cura/custom_visible_settings", basic_visibile_settings)
|
||||
self._preferences.preferenceChanged.connect(self._onPreferencesChanged)
|
||||
|
||||
self._active_preset_item = self._getItem(self._preferences.getValue("cura/active_setting_visibility_preset"))
|
||||
self._active_preset_item = self.getVisibilityPresetById(self._preferences.getValue("cura/active_setting_visibility_preset"))
|
||||
|
||||
# Initialize visible settings if it is not done yet
|
||||
visible_settings = self._preferences.getValue("general/visible_settings")
|
||||
|
||||
if not visible_settings:
|
||||
self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item["settings"]))
|
||||
self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item.settings))
|
||||
else:
|
||||
self._onPreferencesChanged("general/visible_settings")
|
||||
|
||||
self.activePresetChanged.emit()
|
||||
|
||||
def _getItem(self, item_id: str) -> Optional[dict]:
|
||||
def getVisibilityPresetById(self, item_id: str) -> Optional[SettingVisibilityPreset]:
|
||||
result = None
|
||||
for item in self.items:
|
||||
if item["id"] == item_id:
|
||||
for item in self._items:
|
||||
if item.presetId == item_id:
|
||||
result = item
|
||||
break
|
||||
return result
|
||||
|
||||
def _populate(self) -> None:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
items = []
|
||||
items = [] # type: List[SettingVisibilityPreset]
|
||||
|
||||
custom_preset = SettingVisibilityPreset(preset_id="custom", name ="Custom selection", weight = -100)
|
||||
items.append(custom_preset)
|
||||
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
||||
setting_visibility_preset = SettingVisibilityPreset()
|
||||
try:
|
||||
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
|
||||
except MimeTypeNotFoundError:
|
||||
Logger.log("e", "Could not determine mime type of file %s", file_path)
|
||||
continue
|
||||
|
||||
item_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_path)))
|
||||
if not os.path.isfile(file_path):
|
||||
Logger.log("e", "[%s] is not a file", file_path)
|
||||
continue
|
||||
|
||||
parser = ConfigParser(allow_no_value = True) # accept options without any value,
|
||||
try:
|
||||
parser.read([file_path])
|
||||
if not parser.has_option("general", "name") or not parser.has_option("general", "weight"):
|
||||
continue
|
||||
|
||||
settings = []
|
||||
for section in parser.sections():
|
||||
if section == 'general':
|
||||
continue
|
||||
|
||||
settings.append(section)
|
||||
for option in parser[section].keys():
|
||||
settings.append(option)
|
||||
|
||||
items.append({
|
||||
"id": item_id,
|
||||
"name": catalog.i18nc("@action:inmenu", parser["general"]["name"]),
|
||||
"weight": parser["general"]["weight"],
|
||||
"settings": settings,
|
||||
})
|
||||
|
||||
setting_visibility_preset.loadFromFile(file_path)
|
||||
except Exception:
|
||||
Logger.logException("e", "Failed to load setting preset %s", file_path)
|
||||
|
||||
items.sort(key = lambda k: (int(k["weight"]), k["id"]))
|
||||
# Put "custom" at the top
|
||||
items.insert(0, {"id": "custom",
|
||||
"name": "Custom selection",
|
||||
"weight": -100,
|
||||
"settings": []})
|
||||
items.append(setting_visibility_preset)
|
||||
|
||||
# Sort them on weight (and if that fails, use ID)
|
||||
items.sort(key = lambda k: (int(k.weight), k.presetId))
|
||||
|
||||
self.setItems(items)
|
||||
|
||||
@pyqtProperty("QVariantList", notify = onItemsChanged)
|
||||
def items(self):
|
||||
return self._items
|
||||
|
||||
def setItems(self, items: List[SettingVisibilityPreset]) -> None:
|
||||
if self._items != items:
|
||||
self._items = items
|
||||
self.onItemsChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setActivePreset(self, preset_id: str):
|
||||
if preset_id == self._active_preset_item["id"]:
|
||||
def setActivePreset(self, preset_id: str) -> None:
|
||||
if preset_id == self._active_preset_item.presetId:
|
||||
Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id)
|
||||
return
|
||||
|
||||
preset_item = None
|
||||
for item in self.items:
|
||||
if item["id"] == preset_id:
|
||||
preset_item = item
|
||||
break
|
||||
preset_item = self.getVisibilityPresetById(preset_id)
|
||||
if preset_item is None:
|
||||
Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
|
||||
return
|
||||
|
||||
need_to_save_to_custom = self._active_preset_item["id"] == "custom" and preset_id != "custom"
|
||||
need_to_save_to_custom = self._active_preset_item.presetId == "custom" and preset_id != "custom"
|
||||
if need_to_save_to_custom:
|
||||
# Save the current visibility settings to custom
|
||||
current_visibility_string = self._preferences.getValue("general/visible_settings")
|
||||
if current_visibility_string:
|
||||
self._preferences.setValue("cura/custom_visible_settings", current_visibility_string)
|
||||
|
||||
new_visibility_string = ";".join(preset_item["settings"])
|
||||
new_visibility_string = ";".join(preset_item.settings)
|
||||
if preset_id == "custom":
|
||||
# Get settings from the stored custom data
|
||||
new_visibility_string = self._preferences.getValue("cura/custom_visible_settings")
|
||||
|
@ -141,11 +115,9 @@ class SettingVisibilityPresetsModel(ListModel):
|
|||
self._active_preset_item = preset_item
|
||||
self.activePresetChanged.emit()
|
||||
|
||||
activePresetChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(str, notify = activePresetChanged)
|
||||
def activePreset(self) -> str:
|
||||
return self._active_preset_item["id"]
|
||||
return self._active_preset_item.presetId
|
||||
|
||||
def _onPreferencesChanged(self, name: str) -> None:
|
||||
if name != "general/visible_settings":
|
||||
|
@ -158,25 +130,26 @@ class SettingVisibilityPresetsModel(ListModel):
|
|||
|
||||
visibility_set = set(visibility_string.split(";"))
|
||||
matching_preset_item = None
|
||||
for item in self.items:
|
||||
if item["id"] == "custom":
|
||||
for item in self._items:
|
||||
if item.presetId == "custom":
|
||||
continue
|
||||
if set(item["settings"]) == visibility_set:
|
||||
if set(item.settings) == visibility_set:
|
||||
matching_preset_item = item
|
||||
break
|
||||
|
||||
item_to_set = self._active_preset_item
|
||||
if matching_preset_item is None:
|
||||
# The new visibility setup is "custom" should be custom
|
||||
if self._active_preset_item["id"] == "custom":
|
||||
if self._active_preset_item is None or self._active_preset_item.presetId == "custom":
|
||||
# We are already in custom, just save the settings
|
||||
self._preferences.setValue("cura/custom_visible_settings", visibility_string)
|
||||
else:
|
||||
item_to_set = self.items[0] # 0 is custom
|
||||
# We need to move to custom preset.
|
||||
item_to_set = self.getVisibilityPresetById("custom")
|
||||
else:
|
||||
item_to_set = matching_preset_item
|
||||
|
||||
if self._active_preset_item is None or self._active_preset_item["id"] != item_to_set["id"]:
|
||||
if self._active_preset_item is None or self._active_preset_item.presetId != item_to_set.presetId:
|
||||
self._active_preset_item = item_to_set
|
||||
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item["id"])
|
||||
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item.presetId)
|
||||
self.activePresetChanged.emit()
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, cast, Dict, List
|
||||
from typing import TYPE_CHECKING, Optional, cast, Dict, List, Set
|
||||
|
||||
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||
from UM.Logger import Logger
|
||||
from UM.Util import parseBool
|
||||
|
@ -21,7 +20,6 @@ if TYPE_CHECKING:
|
|||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from .QualityChangesGroup import QualityChangesGroup
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
|
||||
#
|
||||
|
@ -38,11 +36,11 @@ class QualityManager(QObject):
|
|||
|
||||
qualitiesUpdated = pyqtSignal()
|
||||
|
||||
def __init__(self, container_registry: "ContainerRegistry", parent = None) -> None:
|
||||
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._application = Application.getInstance() # type: CuraApplication
|
||||
self._application = application
|
||||
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_changes_container = self._application.empty_quality_changes_container
|
||||
|
@ -261,11 +259,15 @@ class QualityManager(QObject):
|
|||
root_material_id = self._material_manager.getRootMaterialIDWithoutDiameter(root_material_id)
|
||||
root_material_id_list.append(root_material_id)
|
||||
|
||||
# Also try to get the fallback material
|
||||
material_type = extruder.material.getMetaDataEntry("material")
|
||||
fallback_root_material_id = self._material_manager.getFallbackMaterialIdByMaterialType(material_type)
|
||||
if fallback_root_material_id:
|
||||
root_material_id_list.append(fallback_root_material_id)
|
||||
# Also try to get the fallback materials
|
||||
fallback_ids = self._material_manager.getFallBackMaterialIdsByMaterial(extruder.material)
|
||||
|
||||
if fallback_ids:
|
||||
root_material_id_list.extend(fallback_ids)
|
||||
|
||||
# Weed out duplicates while preserving the order.
|
||||
seen = set() # type: Set[str]
|
||||
root_material_id_list = [x for x in root_material_id_list if x not in seen and not seen.add(x)] # type: ignore
|
||||
|
||||
# Here we construct a list of nodes we want to look for qualities with the highest priority first.
|
||||
# The use case is that, when we look for qualities for a machine, we first want to search in the following
|
||||
|
@ -458,7 +460,7 @@ class QualityManager(QObject):
|
|||
# stack and clear the user settings.
|
||||
@pyqtSlot(str)
|
||||
def createQualityChanges(self, base_name: str) -> None:
|
||||
machine_manager = Application.getInstance().getMachineManager()
|
||||
machine_manager = self._application.getMachineManager()
|
||||
|
||||
global_stack = machine_manager.activeMachine
|
||||
if not global_stack:
|
||||
|
|
|
@ -115,17 +115,24 @@ class VariantManager:
|
|||
|
||||
#
|
||||
# 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",
|
||||
variant_type: VariantType) -> Optional["ContainerNode"]:
|
||||
variant_type: "VariantType",
|
||||
global_stack: Optional["GlobalStack"] = None) -> Optional["ContainerNode"]:
|
||||
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
|
||||
if variant_type == VariantType.BUILD_PLATE:
|
||||
if parseBool(machine_definition.getMetaDataEntry("has_variant_buildplates", False)):
|
||||
preferred_variant_name = machine_definition.getMetaDataEntry("preferred_variant_buildplate_name")
|
||||
if parseBool(container_for_metadata_fetching.getMetaDataEntry("has_variant_buildplates", False)):
|
||||
preferred_variant_name = container_for_metadata_fetching.getMetaDataEntry("preferred_variant_buildplate_name")
|
||||
else:
|
||||
if parseBool(machine_definition.getMetaDataEntry("has_variants", False)):
|
||||
preferred_variant_name = machine_definition.getMetaDataEntry("preferred_variant_name")
|
||||
if parseBool(container_for_metadata_fetching.getMetaDataEntry("has_variants", False)):
|
||||
preferred_variant_name = container_for_metadata_fetching.getMetaDataEntry("preferred_variant_name")
|
||||
|
||||
node = None
|
||||
if preferred_variant_name:
|
||||
|
|
112
cura/OAuth2/AuthorizationHelpers.py
Normal file
112
cura/OAuth2/AuthorizationHelpers.py
Normal 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()
|
101
cura/OAuth2/AuthorizationRequestHandler.py
Normal file
101
cura/OAuth2/AuthorizationRequestHandler.py
Normal 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]
|
26
cura/OAuth2/AuthorizationRequestServer.py
Normal file
26
cura/OAuth2/AuthorizationRequestServer.py
Normal 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
|
168
cura/OAuth2/AuthorizationService.py
Normal file
168
cura/OAuth2/AuthorizationService.py
Normal file
|
@ -0,0 +1,168 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
import 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": "(.Y.)",
|
||||
"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)
|
64
cura/OAuth2/LocalAuthorizationServer.py
Normal file
64
cura/OAuth2/LocalAuthorizationServer.py
Normal 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
60
cura/OAuth2/Models.py
Normal 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
2
cura/OAuth2/__init__.py
Normal file
|
@ -0,0 +1,2 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
@ -9,6 +9,7 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.i18n import i18nCatalog
|
||||
from collections import defaultdict
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -40,6 +41,8 @@ class ObjectsModel(ListModel):
|
|||
filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate")
|
||||
active_build_plate_number = self._build_plate_number
|
||||
group_nr = 1
|
||||
name_count_dict = defaultdict(int)
|
||||
|
||||
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
|
@ -55,6 +58,7 @@ class ObjectsModel(ListModel):
|
|||
|
||||
if not node.callDecoration("isGroup"):
|
||||
name = node.getName()
|
||||
|
||||
else:
|
||||
name = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = str(group_nr))
|
||||
group_nr += 1
|
||||
|
@ -63,6 +67,14 @@ class ObjectsModel(ListModel):
|
|||
is_outside_build_area = node.isOutsideBuildArea()
|
||||
else:
|
||||
is_outside_build_area = False
|
||||
|
||||
#check if we already have an instance of the object based on name
|
||||
name_count_dict[name] += 1
|
||||
name_count = name_count_dict[name]
|
||||
|
||||
if name_count > 1:
|
||||
name = "{0}({1})".format(name, name_count-1)
|
||||
node.setName(name)
|
||||
|
||||
nodes.append({
|
||||
"name": name,
|
||||
|
@ -71,6 +83,7 @@ class ObjectsModel(ListModel):
|
|||
"buildPlateNumber": node_build_plate_number,
|
||||
"node": node
|
||||
})
|
||||
|
||||
nodes = sorted(nodes, key=lambda n: n["name"])
|
||||
self.setItems(nodes)
|
||||
|
||||
|
|
|
@ -6,73 +6,59 @@ import math
|
|||
import os
|
||||
import unicodedata
|
||||
import re # To create abbreviations for printer names.
|
||||
from typing import Dict
|
||||
from typing import Dict, List, Optional
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.Duration import Duration
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.MimeTypeDatabase import MimeTypeDatabase
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## A class for processing and calculating minimum, current and maximum print time as well as managing the job name
|
||||
#
|
||||
# This class contains all the logic relating to calculation and slicing for the
|
||||
# time/quality slider concept. It is a rather tricky combination of event handling
|
||||
# and state management. The logic behind this is as follows:
|
||||
#
|
||||
# - A scene change or setting change event happens.
|
||||
# We track what the source was of the change, either a scene change, a setting change, an active machine change or something else.
|
||||
# - This triggers a new slice with the current settings - this is the "current settings pass".
|
||||
# - When the slice is done, we update the current print time and material amount.
|
||||
# - If the source of the slice was not a Setting change, we start the second slice pass, the "low quality settings pass". Otherwise we stop here.
|
||||
# - When that is done, we update the minimum print time and start the final slice pass, the "Extra Fine settings pass".
|
||||
# - When the Extra Fine pass is done, we update the maximum print time.
|
||||
## A class for processing and the print times per build plate as well as managing the job name
|
||||
#
|
||||
# This class also mangles the current machine name and the filename of the first loaded mesh into a job name.
|
||||
# This job name is requested by the JobSpecs qml file.
|
||||
class PrintInformation(QObject):
|
||||
class SlicePass:
|
||||
CurrentSettings = 1
|
||||
LowQualitySettings = 2
|
||||
HighQualitySettings = 3
|
||||
|
||||
class SliceReason:
|
||||
SceneChanged = 1
|
||||
SettingChanged = 2
|
||||
ActiveMachineChanged = 3
|
||||
Other = 4
|
||||
UNTITLED_JOB_NAME = "Untitled"
|
||||
|
||||
def __init__(self, application, parent = None):
|
||||
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._application = application
|
||||
|
||||
self.initializeCuraMessagePrintTimeProperties()
|
||||
|
||||
self._material_lengths = {} # indexed by build plate number
|
||||
self._material_weights = {}
|
||||
self._material_costs = {}
|
||||
self._material_names = {}
|
||||
# Indexed by build plate number
|
||||
self._material_lengths = {} # type: Dict[int, List[float]]
|
||||
self._material_weights = {} # type: Dict[int, List[float]]
|
||||
self._material_costs = {} # type: Dict[int, List[float]]
|
||||
self._material_names = {} # type: Dict[int, List[str]]
|
||||
|
||||
self._pre_sliced = False
|
||||
|
||||
self._backend = self._application.getBackend()
|
||||
if self._backend:
|
||||
self._backend.printDurationMessage.connect(self._onPrintDurationMessage)
|
||||
|
||||
self._application.getController().getScene().sceneChanged.connect(self._onSceneChanged)
|
||||
|
||||
self._is_user_specified_job_name = False
|
||||
self._base_name = ""
|
||||
self._abbr_machine = ""
|
||||
self._job_name = ""
|
||||
self._project_name = ""
|
||||
self._active_build_plate = 0
|
||||
self._initVariablesWithBuildPlate(self._active_build_plate)
|
||||
self._initVariablesByBuildPlate(self._active_build_plate)
|
||||
|
||||
self._multi_build_plate_model = self._application.getMultiBuildPlateModel()
|
||||
|
||||
|
@ -80,19 +66,17 @@ class PrintInformation(QObject):
|
|||
self._application.globalContainerStackChanged.connect(self.setToZeroPrintInformation)
|
||||
self._application.fileLoaded.connect(self.setBaseName)
|
||||
self._application.workspaceLoaded.connect(self.setProjectName)
|
||||
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
|
||||
|
||||
self._application.getMachineManager().rootMaterialChanged.connect(self._onActiveMaterialsChanged)
|
||||
self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
|
||||
|
||||
self._application.getMachineManager().rootMaterialChanged.connect(self._onActiveMaterialsChanged)
|
||||
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
|
||||
|
||||
self._onActiveMaterialsChanged()
|
||||
|
||||
self._material_amounts = []
|
||||
self._material_amounts = [] # type: List[float]
|
||||
|
||||
# Crate cura message translations and using translation keys initialize empty time Duration object for total time
|
||||
# and time for each feature
|
||||
def initializeCuraMessagePrintTimeProperties(self):
|
||||
self._current_print_time = {} # Duration(None, self)
|
||||
def initializeCuraMessagePrintTimeProperties(self) -> None:
|
||||
self._current_print_time = {} # type: Dict[int, Duration]
|
||||
|
||||
self._print_time_message_translations = {
|
||||
"inset_0": catalog.i18nc("@tooltip", "Outer Wall"),
|
||||
|
@ -108,17 +92,17 @@ class PrintInformation(QObject):
|
|||
"none": catalog.i18nc("@tooltip", "Other")
|
||||
}
|
||||
|
||||
self._print_time_message_values = {}
|
||||
self._print_times_per_feature = {} # type: Dict[int, Dict[str, Duration]]
|
||||
|
||||
def _initPrintTimeMessageValues(self, build_plate_number):
|
||||
def _initPrintTimesPerFeature(self, build_plate_number: int) -> None:
|
||||
# Full fill message values using keys from _print_time_message_translations
|
||||
self._print_time_message_values[build_plate_number] = {}
|
||||
self._print_times_per_feature[build_plate_number] = {}
|
||||
for key in self._print_time_message_translations.keys():
|
||||
self._print_time_message_values[build_plate_number][key] = Duration(None, self)
|
||||
self._print_times_per_feature[build_plate_number][key] = Duration(None, self)
|
||||
|
||||
def _initVariablesWithBuildPlate(self, build_plate_number):
|
||||
if build_plate_number not in self._print_time_message_values:
|
||||
self._initPrintTimeMessageValues(build_plate_number)
|
||||
def _initVariablesByBuildPlate(self, build_plate_number: int) -> None:
|
||||
if build_plate_number not in self._print_times_per_feature:
|
||||
self._initPrintTimesPerFeature(build_plate_number)
|
||||
if self._active_build_plate not in self._material_lengths:
|
||||
self._material_lengths[self._active_build_plate] = []
|
||||
if self._active_build_plate not in self._material_weights:
|
||||
|
@ -128,23 +112,24 @@ class PrintInformation(QObject):
|
|||
if self._active_build_plate not in self._material_names:
|
||||
self._material_names[self._active_build_plate] = []
|
||||
if self._active_build_plate not in self._current_print_time:
|
||||
self._current_print_time[self._active_build_plate] = Duration(None, self)
|
||||
self._current_print_time[self._active_build_plate] = Duration(parent = self)
|
||||
|
||||
currentPrintTimeChanged = pyqtSignal()
|
||||
|
||||
preSlicedChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify=preSlicedChanged)
|
||||
def preSliced(self):
|
||||
def preSliced(self) -> bool:
|
||||
return self._pre_sliced
|
||||
|
||||
def setPreSliced(self, pre_sliced):
|
||||
self._pre_sliced = pre_sliced
|
||||
self._updateJobName()
|
||||
self.preSlicedChanged.emit()
|
||||
def setPreSliced(self, pre_sliced: bool) -> None:
|
||||
if self._pre_sliced != pre_sliced:
|
||||
self._pre_sliced = pre_sliced
|
||||
self._updateJobName()
|
||||
self.preSlicedChanged.emit()
|
||||
|
||||
@pyqtProperty(Duration, notify = currentPrintTimeChanged)
|
||||
def currentPrintTime(self):
|
||||
def currentPrintTime(self) -> Duration:
|
||||
return self._current_print_time[self._active_build_plate]
|
||||
|
||||
materialLengthsChanged = pyqtSignal()
|
||||
|
@ -171,36 +156,41 @@ class PrintInformation(QObject):
|
|||
def materialNames(self):
|
||||
return self._material_names[self._active_build_plate]
|
||||
|
||||
def printTimes(self):
|
||||
return self._print_time_message_values[self._active_build_plate]
|
||||
# Get all print times (by feature) of the active buildplate.
|
||||
def printTimes(self) -> Dict[str, Duration]:
|
||||
return self._print_times_per_feature[self._active_build_plate]
|
||||
|
||||
def _onPrintDurationMessage(self, build_plate_number, print_time: Dict[str, int], material_amounts: list):
|
||||
self._updateTotalPrintTimePerFeature(build_plate_number, print_time)
|
||||
def _onPrintDurationMessage(self, build_plate_number: int, print_times_per_feature: Dict[str, int], material_amounts: List[float]) -> None:
|
||||
self._updateTotalPrintTimePerFeature(build_plate_number, print_times_per_feature)
|
||||
self.currentPrintTimeChanged.emit()
|
||||
|
||||
self._material_amounts = material_amounts
|
||||
self._calculateInformation(build_plate_number)
|
||||
|
||||
def _updateTotalPrintTimePerFeature(self, build_plate_number, print_time: Dict[str, int]):
|
||||
def _updateTotalPrintTimePerFeature(self, build_plate_number: int, print_times_per_feature: Dict[str, int]) -> None:
|
||||
total_estimated_time = 0
|
||||
|
||||
if build_plate_number not in self._print_time_message_values:
|
||||
self._initPrintTimeMessageValues(build_plate_number)
|
||||
if build_plate_number not in self._print_times_per_feature:
|
||||
self._initPrintTimesPerFeature(build_plate_number)
|
||||
|
||||
for feature, time in print_times_per_feature.items():
|
||||
if feature not in self._print_times_per_feature[build_plate_number]:
|
||||
self._print_times_per_feature[build_plate_number][feature] = Duration(parent=self)
|
||||
duration = self._print_times_per_feature[build_plate_number][feature]
|
||||
|
||||
for feature, time in print_time.items():
|
||||
if time != time: # Check for NaN. Engine can sometimes give us weird values.
|
||||
self._print_time_message_values[build_plate_number].get(feature).setDuration(0)
|
||||
duration.setDuration(0)
|
||||
Logger.log("w", "Received NaN for print duration message")
|
||||
continue
|
||||
|
||||
total_estimated_time += time
|
||||
self._print_time_message_values[build_plate_number].get(feature).setDuration(time)
|
||||
duration.setDuration(time)
|
||||
|
||||
if build_plate_number not in self._current_print_time:
|
||||
self._current_print_time[build_plate_number] = Duration(None, self)
|
||||
self._current_print_time[build_plate_number].setDuration(total_estimated_time)
|
||||
|
||||
def _calculateInformation(self, build_plate_number):
|
||||
def _calculateInformation(self, build_plate_number: int) -> None:
|
||||
global_stack = self._application.getGlobalContainerStack()
|
||||
if global_stack is None:
|
||||
return
|
||||
|
@ -213,39 +203,45 @@ class PrintInformation(QObject):
|
|||
material_preference_values = json.loads(self._application.getInstance().getPreferences().getValue("cura/material_settings"))
|
||||
|
||||
extruder_stacks = global_stack.extruders
|
||||
for position, extruder_stack in extruder_stacks.items():
|
||||
|
||||
for position in extruder_stacks:
|
||||
extruder_stack = extruder_stacks[position]
|
||||
index = int(position)
|
||||
if index >= len(self._material_amounts):
|
||||
continue
|
||||
amount = self._material_amounts[index]
|
||||
## Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some
|
||||
# list comprehension filtering to solve this for us.
|
||||
# Find the right extruder stack. As the list isn't sorted because it's a annoying generator, we do some
|
||||
# list comprehension filtering to solve this for us.
|
||||
density = extruder_stack.getMetaDataEntry("properties", {}).get("density", 0)
|
||||
material = extruder_stack.findContainer({"type": "material"})
|
||||
material = extruder_stack.material
|
||||
radius = extruder_stack.getProperty("material_diameter", "value") / 2
|
||||
|
||||
weight = float(amount) * float(density) / 1000
|
||||
cost = 0
|
||||
material_name = catalog.i18nc("@label unknown material", "Unknown")
|
||||
if material:
|
||||
material_guid = material.getMetaDataEntry("GUID")
|
||||
material_name = material.getName()
|
||||
if material_guid in material_preference_values:
|
||||
material_values = material_preference_values[material_guid]
|
||||
cost = 0.
|
||||
|
||||
weight_per_spool = float(material_values["spool_weight"] if material_values and "spool_weight" in material_values else 0)
|
||||
cost_per_spool = float(material_values["spool_cost"] if material_values and "spool_cost" in material_values else 0)
|
||||
material_guid = material.getMetaDataEntry("GUID")
|
||||
material_name = material.getName()
|
||||
if material_guid in material_preference_values:
|
||||
material_values = material_preference_values[material_guid]
|
||||
|
||||
if weight_per_spool != 0:
|
||||
cost = cost_per_spool * weight / weight_per_spool
|
||||
else:
|
||||
cost = 0
|
||||
if material_values and "spool_weight" in material_values:
|
||||
weight_per_spool = float(material_values["spool_weight"])
|
||||
else:
|
||||
weight_per_spool = float(extruder_stack.getMetaDataEntry("properties", {}).get("weight", 0))
|
||||
|
||||
cost_per_spool = float(material_values["spool_cost"] if material_values and "spool_cost" in material_values else 0)
|
||||
|
||||
if weight_per_spool != 0:
|
||||
cost = cost_per_spool * weight / weight_per_spool
|
||||
else:
|
||||
cost = 0
|
||||
|
||||
# Material amount is sent as an amount of mm^3, so calculate length from that
|
||||
if radius != 0:
|
||||
length = round((amount / (math.pi * radius ** 2)) / 1000, 2)
|
||||
else:
|
||||
length = 0
|
||||
|
||||
self._material_weights[build_plate_number].append(weight)
|
||||
self._material_lengths[build_plate_number].append(length)
|
||||
self._material_costs[build_plate_number].append(cost)
|
||||
|
@ -256,20 +252,20 @@ class PrintInformation(QObject):
|
|||
self.materialCostsChanged.emit()
|
||||
self.materialNamesChanged.emit()
|
||||
|
||||
def _onPreferencesChanged(self, preference):
|
||||
def _onPreferencesChanged(self, preference: str) -> None:
|
||||
if preference != "cura/material_settings":
|
||||
return
|
||||
|
||||
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
|
||||
self._calculateInformation(build_plate_number)
|
||||
|
||||
def _onActiveBuildPlateChanged(self):
|
||||
def _onActiveBuildPlateChanged(self) -> None:
|
||||
new_active_build_plate = self._multi_build_plate_model.activeBuildPlate
|
||||
if new_active_build_plate != self._active_build_plate:
|
||||
self._active_build_plate = new_active_build_plate
|
||||
self._updateJobName()
|
||||
|
||||
self._initVariablesWithBuildPlate(self._active_build_plate)
|
||||
self._initVariablesByBuildPlate(self._active_build_plate)
|
||||
|
||||
self.materialLengthsChanged.emit()
|
||||
self.materialWeightsChanged.emit()
|
||||
|
@ -277,14 +273,14 @@ class PrintInformation(QObject):
|
|||
self.materialNamesChanged.emit()
|
||||
self.currentPrintTimeChanged.emit()
|
||||
|
||||
def _onActiveMaterialsChanged(self, *args, **kwargs):
|
||||
def _onActiveMaterialsChanged(self, *args, **kwargs) -> None:
|
||||
for build_plate_number in range(self._multi_build_plate_model.maxBuildPlate + 1):
|
||||
self._calculateInformation(build_plate_number)
|
||||
|
||||
# Manual override of job name should also set the base name so that when the printer prefix is updated, it the
|
||||
# prefix can be added to the manually added name, not the old base name
|
||||
@pyqtSlot(str, bool)
|
||||
def setJobName(self, name, is_user_specified_job_name = False):
|
||||
def setJobName(self, name: str, is_user_specified_job_name = False) -> None:
|
||||
self._is_user_specified_job_name = is_user_specified_job_name
|
||||
self._job_name = name
|
||||
self._base_name = name.replace(self._abbr_machine + "_", "")
|
||||
|
@ -298,15 +294,15 @@ class PrintInformation(QObject):
|
|||
def jobName(self):
|
||||
return self._job_name
|
||||
|
||||
def _updateJobName(self):
|
||||
def _updateJobName(self) -> None:
|
||||
if self._base_name == "":
|
||||
self._job_name = "Untitled"
|
||||
self._job_name = self.UNTITLED_JOB_NAME
|
||||
self._is_user_specified_job_name = False
|
||||
self.jobNameChanged.emit()
|
||||
return
|
||||
|
||||
base_name = self._stripAccents(self._base_name)
|
||||
self._setAbbreviatedMachineName()
|
||||
self._defineAbbreviatedMachineName()
|
||||
|
||||
# Only update the job name when it's not user-specified.
|
||||
if not self._is_user_specified_job_name:
|
||||
|
@ -333,12 +329,12 @@ class PrintInformation(QObject):
|
|||
self.jobNameChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setProjectName(self, name):
|
||||
def setProjectName(self, name: str) -> None:
|
||||
self.setBaseName(name, is_project_file = True)
|
||||
|
||||
baseNameChanged = pyqtSignal()
|
||||
|
||||
def setBaseName(self, base_name: str, is_project_file: bool = False):
|
||||
def setBaseName(self, base_name: str, is_project_file: bool = False) -> None:
|
||||
self._is_user_specified_job_name = False
|
||||
|
||||
# Ensure that we don't use entire path but only filename
|
||||
|
@ -373,6 +369,16 @@ class PrintInformation(QObject):
|
|||
else:
|
||||
self._base_name = ""
|
||||
|
||||
# Strip the old "curaproject" extension from the name
|
||||
OLD_CURA_PROJECT_EXT = ".curaproject"
|
||||
if self._base_name.lower().endswith(OLD_CURA_PROJECT_EXT):
|
||||
self._base_name = self._base_name[:len(self._base_name) - len(OLD_CURA_PROJECT_EXT)]
|
||||
|
||||
# CURA-5896 Try to strip extra extensions with an infinite amount of ".curaproject.3mf".
|
||||
OLD_CURA_PROJECT_3MF_EXT = ".curaproject.3mf"
|
||||
while self._base_name.lower().endswith(OLD_CURA_PROJECT_3MF_EXT):
|
||||
self._base_name = self._base_name[:len(self._base_name) - len(OLD_CURA_PROJECT_3MF_EXT)]
|
||||
|
||||
self._updateJobName()
|
||||
|
||||
@pyqtProperty(str, fset = setBaseName, notify = baseNameChanged)
|
||||
|
@ -382,7 +388,7 @@ class PrintInformation(QObject):
|
|||
## Created an acronym-like abbreviated machine name from the currently
|
||||
# active machine name.
|
||||
# Called each time the global stack is switched.
|
||||
def _setAbbreviatedMachineName(self):
|
||||
def _defineAbbreviatedMachineName(self) -> None:
|
||||
global_container_stack = self._application.getGlobalContainerStack()
|
||||
if not global_container_stack:
|
||||
self._abbr_machine = ""
|
||||
|
@ -406,15 +412,15 @@ class PrintInformation(QObject):
|
|||
self._abbr_machine = abbr_machine
|
||||
|
||||
## Utility method that strips accents from characters (eg: â -> a)
|
||||
def _stripAccents(self, str):
|
||||
return ''.join(char for char in unicodedata.normalize('NFD', str) if unicodedata.category(char) != 'Mn')
|
||||
def _stripAccents(self, to_strip: str) -> str:
|
||||
return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')
|
||||
|
||||
@pyqtSlot(result = "QVariantMap")
|
||||
def getFeaturePrintTimes(self):
|
||||
result = {}
|
||||
if self._active_build_plate not in self._print_time_message_values:
|
||||
self._initPrintTimeMessageValues(self._active_build_plate)
|
||||
for feature, time in self._print_time_message_values[self._active_build_plate].items():
|
||||
if self._active_build_plate not in self._print_times_per_feature:
|
||||
self._initPrintTimesPerFeature(self._active_build_plate)
|
||||
for feature, time in self._print_times_per_feature[self._active_build_plate].items():
|
||||
if feature in self._print_time_message_translations:
|
||||
result[self._print_time_message_translations[feature]] = time
|
||||
else:
|
||||
|
@ -422,22 +428,22 @@ class PrintInformation(QObject):
|
|||
return result
|
||||
|
||||
# Simulate message with zero time duration
|
||||
def setToZeroPrintInformation(self, build_plate = None):
|
||||
def setToZeroPrintInformation(self, build_plate: Optional[int] = None) -> None:
|
||||
if build_plate is None:
|
||||
build_plate = self._active_build_plate
|
||||
|
||||
# Construct the 0-time message
|
||||
temp_message = {}
|
||||
if build_plate not in self._print_time_message_values:
|
||||
self._print_time_message_values[build_plate] = {}
|
||||
for key in self._print_time_message_values[build_plate].keys():
|
||||
if build_plate not in self._print_times_per_feature:
|
||||
self._print_times_per_feature[build_plate] = {}
|
||||
for key in self._print_times_per_feature[build_plate].keys():
|
||||
temp_message[key] = 0
|
||||
temp_material_amounts = [0]
|
||||
temp_material_amounts = [0.]
|
||||
|
||||
self._onPrintDurationMessage(build_plate, temp_message, temp_material_amounts)
|
||||
|
||||
## Listen to scene changes to check if we need to reset the print information
|
||||
def _onSceneChanged(self, scene_node):
|
||||
def _onSceneChanged(self, scene_node: SceneNode) -> None:
|
||||
# Ignore any changes that are not related to sliceable objects
|
||||
if not isinstance(scene_node, SceneNode)\
|
||||
or not scene_node.callDecoration("isSliceable")\
|
||||
|
|
78
cura/PrinterOutput/FirmwareUpdater.py
Normal file
78
cura/PrinterOutput/FirmwareUpdater.py
Normal 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
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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 PyQt5.QtCore import QTimer
|
||||
|
@ -9,27 +9,28 @@ from PyQt5.QtCore import QTimer
|
|||
if TYPE_CHECKING:
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
||||
|
||||
|
||||
class GenericOutputController(PrinterOutputController):
|
||||
def __init__(self, output_device):
|
||||
def __init__(self, output_device: "PrinterOutputDevice") -> None:
|
||||
super().__init__(output_device)
|
||||
|
||||
self._preheat_bed_timer = QTimer()
|
||||
self._preheat_bed_timer.setSingleShot(True)
|
||||
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.setSingleShot(True)
|
||||
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._active_printer = None
|
||||
self._active_printer = None # type: Optional[PrinterOutputModel]
|
||||
|
||||
def _onPrintersChanged(self):
|
||||
def _onPrintersChanged(self) -> None:
|
||||
if self._active_printer:
|
||||
self._active_printer.stateChanged.disconnect(self._onPrinterStateChanged)
|
||||
self._active_printer.targetBedTemperatureChanged.disconnect(self._onTargetBedTemperatureChanged)
|
||||
|
@ -43,32 +44,33 @@ class GenericOutputController(PrinterOutputController):
|
|||
for extruder in self._active_printer.extruders:
|
||||
extruder.targetHotendTemperatureChanged.connect(self._onTargetHotendTemperatureChanged)
|
||||
|
||||
def _onPrinterStateChanged(self):
|
||||
if self._active_printer.state != "idle":
|
||||
def _onPrinterStateChanged(self) -> None:
|
||||
if self._active_printer and self._active_printer.state != "idle":
|
||||
if self._preheat_bed_timer.isActive():
|
||||
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():
|
||||
self._preheat_hotends_timer.stop()
|
||||
for extruder in self._preheat_hotends:
|
||||
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("G0 X%s Y%s Z%s F%s" % (x, y, z, speed))
|
||||
self._output_device.sendCommand("G90")
|
||||
|
||||
def homeHead(self, printer):
|
||||
def homeHead(self, printer: "PrinterOutputModel") -> None:
|
||||
self._output_device.sendCommand("G28 X Y")
|
||||
|
||||
def homeBed(self, printer):
|
||||
def homeBed(self, printer: "PrinterOutputModel") -> None:
|
||||
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.
|
||||
|
||||
def setJobState(self, job: "PrintJobOutputModel", state: str):
|
||||
def setJobState(self, job: "PrintJobOutputModel", state: str) -> None:
|
||||
if state == "pause":
|
||||
self._output_device.pausePrint()
|
||||
job.updateState("paused")
|
||||
|
@ -79,15 +81,15 @@ class GenericOutputController(PrinterOutputController):
|
|||
self._output_device.cancelPrint()
|
||||
pass
|
||||
|
||||
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int):
|
||||
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int) -> None:
|
||||
self._output_device.sendCommand("M140 S%s" % temperature)
|
||||
|
||||
def _onTargetBedTemperatureChanged(self):
|
||||
if self._preheat_bed_timer.isActive() and self._preheat_printer.targetBedTemperature == 0:
|
||||
def _onTargetBedTemperatureChanged(self) -> None:
|
||||
if self._preheat_bed_timer.isActive() and self._preheat_printer and self._preheat_printer.targetBedTemperature == 0:
|
||||
self._preheat_bed_timer.stop()
|
||||
self._preheat_printer.updateIsPreheating(False)
|
||||
|
||||
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration):
|
||||
def preheatBed(self, printer: "PrinterOutputModel", temperature, duration) -> None:
|
||||
try:
|
||||
temperature = round(temperature) # The API doesn't allow floating point.
|
||||
duration = round(duration)
|
||||
|
@ -100,21 +102,25 @@ class GenericOutputController(PrinterOutputController):
|
|||
self._preheat_printer = printer
|
||||
printer.updateIsPreheating(True)
|
||||
|
||||
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
|
||||
def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
|
||||
self.setTargetBedTemperature(printer, temperature=0)
|
||||
self._preheat_bed_timer.stop()
|
||||
printer.updateIsPreheating(False)
|
||||
|
||||
def _onPreheatBedTimerFinished(self):
|
||||
def _onPreheatBedTimerFinished(self) -> None:
|
||||
if not self._preheat_printer:
|
||||
return
|
||||
self.setTargetBedTemperature(self._preheat_printer, 0)
|
||||
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))
|
||||
|
||||
def _onTargetHotendTemperatureChanged(self):
|
||||
def _onTargetHotendTemperatureChanged(self) -> None:
|
||||
if not self._preheat_hotends_timer.isActive():
|
||||
return
|
||||
if not self._active_printer:
|
||||
return
|
||||
|
||||
for extruder in self._active_printer.extruders:
|
||||
if extruder in self._preheat_hotends and extruder.targetHotendTemperature == 0:
|
||||
|
@ -123,7 +129,7 @@ class GenericOutputController(PrinterOutputController):
|
|||
if not self._preheat_hotends:
|
||||
self._preheat_hotends_timer.stop()
|
||||
|
||||
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration):
|
||||
def preheatHotend(self, extruder: "ExtruderOutputModel", temperature, duration) -> None:
|
||||
position = extruder.getPosition()
|
||||
number_of_extruders = len(extruder.getPrinter().extruders)
|
||||
if position >= number_of_extruders:
|
||||
|
@ -141,7 +147,7 @@ class GenericOutputController(PrinterOutputController):
|
|||
self._preheat_hotends.add(extruder)
|
||||
extruder.updateIsPreheating(True)
|
||||
|
||||
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"):
|
||||
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel") -> None:
|
||||
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), temperature=0)
|
||||
if extruder in self._preheat_hotends:
|
||||
extruder.updateIsPreheating(False)
|
||||
|
@ -149,21 +155,22 @@ class GenericOutputController(PrinterOutputController):
|
|||
if not self._preheat_hotends and self._preheat_hotends_timer.isActive():
|
||||
self._preheat_hotends_timer.stop()
|
||||
|
||||
def _onPreheatHotendsTimerFinished(self):
|
||||
def _onPreheatHotendsTimerFinished(self) -> None:
|
||||
for extruder in self._preheat_hotends:
|
||||
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
|
||||
# This can be used eg at the start of a print
|
||||
def stopPreheatTimers(self):
|
||||
def stopPreheatTimers(self) -> None:
|
||||
if self._preheat_hotends_timer.isActive():
|
||||
for extruder in self._preheat_hotends:
|
||||
extruder.updateIsPreheating(False)
|
||||
self._preheat_hotends = set()
|
||||
self._preheat_hotends = set() #type: Set[ExtruderOutputModel]
|
||||
|
||||
self._preheat_hotends_timer.stop()
|
||||
|
||||
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()
|
||||
|
|
|
@ -1,119 +0,0 @@
|
|||
from UM.Logger import Logger
|
||||
|
||||
from PyQt5.QtCore import QUrl, pyqtProperty, pyqtSignal, QObject, pyqtSlot
|
||||
from PyQt5.QtGui import QImage
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
|
||||
|
||||
|
||||
class NetworkCamera(QObject):
|
||||
newImage = pyqtSignal()
|
||||
|
||||
def __init__(self, target = None, parent = None):
|
||||
super().__init__(parent)
|
||||
self._stream_buffer = b""
|
||||
self._stream_buffer_start_index = -1
|
||||
self._manager = None
|
||||
self._image_request = None
|
||||
self._image_reply = None
|
||||
self._image = QImage()
|
||||
self._image_id = 0
|
||||
|
||||
self._target = target
|
||||
self._started = False
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setTarget(self, target):
|
||||
restart_required = False
|
||||
if self._started:
|
||||
self.stop()
|
||||
restart_required = True
|
||||
|
||||
self._target = target
|
||||
|
||||
if restart_required:
|
||||
self.start()
|
||||
|
||||
@pyqtProperty(QUrl, notify=newImage)
|
||||
def latestImage(self):
|
||||
self._image_id += 1
|
||||
# There is an image provider that is called "camera". In order to ensure that the image qml object, that
|
||||
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
|
||||
# as new (instead of relying on cached version and thus forces an update.
|
||||
temp = "image://camera/" + str(self._image_id)
|
||||
|
||||
return QUrl(temp, QUrl.TolerantMode)
|
||||
|
||||
@pyqtSlot()
|
||||
def start(self):
|
||||
# Ensure that previous requests (if any) are stopped.
|
||||
self.stop()
|
||||
if self._target is None:
|
||||
Logger.log("w", "Unable to start camera stream without target!")
|
||||
return
|
||||
self._started = True
|
||||
url = QUrl(self._target)
|
||||
self._image_request = QNetworkRequest(url)
|
||||
if self._manager is None:
|
||||
self._manager = QNetworkAccessManager()
|
||||
|
||||
self._image_reply = self._manager.get(self._image_request)
|
||||
self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
|
||||
|
||||
@pyqtSlot()
|
||||
def stop(self):
|
||||
self._stream_buffer = b""
|
||||
self._stream_buffer_start_index = -1
|
||||
|
||||
if self._image_reply:
|
||||
try:
|
||||
# disconnect the signal
|
||||
try:
|
||||
self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
|
||||
except Exception:
|
||||
pass
|
||||
# abort the request if it's not finished
|
||||
if not self._image_reply.isFinished():
|
||||
self._image_reply.close()
|
||||
except Exception as e: # RuntimeError
|
||||
pass # It can happen that the wrapped c++ object is already deleted.
|
||||
|
||||
self._image_reply = None
|
||||
self._image_request = None
|
||||
|
||||
self._manager = None
|
||||
|
||||
self._started = False
|
||||
|
||||
def getImage(self):
|
||||
return self._image
|
||||
|
||||
## Ensure that close gets called when object is destroyed
|
||||
def __del__(self):
|
||||
self.stop()
|
||||
|
||||
def _onStreamDownloadProgress(self, bytes_received, bytes_total):
|
||||
# An MJPG stream is (for our purpose) a stream of concatenated JPG images.
|
||||
# JPG images start with the marker 0xFFD8, and end with 0xFFD9
|
||||
if self._image_reply is None:
|
||||
return
|
||||
self._stream_buffer += self._image_reply.readAll()
|
||||
|
||||
if len(self._stream_buffer) > 2000000: # No single camera frame should be 2 Mb or larger
|
||||
Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...")
|
||||
self.stop() # resets stream buffer and start index
|
||||
self.start()
|
||||
return
|
||||
|
||||
if self._stream_buffer_start_index == -1:
|
||||
self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
|
||||
stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')
|
||||
# If this happens to be more than a single frame, then so be it; the JPG decoder will
|
||||
# ignore the extra data. We do it like this in order not to get a buildup of frames
|
||||
|
||||
if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
|
||||
jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
|
||||
self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
|
||||
self._stream_buffer_start_index = -1
|
||||
self._image.loadFromData(jpg_data)
|
||||
|
||||
self.newImage.emit()
|
153
cura/PrinterOutput/NetworkMJPGImage.py
Normal file
153
cura/PrinterOutput/NetworkMJPGImage.py
Normal file
|
@ -0,0 +1,153 @@
|
|||
# Copyright (c) 2018 Aldo Hoeben / fieldOfView
|
||||
# NetworkMJPGImage is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QUrl, pyqtProperty, pyqtSignal, pyqtSlot, QRect, QByteArray
|
||||
from PyQt5.QtGui import QImage, QPainter
|
||||
from PyQt5.QtQuick import QQuickPaintedItem
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
||||
#
|
||||
# A QQuickPaintedItem that progressively downloads a network mjpeg stream,
|
||||
# picks it apart in individual jpeg frames, and paints it.
|
||||
#
|
||||
class NetworkMJPGImage(QQuickPaintedItem):
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._stream_buffer = QByteArray()
|
||||
self._stream_buffer_start_index = -1
|
||||
self._network_manager = None # type: QNetworkAccessManager
|
||||
self._image_request = None # type: QNetworkRequest
|
||||
self._image_reply = None # type: QNetworkReply
|
||||
self._image = QImage()
|
||||
self._image_rect = QRect()
|
||||
|
||||
self._source_url = QUrl()
|
||||
self._started = False
|
||||
|
||||
self._mirror = False
|
||||
|
||||
self.setAntialiasing(True)
|
||||
|
||||
## Ensure that close gets called when object is destroyed
|
||||
def __del__(self) -> None:
|
||||
self.stop()
|
||||
|
||||
|
||||
def paint(self, painter: "QPainter") -> None:
|
||||
if self._mirror:
|
||||
painter.drawImage(self.contentsBoundingRect(), self._image.mirrored())
|
||||
return
|
||||
|
||||
painter.drawImage(self.contentsBoundingRect(), self._image)
|
||||
|
||||
|
||||
def setSourceURL(self, source_url: "QUrl") -> None:
|
||||
self._source_url = source_url
|
||||
self.sourceURLChanged.emit()
|
||||
if self._started:
|
||||
self.start()
|
||||
|
||||
def getSourceURL(self) -> "QUrl":
|
||||
return self._source_url
|
||||
|
||||
sourceURLChanged = pyqtSignal()
|
||||
source = pyqtProperty(QUrl, fget = getSourceURL, fset = setSourceURL, notify = sourceURLChanged)
|
||||
|
||||
def setMirror(self, mirror: bool) -> None:
|
||||
if mirror == self._mirror:
|
||||
return
|
||||
self._mirror = mirror
|
||||
self.mirrorChanged.emit()
|
||||
self.update()
|
||||
|
||||
def getMirror(self) -> bool:
|
||||
return self._mirror
|
||||
|
||||
mirrorChanged = pyqtSignal()
|
||||
mirror = pyqtProperty(bool, fget = getMirror, fset = setMirror, notify = mirrorChanged)
|
||||
|
||||
imageSizeChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(int, notify = imageSizeChanged)
|
||||
def imageWidth(self) -> int:
|
||||
return self._image.width()
|
||||
|
||||
@pyqtProperty(int, notify = imageSizeChanged)
|
||||
def imageHeight(self) -> int:
|
||||
return self._image.height()
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def start(self) -> None:
|
||||
self.stop() # Ensure that previous requests (if any) are stopped.
|
||||
|
||||
if not self._source_url:
|
||||
Logger.log("w", "Unable to start camera stream without target!")
|
||||
return
|
||||
self._started = True
|
||||
|
||||
self._image_request = QNetworkRequest(self._source_url)
|
||||
if self._network_manager is None:
|
||||
self._network_manager = QNetworkAccessManager()
|
||||
|
||||
self._image_reply = self._network_manager.get(self._image_request)
|
||||
self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
|
||||
|
||||
@pyqtSlot()
|
||||
def stop(self) -> None:
|
||||
self._stream_buffer = QByteArray()
|
||||
self._stream_buffer_start_index = -1
|
||||
|
||||
if self._image_reply:
|
||||
try:
|
||||
try:
|
||||
self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not self._image_reply.isFinished():
|
||||
self._image_reply.close()
|
||||
except Exception as e: # RuntimeError
|
||||
pass # It can happen that the wrapped c++ object is already deleted.
|
||||
|
||||
self._image_reply = None
|
||||
self._image_request = None
|
||||
|
||||
self._network_manager = None
|
||||
|
||||
self._started = False
|
||||
|
||||
|
||||
def _onStreamDownloadProgress(self, bytes_received: int, bytes_total: int) -> None:
|
||||
# An MJPG stream is (for our purpose) a stream of concatenated JPG images.
|
||||
# JPG images start with the marker 0xFFD8, and end with 0xFFD9
|
||||
if self._image_reply is None:
|
||||
return
|
||||
self._stream_buffer += self._image_reply.readAll()
|
||||
|
||||
if len(self._stream_buffer) > 2000000: # No single camera frame should be 2 Mb or larger
|
||||
Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...")
|
||||
self.stop() # resets stream buffer and start index
|
||||
self.start()
|
||||
return
|
||||
|
||||
if self._stream_buffer_start_index == -1:
|
||||
self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
|
||||
stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')
|
||||
# If this happens to be more than a single frame, then so be it; the JPG decoder will
|
||||
# ignore the extra data. We do it like this in order not to get a buildup of frames
|
||||
|
||||
if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
|
||||
jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
|
||||
self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
|
||||
self._stream_buffer_start_index = -1
|
||||
self._image.loadFromData(jpg_data)
|
||||
|
||||
if self._image.rect() != self._image_rect:
|
||||
self.imageSizeChanged.emit()
|
||||
|
||||
self.update()
|
|
@ -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
|
||||
# sleep.
|
||||
if time_since_last_response > self._recreate_network_manager_time:
|
||||
if self._last_manager_create_time is None:
|
||||
self._createNetworkManager()
|
||||
elif time() - self._last_manager_create_time > self._recreate_network_manager_time:
|
||||
if self._last_manager_create_time is None or time() - self._last_manager_create_time > self._recreate_network_manager_time:
|
||||
self._createNetworkManager()
|
||||
assert(self._manager is not None)
|
||||
elif self._connection_state == ConnectionState.closed:
|
||||
|
@ -215,7 +213,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
|||
request = self._createEmptyRequest(target)
|
||||
self._last_request_time = time()
|
||||
if self._manager is not None:
|
||||
reply = self._manager.post(request, data)
|
||||
reply = self._manager.post(request, data.encode())
|
||||
if on_progress is not None:
|
||||
reply.uploadProgress.connect(on_progress)
|
||||
self._registerOnFinishedCallback(reply, on_finished)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
||||
|
@ -12,7 +12,6 @@ if TYPE_CHECKING:
|
|||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||
|
||||
|
||||
class PrintJobOutputModel(QObject):
|
||||
stateChanged = pyqtSignal()
|
||||
timeTotalChanged = pyqtSignal()
|
||||
|
@ -55,7 +54,7 @@ class PrintJobOutputModel(QObject):
|
|||
@pyqtProperty(QUrl, notify=previewImageChanged)
|
||||
def previewImageUrl(self):
|
||||
self._preview_image_id += 1
|
||||
# There is an image provider that is called "camera". In order to ensure that the image qml object, that
|
||||
# There is an image provider that is called "print_job_preview". In order to ensure that the image qml object, that
|
||||
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
|
||||
# as new (instead of relying on cached version and thus forces an update.
|
||||
temp = "image://print_job_preview/" + str(self._preview_image_id) + "/" + self._key
|
||||
|
@ -91,7 +90,7 @@ class PrintJobOutputModel(QObject):
|
|||
def assignedPrinter(self):
|
||||
return self._assigned_printer
|
||||
|
||||
def updateAssignedPrinter(self, assigned_printer: "PrinterOutputModel"):
|
||||
def updateAssignedPrinter(self, assigned_printer: Optional["PrinterOutputModel"]) -> None:
|
||||
if self._assigned_printer != assigned_printer:
|
||||
old_printer = self._assigned_printer
|
||||
self._assigned_printer = assigned_printer
|
||||
|
@ -147,4 +146,4 @@ class PrintJobOutputModel(QObject):
|
|||
|
||||
@pyqtSlot(str)
|
||||
def setState(self, state):
|
||||
self._output_controller.setJobState(self, state)
|
||||
self._output_controller.setJobState(self, state)
|
||||
|
|
|
@ -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.
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import Signal
|
||||
|
||||
from typing import Union
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||
|
||||
|
||||
class PrinterOutputController:
|
||||
def __init__(self, output_device):
|
||||
def __init__(self, output_device: "PrinterOutputDevice") -> None:
|
||||
self.can_pause = True
|
||||
self.can_abort = True
|
||||
self.can_pre_heat_bed = True
|
||||
self.can_pre_heat_hotends = True
|
||||
self.can_send_raw_gcode = True
|
||||
self.can_control_manually = True
|
||||
self.can_update_firmware = False
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
def cancelPreheatBed(self, printer: "PrinterOutputModel"):
|
||||
def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
|
||||
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")
|
||||
|
||||
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel"):
|
||||
def cancelPreheatHotend(self, extruder: "ExtruderOutputModel") -> None:
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
def homeBed(self, printer: "PrinterOutputModel"):
|
||||
def homeBed(self, printer: "PrinterOutputModel") -> None:
|
||||
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")
|
||||
|
||||
def sendRawCommand(self, printer: "PrinterOutputModel", command: str):
|
||||
def sendRawCommand(self, printer: "PrinterOutputModel", command: str) -> None:
|
||||
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()
|
|
@ -1,8 +1,8 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
|
||||
from typing import Optional
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot, QUrl
|
||||
from typing import List, Dict, Optional
|
||||
from UM.Math.Vector import Vector
|
||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
||||
|
@ -24,8 +24,9 @@ class PrinterOutputModel(QObject):
|
|||
keyChanged = pyqtSignal()
|
||||
printerTypeChanged = pyqtSignal()
|
||||
buildplateChanged = pyqtSignal()
|
||||
cameraChanged = pyqtSignal()
|
||||
cameraUrlChanged = pyqtSignal()
|
||||
configurationChanged = pyqtSignal()
|
||||
canUpdateFirmwareChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None:
|
||||
super().__init__(parent)
|
||||
|
@ -34,6 +35,7 @@ class PrinterOutputModel(QObject):
|
|||
self._name = ""
|
||||
self._key = "" # Unique identifier
|
||||
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._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
|
||||
self._head_position = Vector(0, 0, 0)
|
||||
|
@ -42,40 +44,40 @@ class PrinterOutputModel(QObject):
|
|||
self._printer_state = "unknown"
|
||||
self._is_preheating = False
|
||||
self._printer_type = ""
|
||||
self._buildplate_name = None
|
||||
self._buildplate_name = ""
|
||||
|
||||
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
|
||||
self._extruders]
|
||||
|
||||
self._camera = None
|
||||
self._camera_url = QUrl() # type: QUrl
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def firmwareVersion(self):
|
||||
def firmwareVersion(self) -> str:
|
||||
return self._firmware_version
|
||||
|
||||
def setCamera(self, camera):
|
||||
if self._camera is not camera:
|
||||
self._camera = camera
|
||||
self.cameraChanged.emit()
|
||||
def setCameraUrl(self, camera_url: "QUrl") -> None:
|
||||
if self._camera_url != camera_url:
|
||||
self._camera_url = camera_url
|
||||
self.cameraUrlChanged.emit()
|
||||
|
||||
def updateIsPreheating(self, pre_heating):
|
||||
@pyqtProperty(QUrl, fset = setCameraUrl, notify = cameraUrlChanged)
|
||||
def cameraUrl(self) -> "QUrl":
|
||||
return self._camera_url
|
||||
|
||||
def updateIsPreheating(self, pre_heating: bool) -> None:
|
||||
if self._is_preheating != pre_heating:
|
||||
self._is_preheating = pre_heating
|
||||
self.isPreheatingChanged.emit()
|
||||
|
||||
@pyqtProperty(bool, notify=isPreheatingChanged)
|
||||
def isPreheating(self):
|
||||
def isPreheating(self) -> bool:
|
||||
return self._is_preheating
|
||||
|
||||
@pyqtProperty(QObject, notify=cameraChanged)
|
||||
def camera(self):
|
||||
return self._camera
|
||||
|
||||
@pyqtProperty(str, notify = printerTypeChanged)
|
||||
def type(self):
|
||||
def type(self) -> str:
|
||||
return self._printer_type
|
||||
|
||||
def updateType(self, printer_type):
|
||||
def updateType(self, printer_type: str) -> None:
|
||||
if self._printer_type != printer_type:
|
||||
self._printer_type = printer_type
|
||||
self._printer_configuration.printerType = self._printer_type
|
||||
|
@ -83,10 +85,10 @@ class PrinterOutputModel(QObject):
|
|||
self.configurationChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify = buildplateChanged)
|
||||
def buildplate(self):
|
||||
def buildplate(self) -> str:
|
||||
return self._buildplate_name
|
||||
|
||||
def updateBuildplateName(self, buildplate_name):
|
||||
def updateBuildplateName(self, buildplate_name: str) -> None:
|
||||
if self._buildplate_name != buildplate_name:
|
||||
self._buildplate_name = buildplate_name
|
||||
self._printer_configuration.buildplateConfiguration = self._buildplate_name
|
||||
|
@ -94,66 +96,66 @@ class PrinterOutputModel(QObject):
|
|||
self.configurationChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=keyChanged)
|
||||
def key(self):
|
||||
def key(self) -> str:
|
||||
return self._key
|
||||
|
||||
def updateKey(self, key: str):
|
||||
def updateKey(self, key: str) -> None:
|
||||
if self._key != key:
|
||||
self._key = key
|
||||
self.keyChanged.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def homeHead(self):
|
||||
def homeHead(self) -> None:
|
||||
self._controller.homeHead(self)
|
||||
|
||||
@pyqtSlot()
|
||||
def homeBed(self):
|
||||
def homeBed(self) -> None:
|
||||
self._controller.homeBed(self)
|
||||
|
||||
@pyqtSlot(str)
|
||||
def sendRawCommand(self, command: str):
|
||||
def sendRawCommand(self, command: str) -> None:
|
||||
self._controller.sendRawCommand(self, command)
|
||||
|
||||
@pyqtProperty("QVariantList", constant = True)
|
||||
def extruders(self):
|
||||
def extruders(self) -> List["ExtruderOutputModel"]:
|
||||
return self._extruders
|
||||
|
||||
@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}
|
||||
|
||||
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:
|
||||
self._head_position = Vector(x, y, z)
|
||||
self.headPositionChanged.emit()
|
||||
|
||||
@pyqtProperty(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._controller.setHeadPosition(self, x, y, z, speed)
|
||||
|
||||
@pyqtProperty(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._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed)
|
||||
|
||||
@pyqtProperty(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._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed)
|
||||
|
||||
@pyqtProperty(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._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed)
|
||||
|
||||
@pyqtSlot(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)
|
||||
|
||||
## Pre-heats the heated bed of the printer.
|
||||
|
@ -162,47 +164,47 @@ class PrinterOutputModel(QObject):
|
|||
# Celsius.
|
||||
# \param duration How long the bed should stay warm, in seconds.
|
||||
@pyqtSlot(float, float)
|
||||
def preheatBed(self, temperature, duration):
|
||||
def preheatBed(self, temperature: float, duration: float) -> None:
|
||||
self._controller.preheatBed(self, temperature, duration)
|
||||
|
||||
@pyqtSlot()
|
||||
def cancelPreheatBed(self):
|
||||
def cancelPreheatBed(self) -> None:
|
||||
self._controller.cancelPreheatBed(self)
|
||||
|
||||
def getController(self):
|
||||
def getController(self) -> "PrinterOutputController":
|
||||
return self._controller
|
||||
|
||||
@pyqtProperty(str, notify=nameChanged)
|
||||
def name(self):
|
||||
@pyqtProperty(str, notify = nameChanged)
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
def setName(self, name):
|
||||
def setName(self, name: str) -> None:
|
||||
self._setName(name)
|
||||
self.updateName(name)
|
||||
|
||||
def updateName(self, name):
|
||||
def updateName(self, name: str) -> None:
|
||||
if self._name != name:
|
||||
self._name = name
|
||||
self.nameChanged.emit()
|
||||
|
||||
## Update the bed temperature. This only changes it locally.
|
||||
def updateBedTemperature(self, temperature):
|
||||
def updateBedTemperature(self, temperature: int) -> None:
|
||||
if self._bed_temperature != temperature:
|
||||
self._bed_temperature = temperature
|
||||
self.bedTemperatureChanged.emit()
|
||||
|
||||
def updateTargetBedTemperature(self, temperature):
|
||||
def updateTargetBedTemperature(self, temperature: int) -> None:
|
||||
if self._target_bed_temperature != temperature:
|
||||
self._target_bed_temperature = temperature
|
||||
self.targetBedTemperatureChanged.emit()
|
||||
|
||||
## Set the target bed temperature. This ensures that it's actually sent to the remote.
|
||||
@pyqtSlot(int)
|
||||
def setTargetBedTemperature(self, temperature):
|
||||
def setTargetBedTemperature(self, temperature: int) -> None:
|
||||
self._controller.setTargetBedTemperature(self, temperature)
|
||||
self.updateTargetBedTemperature(temperature)
|
||||
|
||||
def updateActivePrintJob(self, print_job):
|
||||
def updateActivePrintJob(self, print_job: Optional["PrintJobOutputModel"]) -> None:
|
||||
if self._active_print_job != print_job:
|
||||
old_print_job = self._active_print_job
|
||||
|
||||
|
@ -214,72 +216,83 @@ class PrinterOutputModel(QObject):
|
|||
old_print_job.updateAssignedPrinter(None)
|
||||
self.activePrintJobChanged.emit()
|
||||
|
||||
def updateState(self, printer_state):
|
||||
def updateState(self, printer_state: str) -> None:
|
||||
if self._printer_state != printer_state:
|
||||
self._printer_state = printer_state
|
||||
self.stateChanged.emit()
|
||||
|
||||
@pyqtProperty(QObject, notify = activePrintJobChanged)
|
||||
def activePrintJob(self):
|
||||
def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
|
||||
return self._active_print_job
|
||||
|
||||
@pyqtProperty(str, notify=stateChanged)
|
||||
def state(self):
|
||||
def state(self) -> str:
|
||||
return self._printer_state
|
||||
|
||||
@pyqtProperty(int, notify = bedTemperatureChanged)
|
||||
def bedTemperature(self):
|
||||
@pyqtProperty(int, notify=bedTemperatureChanged)
|
||||
def bedTemperature(self) -> int:
|
||||
return self._bed_temperature
|
||||
|
||||
@pyqtProperty(int, notify=targetBedTemperatureChanged)
|
||||
def targetBedTemperature(self):
|
||||
def targetBedTemperature(self) -> int:
|
||||
return self._target_bed_temperature
|
||||
|
||||
# Does the printer support pre-heating the bed at all
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def canPreHeatBed(self):
|
||||
def canPreHeatBed(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_pre_heat_bed
|
||||
return False
|
||||
|
||||
# Does the printer support pre-heating the bed at all
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def canPreHeatHotends(self):
|
||||
def canPreHeatHotends(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_pre_heat_hotends
|
||||
return False
|
||||
|
||||
# Does the printer support sending raw G-code at all
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def canSendRawGcode(self):
|
||||
def canSendRawGcode(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_send_raw_gcode
|
||||
return False
|
||||
|
||||
# Does the printer support pause at all
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def canPause(self):
|
||||
def canPause(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_pause
|
||||
return False
|
||||
|
||||
# Does the printer support abort at all
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def canAbort(self):
|
||||
def canAbort(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_abort
|
||||
return False
|
||||
|
||||
# Does the printer support manual control at all
|
||||
@pyqtProperty(bool, constant=True)
|
||||
def canControlManually(self):
|
||||
def canControlManually(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_control_manually
|
||||
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
|
||||
@pyqtProperty(QObject, notify = configurationChanged)
|
||||
def printerConfiguration(self):
|
||||
def printerConfiguration(self) -> Optional[ConfigurationModel]:
|
||||
if self._printer_configuration.isValid():
|
||||
return self._printer_configuration
|
||||
return None
|
|
@ -4,22 +4,24 @@
|
|||
from UM.Decorators import deprecated
|
||||
from UM.i18n import i18nCatalog
|
||||
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 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.Qt.QtApplication import QtApplication
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
|
||||
from enum import IntEnum # For the connection state tracking.
|
||||
from typing import Callable, List, Optional
|
||||
from typing import Callable, List, Optional, Union
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
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")
|
||||
|
||||
|
@ -83,6 +85,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
|
||||
self._connection_state = ConnectionState.closed #type: ConnectionState
|
||||
|
||||
self._firmware_updater = None #type: Optional[FirmwareUpdater]
|
||||
self._firmware_name = None #type: Optional[str]
|
||||
self._address = "" #type: str
|
||||
self._connection_text = "" #type: str
|
||||
|
@ -128,7 +131,7 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
|
||||
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")
|
||||
|
||||
@pyqtProperty(QObject, notify = printersChanged)
|
||||
|
@ -225,4 +228,14 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
|||
#
|
||||
# This name can be used to define device type
|
||||
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)
|
|
@ -1,9 +1,12 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
|
||||
|
||||
class BlockSlicingDecorator(SceneNodeDecorator):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
def isBlockSlicing(self):
|
||||
def isBlockSlicing(self) -> bool:
|
||||
return True
|
||||
|
|
|
@ -5,6 +5,7 @@ from PyQt5.QtCore import QTimer
|
|||
|
||||
from UM.Application import Application
|
||||
from UM.Math.Polygon import Polygon
|
||||
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
|
||||
|
@ -18,6 +19,8 @@ from typing import TYPE_CHECKING, Any, Optional
|
|||
if TYPE_CHECKING:
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
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.
|
||||
|
@ -33,17 +36,17 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
|
||||
# Make sure the timer is created on the main thread
|
||||
self._recompute_convex_hull_timer = None # type: Optional[QTimer]
|
||||
|
||||
if Application.getInstance() is not None:
|
||||
Application.getInstance().callLater(self.createRecomputeConvexHullTimer)
|
||||
from cura.CuraApplication import CuraApplication
|
||||
if CuraApplication.getInstance() is not None:
|
||||
CuraApplication.getInstance().callLater(self.createRecomputeConvexHullTimer)
|
||||
|
||||
self._raft_thickness = 0.0
|
||||
self._build_volume = Application.getInstance().getBuildVolume()
|
||||
self._build_volume = CuraApplication.getInstance().getBuildVolume()
|
||||
self._build_volume.raftThicknessChanged.connect(self._onChanged)
|
||||
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||
Application.getInstance().getController().toolOperationStarted.connect(self._onChanged)
|
||||
Application.getInstance().getController().toolOperationStopped.connect(self._onChanged)
|
||||
CuraApplication.getInstance().globalContainerStackChanged.connect(self._onGlobalStackChanged)
|
||||
CuraApplication.getInstance().getController().toolOperationStarted.connect(self._onChanged)
|
||||
CuraApplication.getInstance().getController().toolOperationStopped.connect(self._onChanged)
|
||||
|
||||
self._onGlobalStackChanged()
|
||||
|
||||
|
@ -61,9 +64,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
previous_node.parentChanged.disconnect(self._onChanged)
|
||||
|
||||
super().setNode(node)
|
||||
|
||||
self._node.transformationChanged.connect(self._onChanged)
|
||||
self._node.parentChanged.connect(self._onChanged)
|
||||
# Mypy doesn't understand that self._node is no longer optional, so just use the node.
|
||||
node.transformationChanged.connect(self._onChanged)
|
||||
node.parentChanged.connect(self._onChanged)
|
||||
|
||||
self._onChanged()
|
||||
|
||||
|
@ -78,9 +81,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
|
||||
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.
|
||||
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 = self._add2DAdhesionMargin(hull)
|
||||
return hull
|
||||
|
@ -92,6 +95,13 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
|
||||
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
|
||||
# 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
|
||||
|
@ -100,8 +110,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return None
|
||||
|
||||
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()
|
||||
if head_with_fans is None:
|
||||
return None
|
||||
head_with_fans_with_adhesion_margin = self._add2DAdhesionMargin(head_with_fans)
|
||||
return head_with_fans_with_adhesion_margin
|
||||
return None
|
||||
|
@ -114,7 +126,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return None
|
||||
|
||||
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
|
||||
return self._compute2DConvexHull()
|
||||
return None
|
||||
|
@ -130,6 +142,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
controller = Application.getInstance().getController()
|
||||
root = controller.getScene().getRoot()
|
||||
if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node):
|
||||
# If the tool operation is still active, we need to compute the convex hull later after the controller is
|
||||
# no longer active.
|
||||
if controller.isToolOperationActive():
|
||||
self.recomputeConvexHullDelayed()
|
||||
return
|
||||
|
||||
if self._convex_hull_node:
|
||||
self._convex_hull_node.setParent(None)
|
||||
self._convex_hull_node = None
|
||||
|
@ -153,15 +171,17 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
|
||||
def _init2DConvexHullCache(self) -> None:
|
||||
# Cache for the group code path in _compute2DConvexHull()
|
||||
self._2d_convex_hull_group_child_polygon = None
|
||||
self._2d_convex_hull_group_result = None
|
||||
self._2d_convex_hull_group_child_polygon = None # type: Optional[Polygon]
|
||||
self._2d_convex_hull_group_result = None # type: Optional[Polygon]
|
||||
|
||||
# Cache for the mesh code path in _compute2DConvexHull()
|
||||
self._2d_convex_hull_mesh = None
|
||||
self._2d_convex_hull_mesh_world_transform = None
|
||||
self._2d_convex_hull_mesh_result = None
|
||||
self._2d_convex_hull_mesh = None # type: Optional[MeshData]
|
||||
self._2d_convex_hull_mesh_world_transform = None # type: Optional[Matrix]
|
||||
self._2d_convex_hull_mesh_result = None # type: Optional[Polygon]
|
||||
|
||||
def _compute2DConvexHull(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
if self._node.callDecoration("isGroup"):
|
||||
points = numpy.zeros((0, 2), dtype=numpy.int32)
|
||||
for child in self._node.getChildren():
|
||||
|
@ -187,47 +207,47 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return offset_hull
|
||||
|
||||
else:
|
||||
offset_hull = None
|
||||
if self._node.getMeshData():
|
||||
mesh = self._node.getMeshData()
|
||||
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:
|
||||
offset_hull = Polygon([])
|
||||
mesh = self._node.getMeshData()
|
||||
if mesh is None:
|
||||
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
|
||||
self._2d_convex_hull_mesh = mesh
|
||||
self._2d_convex_hull_mesh_world_transform = world_transform
|
||||
|
@ -241,7 +261,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return Polygon()
|
||||
|
||||
def _compute2DConvexHeadFull(self) -> Optional[Polygon]:
|
||||
convex_hull = self._compute2DConvexHeadFull()
|
||||
convex_hull = self._compute2DConvexHull()
|
||||
if convex_hull:
|
||||
return convex_hull.getMinkowskiHull(self._getHeadAndFans())
|
||||
return None
|
||||
|
@ -338,7 +358,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
|
||||
## 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:
|
||||
if not self._global_stack:
|
||||
if self._global_stack is None or self._node is None:
|
||||
return None
|
||||
per_mesh_stack = self._node.callDecoration("getStack")
|
||||
if per_mesh_stack:
|
||||
|
@ -358,7 +378,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
|||
return self._global_stack.getProperty(setting_key, prop)
|
||||
|
||||
## 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:
|
||||
return False
|
||||
if root is node:
|
||||
|
|
|
@ -28,10 +28,10 @@ if TYPE_CHECKING:
|
|||
from cura.Machines.MaterialNode import MaterialNode
|
||||
from cura.Machines.QualityChangesGroup import QualityChangesGroup
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from cura.Settings.MachineManager import MachineManager
|
||||
from cura.Machines.MaterialManager import MaterialManager
|
||||
from cura.Machines.QualityManager import QualityManager
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
@ -52,7 +52,7 @@ class ContainerManager(QObject):
|
|||
|
||||
self._application = application # type: CuraApplication
|
||||
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._material_manager = self._application.getMaterialManager() # type: MaterialManager
|
||||
self._quality_manager = self._application.getQualityManager() # type: QualityManager
|
||||
|
@ -391,7 +391,8 @@ class ContainerManager(QObject):
|
|||
continue
|
||||
|
||||
mime_type = self._container_registry.getMimeTypeForContainer(container_type)
|
||||
|
||||
if mime_type is None:
|
||||
continue
|
||||
entry = {
|
||||
"type": serialize_type,
|
||||
"mime": mime_type,
|
||||
|
|
|
@ -187,11 +187,11 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
try:
|
||||
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
|
||||
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:
|
||||
# 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))
|
||||
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:
|
||||
# Ensure it is always a list of profiles
|
||||
|
@ -215,7 +215,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
if not global_profile:
|
||||
Logger.log("e", "Incorrect profile [%s]. Could not find global profile", file_name)
|
||||
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")
|
||||
|
||||
# Make sure we have a profile_definition in the file:
|
||||
|
@ -225,7 +225,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
if not machine_definition:
|
||||
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
|
||||
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]
|
||||
|
||||
|
@ -238,7 +238,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
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)
|
||||
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
|
||||
global_profile.setMetaDataEntry("definition", expected_machine_definition)
|
||||
|
@ -269,8 +269,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
if idx == 0:
|
||||
# move all per-extruder settings to the first extruder's quality_changes
|
||||
for qc_setting_key in global_profile.getAllKeys():
|
||||
settable_per_extruder = global_stack.getProperty(qc_setting_key,
|
||||
"settable_per_extruder")
|
||||
settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder")
|
||||
if settable_per_extruder:
|
||||
setting_value = global_profile.getProperty(qc_setting_key, "value")
|
||||
|
||||
|
@ -310,8 +309,8 @@ class CuraContainerRegistry(ContainerRegistry):
|
|||
if result is not None:
|
||||
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, result)}
|
||||
"Failed to import profile from <filename>{0}</filename>:",
|
||||
file_name) + " <message>" + result + "</message>"}
|
||||
|
||||
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):
|
||||
continue
|
||||
|
||||
parser = configparser.ConfigParser(interpolation=None)
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
try:
|
||||
parser.read([file_path])
|
||||
except:
|
||||
|
|
|
@ -145,13 +145,11 @@ class CuraContainerStack(ContainerStack):
|
|||
def setDefinition(self, new_definition: DefinitionContainerInterface) -> None:
|
||||
self.replaceContainer(_ContainerIndexes.Definition, new_definition)
|
||||
|
||||
## Get the definition container.
|
||||
#
|
||||
# \return The definition container. Should always be a valid container, but can be equal to the empty InstanceContainer.
|
||||
@pyqtProperty(QObject, fset = setDefinition, notify = pyqtContainersChanged)
|
||||
def definition(self) -> DefinitionContainer:
|
||||
def getDefinition(self) -> "DefinitionContainer":
|
||||
return cast(DefinitionContainer, self._containers[_ContainerIndexes.Definition])
|
||||
|
||||
definition = pyqtProperty(QObject, fget = getDefinition, fset = setDefinition, notify = pyqtContainersChanged)
|
||||
|
||||
@override(ContainerStack)
|
||||
def getBottom(self) -> "DefinitionContainer":
|
||||
return self.definition
|
||||
|
|
130
cura/Settings/CuraFormulaFunctions.py
Normal file
130
cura/Settings/CuraFormulaFunctions.py
Normal 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
|
|
@ -114,7 +114,8 @@ class CuraStackBuilder:
|
|||
|
||||
# get variant container for extruders
|
||||
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
|
||||
if extruder_variant_node:
|
||||
extruder_variant_container = extruder_variant_node.getContainer()
|
||||
|
@ -128,7 +129,7 @@ class CuraStackBuilder:
|
|||
|
||||
# get material container for extruders
|
||||
material_container = application.empty_material_container
|
||||
material_node = material_manager.getDefaultMaterial(global_stack, extruder_position, extruder_variant_name,
|
||||
material_node = material_manager.getDefaultMaterial(global_stack, str(extruder_position), extruder_variant_name,
|
||||
extruder_definition = extruder_definition)
|
||||
if material_node and material_node.getContainer():
|
||||
material_container = material_node.getContainer()
|
||||
|
@ -144,7 +145,6 @@ class CuraStackBuilder:
|
|||
quality_container = application.empty_quality_container
|
||||
)
|
||||
new_extruder.setNextStack(global_stack)
|
||||
global_stack.addExtruder(new_extruder)
|
||||
|
||||
registry.addContainer(new_extruder)
|
||||
|
||||
|
|
|
@ -12,9 +12,7 @@ from UM.Scene.SceneNode import SceneNode
|
|||
from UM.Scene.Selection import Selection
|
||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||
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.PropertyEvaluationContext import PropertyEvaluationContext
|
||||
|
||||
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.
|
||||
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.
|
||||
@pyqtProperty("QVariantMap", notify = extrudersChanged)
|
||||
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
|
||||
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
|
||||
def _fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["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:
|
||||
Logger.log("i", "No extruder stack for global stack [%s], create one", global_stack.getId())
|
||||
|
@ -372,90 +371,9 @@ class ExtruderManager(QObject):
|
|||
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(
|
||||
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_stack_0.definition = extruder_definition
|
||||
|
||||
## Get all extruder values for a certain setting.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# This is exposed to qml for display purposes
|
||||
|
@ -464,62 +382,8 @@ class ExtruderManager(QObject):
|
|||
#
|
||||
# \return String representing the extruder values
|
||||
@pyqtSlot(str, result="QVariant")
|
||||
def getInstanceExtruderValues(self, key) -> List:
|
||||
return ExtruderManager.getExtruderValues(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
|
||||
def getInstanceExtruderValues(self, key: str) -> List:
|
||||
return self._application.getCuraFormulaFunctions().getValuesInAllExtruders(key)
|
||||
|
||||
## Get the resolve value or value for a given key
|
||||
#
|
||||
|
@ -535,28 +399,6 @@ class ExtruderManager(QObject):
|
|||
|
||||
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
|
||||
|
||||
@classmethod
|
||||
|
|
|
@ -65,16 +65,33 @@ class ExtruderStack(CuraContainerStack):
|
|||
def getLoadingPriority(cls) -> int:
|
||||
return 3
|
||||
|
||||
compatibleMaterialDiameterChanged = pyqtSignal()
|
||||
|
||||
## Return the filament diameter that the machine requires.
|
||||
#
|
||||
# If the machine has no requirement for the diameter, -1 is returned.
|
||||
# \return The filament diameter for the printer
|
||||
@property
|
||||
def materialDiameter(self) -> float:
|
||||
def getCompatibleMaterialDiameter(self) -> float:
|
||||
context = PropertyEvaluationContext(self)
|
||||
context.context["evaluate_from_container_index"] = _ContainerIndexes.Variant
|
||||
|
||||
return self.getProperty("material_diameter", "value", context = context)
|
||||
return float(self.getProperty("material_diameter", "value", context = context))
|
||||
|
||||
def setCompatibleMaterialDiameter(self, value: float) -> None:
|
||||
old_approximate_diameter = self.getApproximateMaterialDiameter()
|
||||
if self.getCompatibleMaterialDiameter() != value:
|
||||
self.definitionChanges.setProperty("material_diameter", "value", value)
|
||||
self.compatibleMaterialDiameterChanged.emit()
|
||||
|
||||
# Emit approximate diameter changed signal if needed
|
||||
if old_approximate_diameter != self.getApproximateMaterialDiameter():
|
||||
self.approximateMaterialDiameterChanged.emit()
|
||||
|
||||
compatibleMaterialDiameter = pyqtProperty(float, fset = setCompatibleMaterialDiameter,
|
||||
fget = getCompatibleMaterialDiameter,
|
||||
notify = compatibleMaterialDiameterChanged)
|
||||
|
||||
approximateMaterialDiameterChanged = pyqtSignal()
|
||||
|
||||
## Return the approximate filament diameter that the machine requires.
|
||||
#
|
||||
|
@ -84,9 +101,11 @@ class ExtruderStack(CuraContainerStack):
|
|||
# If the machine has no requirement for the diameter, -1 is returned.
|
||||
#
|
||||
# \return The approximate filament diameter for the printer
|
||||
@pyqtProperty(float)
|
||||
def approximateMaterialDiameter(self) -> float:
|
||||
return round(float(self.materialDiameter))
|
||||
def getApproximateMaterialDiameter(self) -> float:
|
||||
return round(self.getCompatibleMaterialDiameter())
|
||||
|
||||
approximateMaterialDiameter = pyqtProperty(float, fget = getApproximateMaterialDiameter,
|
||||
notify = approximateMaterialDiameterChanged)
|
||||
|
||||
## Overridden from ContainerStack
|
||||
#
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
from collections import defaultdict
|
||||
import threading
|
||||
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.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||
|
@ -13,6 +13,8 @@ from UM.Settings.SettingInstance import InstanceState
|
|||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.Interfaces import PropertyEvaluationContext
|
||||
from UM.Logger import Logger
|
||||
from UM.Resources import Resources
|
||||
from UM.Platform import Platform
|
||||
from UM.Util import parseBool
|
||||
|
||||
import cura.CuraApplication
|
||||
|
@ -200,6 +202,31 @@ class GlobalStack(CuraContainerStack):
|
|||
def getHasMachineQuality(self) -> bool:
|
||||
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:
|
||||
global_stack_mime = MimeType(
|
||||
|
|
|
@ -20,7 +20,6 @@ from UM.Message import Message
|
|||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from UM.Signal import postponeSignals, CompressTechnique
|
||||
|
||||
import cura.CuraApplication
|
||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||
|
@ -29,6 +28,9 @@ from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
|||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
from cura.Settings.cura_empty_instance_containers import (empty_definition_changes_container, empty_variant_container,
|
||||
empty_material_container, empty_quality_container,
|
||||
empty_quality_changes_container)
|
||||
|
||||
from .CuraStackBuilder import CuraStackBuilder
|
||||
|
||||
|
@ -36,6 +38,7 @@ from UM.i18n import i18nCatalog
|
|||
catalog = i18nCatalog("cura")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.CuraContainerStack import CuraContainerStack
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from cura.Machines.MaterialManager import MaterialManager
|
||||
|
@ -47,7 +50,7 @@ if TYPE_CHECKING:
|
|||
|
||||
|
||||
class MachineManager(QObject):
|
||||
def __init__(self, parent: QObject = None) -> None:
|
||||
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self._active_container_stack = None # type: Optional[ExtruderStack]
|
||||
|
@ -66,9 +69,10 @@ class MachineManager(QObject):
|
|||
self._instance_container_timer.setSingleShot(True)
|
||||
self._instance_container_timer.timeout.connect(self.__emitChangedSignals)
|
||||
|
||||
self._application = cura.CuraApplication.CuraApplication.getInstance() #type: cura.CuraApplication.CuraApplication
|
||||
self._application = application
|
||||
self._container_registry = self._application.getContainerRegistry()
|
||||
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._application.getContainerRegistry().containerLoadComplete.connect(self._onContainersChanged)
|
||||
self._container_registry.containerLoadComplete.connect(self._onContainersChanged)
|
||||
|
||||
## When the global container is changed, active material probably needs to be updated.
|
||||
self.globalContainerChanged.connect(self.activeMaterialChanged)
|
||||
|
@ -80,13 +84,6 @@ class MachineManager(QObject):
|
|||
|
||||
self._stacks_have_errors = None # type: Optional[bool]
|
||||
|
||||
self._empty_container = CuraContainerRegistry.getInstance().getEmptyInstanceContainer() #type: InstanceContainer
|
||||
self._empty_definition_changes_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_definition_changes")[0] #type: InstanceContainer
|
||||
self._empty_variant_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_variant")[0] #type: InstanceContainer
|
||||
self._empty_material_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_material")[0] #type: InstanceContainer
|
||||
self._empty_quality_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_quality")[0] #type: InstanceContainer
|
||||
self._empty_quality_changes_container = CuraContainerRegistry.getInstance().findContainers(id = "empty_quality_changes")[0] #type: InstanceContainer
|
||||
|
||||
self._onGlobalContainerChanged()
|
||||
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
|
||||
|
@ -192,19 +189,21 @@ class MachineManager(QObject):
|
|||
for extruder in self._global_container_stack.extruders.values():
|
||||
extruder_configuration = ExtruderConfigurationModel()
|
||||
# For compare just the GUID is needed at this moment
|
||||
mat_type = extruder.material.getMetaDataEntry("material") if extruder.material != self._empty_material_container else None
|
||||
mat_guid = extruder.material.getMetaDataEntry("GUID") if extruder.material != self._empty_material_container else None
|
||||
mat_color = extruder.material.getMetaDataEntry("color_name") if extruder.material != self._empty_material_container else None
|
||||
mat_brand = extruder.material.getMetaDataEntry("brand") if extruder.material != self._empty_material_container else None
|
||||
mat_name = extruder.material.getMetaDataEntry("name") if extruder.material != self._empty_material_container else None
|
||||
mat_type = extruder.material.getMetaDataEntry("material") if extruder.material != empty_material_container else None
|
||||
mat_guid = extruder.material.getMetaDataEntry("GUID") if extruder.material != empty_material_container else None
|
||||
mat_color = extruder.material.getMetaDataEntry("color_name") if extruder.material != empty_material_container else None
|
||||
mat_brand = extruder.material.getMetaDataEntry("brand") if extruder.material != empty_material_container else None
|
||||
mat_name = extruder.material.getMetaDataEntry("name") if extruder.material != empty_material_container else None
|
||||
material_model = MaterialOutputModel(mat_guid, mat_type, mat_color, mat_brand, mat_name)
|
||||
|
||||
extruder_configuration.position = int(extruder.getMetaDataEntry("position"))
|
||||
extruder_configuration.material = material_model
|
||||
extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != self._empty_variant_container else None
|
||||
extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != empty_variant_container else None
|
||||
self._current_printer_configuration.extruderConfigurations.append(extruder_configuration)
|
||||
|
||||
self._current_printer_configuration.buildplateConfiguration = self._global_container_stack.getProperty("machine_buildplate_type", "value") if self._global_container_stack.variant != self._empty_variant_container else None
|
||||
# an empty build plate configuration from the network printer is presented as an empty string, so use "" for an
|
||||
# empty build plate.
|
||||
self._current_printer_configuration.buildplateConfiguration = self._global_container_stack.getProperty("machine_buildplate_type", "value") if self._global_container_stack.variant != empty_variant_container else ""
|
||||
self.currentConfigurationChanged.emit()
|
||||
|
||||
@pyqtSlot(QObject, result = bool)
|
||||
|
@ -258,14 +257,14 @@ class MachineManager(QObject):
|
|||
|
||||
# Global stack can have only a variant if it is a buildplate
|
||||
global_variant = self._global_container_stack.variant
|
||||
if global_variant != self._empty_variant_container:
|
||||
if global_variant != empty_variant_container:
|
||||
if global_variant.getMetaDataEntry("hardware_type") != "buildplate":
|
||||
self._global_container_stack.setVariant(self._empty_variant_container)
|
||||
self._global_container_stack.setVariant(empty_variant_container)
|
||||
|
||||
# set the global material to empty as we now use the extruder stack at all times - CURA-4482
|
||||
global_material = self._global_container_stack.material
|
||||
if global_material != self._empty_material_container:
|
||||
self._global_container_stack.setMaterial(self._empty_material_container)
|
||||
if global_material != empty_material_container:
|
||||
self._global_container_stack.setMaterial(empty_material_container)
|
||||
|
||||
# Listen for changes on all extruder stacks
|
||||
for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
|
||||
|
@ -367,6 +366,10 @@ class MachineManager(QObject):
|
|||
return
|
||||
|
||||
global_stack = containers[0]
|
||||
|
||||
# Make sure that the default machine actions for this machine have been added
|
||||
self._application.getMachineActionManager().addDefaultMachineActions(global_stack)
|
||||
|
||||
ExtruderManager.getInstance()._fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||
if not global_stack.isValid():
|
||||
# Mark global stack as invalid
|
||||
|
@ -593,7 +596,7 @@ class MachineManager(QObject):
|
|||
def globalVariantName(self) -> str:
|
||||
if self._global_container_stack:
|
||||
variant = self._global_container_stack.variant
|
||||
if variant and not isinstance(variant, type(self._empty_variant_container)):
|
||||
if variant and not isinstance(variant, type(empty_variant_container)):
|
||||
return variant.getName()
|
||||
return ""
|
||||
|
||||
|
@ -781,7 +784,7 @@ class MachineManager(QObject):
|
|||
if not stack.isEnabled:
|
||||
continue
|
||||
material_container = stack.material
|
||||
if material_container == self._empty_material_container:
|
||||
if material_container == empty_material_container:
|
||||
continue
|
||||
if material_container.getMetaDataEntry("buildplate_compatible"):
|
||||
buildplate_compatible = buildplate_compatible and material_container.getMetaDataEntry("buildplate_compatible")[self.activeVariantBuildplateName]
|
||||
|
@ -803,7 +806,7 @@ class MachineManager(QObject):
|
|||
extruder_stacks = self._global_container_stack.extruders.values()
|
||||
for stack in extruder_stacks:
|
||||
material_container = stack.material
|
||||
if material_container == self._empty_material_container:
|
||||
if material_container == empty_material_container:
|
||||
continue
|
||||
buildplate_compatible = material_container.getMetaDataEntry("buildplate_compatible")[self.activeVariantBuildplateName] if material_container.getMetaDataEntry("buildplate_compatible") else True
|
||||
buildplate_usable = material_container.getMetaDataEntry("buildplate_recommended")[self.activeVariantBuildplateName] if material_container.getMetaDataEntry("buildplate_recommended") else True
|
||||
|
@ -873,7 +876,7 @@ class MachineManager(QObject):
|
|||
extruder_manager = self._application.getExtruderManager()
|
||||
|
||||
definition_changes_container = self._global_container_stack.definitionChanges
|
||||
if not self._global_container_stack or definition_changes_container == self._empty_definition_changes_container:
|
||||
if not self._global_container_stack or definition_changes_container == empty_definition_changes_container:
|
||||
return
|
||||
|
||||
previous_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
|
||||
|
@ -1072,7 +1075,7 @@ class MachineManager(QObject):
|
|||
for stack in active_stacks:
|
||||
variant_container = stack.variant
|
||||
position = stack.getMetaDataEntry("position")
|
||||
if variant_container and variant_container != self._empty_variant_container:
|
||||
if variant_container and variant_container != empty_variant_container:
|
||||
result[position] = variant_container.getName()
|
||||
|
||||
return result
|
||||
|
@ -1086,11 +1089,11 @@ class MachineManager(QObject):
|
|||
return
|
||||
self._current_quality_group = None
|
||||
self._current_quality_changes_group = None
|
||||
self._global_container_stack.quality = self._empty_quality_container
|
||||
self._global_container_stack.qualityChanges = self._empty_quality_changes_container
|
||||
self._global_container_stack.quality = empty_quality_container
|
||||
self._global_container_stack.qualityChanges = empty_quality_changes_container
|
||||
for extruder in self._global_container_stack.extruders.values():
|
||||
extruder.quality = self._empty_quality_container
|
||||
extruder.qualityChanges = self._empty_quality_changes_container
|
||||
extruder.quality = empty_quality_container
|
||||
extruder.qualityChanges = empty_quality_changes_container
|
||||
|
||||
self.activeQualityGroupChanged.emit()
|
||||
self.activeQualityChangesGroupChanged.emit()
|
||||
|
@ -1115,13 +1118,13 @@ class MachineManager(QObject):
|
|||
# Set quality and quality_changes for the GlobalStack
|
||||
self._global_container_stack.quality = quality_group.node_for_global.getContainer()
|
||||
if empty_quality_changes:
|
||||
self._global_container_stack.qualityChanges = self._empty_quality_changes_container
|
||||
self._global_container_stack.qualityChanges = empty_quality_changes_container
|
||||
|
||||
# Set quality and quality_changes for each ExtruderStack
|
||||
for position, node in quality_group.nodes_for_extruders.items():
|
||||
self._global_container_stack.extruders[str(position)].quality = node.getContainer()
|
||||
if empty_quality_changes:
|
||||
self._global_container_stack.extruders[str(position)].qualityChanges = self._empty_quality_changes_container
|
||||
self._global_container_stack.extruders[str(position)].qualityChanges = empty_quality_changes_container
|
||||
|
||||
self.activeQualityGroupChanged.emit()
|
||||
self.activeQualityChangesGroupChanged.emit()
|
||||
|
@ -1147,8 +1150,8 @@ class MachineManager(QObject):
|
|||
if quality_group is None:
|
||||
self._fixQualityChangesGroupToNotSupported(quality_changes_group)
|
||||
|
||||
quality_changes_container = self._empty_quality_changes_container
|
||||
quality_container = self._empty_quality_container
|
||||
quality_changes_container = empty_quality_changes_container
|
||||
quality_container = empty_quality_container # type: Optional[InstanceContainer]
|
||||
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())
|
||||
if quality_group is not None and quality_group.node_for_global and quality_group.node_for_global.getContainer():
|
||||
|
@ -1163,8 +1166,8 @@ class MachineManager(QObject):
|
|||
if quality_group is not None:
|
||||
quality_node = quality_group.nodes_for_extruders.get(position)
|
||||
|
||||
quality_changes_container = self._empty_quality_changes_container
|
||||
quality_container = self._empty_quality_container
|
||||
quality_changes_container = empty_quality_changes_container
|
||||
quality_container = empty_quality_container
|
||||
if quality_changes_node and quality_changes_node.getContainer():
|
||||
quality_changes_container = cast(InstanceContainer, quality_changes_node.getContainer())
|
||||
if quality_node and quality_node.getContainer():
|
||||
|
@ -1198,7 +1201,7 @@ class MachineManager(QObject):
|
|||
self._global_container_stack.extruders[position].material = container_node.getContainer()
|
||||
root_material_id = container_node.getMetaDataEntry("base_file", None)
|
||||
else:
|
||||
self._global_container_stack.extruders[position].material = self._empty_material_container
|
||||
self._global_container_stack.extruders[position].material = empty_material_container
|
||||
root_material_id = None
|
||||
# The _current_root_material_id is used in the MaterialMenu to see which material is selected
|
||||
if root_material_id != self._current_root_material_id[position]:
|
||||
|
@ -1273,14 +1276,10 @@ class MachineManager(QObject):
|
|||
|
||||
current_material_base_name = extruder.material.getMetaDataEntry("base_file")
|
||||
current_nozzle_name = None
|
||||
if extruder.variant.getId() != self._empty_variant_container.getId():
|
||||
if extruder.variant.getId() != empty_variant_container.getId():
|
||||
current_nozzle_name = extruder.variant.getMetaDataEntry("name")
|
||||
|
||||
from UM.Settings.Interfaces import PropertyEvaluationContext
|
||||
from cura.Settings.CuraContainerStack import _ContainerIndexes
|
||||
context = PropertyEvaluationContext(extruder)
|
||||
context.context["evaluate_from_container_index"] = _ContainerIndexes.DefinitionChanges
|
||||
material_diameter = extruder.getProperty("material_diameter", "value", context)
|
||||
material_diameter = extruder.getCompatibleMaterialDiameter()
|
||||
candidate_materials = self._material_manager.getAvailableMaterials(
|
||||
self._global_container_stack.definition,
|
||||
current_nozzle_name,
|
||||
|
@ -1348,12 +1347,12 @@ class MachineManager(QObject):
|
|||
if variant_container_node:
|
||||
self._setVariantNode(position, variant_container_node)
|
||||
else:
|
||||
self._global_container_stack.extruders[position].variant = self._empty_variant_container
|
||||
self._global_container_stack.extruders[position].variant = empty_variant_container
|
||||
|
||||
if material_container_node:
|
||||
self._setMaterial(position, material_container_node)
|
||||
else:
|
||||
self._global_container_stack.extruders[position].material = self._empty_material_container
|
||||
self._global_container_stack.extruders[position].material = empty_material_container
|
||||
self.updateMaterialWithVariant(position)
|
||||
|
||||
if configuration.buildplateConfiguration is not None:
|
||||
|
@ -1361,9 +1360,9 @@ class MachineManager(QObject):
|
|||
if global_variant_container_node:
|
||||
self._setGlobalVariant(global_variant_container_node)
|
||||
else:
|
||||
self._global_container_stack.variant = self._empty_variant_container
|
||||
self._global_container_stack.variant = empty_variant_container
|
||||
else:
|
||||
self._global_container_stack.variant = self._empty_variant_container
|
||||
self._global_container_stack.variant = empty_variant_container
|
||||
self._updateQualityWithMaterial()
|
||||
|
||||
# See if we need to show the Discard or Keep changes screen
|
||||
|
@ -1415,7 +1414,7 @@ class MachineManager(QObject):
|
|||
position = str(position)
|
||||
extruder_stack = self._global_container_stack.extruders[position]
|
||||
nozzle_name = extruder_stack.variant.getName()
|
||||
material_diameter = extruder_stack.approximateMaterialDiameter
|
||||
material_diameter = extruder_stack.getApproximateMaterialDiameter()
|
||||
material_node = self._material_manager.getMaterialNode(machine_definition_id, nozzle_name, buildplate_name,
|
||||
material_diameter, root_material_id)
|
||||
self.setMaterial(position, material_node)
|
||||
|
@ -1481,7 +1480,7 @@ class MachineManager(QObject):
|
|||
# This is not changing the quality for the active machine !!!!!!!!
|
||||
global_stack.quality = quality_group.node_for_global.getContainer()
|
||||
for extruder_nr, extruder_stack in global_stack.extruders.items():
|
||||
quality_container = self._empty_quality_container
|
||||
quality_container = empty_quality_container
|
||||
if extruder_nr in quality_group.nodes_for_extruders:
|
||||
container = quality_group.nodes_for_extruders[extruder_nr].getContainer()
|
||||
quality_container = container if container is not None else quality_container
|
||||
|
@ -1525,7 +1524,7 @@ class MachineManager(QObject):
|
|||
|
||||
@pyqtProperty(str, notify = activeQualityGroupChanged)
|
||||
def activeQualityOrQualityChangesName(self) -> str:
|
||||
name = self._empty_quality_container.getName()
|
||||
name = empty_quality_container.getName()
|
||||
if self._current_quality_changes_group:
|
||||
name = self._current_quality_changes_group.name
|
||||
elif self._current_quality_group:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# 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 UM.FlameProfiler import pyqtSlot
|
||||
|
@ -20,13 +20,18 @@ from UM.Settings.SettingInstance import InstanceState
|
|||
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
from UM.Settings.SettingDefinition import SettingDefinition
|
||||
|
||||
|
||||
class SettingInheritanceManager(QObject):
|
||||
def __init__(self, parent = None):
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
self._global_container_stack = None
|
||||
self._settings_with_inheritance_warning = []
|
||||
self._active_container_stack = None
|
||||
self._global_container_stack = None # type: Optional[ContainerStack]
|
||||
self._settings_with_inheritance_warning = [] # type: List[str]
|
||||
self._active_container_stack = None # type: Optional[ExtruderStack]
|
||||
self._onGlobalContainerChanged()
|
||||
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderChanged)
|
||||
|
@ -41,7 +46,9 @@ class SettingInheritanceManager(QObject):
|
|||
|
||||
## Get the keys of all children settings with an override.
|
||||
@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)
|
||||
if not definitions:
|
||||
Logger.log("w", "Could not find definition for key [%s]", key)
|
||||
|
@ -53,9 +60,11 @@ class SettingInheritanceManager(QObject):
|
|||
return result
|
||||
|
||||
@pyqtSlot(str, str, result = "QStringList")
|
||||
def getOverridesForExtruder(self, key, extruder_index):
|
||||
result = []
|
||||
def getOverridesForExtruder(self, key: str, extruder_index: str) -> List[str]:
|
||||
if self._global_container_stack is None:
|
||||
return []
|
||||
|
||||
result = [] # type: List[str]
|
||||
extruder_stack = ExtruderManager.getInstance().getExtruderStack(extruder_index)
|
||||
if not extruder_stack:
|
||||
Logger.log("w", "Unable to find extruder for current machine with index %s", extruder_index)
|
||||
|
@ -73,16 +82,16 @@ class SettingInheritanceManager(QObject):
|
|||
return result
|
||||
|
||||
@pyqtSlot(str)
|
||||
def manualRemoveOverride(self, key):
|
||||
def manualRemoveOverride(self, key: str) -> None:
|
||||
if key in self._settings_with_inheritance_warning:
|
||||
self._settings_with_inheritance_warning.remove(key)
|
||||
self.settingsWithIntheritanceChanged.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def forceUpdate(self):
|
||||
def forceUpdate(self) -> None:
|
||||
self._update()
|
||||
|
||||
def _onActiveExtruderChanged(self):
|
||||
def _onActiveExtruderChanged(self) -> None:
|
||||
new_active_stack = ExtruderManager.getInstance().getActiveExtruderStack()
|
||||
if not new_active_stack:
|
||||
self._active_container_stack = None
|
||||
|
@ -94,13 +103,14 @@ class SettingInheritanceManager(QObject):
|
|||
self._active_container_stack.containersChanged.disconnect(self._onContainersChanged)
|
||||
|
||||
self._active_container_stack = new_active_stack
|
||||
self._active_container_stack.propertyChanged.connect(self._onPropertyChanged)
|
||||
self._active_container_stack.containersChanged.connect(self._onContainersChanged)
|
||||
if self._active_container_stack is not None:
|
||||
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.
|
||||
|
||||
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:
|
||||
definitions = self._global_container_stack.definition.findDefinitions(key = key)
|
||||
definitions = self._global_container_stack.definition.findDefinitions(key = key) # type: List["SettingDefinition"]
|
||||
if not definitions:
|
||||
return
|
||||
|
||||
|
@ -139,7 +149,7 @@ class SettingInheritanceManager(QObject):
|
|||
if settings_with_inheritance_warning_changed:
|
||||
self.settingsWithIntheritanceChanged.emit()
|
||||
|
||||
def _recursiveCheck(self, definition):
|
||||
def _recursiveCheck(self, definition: "SettingDefinition") -> bool:
|
||||
for child in definition.children:
|
||||
if child.key in self._settings_with_inheritance_warning:
|
||||
return True
|
||||
|
@ -149,7 +159,7 @@ class SettingInheritanceManager(QObject):
|
|||
return False
|
||||
|
||||
@pyqtProperty("QVariantList", notify = settingsWithIntheritanceChanged)
|
||||
def settingsWithInheritanceWarning(self):
|
||||
def settingsWithInheritanceWarning(self) -> List[str]:
|
||||
return self._settings_with_inheritance_warning
|
||||
|
||||
## Check if a setting has an inheritance function that is overwritten
|
||||
|
@ -157,9 +167,14 @@ class SettingInheritanceManager(QObject):
|
|||
has_setting_function = False
|
||||
if not stack:
|
||||
stack = self._active_container_stack
|
||||
if not stack: #No active container stack yet!
|
||||
if not stack: # No active container stack yet!
|
||||
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.
|
||||
has_user_state = stack.getProperty(key, "state") == InstanceState.User
|
||||
|
@ -190,8 +205,8 @@ class SettingInheritanceManager(QObject):
|
|||
has_setting_function = isinstance(value, SettingFunction)
|
||||
if has_setting_function:
|
||||
for setting_key in value.getUsedSettingKeys():
|
||||
if setting_key in self._active_container_stack.getAllKeys():
|
||||
break # We found an actual setting. So has_setting_function can remain true
|
||||
if setting_key in all_keys:
|
||||
break # We found an actual setting. So has_setting_function can remain true
|
||||
else:
|
||||
# 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.
|
||||
|
@ -205,7 +220,7 @@ class SettingInheritanceManager(QObject):
|
|||
break # There is a setting function somewhere, stop looking deeper.
|
||||
return has_setting_function and has_non_function_value
|
||||
|
||||
def _update(self):
|
||||
def _update(self) -> None:
|
||||
self._settings_with_inheritance_warning = [] # Reset previous data.
|
||||
|
||||
# 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.
|
||||
self.settingsWithIntheritanceChanged.emit()
|
||||
|
||||
def _onGlobalContainerChanged(self):
|
||||
def _onGlobalContainerChanged(self) -> None:
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
|
||||
self._global_container_stack.containersChanged.disconnect(self._onContainersChanged)
|
||||
|
|
90
cura/Settings/SettingVisibilityPreset.py
Normal file
90
cura/Settings/SettingVisibilityPreset.py
Normal file
|
@ -0,0 +1,90 @@
|
|||
import os
|
||||
import urllib.parse
|
||||
from configparser import ConfigParser
|
||||
from typing import List
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.MimeTypeDatabase import MimeTypeDatabase
|
||||
|
||||
|
||||
class SettingVisibilityPreset(QObject):
|
||||
onSettingsChanged = pyqtSignal()
|
||||
onNameChanged = pyqtSignal()
|
||||
onWeightChanged = pyqtSignal()
|
||||
onIdChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, preset_id: str = "", name: str = "", weight: int = 0, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._settings = [] # type: List[str]
|
||||
self._id = preset_id
|
||||
self._weight = weight
|
||||
self._name = name
|
||||
|
||||
@pyqtProperty("QStringList", notify = onSettingsChanged)
|
||||
def settings(self) -> List[str]:
|
||||
return self._settings
|
||||
|
||||
@pyqtProperty(str, notify = onIdChanged)
|
||||
def presetId(self) -> str:
|
||||
return self._id
|
||||
|
||||
@pyqtProperty(int, notify = onWeightChanged)
|
||||
def weight(self) -> int:
|
||||
return self._weight
|
||||
|
||||
@pyqtProperty(str, notify = onNameChanged)
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
def setName(self, name: str) -> None:
|
||||
if name != self._name:
|
||||
self._name = name
|
||||
self.onNameChanged.emit()
|
||||
|
||||
def setId(self, id: str) -> None:
|
||||
if id != self._id:
|
||||
self._id = id
|
||||
self.onIdChanged.emit()
|
||||
|
||||
def setWeight(self, weight: int) -> None:
|
||||
if weight != self._weight:
|
||||
self._weight = weight
|
||||
self.onWeightChanged.emit()
|
||||
|
||||
def setSettings(self, settings: List[str]) -> None:
|
||||
if set(settings) != set(self._settings):
|
||||
self._settings = list(set(settings)) # filter out non unique
|
||||
self.onSettingsChanged.emit()
|
||||
|
||||
# Load a preset from file. We expect a file that can be parsed by means of the config parser.
|
||||
# The sections indicate the categories and the parameters placed in it (which don't need values) are the settings
|
||||
# that should be considered visible.
|
||||
def loadFromFile(self, file_path: str) -> None:
|
||||
mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
|
||||
|
||||
item_id = urllib.parse.unquote_plus(mime_type.stripExtension(os.path.basename(file_path)))
|
||||
if not os.path.isfile(file_path):
|
||||
Logger.log("e", "[%s] is not a file", file_path)
|
||||
return None
|
||||
|
||||
parser = ConfigParser(interpolation = None, allow_no_value = True) # Accept options without any value,
|
||||
|
||||
parser.read([file_path])
|
||||
if not parser.has_option("general", "name") or not parser.has_option("general", "weight"):
|
||||
return None
|
||||
|
||||
settings = [] # type: List[str]
|
||||
for section in parser.sections():
|
||||
if section == "general":
|
||||
continue
|
||||
|
||||
settings.append(section)
|
||||
for option in parser[section].keys():
|
||||
settings.append(option)
|
||||
self.setSettings(settings)
|
||||
self.setId(item_id)
|
||||
self.setName(parser["general"]["name"])
|
||||
self.setWeight(int(parser["general"]["weight"]))
|
||||
|
|
@ -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 UM.Application import Application
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
|
||||
|
||||
from collections import OrderedDict
|
||||
import os
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
|
||||
class UserChangesModel(ListModel):
|
||||
|
@ -38,9 +40,13 @@ class UserChangesModel(ListModel):
|
|||
self._update()
|
||||
|
||||
def _update(self):
|
||||
application = Application.getInstance()
|
||||
machine_manager = application.getMachineManager()
|
||||
cura_formula_functions = application.getCuraFormulaFunctions()
|
||||
|
||||
item_dict = OrderedDict()
|
||||
item_list = []
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
global_stack = machine_manager.activeMachine
|
||||
if not global_stack:
|
||||
return
|
||||
|
||||
|
@ -71,13 +77,7 @@ class UserChangesModel(ListModel):
|
|||
|
||||
# Override "getExtruderValue" with "getDefaultExtruderValue" so we can get the default values
|
||||
user_changes = containers.pop(0)
|
||||
default_value_resolve_context = PropertyEvaluationContext(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
|
||||
}
|
||||
default_value_resolve_context = cura_formula_functions.createContextForDefaultValueEvaluation(stack)
|
||||
|
||||
for setting_key in user_changes.getAllKeys():
|
||||
original_value = None
|
||||
|
|
156
installer.nsi
156
installer.nsi
|
@ -1,156 +0,0 @@
|
|||
!ifndef VERSION
|
||||
!define VERSION '15.09.80'
|
||||
!endif
|
||||
|
||||
; The name of the installer
|
||||
Name "Cura ${VERSION}"
|
||||
|
||||
; The file to write
|
||||
OutFile "Cura_${VERSION}.exe"
|
||||
|
||||
; The default installation directory
|
||||
InstallDir $PROGRAMFILES\Cura_${VERSION}
|
||||
|
||||
; Registry key to check for directory (so if you install again, it will
|
||||
; overwrite the old one automatically)
|
||||
InstallDirRegKey HKLM "Software\Cura_${VERSION}" "Install_Dir"
|
||||
|
||||
; Request application privileges for Windows Vista
|
||||
RequestExecutionLevel admin
|
||||
|
||||
; Set the LZMA compressor to reduce size.
|
||||
SetCompressor /SOLID lzma
|
||||
;--------------------------------
|
||||
|
||||
!include "MUI2.nsh"
|
||||
!include "Library.nsh"
|
||||
|
||||
; !define MUI_ICON "dist/resources/cura.ico"
|
||||
!define MUI_BGCOLOR FFFFFF
|
||||
|
||||
; Directory page defines
|
||||
!define MUI_DIRECTORYPAGE_VERIFYONLEAVE
|
||||
|
||||
; Header
|
||||
; Don't show the component description box
|
||||
!define MUI_COMPONENTSPAGE_NODESC
|
||||
|
||||
;Do not leave (Un)Installer page automaticly
|
||||
!define MUI_FINISHPAGE_NOAUTOCLOSE
|
||||
!define MUI_UNFINISHPAGE_NOAUTOCLOSE
|
||||
|
||||
;Run Cura after installing
|
||||
!define MUI_FINISHPAGE_RUN
|
||||
!define MUI_FINISHPAGE_RUN_TEXT "Start Cura ${VERSION}"
|
||||
!define MUI_FINISHPAGE_RUN_FUNCTION "LaunchLink"
|
||||
|
||||
;Add an option to show release notes
|
||||
!define MUI_FINISHPAGE_SHOWREADME "$INSTDIR\plugins\ChangeLogPlugin\changelog.txt"
|
||||
|
||||
; Pages
|
||||
;!insertmacro MUI_PAGE_WELCOME
|
||||
!insertmacro MUI_PAGE_DIRECTORY
|
||||
!insertmacro MUI_PAGE_COMPONENTS
|
||||
!insertmacro MUI_PAGE_INSTFILES
|
||||
!insertmacro MUI_PAGE_FINISH
|
||||
!insertmacro MUI_UNPAGE_CONFIRM
|
||||
!insertmacro MUI_UNPAGE_INSTFILES
|
||||
!insertmacro MUI_UNPAGE_FINISH
|
||||
|
||||
; Languages
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
; Reserve Files
|
||||
!insertmacro MUI_RESERVEFILE_LANGDLL
|
||||
ReserveFile '${NSISDIR}\Plugins\InstallOptions.dll'
|
||||
|
||||
;--------------------------------
|
||||
|
||||
; The stuff to install
|
||||
Section "Cura ${VERSION}"
|
||||
|
||||
SectionIn RO
|
||||
|
||||
; Set output path to the installation directory.
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
; Put file there
|
||||
File /r "dist\"
|
||||
|
||||
; Write the installation path into the registry
|
||||
WriteRegStr HKLM "SOFTWARE\Cura_${VERSION}" "Install_Dir" "$INSTDIR"
|
||||
|
||||
; Write the uninstall keys for Windows
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}" "DisplayName" "Cura ${VERSION}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}" "UninstallString" '"$INSTDIR\uninstall.exe"'
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}" "NoModify" 1
|
||||
WriteRegDWORD HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}" "NoRepair" 1
|
||||
WriteUninstaller "uninstall.exe"
|
||||
|
||||
; Write start menu entries for all users
|
||||
SetShellVarContext all
|
||||
|
||||
CreateDirectory "$SMPROGRAMS\Cura ${VERSION}"
|
||||
CreateShortCut "$SMPROGRAMS\Cura ${VERSION}\Uninstall Cura ${VERSION}.lnk" "$INSTDIR\uninstall.exe" "" "$INSTDIR\uninstall.exe" 0
|
||||
CreateShortCut "$SMPROGRAMS\Cura ${VERSION}\Cura ${VERSION}.lnk" "$INSTDIR\Cura.exe" '' "$INSTDIR\Cura.exe" 0
|
||||
|
||||
SectionEnd
|
||||
|
||||
Function LaunchLink
|
||||
; Write start menu entries for all users
|
||||
SetShellVarContext all
|
||||
Exec '"$WINDIR\explorer.exe" "$SMPROGRAMS\Cura ${VERSION}\Cura ${VERSION}.lnk"'
|
||||
FunctionEnd
|
||||
|
||||
Section "Install Visual Studio 2010 Redistributable"
|
||||
SetOutPath "$INSTDIR"
|
||||
File "vcredist_2010_20110908_x86.exe"
|
||||
|
||||
IfSilent +2
|
||||
ExecWait '"$INSTDIR\vcredist_2010_20110908_x86.exe" /q /norestart'
|
||||
|
||||
SectionEnd
|
||||
|
||||
Section "Install Arduino Drivers"
|
||||
; Set output path to the driver directory.
|
||||
SetOutPath "$INSTDIR\drivers\"
|
||||
File /r "drivers\"
|
||||
|
||||
${If} ${RunningX64}
|
||||
IfSilent +2
|
||||
ExecWait '"$INSTDIR\drivers\dpinst64.exe" /lm'
|
||||
${Else}
|
||||
IfSilent +2
|
||||
ExecWait '"$INSTDIR\drivers\dpinst32.exe" /lm'
|
||||
${EndIf}
|
||||
SectionEnd
|
||||
|
||||
Section "Open STL files with Cura"
|
||||
${registerExtension} "$INSTDIR\Cura.exe" ".stl" "STL_File"
|
||||
SectionEnd
|
||||
|
||||
Section /o "Open OBJ files with Cura"
|
||||
WriteRegStr HKCR .obj "" "Cura OBJ model file"
|
||||
DeleteRegValue HKCR .obj "Content Type"
|
||||
WriteRegStr HKCR "Cura OBJ model file\DefaultIcon" "" "$INSTDIR\Cura.exe,0"
|
||||
WriteRegStr HKCR "Cura OBJ model file\shell" "" "open"
|
||||
WriteRegStr HKCR "Cura OBJ model file\shell\open\command" "" '"$INSTDIR\Cura.exe" "%1"'
|
||||
SectionEnd
|
||||
|
||||
;--------------------------------
|
||||
|
||||
; Uninstaller
|
||||
|
||||
Section "Uninstall"
|
||||
|
||||
; Remove registry keys
|
||||
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\Cura_${VERSION}"
|
||||
DeleteRegKey HKLM "SOFTWARE\Cura_${VERSION}"
|
||||
|
||||
; Write start menu entries for all users
|
||||
SetShellVarContext all
|
||||
; Remove directories used
|
||||
RMDir /r "$SMPROGRAMS\Cura ${VERSION}"
|
||||
RMDir /r "$INSTDIR"
|
||||
|
||||
SectionEnd
|
|
@ -298,7 +298,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
values = parser["values"] if parser.has_section("values") else dict()
|
||||
num_settings_overriden_by_quality_changes += len(values)
|
||||
# Check if quality changes already exists.
|
||||
quality_changes = self._container_registry.findInstanceContainers(id = container_id)
|
||||
quality_changes = self._container_registry.findInstanceContainers(name = custom_quality_name,
|
||||
type = "quality_changes")
|
||||
if quality_changes:
|
||||
containers_found_dict["quality_changes"] = True
|
||||
# Check if there really is a conflict by comparing the values
|
||||
|
@ -926,7 +927,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
build_plate_id = global_stack.variant.getId()
|
||||
|
||||
# get material diameter of this extruder
|
||||
machine_material_diameter = extruder_stack.materialDiameter
|
||||
machine_material_diameter = extruder_stack.getCompatibleMaterialDiameter()
|
||||
material_node = material_manager.getMaterialNode(global_stack.definition.getId(),
|
||||
extruder_stack.variant.getName(),
|
||||
build_plate_id,
|
||||
|
@ -1012,7 +1013,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
|
||||
## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data.
|
||||
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)
|
||||
|
||||
container_ids = []
|
||||
|
@ -1033,7 +1034,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
|||
return container_ids
|
||||
|
||||
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)
|
||||
return parser["general"].get("name", "")
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ from . import ThreeMFWorkspaceWriter
|
|||
from UM.i18n import i18nCatalog
|
||||
from UM.Platform import Platform
|
||||
|
||||
i18n_catalog = i18nCatalog("uranium")
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
workspace_extension = "3mf"
|
||||
|
|
|
@ -1,3 +1,78 @@
|
|||
[3.6.0]
|
||||
*Gyroid infill
|
||||
New infill pattern with enhanced strength properties. Gyroid infill is one of the strongest infill types for a given weight, has isotropic properties, and prints relatively fast with reduced material use and a fully connected part interior. Note: Slicing time can increase up to 40 seconds or more, depending on the model. Contributed by smartavionics.
|
||||
|
||||
*Support brim
|
||||
New setting that integrates the first layer of support material with the brim’s geometry. This significantly improves adhesion when printing with support material. Contributed by BagelOrb.
|
||||
|
||||
*Cooling fan number
|
||||
It is now possible to specify the cooling fan to use if your printer has multiple fans. This is implemented under Machine settings in the Extruder tab. Contributed by smartavionics.
|
||||
|
||||
*Settings refactor
|
||||
The CuraEngine has been refactored to create a more testable, future-proof way of storing and representing settings. This makes slicing faster, and future development easier.
|
||||
|
||||
*Print core CC 0.6
|
||||
The new print core CC 0.6 is selectable when the Ultimaker S5 profile is active. This print core is optimized for use with abrasive materials and composites.
|
||||
|
||||
*File name and layer display
|
||||
Added M117 commands to GCODE to give real-time information about the print job file name and layer number shown on the printer’s display when printing via USB. Contributed by adecastilho.
|
||||
|
||||
*Firmware checker/Ultimaker S5
|
||||
The update checker code has been improved and tested for more reliable firmware update notifications in Ultimaker Cura. The Ultimaker S5 is now included.
|
||||
|
||||
*Fullscreen mode shortcuts
|
||||
Fullscreen mode can be toggled using the View menu or with the keyboard shortcuts: Command + Control + F (macOS), or F11 (Windows and Linux). Contributed by KangDroid.
|
||||
|
||||
*Configuration error message
|
||||
In previous versions, Ultimaker Cura would display an error dialog explaining when something happened to user configuration files, including the option to reset to factory defaults. This would not warn about losing the current printer and print profile settings, so this information has been added.
|
||||
|
||||
*Rename Toolbox to Marketplace
|
||||
The entry points to the Toolbox are now renamed to Marketplace.
|
||||
|
||||
*Materials in the Marketplace
|
||||
A new tab has been added to the Marketplace that includes downloadable material profiles, to quickly and easily prepare models for a range of third-party materials.
|
||||
|
||||
*New third-party definitions
|
||||
New profiles added for Anycube 4MAx and Tizyx K25. Contributed by jscurtu and ValentinPitre respectively.
|
||||
|
||||
*Improved definitions for Ender-3
|
||||
The Ender-3 build plate size has been adjusted to the correct size of 235 x 235 mm, corrected the start-up sequence, and the printhead position has been adjusted when prints are purged or completed. Contributed by stelgenhof.
|
||||
|
||||
*Add mesh names to slicing message
|
||||
Added comment generation to indicate which mesh the GCODE after this comment is constructing. Contributed by paukstelis.
|
||||
|
||||
*Bug fixes
|
||||
- The active material is highlighted in Ultimaker Cura’s material manager list. This behavior is now consistent with the profile and machine manager.
|
||||
- The option to use 1.75 mm diameter filament with third-party 3D printers is now fixed and does not revert back to 2.85 mm. This fix also applies the appropriate a Z-axis speed change for 1.75 mm filament printers. Contributed by kaleidoscopeit.
|
||||
- A fix was created to handle OSX version 10.10, but due to the QT upgrade, users with older versions won’t be able to run Ultimaker Cura on their system without a system update. This applies to OSX version 10.09 and 10.08.
|
||||
- Fixed a memory leak when leaving the “Monitor” page open.
|
||||
- Added performance improvements to the PolygonConnector to efficiently connect polygons that are close to each other. This also reduces the chances of the print head collide with previously printed things. Contributed by BagelOrb.
|
||||
- Fixed a bug where the GCODE reader didn’t show retractions.
|
||||
- Changes the USBPrinting update thread to prevent flooding the printer with M105 temperature update requests. Contributed by fieldOfView.
|
||||
- Fix the behavior of the "manage visible settings" button, when pressing the "cog" icon of a particular category. Contributed by fieldOfView.
|
||||
- Add a new post processing script that pauses the print at a certain height that works with RepRap printers. Contributed by Kriechi.
|
||||
- Fix updates to the print monitor temperatures while preheating. Contributed by fieldOfView.
|
||||
- Fixed a bug where material cost is not shown unless weight is changed.
|
||||
- Fixed bugs crashing the CuraEngine when TreeSupport is enabled.
|
||||
- Fixed a bug where Ultimaker Cura would upload the wrong firmware after switching printers in the UI.
|
||||
- Fixed a bug where the layer view was missing if the first layer was empty.
|
||||
- Fixed a bug where erroneous combing movements were taking place.
|
||||
- Fixed a bug where the initial layer temperature is set correctly for the first object but then never again.
|
||||
- Fixed a bug where clicking the fx icon didn’t respond.
|
||||
|
||||
[3.5.1]
|
||||
*Bug fixes
|
||||
- Fixed M104 temperature commands giving inaccurate results.
|
||||
- Fixed crashes caused by loading files from USB stick on Windows platforms.
|
||||
- Fixed several issues with configuration files that missed the type in the metadata.
|
||||
- Fixed issues caused by skin/infill optimization.
|
||||
- Fixed several issues related to missing definition files for third-party printers.
|
||||
- Fixed an issue where combing path generation cuts corners.
|
||||
- Fixed a range of crashes caused by lock files.
|
||||
- Fixed issues with remembering save directories on MacOS.
|
||||
- Fixed an issue where CuraEngine uses incorrect material settings.
|
||||
- Fixed an issue where some support layers don't have support infill.
|
||||
|
||||
[3.5.0]
|
||||
*Monitor page
|
||||
The monitor page of Ultimaker Cura has been remodeled for better consistency with the Cura Connect ‘Print jobs’ interface. This means less switching between interfaces, and more control from within Ultimaker Cura.
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
|
||||
from . import ChangeLog
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
|
|
@ -29,6 +29,7 @@ message Object
|
|||
bytes normals = 3; //An array of 3 floats.
|
||||
bytes indices = 4; //An array of ints.
|
||||
repeated Setting settings = 5; // Setting override per object, overruling the global settings.
|
||||
string name = 6;
|
||||
}
|
||||
|
||||
message Progress
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import gc
|
||||
import sys
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Application import Application
|
||||
|
@ -95,23 +96,35 @@ class ProcessSlicedLayersJob(Job):
|
|||
layer_count = len(self._layers)
|
||||
|
||||
# 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
|
||||
# instead simply offset all other layers so the lowest layer is always 0. It could happens that
|
||||
# the first raft layer has value -8 but there are just 4 raft (negative) layers.
|
||||
min_layer_number = 0
|
||||
# simply offset all other layers so the lowest layer is always 0. It could happens that the first
|
||||
# raft layer has value -8 but there are just 4 raft (negative) layers.
|
||||
min_layer_number = sys.maxsize
|
||||
negative_layers = 0
|
||||
for layer in self._layers:
|
||||
if layer.id < min_layer_number:
|
||||
min_layer_number = layer.id
|
||||
if layer.id < 0:
|
||||
negative_layers += 1
|
||||
if layer.repeatedMessageCount("path_segment") > 0:
|
||||
if layer.id < min_layer_number:
|
||||
min_layer_number = layer.id
|
||||
if layer.id < 0:
|
||||
negative_layers += 1
|
||||
|
||||
current_layer = 0
|
||||
|
||||
for layer in self._layers:
|
||||
# Negative layers are offset by the minimum layer number, but the positive layers are just
|
||||
# offset by the number of negative layers so there is no layer gap between raft and model
|
||||
abs_layer_number = layer.id + abs(min_layer_number) if layer.id < 0 else layer.id + negative_layers
|
||||
# If the layer is below the minimum, it means that there is no data, so that we don't create a layer
|
||||
# data. However, if there are empty layers in between, we compute them.
|
||||
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)
|
||||
this_layer = layer_data.getLayer(abs_layer_number)
|
||||
|
|
|
@ -41,11 +41,15 @@ class StartJobResult(IntEnum):
|
|||
|
||||
## Formatter class that handles token expansion in start/end gcode
|
||||
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),
|
||||
# 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(",")]
|
||||
if len(key_fragments) == 2:
|
||||
|
@ -247,7 +251,10 @@ class StartSliceJob(Job):
|
|||
self._buildGlobalInheritsStackMessage(stack)
|
||||
|
||||
# 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)
|
||||
|
||||
for group in filtered_object_groups:
|
||||
|
@ -270,7 +277,7 @@ class StartSliceJob(Job):
|
|||
|
||||
obj = group_message.addRepeatedMessage("objects")
|
||||
obj.id = id(object)
|
||||
|
||||
obj.name = object.getName()
|
||||
indices = mesh_data.getIndices()
|
||||
if indices is not None:
|
||||
flat_verts = numpy.take(verts, indices.flatten(), axis=0)
|
||||
|
@ -339,7 +346,7 @@ class StartSliceJob(Job):
|
|||
|
||||
try:
|
||||
# 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["default_extruder_nr"] = default_extruder_nr
|
||||
return str(fmt.format(value, **settings))
|
||||
|
|
|
@ -50,7 +50,7 @@ class CuraProfileReader(ProfileReader):
|
|||
# \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.
|
||||
def _upgradeProfile(self, serialized, profile_id):
|
||||
parser = configparser.ConfigParser(interpolation=None)
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
parser.read_string(serialized)
|
||||
|
||||
if "general" not in parser:
|
||||
|
|
|
@ -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.
|
||||
|
||||
import os
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
|
||||
from typing import Set
|
||||
|
||||
from UM.Extension import Extension
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
|
@ -13,6 +16,7 @@ from UM.Settings.ContainerRegistry import ContainerRegistry
|
|||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
from .FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob
|
||||
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
|
||||
|
||||
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
|
||||
# to change it to work for other applications.
|
||||
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__()
|
||||
|
||||
# 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
|
||||
# 'check for updates' option
|
||||
# "check for updates" option
|
||||
Application.getInstance().getPreferences().addPreference("info/automatic_update_check", True)
|
||||
if Application.getInstance().getPreferences().getValue("info/automatic_update_check"):
|
||||
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerAdded)
|
||||
|
||||
self._download_url = 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.
|
||||
def _onActionTriggered(self, message, action):
|
||||
if action == "download":
|
||||
if self._download_url is not None:
|
||||
QDesktopServices.openUrl(QUrl(self._download_url))
|
||||
|
||||
def _onSetDownloadUrl(self, download_url):
|
||||
self._download_url = download_url
|
||||
if action == FirmwareUpdateCheckerMessage.STR_ACTION_DOWNLOAD:
|
||||
machine_id = message.getMachineId()
|
||||
download_url = message.getDownloadUrl()
|
||||
if download_url is not None:
|
||||
if QDesktopServices.openUrl(QUrl(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):
|
||||
# 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.
|
||||
# This is used when checking for a new firmware version at startup.
|
||||
def checkFirmwareVersion(self, container = None, silent = False):
|
||||
# Do not run multiple check jobs in parallel
|
||||
if self._check_job is not None:
|
||||
Logger.log("i", "A firmware update check is already running, do nothing.")
|
||||
container_name = container.definition.getName()
|
||||
if container_name in self._checked_printer_names:
|
||||
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
|
||||
|
||||
self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent, url = self.JEDI_VERSION_URL,
|
||||
callback = self._onActionTriggered,
|
||||
set_download_url_callback = self._onSetDownloadUrl)
|
||||
self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent,
|
||||
machine_name = container_name, metadata = metadata,
|
||||
callback = self._onActionTriggered)
|
||||
self._check_job.start()
|
||||
self._check_job.finished.connect(self._onJobFinished)
|
||||
|
|
|
@ -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.
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Message import Message
|
||||
from UM.Logger import Logger
|
||||
from UM.Job import Job
|
||||
from UM.Version import Version
|
||||
|
||||
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
|
||||
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.
|
||||
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__()
|
||||
self._container = container
|
||||
self.silent = silent
|
||||
self._url = url
|
||||
self._callback = callback
|
||||
self._set_download_url_callback = set_download_url_callback
|
||||
|
||||
def run(self):
|
||||
if not self._url:
|
||||
Logger.log("e", "Can not check for a new release. URL not set!")
|
||||
return
|
||||
self._machine_name = machine_name
|
||||
self._metadata = metadata
|
||||
self._lookups = None # type:Optional[FirmwareUpdateCheckerLookup]
|
||||
self._headers = {} # type:Dict[str, str] # Don't set headers yet.
|
||||
|
||||
def getUrlResponse(self, url: str) -> str:
|
||||
result = self.STRING_ZERO_VERSION
|
||||
|
||||
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()
|
||||
headers = {"User-Agent": "%s - %s" % (application_name, Application.getInstance().getVersion())}
|
||||
request = urllib.request.Request(self._url, headers = headers)
|
||||
current_version_file = urllib.request.urlopen(request)
|
||||
reader = codecs.getreader("utf-8")
|
||||
application_version = Application.getInstance().getVersion()
|
||||
self._headers = {"User-Agent": "%s - %s" % (application_name, application_version)}
|
||||
|
||||
# get machine name from the definition container
|
||||
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
|
||||
# Now we just do that if the active printer is Ultimaker 3 or Ultimaker 3 Extended or any
|
||||
# other Ultimaker 3 that will come in the future
|
||||
if len(machine_name_parts) >= 2 and machine_name_parts[:2] == ["ultimaker", "3"]:
|
||||
Logger.log("i", "You have a UM3 in printer list. Let's check the firmware!")
|
||||
machine_id = self._lookups.getMachineId()
|
||||
if machine_id is not None:
|
||||
Logger.log("i", "You have a(n) {0} in the printer list. Let's check the firmware!".format(machine_name))
|
||||
|
||||
# Nothing to parse, just get the string
|
||||
# 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()
|
||||
current_version = self.getCurrentVersion()
|
||||
|
||||
# If it is the first time the version is checked, the checked_version is ''
|
||||
checked_version = Application.getInstance().getPreferences().getValue("info/latest_checked_firmware")
|
||||
# If it is the first time the version is checked, the checked_version is ""
|
||||
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
|
||||
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)
|
||||
|
||||
# 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.
|
||||
if (checked_version != "") and (checked_version != current_version):
|
||||
Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE")
|
||||
|
||||
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 = FirmwareUpdateCheckerMessage(machine_id, machine_name, self._lookups.getRedirectUserUrl())
|
||||
message.actionTriggered.connect(self._callback)
|
||||
message.show()
|
||||
else:
|
||||
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(machine_name))
|
||||
|
||||
except Exception as e:
|
||||
Logger.log("w", "Failed to check for new version: %s", e)
|
||||
|
|
35
plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerLookup.py
Normal file
35
plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerLookup.py
Normal 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
|
|
@ -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
|
|
@ -1,12 +1,8 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
from . import FirmwareUpdateChecker
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
|
69
plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py
Normal file
69
plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py
Normal 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
|
191
plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.qml
Normal file
191
plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.qml
Normal 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;
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
12
plugins/FirmwareUpdater/__init__.py
Normal file
12
plugins/FirmwareUpdater/__init__.py
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from . import FirmwareUpdaterMachineAction
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
||||
def register(app):
|
||||
return { "machine_action": [
|
||||
FirmwareUpdaterMachineAction.FirmwareUpdaterMachineAction()
|
||||
]}
|
8
plugins/FirmwareUpdater/plugin.json
Normal file
8
plugins/FirmwareUpdater/plugin.json
Normal 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"
|
||||
}
|
|
@ -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._current_layer_thickness = 0.2 # 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)
|
||||
|
||||
|
@ -182,6 +183,7 @@ class FlavorParser:
|
|||
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]:
|
||||
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:
|
||||
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
|
||||
|
@ -191,6 +193,8 @@ class FlavorParser:
|
|||
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._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])
|
||||
else:
|
||||
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)
|
||||
|
@ -227,6 +231,9 @@ class FlavorParser:
|
|||
# Sometimes a G92 E0 is introduced in the middle of the GCode so we need to keep those offsets for calculate the line_width
|
||||
self._extrusion_length_offset[self._extruder_number] += position.e[self._extruder_number] - params.e
|
||||
position.e[self._extruder_number] = params.e
|
||||
self._previous_extrusion_value = params.e
|
||||
else:
|
||||
self._previous_extrusion_value = 0.0
|
||||
return self._position(
|
||||
params.x if params.x is not None else position.x,
|
||||
params.y if params.y is not None else position.y,
|
||||
|
@ -286,7 +293,7 @@ class FlavorParser:
|
|||
self._cancelled = False
|
||||
# We obtain the filament diameter from the selected extruder to calculate line widths
|
||||
global_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
|
||||
|
||||
if not global_stack:
|
||||
return None
|
||||
|
||||
|
@ -329,6 +336,7 @@ class FlavorParser:
|
|||
min_layer_number = 0
|
||||
negative_layers = 0
|
||||
previous_layer = 0
|
||||
self._previous_extrusion_value = 0.0
|
||||
|
||||
for line in stream.split("\n"):
|
||||
if self._cancelled:
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from . import FlavorParser
|
||||
|
||||
# This parser is intented for interpret the RepRap Firmware flavor
|
||||
## This parser is intended to interpret the RepRap Firmware g-code flavor.
|
||||
class RepRapFlavorParser(FlavorParser.FlavorParser):
|
||||
|
||||
def __init__(self):
|
||||
|
|
|
@ -35,7 +35,7 @@ UM.Dialog
|
|||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: catalog.i18nc("@action:label","Height (mm)")
|
||||
text: catalog.i18nc("@action:label", "Height (mm)")
|
||||
width: 150 * screenScaleFactor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
@ -58,7 +58,7 @@ UM.Dialog
|
|||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: catalog.i18nc("@action:label","Base (mm)")
|
||||
text: catalog.i18nc("@action:label", "Base (mm)")
|
||||
width: 150 * screenScaleFactor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
@ -81,7 +81,7 @@ UM.Dialog
|
|||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: catalog.i18nc("@action:label","Width (mm)")
|
||||
text: catalog.i18nc("@action:label", "Width (mm)")
|
||||
width: 150 * screenScaleFactor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
@ -105,7 +105,7 @@ UM.Dialog
|
|||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: catalog.i18nc("@action:label","Depth (mm)")
|
||||
text: catalog.i18nc("@action:label", "Depth (mm)")
|
||||
width: 150 * screenScaleFactor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
@ -151,7 +151,7 @@ UM.Dialog
|
|||
width: parent.width
|
||||
|
||||
Label {
|
||||
text: catalog.i18nc("@action:label","Smoothing")
|
||||
text: catalog.i18nc("@action:label", "Smoothing")
|
||||
width: 150 * screenScaleFactor
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
|
|
@ -152,7 +152,7 @@ class LegacyProfileReader(ProfileReader):
|
|||
profile.setDirty(True)
|
||||
|
||||
#Serialise and deserialise in order to perform the version upgrade.
|
||||
parser = configparser.ConfigParser(interpolation=None)
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
data = profile.serialize()
|
||||
parser.read_string(data)
|
||||
parser["general"]["version"] = "1"
|
||||
|
|
|
@ -405,7 +405,15 @@ Cura.MachineAction
|
|||
{
|
||||
if (settingsTabs.currentIndex > 0)
|
||||
{
|
||||
manager.updateMaterialForDiameter(settingsTabs.currentIndex - 1);
|
||||
manager.updateMaterialForDiameter(settingsTabs.currentIndex - 1)
|
||||
}
|
||||
}
|
||||
function setValueFunction(value)
|
||||
{
|
||||
if (settingsTabs.currentIndex > 0)
|
||||
{
|
||||
const extruderIndex = index.toString()
|
||||
Cura.MachineManager.activeMachine.extruders[extruderIndex].compatibleMaterialDiameter = value
|
||||
}
|
||||
}
|
||||
property bool isExtruderSetting: true
|
||||
|
@ -435,6 +443,18 @@ Cura.MachineAction
|
|||
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 }
|
||||
|
||||
Row
|
||||
|
@ -552,6 +572,7 @@ Cura.MachineAction
|
|||
property bool _forceUpdateOnChange: (typeof(forceUpdateOnChange) === 'undefined') ? false : forceUpdateOnChange
|
||||
property string _label: (typeof(label) === 'undefined') ? "" : label
|
||||
property string _tooltip: (typeof(tooltip) === 'undefined') ? propertyProvider.properties.description : tooltip
|
||||
property var _setValueFunction: (typeof(setValueFunction) === 'undefined') ? undefined : setValueFunction
|
||||
|
||||
UM.SettingPropertyProvider
|
||||
{
|
||||
|
@ -604,14 +625,32 @@ Cura.MachineAction
|
|||
{
|
||||
if (propertyProvider && text != propertyProvider.properties.value)
|
||||
{
|
||||
propertyProvider.setPropertyValue("value", text);
|
||||
// For some properties like the extruder-compatible material diameter, they need to
|
||||
// trigger many updates, such as the available materials, the current material may
|
||||
// need to be switched, etc. Although setting the diameter can be done directly via
|
||||
// the provider, all the updates that need to be triggered then need to depend on
|
||||
// the metadata update, a signal that can be fired way too often. The update functions
|
||||
// can have if-checks to filter out the irrelevant updates, but still it incurs unnecessary
|
||||
// overhead.
|
||||
// The ExtruderStack class has a dedicated function for this call "setCompatibleMaterialDiameter()",
|
||||
// and it triggers the diameter update signals only when it is needed. Here it is optionally
|
||||
// choose to use setCompatibleMaterialDiameter() or other more specific functions that
|
||||
// are available.
|
||||
if (_setValueFunction !== undefined)
|
||||
{
|
||||
_setValueFunction(text)
|
||||
}
|
||||
else
|
||||
{
|
||||
propertyProvider.setPropertyValue("value", text)
|
||||
}
|
||||
if(_forceUpdateOnChange)
|
||||
{
|
||||
manager.forceUpdate();
|
||||
manager.forceUpdate()
|
||||
}
|
||||
if(_afterOnEditingFinished)
|
||||
{
|
||||
_afterOnEditingFinished();
|
||||
_afterOnEditingFinished()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,6 @@
|
|||
|
||||
from . import MachineSettingsAction
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
# 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 UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
from . import MonitorStage
|
||||
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
|
|
@ -185,6 +185,12 @@ Item {
|
|||
{
|
||||
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
|
||||
|
@ -401,14 +407,9 @@ Item {
|
|||
function updateFilter()
|
||||
{
|
||||
var new_filter = {};
|
||||
if (printSequencePropertyProvider.properties.value == "one_at_a_time")
|
||||
{
|
||||
new_filter["settable_per_meshgroup"] = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
new_filter["settable_per_mesh"] = true;
|
||||
}
|
||||
new_filter["settable_per_mesh"] = true;
|
||||
// Don't filter on "settable_per_meshgroup" any more when `printSequencePropertyProvider.properties.value`
|
||||
// is set to "one_at_a_time", because the current backend architecture isn't ready for that.
|
||||
|
||||
if(filterInput.text != "")
|
||||
{
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
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.Resources import Resources
|
||||
|
@ -9,55 +10,62 @@ from UM.Application import Application
|
|||
from UM.Extension import Extension
|
||||
from UM.Logger import Logger
|
||||
|
||||
import configparser #The script lists are stored in metadata as serialised config files.
|
||||
import io #To allow configparser to write to a string.
|
||||
import configparser # The script lists are stored in metadata as serialised config files.
|
||||
import io # To allow configparser to write to a string.
|
||||
import os.path
|
||||
import pkgutil
|
||||
import sys
|
||||
import importlib.util
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
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
|
||||
# g-code files.
|
||||
class PostProcessingPlugin(QObject, Extension):
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
def __init__(self, parent = None) -> None:
|
||||
QObject.__init__(self, parent)
|
||||
Extension.__init__(self)
|
||||
self.addMenuItem(i18n_catalog.i18n("Modify G-Code"), self.showPopup)
|
||||
self._view = None
|
||||
|
||||
# Loaded scripts are all scripts that can be used
|
||||
self._loaded_scripts = {}
|
||||
self._script_labels = {}
|
||||
self._loaded_scripts = {} # type: Dict[str, Type[Script]]
|
||||
self._script_labels = {} # type: Dict[str, str]
|
||||
|
||||
# Script list contains instances of scripts in loaded_scripts.
|
||||
# There can be duplicates, which will be executed in sequence.
|
||||
self._script_list = []
|
||||
self._script_list = [] # type: List[Script]
|
||||
self._selected_script_index = -1
|
||||
|
||||
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().mainWindowChanged.connect(self._createView) #When the main window is created, create the view so that we can display the post-processing icon if necessary.
|
||||
Application.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged) # When the current printer changes, update the list of scripts.
|
||||
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()
|
||||
@pyqtProperty("QVariant", notify = selectedIndexChanged)
|
||||
def selectedScriptDefinitionId(self):
|
||||
|
||||
@pyqtProperty(str, notify = selectedIndexChanged)
|
||||
def selectedScriptDefinitionId(self) -> Optional[str]:
|
||||
try:
|
||||
return self._script_list[self._selected_script_index].getDefinitionId()
|
||||
except:
|
||||
return ""
|
||||
|
||||
@pyqtProperty("QVariant", notify=selectedIndexChanged)
|
||||
def selectedScriptStackId(self):
|
||||
@pyqtProperty(str, notify=selectedIndexChanged)
|
||||
def selectedScriptStackId(self) -> Optional[str]:
|
||||
try:
|
||||
return self._script_list[self._selected_script_index].getStackId()
|
||||
except:
|
||||
return ""
|
||||
|
||||
## Execute all post-processing scripts on the gcode.
|
||||
def execute(self, output_device):
|
||||
def execute(self, output_device) -> None:
|
||||
scene = Application.getInstance().getController().getScene()
|
||||
# If the scene does not have a gcode, do nothing
|
||||
if not hasattr(scene, "gcode_dict"):
|
||||
|
@ -67,7 +75,7 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
return
|
||||
|
||||
# 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]
|
||||
if not gcode_list:
|
||||
return
|
||||
|
@ -86,16 +94,17 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
Logger.log("e", "Already post processed")
|
||||
|
||||
@pyqtSlot(int)
|
||||
def setSelectedScriptIndex(self, index):
|
||||
self._selected_script_index = index
|
||||
self.selectedIndexChanged.emit()
|
||||
def setSelectedScriptIndex(self, index: int) -> None:
|
||||
if self._selected_script_index != index:
|
||||
self._selected_script_index = index
|
||||
self.selectedIndexChanged.emit()
|
||||
|
||||
@pyqtProperty(int, notify = selectedIndexChanged)
|
||||
def selectedScriptIndex(self):
|
||||
def selectedScriptIndex(self) -> int:
|
||||
return self._selected_script_index
|
||||
|
||||
@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:
|
||||
return # nothing needs to be done
|
||||
else:
|
||||
|
@ -107,7 +116,7 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
|
||||
## Remove a script from the active script list by index.
|
||||
@pyqtSlot(int)
|
||||
def removeScriptByIndex(self, index):
|
||||
def removeScriptByIndex(self, index: int) -> None:
|
||||
self._script_list.pop(index)
|
||||
if len(self._script_list) - 1 < self._selected_script_index:
|
||||
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.
|
||||
#
|
||||
# This should probably only be done on init.
|
||||
def loadAllScripts(self):
|
||||
if self._loaded_scripts: #Already loaded.
|
||||
def loadAllScripts(self) -> None:
|
||||
if self._loaded_scripts: # Already loaded.
|
||||
return
|
||||
|
||||
#The PostProcessingPlugin path is for built-in 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 PostProcessingPlugin path is for built-in 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.
|
||||
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")
|
||||
if not os.path.isdir(path):
|
||||
try:
|
||||
|
@ -139,7 +150,7 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
## Load all scripts from provided path.
|
||||
# This should probably only be done on init.
|
||||
# \param path Path to check for scripts.
|
||||
def loadScripts(self, path):
|
||||
def loadScripts(self, path: str) -> None:
|
||||
## Load all scripts in the scripts folders
|
||||
scripts = pkgutil.iter_modules(path = [path])
|
||||
for loader, script_name, ispkg in scripts:
|
||||
|
@ -148,6 +159,8 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
try:
|
||||
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)
|
||||
if spec.loader is None:
|
||||
continue
|
||||
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?
|
||||
|
||||
|
@ -172,23 +185,24 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
|
||||
loadedScriptListChanged = pyqtSignal()
|
||||
@pyqtProperty("QVariantList", notify = loadedScriptListChanged)
|
||||
def loadedScriptList(self):
|
||||
def loadedScriptList(self) -> List[str]:
|
||||
return sorted(list(self._loaded_scripts.keys()))
|
||||
|
||||
@pyqtSlot(str, result = str)
|
||||
def getScriptLabelByKey(self, key):
|
||||
return self._script_labels[key]
|
||||
def getScriptLabelByKey(self, key: str) -> Optional[str]:
|
||||
return self._script_labels.get(key)
|
||||
|
||||
scriptListChanged = pyqtSignal()
|
||||
@pyqtProperty("QVariantList", notify = scriptListChanged)
|
||||
def scriptList(self):
|
||||
@pyqtProperty("QStringList", notify = scriptListChanged)
|
||||
def scriptList(self) -> List[str]:
|
||||
script_list = [script.getSettingData()["key"] for script in self._script_list]
|
||||
return script_list
|
||||
|
||||
@pyqtSlot(str)
|
||||
def addScriptToList(self, key):
|
||||
def addScriptToList(self, key: str) -> None:
|
||||
Logger.log("d", "Adding script %s to list.", key)
|
||||
new_script = self._loaded_scripts[key]()
|
||||
new_script.initialize()
|
||||
self._script_list.append(new_script)
|
||||
self.setSelectedScriptIndex(len(self._script_list) - 1)
|
||||
self.scriptListChanged.emit()
|
||||
|
@ -196,81 +210,89 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
|
||||
## When the global container stack is changed, swap out the list of active
|
||||
# scripts.
|
||||
def _onGlobalContainerStackChanged(self):
|
||||
def _onGlobalContainerStackChanged(self) -> None:
|
||||
self.loadAllScripts()
|
||||
new_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if new_stack is None:
|
||||
return
|
||||
self._script_list.clear()
|
||||
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.
|
||||
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.
|
||||
return
|
||||
|
||||
self._script_list.clear()
|
||||
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.
|
||||
if not script_str: #There were no scripts in this one (or a corrupt file caused more than 3 consecutive newlines here).
|
||||
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).
|
||||
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.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)
|
||||
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.
|
||||
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.
|
||||
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))
|
||||
continue
|
||||
new_script = self._loaded_scripts[script_name]()
|
||||
for setting_key, setting_value in settings.items(): #Put all setting values into the script.
|
||||
new_script._instance.setProperty(setting_key, "value", setting_value)
|
||||
new_script.initialize()
|
||||
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.setSelectedScriptIndex(0)
|
||||
self.scriptListChanged.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def writeScriptsToStack(self):
|
||||
script_list_strs = []
|
||||
def writeScriptsToStack(self) -> None:
|
||||
script_list_strs = [] # type: List[str]
|
||||
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.optionxform = str #Don't transform the setting keys as they are case-sensitive.
|
||||
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 # type: ignore # Don't transform the setting keys as they are case-sensitive.
|
||||
script_name = script.getSettingData()["key"]
|
||||
parser.add_section(script_name)
|
||||
for key in script.getSettingData()["settings"]:
|
||||
value = script.getSettingValueByKey(key)
|
||||
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)
|
||||
serialized.seek(0)
|
||||
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 = "\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()
|
||||
if global_stack is None:
|
||||
return
|
||||
|
||||
if "post_processing_scripts" not in global_stack.getMetaData():
|
||||
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.
|
||||
def _createView(self):
|
||||
def _createView(self) -> None:
|
||||
Logger.log("d", "Creating post processing plugin view.")
|
||||
|
||||
self.loadAllScripts()
|
||||
|
||||
# Create the plugin dialog component
|
||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")
|
||||
self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
|
||||
path = os.path.join(cast(str, PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin")), "PostProcessingPlugin.qml")
|
||||
self._view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
|
||||
if self._view is None:
|
||||
Logger.log("e", "Not creating PostProcessing button near save button because the QML component failed to be created.")
|
||||
return
|
||||
Logger.log("d", "Post processing view created.")
|
||||
|
||||
# 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.
|
||||
def showPopup(self):
|
||||
def showPopup(self) -> None:
|
||||
if self._view is None:
|
||||
self._createView()
|
||||
if self._view is None:
|
||||
|
@ -282,8 +304,9 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
# To do this we use the global container stack propertyChanged.
|
||||
# Re-slicing is necessary for setting changes in this plugin, because the changes
|
||||
# are applied only once per "fresh" gcode
|
||||
def _propertyChanged(self):
|
||||
def _propertyChanged(self) -> None:
|
||||
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")
|
||||
|
||||
|
||||
|
|
|
@ -62,6 +62,7 @@ UM.Dialog
|
|||
anchors.right: parent.right
|
||||
anchors.rightMargin: base.textMargin
|
||||
font: UM.Theme.getFont("large")
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
ListView
|
||||
{
|
||||
|
@ -115,6 +116,7 @@ UM.Dialog
|
|||
{
|
||||
wrapMode: Text.Wrap
|
||||
text: control.text
|
||||
elide: Text.ElideRight
|
||||
color: activeScriptButton.checked ? palette.highlightedText : palette.text
|
||||
}
|
||||
}
|
||||
|
@ -275,6 +277,7 @@ UM.Dialog
|
|||
anchors.leftMargin: base.textMargin
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: base.textMargin
|
||||
elide: Text.ElideRight
|
||||
height: 20 * screenScaleFactor
|
||||
font: UM.Theme.getFont("large")
|
||||
color: UM.Theme.getColor("text")
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
# Copyright (c) 2015 Jaime van Kessel
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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.i18n import i18nCatalog
|
||||
|
||||
|
@ -17,23 +19,27 @@ import json
|
|||
import collections
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||
|
||||
|
||||
## Base class for scripts. All scripts should inherit the script class.
|
||||
@signalemitter
|
||||
class Script:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._settings = None
|
||||
self._stack = None
|
||||
self._stack = None # type: Optional[ContainerStack]
|
||||
self._definition = None # type: Optional[DefinitionContainerInterface]
|
||||
self._instance = None # type: Optional[InstanceContainer]
|
||||
|
||||
def initialize(self) -> None:
|
||||
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.
|
||||
|
||||
|
||||
## Check if the definition of this script already exists. If not, add it to the registry.
|
||||
if "key" in setting_data:
|
||||
definitions = ContainerRegistry.getInstance().findDefinitionContainers(id = setting_data["key"])
|
||||
definitions = ContainerRegistry.getInstance().findDefinitionContainers(id=setting_data["key"])
|
||||
if definitions:
|
||||
# Definition was found
|
||||
self._definition = definitions[0]
|
||||
|
@ -45,10 +51,13 @@ class Script:
|
|||
except ContainerFormatError:
|
||||
self._definition = None
|
||||
return
|
||||
if self._definition is None:
|
||||
return
|
||||
self._stack.addContainer(self._definition)
|
||||
self._instance = InstanceContainer(container_id="ScriptInstanceContainer")
|
||||
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.propertyChanged.connect(self._onPropertyChanged)
|
||||
|
||||
|
@ -57,16 +66,17 @@ class Script:
|
|||
settingsLoaded = Signal()
|
||||
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":
|
||||
self.valueChanged.emit()
|
||||
|
||||
# Property changed: trigger reslice
|
||||
# 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
|
||||
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.
|
||||
# See the example script for an example.
|
||||
|
@ -74,30 +84,35 @@ class Script:
|
|||
# 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
|
||||
# returning a dict in that the order of settings is maintained.
|
||||
def getSettingData(self):
|
||||
setting_data = self.getSettingDataString()
|
||||
if type(setting_data) == str:
|
||||
setting_data = json.loads(setting_data, object_pairs_hook = collections.OrderedDict)
|
||||
def getSettingData(self) -> Dict[str, Any]:
|
||||
setting_data_as_string = self.getSettingDataString()
|
||||
setting_data = json.loads(setting_data_as_string, object_pairs_hook = collections.OrderedDict)
|
||||
return setting_data
|
||||
|
||||
def getSettingDataString(self):
|
||||
def getSettingDataString(self) -> str:
|
||||
raise NotImplementedError()
|
||||
|
||||
def getDefinitionId(self):
|
||||
def getDefinitionId(self) -> Optional[str]:
|
||||
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:
|
||||
return self._stack.getId()
|
||||
return None
|
||||
|
||||
## Convenience function that retrieves value of a setting from the stack.
|
||||
def getSettingValueByKey(self, key):
|
||||
return self._stack.getProperty(key, "value")
|
||||
def getSettingValueByKey(self, key: str) -> Any:
|
||||
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.
|
||||
# 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(';')):
|
||||
return default
|
||||
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
|
||||
# provided, an entirely new g-code line will be produced.
|
||||
# \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.
|
||||
comment = ""
|
||||
if ";" in line:
|
||||
|
@ -166,5 +181,5 @@ class Script:
|
|||
|
||||
## This is called when the script is executed.
|
||||
# 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()
|
||||
|
|
|
@ -2,10 +2,10 @@
|
|||
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from . import PostProcessingPlugin
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
||||
|
||||
def register(app):
|
||||
return {"extension": PostProcessingPlugin.PostProcessingPlugin()}
|
|
@ -407,13 +407,13 @@ class ChangeAtZ(Script):
|
|||
if "M106" in line and state < 3: #looking for fan speed
|
||||
old["fanSpeed"] = self.getValue(line, "S", old["fanSpeed"])
|
||||
if "M221" in line and state < 3: #looking for flow rate
|
||||
tmp_extruder = self.getValue(line,"T",None)
|
||||
tmp_extruder = self.getValue(line, "T", None)
|
||||
if tmp_extruder == None: #check if extruder is specified
|
||||
old["flowrate"] = self.getValue(line, "S", old["flowrate"])
|
||||
elif tmp_extruder == 0: #first extruder
|
||||
old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"])
|
||||
elif tmp_extruder == 1: #second extruder
|
||||
old["flowrateOne"] = self.getValue(line, "S", old["flowrateOne"])
|
||||
old["flowrateTwo"] = self.getValue(line, "S", old["flowrateTwo"])
|
||||
if ("M84" in line or "M25" in line):
|
||||
if state>0 and ChangeProp["speed"]: #"finish" commands for UM Original and UM2
|
||||
modified_gcode += "M220 S100 ; speed reset to 100% at the end of print\n"
|
||||
|
|
|
@ -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
|
|
@ -1,5 +1,6 @@
|
|||
# This PostProcessing Plugin script is released
|
||||
# under the terms of the AGPLv3 or higher
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
@ -54,17 +55,17 @@ class FilamentChange(Script):
|
|||
layer_nums = self.getSettingValueByKey("layer_number")
|
||||
initial_retract = self.getSettingValueByKey("initial_retract")
|
||||
later_retract = self.getSettingValueByKey("later_retract")
|
||||
|
||||
|
||||
color_change = "M600"
|
||||
|
||||
|
||||
if initial_retract is not None and initial_retract > 0.:
|
||||
color_change = color_change + (" E-%.2f" % initial_retract)
|
||||
|
||||
color_change = color_change + (" E%.2f" % initial_retract)
|
||||
|
||||
if later_retract is not None and later_retract > 0.:
|
||||
color_change = color_change + (" L-%.2f" % later_retract)
|
||||
|
||||
color_change = color_change + (" L%.2f" % later_retract)
|
||||
|
||||
color_change = color_change + " ; Generated by FilamentChange plugin"
|
||||
|
||||
|
||||
layer_targets = layer_nums.split(",")
|
||||
if len(layer_targets) > 0:
|
||||
for layer_num in layer_targets:
|
||||
|
|
|
@ -3,12 +3,10 @@
|
|||
|
||||
from UM.Platform import Platform
|
||||
from UM.Logger import Logger
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
}
|
||||
return {}
|
||||
|
||||
def register(app):
|
||||
if Platform.isWindows():
|
||||
|
|
|
@ -234,6 +234,11 @@ Item
|
|||
UM.SimulationView.setCurrentLayer(value)
|
||||
|
||||
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)))
|
||||
y = newUpperYPosition
|
||||
|
||||
|
@ -339,6 +344,11 @@ Item
|
|||
UM.SimulationView.setMinimumLayer(value)
|
||||
|
||||
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)))
|
||||
y = newLowerYPosition
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ UM.PointingRectangle {
|
|||
horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
width: (maximumValue.toString().length + 1) * 10 * screenScaleFactor
|
||||
width: ((maximumValue + 1).toString().length + 1) * 10 * screenScaleFactor
|
||||
text: sliderLabelRoot.value + startFrom // the current handle value, add 1 because layers is an array
|
||||
horizontalAlignment: TextInput.AlignRight
|
||||
|
||||
|
|
|
@ -21,9 +21,10 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|||
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Signal import Signal
|
||||
from UM.View.CompositePass import CompositePass
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
from UM.View.GL.OpenGLContext import OpenGLContext
|
||||
|
||||
from UM.View.GL.ShaderProgram import ShaderProgram
|
||||
|
||||
from UM.View.View import View
|
||||
from UM.i18n import i18nCatalog
|
||||
|
@ -36,13 +37,11 @@ from .SimulationViewProxy import SimulationViewProxy
|
|||
import numpy
|
||||
import os.path
|
||||
|
||||
from typing import Optional, TYPE_CHECKING, List
|
||||
from typing import Optional, TYPE_CHECKING, List, cast
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
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
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
@ -64,7 +63,7 @@ class SimulationView(View):
|
|||
self._minimum_layer_num = 0
|
||||
self._current_layer_mesh = None
|
||||
self._current_layer_jumps = None
|
||||
self._top_layers_job = None
|
||||
self._top_layers_job = None # type: Optional["_CreateTopLayersJob"]
|
||||
self._activity = False
|
||||
self._old_max_layers = 0
|
||||
|
||||
|
@ -78,10 +77,10 @@ class SimulationView(View):
|
|||
|
||||
self._ghost_shader = None # type: Optional["ShaderProgram"]
|
||||
self._layer_pass = None # type: Optional[SimulationPass]
|
||||
self._composite_pass = None # type: Optional[RenderPass]
|
||||
self._old_layer_bindings = None
|
||||
self._composite_pass = None # type: Optional[CompositePass]
|
||||
self._old_layer_bindings = None # type: Optional[List[str]]
|
||||
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._proxy = SimulationViewProxy()
|
||||
|
@ -204,9 +203,11 @@ class SimulationView(View):
|
|||
|
||||
if not self._ghost_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.
|
||||
# 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()):
|
||||
|
@ -346,8 +347,8 @@ class SimulationView(View):
|
|||
|
||||
self._old_max_layers = self._max_layers
|
||||
## Recalculate num max layers
|
||||
new_max_layers = 0
|
||||
for node in DepthFirstIterator(scene.getRoot()):
|
||||
new_max_layers = -1
|
||||
for node in DepthFirstIterator(scene.getRoot()): # type: ignore
|
||||
layer_data = node.callDecoration("getLayerData")
|
||||
if not layer_data:
|
||||
continue
|
||||
|
@ -381,7 +382,7 @@ class SimulationView(View):
|
|||
if 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
|
||||
|
||||
# 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:
|
||||
# Update the currentPath
|
||||
scene = self.getController().getScene()
|
||||
for node in DepthFirstIterator(scene.getRoot()):
|
||||
for node in DepthFirstIterator(scene.getRoot()): # type: ignore
|
||||
layer_data = node.callDecoration("getLayerData")
|
||||
if not layer_data:
|
||||
continue
|
||||
|
@ -474,15 +475,17 @@ class SimulationView(View):
|
|||
self._onGlobalStackChanged()
|
||||
|
||||
if not self._simulationview_composite_shader:
|
||||
self._simulationview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(PluginRegistry.getInstance().getPluginPath("SimulationView"), "simulationview_composite.shader"))
|
||||
theme = Application.getInstance().getTheme()
|
||||
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()))
|
||||
plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath("SimulationView"))
|
||||
self._simulationview_composite_shader = OpenGL.getInstance().createShaderProgram(os.path.join(plugin_path, "simulationview_composite.shader"))
|
||||
theme = CuraApplication.getInstance().getTheme()
|
||||
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:
|
||||
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._old_composite_shader = self._composite_pass.getCompositeShader()
|
||||
self._composite_pass.setCompositeShader(self._simulationview_composite_shader)
|
||||
|
@ -496,8 +499,8 @@ class SimulationView(View):
|
|||
self._nozzle_node.setParent(None)
|
||||
self.getRenderer().removeRenderPass(self._layer_pass)
|
||||
if self._composite_pass:
|
||||
self._composite_pass.setLayerBindings(self._old_layer_bindings)
|
||||
self._composite_pass.setCompositeShader(self._old_composite_shader)
|
||||
self._composite_pass.setLayerBindings(cast(List[str], self._old_layer_bindings))
|
||||
self._composite_pass.setCompositeShader(cast(ShaderProgram, self._old_composite_shader))
|
||||
|
||||
return False
|
||||
|
||||
|
@ -606,7 +609,7 @@ class _CreateTopLayersJob(Job):
|
|||
|
||||
def run(self) -> 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")
|
||||
if layer_data:
|
||||
break
|
||||
|
|
|
@ -33,30 +33,35 @@ class SliceInfo(QObject, Extension):
|
|||
def __init__(self, parent = None):
|
||||
QObject.__init__(self, parent)
|
||||
Extension.__init__(self)
|
||||
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._onWriteStarted)
|
||||
Application.getInstance().getPreferences().addPreference("info/send_slice_info", True)
|
||||
Application.getInstance().getPreferences().addPreference("info/asked_send_slice_info", False)
|
||||
|
||||
self._application = Application.getInstance()
|
||||
|
||||
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._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."),
|
||||
lifetime = 0,
|
||||
dismissable = False,
|
||||
title = catalog.i18nc("@info:title", "Collecting Data"))
|
||||
|
||||
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,
|
||||
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.show()
|
||||
|
||||
Application.getInstance().initializationFinished.connect(self._onAppInitialized)
|
||||
|
||||
def _onAppInitialized(self):
|
||||
if self._more_info_dialog is None:
|
||||
self._more_info_dialog = self._createDialog("MoreInfoWindow.qml")
|
||||
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from . import SliceInfo
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
}
|
||||
return {}
|
||||
|
||||
def register(app):
|
||||
return { "extension": SliceInfo.SliceInfo()}
|
|
@ -10,7 +10,7 @@ Window
|
|||
{
|
||||
id: base
|
||||
property var selection: null
|
||||
title: catalog.i18nc("@title", "Toolbox")
|
||||
title: catalog.i18nc("@title", "Marketplace")
|
||||
modality: Qt.ApplicationModal
|
||||
flags: Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ Item
|
|||
{
|
||||
id: sidebar
|
||||
}
|
||||
Rectangle
|
||||
Item
|
||||
{
|
||||
id: header
|
||||
anchors
|
||||
|
|
|
@ -23,6 +23,7 @@ Item
|
|||
{
|
||||
id: button
|
||||
text: catalog.i18nc("@action:button", "Back")
|
||||
enabled: !toolbox.isDownloading
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: backArrow
|
||||
|
@ -39,7 +40,7 @@ Item
|
|||
width: width
|
||||
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")
|
||||
}
|
||||
width: UM.Theme.getSize("toolbox_back_button").width
|
||||
|
@ -59,7 +60,7 @@ Item
|
|||
{
|
||||
id: labelStyle
|
||||
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")
|
||||
horizontalAlignment: Text.AlignRight
|
||||
width: control.width
|
||||
|
|
|
@ -11,157 +11,208 @@ Item
|
|||
id: base
|
||||
|
||||
property var packageData
|
||||
property var technicalDataSheetUrl: {
|
||||
property var technicalDataSheetUrl:
|
||||
{
|
||||
var link = undefined
|
||||
if ("Technical Data Sheet" in packageData.links)
|
||||
{
|
||||
// HACK: This is the way the old API (used in 3.6-beta) used to do it. For safety it's still here,
|
||||
// but it can be removed over time.
|
||||
link = packageData.links["Technical Data Sheet"]
|
||||
}
|
||||
else if ("technicalDataSheet" in packageData.links)
|
||||
{
|
||||
link = packageData.links["technicalDataSheet"]
|
||||
}
|
||||
return link
|
||||
}
|
||||
|
||||
property var safetyDataSheetUrl:
|
||||
{
|
||||
var sds_name = "safetyDataSheet"
|
||||
return (sds_name in packageData.links) ? packageData.links[sds_name] : undefined
|
||||
}
|
||||
property var printingGuidelinesUrl:
|
||||
{
|
||||
var pg_name = "printingGuidelines"
|
||||
return (pg_name in packageData.links) ? packageData.links[pg_name] : undefined
|
||||
}
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
height: visible ? childrenRect.height : 0
|
||||
visible: packageData.type == "material" && packageData.has_configs
|
||||
Label
|
||||
|
||||
visible: packageData.type == "material" &&
|
||||
(packageData.has_configs || technicalDataSheetUrl !== undefined ||
|
||||
safetyDataSheetUrl !== undefined || printingGuidelinesUrl !== undefined)
|
||||
|
||||
Item
|
||||
{
|
||||
id: heading
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
id: combatibilityItem
|
||||
visible: packageData.has_configs
|
||||
width: parent.width
|
||||
text: catalog.i18nc("@label", "Compatibility")
|
||||
wrapMode: Text.WordWrap
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
font: UM.Theme.getFont("medium")
|
||||
}
|
||||
TableView
|
||||
{
|
||||
id: table
|
||||
anchors.top: heading.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
width: parent.width
|
||||
frameVisible: false
|
||||
// This is a bit of a hack, but the whole QML is pretty messy right now. This needs a big overhaul.
|
||||
height: visible ? heading.height + table.height: 0
|
||||
|
||||
// Workaround for scroll issues (QTBUG-49652)
|
||||
flickableItem.interactive: false
|
||||
Component.onCompleted:
|
||||
Label
|
||||
{
|
||||
for (var i = 0; i < flickableItem.children.length; ++i)
|
||||
{
|
||||
flickableItem.children[i].enabled = false
|
||||
}
|
||||
}
|
||||
selectionMode: 0
|
||||
model: packageData.supported_configs
|
||||
headerDelegate: Rectangle
|
||||
{
|
||||
color: UM.Theme.getColor("sidebar")
|
||||
height: UM.Theme.getSize("toolbox_chart_row").height
|
||||
Label
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
text: styleData.value || ""
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
}
|
||||
Rectangle
|
||||
{
|
||||
anchors.bottom: parent.bottom
|
||||
height: UM.Theme.getSize("default_lining").height
|
||||
width: parent.width
|
||||
color: "black"
|
||||
}
|
||||
}
|
||||
rowDelegate: Item
|
||||
{
|
||||
height: UM.Theme.getSize("toolbox_chart_row").height
|
||||
Label
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
text: styleData.value || ""
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
font: UM.Theme.getFont("default")
|
||||
}
|
||||
}
|
||||
itemDelegate: Item
|
||||
{
|
||||
height: UM.Theme.getSize("toolbox_chart_row").height
|
||||
Label
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
text: styleData.value || ""
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
font: UM.Theme.getFont("default")
|
||||
}
|
||||
id: heading
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
width: parent.width
|
||||
text: catalog.i18nc("@label", "Compatibility")
|
||||
wrapMode: Text.WordWrap
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
font: UM.Theme.getFont("medium")
|
||||
}
|
||||
|
||||
Component
|
||||
TableView
|
||||
{
|
||||
id: columnTextDelegate
|
||||
Label
|
||||
{
|
||||
anchors.fill: parent
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: styleData.value || ""
|
||||
elide: Text.ElideRight
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
font: UM.Theme.getFont("default")
|
||||
}
|
||||
}
|
||||
id: table
|
||||
anchors.top: heading.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
||||
width: parent.width
|
||||
frameVisible: false
|
||||
|
||||
TableViewColumn
|
||||
{
|
||||
role: "machine"
|
||||
title: "Machine"
|
||||
width: Math.floor(table.width * 0.25)
|
||||
delegate: columnTextDelegate
|
||||
}
|
||||
TableViewColumn
|
||||
{
|
||||
role: "print_core"
|
||||
title: "Print Core"
|
||||
width: Math.floor(table.width * 0.2)
|
||||
}
|
||||
TableViewColumn
|
||||
{
|
||||
role: "build_plate"
|
||||
title: "Build Plate"
|
||||
width: Math.floor(table.width * 0.225)
|
||||
}
|
||||
TableViewColumn
|
||||
{
|
||||
role: "support_material"
|
||||
title: "Support"
|
||||
width: Math.floor(table.width * 0.225)
|
||||
}
|
||||
TableViewColumn
|
||||
{
|
||||
role: "quality"
|
||||
title: "Quality"
|
||||
width: Math.floor(table.width * 0.1)
|
||||
// Workaround for scroll issues (QTBUG-49652)
|
||||
flickableItem.interactive: false
|
||||
Component.onCompleted:
|
||||
{
|
||||
for (var i = 0; i < flickableItem.children.length; ++i)
|
||||
{
|
||||
flickableItem.children[i].enabled = false
|
||||
}
|
||||
}
|
||||
selectionMode: 0
|
||||
model: packageData.supported_configs
|
||||
headerDelegate: Rectangle
|
||||
{
|
||||
color: UM.Theme.getColor("sidebar")
|
||||
height: UM.Theme.getSize("toolbox_chart_row").height
|
||||
Label
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
text: styleData.value || ""
|
||||
color: UM.Theme.getColor("text")
|
||||
font: UM.Theme.getFont("default_bold")
|
||||
}
|
||||
Rectangle
|
||||
{
|
||||
anchors.bottom: parent.bottom
|
||||
height: UM.Theme.getSize("default_lining").height
|
||||
width: parent.width
|
||||
color: "black"
|
||||
}
|
||||
}
|
||||
rowDelegate: Item
|
||||
{
|
||||
height: UM.Theme.getSize("toolbox_chart_row").height
|
||||
Label
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
text: styleData.value || ""
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
font: UM.Theme.getFont("default")
|
||||
}
|
||||
}
|
||||
itemDelegate: Item
|
||||
{
|
||||
height: UM.Theme.getSize("toolbox_chart_row").height
|
||||
Label
|
||||
{
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
elide: Text.ElideRight
|
||||
text: styleData.value || ""
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
font: UM.Theme.getFont("default")
|
||||
}
|
||||
}
|
||||
|
||||
Component
|
||||
{
|
||||
id: columnTextDelegate
|
||||
Label
|
||||
{
|
||||
anchors.fill: parent
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
text: styleData.value || ""
|
||||
elide: Text.ElideRight
|
||||
color: UM.Theme.getColor("text_medium")
|
||||
font: UM.Theme.getFont("default")
|
||||
}
|
||||
}
|
||||
|
||||
TableViewColumn
|
||||
{
|
||||
role: "machine"
|
||||
title: "Machine"
|
||||
width: Math.floor(table.width * 0.25)
|
||||
delegate: columnTextDelegate
|
||||
}
|
||||
TableViewColumn
|
||||
{
|
||||
role: "print_core"
|
||||
title: "Print Core"
|
||||
width: Math.floor(table.width * 0.2)
|
||||
}
|
||||
TableViewColumn
|
||||
{
|
||||
role: "build_plate"
|
||||
title: "Build Plate"
|
||||
width: Math.floor(table.width * 0.225)
|
||||
}
|
||||
TableViewColumn
|
||||
{
|
||||
role: "support_material"
|
||||
title: "Support"
|
||||
width: Math.floor(table.width * 0.225)
|
||||
}
|
||||
TableViewColumn
|
||||
{
|
||||
role: "quality"
|
||||
title: "Quality"
|
||||
width: Math.floor(table.width * 0.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
{
|
||||
id: technical_data_sheet
|
||||
anchors.top: table.bottom
|
||||
id: data_sheet_links
|
||||
anchors.top: combatibilityItem.bottom
|
||||
anchors.topMargin: UM.Theme.getSize("default_margin").height / 2
|
||||
visible: base.technicalDataSheetUrl !== undefined
|
||||
visible: base.technicalDataSheetUrl !== undefined ||
|
||||
base.safetyDataSheetUrl !== undefined || base.printingGuidelinesUrl !== undefined
|
||||
height: visible ? contentHeight : 0
|
||||
text:
|
||||
{
|
||||
var result = ""
|
||||
if (base.technicalDataSheetUrl !== undefined)
|
||||
{
|
||||
return "<a href='%1'>%2</a>".arg(base.technicalDataSheetUrl).arg("Technical Data Sheet")
|
||||
var tds_name = catalog.i18nc("@action:label", "Technical Data Sheet")
|
||||
result += "<a href='%1'>%2</a>".arg(base.technicalDataSheetUrl).arg(tds_name)
|
||||
}
|
||||
return ""
|
||||
if (base.safetyDataSheetUrl !== undefined)
|
||||
{
|
||||
if (result.length > 0)
|
||||
{
|
||||
result += "<br/>"
|
||||
}
|
||||
var sds_name = catalog.i18nc("@action:label", "Safety Data Sheet")
|
||||
result += "<a href='%1'>%2</a>".arg(base.safetyDataSheetUrl).arg(sds_name)
|
||||
}
|
||||
if (base.printingGuidelinesUrl !== undefined)
|
||||
{
|
||||
if (result.length > 0)
|
||||
{
|
||||
result += "<br/>"
|
||||
}
|
||||
var pg_name = catalog.i18nc("@action:label", "Printing Guidelines")
|
||||
result += "<a href='%1'>%2</a>".arg(base.printingGuidelinesUrl).arg(pg_name)
|
||||
}
|
||||
return result
|
||||
}
|
||||
font: UM.Theme.getFont("very_small")
|
||||
color: UM.Theme.getColor("text")
|
||||
linkColor: UM.Theme.getColor("text_link")
|
||||
onLinkActivated: Qt.openUrlExternally(link)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
id: base
|
||||
|
||||
title: catalog.i18nc("@title:window", "Confirm uninstall ") + toolbox.pluginToUninstall
|
||||
title: catalog.i18nc("@title:window", "Confirm uninstall") + toolbox.pluginToUninstall
|
||||
width: 450 * screenScaleFactor
|
||||
height: 50 * screenScaleFactor + dialogText.height + buttonBar.height
|
||||
|
||||
|
|
|
@ -9,9 +9,8 @@ import UM 1.1 as UM
|
|||
Item
|
||||
{
|
||||
id: page
|
||||
property var details: base.selection
|
||||
property var details: base.selection || {}
|
||||
anchors.fill: parent
|
||||
width: parent.width
|
||||
ToolboxBackColumn
|
||||
{
|
||||
id: sidebar
|
||||
|
@ -26,14 +25,11 @@ Item
|
|||
rightMargin: UM.Theme.getSize("wide_margin").width
|
||||
}
|
||||
height: UM.Theme.getSize("toolbox_detail_header").height
|
||||
Image
|
||||
Rectangle
|
||||
{
|
||||
id: thumbnail
|
||||
width: UM.Theme.getSize("toolbox_thumbnail_medium").width
|
||||
height: UM.Theme.getSize("toolbox_thumbnail_medium").height
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: details === null ? "" : (details.icon_url || "../images/logobot.svg")
|
||||
mipmap: true
|
||||
anchors
|
||||
{
|
||||
top: parent.top
|
||||
|
@ -41,6 +37,14 @@ Item
|
|||
leftMargin: UM.Theme.getSize("wide_margin").width
|
||||
topMargin: UM.Theme.getSize("wide_margin").height
|
||||
}
|
||||
color: white //Always a white background for image (regardless of theme).
|
||||
Image
|
||||
{
|
||||
anchors.fill: parent
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: details === null ? "" : (details.icon_url || "../images/logobot.svg")
|
||||
mipmap: true
|
||||
}
|
||||
}
|
||||
|
||||
Label
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Copyright (c) 2018 Ultimaker B.V.
|
||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.7
|
||||
import QtQuick.Controls 1.4
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue