Merge branch 'master' into feature_z_hop_extruder_switch

This commit is contained in:
Vlad Gribinchuk 2019-04-22 10:45:57 +03:00
commit fec0272c5f
1601 changed files with 180516 additions and 102013 deletions

2
.gitignore vendored
View File

@ -42,7 +42,6 @@ plugins/cura-siemensnx-plugin
plugins/CuraBlenderPlugin plugins/CuraBlenderPlugin
plugins/CuraCloudPlugin plugins/CuraCloudPlugin
plugins/CuraDrivePlugin plugins/CuraDrivePlugin
plugins/CuraDrive
plugins/CuraLiveScriptingPlugin plugins/CuraLiveScriptingPlugin
plugins/CuraOpenSCADPlugin plugins/CuraOpenSCADPlugin
plugins/CuraPrintProfileCreator plugins/CuraPrintProfileCreator
@ -72,3 +71,4 @@ run.sh
.scannerwork/ .scannerwork/
CuraEngine CuraEngine
/.coverage

12
.gitlab-ci.yml Normal file
View File

@ -0,0 +1,12 @@
image: registry.gitlab.com/ultimaker/cura/cura-build-environment:centos7
stages:
- build
build-and-test:
stage: build
script:
- docker/build.sh
artifacts:
paths:
- build

View File

@ -1,11 +1,10 @@
project(cura NONE) project(cura)
cmake_minimum_required(VERSION 2.8.12) cmake_minimum_required(VERSION 3.6)
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/
${CMAKE_MODULE_PATH})
include(GNUInstallDirs) include(GNUInstallDirs)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository") set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository")
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository") set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
@ -17,6 +16,8 @@ if(CURA_DEBUGMODE)
set(_cura_debugmode "ON") set(_cura_debugmode "ON")
endif() endif()
set(CURA_APP_NAME "cura" CACHE STRING "Short name of Cura, used for configuration folder")
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_VERSION "master" CACHE STRING "Version name of Cura")
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'") set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
set(CURA_SDK_VERSION "" CACHE STRING "SDK version of Cura") set(CURA_SDK_VERSION "" CACHE STRING "SDK version of Cura")
@ -26,6 +27,26 @@ set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY) configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY) configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
# FIXME: Remove the code for CMake <3.12 once we have switched over completely.
# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
# module is copied from the CMake repository here so in CMake <3.12 we can still use it.
if(${CMAKE_VERSION} VERSION_LESS 3.12)
# Use FindPythonInterp and FindPythonLibs for CMake <3.12
find_package(PythonInterp 3 REQUIRED)
set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
set(Python3_VERSION ${PYTHON_VERSION_STRING})
set(Python3_VERSION_MAJOR ${PYTHON_VERSION_MAJOR})
set(Python3_VERSION_MINOR ${PYTHON_VERSION_MINOR})
set(Python3_VERSION_PATCH ${PYTHON_VERSION_PATCH})
else()
# Use FindPython3 for CMake >=3.12
find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
endif()
if(NOT ${URANIUM_DIR} STREQUAL "") if(NOT ${URANIUM_DIR} STREQUAL "")
set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake") set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake")
endif() endif()
@ -38,12 +59,12 @@ if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
CREATE_TRANSLATION_TARGETS() CREATE_TRANSLATION_TARGETS()
endif() endif()
find_package(PythonInterp 3.5.0 REQUIRED)
install(DIRECTORY resources install(DIRECTORY resources
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura) DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
install(DIRECTORY plugins install(DIRECTORY plugins
DESTINATION lib${LIB_SUFFIX}/cura) DESTINATION lib${LIB_SUFFIX}/cura)
if(NOT APPLE AND NOT WIN32) if(NOT APPLE AND NOT WIN32)
install(FILES cura_app.py install(FILES cura_app.py
DESTINATION ${CMAKE_INSTALL_BINDIR} DESTINATION ${CMAKE_INSTALL_BINDIR}
@ -51,16 +72,16 @@ if(NOT APPLE AND NOT WIN32)
RENAME cura) RENAME cura)
if(EXISTS /etc/debian_version) if(EXISTS /etc/debian_version)
install(DIRECTORY cura install(DIRECTORY cura
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}/dist-packages DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}/dist-packages
FILES_MATCHING PATTERN *.py) FILES_MATCHING PATTERN *.py)
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}/dist-packages/cura) DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}/dist-packages/cura)
else() else()
install(DIRECTORY cura install(DIRECTORY cura
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages
FILES_MATCHING PATTERN *.py) FILES_MATCHING PATTERN *.py)
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura) DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages/cura)
endif() endif()
install(FILES ${CMAKE_BINARY_DIR}/cura.desktop install(FILES ${CMAKE_BINARY_DIR}/cura.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications) DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
@ -76,8 +97,8 @@ else()
DESTINATION ${CMAKE_INSTALL_BINDIR} DESTINATION ${CMAKE_INSTALL_BINDIR}
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE) PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
install(DIRECTORY cura install(DIRECTORY cura
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages
FILES_MATCHING PATTERN *.py) FILES_MATCHING PATTERN *.py)
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura) DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages/cura)
endif() endif()

43
Jenkinsfile vendored
View File

@ -38,20 +38,9 @@ parallel_nodes(['linux && cura', 'windows && cura'])
{ {
if (isUnix()) if (isUnix())
{ {
// For Linux to show everything // For Linux
def branch = env.BRANCH_NAME
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
{
branch = "master"
}
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
try { try {
sh """ sh 'make CTEST_OUTPUT_ON_FAILURE=TRUE test'
cd ..
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" currentBuild.result = "UNSTABLE"
@ -70,34 +59,6 @@ parallel_nodes(['linux && cura', 'windows && cura'])
} }
} }
} }
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}"))
{
branch = "master"
}
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
try
{
sh """
cd ..
export PYTHONPATH=.:"${uranium_dir}"
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/python3 run_mypy.py
"""
}
catch(e)
{
currentBuild.result = "UNSTABLE"
}
}
}
} }
} }

View File

@ -20,8 +20,9 @@ Dependencies
------------ ------------
* [Uranium](https://github.com/Ultimaker/Uranium) Cura is built on top of the Uranium framework. * [Uranium](https://github.com/Ultimaker/Uranium) Cura is built on top of the Uranium framework.
* [CuraEngine](https://github.com/Ultimaker/CuraEngine) This will be needed at runtime to perform the actual slicing. * [CuraEngine](https://github.com/Ultimaker/CuraEngine) This will be needed at runtime to perform the actual slicing.
* [fdm_materials](https://github.com/Ultimaker/fdm_materials) Required to load a printer that has swappable material profiles.
* [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support. * [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support.
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers * [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers.
Build scripts Build scripts
------------- -------------

View File

@ -1,10 +1,23 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
enable_testing() include(CTest)
include(CMakeParseArguments) include(CMakeParseArguments)
find_package(PythonInterp 3.5.0 REQUIRED) # FIXME: Remove the code for CMake <3.12 once we have switched over completely.
# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
# module is copied from the CMake repository here so in CMake <3.12 we can still use it.
if(${CMAKE_VERSION} VERSION_LESS 3.12)
# Use FindPythonInterp and FindPythonLibs for CMake <3.12
find_package(PythonInterp 3 REQUIRED)
set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
else()
# Use FindPython3 for CMake >=3.12
find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
endif()
add_custom_target(test-verbose COMMAND ${CMAKE_CTEST_COMMAND} --verbose)
function(cura_add_test) function(cura_add_test)
set(_single_args NAME DIRECTORY PYTHONPATH) set(_single_args NAME DIRECTORY PYTHONPATH)
@ -34,7 +47,7 @@ function(cura_add_test)
if (NOT ${test_exists}) if (NOT ${test_exists})
add_test( add_test(
NAME ${_NAME} NAME ${_NAME}
COMMAND ${PYTHON_EXECUTABLE} -m pytest --verbose --full-trace --capture=no --no-print-log --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY} COMMAND ${Python3_EXECUTABLE} -m pytest --verbose --full-trace --capture=no --no-print-log --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
) )
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C) set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C)
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}") set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")
@ -57,13 +70,13 @@ endforeach()
#Add code style test. #Add code style test.
add_test( add_test(
NAME "code-style" NAME "code-style"
COMMAND ${PYTHON_EXECUTABLE} run_mypy.py COMMAND ${Python3_EXECUTABLE} run_mypy.py
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
) )
#Add test for whether the shortcut alt-keys are unique in every translation. #Add test for whether the shortcut alt-keys are unique in every translation.
add_test( add_test(
NAME "shortcut-keys" NAME "shortcut-keys"
COMMAND ${PYTHON_EXECUTABLE} scripts/check_shortcut_keys.py COMMAND ${Python3_EXECUTABLE} scripts/check_shortcut_keys.py
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
) )

19
contributing.md Normal file
View File

@ -0,0 +1,19 @@
Submitting bug reports
----------------------
Please submit bug reports for all of Cura and CuraEngine to the [Cura repository](https://github.com/Ultimaker/Cura/issues). There will be a template there to fill in. Depending on the type of issue, we will usually ask for the [Cura log](Logging Issues) or a project file.
If a bug report would contain private information, such as a proprietary 3D model, you may also e-mail us. Ask for contact information in the issue.
Bugs related to supporting certain types of printers can usually not be solved by the Cura maintainers, since we don't have access to every 3D printer model in the world either. We have to rely on external contributors to fix this. If it's something simple and obvious, such as a mistake in the start g-code, then we can directly fix it for you, but e.g. issues with USB cable connectivity are impossible for us to debug.
Requesting features
-------------------
The issue template in the Cura repository does not apply to feature requests. You can ignore it.
When requesting a feature, please describe clearly what you need and why you think this is valuable to users or what problem it solves.
Making pull requests
--------------------
If you want to propose a change to Cura's source code, please create a pull request in the appropriate repository (being [Cura](https://github.com/Ultimaker/Cura), [Uranium](https://github.com/Ultimaker/Uranium), [CuraEngine](https://github.com/Ultimaker/CuraEngine), [fdm_materials](https://github.com/Ultimaker/fdm_materials), [libArcus](https://github.com/Ultimaker/libArcus), [cura-build](https://github.com/Ultimaker/cura-build), [cura-build-environment](https://github.com/Ultimaker/cura-build-environment), [libSavitar](https://github.com/Ultimaker/libSavitar), [libCharon](https://github.com/Ultimaker/libCharon) or [cura-binary-data](https://github.com/Ultimaker/cura-binary-data)) and if your change requires changes on multiple of these repositories, please link them together so that we know to merge them together.
Some of these repositories will have automated tests running when you create a pull request, indicated by green check marks or red crosses in the Github web page. If you see a red cross, that means that a test has failed. If the test doesn't fail on the Master branch but does fail on your branch, that indicates that you've probably made a mistake and you need to do that. Click on the cross for more details, or run the test locally by running `cmake . && ctest --verbose`.

View File

@ -13,6 +13,6 @@ TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
Icon=cura-icon Icon=cura-icon
Terminal=false Terminal=false
Type=Application Type=Application
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml; MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;text/x-gcode;
Categories=Graphics; Categories=Graphics;
Keywords=3D;Printing;Slicer; Keywords=3D;Printing;Slicer;

View File

@ -19,4 +19,12 @@
<glob-deleteall/> <glob-deleteall/>
<glob pattern="*.obj"/> <glob pattern="*.obj"/>
</mime-type> </mime-type>
<mime-type type="text/x-gcode">
<sub-class-of type="text/plain"/>
<comment>Gcode file</comment>
<icon name="unknown"/>
<glob-deleteall/>
<glob pattern="*.gcode"/>
<glob pattern="*.g"/>
</mime-type>
</mime-info> </mime-info>

View File

@ -6,6 +6,7 @@ from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Message import Message from UM.Message import Message
from cura import UltimakerCloudAuthentication
from cura.OAuth2.AuthorizationService import AuthorizationService from cura.OAuth2.AuthorizationService import AuthorizationService
from cura.OAuth2.Models import OAuth2Settings from cura.OAuth2.Models import OAuth2Settings
@ -37,15 +38,16 @@ class Account(QObject):
self._logged_in = False self._logged_in = False
self._callback_port = 32118 self._callback_port = 32118
self._oauth_root = "https://account.ultimaker.com" self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
self._cloud_api_root = "https://api.ultimaker.com"
self._oauth_settings = OAuth2Settings( self._oauth_settings = OAuth2Settings(
OAUTH_SERVER_URL= self._oauth_root, OAUTH_SERVER_URL= self._oauth_root,
CALLBACK_PORT=self._callback_port, CALLBACK_PORT=self._callback_port,
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port), CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
CLIENT_ID="um---------------ultimaker_cura_drive_plugin", CLIENT_ID="um----------------------------ultimaker_cura",
CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.write", CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download "
"packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write "
"cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write",
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data", AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root), AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root) AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
@ -55,11 +57,15 @@ class Account(QObject):
def initialize(self) -> None: def initialize(self) -> None:
self._authorization_service.initialize(self._application.getPreferences()) self._authorization_service.initialize(self._application.getPreferences())
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged) self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged) self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
self._authorization_service.loadAuthDataFromPreferences() self._authorization_service.loadAuthDataFromPreferences()
## Returns a boolean indicating whether the given authentication is applied against staging or not.
@property
def is_staging(self) -> bool:
return "staging" in self._oauth_root
@pyqtProperty(bool, notify=loginStateChanged) @pyqtProperty(bool, notify=loginStateChanged)
def isLoggedIn(self) -> bool: def isLoggedIn(self) -> bool:
return self._logged_in return self._logged_in
@ -70,6 +76,9 @@ class Account(QObject):
self._error_message.hide() self._error_message.hide()
self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed")) self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed"))
self._error_message.show() self._error_message.show()
self._logged_in = False
self.loginStateChanged.emit(False)
return
if self._logged_in != logged_in: if self._logged_in != logged_in:
self._logged_in = logged_in self._logged_in = logged_in

View File

@ -1,6 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Tuple, Optional, TYPE_CHECKING from typing import Tuple, Optional, TYPE_CHECKING, Dict, Any
from cura.Backups.BackupsManager import BackupsManager from cura.Backups.BackupsManager import BackupsManager
@ -24,12 +24,12 @@ class Backups:
## Create a new back-up using the BackupsManager. ## Create a new back-up using the BackupsManager.
# \return Tuple containing a ZIP file with the back-up data and a dict # \return Tuple containing a ZIP file with the back-up data and a dict
# with metadata about the back-up. # with metadata about the back-up.
def createBackup(self) -> Tuple[Optional[bytes], Optional[dict]]: def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]:
return self.manager.createBackup() return self.manager.createBackup()
## Restore a back-up using the BackupsManager. ## Restore a back-up using the BackupsManager.
# \param zip_file A ZIP file containing the actual back-up data. # \param zip_file A ZIP file containing the actual back-up data.
# \param meta_data Some metadata needed for restoring a back-up, like the # \param meta_data Some metadata needed for restoring a back-up, like the
# Cura version number. # Cura version number.
def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None: def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None:
return self.manager.restoreBackup(zip_file, meta_data) return self.manager.restoreBackup(zip_file, meta_data)

View File

@ -3,7 +3,6 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from UM.PluginRegistry import PluginRegistry
from cura.API.Interface.Settings import Settings from cura.API.Interface.Settings import Settings
if TYPE_CHECKING: if TYPE_CHECKING:
@ -23,9 +22,6 @@ if TYPE_CHECKING:
class Interface: class Interface:
# For now we use the same API version to be consistent.
VERSION = PluginRegistry.APIVersion
def __init__(self, application: "CuraApplication") -> None: def __init__(self, application: "CuraApplication") -> None:
# API methods specific to the settings portion of the UI # API methods specific to the settings portion of the UI
self.settings = Settings(application) self.settings = Settings(application)

View File

@ -4,7 +4,6 @@ from typing import Optional, TYPE_CHECKING
from PyQt5.QtCore import QObject, pyqtProperty from PyQt5.QtCore import QObject, pyqtProperty
from UM.PluginRegistry import PluginRegistry
from cura.API.Backups import Backups from cura.API.Backups import Backups
from cura.API.Interface import Interface from cura.API.Interface import Interface
from cura.API.Account import Account from cura.API.Account import Account
@ -22,7 +21,6 @@ if TYPE_CHECKING:
class CuraAPI(QObject): class CuraAPI(QObject):
# For now we use the same API version to be consistent. # For now we use the same API version to be consistent.
VERSION = PluginRegistry.APIVersion
__instance = None # type: "CuraAPI" __instance = None # type: "CuraAPI"
_application = None # type: CuraApplication _application = None # type: CuraApplication
@ -62,4 +60,4 @@ class CuraAPI(QObject):
@property @property
def interface(self) -> "Interface": def interface(self) -> "Interface":
return self._interface return self._interface

View File

@ -0,0 +1,50 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# ---------
# General constants used in Cura
# ---------
DEFAULT_CURA_APP_NAME = "cura"
DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
DEFAULT_CURA_VERSION = "master"
DEFAULT_CURA_BUILD_TYPE = ""
DEFAULT_CURA_DEBUG_MODE = False
DEFAULT_CURA_SDK_VERSION = "6.0.0"
try:
from cura.CuraVersion import CuraAppName # type: ignore
if CuraAppName == "":
CuraAppName = DEFAULT_CURA_APP_NAME
except ImportError:
CuraAppName = DEFAULT_CURA_APP_NAME
try:
from cura.CuraVersion import CuraAppDisplayName # type: ignore
if CuraAppDisplayName == "":
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
except ImportError:
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
try:
from cura.CuraVersion import CuraVersion # type: ignore
if CuraVersion == "":
CuraVersion = DEFAULT_CURA_VERSION
except ImportError:
CuraVersion = DEFAULT_CURA_VERSION # [CodeStyle: Reflecting imported value]
try:
from cura.CuraVersion import CuraBuildType # type: ignore
except ImportError:
CuraBuildType = DEFAULT_CURA_BUILD_TYPE
try:
from cura.CuraVersion import CuraDebugMode # type: ignore
except ImportError:
CuraDebugMode = DEFAULT_CURA_DEBUG_MODE
try:
from cura.CuraVersion import CuraSDKVersion # type: ignore
if CuraSDKVersion == "":
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION
except ImportError:
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION

View File

@ -66,6 +66,11 @@ class Arrange:
continue continue
vertices = vertices.getMinkowskiHull(Polygon.approximatedCircle(min_offset)) vertices = vertices.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
points = copy.deepcopy(vertices._points) points = copy.deepcopy(vertices._points)
# After scaling (like up to 0.1 mm) the node might not have points
if len(points) == 0:
continue
shape_arr = ShapeArray.fromPolygon(points, scale = scale) shape_arr = ShapeArray.fromPolygon(points, scale = scale)
arranger.place(0, 0, shape_arr) arranger.place(0, 0, shape_arr)
@ -212,11 +217,6 @@ class Arrange:
prio_slice = self._priority[min_y:max_y, min_x:max_x] prio_slice = self._priority[min_y:max_y, min_x:max_x]
prio_slice[new_occupied] = 999 prio_slice[new_occupied] = 999
# If you want to see how the rasterized arranger build plate looks like, uncomment this code
# numpy.set_printoptions(linewidth=500, edgeitems=200)
# print(self._occupied.shape)
# print(self._occupied)
@property @property
def isEmpty(self): def isEmpty(self):
return self._is_empty return self._is_empty

View File

@ -1,4 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application from UM.Application import Application
@ -48,7 +48,6 @@ class ArrangeArray:
return self._count return self._count
def get(self, index): def get(self, index):
print(self._arrange)
return self._arrange[index] return self._arrange[index]
def getFirstEmpty(self): def getFirstEmpty(self):

View File

@ -1,4 +1,4 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application from UM.Application import Application
@ -39,10 +39,17 @@ class ArrangeObjectsJob(Job):
arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes, min_offset = self._min_offset) arranger = Arrange.create(x = machine_width, y = machine_depth, fixed_nodes = self._fixed_nodes, min_offset = self._min_offset)
# Build set to exclude children (those get arranged together with the parents).
included_as_child = set()
for node in self._nodes:
included_as_child.update(node.getAllChildren())
# Collect nodes to be placed # Collect nodes to be placed
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr) nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
for node in self._nodes: for node in self._nodes:
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset) if node in included_as_child:
continue
offset_shape_arr, hull_shape_arr = ShapeArray.fromNode(node, min_offset = self._min_offset, include_children = True)
if offset_shape_arr is None: if offset_shape_arr is None:
Logger.log("w", "Node [%s] could not be converted to an array for arranging...", str(node)) Logger.log("w", "Node [%s] could not be converted to an array for arranging...", str(node))
continue continue

View File

@ -42,7 +42,7 @@ class ShapeArray:
# \param min_offset offset for the offset ShapeArray # \param min_offset offset for the offset ShapeArray
# \param scale scale the coordinates # \param scale scale the coordinates
@classmethod @classmethod
def fromNode(cls, node, min_offset, scale = 0.5): def fromNode(cls, node, min_offset, scale = 0.5, include_children = False):
transform = node._transformation transform = node._transformation
transform_x = transform._data[0][3] transform_x = transform._data[0][3]
transform_y = transform._data[2][3] transform_y = transform._data[2][3]
@ -52,6 +52,21 @@ class ShapeArray:
return None, None return None, None
# For one_at_a_time printing you need the convex hull head. # For one_at_a_time printing you need the convex hull head.
hull_head_verts = node.callDecoration("getConvexHullHead") or hull_verts hull_head_verts = node.callDecoration("getConvexHullHead") or hull_verts
if hull_head_verts is None:
hull_head_verts = Polygon()
# If the child-nodes are included, adjust convex hulls as well:
if include_children:
children = node.getAllChildren()
if not children is None:
for child in children:
# 'Inefficient' combination of convex hulls through known code rather than mess it up:
child_hull = child.callDecoration("getConvexHull")
if not child_hull is None:
hull_verts = hull_verts.unionConvexHulls(child_hull)
child_hull_head = child.callDecoration("getConvexHullHead") or child_hull
if not child_hull_head is None:
hull_head_verts = hull_head_verts.unionConvexHulls(child_hull_head)
offset_verts = hull_head_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset)) offset_verts = hull_head_verts.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
offset_points = copy.deepcopy(offset_verts._points) # x, y offset_points = copy.deepcopy(offset_verts._points) # x, y

View File

@ -46,12 +46,13 @@ class Backup:
# We copy the preferences file to the user data directory in Linux as it's in a different location there. # We copy the preferences file to the user data directory in Linux as it's in a different location there.
# When restoring a backup on Linux, we move it back. # When restoring a backup on Linux, we move it back.
if Platform.isLinux(): if Platform.isLinux(): #TODO: This should check for the config directory not being the same as the data directory, rather than hard-coding that to Linux systems.
preferences_file_name = self._application.getApplicationName() preferences_file_name = self._application.getApplicationName()
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name)) preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name)) backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file) if os.path.exists(preferences_file) and (not os.path.exists(backup_preferences_file) or not os.path.samefile(preferences_file, backup_preferences_file)):
shutil.copyfile(preferences_file, backup_preferences_file) Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
shutil.copyfile(preferences_file, backup_preferences_file)
# Create an empty buffer and write the archive to it. # Create an empty buffer and write the archive to it.
buffer = io.BytesIO() buffer = io.BytesIO()
@ -115,12 +116,13 @@ class Backup:
current_version = self._application.getVersion() current_version = self._application.getVersion()
version_to_restore = self.meta_data.get("cura_release", "master") version_to_restore = self.meta_data.get("cura_release", "master")
if current_version != version_to_restore:
# Cannot restore version older or newer than current because settings might have changed. if current_version < version_to_restore:
# Restoring this will cause a lot of issues so we don't allow this for now. # Cannot restore version newer than current because settings might have changed.
Logger.log("d", "Tried to restore a Cura backup of version {version_to_restore} with cura version {current_version}".format(version_to_restore = version_to_restore, current_version = current_version))
self._showMessage( self._showMessage(
self.catalog.i18nc("@info:backup_failed", self.catalog.i18nc("@info:backup_failed",
"Tried to restore a Cura backup that does not match your current version.")) "Tried to restore a Cura backup that is higher than the current version."))
return False return False
version_data_dir = Resources.getDataStoragePath() version_data_dir = Resources.getDataStoragePath()

View File

@ -1,6 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Scene.Camera import Camera
from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from UM.Application import Application #To modify the maximum zoom level. from UM.Application import Application #To modify the maximum zoom level.
@ -83,7 +83,14 @@ class BuildVolume(SceneNode):
" with printed models."), title = catalog.i18nc("@info:title", "Build Volume")) " with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
self._global_container_stack = None self._global_container_stack = None
self._stack_change_timer = QTimer()
self._stack_change_timer.setInterval(100)
self._stack_change_timer.setSingleShot(True)
self._stack_change_timer.timeout.connect(self._onStackChangeTimerFinished)
self._application.globalContainerStackChanged.connect(self._onStackChanged) self._application.globalContainerStackChanged.connect(self._onStackChanged)
self._onStackChanged() self._onStackChanged()
self._engine_ready = False self._engine_ready = False
@ -122,7 +129,9 @@ class BuildVolume(SceneNode):
def _onSceneChanged(self, source): def _onSceneChanged(self, source):
if self._global_container_stack: if self._global_container_stack:
self._scene_change_timer.start() # Ignore anything that is not something we can slice in the first place!
if source.callDecoration("isSliceable"):
self._scene_change_timer.start()
def _onSceneChangeTimerFinished(self): def _onSceneChangeTimerFinished(self):
root = self._application.getController().getScene().getRoot() root = self._application.getController().getScene().getRoot()
@ -139,7 +148,7 @@ class BuildVolume(SceneNode):
if active_extruder_changed is not None: if active_extruder_changed is not None:
node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild) node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild)
node.decoratorsChanged.disconnect(self._updateNodeListeners) node.decoratorsChanged.disconnect(self._updateNodeListeners)
self._updateDisallowedAreasAndRebuild() # make sure we didn't miss anything before we updated the node listeners self.rebuild()
self._scene_objects = new_scene_objects self._scene_objects = new_scene_objects
self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered. self._onSettingPropertyChanged("print_sequence", "value") # Create fake event, so right settings are triggered.
@ -479,6 +488,8 @@ class BuildVolume(SceneNode):
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1) maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
) )
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds
self.updateNodeBoundaryCheck() self.updateNodeBoundaryCheck()
def getBoundingBox(self) -> AxisAlignedBox: def getBoundingBox(self) -> AxisAlignedBox:
@ -489,7 +500,9 @@ class BuildVolume(SceneNode):
def _updateRaftThickness(self): def _updateRaftThickness(self):
old_raft_thickness = self._raft_thickness old_raft_thickness = self._raft_thickness
self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value") if self._global_container_stack.extruders:
# This might be called before the extruder stacks have initialised, in which case getting the adhesion_type fails
self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value")
self._raft_thickness = 0.0 self._raft_thickness = 0.0
if self._adhesion_type == "raft": if self._adhesion_type == "raft":
self._raft_thickness = ( self._raft_thickness = (
@ -522,8 +535,11 @@ class BuildVolume(SceneNode):
if extra_z != self._extra_z_clearance: if extra_z != self._extra_z_clearance:
self._extra_z_clearance = extra_z self._extra_z_clearance = extra_z
## Update the build volume visualization
def _onStackChanged(self): def _onStackChanged(self):
self._stack_change_timer.start()
## Update the build volume visualization
def _onStackChangeTimerFinished(self):
if self._global_container_stack: if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged) self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
extruders = ExtruderManager.getInstance().getActiveExtruderStacks() extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
@ -651,6 +667,7 @@ class BuildVolume(SceneNode):
# ``_updateDisallowedAreas`` method itself shouldn't call ``rebuild``, # ``_updateDisallowedAreas`` method itself shouldn't call ``rebuild``,
# since there may be other changes before it needs to be rebuilt, which # since there may be other changes before it needs to be rebuilt, which
# would hit performance. # would hit performance.
def _updateDisallowedAreasAndRebuild(self): def _updateDisallowedAreasAndRebuild(self):
self._updateDisallowedAreas() self._updateDisallowedAreas()
self._updateRaftThickness() self._updateRaftThickness()
@ -723,13 +740,17 @@ class BuildVolume(SceneNode):
prime_tower_collision = False prime_tower_collision = False
prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders) prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
for extruder_id in prime_tower_areas: for extruder_id in prime_tower_areas:
for prime_tower_area in prime_tower_areas[extruder_id]: for i_area, prime_tower_area in enumerate(prime_tower_areas[extruder_id]):
for area in result_areas[extruder_id]: for area in result_areas[extruder_id]:
if prime_tower_area.intersectsPolygon(area) is not None: if prime_tower_area.intersectsPolygon(area) is not None:
prime_tower_collision = True prime_tower_collision = True
break break
if prime_tower_collision: #Already found a collision. if prime_tower_collision: #Already found a collision.
break break
if (ExtruderManager.getInstance().getResolveOrValue("prime_tower_brim_enable") and
ExtruderManager.getInstance().getResolveOrValue("adhesion_type") != "raft"):
prime_tower_areas[extruder_id][i_area] = prime_tower_area.getMinkowskiHull(
Polygon.approximatedCircle(disallowed_border_size))
if not prime_tower_collision: if not prime_tower_collision:
result_areas[extruder_id].extend(prime_tower_areas[extruder_id]) result_areas[extruder_id].extend(prime_tower_areas[extruder_id])
result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id]) result_areas_no_brim[extruder_id].extend(prime_tower_areas[extruder_id])
@ -769,6 +790,16 @@ class BuildVolume(SceneNode):
prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left. prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
prime_tower_y = prime_tower_y + machine_depth / 2 prime_tower_y = prime_tower_y + machine_depth / 2
if (ExtruderManager.getInstance().getResolveOrValue("prime_tower_brim_enable") and
ExtruderManager.getInstance().getResolveOrValue("adhesion_type") != "raft"):
brim_size = (
extruder.getProperty("brim_line_count", "value") *
extruder.getProperty("skirt_brim_line_width", "value") / 100.0 *
extruder.getProperty("initial_layer_line_width_factor", "value")
)
prime_tower_x -= brim_size
prime_tower_y += brim_size
if self._global_container_stack.getProperty("prime_tower_circular", "value"): if self._global_container_stack.getProperty("prime_tower_circular", "value"):
radius = prime_tower_size / 2 radius = prime_tower_size / 2
prime_tower_area = Polygon.approximatedCircle(radius) prime_tower_area = Polygon.approximatedCircle(radius)
@ -1008,7 +1039,9 @@ class BuildVolume(SceneNode):
# We don't create an additional line for the extruder we're printing the skirt with. # We don't create an additional line for the extruder we're printing the skirt with.
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0 bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
elif adhesion_type == "brim": elif (adhesion_type == "brim" or
(self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and
self._global_container_stack.getProperty("adhesion_type", "value") != "raft")):
brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value") brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0 bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
@ -1027,6 +1060,12 @@ class BuildVolume(SceneNode):
else: else:
raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?") raise Exception("Unknown bed adhesion type. Did you forget to update the build volume calculations for your new bed adhesion type?")
max_length_available = 0.5 * min(
self._global_container_stack.getProperty("machine_width", "value"),
self._global_container_stack.getProperty("machine_depth", "value")
)
bed_adhesion_size = min(bed_adhesion_size, max_length_available)
support_expansion = 0 support_expansion = 0
support_enabled = self._global_container_stack.getProperty("support_enable", "value") support_enabled = self._global_container_stack.getProperty("support_enable", "value")
support_offset = self._global_container_stack.getProperty("support_offset", "value") support_offset = self._global_container_stack.getProperty("support_offset", "value")
@ -1061,7 +1100,7 @@ class BuildVolume(SceneNode):
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"] _raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
_extra_z_settings = ["retraction_hop_enabled", "retraction_hop"] _extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"] _prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "extruder_prime_pos_z", "prime_blob_enable"]
_tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y"] _tower_settings = ["prime_tower_enable", "prime_tower_circular", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable"]
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"] _ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports"] _distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports"]
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used. _extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.

View File

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

View File

@ -36,18 +36,14 @@ else:
except ImportError: except ImportError:
CuraDebugMode = False # [CodeStyle: Reflecting imported value] CuraDebugMode = False # [CodeStyle: Reflecting imported value]
# List of exceptions that should be considered "fatal" and abort the program. # List of exceptions that should not be considered "fatal" and abort the program.
# These are primarily some exception types that we simply cannot really recover from # These are primarily some exception types that we simply skip
# (MemoryError and SystemError) and exceptions that indicate grave errors in the skip_exception_types = [
# code that cause the Python interpreter to fail (SyntaxError, ImportError). SystemExit,
fatal_exception_types = [ KeyboardInterrupt,
MemoryError, GeneratorExit
SyntaxError,
ImportError,
SystemError,
] ]
class CrashHandler: class CrashHandler:
crash_url = "https://stats.ultimaker.com/api/cura" crash_url = "https://stats.ultimaker.com/api/cura"
@ -70,7 +66,7 @@ class CrashHandler:
# If Cura has fully started, we only show fatal errors. # If Cura has fully started, we only show fatal errors.
# If Cura has not fully started yet, we always show the early crash dialog. Otherwise, Cura will just crash # If Cura has not fully started yet, we always show the early crash dialog. Otherwise, Cura will just crash
# without any information. # without any information.
if has_started and exception_type not in fatal_exception_types: if has_started and exception_type in skip_exception_types:
return return
if not has_started: if not has_started:
@ -387,7 +383,7 @@ class CrashHandler:
Application.getInstance().callLater(self._show) Application.getInstance().callLater(self._show)
def _show(self): def _show(self):
# When the exception is not in the fatal_exception_types list, the dialog is not created, so we don't need to show it # When the exception is in the skip_exception_types list, the dialog is not created, so we don't need to show it
if self.dialog: if self.dialog:
self.dialog.exec_() self.dialog.exec_()
os._exit(1) os._exit(1)

View File

@ -3,7 +3,7 @@
from PyQt5.QtCore import QObject, QUrl from PyQt5.QtCore import QObject, QUrl
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from typing import List, TYPE_CHECKING from typing import List, TYPE_CHECKING, cast
from UM.Event import CallFunctionEvent from UM.Event import CallFunctionEvent
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
@ -36,12 +36,12 @@ class CuraActions(QObject):
# Starting a web browser from a signal handler connected to a menu will crash on windows. # Starting a web browser from a signal handler connected to a menu will crash on windows.
# So instead, defer the call to the next run of the event loop, since that does work. # So instead, defer the call to the next run of the event loop, since that does work.
# Note that weirdly enough, only signal handlers that open a web browser fail like that. # Note that weirdly enough, only signal handlers that open a web browser fail like that.
event = CallFunctionEvent(self._openUrl, [QUrl("http://ultimaker.com/en/support/software")], {}) event = CallFunctionEvent(self._openUrl, [QUrl("https://ultimaker.com/en/resources/manuals/software")], {})
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event) cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
@pyqtSlot() @pyqtSlot()
def openBugReportPage(self) -> None: def openBugReportPage(self) -> None:
event = CallFunctionEvent(self._openUrl, [QUrl("http://github.com/Ultimaker/Cura/issues")], {}) event = CallFunctionEvent(self._openUrl, [QUrl("https://github.com/Ultimaker/Cura/issues")], {})
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event) cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
## Reset camera position and direction to default ## Reset camera position and direction to default
@ -61,8 +61,10 @@ class CuraActions(QObject):
operation = GroupedOperation() operation = GroupedOperation()
for node in Selection.getAllSelectedObjects(): for node in Selection.getAllSelectedObjects():
current_node = node current_node = node
while current_node.getParent() and current_node.getParent().callDecoration("isGroup"): parent_node = current_node.getParent()
current_node = current_node.getParent() while parent_node and parent_node.callDecoration("isGroup"):
current_node = parent_node
parent_node = current_node.getParent()
# This was formerly done with SetTransformOperation but because of # This was formerly done with SetTransformOperation but because of
# unpredictable matrix deconstruction it was possible that mirrors # unpredictable matrix deconstruction it was possible that mirrors
@ -150,13 +152,13 @@ class CuraActions(QObject):
root = cura.CuraApplication.CuraApplication.getInstance().getController().getScene().getRoot() root = cura.CuraApplication.CuraApplication.getInstance().getController().getScene().getRoot()
nodes_to_change = [] nodes_to_change = [] # type: List[SceneNode]
for node in Selection.getAllSelectedObjects(): for node in Selection.getAllSelectedObjects():
parent_node = node # Find the parent node to change instead parent_node = node # Find the parent node to change instead
while parent_node.getParent() != root: while parent_node.getParent() != root:
parent_node = parent_node.getParent() parent_node = cast(SceneNode, parent_node.getParent())
for single_node in BreadthFirstIterator(parent_node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. for single_node in BreadthFirstIterator(parent_node): # type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
nodes_to_change.append(single_node) nodes_to_change.append(single_node)
if not nodes_to_change: if not nodes_to_change:

View File

@ -1,10 +1,10 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os import os
import sys import sys
import time import time
from typing import cast, TYPE_CHECKING, Optional, Callable from typing import cast, TYPE_CHECKING, Optional, Callable, List
import numpy import numpy
@ -13,109 +13,118 @@ from PyQt5.QtGui import QColor, QIcon
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
from UM.i18n import i18nCatalog
from UM.Application import Application from UM.Application import Application
from UM.Decorators import override
from UM.FlameProfiler import pyqtSlot
from UM.Logger import Logger
from UM.Message import Message
from UM.Platform import Platform
from UM.PluginError import PluginNotFoundError from UM.PluginError import PluginNotFoundError
from UM.Scene.SceneNode import SceneNode from UM.Resources import Resources
from UM.Scene.Camera import Camera from UM.Preferences import Preferences
from UM.Math.Vector import Vector from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
from UM.Math.Quaternion import Quaternion import UM.Util
from UM.View.SelectionPass import SelectionPass # For typing.
from UM.Math.AxisAlignedBox import AxisAlignedBox from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Matrix import Matrix from UM.Math.Matrix import Matrix
from UM.Platform import Platform from UM.Math.Quaternion import Quaternion
from UM.Resources import Resources from UM.Math.Vector import Vector
from UM.Scene.ToolHandle import ToolHandle
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Mesh.ReadMeshJob import ReadMeshJob from UM.Mesh.ReadMeshJob import ReadMeshJob
from UM.Logger import Logger
from UM.Preferences import Preferences
from UM.Qt.QtApplication import QtApplication #The class we're inheriting from.
from UM.View.SelectionPass import SelectionPass #For typing.
from UM.Scene.Selection import Selection
from UM.Scene.GroupDecorator import GroupDecorator
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.Validator import Validator
from UM.Message import Message
from UM.i18n import i18nCatalog
from UM.Workspace.WorkspaceReader import WorkspaceReader
from UM.Decorators import deprecated
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.SetTransformOperation import SetTransformOperation from UM.Operations.SetTransformOperation import SetTransformOperation
from UM.Scene.Camera import Camera
from UM.Scene.GroupDecorator import GroupDecorator
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection
from UM.Scene.ToolHandle import ToolHandle
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.Validator import Validator
from UM.Workspace.WorkspaceReader import WorkspaceReader
from cura.API import CuraAPI from cura.API import CuraAPI
from cura.Arranging.Arrange import Arrange from cura.Arranging.Arrange import Arrange
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
from cura.Arranging.ShapeArray import ShapeArray from cura.Arranging.ShapeArray import ShapeArray
from cura.MultiplyObjectsJob import MultiplyObjectsJob
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
from cura.Operations.SetParentOperation import SetParentOperation from cura.Operations.SetParentOperation import SetParentOperation
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.CuraSceneNode import CuraSceneNode from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
from cura.Scene.CuraSceneController import CuraSceneController from cura.Scene.CuraSceneController import CuraSceneController
from cura.Scene.CuraSceneNode import CuraSceneNode
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from UM.Settings.ContainerRegistry import ContainerRegistry from cura.Scene import ZOffsetDecorator
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
from cura.Machines.Models.NozzleModel import NozzleModel
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
from cura.Machines.Models.QualityManagementModel import QualityManagementModel
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
from cura.Machines.Models.MachineManagementModel import MachineManagementModel
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
from cura.Machines.MachineErrorChecker import MachineErrorChecker from cura.Machines.MachineErrorChecker import MachineErrorChecker
from cura.Machines.VariantManager import VariantManager
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
from cura.Machines.Models.DiscoveredPrintersModel import DiscoveredPrintersModel
from cura.Machines.Models.ExtrudersModel import ExtrudersModel
from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachineActionsModel
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
from cura.Machines.Models.NozzleModel import NozzleModel
from cura.Machines.Models.QualityManagementModel import QualityManagementModel
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
from cura.Machines.Models.UserChangesModel import UserChangesModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
import cura.Settings.cura_empty_instance_containers
from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.MachineManager import MachineManager
from cura.Settings.MachineNameValidator import MachineNameValidator
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
from cura.Machines.VariantManager import VariantManager from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
from cura.UI import CuraSplashScreen, MachineActionManager, PrintInformation
from cura.UI.MachineSettingsManager import MachineSettingsManager
from cura.UI.ObjectsModel import ObjectsModel
from cura.UI.TextManager import TextManager
from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
from cura.UI.WelcomePagesModel import WelcomePagesModel
from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel
from .SingleInstance import SingleInstance from .SingleInstance import SingleInstance
from .AutoSave import AutoSave from .AutoSave import AutoSave
from . import PlatformPhysics from . import PlatformPhysics
from . import BuildVolume from . import BuildVolume
from . import CameraAnimation from . import CameraAnimation
from . import PrintInformation
from . import CuraActions from . import CuraActions
from cura.Scene import ZOffsetDecorator
from . import CuraSplashScreen
from . import CameraImageProvider
from . import PrintJobPreviewImageProvider from . import PrintJobPreviewImageProvider
from . import MachineActionManager
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager from cura import ApplicationMetadata, UltimakerCloudAuthentication
from cura.Settings.MachineManager import MachineManager
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.UserChangesModel import UserChangesModel
from cura.Settings.ExtrudersModel import ExtrudersModel
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
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 UM.Decorators import override
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.Machines.MaterialManager import MaterialManager from cura.Machines.MaterialManager import MaterialManager
@ -126,20 +135,12 @@ if TYPE_CHECKING:
numpy.seterr(all = "ignore") numpy.seterr(all = "ignore")
try:
from cura.CuraVersion import CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion
except ImportError:
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
CuraBuildType = ""
CuraDebugMode = False
CuraSDKVersion = ""
class CuraApplication(QtApplication): class CuraApplication(QtApplication):
# SettingVersion represents the set of settings available in the machine/extruder definitions. # SettingVersion represents the set of settings available in the machine/extruder definitions.
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible # You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
# changes of the settings. # changes of the settings.
SettingVersion = 5 SettingVersion = 7
Created = False Created = False
@ -159,10 +160,12 @@ class CuraApplication(QtApplication):
Q_ENUMS(ResourceTypes) Q_ENUMS(ResourceTypes)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(name = "cura", super().__init__(name = ApplicationMetadata.CuraAppName,
version = CuraVersion, app_display_name = ApplicationMetadata.CuraAppDisplayName,
buildtype = CuraBuildType, version = ApplicationMetadata.CuraVersion,
is_debug_mode = CuraDebugMode, api_version = ApplicationMetadata.CuraSDKVersion,
buildtype = ApplicationMetadata.CuraBuildType,
is_debug_mode = ApplicationMetadata.CuraDebugMode,
tray_icon_name = "cura-icon-32.png", tray_icon_name = "cura-icon-32.png",
**kwargs) **kwargs)
@ -177,7 +180,6 @@ class CuraApplication(QtApplication):
# Variables set from CLI # Variables set from CLI
self._files_to_open = [] self._files_to_open = []
self._use_single_instance = False self._use_single_instance = False
self._trigger_early_crash = False # For debug only
self._single_instance = None self._single_instance = None
@ -202,6 +204,8 @@ class CuraApplication(QtApplication):
self._container_manager = None self._container_manager = None
self._object_manager = None self._object_manager = None
self._extruders_model = None
self._extruders_model_with_optional = None
self._build_plate_model = None self._build_plate_model = None
self._multi_build_plate_model = None self._multi_build_plate_model = None
self._setting_visibility_presets_model = None self._setting_visibility_presets_model = None
@ -210,6 +214,15 @@ class CuraApplication(QtApplication):
self._cura_scene_controller = None self._cura_scene_controller = None
self._machine_error_checker = None self._machine_error_checker = None
self._machine_settings_manager = MachineSettingsManager(self, parent = self)
self._discovered_printer_model = DiscoveredPrintersModel(parent = self)
self._first_start_machine_actions_model = FirstStartMachineActionsModel(self, parent = self)
self._welcome_pages_model = WelcomePagesModel(self, parent = self)
self._add_printer_pages_model = AddPrinterPagesModel(self, parent = self)
self._whats_new_pages_model = WhatsNewPagesModel(self, parent = self)
self._text_manager = TextManager(parent = self)
self._quality_profile_drop_down_menu_model = None self._quality_profile_drop_down_menu_model = None
self._custom_quality_profile_drop_down_menu_model = None self._custom_quality_profile_drop_down_menu_model = None
self._cura_API = CuraAPI(self) self._cura_API = CuraAPI(self)
@ -239,8 +252,6 @@ class CuraApplication(QtApplication):
self._update_platform_activity_timer = None self._update_platform_activity_timer = None
self._need_to_show_user_agreement = True
self._sidebar_custom_menu_items = [] # type: list # Keeps list of custom menu items for the side bar self._sidebar_custom_menu_items = [] # type: list # Keeps list of custom menu items for the side bar
self._plugins_loaded = False self._plugins_loaded = False
@ -256,6 +267,14 @@ class CuraApplication(QtApplication):
from cura.CuraPackageManager import CuraPackageManager from cura.CuraPackageManager import CuraPackageManager
self._package_manager_class = CuraPackageManager self._package_manager_class = CuraPackageManager
@pyqtProperty(str, constant=True)
def ultimakerCloudApiRootUrl(self) -> str:
return UltimakerCloudAuthentication.CuraCloudAPIRoot
@pyqtProperty(str, constant = True)
def ultimakerCloudAccountRootUrl(self) -> str:
return UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
# Adds command line options to the command line parser. This should be called after the application is created and # Adds command line options to the command line parser. This should be called after the application is created and
# before the pre-start. # before the pre-start.
def addCommandLineOptions(self): def addCommandLineOptions(self):
@ -288,7 +307,10 @@ class CuraApplication(QtApplication):
sys.exit(0) sys.exit(0)
self._use_single_instance = self._cli_args.single_instance self._use_single_instance = self._cli_args.single_instance
self._trigger_early_crash = self._cli_args.trigger_early_crash # FOR TESTING ONLY
if self._cli_args.trigger_early_crash:
assert not "This crash is triggered by the trigger_early_crash command line argument."
for filename in self._cli_args.file: for filename in self._cli_args.file:
self._files_to_open.append(os.path.abspath(filename)) self._files_to_open.append(os.path.abspath(filename))
@ -298,7 +320,8 @@ class CuraApplication(QtApplication):
super().initialize() super().initialize()
self.__sendCommandToSingleInstance() self.__sendCommandToSingleInstance()
self.__initializeSettingDefinitionsAndFunctions() self._initializeSettingDefinitions()
self._initializeSettingFunctions()
self.__addAllResourcesAndContainerResources() self.__addAllResourcesAndContainerResources()
self.__addAllEmptyContainers() self.__addAllEmptyContainers()
self.__setLatestResouceVersionsForVersionUpgrade() self.__setLatestResouceVersionsForVersionUpgrade()
@ -327,31 +350,40 @@ class CuraApplication(QtApplication):
resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources") resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")
Resources.addSearchPath(resource_path) Resources.addSearchPath(resource_path)
# Adds custom property types, settings types, and extra operators (functions) that need to be registered in @classmethod
# SettingDefinition and SettingFunction. def _initializeSettingDefinitions(cls):
def __initializeSettingDefinitionsAndFunctions(self):
self._cura_formula_functions = CuraFormulaFunctions(self)
# Need to do this before ContainerRegistry tries to load the machines # Need to do this before ContainerRegistry tries to load the machines
SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True, read_only = True) SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default=True,
SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True, read_only = True) read_only=True)
SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default=True,
read_only=True)
# this setting can be changed for each group in one-at-a-time mode # this setting can be changed for each group in one-at-a-time mode
SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default = True, read_only = True) SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default=True,
SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default = True, read_only = True) read_only=True)
SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default=True,
read_only=True)
# From which stack the setting would inherit if not defined per object (handled in the engine) # From which stack the setting would inherit if not defined per object (handled in the engine)
# AND for settings which are not settable_per_mesh: # AND for settings which are not settable_per_mesh:
# which extruder is the only extruder this setting is obtained from # which extruder is the only extruder this setting is obtained from
SettingDefinition.addSupportedProperty("limit_to_extruder", DefinitionPropertyType.Function, default = "-1", depends_on = "value") SettingDefinition.addSupportedProperty("limit_to_extruder", DefinitionPropertyType.Function, default="-1",
depends_on="value")
# For settings which are not settable_per_mesh and not settable_per_extruder: # For settings which are not settable_per_mesh and not settable_per_extruder:
# A function which determines the glabel/meshgroup value by looking at the values of the setting in all (used) extruders # A function which determines the glabel/meshgroup value by looking at the values of the setting in all (used) extruders
SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default = None, depends_on = "value") SettingDefinition.addSupportedProperty("resolve", DefinitionPropertyType.Function, default=None,
depends_on="value")
SettingDefinition.addSettingType("extruder", None, str, Validator) SettingDefinition.addSettingType("extruder", None, str, Validator)
SettingDefinition.addSettingType("optional_extruder", None, str, None) SettingDefinition.addSettingType("optional_extruder", None, str, None)
SettingDefinition.addSettingType("[int]", None, str, None) SettingDefinition.addSettingType("[int]", None, str, None)
# Adds custom property types, settings types, and extra operators (functions) that need to be registered in
# SettingDefinition and SettingFunction.
def _initializeSettingFunctions(self):
self._cura_formula_functions = CuraFormulaFunctions(self)
SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder) SettingFunction.registerOperator("extruderValue", self._cura_formula_functions.getValueInExtruder)
SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders) SettingFunction.registerOperator("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue) SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
@ -424,38 +456,35 @@ class CuraApplication(QtApplication):
def startSplashWindowPhase(self) -> None: def startSplashWindowPhase(self) -> None:
super().startSplashWindowPhase() super().startSplashWindowPhase()
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png"))) if not self.getIsHeadLess():
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
self.setRequiredPlugins([ self.setRequiredPlugins([
# Misc.: # Misc.:
"ConsoleLogger", "ConsoleLogger", #You want to be able to read the log if something goes wrong.
"CuraEngineBackend", "CuraEngineBackend", #Cura is useless without this one since you can't slice.
"UserAgreement", "FileLogger", #You want to be able to read the log if something goes wrong.
"FileLogger", "XmlMaterialProfile", #Cura crashes without this one.
"XmlMaterialProfile", "Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
"Toolbox", "PrepareStage", #Cura is useless without this one since you can't load models.
"PrepareStage", "PreviewStage", #This shows the list of the plugin views that are installed in Cura.
"MonitorStage", "MonitorStage", #Major part of Cura's functionality.
"LocalFileOutputDevice", "LocalFileOutputDevice", #Major part of Cura's functionality.
"LocalContainerProvider", "LocalContainerProvider", #Cura is useless without any profiles or setting definitions.
# Views: # Views:
"SimpleView", "SimpleView", #Dependency of SolidView.
"SimulationView", "SolidView", #Displays models. Cura is useless without it.
"SolidView",
# Readers & Writers: # Readers & Writers:
"GCodeWriter", "GCodeWriter", #Cura is useless if it can't write its output.
"STLReader", "STLReader", #Most common model format, so disabling this makes Cura 90% useless.
"3MFWriter", "3MFWriter", #Required for writing project files.
# Tools: # Tools:
"CameraTool", "CameraTool", #Needed to see the scene. Cura is useless without it.
"MirrorTool", "SelectionTool", #Dependency of the rest of the tools.
"RotateTool", "TranslateTool", #You'll need this for almost every print.
"ScaleTool",
"SelectionTool",
"TranslateTool",
]) ])
self._i18n_catalog = i18nCatalog("cura") self._i18n_catalog = i18nCatalog("cura")
@ -492,7 +521,8 @@ class CuraApplication(QtApplication):
preferences.addPreference("cura/choice_on_profile_override", "always_ask") preferences.addPreference("cura/choice_on_profile_override", "always_ask")
preferences.addPreference("cura/choice_on_open_project", "always_ask") preferences.addPreference("cura/choice_on_open_project", "always_ask")
preferences.addPreference("cura/use_multi_build_plate", False) preferences.addPreference("cura/use_multi_build_plate", False)
preferences.addPreference("view/settings_list_height", 400)
preferences.addPreference("view/settings_visible", False)
preferences.addPreference("cura/currency", "") preferences.addPreference("cura/currency", "")
preferences.addPreference("cura/material_settings", "{}") preferences.addPreference("cura/material_settings", "{}")
@ -504,7 +534,7 @@ class CuraApplication(QtApplication):
preferences.addPreference("cura/expanded_brands", "") preferences.addPreference("cura/expanded_brands", "")
preferences.addPreference("cura/expanded_types", "") preferences.addPreference("cura/expanded_types", "")
self._need_to_show_user_agreement = not preferences.getValue("general/accepted_user_agreement") preferences.addPreference("general/accepted_user_agreement", False)
for key in [ for key in [
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin "dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
@ -523,18 +553,24 @@ class CuraApplication(QtApplication):
CuraApplication.Created = True CuraApplication.Created = True
def _onEngineCreated(self): def _onEngineCreated(self):
self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider()) self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider())
@pyqtProperty(bool) @pyqtProperty(bool)
def needToShowUserAgreement(self) -> bool: def needToShowUserAgreement(self) -> bool:
return self._need_to_show_user_agreement return not UM.Util.parseBool(self.getPreferences().getValue("general/accepted_user_agreement"))
def setNeedToShowUserAgreement(self, set_value = True) -> None: @pyqtSlot(bool)
self._need_to_show_user_agreement = set_value def setNeedToShowUserAgreement(self, set_value: bool = True) -> None:
self.getPreferences().setValue("general/accepted_user_agreement", str(not set_value))
@pyqtSlot(str, str)
def writeToLog(self, severity: str, message: str) -> None:
Logger.log(severity, message)
# DO NOT call this function to close the application, use checkAndExitApplication() instead which will perform # 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. # pre-exit checks such as checking for in-progress USB printing, etc.
# Except for the 'Decline and close' in the 'User Agreement'-step in the Welcome-pages, that should be a hard exit.
@pyqtSlot()
def closeApplication(self) -> None: def closeApplication(self) -> None:
Logger.log("i", "Close application") Logger.log("i", "Close application")
main_window = self.getMainWindow() main_window = self.getMainWindow()
@ -632,9 +668,7 @@ class CuraApplication(QtApplication):
self._message_box_callback(button, *self._message_box_callback_arguments) self._message_box_callback(button, *self._message_box_callback_arguments)
self._message_box_callback = None self._message_box_callback = None
self._message_box_callback_arguments = [] self._message_box_callback_arguments = []
showPrintMonitor = pyqtSignal(bool, arguments = ["show"])
def setSaveDataEnabled(self, enabled: bool) -> None: def setSaveDataEnabled(self, enabled: bool) -> None:
self._save_data_enabled = enabled self._save_data_enabled = enabled
@ -660,12 +694,12 @@ class CuraApplication(QtApplication):
## Handle loading of all plugin types (and the backend explicitly) ## Handle loading of all plugin types (and the backend explicitly)
# \sa PluginRegistry # \sa PluginRegistry
def _loadPlugins(self): def _loadPlugins(self) -> None:
self._plugin_registry.addType("profile_reader", self._addProfileReader) self._plugin_registry.addType("profile_reader", self._addProfileReader)
self._plugin_registry.addType("profile_writer", self._addProfileWriter) self._plugin_registry.addType("profile_writer", self._addProfileWriter)
if Platform.isLinux(): if Platform.isLinux():
lib_suffixes = {"", "64", "32", "x32"} #A few common ones on different distributions. lib_suffixes = {"", "64", "32", "x32"} # A few common ones on different distributions.
else: else:
lib_suffixes = {""} lib_suffixes = {""}
for suffix in lib_suffixes: for suffix in lib_suffixes:
@ -730,6 +764,11 @@ class CuraApplication(QtApplication):
# Initialize Cura API # Initialize Cura API
self._cura_API.initialize() self._cura_API.initialize()
self._output_device_manager.start()
self._welcome_pages_model.initialize()
self._add_printer_pages_model.initialize()
self._whats_new_pages_model.initialize()
# Detect in which mode to run and execute that mode # Detect in which mode to run and execute that mode
if self._is_headless: if self._is_headless:
self.runWithoutGUI() self.runWithoutGUI()
@ -824,10 +863,38 @@ class CuraApplication(QtApplication):
# Hide the splash screen # Hide the splash screen
self.closeSplash() self.closeSplash()
@pyqtSlot(result = QObject)
def getDiscoveredPrintersModel(self, *args) -> "DiscoveredPrintersModel":
return self._discovered_printer_model
@pyqtSlot(result = QObject)
def getFirstStartMachineActionsModel(self, *args) -> "FirstStartMachineActionsModel":
return self._first_start_machine_actions_model
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel: def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
return self._setting_visibility_presets_model return self._setting_visibility_presets_model
@pyqtSlot(result = QObject)
def getWelcomePagesModel(self, *args) -> "WelcomePagesModel":
return self._welcome_pages_model
@pyqtSlot(result = QObject)
def getAddPrinterPagesModel(self, *args) -> "AddPrinterPagesModel":
return self._add_printer_pages_model
@pyqtSlot(result = QObject)
def getWhatsNewPagesModel(self, *args) -> "WhatsNewPagesModel":
return self._whats_new_pages_model
@pyqtSlot(result = QObject)
def getMachineSettingsManager(self, *args) -> "MachineSettingsManager":
return self._machine_settings_manager
@pyqtSlot(result = QObject)
def getTextManager(self, *args) -> "TextManager":
return self._text_manager
def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions": def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
if self._cura_formula_functions is None: if self._cura_formula_functions is None:
self._cura_formula_functions = CuraFormulaFunctions(self) self._cura_formula_functions = CuraFormulaFunctions(self)
@ -862,6 +929,19 @@ class CuraApplication(QtApplication):
self._object_manager = ObjectsModel.createObjectsModel() self._object_manager = ObjectsModel.createObjectsModel()
return self._object_manager return self._object_manager
@pyqtSlot(result = QObject)
def getExtrudersModel(self, *args) -> "ExtrudersModel":
if self._extruders_model is None:
self._extruders_model = ExtrudersModel(self)
return self._extruders_model
@pyqtSlot(result = QObject)
def getExtrudersModelWithOptional(self, *args) -> "ExtrudersModel":
if self._extruders_model_with_optional is None:
self._extruders_model_with_optional = ExtrudersModel(self)
self._extruders_model_with_optional.setAddOptionalExtruder(True)
return self._extruders_model_with_optional
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel: def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel:
if self._multi_build_plate_model is None: if self._multi_build_plate_model is None:
@ -936,7 +1016,7 @@ class CuraApplication(QtApplication):
engine.rootContext().setContextProperty("CuraApplication", self) engine.rootContext().setContextProperty("CuraApplication", self)
engine.rootContext().setContextProperty("PrintInformation", self._print_information) engine.rootContext().setContextProperty("PrintInformation", self._print_information)
engine.rootContext().setContextProperty("CuraActions", self._cura_actions) engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
engine.rootContext().setContextProperty("CuraSDKVersion", CuraSDKVersion) engine.rootContext().setContextProperty("CuraSDKVersion", ApplicationMetadata.CuraSDKVersion)
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type") qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
@ -947,17 +1027,26 @@ class CuraApplication(QtApplication):
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager) qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager) qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
qmlRegisterType(WelcomePagesModel, "Cura", 1, 0, "WelcomePagesModel")
qmlRegisterType(WhatsNewPagesModel, "Cura", 1, 0, "WhatsNewPagesModel")
qmlRegisterType(AddPrinterPagesModel, "Cura", 1, 0, "AddPrinterPagesModel")
qmlRegisterType(TextManager, "Cura", 1, 0, "TextManager")
qmlRegisterType(NetworkMJPGImage, "Cura", 1, 0, "NetworkMJPGImage")
qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 0, "ObjectsModel", self.getObjectsModel) qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 0, "ObjectsModel", self.getObjectsModel)
qmlRegisterType(BuildPlateModel, "Cura", 1, 0, "BuildPlateModel") qmlRegisterType(BuildPlateModel, "Cura", 1, 0, "BuildPlateModel")
qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel") qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer") qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel") qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
qmlRegisterType(GlobalStacksModel, "Cura", 1, 0, "GlobalStacksModel")
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel") qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel") qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel") qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel") qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel")
qmlRegisterType(DiscoveredPrintersModel, "Cura", 1, 0, "DiscoveredPrintersModel")
qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0, qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
"QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel) "QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
@ -968,11 +1057,14 @@ class CuraApplication(QtApplication):
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler") qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel") qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel")
qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel") qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
qmlRegisterType(FirstStartMachineActionsModel, "Cura", 1, 0, "FirstStartMachineActionsModel")
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator") qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel") qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance) qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel") qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
qmlRegisterType(PrinterOutputDevice, "Cura", 1, 0, "PrinterOutputDevice")
from cura.API import CuraAPI from cura.API import CuraAPI
qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI) qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
@ -1022,7 +1114,6 @@ class CuraApplication(QtApplication):
self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition()) self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
self._camera_animation.start() self._camera_animation.start()
requestAddPrinter = pyqtSignal()
activityChanged = pyqtSignal() activityChanged = pyqtSignal()
sceneBoundingBoxChanged = pyqtSignal() sceneBoundingBoxChanged = pyqtSignal()
@ -1083,88 +1174,6 @@ class CuraApplication(QtApplication):
self._platform_activity = True if count > 0 else False self._platform_activity = True if count > 0 else False
self.activityChanged.emit() self.activityChanged.emit()
# Remove all selected objects from the scene.
@pyqtSlot()
@deprecated("Moved to CuraActions", "2.6")
def deleteSelection(self):
if not self.getController().getToolsEnabled():
return
removed_group_nodes = []
op = GroupedOperation()
nodes = Selection.getAllSelectedObjects()
for node in nodes:
op.addOperation(RemoveSceneNodeOperation(node))
group_node = node.getParent()
if group_node and group_node.callDecoration("isGroup") and group_node not in removed_group_nodes:
remaining_nodes_in_group = list(set(group_node.getChildren()) - set(nodes))
if len(remaining_nodes_in_group) == 1:
removed_group_nodes.append(group_node)
op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent()))
op.addOperation(RemoveSceneNodeOperation(group_node))
op.push()
## Remove an object from the scene.
# Note that this only removes an object if it is selected.
@pyqtSlot("quint64")
@deprecated("Use deleteSelection instead", "2.6")
def deleteObject(self, object_id):
if not self.getController().getToolsEnabled():
return
node = self.getController().getScene().findObject(object_id)
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
node = Selection.getSelectedObject(0)
if node:
op = GroupedOperation()
op.addOperation(RemoveSceneNodeOperation(node))
group_node = node.getParent()
if group_node:
# Note that at this point the node has not yet been deleted
if len(group_node.getChildren()) <= 2 and group_node.callDecoration("isGroup"):
op.addOperation(SetParentOperation(group_node.getChildren()[0], group_node.getParent()))
op.addOperation(RemoveSceneNodeOperation(group_node))
op.push()
## Create a number of copies of existing object.
# \param object_id
# \param count number of copies
# \param min_offset minimum offset to other objects.
@pyqtSlot("quint64", int)
@deprecated("Use CuraActions::multiplySelection", "2.6")
def multiplyObject(self, object_id, count, min_offset = 8):
node = self.getController().getScene().findObject(object_id)
if not node:
node = Selection.getSelectedObject(0)
while node.getParent() and node.getParent().callDecoration("isGroup"):
node = node.getParent()
job = MultiplyObjectsJob([node], count, min_offset)
job.start()
return
## Center object on platform.
@pyqtSlot("quint64")
@deprecated("Use CuraActions::centerSelection", "2.6")
def centerObject(self, object_id):
node = self.getController().getScene().findObject(object_id)
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
node = Selection.getSelectedObject(0)
if not node:
return
if node.getParent() and node.getParent().callDecoration("isGroup"):
node = node.getParent()
if node:
op = SetTransformOperation(node, Vector())
op.push()
## Select all nodes containing mesh data in the scene. ## Select all nodes containing mesh data in the scene.
@pyqtSlot() @pyqtSlot()
def selectAll(self): def selectAll(self):
@ -1244,62 +1253,75 @@ class CuraApplication(QtApplication):
## Arrange all objects. ## Arrange all objects.
@pyqtSlot() @pyqtSlot()
def arrangeObjectsToAllBuildPlates(self): def arrangeObjectsToAllBuildPlates(self) -> None:
nodes = [] nodes_to_arrange = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
if not isinstance(node, SceneNode): if not isinstance(node, SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted) parent_node = node.getParent()
if parent_node and parent_node.callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is reset)
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"): if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data continue # i.e. node with layer data
bounding_box = node.getBoundingBox()
# Skip nodes that are too big # Skip nodes that are too big
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
nodes.append(node) nodes_to_arrange.append(node)
job = ArrangeObjectsAllBuildPlatesJob(nodes) job = ArrangeObjectsAllBuildPlatesJob(nodes_to_arrange)
job.start() job.start()
self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
# Single build plate # Single build plate
@pyqtSlot() @pyqtSlot()
def arrangeAll(self): def arrangeAll(self) -> None:
nodes = [] nodes_to_arrange = []
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
if not isinstance(node, SceneNode): if not isinstance(node, SceneNode):
continue continue
if not node.getMeshData() and not node.callDecoration("isGroup"): if not node.getMeshData() and not node.callDecoration("isGroup"):
continue # Node that doesnt have a mesh and is not a group. continue # Node that doesnt have a mesh and is not a group.
if node.getParent() and node.getParent().callDecoration("isGroup"):
parent_node = node.getParent()
if parent_node and parent_node.callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted) continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.isSelectable(): if not node.isSelectable():
continue # i.e. node with layer data continue # i.e. node with layer data
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"): if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data continue # i.e. node with layer data
if node.callDecoration("getBuildPlateNumber") == active_build_plate: if node.callDecoration("getBuildPlateNumber") == active_build_plate:
# Skip nodes that are too big # Skip nodes that are too big
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth: bounding_box = node.getBoundingBox()
nodes.append(node) if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
self.arrange(nodes, fixed_nodes = []) nodes_to_arrange.append(node)
self.arrange(nodes_to_arrange, fixed_nodes = [])
## Arrange a set of nodes given a set of fixed nodes ## Arrange a set of nodes given a set of fixed nodes
# \param nodes nodes that we have to place # \param nodes nodes that we have to place
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes # \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
def arrange(self, nodes, fixed_nodes): def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None:
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8)) job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
job.start() job.start()
## Reload all mesh data on the screen from file. ## Reload all mesh data on the screen from file.
@pyqtSlot() @pyqtSlot()
def reloadAll(self): def reloadAll(self) -> None:
Logger.log("i", "Reloading all loaded mesh data.") Logger.log("i", "Reloading all loaded mesh data.")
nodes = [] nodes = []
has_merged_nodes = False has_merged_nodes = False
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
if not isinstance(node, CuraSceneNode) or not node.getMeshData() : if not isinstance(node, CuraSceneNode) or not node.getMeshData():
if node.getName() == "MergedMesh": if node.getName() == "MergedMesh":
has_merged_nodes = True has_merged_nodes = True
continue continue
@ -1313,7 +1335,7 @@ class CuraApplication(QtApplication):
file_name = node.getMeshData().getFileName() file_name = node.getMeshData().getFileName()
if file_name: if file_name:
job = ReadMeshJob(file_name) job = ReadMeshJob(file_name)
job._node = node job._node = node # type: ignore
job.finished.connect(self._reloadMeshFinished) job.finished.connect(self._reloadMeshFinished)
if has_merged_nodes: if has_merged_nodes:
job.finished.connect(self.updateOriginOfMergedMeshes) job.finished.connect(self.updateOriginOfMergedMeshes)
@ -1322,20 +1344,8 @@ class CuraApplication(QtApplication):
else: else:
Logger.log("w", "Unable to reload data because we don't have a filename.") Logger.log("w", "Unable to reload data because we don't have a filename.")
## Get logging data of the backend engine
# \returns \type{string} Logging data
@pyqtSlot(result = str)
def getEngineLog(self):
log = ""
for entry in self.getBackend().getLog():
log += entry.decode()
return log
@pyqtSlot("QStringList") @pyqtSlot("QStringList")
def setExpandedCategories(self, categories): def setExpandedCategories(self, categories: List[str]) -> None:
categories = list(set(categories)) categories = list(set(categories))
categories.sort() categories.sort()
joined = ";".join(categories) joined = ";".join(categories)
@ -1346,7 +1356,7 @@ class CuraApplication(QtApplication):
expandedCategoriesChanged = pyqtSignal() expandedCategoriesChanged = pyqtSignal()
@pyqtProperty("QStringList", notify = expandedCategoriesChanged) @pyqtProperty("QStringList", notify = expandedCategoriesChanged)
def expandedCategories(self): def expandedCategories(self) -> List[str]:
return self.getPreferences().getValue("cura/categories_expanded").split(";") return self.getPreferences().getValue("cura/categories_expanded").split(";")
@pyqtSlot() @pyqtSlot()
@ -1396,13 +1406,12 @@ class CuraApplication(QtApplication):
## Updates origin position of all merged meshes ## Updates origin position of all merged meshes
# \param jobNode \type{Job} empty object which passed which is required by JobQueue def updateOriginOfMergedMeshes(self, _):
def updateOriginOfMergedMeshes(self, jobNode):
group_nodes = [] group_nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()): for node in DepthFirstIterator(self.getController().getScene().getRoot()):
if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh": if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh":
#checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator # Checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator
for decorator in node.getDecorators(): for decorator in node.getDecorators():
if isinstance(decorator, GroupDecorator): if isinstance(decorator, GroupDecorator):
group_nodes.append(node) group_nodes.append(node)
@ -1446,7 +1455,7 @@ class CuraApplication(QtApplication):
@pyqtSlot() @pyqtSlot()
def groupSelected(self): def groupSelected(self) -> None:
# Create a group-node # Create a group-node
group_node = CuraSceneNode() group_node = CuraSceneNode()
group_decorator = GroupDecorator() group_decorator = GroupDecorator()
@ -1462,7 +1471,8 @@ class CuraApplication(QtApplication):
# Remove nodes that are directly parented to another selected node from the selection so they remain parented # Remove nodes that are directly parented to another selected node from the selection so they remain parented
selected_nodes = Selection.getAllSelectedObjects().copy() selected_nodes = Selection.getAllSelectedObjects().copy()
for node in selected_nodes: for node in selected_nodes:
if node.getParent() in selected_nodes and not node.getParent().callDecoration("isGroup"): parent = node.getParent()
if parent is not None and parent in selected_nodes and not parent.callDecoration("isGroup"):
Selection.remove(node) Selection.remove(node)
# Move selected nodes into the group-node # Move selected nodes into the group-node
@ -1474,7 +1484,7 @@ class CuraApplication(QtApplication):
Selection.add(group_node) Selection.add(group_node)
@pyqtSlot() @pyqtSlot()
def ungroupSelected(self): def ungroupSelected(self) -> None:
selected_objects = Selection.getAllSelectedObjects().copy() selected_objects = Selection.getAllSelectedObjects().copy()
for node in selected_objects: for node in selected_objects:
if node.callDecoration("isGroup"): if node.callDecoration("isGroup"):
@ -1497,7 +1507,7 @@ class CuraApplication(QtApplication):
# Note: The group removes itself from the scene once all its children have left it, # Note: The group removes itself from the scene once all its children have left it,
# see GroupDecorator._onChildrenChanged # see GroupDecorator._onChildrenChanged
def _createSplashScreen(self): def _createSplashScreen(self) -> Optional[CuraSplashScreen.CuraSplashScreen]:
if self._is_headless: if self._is_headless:
return None return None
return CuraSplashScreen.CuraSplashScreen() return CuraSplashScreen.CuraSplashScreen()
@ -1663,7 +1673,9 @@ class CuraApplication(QtApplication):
is_non_sliceable = "." + file_extension in self._non_sliceable_extensions is_non_sliceable = "." + file_extension in self._non_sliceable_extensions
if is_non_sliceable: if is_non_sliceable:
self.callLater(lambda: self.getController().setActiveView("SimulationView")) # Need to switch first to the preview stage and then to layer view
self.callLater(lambda: (self.getController().setActiveStage("PreviewStage"),
self.getController().setActiveView("SimulationView")))
block_slicing_decorator = BlockSlicingDecorator() block_slicing_decorator = BlockSlicingDecorator()
node.addDecorator(block_slicing_decorator) node.addDecorator(block_slicing_decorator)
@ -1761,3 +1773,16 @@ class CuraApplication(QtApplication):
def getSidebarCustomMenuItems(self) -> list: def getSidebarCustomMenuItems(self) -> list:
return self._sidebar_custom_menu_items return self._sidebar_custom_menu_items
@pyqtSlot(result = bool)
def shouldShowWelcomeDialog(self) -> bool:
# Only show the complete flow if there is no printer yet.
return self._machine_manager.activeMachine is None
@pyqtSlot(result = bool)
def shouldShowWhatsNewDialog(self) -> bool:
has_active_machine = self._machine_manager.activeMachine is not None
has_app_just_upgraded = self.hasJustUpdatedFromOldVersion()
# Only show the what's new dialog if there's no machine and we have just upgraded
show_whatsnew_only = has_active_machine and has_app_just_upgraded
return show_whatsnew_only

View File

@ -1,9 +1,12 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
CuraAppName = "@CURA_APP_NAME@"
CuraAppDisplayName = "@CURA_APP_DISPLAY_NAME@"
CuraVersion = "@CURA_VERSION@" CuraVersion = "@CURA_VERSION@"
CuraBuildType = "@CURA_BUILDTYPE@" CuraBuildType = "@CURA_BUILDTYPE@"
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
CuraSDKVersion = "@CURA_SDK_VERSION@" CuraSDKVersion = "@CURA_SDK_VERSION@"
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@" CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@" CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"

24
cura/CuraView.py Normal file
View File

@ -0,0 +1,24 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, QUrl
from UM.View.View import View
# Since Cura has a few pre-defined "space claims" for the locations of certain components, we've provided some structure
# to indicate this.
# MainComponent works in the same way the MainComponent of a stage.
# the stageMenuComponent returns an item that should be used somehwere in the stage menu. It's up to the active stage
# to actually do something with this.
class CuraView(View):
def __init__(self, parent = None) -> None:
super().__init__(parent)
@pyqtProperty(QUrl, constant = True)
def mainComponent(self) -> QUrl:
return self.getDisplayComponent("main")
@pyqtProperty(QUrl, constant = True)
def stageMenuComponent(self) -> QUrl:
return self.getDisplayComponent("menu")

View File

@ -7,43 +7,36 @@ from UM.Mesh.MeshBuilder import MeshBuilder
from .LayerData import LayerData from .LayerData import LayerData
import numpy import numpy
from typing import Dict, Optional
## Builder class for constructing a LayerData object ## Builder class for constructing a LayerData object
class LayerDataBuilder(MeshBuilder): class LayerDataBuilder(MeshBuilder):
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
self._layers = {} self._layers = {} # type: Dict[int, Layer]
self._element_counts = {} self._element_counts = {} # type: Dict[int, int]
def addLayer(self, layer): def addLayer(self, layer: int) -> None:
if layer not in self._layers: if layer not in self._layers:
self._layers[layer] = Layer(layer) self._layers[layer] = Layer(layer)
def addPolygon(self, layer, polygon_type, data, line_width, line_thickness, line_feedrate): def getLayer(self, layer: int) -> Optional[Layer]:
if layer not in self._layers: return self._layers.get(layer)
self.addLayer(layer)
p = LayerPolygon(self, polygon_type, data, line_width, line_thickness, line_feedrate) def getLayers(self) -> Dict[int, Layer]:
self._layers[layer].polygons.append(p)
def getLayer(self, layer):
if layer in self._layers:
return self._layers[layer]
def getLayers(self):
return self._layers return self._layers
def getElementCounts(self): def getElementCounts(self) -> Dict[int, int]:
return self._element_counts return self._element_counts
def setLayerHeight(self, layer, height): def setLayerHeight(self, layer: int, height: float) -> None:
if layer not in self._layers: if layer not in self._layers:
self.addLayer(layer) self.addLayer(layer)
self._layers[layer].setHeight(height) self._layers[layer].setHeight(height)
def setLayerThickness(self, layer, thickness): def setLayerThickness(self, layer: int, thickness: float) -> None:
if layer not in self._layers: if layer not in self._layers:
self.addLayer(layer) self.addLayer(layer)
@ -71,7 +64,7 @@ class LayerDataBuilder(MeshBuilder):
vertex_offset = 0 vertex_offset = 0
index_offset = 0 index_offset = 0
for layer, data in sorted(self._layers.items()): for layer, data in sorted(self._layers.items()):
( vertex_offset, index_offset ) = data.build( vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices) vertex_offset, index_offset = data.build(vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices)
self._element_counts[layer] = data.elementCount self._element_counts[layer] = data.elementCount
self.addVertices(vertices) self.addVertices(vertices)

View File

@ -1,13 +1,25 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
from cura.LayerData import LayerData
## Simple decorator to indicate a scene node holds layer data. ## Simple decorator to indicate a scene node holds layer data.
class LayerDataDecorator(SceneNodeDecorator): class LayerDataDecorator(SceneNodeDecorator):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._layer_data = None self._layer_data = None # type: Optional[LayerData]
def getLayerData(self): def getLayerData(self) -> Optional["LayerData"]:
return self._layer_data return self._layer_data
def setLayerData(self, layer_data): def setLayerData(self, layer_data: LayerData) -> None:
self._layer_data = layer_data self._layer_data = layer_data
def __deepcopy__(self, memo) -> "LayerDataDecorator":
copied_decorator = LayerDataDecorator()
copied_decorator._layer_data = self._layer_data
return copied_decorator

View File

@ -2,9 +2,11 @@
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Application import Application from UM.Application import Application
from typing import Any from typing import Any, Optional
import numpy import numpy
from UM.Logger import Logger
class LayerPolygon: class LayerPolygon:
NoneType = 0 NoneType = 0
@ -18,22 +20,24 @@ class LayerPolygon:
MoveCombingType = 8 MoveCombingType = 8
MoveRetractionType = 9 MoveRetractionType = 9
SupportInterfaceType = 10 SupportInterfaceType = 10
__number_of_types = 11 PrimeTower = 11
__number_of_types = 12
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType) __jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType)
## LayerPolygon, used in ProcessSlicedLayersJob ## LayerPolygon, used in ProcessSlicedLayersJob
# \param extruder # \param extruder The position of the extruder
# \param line_types array with line_types # \param line_types array with line_types
# \param data new_points # \param data new_points
# \param line_widths array with line widths # \param line_widths array with line widths
# \param line_thicknesses: array with type as index and thickness as value # \param line_thicknesses: array with type as index and thickness as value
# \param line_feedrates array with line feedrates # \param line_feedrates array with line feedrates
def __init__(self, extruder, line_types, data, line_widths, line_thicknesses, line_feedrates): def __init__(self, extruder: int, line_types: numpy.ndarray, data: numpy.ndarray, line_widths: numpy.ndarray, line_thicknesses: numpy.ndarray, line_feedrates: numpy.ndarray) -> None:
self._extruder = extruder self._extruder = extruder
self._types = line_types self._types = line_types
for i in range(len(self._types)): for i in range(len(self._types)):
if self._types[i] >= self.__number_of_types: #Got faulty line data from the engine. if self._types[i] >= self.__number_of_types: # Got faulty line data from the engine.
Logger.log("w", "Found an unknown line type: %s", i)
self._types[i] = self.NoneType self._types[i] = self.NoneType
self._data = data self._data = data
self._line_widths = line_widths self._line_widths = line_widths
@ -53,16 +57,16 @@ class LayerPolygon:
# Buffering the colors shouldn't be necessary as it is not # Buffering the colors shouldn't be necessary as it is not
# re-used and can save alot of memory usage. # re-used and can save alot of memory usage.
self._color_map = LayerPolygon.getColorMap() self._color_map = LayerPolygon.getColorMap()
self._colors = self._color_map[self._types] self._colors = self._color_map[self._types] # type: numpy.ndarray
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType # When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
# Should be generated in better way, not hardcoded. # Should be generated in better way, not hardcoded.
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1], dtype=numpy.bool) self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1], dtype=numpy.bool)
self._build_cache_line_mesh_mask = None self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray]
self._build_cache_needed_points = None self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
def buildCache(self): def buildCache(self) -> None:
# For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out. # For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype=bool) self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype=bool)
mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask) mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask)
@ -90,10 +94,14 @@ class LayerPolygon:
# \param extruders : vertex numpy array to be filled # \param extruders : vertex numpy array to be filled
# \param line_types : vertex numpy array to be filled # \param line_types : vertex numpy array to be filled
# \param indices : index numpy array to be filled # \param indices : index numpy array to be filled
def build(self, vertex_offset, index_offset, vertices, colors, line_dimensions, feedrates, extruders, line_types, indices): def build(self, vertex_offset: int, index_offset: int, vertices: numpy.ndarray, colors: numpy.ndarray, line_dimensions: numpy.ndarray, feedrates: numpy.ndarray, extruders: numpy.ndarray, line_types: numpy.ndarray, indices: numpy.ndarray) -> None:
if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None: if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None:
self.buildCache() self.buildCache()
if self._build_cache_line_mesh_mask is None or self._build_cache_needed_points is None:
Logger.log("w", "Failed to build cache for layer polygon")
return
line_mesh_mask = self._build_cache_line_mesh_mask line_mesh_mask = self._build_cache_line_mesh_mask
needed_points_list = self._build_cache_needed_points needed_points_list = self._build_cache_needed_points
@ -236,7 +244,8 @@ class LayerPolygon:
theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
theme.getColor("layerview_support_interface").getRgbF() # SupportInterfaceType theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType
theme.getColor("layerview_prime_tower").getRgbF()
]) ])
return cls.__color_map return cls.__color_map

View File

@ -33,6 +33,12 @@ class MachineAction(QObject, PluginObject):
def getKey(self) -> str: def getKey(self) -> str:
return self._key return self._key
## Whether this action needs to ask the user anything.
# If not, we shouldn't present the user with certain screens which otherwise show up.
# Defaults to true to be in line with the old behaviour.
def needsUserInteraction(self) -> bool:
return True
@pyqtProperty(str, notify = labelChanged) @pyqtProperty(str, notify = labelChanged)
def label(self) -> str: def label(self) -> str:
return self._label return self._label

View File

@ -64,21 +64,21 @@ class MachineErrorChecker(QObject):
def _onMachineChanged(self) -> None: def _onMachineChanged(self) -> None:
if self._global_stack: if self._global_stack:
self._global_stack.propertyChanged.disconnect(self.startErrorCheck) self._global_stack.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
self._global_stack.containersChanged.disconnect(self.startErrorCheck) self._global_stack.containersChanged.disconnect(self.startErrorCheck)
for extruder in self._global_stack.extruders.values(): for extruder in self._global_stack.extruders.values():
extruder.propertyChanged.disconnect(self.startErrorCheck) extruder.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
extruder.containersChanged.disconnect(self.startErrorCheck) extruder.containersChanged.disconnect(self.startErrorCheck)
self._global_stack = self._machine_manager.activeMachine self._global_stack = self._machine_manager.activeMachine
if self._global_stack: if self._global_stack:
self._global_stack.propertyChanged.connect(self.startErrorCheck) self._global_stack.propertyChanged.connect(self.startErrorCheckPropertyChanged)
self._global_stack.containersChanged.connect(self.startErrorCheck) self._global_stack.containersChanged.connect(self.startErrorCheck)
for extruder in self._global_stack.extruders.values(): for extruder in self._global_stack.extruders.values():
extruder.propertyChanged.connect(self.startErrorCheck) extruder.propertyChanged.connect(self.startErrorCheckPropertyChanged)
extruder.containersChanged.connect(self.startErrorCheck) extruder.containersChanged.connect(self.startErrorCheck)
hasErrorUpdated = pyqtSignal() hasErrorUpdated = pyqtSignal()
@ -93,6 +93,13 @@ class MachineErrorChecker(QObject):
def needToWaitForResult(self) -> bool: def needToWaitForResult(self) -> bool:
return self._need_to_check or self._check_in_progress return self._need_to_check or self._check_in_progress
# Start the error check for property changed
# this is seperate from the startErrorCheck because it ignores a number property types
def startErrorCheckPropertyChanged(self, key, property_name):
if property_name != "value":
return
self.startErrorCheck()
# Starts the error check timer to schedule a new error check. # Starts the error check timer to schedule a new error check.
def startErrorCheck(self, *args) -> None: def startErrorCheck(self, *args) -> None:
if not self._check_in_progress: if not self._check_in_progress:

View File

@ -21,6 +21,7 @@ from .VariantType import VariantType
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.DefinitionContainer import DefinitionContainer
from UM.Settings.InstanceContainer import InstanceContainer
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
@ -218,7 +219,7 @@ class MaterialManager(QObject):
root_material_id = material_metadata["base_file"] root_material_id = material_metadata["base_file"]
definition = material_metadata["definition"] definition = material_metadata["definition"]
approximate_diameter = material_metadata["approximate_diameter"] approximate_diameter = str(material_metadata["approximate_diameter"])
if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map: if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {} self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}
@ -298,9 +299,13 @@ class MaterialManager(QObject):
def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str: def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
return self._diameter_material_map.get(root_material_id, "") 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) return self._guid_material_groups_map.get(guid)
# Returns a dict of all material groups organized by root_material_id.
def getAllMaterialGroups(self) -> Dict[str, "MaterialGroup"]:
return self._material_group_map
# #
# Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup. # Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
# #
@ -327,7 +332,6 @@ class MaterialManager(QObject):
buildplate_node = nozzle_node.getChildNode(buildplate_name) buildplate_node = nozzle_node.getChildNode(buildplate_name)
nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node] nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]
# Fallback mechanism of finding materials: # Fallback mechanism of finding materials:
# 1. buildplate-specific material # 1. buildplate-specific material
# 2. nozzle-specific material # 2. nozzle-specific material
@ -446,6 +450,28 @@ class MaterialManager(QObject):
material_diameter, root_material_id) material_diameter, root_material_id)
return node 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". # 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 # For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
@ -510,16 +536,40 @@ class MaterialManager(QObject):
return return
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
# Sort all nodes with respect to the container ID lengths in the ascending order so the base material container
# will be the first one to be removed. We need to do this to ensure that all containers get loaded & deleted.
nodes_to_remove = sorted(nodes_to_remove, key = lambda x: len(x.getMetaDataEntry("id", "")))
# Try to load all containers first. If there is any faulty ones, they will be put into the faulty container
# list, so removeContainer() can ignore those ones.
for node in nodes_to_remove:
container_id = node.getMetaDataEntry("id", "")
results = self._container_registry.findContainers(id = container_id)
if not results:
self._container_registry.addWrongContainerId(container_id)
for node in nodes_to_remove: for node in nodes_to_remove:
self._container_registry.removeContainer(node.getMetaDataEntry("id", "")) self._container_registry.removeContainer(node.getMetaDataEntry("id", ""))
# #
# Methods for GUI # Methods for GUI
# #
@pyqtSlot("QVariant", result=bool)
def canMaterialBeRemoved(self, material_node: "MaterialNode"):
# Check if the material is active in any extruder train. In that case, the material shouldn't be removed!
# In the future we might enable this again, but right now, it's causing a ton of issues if we do (since it
# corrupts the configuration)
root_material_id = material_node.getMetaDataEntry("base_file")
material_group = self.getMaterialGroup(root_material_id)
if not material_group:
return False
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
ids_to_remove = [node.getMetaDataEntry("id", "") for node in nodes_to_remove]
for extruder_stack in self._container_registry.findContainerStacks(type="extruder_train"):
if extruder_stack.material.getId() in ids_to_remove:
return False
return True
#
# Sets the new name for the given material.
#
@pyqtSlot("QVariant", str) @pyqtSlot("QVariant", str)
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None: def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
root_material_id = material_node.getMetaDataEntry("base_file") root_material_id = material_node.getMetaDataEntry("base_file")
@ -602,7 +652,6 @@ class MaterialManager(QObject):
container_to_add.setDirty(True) container_to_add.setDirty(True)
self._container_registry.addContainer(container_to_add) self._container_registry.addContainer(container_to_add)
# if the duplicated material was favorite then the new material should also be added to favorite. # if the duplicated material was favorite then the new material should also be added to favorite.
if root_material_id in self.getFavorites(): if root_material_id in self.getFavorites():
self.addFavorite(new_base_id) self.addFavorite(new_base_id)
@ -622,8 +671,10 @@ class MaterialManager(QObject):
machine_manager = self._application.getMachineManager() machine_manager = self._application.getMachineManager()
extruder_stack = machine_manager.activeStack extruder_stack = machine_manager.activeStack
machine_definition = self._application.getGlobalContainerStack().definition
root_material_id = machine_definition.getMetaDataEntry("preferred_material", default = "generic_pla")
approximate_diameter = str(extruder_stack.approximateMaterialDiameter) approximate_diameter = str(extruder_stack.approximateMaterialDiameter)
root_material_id = "generic_pla"
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter) root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
material_group = self.getMaterialGroup(root_material_id) material_group = self.getMaterialGroup(root_material_id)
@ -654,7 +705,11 @@ class MaterialManager(QObject):
@pyqtSlot(str) @pyqtSlot(str)
def removeFavorite(self, root_material_id: str) -> None: def removeFavorite(self, root_material_id: str) -> None:
self._favorites.remove(root_material_id) try:
self._favorites.remove(root_material_id)
except KeyError:
Logger.log("w", "Could not delete material %s from favorites as it was already deleted", root_material_id)
return
self.materialsUpdated.emit() self.materialsUpdated.emit()
# Ensure all settings are saved. # Ensure all settings are saved.
@ -663,4 +718,4 @@ class MaterialManager(QObject):
@pyqtSlot() @pyqtSlot()
def getFavorites(self): def getFavorites(self):
return self._favorites return self._favorites

View File

@ -1,5 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Dict, Set
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
@ -9,13 +10,16 @@ from UM.Qt.ListModel import ListModel
# Those 2 models are used by the material drop down menu to show generic materials and branded materials separately. # Those 2 models are used by the material drop down menu to show generic materials and branded materials separately.
# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top # The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top
# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu # bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
from cura.Machines.MaterialNode import MaterialNode
class BaseMaterialsModel(ListModel): class BaseMaterialsModel(ListModel):
extruderPositionChanged = pyqtSignal() extruderPositionChanged = pyqtSignal()
enabledChanged = pyqtSignal()
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
self._application = CuraApplication.getInstance() self._application = CuraApplication.getInstance()
@ -54,8 +58,9 @@ class BaseMaterialsModel(ListModel):
self._extruder_position = 0 self._extruder_position = 0
self._extruder_stack = None self._extruder_stack = None
self._available_materials = None self._available_materials = None # type: Optional[Dict[str, MaterialNode]]
self._favorite_ids = None self._favorite_ids = set() # type: Set[str]
self._enabled = True
def _updateExtruderStack(self): def _updateExtruderStack(self):
global_stack = self._machine_manager.activeMachine global_stack = self._machine_manager.activeMachine
@ -82,6 +87,18 @@ class BaseMaterialsModel(ListModel):
def extruderPosition(self) -> int: def extruderPosition(self) -> int:
return self._extruder_position return self._extruder_position
def setEnabled(self, enabled):
if self._enabled != enabled:
self._enabled = enabled
if self._enabled:
# ensure the data is there again.
self._update()
self.enabledChanged.emit()
@pyqtProperty(bool, fset=setEnabled, notify=enabledChanged)
def enabled(self):
return self._enabled
## This is an abstract method that needs to be implemented by the specific ## This is an abstract method that needs to be implemented by the specific
# models themselves. # models themselves.
def _update(self): def _update(self):
@ -93,7 +110,7 @@ class BaseMaterialsModel(ListModel):
def _canUpdate(self): def _canUpdate(self):
global_stack = self._machine_manager.activeMachine global_stack = self._machine_manager.activeMachine
if global_stack is None: if global_stack is None or not self._enabled:
return False return False
extruder_position = str(self._extruder_position) extruder_position = str(self._extruder_position)
@ -102,7 +119,6 @@ class BaseMaterialsModel(ListModel):
return False return False
extruder_stack = global_stack.extruders[extruder_position] extruder_stack = global_stack.extruders[extruder_position]
self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack) self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack)
if self._available_materials is None: if self._available_materials is None:
return False return False

View File

@ -0,0 +1,167 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Callable, Dict, List, Optional, TYPE_CHECKING
from PyQt5.QtCore import pyqtSlot, pyqtProperty, pyqtSignal, QObject
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Util import parseBool
if TYPE_CHECKING:
from PyQt5.QtCore import QObject
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice
catalog = i18nCatalog("cura")
class DiscoveredPrinter(QObject):
def __init__(self, ip_address: str, key: str, name: str, create_callback: Callable[[str], None], machine_type: str,
device: "NetworkedPrinterOutputDevice", parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._ip_address = ip_address
self._key = key
self._name = name
self.create_callback = create_callback
self._machine_type = machine_type
self._device = device
nameChanged = pyqtSignal()
def getKey(self) -> str:
return self._key
@pyqtProperty(str, notify = nameChanged)
def name(self) -> str:
return self._name
def setName(self, name: str) -> None:
if self._name != name:
self._name = name
self.nameChanged.emit()
machineTypeChanged = pyqtSignal()
@pyqtProperty(str, notify = machineTypeChanged)
def machineType(self) -> str:
return self._machine_type
def setMachineType(self, machine_type: str) -> None:
if self._machine_type != machine_type:
self._machine_type = machine_type
self.machineTypeChanged.emit()
# Human readable machine type string
@pyqtProperty(str, notify = machineTypeChanged)
def readableMachineType(self) -> str:
from cura.CuraApplication import CuraApplication
readable_type = CuraApplication.getInstance().getMachineManager().getMachineTypeNameFromId(self._machine_type)
if not readable_type:
readable_type = catalog.i18nc("@label", "Unknown")
return readable_type
@pyqtProperty(bool, notify = machineTypeChanged)
def isUnknownMachineType(self) -> bool:
from cura.CuraApplication import CuraApplication
readable_type = CuraApplication.getInstance().getMachineManager().getMachineTypeNameFromId(self._machine_type)
return not readable_type
@pyqtProperty(QObject, constant = True)
def device(self) -> "NetworkedPrinterOutputDevice":
return self._device
@pyqtProperty(bool, constant = True)
def isHostOfGroup(self) -> bool:
return getattr(self._device, "clusterSize", 1) > 0
@pyqtProperty(str, constant = True)
def sectionName(self) -> str:
if self.isUnknownMachineType or not self.isHostOfGroup:
return catalog.i18nc("@label", "The printer(s) below cannot be connected because they are part of a group")
else:
return catalog.i18nc("@label", "Available networked printers")
#
# Discovered printers are all the printers that were found on the network, which provide a more convenient way
# to add networked printers (Plugin finds a bunch of printers, user can select one from the list, plugin can then
# add that printer to Cura as the active one).
#
class DiscoveredPrintersModel(QObject):
def __init__(self, parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._discovered_printer_by_ip_dict = dict() # type: Dict[str, DiscoveredPrinter]
discoveredPrintersChanged = pyqtSignal()
@pyqtProperty(list, notify = discoveredPrintersChanged)
def discoveredPrinters(self) -> List["DiscoveredPrinter"]:
item_list = list(
x for x in self._discovered_printer_by_ip_dict.values() if not parseBool(x.device.getProperty("temporary")))
# Split the printers into 2 lists and sort them ascending based on names.
available_list = []
not_available_list = []
for item in item_list:
if item.isUnknownMachineType or getattr(item.device, "clusterSize", 1) < 1:
not_available_list.append(item)
else:
available_list.append(item)
available_list.sort(key = lambda x: x.device.name)
not_available_list.sort(key = lambda x: x.device.name)
return available_list + not_available_list
def addDiscoveredPrinter(self, ip_address: str, key: str, name: str, create_callback: Callable[[str], None],
machine_type: str, device: "NetworkedPrinterOutputDevice") -> None:
if ip_address in self._discovered_printer_by_ip_dict:
Logger.log("e", "Printer with ip [%s] has already been added", ip_address)
return
discovered_printer = DiscoveredPrinter(ip_address, key, name, create_callback, machine_type, device, parent = self)
self._discovered_printer_by_ip_dict[ip_address] = discovered_printer
self.discoveredPrintersChanged.emit()
def updateDiscoveredPrinter(self, ip_address: str,
name: Optional[str] = None,
machine_type: Optional[str] = None) -> None:
if ip_address not in self._discovered_printer_by_ip_dict:
Logger.log("w", "Printer with ip [%s] is not known", ip_address)
return
item = self._discovered_printer_by_ip_dict[ip_address]
if name is not None:
item.setName(name)
if machine_type is not None:
item.setMachineType(machine_type)
def removeDiscoveredPrinter(self, ip_address: str) -> None:
if ip_address not in self._discovered_printer_by_ip_dict:
Logger.log("w", "Key [%s] does not exist in the discovered printers list.", ip_address)
return
del self._discovered_printer_by_ip_dict[ip_address]
self.discoveredPrintersChanged.emit()
# A convenience function for QML to create a machine (GlobalStack) out of the given discovered printer.
# This function invokes the given discovered printer's "create_callback" to do this.
@pyqtSlot("QVariant")
def createMachineFromDiscoveredPrinter(self, discovered_printer: "DiscoveredPrinter") -> None:
discovered_printer.create_callback(discovered_printer.getKey())
@pyqtSlot(str)
def createMachineFromDiscoveredPrinterAddress(self, ip_address: str) -> None:
if ip_address not in self._discovered_printer_by_ip_dict:
Logger.log("i", "Key [%s] does not exist in the discovered printers list.", ip_address)
return
self.createMachineFromDiscoveredPrinter(self._discovered_printer_by_ip_dict[ip_address])

View File

@ -1,31 +1,31 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, pyqtProperty, QTimer from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer
from typing import Iterable from typing import Iterable, TYPE_CHECKING
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
import UM.Qt.ListModel from UM.Qt.ListModel import ListModel
from UM.Application import Application from UM.Application import Application
import UM.FlameProfiler import UM.FlameProfiler
from cura.Settings.ExtruderStack import ExtruderStack # To listen to changes on the extruders. if TYPE_CHECKING:
from cura.Settings.ExtruderStack import ExtruderStack # To listen to changes on the extruders.
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
## Model that holds extruders. ## Model that holds extruders.
# #
# This model is designed for use by any list of extruders, but specifically # This model is designed for use by any list of extruders, but specifically
# intended for drop-down lists of the current machine's extruders in place of # intended for drop-down lists of the current machine's extruders in place of
# settings. # settings.
class ExtrudersModel(UM.Qt.ListModel.ListModel): class ExtrudersModel(ListModel):
# The ID of the container stack for the extruder. # The ID of the container stack for the extruder.
IdRole = Qt.UserRole + 1 IdRole = Qt.UserRole + 1
## Human-readable name of the extruder. ## Human-readable name of the extruder.
NameRole = Qt.UserRole + 2 NameRole = Qt.UserRole + 2
## Is the extruder enabled?
EnabledRole = Qt.UserRole + 9
## Colour of the material loaded in the extruder. ## Colour of the material loaded in the extruder.
ColorRole = Qt.UserRole + 3 ColorRole = Qt.UserRole + 3
@ -47,6 +47,12 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
VariantRole = Qt.UserRole + 7 VariantRole = Qt.UserRole + 7
StackRole = Qt.UserRole + 8 StackRole = Qt.UserRole + 8
MaterialBrandRole = Qt.UserRole + 9
ColorNameRole = Qt.UserRole + 10
## Is the extruder enabled?
EnabledRole = Qt.UserRole + 11
## List of colours to display if there is no material or the material has no known ## List of colours to display if there is no material or the material has no known
# colour. # colour.
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"] defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
@ -67,14 +73,13 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
self.addRoleName(self.MaterialRole, "material") self.addRoleName(self.MaterialRole, "material")
self.addRoleName(self.VariantRole, "variant") self.addRoleName(self.VariantRole, "variant")
self.addRoleName(self.StackRole, "stack") self.addRoleName(self.StackRole, "stack")
self.addRoleName(self.MaterialBrandRole, "material_brand")
self.addRoleName(self.ColorNameRole, "color_name")
self._update_extruder_timer = QTimer() self._update_extruder_timer = QTimer()
self._update_extruder_timer.setInterval(100) self._update_extruder_timer.setInterval(100)
self._update_extruder_timer.setSingleShot(True) self._update_extruder_timer.setSingleShot(True)
self._update_extruder_timer.timeout.connect(self.__updateExtruders) self._update_extruder_timer.timeout.connect(self.__updateExtruders)
self._simple_names = False
self._active_machine_extruders = [] # type: Iterable[ExtruderStack] self._active_machine_extruders = [] # type: Iterable[ExtruderStack]
self._add_optional_extruder = False self._add_optional_extruder = False
@ -96,21 +101,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
def addOptionalExtruder(self): def addOptionalExtruder(self):
return self._add_optional_extruder return self._add_optional_extruder
## Set the simpleNames property.
def setSimpleNames(self, simple_names):
if simple_names != self._simple_names:
self._simple_names = simple_names
self.simpleNamesChanged.emit()
self._updateExtruders()
## Emitted when the simpleNames property changes.
simpleNamesChanged = pyqtSignal()
## Whether or not the model should show all definitions regardless of visibility.
@pyqtProperty(bool, fset = setSimpleNames, notify = simpleNamesChanged)
def simpleNames(self):
return self._simple_names
## Links to the stack-changed signal of the new extruders when an extruder ## Links to the stack-changed signal of the new extruders when an extruder
# is swapped out or added in the current machine. # is swapped out or added in the current machine.
# #
@ -119,17 +109,19 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
# that signal. Application.globalContainerStackChanged doesn't fill this # that signal. Application.globalContainerStackChanged doesn't fill this
# signal; it's assumed to be the current printer in that case. # signal; it's assumed to be the current printer in that case.
def _extrudersChanged(self, machine_id = None): def _extrudersChanged(self, machine_id = None):
machine_manager = Application.getInstance().getMachineManager()
if machine_id is not None: if machine_id is not None:
if Application.getInstance().getGlobalContainerStack() is None: if machine_manager.activeMachine is None:
# No machine, don't need to update the current machine's extruders # No machine, don't need to update the current machine's extruders
return return
if machine_id != Application.getInstance().getGlobalContainerStack().getId(): if machine_id != machine_manager.activeMachine.getId():
# Not the current machine # Not the current machine
return return
# Unlink from old extruders # Unlink from old extruders
for extruder in self._active_machine_extruders: for extruder in self._active_machine_extruders:
extruder.containersChanged.disconnect(self._onExtruderStackContainersChanged) extruder.containersChanged.disconnect(self._onExtruderStackContainersChanged)
extruder.enabledChanged.disconnect(self._updateExtruders)
# Link to new extruders # Link to new extruders
self._active_machine_extruders = [] self._active_machine_extruders = []
@ -138,13 +130,14 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
if extruder is None: #This extruder wasn't loaded yet. This happens asynchronously while this model is constructed from QML. if extruder is None: #This extruder wasn't loaded yet. This happens asynchronously while this model is constructed from QML.
continue continue
extruder.containersChanged.connect(self._onExtruderStackContainersChanged) extruder.containersChanged.connect(self._onExtruderStackContainersChanged)
extruder.enabledChanged.connect(self._updateExtruders)
self._active_machine_extruders.append(extruder) self._active_machine_extruders.append(extruder)
self._updateExtruders() # Since the new extruders may have different properties, update our own model. self._updateExtruders() # Since the new extruders may have different properties, update our own model.
def _onExtruderStackContainersChanged(self, container): def _onExtruderStackContainersChanged(self, container):
# Update when there is an empty container or material change # Update when there is an empty container or material or variant change
if container.getMetaDataEntry("type") == "material" or container.getMetaDataEntry("type") is None: if container.getMetaDataEntry("type") in ["material", "variant", None]:
# The ExtrudersModel needs to be updated when the material-name or -color changes, because the user identifies extruders by material-name # The ExtrudersModel needs to be updated when the material-name or -color changes, because the user identifies extruders by material-name
self._updateExtruders() self._updateExtruders()
@ -160,7 +153,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
def __updateExtruders(self): def __updateExtruders(self):
extruders_changed = False extruders_changed = False
if self.rowCount() != 0: if self.count != 0:
extruders_changed = True extruders_changed = True
items = [] items = []
@ -172,7 +165,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value") machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
for extruder in Application.getInstance().getExtruderManager().getActiveExtruderStacks(): for extruder in Application.getInstance().getExtruderManager().getActiveExtruderStacks():
position = extruder.getMetaDataEntry("position", default = "0") # Get the position position = extruder.getMetaDataEntry("position", default = "0")
try: try:
position = int(position) position = int(position)
except ValueError: except ValueError:
@ -183,7 +176,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
default_color = self.defaultColors[position] if 0 <= position < len(self.defaultColors) else self.defaultColors[0] default_color = self.defaultColors[position] if 0 <= position < len(self.defaultColors) else self.defaultColors[0]
color = extruder.material.getMetaDataEntry("color_code", default = default_color) if extruder.material else default_color color = extruder.material.getMetaDataEntry("color_code", default = default_color) if extruder.material else default_color
material_brand = extruder.material.getMetaDataEntry("brand", default = "generic")
color_name = extruder.material.getMetaDataEntry("color_name")
# construct an item with only the relevant information # construct an item with only the relevant information
item = { item = {
"id": extruder.getId(), "id": extruder.getId(),
@ -195,6 +189,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
"material": extruder.material.getName() if extruder.material else "", "material": extruder.material.getName() if extruder.material else "",
"variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core "variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core
"stack": extruder, "stack": extruder,
"material_brand": material_brand,
"color_name": color_name
} }
items.append(item) items.append(item)
@ -213,9 +209,14 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
"enabled": True, "enabled": True,
"color": "#ffffff", "color": "#ffffff",
"index": -1, "index": -1,
"definition": "" "definition": "",
"material": "",
"variant": "",
"stack": None,
"material_brand": "",
"color_name": "",
} }
items.append(item) items.append(item)
if self._items != items:
self.setItems(items) self.setItems(items)
self.modelChanged.emit() self.modelChanged.emit()

View File

@ -1,20 +1,16 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
## Model that shows the list of favorite materials.
class FavoriteMaterialsModel(BaseMaterialsModel): class FavoriteMaterialsModel(BaseMaterialsModel):
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
self._update() self._update()
def _update(self): def _update(self):
# Perform standard check and reset if the check fails
if not self._canUpdate(): if not self._canUpdate():
self.setItems([])
return return
# Get updated list of favorites # Get updated list of favorites

View File

@ -0,0 +1,104 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional, Dict, Any, TYPE_CHECKING
from PyQt5.QtCore import QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
from UM.Qt.ListModel import ListModel
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
#
# This model holds all first-start machine actions for the currently active machine. It has 2 roles:
# - title : the title/name of the action
# - content : the QObject of the QML content of the action
# - action : the MachineAction object itself
#
class FirstStartMachineActionsModel(ListModel):
TitleRole = Qt.UserRole + 1
ContentRole = Qt.UserRole + 2
ActionRole = Qt.UserRole + 3
def __init__(self, application: "CuraApplication", parent: Optional[QObject] = None) -> None:
super().__init__(parent)
self.addRoleName(self.TitleRole, "title")
self.addRoleName(self.ContentRole, "content")
self.addRoleName(self.ActionRole, "action")
self._current_action_index = 0
self._application = application
self._application.initializationFinished.connect(self._initialize)
def _initialize(self) -> None:
self._application.getMachineManager().globalContainerChanged.connect(self._update)
self._update()
currentActionIndexChanged = pyqtSignal()
allFinished = pyqtSignal() # Emitted when all actions have been finished.
@pyqtProperty(int, notify = currentActionIndexChanged)
def currentActionIndex(self) -> int:
return self._current_action_index
@pyqtProperty("QVariantMap", notify = currentActionIndexChanged)
def currentItem(self) -> Optional[Dict[str, Any]]:
if self._current_action_index >= self.count:
return dict()
else:
return self.getItem(self._current_action_index)
@pyqtProperty(bool, notify = currentActionIndexChanged)
def hasMoreActions(self) -> bool:
return self._current_action_index < self.count - 1
@pyqtSlot()
def goToNextAction(self) -> None:
# finish the current item
if "action" in self.currentItem:
self.currentItem["action"].setFinished()
if not self.hasMoreActions:
self.allFinished.emit()
self.reset()
return
self._current_action_index += 1
self.currentActionIndexChanged.emit()
# Resets the current action index to 0 so the wizard panel can show actions from the beginning.
@pyqtSlot()
def reset(self) -> None:
self._current_action_index = 0
self.currentActionIndexChanged.emit()
if self.count == 0:
self.allFinished.emit()
def _update(self) -> None:
global_stack = self._application.getMachineManager().activeMachine
if global_stack is None:
self.setItems([])
return
definition_id = global_stack.definition.getId()
first_start_actions = self._application.getMachineActionManager().getFirstStartActions(definition_id)
item_list = []
for item in first_start_actions:
item_list.append({"title": item.label,
"content": item.displayItem,
"action": item,
})
item.reset()
self.setItems(item_list)
self.reset()
__all__ = ["FirstStartMachineActionsModel"]

View File

@ -1,7 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
class GenericMaterialsModel(BaseMaterialsModel): class GenericMaterialsModel(BaseMaterialsModel):
@ -11,10 +10,7 @@ class GenericMaterialsModel(BaseMaterialsModel):
self._update() self._update()
def _update(self): def _update(self):
# Perform standard check and reset if the check fails
if not self._canUpdate(): if not self._canUpdate():
self.setItems([])
return return
# Get updated list of favorites # Get updated list of favorites

View File

@ -0,0 +1,77 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, QTimer
from UM.Qt.ListModel import ListModel
from UM.i18n import i18nCatalog
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.GlobalStack import GlobalStack
class GlobalStacksModel(ListModel):
NameRole = Qt.UserRole + 1
IdRole = Qt.UserRole + 2
HasRemoteConnectionRole = Qt.UserRole + 3
ConnectionTypeRole = Qt.UserRole + 4
MetaDataRole = Qt.UserRole + 5
DiscoverySourceRole = Qt.UserRole + 6 # For separating local and remote printers in the machine management page
def __init__(self, parent = None) -> None:
super().__init__(parent)
self._catalog = i18nCatalog("cura")
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.IdRole, "id")
self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection")
self.addRoleName(self.MetaDataRole, "metadata")
self.addRoleName(self.DiscoverySourceRole, "discoverySource")
self._change_timer = QTimer()
self._change_timer.setInterval(200)
self._change_timer.setSingleShot(True)
self._change_timer.timeout.connect(self._update)
# Listen to changes
CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
CuraContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
self._updateDelayed()
## Handler for container added/removed events from registry
def _onContainerChanged(self, container) -> None:
# We only need to update when the added / removed container GlobalStack
if isinstance(container, GlobalStack):
self._updateDelayed()
def _updateDelayed(self) -> None:
self._change_timer.start()
def _update(self) -> None:
items = []
container_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine")
for container_stack in container_stacks:
has_remote_connection = False
for connection_type in container_stack.configuredConnectionTypes:
has_remote_connection |= connection_type in [ConnectionType.NetworkConnection.value,
ConnectionType.CloudConnection.value]
if container_stack.getMetaDataEntry("hidden", False) in ["True", True]:
continue
section_name = "Network enabled printers" if has_remote_connection else "Local printers"
section_name = self._catalog.i18nc("@info:title", section_name)
items.append({"name": container_stack.getMetaDataEntry("group_name", container_stack.getName()),
"id": container_stack.getId(),
"hasRemoteConnection": has_remote_connection,
"metadata": container_stack.getMetaData().copy(),
"discoverySource": section_name})
items.sort(key = lambda i: not i["hasRemoteConnection"])
self.setItems(items)

View File

@ -1,82 +0,0 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.Qt.ListModel import ListModel
from PyQt5.QtCore import Qt
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
#
# This the QML model for the quality management page.
#
class MachineManagementModel(ListModel):
NameRole = Qt.UserRole + 1
IdRole = Qt.UserRole + 2
MetaDataRole = Qt.UserRole + 3
GroupRole = Qt.UserRole + 4
def __init__(self, parent = None):
super().__init__(parent)
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.IdRole, "id")
self.addRoleName(self.MetaDataRole, "metadata")
self.addRoleName(self.GroupRole, "group")
self._local_container_stacks = []
self._network_container_stacks = []
# Listen to changes
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
ContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
ContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
self._filter_dict = {}
self._update()
## Handler for container added/removed events from registry
def _onContainerChanged(self, container):
# We only need to update when the added / removed container is a stack.
if isinstance(container, ContainerStack) and container.getMetaDataEntry("type") == "machine":
self._update()
## Private convenience function to reset & repopulate the model.
def _update(self):
items = []
# Get first the network enabled printers
network_filter_printers = {"type": "machine",
"um_network_key": "*",
"hidden": "False"}
self._network_container_stacks = ContainerRegistry.getInstance().findContainerStacks(**network_filter_printers)
self._network_container_stacks.sort(key = lambda i: i.getMetaDataEntry("connect_group_name"))
for container in self._network_container_stacks:
metadata = container.getMetaData().copy()
if container.getBottom():
metadata["definition_name"] = container.getBottom().getName()
items.append({"name": metadata["connect_group_name"],
"id": container.getId(),
"metadata": metadata,
"group": catalog.i18nc("@info:title", "Network enabled printers")})
# Get now the local printers
local_filter_printers = {"type": "machine", "um_network_key": None}
self._local_container_stacks = ContainerRegistry.getInstance().findContainerStacks(**local_filter_printers)
self._local_container_stacks.sort(key = lambda i: i.getName())
for container in self._local_container_stacks:
metadata = container.getMetaData().copy()
if container.getBottom():
metadata["definition_name"] = container.getBottom().getName()
items.append({"name": container.getName(),
"id": container.getId(),
"metadata": metadata,
"group": catalog.i18nc("@info:title", "Local printers")})
self.setItems(items)

View File

@ -1,9 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty from PyQt5.QtCore import Qt, pyqtSignal
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
class MaterialTypesModel(ListModel): class MaterialTypesModel(ListModel):
@ -28,12 +27,8 @@ class MaterialBrandsModel(BaseMaterialsModel):
self._update() self._update()
def _update(self): def _update(self):
# Perform standard check and reset if the check fails
if not self._canUpdate(): if not self._canUpdate():
self.setItems([])
return return
# Get updated list of favorites # Get updated list of favorites
self._favorite_ids = self._material_manager.getFavorites() self._favorite_ids = self._material_manager.getFavorites()

View File

@ -4,6 +4,7 @@
from PyQt5.QtCore import QTimer, pyqtSignal, pyqtProperty from PyQt5.QtCore import QTimer, pyqtSignal, pyqtProperty
from UM.Application import Application from UM.Application import Application
from UM.Scene.Camera import Camera
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
@ -34,8 +35,9 @@ class MultiBuildPlateModel(ListModel):
self._active_build_plate = -1 self._active_build_plate = -1
def setMaxBuildPlate(self, max_build_plate): def setMaxBuildPlate(self, max_build_plate):
self._max_build_plate = max_build_plate if self._max_build_plate != max_build_plate:
self.maxBuildPlateChanged.emit() self._max_build_plate = max_build_plate
self.maxBuildPlateChanged.emit()
## Return the highest build plate number ## Return the highest build plate number
@pyqtProperty(int, notify = maxBuildPlateChanged) @pyqtProperty(int, notify = maxBuildPlateChanged)
@ -43,15 +45,17 @@ class MultiBuildPlateModel(ListModel):
return self._max_build_plate return self._max_build_plate
def setActiveBuildPlate(self, nr): def setActiveBuildPlate(self, nr):
self._active_build_plate = nr if self._active_build_plate != nr:
self.activeBuildPlateChanged.emit() self._active_build_plate = nr
self.activeBuildPlateChanged.emit()
@pyqtProperty(int, notify = activeBuildPlateChanged) @pyqtProperty(int, notify = activeBuildPlateChanged)
def activeBuildPlate(self): def activeBuildPlate(self):
return self._active_build_plate return self._active_build_plate
def _updateSelectedObjectBuildPlateNumbersDelayed(self, *args): def _updateSelectedObjectBuildPlateNumbersDelayed(self, *args):
self._update_timer.start() if not isinstance(args[0], Camera):
self._update_timer.start()
def _updateSelectedObjectBuildPlateNumbers(self, *args): def _updateSelectedObjectBuildPlateNumbers(self, *args):
result = set() result = set()

View File

@ -33,8 +33,6 @@ class NozzleModel(ListModel):
def _update(self): def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
self.items.clear()
global_stack = self._machine_manager.activeMachine global_stack = self._machine_manager.activeMachine
if global_stack is None: if global_stack is None:
self.setItems([]) self.setItems([])

View File

@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt, QTimer
from UM.Application import Application from UM.Application import Application
from UM.Logger import Logger from UM.Logger import Logger
@ -10,6 +10,7 @@ from UM.Settings.SettingFunction import SettingFunction
from cura.Machines.QualityManager import QualityGroup from cura.Machines.QualityManager import QualityGroup
# #
# QML Model for all built-in quality profiles. This model is used for the drop-down quality menu. # QML Model for all built-in quality profiles. This model is used for the drop-down quality menu.
# #
@ -21,6 +22,7 @@ class QualityProfilesDropDownMenuModel(ListModel):
AvailableRole = Qt.UserRole + 5 AvailableRole = Qt.UserRole + 5
QualityGroupRole = Qt.UserRole + 6 QualityGroupRole = Qt.UserRole + 6
QualityChangesGroupRole = Qt.UserRole + 7 QualityChangesGroupRole = Qt.UserRole + 7
IsExperimentalRole = Qt.UserRole + 8
def __init__(self, parent = None): def __init__(self, parent = None):
super().__init__(parent) super().__init__(parent)
@ -32,19 +34,28 @@ class QualityProfilesDropDownMenuModel(ListModel):
self.addRoleName(self.AvailableRole, "available") #Whether the quality profile is available in our current nozzle + material. self.addRoleName(self.AvailableRole, "available") #Whether the quality profile is available in our current nozzle + material.
self.addRoleName(self.QualityGroupRole, "quality_group") self.addRoleName(self.QualityGroupRole, "quality_group")
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group") self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
self.addRoleName(self.IsExperimentalRole, "is_experimental")
self._application = Application.getInstance() self._application = Application.getInstance()
self._machine_manager = self._application.getMachineManager() self._machine_manager = self._application.getMachineManager()
self._quality_manager = Application.getInstance().getQualityManager() self._quality_manager = Application.getInstance().getQualityManager()
self._application.globalContainerStackChanged.connect(self._update) self._application.globalContainerStackChanged.connect(self._onChange)
self._machine_manager.activeQualityGroupChanged.connect(self._update) self._machine_manager.activeQualityGroupChanged.connect(self._onChange)
self._machine_manager.extruderChanged.connect(self._update) self._machine_manager.extruderChanged.connect(self._onChange)
self._quality_manager.qualitiesUpdated.connect(self._update) self._quality_manager.qualitiesUpdated.connect(self._onChange)
self._layer_height_unit = "" # This is cached self._layer_height_unit = "" # This is cached
self._update() self._update_timer = QTimer() # type: QTimer
self._update_timer.setInterval(100)
self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._update)
self._onChange()
def _onChange(self) -> None:
self._update_timer.start()
def _update(self): def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__)) Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
@ -74,7 +85,8 @@ class QualityProfilesDropDownMenuModel(ListModel):
"layer_height": layer_height, "layer_height": layer_height,
"layer_height_unit": self._layer_height_unit, "layer_height_unit": self._layer_height_unit,
"available": quality_group.is_available, "available": quality_group.is_available,
"quality_group": quality_group} "quality_group": quality_group,
"is_experimental": quality_group.is_experimental}
item_list.append(item) item_list.append(item)

View File

@ -6,6 +6,7 @@ from typing import Optional, List
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from UM.Logger import Logger from UM.Logger import Logger
from UM.Preferences import Preferences
from UM.Resources import Resources from UM.Resources import Resources
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
@ -18,14 +19,20 @@ class SettingVisibilityPresetsModel(QObject):
onItemsChanged = pyqtSignal() onItemsChanged = pyqtSignal()
activePresetChanged = pyqtSignal() activePresetChanged = pyqtSignal()
def __init__(self, preferences, parent = None): def __init__(self, preferences: Preferences, parent = None) -> None:
super().__init__(parent) super().__init__(parent)
self._items = [] # type: List[SettingVisibilityPreset] self._items = [] # type: List[SettingVisibilityPreset]
self._custom_preset = SettingVisibilityPreset(preset_id = "custom", name = "Custom selection", weight = -100)
self._populate() self._populate()
basic_item = self.getVisibilityPresetById("basic") basic_item = self.getVisibilityPresetById("basic")
basic_visibile_settings = ";".join(basic_item.settings) if basic_item is not None:
basic_visibile_settings = ";".join(basic_item.settings)
else:
Logger.log("w", "Unable to find the basic visiblity preset.")
basic_visibile_settings = ""
self._preferences = preferences self._preferences = preferences
@ -42,7 +49,8 @@ class SettingVisibilityPresetsModel(QObject):
visible_settings = self._preferences.getValue("general/visible_settings") visible_settings = self._preferences.getValue("general/visible_settings")
if not visible_settings: if not visible_settings:
self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item.settings)) new_visible_settings = self._active_preset_item.settings if self._active_preset_item is not None else []
self._preferences.setValue("general/visible_settings", ";".join(new_visible_settings))
else: else:
self._onPreferencesChanged("general/visible_settings") self._onPreferencesChanged("general/visible_settings")
@ -59,9 +67,7 @@ class SettingVisibilityPresetsModel(QObject):
def _populate(self) -> None: def _populate(self) -> None:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
items = [] # type: List[SettingVisibilityPreset] items = [] # type: List[SettingVisibilityPreset]
items.append(self._custom_preset)
custom_preset = SettingVisibilityPreset(preset_id="custom", name ="Custom selection", weight = -100)
items.append(custom_preset)
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset): for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
setting_visibility_preset = SettingVisibilityPreset() setting_visibility_preset = SettingVisibilityPreset()
try: try:
@ -77,7 +83,7 @@ class SettingVisibilityPresetsModel(QObject):
self.setItems(items) self.setItems(items)
@pyqtProperty("QVariantList", notify = onItemsChanged) @pyqtProperty("QVariantList", notify = onItemsChanged)
def items(self): def items(self) -> List[SettingVisibilityPreset]:
return self._items return self._items
def setItems(self, items: List[SettingVisibilityPreset]) -> None: def setItems(self, items: List[SettingVisibilityPreset]) -> None:
@ -87,7 +93,7 @@ class SettingVisibilityPresetsModel(QObject):
@pyqtSlot(str) @pyqtSlot(str)
def setActivePreset(self, preset_id: str) -> None: def setActivePreset(self, preset_id: str) -> None:
if preset_id == self._active_preset_item.presetId: if self._active_preset_item is not None and preset_id == self._active_preset_item.presetId:
Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id) Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id)
return return
@ -96,7 +102,7 @@ class SettingVisibilityPresetsModel(QObject):
Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id) Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
return return
need_to_save_to_custom = self._active_preset_item.presetId == "custom" and preset_id != "custom" need_to_save_to_custom = self._active_preset_item is None or (self._active_preset_item.presetId == "custom" and preset_id != "custom")
if need_to_save_to_custom: if need_to_save_to_custom:
# Save the current visibility settings to custom # Save the current visibility settings to custom
current_visibility_string = self._preferences.getValue("general/visible_settings") current_visibility_string = self._preferences.getValue("general/visible_settings")
@ -117,7 +123,9 @@ class SettingVisibilityPresetsModel(QObject):
@pyqtProperty(str, notify = activePresetChanged) @pyqtProperty(str, notify = activePresetChanged)
def activePreset(self) -> str: def activePreset(self) -> str:
return self._active_preset_item.presetId if self._active_preset_item is not None:
return self._active_preset_item.presetId
return ""
def _onPreferencesChanged(self, name: str) -> None: def _onPreferencesChanged(self, name: str) -> None:
if name != "general/visible_settings": if name != "general/visible_settings":
@ -140,7 +148,7 @@ class SettingVisibilityPresetsModel(QObject):
item_to_set = self._active_preset_item item_to_set = self._active_preset_item
if matching_preset_item is None: if matching_preset_item is None:
# The new visibility setup is "custom" should be custom # The new visibility setup is "custom" should be custom
if self._active_preset_item.presetId == "custom": if self._active_preset_item is None or self._active_preset_item.presetId == "custom":
# We are already in custom, just save the settings # We are already in custom, just save the settings
self._preferences.setValue("cura/custom_visible_settings", visibility_string) self._preferences.setValue("cura/custom_visible_settings", visibility_string)
else: else:
@ -149,7 +157,12 @@ class SettingVisibilityPresetsModel(QObject):
else: else:
item_to_set = matching_preset_item item_to_set = matching_preset_item
# If we didn't find a matching preset, fallback to custom.
if item_to_set is None:
item_to_set = self._custom_preset
if self._active_preset_item is None or self._active_preset_item.presetId != item_to_set.presetId: 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._active_preset_item = item_to_set
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item.presetId) if self._active_preset_item is not None:
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item.presetId)
self.activePresetChanged.emit() self.activePresetChanged.emit()

View File

@ -10,7 +10,6 @@ from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.Settings.SettingFunction import SettingFunction from UM.Settings.SettingFunction import SettingFunction
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel

View File

@ -4,6 +4,9 @@
from typing import Dict, Optional, List, Set from typing import Dict, Optional, List, Set
from PyQt5.QtCore import QObject, pyqtSlot from PyQt5.QtCore import QObject, pyqtSlot
from UM.Util import parseBool
from cura.Machines.ContainerNode import ContainerNode from cura.Machines.ContainerNode import ContainerNode
@ -29,6 +32,7 @@ class QualityGroup(QObject):
self.nodes_for_extruders = {} # type: Dict[int, ContainerNode] self.nodes_for_extruders = {} # type: Dict[int, ContainerNode]
self.quality_type = quality_type self.quality_type = quality_type
self.is_available = False self.is_available = False
self.is_experimental = False
@pyqtSlot(result = str) @pyqtSlot(result = str)
def getName(self) -> str: def getName(self) -> str:
@ -51,3 +55,17 @@ class QualityGroup(QObject):
for extruder_node in self.nodes_for_extruders.values(): for extruder_node in self.nodes_for_extruders.values():
result.append(extruder_node) result.append(extruder_node)
return result return result
def setGlobalNode(self, node: "ContainerNode") -> None:
self.node_for_global = node
# Update is_experimental flag
is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
self.is_experimental |= is_experimental
def setExtruderNode(self, position: int, node: "ContainerNode") -> None:
self.nodes_for_extruders[position] = node
# Update is_experimental flag
is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
self.is_experimental |= is_experimental

View File

@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import TYPE_CHECKING, Optional, cast, Dict, List from typing import TYPE_CHECKING, Optional, cast, Dict, List, Set
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
@ -16,7 +16,7 @@ from .QualityGroup import QualityGroup
from .QualityNode import QualityNode from .QualityNode import QualityNode
if TYPE_CHECKING: if TYPE_CHECKING:
from UM.Settings.DefinitionContainer import DefinitionContainer from UM.Settings.Interfaces import DefinitionContainerInterface
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from .QualityChangesGroup import QualityChangesGroup from .QualityChangesGroup import QualityChangesGroup
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
@ -209,6 +209,7 @@ class QualityManager(QObject):
# (1) the machine-specific node # (1) the machine-specific node
# (2) the generic node # (2) the generic node
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id) machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
# Check if this machine has specific quality profiles for its extruders, if so, when looking up extruder # Check if this machine has specific quality profiles for its extruders, if so, when looking up extruder
# qualities, we should not fall back to use the global qualities. # qualities, we should not fall back to use the global qualities.
has_extruder_specific_qualities = False has_extruder_specific_qualities = False
@ -235,7 +236,7 @@ class QualityManager(QObject):
for quality_type, quality_node in node.quality_type_map.items(): for quality_type, quality_node in node.quality_type_map.items():
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type) quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
quality_group.node_for_global = quality_node quality_group.setGlobalNode(quality_node)
quality_group_dict[quality_type] = quality_group quality_group_dict[quality_type] = quality_group
break break
@ -259,11 +260,15 @@ class QualityManager(QObject):
root_material_id = self._material_manager.getRootMaterialIDWithoutDiameter(root_material_id) root_material_id = self._material_manager.getRootMaterialIDWithoutDiameter(root_material_id)
root_material_id_list.append(root_material_id) root_material_id_list.append(root_material_id)
# Also try to get the fallback material # Also try to get the fallback materials
material_type = extruder.material.getMetaDataEntry("material") fallback_ids = self._material_manager.getFallBackMaterialIdsByMaterial(extruder.material)
fallback_root_material_id = self._material_manager.getFallbackMaterialIdByMaterialType(material_type)
if fallback_root_material_id: if fallback_ids:
root_material_id_list.append(fallback_root_material_id) 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. # 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 # The use case is that, when we look for qualities for a machine, we first want to search in the following
@ -333,7 +338,7 @@ class QualityManager(QObject):
quality_group = quality_group_dict[quality_type] quality_group = quality_group_dict[quality_type]
if position not in quality_group.nodes_for_extruders: if position not in quality_group.nodes_for_extruders:
quality_group.nodes_for_extruders[position] = quality_node quality_group.setExtruderNode(position, quality_node)
# If the machine has its own specific qualities, for extruders, it should skip the global qualities # If the machine has its own specific qualities, for extruders, it should skip the global qualities
# and use the material/variant specific qualities. # and use the material/variant specific qualities.
@ -363,7 +368,7 @@ class QualityManager(QObject):
if node and node.quality_type_map: if node and node.quality_type_map:
for quality_type, quality_node in node.quality_type_map.items(): for quality_type, quality_node in node.quality_type_map.items():
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type) quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
quality_group.node_for_global = quality_node quality_group.setGlobalNode(quality_node)
quality_group_dict[quality_type] = quality_group quality_group_dict[quality_type] = quality_group
break break
@ -437,7 +442,8 @@ class QualityManager(QObject):
quality_changes_group = quality_model_item["quality_changes_group"] quality_changes_group = quality_model_item["quality_changes_group"]
if quality_changes_group is None: if quality_changes_group is None:
# create global quality changes only # create global quality changes only
new_quality_changes = self._createQualityChanges(quality_group.quality_type, quality_changes_name, new_name = self._container_registry.uniqueName(quality_changes_name)
new_quality_changes = self._createQualityChanges(quality_group.quality_type, new_name,
global_stack, None) global_stack, None)
self._container_registry.addContainer(new_quality_changes) self._container_registry.addContainer(new_quality_changes)
else: else:
@ -534,7 +540,7 @@ class QualityManager(QObject):
# Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended # Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
# shares the same set of qualities profiles as Ultimaker 3. # shares the same set of qualities profiles as Ultimaker 3.
# #
def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainer", def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainerInterface",
default_definition_id: str = "fdmprinter") -> str: default_definition_id: str = "fdmprinter") -> str:
machine_definition_id = default_definition_id machine_definition_id = default_definition_id
if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)): if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)):

View File

@ -107,7 +107,7 @@ class VariantManager:
break break
return variant_node return variant_node
return self._machine_to_variant_dict_map[machine_definition_id].get(variant_type, {}).get(variant_name) return self._machine_to_variant_dict_map.get(machine_definition_id, {}).get(variant_type, {}).get(variant_name)
def getVariantNodes(self, machine: "GlobalStack", variant_type: "VariantType") -> Dict[str, ContainerNode]: def getVariantNodes(self, machine: "GlobalStack", variant_type: "VariantType") -> Dict[str, ContainerNode]:
machine_definition_id = machine.definition.getId() machine_definition_id = machine.definition.getId()

View File

@ -25,7 +25,7 @@ class MultiplyObjectsJob(Job):
def run(self): def run(self):
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0, status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Object")) dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Objects"))
status_message.show() status_message.show()
scene = Application.getInstance().getController().getScene() scene = Application.getInstance().getController().getScene()

View File

@ -1,33 +1,37 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from datetime import datetime
import json import json
import random import random
from hashlib import sha512 from hashlib import sha512
from base64 import b64encode from base64 import b64encode
from typing import Dict, Optional from typing import Optional
import requests import requests
from UM.i18n import i18nCatalog
from UM.Logger import Logger from UM.Logger import Logger
from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings from cura.OAuth2.Models import AuthenticationResponse, UserProfile, OAuth2Settings
catalog = i18nCatalog("cura")
TOKEN_TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S"
## Class containing several helpers to deal with the authorization flow.
# Class containing several helpers to deal with the authorization flow.
class AuthorizationHelpers: class AuthorizationHelpers:
def __init__(self, settings: "OAuth2Settings") -> None: def __init__(self, settings: "OAuth2Settings") -> None:
self._settings = settings self._settings = settings
self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL) self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
@property @property
# The OAuth2 settings object. ## The OAuth2 settings object.
def settings(self) -> "OAuth2Settings": def settings(self) -> "OAuth2Settings":
return self._settings return self._settings
# Request the access token from the authorization server. ## Request the access token from the authorization server.
# \param authorization_code: The authorization code from the 1st step. # \param authorization_code: The authorization code from the 1st step.
# \param verification_code: The verification code needed for the PKCE extension. # \param verification_code: The verification code needed for the PKCE
# \return: An AuthenticationResponse object. # extension.
# \return An AuthenticationResponse object.
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse": def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse":
data = { data = {
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", "client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
@ -37,12 +41,16 @@ class AuthorizationHelpers:
"code_verifier": verification_code, "code_verifier": verification_code,
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "", "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 try:
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
except requests.exceptions.ConnectionError:
return AuthenticationResponse(success=False, err_message="Unable to connect to remote server")
# Request the access token from the authorization server using a refresh token. ## Request the access token from the authorization server using a refresh token.
# \param refresh_token: # \param refresh_token:
# \return: An AuthenticationResponse object. # \return An AuthenticationResponse object.
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse": def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
Logger.log("d", "Refreshing the access token.")
data = { data = {
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "", "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 "", "redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
@ -50,12 +58,15 @@ class AuthorizationHelpers:
"refresh_token": refresh_token, "refresh_token": refresh_token,
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "", "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 try:
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
except requests.exceptions.ConnectionError:
return AuthenticationResponse(success=False, err_message="Unable to connect to remote server")
@staticmethod @staticmethod
# Parse the token response from the authorization server into an AuthenticationResponse object. ## Parse the token response from the authorization server into an AuthenticationResponse object.
# \param token_response: The JSON string data response from the authorization server. # \param token_response: The JSON string data response from the authorization server.
# \return: An AuthenticationResponse object. # \return An AuthenticationResponse object.
def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse": def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse":
token_data = None token_data = None
@ -65,25 +76,31 @@ class AuthorizationHelpers:
Logger.log("w", "Could not parse token response data: %s", token_response.text) Logger.log("w", "Could not parse token response data: %s", token_response.text)
if not token_data: if not token_data:
return AuthenticationResponse(success=False, err_message="Could not read response.") return AuthenticationResponse(success = False, err_message = catalog.i18nc("@message", "Could not read response."))
if token_response.status_code not in (200, 201): if token_response.status_code not in (200, 201):
return AuthenticationResponse(success=False, err_message=token_data["error_description"]) return AuthenticationResponse(success = False, err_message = token_data["error_description"])
return AuthenticationResponse(success=True, return AuthenticationResponse(success=True,
token_type=token_data["token_type"], token_type=token_data["token_type"],
access_token=token_data["access_token"], access_token=token_data["access_token"],
refresh_token=token_data["refresh_token"], refresh_token=token_data["refresh_token"],
expires_in=token_data["expires_in"], expires_in=token_data["expires_in"],
scope=token_data["scope"]) scope=token_data["scope"],
received_at=datetime.now().strftime(TOKEN_TIMESTAMP_FORMAT))
# Calls the authentication API endpoint to get the token data. ## Calls the authentication API endpoint to get the token data.
# \param access_token: The encoded JWT token. # \param access_token: The encoded JWT token.
# \return: Dict containing some profile data. # \return Dict containing some profile data.
def parseJWT(self, access_token: str) -> Optional["UserProfile"]: def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = { try:
"Authorization": "Bearer {}".format(access_token) token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
}) "Authorization": "Bearer {}".format(access_token)
})
except requests.exceptions.ConnectionError:
# Connection was suddenly dropped. Nothing we can do about that.
Logger.log("w", "Something failed while attempting to parse the JWT token")
return None
if token_request.status_code not in (200, 201): if token_request.status_code not in (200, 201):
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text) Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
return None return None
@ -98,15 +115,15 @@ class AuthorizationHelpers:
) )
@staticmethod @staticmethod
# Generate a 16-character verification code. ## Generate a 16-character verification code.
# \param code_length: How long should the code be? # \param code_length: How long should the code be?
def generateVerificationCode(code_length: int = 16) -> str: def generateVerificationCode(code_length: int = 16) -> str:
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length)) return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
@staticmethod @staticmethod
# Generates a base64 encoded sha512 encrypted version of a given string. ## Generates a base64 encoded sha512 encrypted version of a given string.
# \param verification_code: # \param verification_code:
# \return: The encrypted code in base64 format. # \return The encrypted code in base64 format.
def generateVerificationCodeChallenge(verification_code: str) -> str: def generateVerificationCodeChallenge(verification_code: str) -> str:
encoded = sha512(verification_code.encode()).digest() encoded = sha512(verification_code.encode()).digest()
return b64encode(encoded, altchars = b"_-").decode() return b64encode(encoded, altchars = b"_-").decode()

View File

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

View File

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

View File

@ -1,28 +1,33 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import json import json
import webbrowser import webbrowser
from datetime import datetime, timedelta
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from urllib.parse import urlencode from urllib.parse import urlencode
import requests.exceptions
from UM.Logger import Logger from UM.Logger import Logger
from UM.Message import Message
from UM.Signal import Signal from UM.Signal import Signal
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers, TOKEN_TIMESTAMP_FORMAT
from cura.OAuth2.Models import AuthenticationResponse from cura.OAuth2.Models import AuthenticationResponse
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.OAuth2.Models import UserProfile, OAuth2Settings from cura.OAuth2.Models import UserProfile, OAuth2Settings
from UM.Preferences import Preferences from UM.Preferences import Preferences
## The authorization service is responsible for handling the login flow,
# storing user credentials and providing account information.
class AuthorizationService: 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. # Emit signal when authentication is completed.
onAuthStateChanged = Signal() onAuthStateChanged = Signal()
@ -38,31 +43,48 @@ class AuthorizationService:
self._preferences = preferences self._preferences = preferences
self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True) self._server = LocalAuthorizationServer(self._auth_helpers, self._onAuthStateChanged, daemon=True)
self._unable_to_get_data_message = None # type: Optional[Message]
self.onAuthStateChanged.connect(self._authChanged)
def _authChanged(self, logged_in):
if logged_in and self._unable_to_get_data_message is not None:
self._unable_to_get_data_message.hide()
def initialize(self, preferences: Optional["Preferences"] = None) -> None: def initialize(self, preferences: Optional["Preferences"] = None) -> None:
if preferences is not None: if preferences is not None:
self._preferences = preferences self._preferences = preferences
if self._preferences: if self._preferences:
self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}") self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}")
# Get the user profile as obtained from the JWT (JSON Web Token). ## 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. # If the JWT is not yet parsed, calling this will take care of that.
# \return UserProfile if a user is logged in, None otherwise. # \return UserProfile if a user is logged in, None otherwise.
# \sa _parseJWT # \sa _parseJWT
def getUserProfile(self) -> Optional["UserProfile"]: def getUserProfile(self) -> Optional["UserProfile"]:
if not self._user_profile: if not self._user_profile:
# If no user profile was stored locally, we try to get it from JWT. # If no user profile was stored locally, we try to get it from JWT.
self._user_profile = self._parseJWT() try:
if not self._user_profile: self._user_profile = self._parseJWT()
except requests.exceptions.ConnectionError:
# Unable to get connection, can't login.
Logger.logException("w", "Unable to validate user data with the remote server.")
return None
if not self._user_profile and self._auth_data:
# If there is still no user profile from the JWT, we have to log in again. # If there is still no user profile from the JWT, we have to log in again.
Logger.log("w", "The user profile could not be loaded. The user must log in again!")
self.deleteAuthData()
return None return None
return self._user_profile return self._user_profile
# Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there. ## 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. # \return UserProfile if it was able to parse, None otherwise.
def _parseJWT(self) -> Optional["UserProfile"]: def _parseJWT(self) -> Optional["UserProfile"]:
if not self._auth_data or self._auth_data.access_token is None: if not self._auth_data or self._auth_data.access_token is None:
# If no auth data exists, we should always log in again. # If no auth data exists, we should always log in again.
Logger.log("d", "There was no auth data or access token")
return None return None
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token) user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
if user_data: if user_data:
@ -70,41 +92,51 @@ class AuthorizationService:
return user_data return user_data
# The JWT was expired or invalid and we should request a new one. # The JWT was expired or invalid and we should request a new one.
if self._auth_data.refresh_token is None: if self._auth_data.refresh_token is None:
Logger.log("w", "There was no refresh token in the auth data.")
return None return None
self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token) 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: if not self._auth_data or self._auth_data.access_token is None:
Logger.log("w", "Unable to use the refresh token to get a new access token.")
# The token could not be refreshed using the refresh token. We should login again. # The token could not be refreshed using the refresh token. We should login again.
return None return None
return self._auth_helpers.parseJWT(self._auth_data.access_token) return self._auth_helpers.parseJWT(self._auth_data.access_token)
# Get the access token as provided by the repsonse data. ## Get the access token as provided by the repsonse data.
def getAccessToken(self) -> Optional[str]: 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: if self._auth_data is None:
Logger.log("d", "No auth data to retrieve the access_token from")
return None return None
return self._auth_data.access_token # Check if the current access token is expired and refresh it if that is the case.
# We have a fallback on a date far in the past for currently stored auth data in cura.cfg.
received_at = datetime.strptime(self._auth_data.received_at, TOKEN_TIMESTAMP_FORMAT) \
if self._auth_data.received_at else datetime(2000, 1, 1)
expiry_date = received_at + timedelta(seconds = float(self._auth_data.expires_in or 0) - 60)
if datetime.now() > expiry_date:
self.refreshAccessToken()
# Try to refresh the access token. This should be used when it has expired. return self._auth_data.access_token if self._auth_data else None
## Try to refresh the access token. This should be used when it has expired.
def refreshAccessToken(self) -> None: def refreshAccessToken(self) -> None:
if self._auth_data is None or self._auth_data.refresh_token is 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.") Logger.log("w", "Unable to refresh access token, since there is no refresh token.")
return return
self._storeAuthData(self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)) response = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
self.onAuthStateChanged.emit(logged_in=True) if response.success:
self._storeAuthData(response)
self.onAuthStateChanged.emit(logged_in = True)
else:
self.onAuthStateChanged.emit(logged_in = False)
# Delete the authentication data that we have stored locally (eg; logout) ## Delete the authentication data that we have stored locally (eg; logout)
def deleteAuthData(self) -> None: def deleteAuthData(self) -> None:
if self._auth_data is not None: if self._auth_data is not None:
self._storeAuthData() self._storeAuthData()
self.onAuthStateChanged.emit(logged_in=False) self.onAuthStateChanged.emit(logged_in = False)
# Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login. ## Start the flow to become authenticated. This will start a new webbrowser tap, prompting the user to login.
def startAuthorizationFlow(self) -> None: def startAuthorizationFlow(self) -> None:
Logger.log("d", "Starting new OAuth2 flow...") Logger.log("d", "Starting new OAuth2 flow...")
@ -120,7 +152,7 @@ class AuthorizationService:
"redirect_uri": self._settings.CALLBACK_URL, "redirect_uri": self._settings.CALLBACK_URL,
"scope": self._settings.CLIENT_SCOPES, "scope": self._settings.CLIENT_SCOPES,
"response_type": "code", "response_type": "code",
"state": "CuraDriveIsAwesome", "state": "(.Y.)",
"code_challenge": challenge_code, "code_challenge": challenge_code,
"code_challenge_method": "S512" "code_challenge_method": "S512"
}) })
@ -131,16 +163,16 @@ class AuthorizationService:
# Start a local web server to receive the callback URL on. # Start a local web server to receive the callback URL on.
self._server.start(verification_code) self._server.start(verification_code)
# Callback method for the authentication flow. ## Callback method for the authentication flow.
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None: def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
if auth_response.success: if auth_response.success:
self._storeAuthData(auth_response) self._storeAuthData(auth_response)
self.onAuthStateChanged.emit(logged_in=True) self.onAuthStateChanged.emit(logged_in = True)
else: else:
self.onAuthenticationError.emit(logged_in=False, error_message=auth_response.err_message) self.onAuthenticationError.emit(logged_in = False, error_message = auth_response.err_message)
self._server.stop() # Stop the web server at all times. self._server.stop() # Stop the web server at all times.
# Load authentication data from preferences. ## Load authentication data from preferences.
def loadAuthDataFromPreferences(self) -> None: def loadAuthDataFromPreferences(self) -> None:
if self._preferences is None: if self._preferences is None:
Logger.log("e", "Unable to load authentication data, since no preference has been set!") Logger.log("e", "Unable to load authentication data, since no preference has been set!")
@ -149,11 +181,22 @@ class AuthorizationService:
preferences_data = json.loads(self._preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY)) preferences_data = json.loads(self._preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY))
if preferences_data: if preferences_data:
self._auth_data = AuthenticationResponse(**preferences_data) self._auth_data = AuthenticationResponse(**preferences_data)
self.onAuthStateChanged.emit(logged_in=True) # Also check if we can actually get the user profile information.
user_profile = self.getUserProfile()
if user_profile is not None:
self.onAuthStateChanged.emit(logged_in = True)
else:
if self._unable_to_get_data_message is not None:
self._unable_to_get_data_message.hide()
self._unable_to_get_data_message = Message(i18n_catalog.i18nc("@info", "Unable to reach the Ultimaker account server."), title = i18n_catalog.i18nc("@info:title", "Warning"))
self._unable_to_get_data_message.addAction("retry", i18n_catalog.i18nc("@action:button", "Retry"), "[no_icon]", "[no_description]")
self._unable_to_get_data_message.actionTriggered.connect(self._onMessageActionTriggered)
self._unable_to_get_data_message.show()
except ValueError: except ValueError:
Logger.logException("w", "Could not load auth data from preferences") Logger.logException("w", "Could not load auth data from preferences")
# Store authentication data in preferences. ## Store authentication data in preferences.
def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None: def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
if self._preferences is None: if self._preferences is None:
Logger.log("e", "Unable to save authentication data, since no preference has been set!") Logger.log("e", "Unable to save authentication data, since no preference has been set!")
@ -166,3 +209,7 @@ class AuthorizationService:
else: else:
self._user_profile = None self._user_profile = None
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY) self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
def _onMessageActionTriggered(self, _, action):
if action == "retry":
self.loadAuthDataFromPreferences()

View File

@ -1,5 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import threading import threading
from typing import Optional, Callable, Any, TYPE_CHECKING from typing import Optional, Callable, Any, TYPE_CHECKING
@ -14,12 +15,15 @@ if TYPE_CHECKING:
class LocalAuthorizationServer: class LocalAuthorizationServer:
# The local LocalAuthorizationServer takes care of the oauth2 callbacks. ## The local LocalAuthorizationServer takes care of the oauth2 callbacks.
# Once the flow is completed, this server should be closed down again by calling stop() # Once the flow is completed, this server should be closed down again by
# \param auth_helpers: An instance of the authorization helpers class. # calling stop()
# \param auth_state_changed_callback: A callback function to be called when the authorization state changes. # \param auth_helpers An instance of the authorization helpers class.
# \param daemon: Whether the server thread should be run in daemon mode. Note: Daemon threads are abruptly stopped # \param auth_state_changed_callback A callback function to be called when
# at shutdown. Their resources (e.g. open files) may never be released. # 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", def __init__(self, auth_helpers: "AuthorizationHelpers",
auth_state_changed_callback: Callable[["AuthenticationResponse"], Any], auth_state_changed_callback: Callable[["AuthenticationResponse"], Any],
daemon: bool) -> None: daemon: bool) -> None:
@ -30,8 +34,8 @@ class LocalAuthorizationServer:
self._auth_state_changed_callback = auth_state_changed_callback self._auth_state_changed_callback = auth_state_changed_callback
self._daemon = daemon self._daemon = daemon
# Starts the local web server to handle the authorization callback. ## Starts the local web server to handle the authorization callback.
# \param verification_code: The verification code part of the OAuth2 client identification. # \param verification_code The verification code part of the OAuth2 client identification.
def start(self, verification_code: str) -> None: def start(self, verification_code: str) -> None:
if self._web_server: if self._web_server:
# If the server is already running (because of a previously aborted auth flow), we don't have to start it. # If the server is already running (because of a previously aborted auth flow), we don't have to start it.
@ -54,7 +58,7 @@ class LocalAuthorizationServer:
self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon) self._web_server_thread = threading.Thread(None, self._web_server.serve_forever, daemon = self._daemon)
self._web_server_thread.start() self._web_server_thread.start()
# Stops the web server if it was running. It also does some cleanup. ## Stops the web server if it was running. It also does some cleanup.
def stop(self) -> None: def stop(self) -> None:
Logger.log("d", "Stopping local oauth2 web server...") Logger.log("d", "Stopping local oauth2 web server...")

View File

@ -1,4 +1,5 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional from typing import Optional
@ -7,7 +8,7 @@ class BaseModel:
self.__dict__.update(kwargs) self.__dict__.update(kwargs)
# OAuth OAuth2Settings data template. ## OAuth OAuth2Settings data template.
class OAuth2Settings(BaseModel): class OAuth2Settings(BaseModel):
CALLBACK_PORT = None # type: Optional[int] CALLBACK_PORT = None # type: Optional[int]
OAUTH_SERVER_URL = None # type: Optional[str] OAUTH_SERVER_URL = None # type: Optional[str]
@ -19,14 +20,14 @@ class OAuth2Settings(BaseModel):
AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str
# User profile data template. ## User profile data template.
class UserProfile(BaseModel): class UserProfile(BaseModel):
user_id = None # type: Optional[str] user_id = None # type: Optional[str]
username = None # type: Optional[str] username = None # type: Optional[str]
profile_image_url = None # type: Optional[str] profile_image_url = None # type: Optional[str]
# Authentication data template. ## Authentication data template.
class AuthenticationResponse(BaseModel): class AuthenticationResponse(BaseModel):
"""Data comes from the token response with success flag and error message added.""" """Data comes from the token response with success flag and error message added."""
success = True # type: bool success = True # type: bool
@ -36,25 +37,25 @@ class AuthenticationResponse(BaseModel):
expires_in = None # type: Optional[str] expires_in = None # type: Optional[str]
scope = None # type: Optional[str] scope = None # type: Optional[str]
err_message = None # type: Optional[str] err_message = None # type: Optional[str]
received_at = None # type: Optional[str]
# Response status template. ## Response status template.
class ResponseStatus(BaseModel): class ResponseStatus(BaseModel):
code = 200 # type: int code = 200 # type: int
message = "" # type str message = "" # type: str
# Response data template. ## Response data template.
class ResponseData(BaseModel): class ResponseData(BaseModel):
status = None # type: ResponseStatus status = None # type: ResponseStatus
data_stream = None # type: Optional[bytes] data_stream = None # type: Optional[bytes]
redirect_uri = None # type: Optional[str] redirect_uri = None # type: Optional[str]
content_type = "text/html" # type: str content_type = "text/html" # type: str
## Possible HTTP responses.
# Possible HTTP responses.
HTTP_STATUS = { HTTP_STATUS = {
"OK": ResponseStatus(code=200, message="OK"), "OK": ResponseStatus(code = 200, message = "OK"),
"NOT_FOUND": ResponseStatus(code=404, message="NOT FOUND"), "NOT_FOUND": ResponseStatus(code = 404, message = "NOT FOUND"),
"REDIRECT": ResponseStatus(code=302, message="REDIRECT") "REDIRECT": ResponseStatus(code = 302, message = "REDIRECT")
} }

View File

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

View File

@ -29,4 +29,4 @@ class PlatformPhysicsOperation(Operation):
return group return group
def __repr__(self): def __repr__(self):
return "PlatformPhysicsOperation(translation = {0})".format(self._translation) return "PlatformPhysicsOp.(trans.={0})".format(self._translation)

View File

@ -17,7 +17,6 @@ from cura.Scene import ZOffsetDecorator
import random # used for list shuffling import random # used for list shuffling
class PlatformPhysics: class PlatformPhysics:
def __init__(self, controller, volume): def __init__(self, controller, volume):
super().__init__() super().__init__()
@ -40,8 +39,9 @@ class PlatformPhysics:
Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True) Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True)
def _onSceneChanged(self, source): def _onSceneChanged(self, source):
if not source.getMeshData(): if not source.callDecoration("isSliceable"):
return return
self._change_timer.start() self._change_timer.start()
def _onChangeTimerFinished(self): def _onChangeTimerFinished(self):

View File

@ -9,7 +9,7 @@ from typing import Union
MYPY = False MYPY = False
if MYPY: if MYPY:
from cura.PrinterOutputDevice import PrinterOutputDevice from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
class FirmwareUpdater(QObject): class FirmwareUpdater(QObject):
firmwareProgressChanged = pyqtSignal() firmwareProgressChanged = pyqtSignal()

View File

@ -3,14 +3,15 @@
from typing import TYPE_CHECKING, Set, Union, Optional from typing import TYPE_CHECKING, Set, Union, Optional
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from PyQt5.QtCore import QTimer from PyQt5.QtCore import QTimer
from .PrinterOutputController import PrinterOutputController
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from .Models.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from .Models.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice from .PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel from .Models.ExtruderOutputModel import ExtruderOutputModel
class GenericOutputController(PrinterOutputController): class GenericOutputController(PrinterOutputController):
@ -81,8 +82,8 @@ class GenericOutputController(PrinterOutputController):
self._output_device.cancelPrint() self._output_device.cancelPrint()
pass pass
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int) -> None: def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: float) -> None:
self._output_device.sendCommand("M140 S%s" % temperature) self._output_device.sendCommand("M140 S%s" % round(temperature)) # The API doesn't allow floating point.
def _onTargetBedTemperatureChanged(self) -> None: def _onTargetBedTemperatureChanged(self) -> None:
if self._preheat_bed_timer.isActive() and self._preheat_printer and self._preheat_printer.targetBedTemperature == 0: if self._preheat_bed_timer.isActive() and self._preheat_printer and self._preheat_printer.targetBedTemperature == 0:
@ -96,14 +97,14 @@ class GenericOutputController(PrinterOutputController):
except ValueError: except ValueError:
return # Got invalid values, can't pre-heat. return # Got invalid values, can't pre-heat.
self.setTargetBedTemperature(printer, temperature=temperature) self.setTargetBedTemperature(printer, temperature = temperature)
self._preheat_bed_timer.setInterval(duration * 1000) self._preheat_bed_timer.setInterval(duration * 1000)
self._preheat_bed_timer.start() self._preheat_bed_timer.start()
self._preheat_printer = printer self._preheat_printer = printer
printer.updateIsPreheating(True) printer.updateIsPreheating(True)
def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None: def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
self.setTargetBedTemperature(printer, temperature=0) self.setTargetBedTemperature(printer, temperature = 0)
self._preheat_bed_timer.stop() self._preheat_bed_timer.stop()
printer.updateIsPreheating(False) printer.updateIsPreheating(False)

View File

@ -1,34 +0,0 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
class MaterialOutputModel(QObject):
def __init__(self, guid, type, color, brand, name, parent = None):
super().__init__(parent)
self._guid = guid
self._type = type
self._color = color
self._brand = brand
self._name = name
@pyqtProperty(str, constant = True)
def guid(self):
return self._guid
@pyqtProperty(str, constant=True)
def type(self):
return self._type
@pyqtProperty(str, constant=True)
def brand(self):
return self._brand
@pyqtProperty(str, constant=True)
def color(self):
return self._color
@pyqtProperty(str, constant=True)
def name(self):
return self._name

View File

@ -4,7 +4,7 @@ from typing import Optional
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel from .MaterialOutputModel import MaterialOutputModel
class ExtruderConfigurationModel(QObject): class ExtruderConfigurationModel(QObject):
@ -67,4 +67,4 @@ class ExtruderConfigurationModel(QObject):
# Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is # Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is
# unique within a set # unique within a set
def __hash__(self): def __hash__(self):
return hash(self._position) ^ (hash(self._material.guid) if self._material is not None else hash(0)) ^ hash(self._hotend_id) return hash(self._position) ^ (hash(self._material.guid) if self._material is not None else hash(0)) ^ hash(self._hotend_id)

View File

@ -1,14 +1,15 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
from typing import Optional, TYPE_CHECKING from typing import Optional, TYPE_CHECKING
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
from .ExtruderConfigurationModel import ExtruderConfigurationModel
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from .MaterialOutputModel import MaterialOutputModel
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel from .PrinterOutputModel import PrinterOutputModel
class ExtruderOutputModel(QObject): class ExtruderOutputModel(QObject):

View File

@ -0,0 +1,36 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from PyQt5.QtCore import pyqtProperty, QObject
class MaterialOutputModel(QObject):
def __init__(self, guid: Optional[str], type: str, color: str, brand: str, name: str, parent = None) -> None:
super().__init__(parent)
self._guid = guid
self._type = type
self._color = color
self._brand = brand
self._name = name
@pyqtProperty(str, constant = True)
def guid(self) -> str:
return self._guid if self._guid else ""
@pyqtProperty(str, constant = True)
def type(self) -> str:
return self._type
@pyqtProperty(str, constant = True)
def brand(self) -> str:
return self._brand
@pyqtProperty(str, constant = True)
def color(self) -> str:
return self._color
@pyqtProperty(str, constant = True)
def name(self) -> str:
return self._name

View File

@ -1,16 +1,16 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
from typing import Optional, TYPE_CHECKING, List from typing import Optional, TYPE_CHECKING, List
from PyQt5.QtCore import QUrl from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot, QUrl
from PyQt5.QtGui import QImage from PyQt5.QtGui import QImage
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
class PrintJobOutputModel(QObject): class PrintJobOutputModel(QObject):
stateChanged = pyqtSignal() stateChanged = pyqtSignal()
@ -24,7 +24,7 @@ class PrintJobOutputModel(QObject):
previewImageChanged = pyqtSignal() previewImageChanged = pyqtSignal()
compatibleMachineFamiliesChanged = pyqtSignal() compatibleMachineFamiliesChanged = pyqtSignal()
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None: def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent = None) -> None:
super().__init__(parent) super().__init__(parent)
self._output_controller = output_controller self._output_controller = output_controller
self._state = "" self._state = ""
@ -35,7 +35,7 @@ class PrintJobOutputModel(QObject):
self._assigned_printer = None # type: Optional[PrinterOutputModel] self._assigned_printer = None # type: Optional[PrinterOutputModel]
self._owner = "" # Who started/owns the print job? self._owner = "" # Who started/owns the print job?
self._configuration = None # type: Optional[ConfigurationModel] self._configuration = None # type: Optional[PrinterConfigurationModel]
self._compatible_machine_families = [] # type: List[str] self._compatible_machine_families = [] # type: List[str]
self._preview_image_id = 0 self._preview_image_id = 0
@ -44,7 +44,7 @@ class PrintJobOutputModel(QObject):
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged) @pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
def compatibleMachineFamilies(self): def compatibleMachineFamilies(self):
# Hack; Some versions of cluster will return a family more than once... # Hack; Some versions of cluster will return a family more than once...
return set(self._compatible_machine_families) return list(set(self._compatible_machine_families))
def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None: def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None:
if self._compatible_machine_families != compatible_machine_families: if self._compatible_machine_families != compatible_machine_families:
@ -54,7 +54,7 @@ class PrintJobOutputModel(QObject):
@pyqtProperty(QUrl, notify=previewImageChanged) @pyqtProperty(QUrl, notify=previewImageChanged)
def previewImageUrl(self): def previewImageUrl(self):
self._preview_image_id += 1 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 # 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. # 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 temp = "image://print_job_preview/" + str(self._preview_image_id) + "/" + self._key
@ -69,10 +69,10 @@ class PrintJobOutputModel(QObject):
self.previewImageChanged.emit() self.previewImageChanged.emit()
@pyqtProperty(QObject, notify=configurationChanged) @pyqtProperty(QObject, notify=configurationChanged)
def configuration(self) -> Optional["ConfigurationModel"]: def configuration(self) -> Optional["PrinterConfigurationModel"]:
return self._configuration return self._configuration
def updateConfiguration(self, configuration: Optional["ConfigurationModel"]) -> None: def updateConfiguration(self, configuration: Optional["PrinterConfigurationModel"]) -> None:
if self._configuration != configuration: if self._configuration != configuration:
self._configuration = configuration self._configuration = configuration
self.configurationChanged.emit() self.configurationChanged.emit()
@ -118,17 +118,39 @@ class PrintJobOutputModel(QObject):
self.nameChanged.emit() self.nameChanged.emit()
@pyqtProperty(int, notify = timeTotalChanged) @pyqtProperty(int, notify = timeTotalChanged)
def timeTotal(self): def timeTotal(self) -> int:
return self._time_total return self._time_total
@pyqtProperty(int, notify = timeElapsedChanged) @pyqtProperty(int, notify = timeElapsedChanged)
def timeElapsed(self): def timeElapsed(self) -> int:
return self._time_elapsed return self._time_elapsed
@pyqtProperty(int, notify = timeElapsedChanged)
def timeRemaining(self) -> int:
# Never get a negative time remaining
return max(self.timeTotal - self.timeElapsed, 0)
@pyqtProperty(float, notify = timeElapsedChanged)
def progress(self) -> float:
result = float(self.timeElapsed) / max(self.timeTotal, 1.0) # Prevent a division by zero exception.
return min(result, 1.0) # Never get a progress past 1.0
@pyqtProperty(str, notify=stateChanged) @pyqtProperty(str, notify=stateChanged)
def state(self): def state(self) -> str:
return self._state return self._state
@pyqtProperty(bool, notify=stateChanged)
def isActive(self) -> bool:
inactive_states = [
"pausing",
"paused",
"resuming",
"wait_cleanup"
]
if self.state in inactive_states and self.timeRemaining > 0:
return False
return True
def updateTimeTotal(self, new_time_total): def updateTimeTotal(self, new_time_total):
if self._time_total != new_time_total: if self._time_total != new_time_total:
self._time_total = new_time_total self._time_total = new_time_total

View File

@ -6,10 +6,10 @@ from typing import List
MYPY = False MYPY = False
if MYPY: if MYPY:
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
class ConfigurationModel(QObject): class PrinterConfigurationModel(QObject):
configurationChanged = pyqtSignal() configurationChanged = pyqtSignal()
@ -19,14 +19,14 @@ class ConfigurationModel(QObject):
self._extruder_configurations = [] # type: List[ExtruderConfigurationModel] self._extruder_configurations = [] # type: List[ExtruderConfigurationModel]
self._buildplate_configuration = "" self._buildplate_configuration = ""
def setPrinterType(self, printer_type): def setPrinterType(self, printer_type: str) -> None:
self._printer_type = printer_type self._printer_type = printer_type
@pyqtProperty(str, fset = setPrinterType, notify = configurationChanged) @pyqtProperty(str, fset = setPrinterType, notify = configurationChanged)
def printerType(self) -> str: def printerType(self) -> str:
return self._printer_type return self._printer_type
def setExtruderConfigurations(self, extruder_configurations: List["ExtruderConfigurationModel"]): def setExtruderConfigurations(self, extruder_configurations: List["ExtruderConfigurationModel"]) -> None:
if self._extruder_configurations != extruder_configurations: if self._extruder_configurations != extruder_configurations:
self._extruder_configurations = extruder_configurations self._extruder_configurations = extruder_configurations
@ -40,7 +40,9 @@ class ConfigurationModel(QObject):
return self._extruder_configurations return self._extruder_configurations
def setBuildplateConfiguration(self, buildplate_configuration: str) -> None: def setBuildplateConfiguration(self, buildplate_configuration: str) -> None:
self._buildplate_configuration = buildplate_configuration if self._buildplate_configuration != buildplate_configuration:
self._buildplate_configuration = buildplate_configuration
self.configurationChanged.emit()
@pyqtProperty(str, fset = setBuildplateConfiguration, notify = configurationChanged) @pyqtProperty(str, fset = setBuildplateConfiguration, notify = configurationChanged)
def buildplateConfiguration(self) -> str: def buildplateConfiguration(self) -> str:
@ -54,7 +56,7 @@ class ConfigurationModel(QObject):
for configuration in self._extruder_configurations: for configuration in self._extruder_configurations:
if configuration is None: if configuration is None:
return False return False
return self._printer_type is not None return self._printer_type != ""
def __str__(self): def __str__(self):
message_chunks = [] message_chunks = []
@ -84,4 +86,4 @@ class ConfigurationModel(QObject):
if first_extruder: if first_extruder:
extruder_hash &= hash(first_extruder) extruder_hash &= hash(first_extruder)
return hash(self._printer_type) ^ extruder_hash ^ hash(self._buildplate_configuration) return hash(self._printer_type) ^ extruder_hash ^ hash(self._buildplate_configuration)

View File

@ -1,17 +1,16 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot, QUrl
from typing import List, Dict, Optional from typing import List, Dict, Optional
from UM.Math.Vector import Vector from UM.Math.Vector import Vector
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel
MYPY = False MYPY = False
if MYPY: if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from cura.PrinterOutput.NetworkCamera import NetworkCamera
class PrinterOutputModel(QObject): class PrinterOutputModel(QObject):
@ -23,43 +22,47 @@ class PrinterOutputModel(QObject):
nameChanged = pyqtSignal() nameChanged = pyqtSignal()
headPositionChanged = pyqtSignal() headPositionChanged = pyqtSignal()
keyChanged = pyqtSignal() keyChanged = pyqtSignal()
printerTypeChanged = pyqtSignal() typeChanged = pyqtSignal()
buildplateChanged = pyqtSignal() buildplateChanged = pyqtSignal()
cameraChanged = pyqtSignal() cameraUrlChanged = pyqtSignal()
configurationChanged = pyqtSignal() configurationChanged = pyqtSignal()
canUpdateFirmwareChanged = pyqtSignal() canUpdateFirmwareChanged = pyqtSignal()
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None: def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None:
super().__init__(parent) super().__init__(parent)
self._bed_temperature = -1 # Use -1 for no heated bed. self._bed_temperature = -1 # type: float # Use -1 for no heated bed.
self._target_bed_temperature = 0 self._target_bed_temperature = 0 # type: float
self._name = "" self._name = ""
self._key = "" # Unique identifier self._key = "" # Unique identifier
self._controller = output_controller self._controller = output_controller
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged) self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)] self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer self._printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer
self._head_position = Vector(0, 0, 0) self._head_position = Vector(0, 0, 0)
self._active_print_job = None # type: Optional[PrintJobOutputModel] self._active_print_job = None # type: Optional[PrintJobOutputModel]
self._firmware_version = firmware_version self._firmware_version = firmware_version
self._printer_state = "unknown" self._printer_state = "unknown"
self._is_preheating = False self._is_preheating = False
self._printer_type = "" self._printer_type = ""
self._buildplate_name = "" self._buildplate = ""
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
self._extruders] self._extruders]
self._camera = None # type: Optional[NetworkCamera] self._camera_url = QUrl() # type: QUrl
@pyqtProperty(str, constant = True) @pyqtProperty(str, constant = True)
def firmwareVersion(self) -> str: def firmwareVersion(self) -> str:
return self._firmware_version return self._firmware_version
def setCamera(self, camera: Optional["NetworkCamera"]) -> None: def setCameraUrl(self, camera_url: "QUrl") -> None:
if self._camera is not camera: if self._camera_url != camera_url:
self._camera = camera self._camera_url = camera_url
self.cameraChanged.emit() self.cameraUrlChanged.emit()
@pyqtProperty(QUrl, fset = setCameraUrl, notify = cameraUrlChanged)
def cameraUrl(self) -> "QUrl":
return self._camera_url
def updateIsPreheating(self, pre_heating: bool) -> None: def updateIsPreheating(self, pre_heating: bool) -> None:
if self._is_preheating != pre_heating: if self._is_preheating != pre_heating:
@ -70,11 +73,7 @@ class PrinterOutputModel(QObject):
def isPreheating(self) -> bool: def isPreheating(self) -> bool:
return self._is_preheating return self._is_preheating
@pyqtProperty(QObject, notify=cameraChanged) @pyqtProperty(str, notify = typeChanged)
def camera(self) -> Optional["NetworkCamera"]:
return self._camera
@pyqtProperty(str, notify = printerTypeChanged)
def type(self) -> str: def type(self) -> str:
return self._printer_type return self._printer_type
@ -82,17 +81,17 @@ class PrinterOutputModel(QObject):
if self._printer_type != printer_type: if self._printer_type != printer_type:
self._printer_type = printer_type self._printer_type = printer_type
self._printer_configuration.printerType = self._printer_type self._printer_configuration.printerType = self._printer_type
self.printerTypeChanged.emit() self.typeChanged.emit()
self.configurationChanged.emit() self.configurationChanged.emit()
@pyqtProperty(str, notify = buildplateChanged) @pyqtProperty(str, notify = buildplateChanged)
def buildplate(self) -> str: def buildplate(self) -> str:
return self._buildplate_name return self._buildplate
def updateBuildplateName(self, buildplate_name: str) -> None: def updateBuildplate(self, buildplate: str) -> None:
if self._buildplate_name != buildplate_name: if self._buildplate != buildplate:
self._buildplate_name = buildplate_name self._buildplate = buildplate
self._printer_configuration.buildplateConfiguration = self._buildplate_name self._printer_configuration.buildplateConfiguration = self._buildplate
self.buildplateChanged.emit() self.buildplateChanged.emit()
self.configurationChanged.emit() self.configurationChanged.emit()
@ -180,7 +179,6 @@ class PrinterOutputModel(QObject):
return self._name return self._name
def setName(self, name: str) -> None: def setName(self, name: str) -> None:
self._setName(name)
self.updateName(name) self.updateName(name)
def updateName(self, name: str) -> None: def updateName(self, name: str) -> None:
@ -189,19 +187,19 @@ class PrinterOutputModel(QObject):
self.nameChanged.emit() self.nameChanged.emit()
## Update the bed temperature. This only changes it locally. ## Update the bed temperature. This only changes it locally.
def updateBedTemperature(self, temperature: int) -> None: def updateBedTemperature(self, temperature: float) -> None:
if self._bed_temperature != temperature: if self._bed_temperature != temperature:
self._bed_temperature = temperature self._bed_temperature = temperature
self.bedTemperatureChanged.emit() self.bedTemperatureChanged.emit()
def updateTargetBedTemperature(self, temperature: int) -> None: def updateTargetBedTemperature(self, temperature: float) -> None:
if self._target_bed_temperature != temperature: if self._target_bed_temperature != temperature:
self._target_bed_temperature = temperature self._target_bed_temperature = temperature
self.targetBedTemperatureChanged.emit() self.targetBedTemperatureChanged.emit()
## Set the target bed temperature. This ensures that it's actually sent to the remote. ## Set the target bed temperature. This ensures that it's actually sent to the remote.
@pyqtSlot(int) @pyqtSlot(float)
def setTargetBedTemperature(self, temperature: int) -> None: def setTargetBedTemperature(self, temperature: float) -> None:
self._controller.setTargetBedTemperature(self, temperature) self._controller.setTargetBedTemperature(self, temperature)
self.updateTargetBedTemperature(temperature) self.updateTargetBedTemperature(temperature)
@ -226,55 +224,55 @@ class PrinterOutputModel(QObject):
def activePrintJob(self) -> Optional["PrintJobOutputModel"]: def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
return self._active_print_job return self._active_print_job
@pyqtProperty(str, notify=stateChanged) @pyqtProperty(str, notify = stateChanged)
def state(self) -> str: def state(self) -> str:
return self._printer_state return self._printer_state
@pyqtProperty(int, notify=bedTemperatureChanged) @pyqtProperty(float, notify = bedTemperatureChanged)
def bedTemperature(self) -> int: def bedTemperature(self) -> float:
return self._bed_temperature return self._bed_temperature
@pyqtProperty(int, notify=targetBedTemperatureChanged) @pyqtProperty(float, notify = targetBedTemperatureChanged)
def targetBedTemperature(self) -> int: def targetBedTemperature(self) -> float:
return self._target_bed_temperature return self._target_bed_temperature
# Does the printer support pre-heating the bed at all # Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant = True)
def canPreHeatBed(self) -> bool: def canPreHeatBed(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_pre_heat_bed return self._controller.can_pre_heat_bed
return False return False
# Does the printer support pre-heating the bed at all # Does the printer support pre-heating the bed at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant = True)
def canPreHeatHotends(self) -> bool: def canPreHeatHotends(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_pre_heat_hotends return self._controller.can_pre_heat_hotends
return False return False
# Does the printer support sending raw G-code at all # Does the printer support sending raw G-code at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant = True)
def canSendRawGcode(self) -> bool: def canSendRawGcode(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_send_raw_gcode return self._controller.can_send_raw_gcode
return False return False
# Does the printer support pause at all # Does the printer support pause at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant = True)
def canPause(self) -> bool: def canPause(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_pause return self._controller.can_pause
return False return False
# Does the printer support abort at all # Does the printer support abort at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant = True)
def canAbort(self) -> bool: def canAbort(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_abort return self._controller.can_abort
return False return False
# Does the printer support manual control at all # Does the printer support manual control at all
@pyqtProperty(bool, constant=True) @pyqtProperty(bool, constant = True)
def canControlManually(self) -> bool: def canControlManually(self) -> bool:
if self._controller: if self._controller:
return self._controller.can_control_manually return self._controller.can_control_manually
@ -293,7 +291,7 @@ class PrinterOutputModel(QObject):
# Returns the configuration (material, variant and buildplate) of the current printer # Returns the configuration (material, variant and buildplate) of the current printer
@pyqtProperty(QObject, notify = configurationChanged) @pyqtProperty(QObject, notify = configurationChanged)
def printerConfiguration(self) -> Optional[ConfigurationModel]: def printerConfiguration(self) -> Optional[PrinterConfigurationModel]:
if self._printer_configuration.isValid(): if self._printer_configuration.isValid():
return self._printer_configuration return self._printer_configuration
return None return None

View File

View File

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

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

View File

@ -4,19 +4,23 @@
from UM.FileHandler.FileHandler import FileHandler #For typing. from UM.FileHandler.FileHandler import FileHandler #For typing.
from UM.Logger import Logger from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode #For typing. from UM.Scene.SceneNode import SceneNode #For typing.
from cura.API import Account
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
from time import time from time import time
from typing import Any, Callable, Dict, List, Optional from typing import Callable, Dict, List, Optional, Union
from enum import IntEnum from enum import IntEnum
import os # To get the username import os # To get the username
import gzip import gzip
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
class AuthState(IntEnum): class AuthState(IntEnum):
NotAuthenticated = 1 NotAuthenticated = 1
AuthenticationRequested = 2 AuthenticationRequested = 2
@ -28,8 +32,8 @@ class AuthState(IntEnum):
class NetworkedPrinterOutputDevice(PrinterOutputDevice): class NetworkedPrinterOutputDevice(PrinterOutputDevice):
authenticationStateChanged = pyqtSignal() authenticationStateChanged = pyqtSignal()
def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], parent: QObject = None) -> None: def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType = ConnectionType.NetworkConnection, parent: QObject = None) -> None:
super().__init__(device_id = device_id, parent = parent) super().__init__(device_id = device_id, connection_type = connection_type, parent = parent)
self._manager = None # type: Optional[QNetworkAccessManager] self._manager = None # type: Optional[QNetworkAccessManager]
self._last_manager_create_time = None # type: Optional[float] self._last_manager_create_time = None # type: Optional[float]
self._recreate_network_manager_time = 30 self._recreate_network_manager_time = 30
@ -41,7 +45,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._api_prefix = "" self._api_prefix = ""
self._address = address self._address = address
self._properties = properties self._properties = properties
self._user_agent = "%s/%s " % (CuraApplication.getInstance().getApplicationName(), CuraApplication.getInstance().getVersion()) self._user_agent = "%s/%s " % (CuraApplication.getInstance().getApplicationName(),
CuraApplication.getInstance().getVersion())
self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]] self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]]
self._authentication_state = AuthState.NotAuthenticated self._authentication_state = AuthState.NotAuthenticated
@ -55,7 +60,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._gcode = [] # type: List[str] self._gcode = [] # type: List[str]
self._connection_state_before_timeout = None # type: Optional[ConnectionState] self._connection_state_before_timeout = None # type: Optional[ConnectionState]
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None: def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
raise NotImplementedError("requestWrite needs to be implemented") raise NotImplementedError("requestWrite needs to be implemented")
def setAuthenticationState(self, authentication_state: AuthState) -> None: def setAuthenticationState(self, authentication_state: AuthState) -> None:
@ -125,7 +131,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
if self._connection_state_before_timeout is None: if self._connection_state_before_timeout is None:
self._connection_state_before_timeout = self._connection_state self._connection_state_before_timeout = self._connection_state
self.setConnectionState(ConnectionState.closed) self.setConnectionState(ConnectionState.Closed)
# We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to # We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to
# sleep. # sleep.
@ -133,7 +139,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
if self._last_manager_create_time is None or 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() self._createNetworkManager()
assert(self._manager is not None) assert(self._manager is not None)
elif self._connection_state == ConnectionState.closed: elif self._connection_state == ConnectionState.Closed:
# Go out of timeout. # Go out of timeout.
if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here
self.setConnectionState(self._connection_state_before_timeout) self.setConnectionState(self._connection_state_before_timeout)
@ -143,10 +149,15 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
url = QUrl("http://" + self._address + self._api_prefix + target) url = QUrl("http://" + self._address + self._api_prefix + target)
request = QNetworkRequest(url) request = QNetworkRequest(url)
if content_type is not None: if content_type is not None:
request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json") request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent) request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
return request return request
## This method was only available privately before, but it was actually called from SendMaterialJob.py.
# We now have a public equivalent as well. We did not remove the private one as plugins might be using that.
def createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
return self._createFormPart(content_header, data, content_type)
def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart: def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
part = QHttpPart() part = QHttpPart()
@ -160,9 +171,15 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
part.setBody(data) part.setBody(data)
return part return part
## Convenience function to get the username from the OS. ## Convenience function to get the username, either from the cloud or from the OS.
# The code was copied from the getpass module, as we try to use as little dependencies as possible.
def _getUserName(self) -> str: def _getUserName(self) -> str:
# check first if we are logged in with the Ultimaker Account
account = CuraApplication.getInstance().getCuraAPI().account # type: Account
if account and account.isLoggedIn:
return account.userName
# Otherwise get the username from the US
# The code below was copied from the getpass module, as we try to use as little dependencies as possible.
for name in ("LOGNAME", "USER", "LNAME", "USERNAME"): for name in ("LOGNAME", "USER", "LNAME", "USERNAME"):
user = os.environ.get(name) user = os.environ.get(name)
if user: if user:
@ -178,49 +195,89 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._createNetworkManager() self._createNetworkManager()
assert (self._manager is not None) assert (self._manager is not None)
def put(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: ## Sends a put request to the given path.
# \param url: The path after the API prefix.
# \param data: The data to be sent in the body
# \param content_type: The content type of the body data.
# \param on_finished: The function to call when the response is received.
# \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
def put(self, url: str, data: Union[str, bytes], content_type: Optional[str] = "application/json",
on_finished: Optional[Callable[[QNetworkReply], None]] = None,
on_progress: Optional[Callable[[int, int], None]] = None) -> None:
self._validateManager() self._validateManager()
request = self._createEmptyRequest(target)
self._last_request_time = time()
if self._manager is not None:
reply = self._manager.put(request, data.encode())
self._registerOnFinishedCallback(reply, on_finished)
else:
Logger.log("e", "Could not find manager.")
def delete(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: request = self._createEmptyRequest(url, content_type = content_type)
self._last_request_time = time()
if not self._manager:
Logger.log("e", "No network manager was created to execute the PUT call with.")
return
body = data if isinstance(data, bytes) else data.encode() # type: bytes
reply = self._manager.put(request, body)
self._registerOnFinishedCallback(reply, on_finished)
if on_progress is not None:
reply.uploadProgress.connect(on_progress)
## Sends a delete request to the given path.
# \param url: The path after the API prefix.
# \param on_finished: The function to be call when the response is received.
def delete(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
self._validateManager() self._validateManager()
request = self._createEmptyRequest(target)
self._last_request_time = time()
if self._manager is not None:
reply = self._manager.deleteResource(request)
self._registerOnFinishedCallback(reply, on_finished)
else:
Logger.log("e", "Could not find manager.")
def get(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: request = self._createEmptyRequest(url)
self._last_request_time = time()
if not self._manager:
Logger.log("e", "No network manager was created to execute the DELETE call with.")
return
reply = self._manager.deleteResource(request)
self._registerOnFinishedCallback(reply, on_finished)
## Sends a get request to the given path.
# \param url: The path after the API prefix.
# \param on_finished: The function to be call when the response is received.
def get(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
self._validateManager() self._validateManager()
request = self._createEmptyRequest(target)
self._last_request_time = time()
if self._manager is not None:
reply = self._manager.get(request)
self._registerOnFinishedCallback(reply, on_finished)
else:
Logger.log("e", "Could not find manager.")
def post(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None: request = self._createEmptyRequest(url)
self._last_request_time = time()
if not self._manager:
Logger.log("e", "No network manager was created to execute the GET call with.")
return
reply = self._manager.get(request)
self._registerOnFinishedCallback(reply, on_finished)
## Sends a post request to the given path.
# \param url: The path after the API prefix.
# \param data: The data to be sent in the body
# \param on_finished: The function to call when the response is received.
# \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
def post(self, url: str, data: Union[str, bytes],
on_finished: Optional[Callable[[QNetworkReply], None]],
on_progress: Optional[Callable[[int, int], None]] = None) -> None:
self._validateManager() self._validateManager()
request = self._createEmptyRequest(target)
self._last_request_time = time()
if self._manager is not None:
reply = self._manager.post(request, data)
if on_progress is not None:
reply.uploadProgress.connect(on_progress)
self._registerOnFinishedCallback(reply, on_finished)
else:
Logger.log("e", "Could not find manager.")
def postFormWithParts(self, target: str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> QNetworkReply: request = self._createEmptyRequest(url)
self._last_request_time = time()
if not self._manager:
Logger.log("e", "Could not find manager.")
return
body = data if isinstance(data, bytes) else data.encode() # type: bytes
reply = self._manager.post(request, body)
if on_progress is not None:
reply.uploadProgress.connect(on_progress)
self._registerOnFinishedCallback(reply, on_finished)
def postFormWithParts(self, target: str, parts: List[QHttpPart],
on_finished: Optional[Callable[[QNetworkReply], None]],
on_progress: Optional[Callable[[int, int], None]] = None) -> QNetworkReply:
self._validateManager() self._validateManager()
request = self._createEmptyRequest(target, content_type=None) request = self._createEmptyRequest(target, content_type=None)
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType) multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
@ -255,22 +312,37 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
def _createNetworkManager(self) -> None: def _createNetworkManager(self) -> None:
Logger.log("d", "Creating network manager") Logger.log("d", "Creating network manager")
if self._manager: if self._manager:
self._manager.finished.disconnect(self.__handleOnFinished) self._manager.finished.disconnect(self._handleOnFinished)
self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired) self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired)
self._manager = QNetworkAccessManager() self._manager = QNetworkAccessManager()
self._manager.finished.connect(self.__handleOnFinished) self._manager.finished.connect(self._handleOnFinished)
self._last_manager_create_time = time() self._last_manager_create_time = time()
self._manager.authenticationRequired.connect(self._onAuthenticationRequired) self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
if self._properties.get(b"temporary", b"false") != b"true": if self._properties.get(b"temporary", b"false") != b"true":
CuraApplication.getInstance().getMachineManager().checkCorrectGroupName(self.getId(), self.name) self._checkCorrectGroupName(self.getId(), self.name)
def _registerOnFinishedCallback(self, reply: QNetworkReply, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None: def _registerOnFinishedCallback(self, reply: QNetworkReply, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
if on_finished is not None: if on_finished is not None:
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished
def __handleOnFinished(self, reply: QNetworkReply) -> None: ## This method checks if the name of the group stored in the definition container is correct.
# After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group
# then all the container stacks are updated, both the current and the hidden ones.
def _checkCorrectGroupName(self, device_id: str, group_name: str) -> None:
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
active_machine_network_name = CuraApplication.getInstance().getMachineManager().activeMachineNetworkKey()
if global_container_stack and device_id == active_machine_network_name:
# Check if the group_name is correct. If not, update all the containers connected to the same printer
if CuraApplication.getInstance().getMachineManager().activeMachineNetworkGroupName != group_name:
metadata_filter = {"um_network_key": active_machine_network_name}
containers = CuraContainerRegistry.getInstance().findContainerStacks(type="machine",
**metadata_filter)
for container in containers:
container.setMetaDataEntry("group_name", group_name)
def _handleOnFinished(self, reply: QNetworkReply) -> None:
# Due to garbage collection, we need to cache certain bits of post operations. # Due to garbage collection, we need to cache certain bits of post operations.
# As we don't want to keep them around forever, delete them if we get a reply. # As we don't want to keep them around forever, delete them if we get a reply.
if reply.operation() == QNetworkAccessManager.PostOperation: if reply.operation() == QNetworkAccessManager.PostOperation:
@ -282,8 +354,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._last_response_time = time() self._last_response_time = time()
if self._connection_state == ConnectionState.connecting: if self._connection_state == ConnectionState.Connecting:
self.setConnectionState(ConnectionState.connected) self.setConnectionState(ConnectionState.Connected)
callback_key = reply.url().toString() + str(reply.operation()) callback_key = reply.url().toString() + str(reply.operation())
try: try:

View File

@ -1,17 +1,15 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger from UM.Logger import Logger
from UM.Signal import Signal from UM.Signal import Signal
from typing import Union
MYPY = False MYPY = False
if MYPY: if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from .Models.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel from .Models.ExtruderOutputModel import ExtruderOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from .Models.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice from .PrinterOutputDevice import PrinterOutputDevice
class PrinterOutputController: class PrinterOutputController:
@ -25,10 +23,10 @@ class PrinterOutputController:
self.can_update_firmware = False self.can_update_firmware = False
self._output_device = output_device self._output_device = output_device
def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: Union[int, float]) -> None: def setTargetHotendTemperature(self, printer: "PrinterOutputModel", position: int, temperature: float) -> None:
Logger.log("w", "Set target hotend temperature not implemented in controller") Logger.log("w", "Set target hotend temperature not implemented in controller")
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int) -> None: def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: float) -> None:
Logger.log("w", "Set target bed temperature not implemented in controller") Logger.log("w", "Set target bed temperature not implemented in controller")
def setJobState(self, job: "PrintJobOutputModel", state: str) -> None: def setJobState(self, job: "PrintJobOutputModel", state: str) -> None:

View File

@ -1,9 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from enum import IntEnum
from typing import Callable, List, Optional, Union
from UM.Decorators import deprecated
from UM.i18n import i18nCatalog
from UM.OutputDevice.OutputDevice import OutputDevice
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
@ -11,28 +10,35 @@ from UM.Logger import Logger
from UM.Signal import signalemitter from UM.Signal import signalemitter
from UM.Qt.QtApplication import QtApplication from UM.Qt.QtApplication import QtApplication
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM.Decorators import deprecated
from enum import IntEnum # For the connection state tracking. from UM.i18n import i18nCatalog
from typing import Callable, List, Optional, Union from UM.OutputDevice.OutputDevice import OutputDevice
MYPY = False MYPY = False
if MYPY: 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.FileHandler.FileHandler import FileHandler
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from .Models.PrinterOutputModel import PrinterOutputModel
from .Models.PrinterConfigurationModel import PrinterConfigurationModel
from .FirmwareUpdater import FirmwareUpdater
i18n_catalog = i18nCatalog("cura") i18n_catalog = i18nCatalog("cura")
## The current processing state of the backend. ## The current processing state of the backend.
class ConnectionState(IntEnum): class ConnectionState(IntEnum):
closed = 0 Closed = 0
connecting = 1 Connecting = 1
connected = 2 Connected = 2
busy = 3 Busy = 3
error = 4 Error = 4
class ConnectionType(IntEnum):
NotConnected = 0
UsbConnection = 1
NetworkConnection = 2
CloudConnection = 3
## Printer output device adds extra interface options on top of output device. ## Printer output device adds extra interface options on top of output device.
@ -46,6 +52,7 @@ class ConnectionState(IntEnum):
# For all other uses it should be used in the same way as a "regular" OutputDevice. # For all other uses it should be used in the same way as a "regular" OutputDevice.
@signalemitter @signalemitter
class PrinterOutputDevice(QObject, OutputDevice): class PrinterOutputDevice(QObject, OutputDevice):
printersChanged = pyqtSignal() printersChanged = pyqtSignal()
connectionStateChanged = pyqtSignal(str) connectionStateChanged = pyqtSignal(str)
acceptsCommandsChanged = pyqtSignal() acceptsCommandsChanged = pyqtSignal()
@ -62,33 +69,34 @@ class PrinterOutputDevice(QObject, OutputDevice):
# Signal to indicate that the configuration of one of the printers has changed. # Signal to indicate that the configuration of one of the printers has changed.
uniqueConfigurationsChanged = pyqtSignal() uniqueConfigurationsChanged = pyqtSignal()
def __init__(self, device_id: str, parent: QObject = None) -> None: def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, parent: QObject = None) -> None:
super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance
self._printers = [] # type: List[PrinterOutputModel] self._printers = [] # type: List[PrinterOutputModel]
self._unique_configurations = [] # type: List[ConfigurationModel] self._unique_configurations = [] # type: List[PrinterConfigurationModel]
self._monitor_view_qml_path = "" #type: str self._monitor_view_qml_path = "" # type: str
self._monitor_component = None #type: Optional[QObject] self._monitor_component = None # type: Optional[QObject]
self._monitor_item = None #type: Optional[QObject] self._monitor_item = None # type: Optional[QObject]
self._control_view_qml_path = "" #type: str self._control_view_qml_path = "" # type: str
self._control_component = None #type: Optional[QObject] self._control_component = None # type: Optional[QObject]
self._control_item = None #type: Optional[QObject] self._control_item = None # type: Optional[QObject]
self._accepts_commands = False #type: bool self._accepts_commands = False # type: bool
self._update_timer = QTimer() #type: QTimer self._update_timer = QTimer() # type: QTimer
self._update_timer.setInterval(2000) # TODO; Add preference for update interval self._update_timer.setInterval(2000) # TODO; Add preference for update interval
self._update_timer.setSingleShot(False) self._update_timer.setSingleShot(False)
self._update_timer.timeout.connect(self._update) self._update_timer.timeout.connect(self._update)
self._connection_state = ConnectionState.closed #type: ConnectionState self._connection_state = ConnectionState.Closed # type: ConnectionState
self._connection_type = connection_type # type: ConnectionType
self._firmware_updater = None #type: Optional[FirmwareUpdater] self._firmware_updater = None # type: Optional[FirmwareUpdater]
self._firmware_name = None #type: Optional[str] self._firmware_name = None # type: Optional[str]
self._address = "" #type: str self._address = "" # type: str
self._connection_text = "" #type: str self._connection_text = "" # type: str
self.printersChanged.connect(self._onPrintersChanged) self.printersChanged.connect(self._onPrintersChanged)
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations) QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
@ -110,15 +118,19 @@ class PrinterOutputDevice(QObject, OutputDevice):
callback(QMessageBox.Yes) callback(QMessageBox.Yes)
def isConnected(self) -> bool: def isConnected(self) -> bool:
return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error return self._connection_state != ConnectionState.Closed and self._connection_state != ConnectionState.Error
def setConnectionState(self, connection_state: ConnectionState) -> None: def setConnectionState(self, connection_state: "ConnectionState") -> None:
if self._connection_state != connection_state: if self._connection_state != connection_state:
self._connection_state = connection_state self._connection_state = connection_state
self.connectionStateChanged.emit(self._id) self.connectionStateChanged.emit(self._id)
@pyqtProperty(str, notify = connectionStateChanged) @pyqtProperty(int, constant = True)
def connectionState(self) -> ConnectionState: def connectionType(self) -> "ConnectionType":
return self._connection_type
@pyqtProperty(int, notify = connectionStateChanged)
def connectionState(self) -> "ConnectionState":
return self._connection_state return self._connection_state
def _update(self) -> None: def _update(self) -> None:
@ -131,7 +143,8 @@ class PrinterOutputDevice(QObject, OutputDevice):
return None return None
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None: def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None:
raise NotImplementedError("requestWrite needs to be implemented") raise NotImplementedError("requestWrite needs to be implemented")
@pyqtProperty(QObject, notify = printersChanged) @pyqtProperty(QObject, notify = printersChanged)
@ -174,13 +187,13 @@ class PrinterOutputDevice(QObject, OutputDevice):
## Attempt to establish connection ## Attempt to establish connection
def connect(self) -> None: def connect(self) -> None:
self.setConnectionState(ConnectionState.connecting) self.setConnectionState(ConnectionState.Connecting)
self._update_timer.start() self._update_timer.start()
## Attempt to close the connection ## Attempt to close the connection
def close(self) -> None: def close(self) -> None:
self._update_timer.stop() self._update_timer.stop()
self.setConnectionState(ConnectionState.closed) self.setConnectionState(ConnectionState.Closed)
## Ensure that close gets called when object is destroyed ## Ensure that close gets called when object is destroyed
def __del__(self) -> None: def __del__(self) -> None:
@ -203,14 +216,21 @@ class PrinterOutputDevice(QObject, OutputDevice):
# Returns the unique configurations of the printers within this output device # Returns the unique configurations of the printers within this output device
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged) @pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
def uniqueConfigurations(self) -> List["ConfigurationModel"]: def uniqueConfigurations(self) -> List["PrinterConfigurationModel"]:
return self._unique_configurations return self._unique_configurations
def _updateUniqueConfigurations(self) -> None: def _updateUniqueConfigurations(self) -> None:
self._unique_configurations = list(set([printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None])) self._unique_configurations = sorted(
self._unique_configurations.sort(key = lambda k: k.printerType) {printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None},
key=lambda config: config.printerType,
)
self.uniqueConfigurationsChanged.emit() self.uniqueConfigurationsChanged.emit()
# Returns the unique configurations of the printers within this output device
@pyqtProperty("QStringList", notify = uniqueConfigurationsChanged)
def uniquePrinterTypes(self) -> List[str]:
return list(sorted(set([configuration.printerType for configuration in self._unique_configurations])))
def _onPrintersChanged(self) -> None: def _onPrintersChanged(self) -> None:
for printer in self._printers: for printer in self._printers:
printer.configurationChanged.connect(self._updateUniqueConfigurations) printer.configurationChanged.connect(self._updateUniqueConfigurations)
@ -238,4 +258,4 @@ class PrinterOutputDevice(QObject, OutputDevice):
if not self._firmware_updater: if not self._firmware_updater:
return return
self._firmware_updater.updateFirmware(firmware_file) self._firmware_updater.updateFirmware(firmware_file)

View File

@ -60,13 +60,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
previous_node = self._node previous_node = self._node
# Disconnect from previous node signals # Disconnect from previous node signals
if previous_node is not None and node is not previous_node: if previous_node is not None and node is not previous_node:
previous_node.transformationChanged.disconnect(self._onChanged) previous_node.boundingBoxChanged.disconnect(self._onChanged)
previous_node.parentChanged.disconnect(self._onChanged)
super().setNode(node) super().setNode(node)
# Mypy doesn't understand that self._node is no longer optional, so just use the node.
node.transformationChanged.connect(self._onChanged) node.boundingBoxChanged.connect(self._onChanged)
node.parentChanged.connect(self._onChanged)
self._onChanged() self._onChanged()
@ -142,6 +140,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
controller = Application.getInstance().getController() controller = Application.getInstance().getController()
root = controller.getScene().getRoot() root = controller.getScene().getRoot()
if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node): 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: if self._convex_hull_node:
self._convex_hull_node.setParent(None) self._convex_hull_node.setParent(None)
self._convex_hull_node = None self._convex_hull_node = None
@ -181,7 +185,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
for child in self._node.getChildren(): for child in self._node.getChildren():
child_hull = child.callDecoration("_compute2DConvexHull") child_hull = child.callDecoration("_compute2DConvexHull")
if child_hull: if child_hull:
points = numpy.append(points, child_hull.getPoints(), axis = 0) try:
points = numpy.append(points, child_hull.getPoints(), axis = 0)
except ValueError:
pass
if points.size < 3: if points.size < 3:
return None return None
@ -233,7 +240,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array # See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view( vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1]))) numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
_, idx = numpy.unique(vertex_byte_view, return_index=True) _, idx = numpy.unique(vertex_byte_view, return_index = True)
vertex_data = vertex_data[idx] # Select the unique rows by index. vertex_data = vertex_data[idx] # Select the unique rows by index.
hull = Polygon(vertex_data) hull = Polygon(vertex_data)
@ -266,7 +273,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored) head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
# Min head hull is used for the push free # Min head hull is used for the push free
convex_hull = self._compute2DConvexHeadFull() convex_hull = self._compute2DConvexHull()
if convex_hull: if convex_hull:
return convex_hull.getMinkowskiHull(head_and_fans) return convex_hull.getMinkowskiHull(head_and_fans)
return None return None
@ -280,16 +287,21 @@ class ConvexHullDecorator(SceneNodeDecorator):
# Add extra margin depending on adhesion type # Add extra margin depending on adhesion type
adhesion_type = self._global_stack.getProperty("adhesion_type", "value") adhesion_type = self._global_stack.getProperty("adhesion_type", "value")
max_length_available = 0.5 * min(
self._getSettingProperty("machine_width", "value"),
self._getSettingProperty("machine_depth", "value")
)
if adhesion_type == "raft": if adhesion_type == "raft":
extra_margin = max(0, self._getSettingProperty("raft_margin", "value")) extra_margin = min(max_length_available, max(0, self._getSettingProperty("raft_margin", "value")))
elif adhesion_type == "brim": elif adhesion_type == "brim":
extra_margin = max(0, self._getSettingProperty("brim_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value")) extra_margin = min(max_length_available, max(0, self._getSettingProperty("brim_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value")))
elif adhesion_type == "none": elif adhesion_type == "none":
extra_margin = 0 extra_margin = 0
elif adhesion_type == "skirt": elif adhesion_type == "skirt":
extra_margin = max( extra_margin = min(max_length_available, max(
0, self._getSettingProperty("skirt_gap", "value") + 0, self._getSettingProperty("skirt_gap", "value") +
self._getSettingProperty("skirt_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value")) self._getSettingProperty("skirt_line_count", "value") * self._getSettingProperty("skirt_brim_line_width", "value")))
else: else:
raise Exception("Unknown bed adhesion type. Did you forget to update the convex hull calculations for your new bed adhesion type?") raise Exception("Unknown bed adhesion type. Did you forget to update the convex hull calculations for your new bed adhesion type?")

View File

@ -1,7 +1,10 @@
# Copyright (c) 2015 Ultimaker B.V. # Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Optional
from UM.Application import Application from UM.Application import Application
from UM.Math.Polygon import Polygon
from UM.Qt.QtApplication import QtApplication
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Resources import Resources from UM.Resources import Resources
from UM.Math.Color import Color from UM.Math.Color import Color
@ -16,7 +19,7 @@ class ConvexHullNode(SceneNode):
# location an object uses on the buildplate. This area (or area's in case of one at a time printing) is # location an object uses on the buildplate. This area (or area's in case of one at a time printing) is
# then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded # then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
# to represent the raft as well. # to represent the raft as well.
def __init__(self, node, hull, thickness, parent = None): def __init__(self, node: SceneNode, hull: Optional[Polygon], thickness: float, parent: Optional[SceneNode] = None) -> None:
super().__init__(parent) super().__init__(parent)
self.setCalculateBoundingBox(False) self.setCalculateBoundingBox(False)
@ -25,7 +28,11 @@ class ConvexHullNode(SceneNode):
# Color of the drawn convex hull # Color of the drawn convex hull
if not Application.getInstance().getIsHeadLess(): if not Application.getInstance().getIsHeadLess():
self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb()) theme = QtApplication.getInstance().getTheme()
if theme:
self._color = Color(*theme.getColor("convex_hull").getRgb())
else:
self._color = Color(0, 0, 0)
else: else:
self._color = Color(0, 0, 0) self._color = Color(0, 0, 0)
@ -47,7 +54,7 @@ class ConvexHullNode(SceneNode):
if hull_mesh_builder.addConvexPolygonExtrusion( if hull_mesh_builder.addConvexPolygonExtrusion(
self._hull.getPoints()[::-1], # bottom layer is reversed self._hull.getPoints()[::-1], # bottom layer is reversed
self._mesh_height-thickness, self._mesh_height, color=self._color): self._mesh_height - thickness, self._mesh_height, color = self._color):
hull_mesh = hull_mesh_builder.build() hull_mesh = hull_mesh_builder.build()
self.setMeshData(hull_mesh) self.setMeshData(hull_mesh)
@ -75,7 +82,7 @@ class ConvexHullNode(SceneNode):
return True return True
def _onNodeDecoratorsChanged(self, node): def _onNodeDecoratorsChanged(self, node: SceneNode) -> None:
convex_hull_head = self._node.callDecoration("getConvexHullHead") convex_hull_head = self._node.callDecoration("getConvexHullHead")
if convex_hull_head: if convex_hull_head:
convex_hull_head_builder = MeshBuilder() convex_hull_head_builder = MeshBuilder()

View File

@ -3,7 +3,8 @@ from UM.Logger import Logger
from PyQt5.QtCore import Qt, pyqtSlot, QObject from PyQt5.QtCore import Qt, pyqtSlot, QObject
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from cura.ObjectsModel import ObjectsModel from UM.Scene.Camera import Camera
from cura.UI.ObjectsModel import ObjectsModel
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
from UM.Application import Application from UM.Application import Application
@ -33,7 +34,7 @@ class CuraSceneController(QObject):
source = args[0] source = args[0]
else: else:
source = None source = None
if not isinstance(source, SceneNode): if not isinstance(source, SceneNode) or isinstance(source, Camera):
return return
max_build_plate = self._calcMaxBuildPlate() max_build_plate = self._calcMaxBuildPlate()
changed = False changed = False

View File

@ -112,21 +112,21 @@ class CuraSceneNode(SceneNode):
## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box ## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box
def _calculateAABB(self) -> None: def _calculateAABB(self) -> None:
self._aabb = None
if self._mesh_data: if self._mesh_data:
aabb = self._mesh_data.getExtents(self.getWorldTransformation()) self._aabb = self._mesh_data.getExtents(self.getWorldTransformation())
else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0)
position = self.getWorldPosition()
aabb = AxisAlignedBox(minimum = position, maximum = position)
for child in self._children: for child in self._children:
if child.callDecoration("isNonPrintingMesh"): if child.callDecoration("isNonPrintingMesh"):
# Non-printing-meshes inside a group should not affect push apart or drop to build plate # Non-printing-meshes inside a group should not affect push apart or drop to build plate
continue continue
if aabb is None: if not child._mesh_data:
aabb = child.getBoundingBox() # Nodes without mesh data should not affect bounding boxes of their parents.
continue
if self._aabb is None:
self._aabb = child.getBoundingBox()
else: else:
aabb = aabb + child.getBoundingBox() self._aabb = self._aabb + child.getBoundingBox()
self._aabb = aabb
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode ## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode": def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode":

View File

@ -10,10 +10,10 @@ class GCodeListDecorator(SceneNodeDecorator):
def getGCodeList(self) -> List[str]: def getGCodeList(self) -> List[str]:
return self._gcode_list return self._gcode_list
def setGCodeList(self, list: List[str]): def setGCodeList(self, list: List[str]) -> None:
self._gcode_list = list self._gcode_list = list
def __deepcopy__(self, memo) -> "GCodeListDecorator": def __deepcopy__(self, memo) -> "GCodeListDecorator":
copied_decorator = GCodeListDecorator() copied_decorator = GCodeListDecorator()
copied_decorator.setGCodeList(self.getGCodeList()) copied_decorator.setGCodeList(self.getGCodeList())
return copied_decorator return copied_decorator

View File

@ -47,8 +47,10 @@ class ContainerManager(QObject):
if ContainerManager.__instance is not None: if ContainerManager.__instance is not None:
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
ContainerManager.__instance = self ContainerManager.__instance = self
try:
super().__init__(parent = application) super().__init__(parent = application)
except TypeError:
super().__init__()
self._application = application # type: CuraApplication self._application = application # type: CuraApplication
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
@ -419,13 +421,13 @@ class ContainerManager(QObject):
self._container_name_filters[name_filter] = entry self._container_name_filters[name_filter] = entry
## Import single profile, file_url does not have to end with curaprofile ## Import single profile, file_url does not have to end with curaprofile
@pyqtSlot(QUrl, result="QVariantMap") @pyqtSlot(QUrl, result = "QVariantMap")
def importProfile(self, file_url: QUrl): def importProfile(self, file_url: QUrl) -> Dict[str, str]:
if not file_url.isValid(): if not file_url.isValid():
return return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
path = file_url.toLocalFile() path = file_url.toLocalFile()
if not path: if not path:
return return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
return self._container_registry.importProfile(path) return self._container_registry.importProfile(path)
@pyqtSlot(QObject, QUrl, str) @pyqtSlot(QObject, QUrl, str)

View File

@ -1,16 +1,16 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import os import os
import re import re
import configparser import configparser
from typing import cast, Optional from typing import Any, cast, Dict, Optional
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
from UM.Decorators import override from UM.Decorators import override
from UM.Settings.ContainerFormatError import ContainerFormatError from UM.Settings.ContainerFormatError import ContainerFormatError
from UM.Settings.Interfaces import ContainerInterface
from UM.Settings.ContainerRegistry import ContainerRegistry from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
@ -28,7 +28,7 @@ from . import GlobalStack
import cura.CuraApplication import cura.CuraApplication
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
from cura.ReaderWriters.ProfileReader import NoProfileException from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
@ -161,20 +161,20 @@ class CuraContainerRegistry(ContainerRegistry):
## Imports a profile from a file ## Imports a profile from a file
# #
# \param file_name \type{str} the full path and filename of the profile to import # \param file_name The full path and filename of the profile to import.
# \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key # \return Dict with a 'status' key containing the string 'ok' or 'error',
# containing a message for the user # and a 'message' key containing a message for the user.
def importProfile(self, file_name): def importProfile(self, file_name: str) -> Dict[str, str]:
Logger.log("d", "Attempting to import profile %s", file_name) Logger.log("d", "Attempting to import profile %s", file_name)
if not file_name: if not file_name:
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, "Invalid path")} return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>: {1}", file_name, "Invalid path")}
plugin_registry = PluginRegistry.getInstance() plugin_registry = PluginRegistry.getInstance()
extension = file_name.split(".")[-1] extension = file_name.split(".")[-1]
global_stack = Application.getInstance().getGlobalContainerStack() global_stack = Application.getInstance().getGlobalContainerStack()
if not global_stack: if not global_stack:
return return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Can't import profile from <filename>{0}</filename> before a printer is added.", file_name)}
machine_extruders = [] machine_extruders = []
for position in sorted(global_stack.extruders): for position in sorted(global_stack.extruders):
@ -183,7 +183,7 @@ class CuraContainerRegistry(ContainerRegistry):
for plugin_id, meta_data in self._getIOPlugins("profile_reader"): for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
if meta_data["profile_reader"][0]["extension"] != extension: if meta_data["profile_reader"][0]["extension"] != extension:
continue continue
profile_reader = plugin_registry.getPluginObject(plugin_id) profile_reader = cast(ProfileReader, plugin_registry.getPluginObject(plugin_id))
try: try:
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader. profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
except NoProfileException: except NoProfileException:
@ -221,13 +221,13 @@ class CuraContainerRegistry(ContainerRegistry):
# Make sure we have a profile_definition in the file: # Make sure we have a profile_definition in the file:
if profile_definition is None: if profile_definition is None:
break break
machine_definition = self.findDefinitionContainers(id = profile_definition) machine_definitions = self.findDefinitionContainers(id = profile_definition)
if not machine_definition: if not machine_definitions:
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition) Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
return {"status": "error", return {"status": "error",
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name) "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)
} }
machine_definition = machine_definition[0] machine_definition = machine_definitions[0]
# Get the expected machine definition. # Get the expected machine definition.
# i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode... # i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode...
@ -267,18 +267,19 @@ class CuraContainerRegistry(ContainerRegistry):
profile.setMetaDataEntry("position", "0") profile.setMetaDataEntry("position", "0")
profile.setDirty(True) profile.setDirty(True)
if idx == 0: if idx == 0:
# move all per-extruder settings to the first extruder's quality_changes # Move all per-extruder settings to the first extruder's quality_changes
for qc_setting_key in global_profile.getAllKeys(): for qc_setting_key in global_profile.getAllKeys():
settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder") settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder")
if settable_per_extruder: if settable_per_extruder:
setting_value = global_profile.getProperty(qc_setting_key, "value") setting_value = global_profile.getProperty(qc_setting_key, "value")
setting_definition = global_stack.getSettingDefinition(qc_setting_key) setting_definition = global_stack.getSettingDefinition(qc_setting_key)
new_instance = SettingInstance(setting_definition, profile) if setting_definition is not None:
new_instance.setProperty("value", setting_value) new_instance = SettingInstance(setting_definition, profile)
new_instance.resetState() # Ensure that the state is not seen as a user state. new_instance.setProperty("value", setting_value)
profile.addInstance(new_instance) new_instance.resetState() # Ensure that the state is not seen as a user state.
profile.setDirty(True) profile.addInstance(new_instance)
profile.setDirty(True)
global_profile.removeInstance(qc_setting_key, postpone_emit=True) global_profile.removeInstance(qc_setting_key, postpone_emit=True)
extruder_profiles.append(profile) extruder_profiles.append(profile)
@ -290,7 +291,7 @@ class CuraContainerRegistry(ContainerRegistry):
for profile_index, profile in enumerate(profile_or_list): for profile_index, profile in enumerate(profile_or_list):
if profile_index == 0: if profile_index == 0:
# This is assumed to be the global profile # This is assumed to be the global profile
profile_id = (global_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_") profile_id = (cast(ContainerInterface, global_stack.getBottom()).getId() + "_" + name_seed).lower().replace(" ", "_")
elif profile_index < len(machine_extruders) + 1: elif profile_index < len(machine_extruders) + 1:
# This is assumed to be an extruder profile # This is assumed to be an extruder profile
@ -302,8 +303,8 @@ class CuraContainerRegistry(ContainerRegistry):
profile.setMetaDataEntry("position", extruder_position) profile.setMetaDataEntry("position", extruder_position)
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_") profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
else: #More extruders in the imported file than in the machine. else: # More extruders in the imported file than in the machine.
continue #Delete the additional profiles. continue # Delete the additional profiles.
result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition) result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
if result is not None: if result is not None:
@ -326,6 +327,23 @@ class CuraContainerRegistry(ContainerRegistry):
self._registerSingleExtrusionMachinesExtruderStacks() self._registerSingleExtrusionMachinesExtruderStacks()
self._connectUpgradedExtruderStacksToMachines() self._connectUpgradedExtruderStacksToMachines()
## Check if the metadata for a container is okay before adding it.
#
# This overrides the one from UM.Settings.ContainerRegistry because we
# also require that the setting_version is correct.
@override(ContainerRegistry)
def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool:
if metadata is None:
return False
if "setting_version" not in metadata:
return False
try:
if int(metadata["setting_version"]) != cura.CuraApplication.CuraApplication.SettingVersion:
return False
except ValueError: #Not parsable as int.
return False
return True
## Update an imported profile to match the current machine configuration. ## Update an imported profile to match the current machine configuration.
# #
# \param profile The profile to configure. # \param profile The profile to configure.
@ -385,30 +403,6 @@ class CuraContainerRegistry(ContainerRegistry):
result.append( (plugin_id, meta_data) ) result.append( (plugin_id, meta_data) )
return result return result
## Returns true if the current machine requires its own materials
# \return True if the current machine requires its own materials
def _machineHasOwnMaterials(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
return global_container_stack.getMetaDataEntry("has_materials", False)
return False
## Gets the ID of the active material
# \return the ID of the active material or the empty string
def _activeMaterialId(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack and global_container_stack.material:
return global_container_stack.material.getId()
return ""
## Returns true if the current machine requires its own quality profiles
# \return true if the current machine requires its own quality profiles
def _machineHasOwnQualities(self):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
return parseBool(global_container_stack.getMetaDataEntry("has_machine_quality", False))
return False
## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack. ## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.
def _convertContainerStack(self, container): def _convertContainerStack(self, container):
assert type(container) == ContainerStack assert type(container) == ContainerStack
@ -520,7 +514,7 @@ class CuraContainerRegistry(ContainerRegistry):
user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position")) user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
if machine.userChanges: if machine.userChanges:
# for the newly created extruder stack, we need to move all "per-extruder" settings to the user changes # For the newly created extruder stack, we need to move all "per-extruder" settings to the user changes
# container to the extruder stack. # container to the extruder stack.
for user_setting_key in machine.userChanges.getAllKeys(): for user_setting_key in machine.userChanges.getAllKeys():
settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder") settable_per_extruder = machine.getProperty(user_setting_key, "settable_per_extruder")
@ -582,7 +576,7 @@ class CuraContainerRegistry(ContainerRegistry):
extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position")) extruder_quality_changes_container.setMetaDataEntry("position", extruder_definition.getMetaDataEntry("position"))
extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0] extruder_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0]
else: else:
# if we still cannot find a quality changes container for the extruder, create a new one # If we still cannot find a quality changes container for the extruder, create a new one
container_name = machine_quality_changes.getName() container_name = machine_quality_changes.getName()
container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name) container_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name)
extruder_quality_changes_container = InstanceContainer(container_id, parent = application) extruder_quality_changes_container = InstanceContainer(container_id, parent = application)
@ -600,7 +594,7 @@ class CuraContainerRegistry(ContainerRegistry):
Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]", Logger.log("w", "Could not find quality_changes named [%s] for extruder [%s]",
machine_quality_changes.getName(), extruder_stack.getId()) machine_quality_changes.getName(), extruder_stack.getId())
else: else:
# move all per-extruder settings to the extruder's quality changes # Move all per-extruder settings to the extruder's quality changes
for qc_setting_key in machine_quality_changes.getAllKeys(): for qc_setting_key in machine_quality_changes.getAllKeys():
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
if settable_per_extruder: if settable_per_extruder:
@ -641,7 +635,7 @@ class CuraContainerRegistry(ContainerRegistry):
if qc_name not in qc_groups: if qc_name not in qc_groups:
qc_groups[qc_name] = [] qc_groups[qc_name] = []
qc_groups[qc_name].append(qc) qc_groups[qc_name].append(qc)
# try to find from the quality changes cura directory too # Try to find from the quality changes cura directory too
quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName()) quality_changes_container = self._findQualityChangesContainerInCuraFolder(machine_quality_changes.getName())
if quality_changes_container: if quality_changes_container:
qc_groups[qc_name].append(quality_changes_container) qc_groups[qc_name].append(quality_changes_container)
@ -655,7 +649,7 @@ class CuraContainerRegistry(ContainerRegistry):
else: else:
qc_dict["global"] = qc qc_dict["global"] = qc
if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1: if qc_dict["global"] is not None and len(qc_dict["extruders"]) == 1:
# move per-extruder settings # Move per-extruder settings
for qc_setting_key in qc_dict["global"].getAllKeys(): for qc_setting_key in qc_dict["global"].getAllKeys():
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder") settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
if settable_per_extruder: if settable_per_extruder:
@ -689,17 +683,17 @@ class CuraContainerRegistry(ContainerRegistry):
try: try:
parser.read([file_path]) parser.read([file_path])
except: except:
# skip, it is not a valid stack file # Skip, it is not a valid stack file
continue continue
if not parser.has_option("general", "name"): if not parser.has_option("general", "name"):
continue continue
if parser["general"]["name"] == name: if parser["general"]["name"] == name:
# load the container # Load the container
container_id = os.path.basename(file_path).replace(".inst.cfg", "") container_id = os.path.basename(file_path).replace(".inst.cfg", "")
if self.findInstanceContainers(id = container_id): if self.findInstanceContainers(id = container_id):
# this container is already in the registry, skip it # This container is already in the registry, skip it
continue continue
instance_container = InstanceContainer(container_id) instance_container = InstanceContainer(container_id)
@ -733,8 +727,8 @@ class CuraContainerRegistry(ContainerRegistry):
else: else:
Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId()) Logger.log("w", "Could not find machine {machine} for extruder {extruder}", machine = extruder_stack.getMetaDataEntry("machine"), extruder = extruder_stack.getId())
#Override just for the type. # Override just for the type.
@classmethod @classmethod
@override(ContainerRegistry) @override(ContainerRegistry)
def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry": def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry":
return cast(CuraContainerRegistry, super().getInstance(*args, **kwargs)) return cast(CuraContainerRegistry, super().getInstance(*args, **kwargs))

View File

@ -5,6 +5,7 @@ from typing import Any, List, Optional, TYPE_CHECKING
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from UM.Settings.SettingFunction import SettingFunction from UM.Settings.SettingFunction import SettingFunction
from UM.Logger import Logger
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
@ -38,7 +39,18 @@ class CuraFormulaFunctions:
extruder_position = int(machine_manager.defaultExtruderPosition) extruder_position = int(machine_manager.defaultExtruderPosition)
global_stack = machine_manager.activeMachine global_stack = machine_manager.activeMachine
extruder_stack = global_stack.extruders[str(extruder_position)] try:
extruder_stack = global_stack.extruders[str(extruder_position)]
except KeyError:
if extruder_position != 0:
Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available. Returning the result form extruder 0 instead" % (property_key, extruder_position))
# This fixes a very specific fringe case; If a profile was created for a custom printer and one of the
# extruder settings has been set to non zero and the profile is loaded for a machine that has only a single extruder
# it would cause all kinds of issues (and eventually a crash).
# See https://github.com/Ultimaker/Cura/issues/5535
return self.getValueInExtruder(0, property_key, context)
Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available. " % (property_key, extruder_position))
return None
value = extruder_stack.getRawProperty(property_key, "value", context = context) value = extruder_stack.getRawProperty(property_key, "value", context = context)
if isinstance(value, SettingFunction): if isinstance(value, SettingFunction):

View File

@ -125,7 +125,12 @@ class CuraStackBuilder:
extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains") extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains")
extruder_definition_id = extruder_definition_dict[str(extruder_position)] extruder_definition_id = extruder_definition_dict[str(extruder_position)]
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0] try:
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
except IndexError as e:
# It still needs to break, but we want to know what extruder ID made it break.
Logger.log("e", "Unable to find extruder with the id %s", extruder_definition_id)
raise e
# get material container for extruders # get material container for extruders
material_container = application.empty_material_container material_container = application.empty_material_container

View File

@ -63,7 +63,7 @@ class ExtruderManager(QObject):
if not self._application.getGlobalContainerStack(): if not self._application.getGlobalContainerStack():
return None # No active machine, so no active extruder. return None # No active machine, so no active extruder.
try: try:
return self._extruder_trains[self._application.getGlobalContainerStack().getId()][str(self._active_extruder_index)].getId() return self._extruder_trains[self._application.getGlobalContainerStack().getId()][str(self.activeExtruderIndex)].getId()
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong. except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
return None return None
@ -83,8 +83,9 @@ class ExtruderManager(QObject):
# \param index The index of the new active extruder. # \param index The index of the new active extruder.
@pyqtSlot(int) @pyqtSlot(int)
def setActiveExtruderIndex(self, index: int) -> None: def setActiveExtruderIndex(self, index: int) -> None:
self._active_extruder_index = index if self._active_extruder_index != index:
self.activeExtruderChanged.emit() self._active_extruder_index = index
self.activeExtruderChanged.emit()
@pyqtProperty(int, notify = activeExtruderChanged) @pyqtProperty(int, notify = activeExtruderChanged)
def activeExtruderIndex(self) -> int: def activeExtruderIndex(self) -> int:
@ -144,7 +145,7 @@ class ExtruderManager(QObject):
@pyqtSlot(result = QObject) @pyqtSlot(result = QObject)
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]: def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
return self.getExtruderStack(self._active_extruder_index) return self.getExtruderStack(self.activeExtruderIndex)
## Get an extruder stack by index ## Get an extruder stack by index
def getExtruderStack(self, index) -> Optional["ExtruderStack"]: def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
@ -223,7 +224,16 @@ class ExtruderManager(QObject):
# Get the extruders of all printable meshes in the scene # Get the extruders of all printable meshes in the scene
meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()] #type: ignore #Ignore type error because iter() should get called automatically by Python syntax. meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()] #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
# Exclude anti-overhang meshes
mesh_list = []
for mesh in meshes: for mesh in meshes:
stack = mesh.callDecoration("getStack")
if stack is not None and (stack.getProperty("anti_overhang_mesh", "value") or stack.getProperty("support_mesh", "value")):
continue
mesh_list.append(mesh)
for mesh in mesh_list:
extruder_stack_id = mesh.callDecoration("getActiveExtruder") extruder_stack_id = mesh.callDecoration("getActiveExtruder")
if not extruder_stack_id: if not extruder_stack_id:
# No per-object settings for this node # No per-object settings for this node
@ -263,7 +273,9 @@ class ExtruderManager(QObject):
used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_roof_extruder_nr", "value")))]) used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_roof_extruder_nr", "value")))])
# The platform adhesion extruder. Not used if using none. # The platform adhesion extruder. Not used if using none.
if global_stack.getProperty("adhesion_type", "value") != "none": if global_stack.getProperty("adhesion_type", "value") != "none" or (
global_stack.getProperty("prime_tower_brim_enable", "value") and
global_stack.getProperty("adhesion_type", "value") != 'raft'):
extruder_str_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value")) extruder_str_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value"))
if extruder_str_nr == "-1": if extruder_str_nr == "-1":
extruder_str_nr = self._application.getMachineManager().defaultExtruderPosition extruder_str_nr = self._application.getMachineManager().defaultExtruderPosition
@ -300,12 +312,7 @@ class ExtruderManager(QObject):
global_stack = self._application.getGlobalContainerStack() global_stack = self._application.getGlobalContainerStack()
if not global_stack: if not global_stack:
return [] return []
return global_stack.extruderList
result_tuple_list = sorted(list(global_stack.extruders.items()), key = lambda x: int(x[0]))
result_list = [item[1] for item in result_tuple_list]
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
return result_list[:machine_extruder_count]
def _globalContainerStackChanged(self) -> None: def _globalContainerStackChanged(self) -> None:
# If the global container changed, the machine changed and might have extruders that were not registered yet # If the global container changed, the machine changed and might have extruders that were not registered yet
@ -340,14 +347,15 @@ class ExtruderManager(QObject):
extruder_train.setNextStack(global_stack) extruder_train.setNextStack(global_stack)
extruders_changed = True extruders_changed = True
self._fixSingleExtrusionMachineExtruderDefinition(global_stack) self.fixSingleExtrusionMachineExtruderDefinition(global_stack)
if extruders_changed: if extruders_changed:
self.extrudersChanged.emit(global_stack_id) self.extrudersChanged.emit(global_stack_id)
self.setActiveExtruderIndex(0) self.setActiveExtruderIndex(0)
self.activeExtruderChanged.emit()
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing # After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this. # "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
def _fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None: def fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
container_registry = ContainerRegistry.getInstance() container_registry = ContainerRegistry.getInstance()
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"] expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
extruder_stack_0 = global_stack.extruders.get("0") extruder_stack_0 = global_stack.extruders.get("0")

View File

@ -52,8 +52,8 @@ class ExtruderStack(CuraContainerStack):
return super().getNextStack() return super().getNextStack()
def setEnabled(self, enabled: bool) -> None: def setEnabled(self, enabled: bool) -> None:
if "enabled" not in self._metadata: if self.getMetaDataEntry("enabled", True) == enabled: # No change.
self.setMetaDataEntry("enabled", "True") return # Don't emit a signal then.
self.setMetaDataEntry("enabled", str(enabled)) self.setMetaDataEntry("enabled", str(enabled))
self.enabledChanged.emit() self.enabledChanged.emit()

View File

@ -3,8 +3,8 @@
from collections import defaultdict from collections import defaultdict
import threading import threading
from typing import Any, Dict, Optional, Set, TYPE_CHECKING from typing import Any, Dict, Optional, Set, TYPE_CHECKING, List
from PyQt5.QtCore import pyqtProperty, pyqtSlot from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
from UM.Decorators import override from UM.Decorators import override
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
@ -42,17 +42,75 @@ class GlobalStack(CuraContainerStack):
# Per thread we have our own resolving_settings, or strange things sometimes occur. # Per thread we have our own resolving_settings, or strange things sometimes occur.
self._resolving_settings = defaultdict(set) #type: Dict[str, Set[str]] # keys are thread names self._resolving_settings = defaultdict(set) #type: Dict[str, Set[str]] # keys are thread names
# Since the metadatachanged is defined in container stack, we can't use it here as a notifier for pyqt
# properties. So we need to tie them together like this.
self.metaDataChanged.connect(self.configuredConnectionTypesChanged)
extrudersChanged = pyqtSignal()
configuredConnectionTypesChanged = pyqtSignal()
## Get the list of extruders of this stack. ## Get the list of extruders of this stack.
# #
# \return The extruders registered with this stack. # \return The extruders registered with this stack.
@pyqtProperty("QVariantMap") @pyqtProperty("QVariantMap", notify = extrudersChanged)
def extruders(self) -> Dict[str, "ExtruderStack"]: def extruders(self) -> Dict[str, "ExtruderStack"]:
return self._extruders return self._extruders
@pyqtProperty("QVariantList", notify = extrudersChanged)
def extruderList(self) -> List["ExtruderStack"]:
result_tuple_list = sorted(list(self.extruders.items()), key=lambda x: int(x[0]))
result_list = [item[1] for item in result_tuple_list]
machine_extruder_count = self.getProperty("machine_extruder_count", "value")
return result_list[:machine_extruder_count]
@pyqtProperty(int, constant = True)
def maxExtruderCount(self):
return len(self.getMetaDataEntry("machine_extruder_trains"))
@classmethod @classmethod
def getLoadingPriority(cls) -> int: def getLoadingPriority(cls) -> int:
return 2 return 2
## The configured connection types can be used to find out if the global
# stack is configured to be connected with a printer, without having to
# know all the details as to how this is exactly done (and without
# actually setting the stack to be active).
#
# This data can then in turn also be used when the global stack is active;
# If we can't get a network connection, but it is configured to have one,
# we can display a different icon to indicate the difference.
@pyqtProperty("QVariantList", notify=configuredConnectionTypesChanged)
def configuredConnectionTypes(self) -> List[int]:
# Requesting it from the metadata actually gets them as strings (as that's what you get from serializing).
# But we do want them returned as a list of ints (so the rest of the code can directly compare)
connection_types = self.getMetaDataEntry("connection_type", "").split(",")
result = []
for connection_type in connection_types:
if connection_type != "":
try:
result.append(int(connection_type))
except ValueError:
# We got invalid data, probably a None.
pass
return result
## \sa configuredConnectionTypes
def addConfiguredConnectionType(self, connection_type: int) -> None:
configured_connection_types = self.configuredConnectionTypes
if connection_type not in configured_connection_types:
# Store the values as a string.
configured_connection_types.append(connection_type)
self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))
## \sa configuredConnectionTypes
def removeConfiguredConnectionType(self, connection_type: int) -> None:
configured_connection_types = self.configuredConnectionTypes
if connection_type in self.configured_connection_types:
# Store the values as a string.
configured_connection_types.remove(connection_type)
self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))
@classmethod @classmethod
def getConfigurationTypeFromSerialized(cls, serialized: str) -> Optional[str]: def getConfigurationTypeFromSerialized(cls, serialized: str) -> Optional[str]:
configuration_type = super().getConfigurationTypeFromSerialized(serialized) configuration_type = super().getConfigurationTypeFromSerialized(serialized)
@ -87,6 +145,7 @@ class GlobalStack(CuraContainerStack):
return return
self._extruders[position] = extruder self._extruders[position] = extruder
self.extrudersChanged.emit()
Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position) Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)
## Overridden from ContainerStack ## Overridden from ContainerStack
@ -153,7 +212,7 @@ class GlobalStack(CuraContainerStack):
# Determine whether or not we should try to get the "resolve" property instead of the # Determine whether or not we should try to get the "resolve" property instead of the
# requested property. # requested property.
def _shouldResolve(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> bool: def _shouldResolve(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> bool:
if property_name is not "value": if property_name != "value":
# Do not try to resolve anything but the "value" property # Do not try to resolve anything but the "value" property
return False return False
@ -199,6 +258,9 @@ class GlobalStack(CuraContainerStack):
def getHasVariants(self) -> bool: def getHasVariants(self) -> bool:
return parseBool(self.getMetaDataEntry("has_variants", False)) return parseBool(self.getMetaDataEntry("has_variants", False))
def getHasVariantsBuildPlates(self) -> bool:
return parseBool(self.getMetaDataEntry("has_variant_buildplates", False))
def getHasMachineQuality(self) -> bool: def getHasMachineQuality(self) -> bool:
return parseBool(self.getMetaDataEntry("has_machine_quality", False)) return parseBool(self.getMetaDataEntry("has_machine_quality", False))

View File

@ -1,17 +1,19 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
import collections
import time import time
from typing import Any, Callable, List, Dict, TYPE_CHECKING, Optional, cast import re
import unicodedata
from typing import Any, List, Dict, TYPE_CHECKING, Optional, cast
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Decorators import deprecated
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Settings.InstanceContainer import InstanceContainer from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.Interfaces import ContainerInterface from UM.Settings.Interfaces import ContainerInterface
from UM.Signal import Signal from UM.Signal import Signal
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer
from UM.FlameProfiler import pyqtSlot from UM.FlameProfiler import pyqtSlot
from UM import Util from UM import Util
from UM.Logger import Logger from UM.Logger import Logger
@ -21,10 +23,10 @@ from UM.Settings.SettingFunction import SettingFunction
from UM.Signal import postponeSignals, CompressTechnique from UM.Signal import postponeSignals, CompressTechnique
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
from cura.PrinterOutputDevice import PrinterOutputDevice from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionType
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.ExtruderManager import ExtruderManager from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack from cura.Settings.ExtruderStack import ExtruderStack
@ -62,9 +64,7 @@ class MachineManager(QObject):
self._default_extruder_position = "0" # to be updated when extruders are switched on and off self._default_extruder_position = "0" # to be updated when extruders are switched on and off
self.machine_extruder_material_update_dict = collections.defaultdict(list) #type: Dict[str, List[Callable[[], None]]] self._instance_container_timer = QTimer() # type: QTimer
self._instance_container_timer = QTimer() #type: QTimer
self._instance_container_timer.setInterval(250) self._instance_container_timer.setInterval(250)
self._instance_container_timer.setSingleShot(True) self._instance_container_timer.setSingleShot(True)
self._instance_container_timer.timeout.connect(self.__emitChangedSignals) self._instance_container_timer.timeout.connect(self.__emitChangedSignals)
@ -74,7 +74,7 @@ class MachineManager(QObject):
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged) self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._container_registry.containerLoadComplete.connect(self._onContainersChanged) self._container_registry.containerLoadComplete.connect(self._onContainersChanged)
## When the global container is changed, active material probably needs to be updated. # When the global container is changed, active material probably needs to be updated.
self.globalContainerChanged.connect(self.activeMaterialChanged) self.globalContainerChanged.connect(self.activeMaterialChanged)
self.globalContainerChanged.connect(self.activeVariantChanged) self.globalContainerChanged.connect(self.activeVariantChanged)
self.globalContainerChanged.connect(self.activeQualityChanged) self.globalContainerChanged.connect(self.activeQualityChanged)
@ -86,12 +86,14 @@ class MachineManager(QObject):
self._onGlobalContainerChanged() self._onGlobalContainerChanged()
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged) extruder_manager = self._application.getExtruderManager()
extruder_manager.activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
self._onActiveExtruderStackChanged() self._onActiveExtruderStackChanged()
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeMaterialChanged) extruder_manager.activeExtruderChanged.connect(self.activeMaterialChanged)
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeVariantChanged) extruder_manager.activeExtruderChanged.connect(self.activeVariantChanged)
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeQualityChanged) extruder_manager.activeExtruderChanged.connect(self.activeQualityChanged)
self.globalContainerChanged.connect(self.activeStackChanged) self.globalContainerChanged.connect(self.activeStackChanged)
self.globalValueChanged.connect(self.activeStackValueChanged) self.globalValueChanged.connect(self.activeStackValueChanged)
@ -105,7 +107,7 @@ class MachineManager(QObject):
# There might already be some output devices by the time the signal is connected # There might already be some output devices by the time the signal is connected
self._onOutputDevicesChanged() self._onOutputDevicesChanged()
self._current_printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer self._current_printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer
self.activeMaterialChanged.connect(self._onCurrentConfigurationChanged) self.activeMaterialChanged.connect(self._onCurrentConfigurationChanged)
self.activeVariantChanged.connect(self._onCurrentConfigurationChanged) self.activeVariantChanged.connect(self._onCurrentConfigurationChanged)
# Force to compute the current configuration # Force to compute the current configuration
@ -113,17 +115,13 @@ class MachineManager(QObject):
self._application.callLater(self.setInitialActiveMachine) self._application.callLater(self.setInitialActiveMachine)
self._material_incompatible_message = Message(catalog.i18nc("@info:status", containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) # type: List[InstanceContainer]
"The selected material is incompatible with the selected machine or configuration."),
title = catalog.i18nc("@info:title", "Incompatible Material")) #type: Message
containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) #type: List[InstanceContainer]
if containers: if containers:
containers[0].nameChanged.connect(self._onMaterialNameChanged) containers[0].nameChanged.connect(self._onMaterialNameChanged)
self._material_manager = self._application.getMaterialManager() #type: MaterialManager self._material_manager = self._application.getMaterialManager() # type: MaterialManager
self._variant_manager = self._application.getVariantManager() #type: VariantManager self._variant_manager = self._application.getVariantManager() # type: VariantManager
self._quality_manager = self._application.getQualityManager() #type: QualityManager self._quality_manager = self._application.getQualityManager() # type: QualityManager
# When the materials lookup table gets updated, it can mean that a material has its name changed, which should # When the materials lookup table gets updated, it can mean that a material has its name changed, which should
# be reflected on the GUI. This signal emission makes sure that it happens. # be reflected on the GUI. This signal emission makes sure that it happens.
@ -156,10 +154,11 @@ class MachineManager(QObject):
blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly
outputDevicesChanged = pyqtSignal() outputDevicesChanged = pyqtSignal()
currentConfigurationChanged = pyqtSignal() # Emitted every time the current configurations of the machine changes currentConfigurationChanged = pyqtSignal() # Emitted every time the current configurations of the machine changes
printerConnectedStatusChanged = pyqtSignal() # Emitted every time the active machine change or the outputdevices change printerConnectedStatusChanged = pyqtSignal() # Emitted every time the active machine change or the outputdevices change
rootMaterialChanged = pyqtSignal() rootMaterialChanged = pyqtSignal()
discoveredPrintersChanged = pyqtSignal()
def setInitialActiveMachine(self) -> None: def setInitialActiveMachine(self) -> None:
active_machine_id = self._application.getPreferences().getValue("cura/active_machine") active_machine_id = self._application.getPreferences().getValue("cura/active_machine")
@ -176,7 +175,7 @@ class MachineManager(QObject):
self.outputDevicesChanged.emit() self.outputDevicesChanged.emit()
@pyqtProperty(QObject, notify = currentConfigurationChanged) @pyqtProperty(QObject, notify = currentConfigurationChanged)
def currentConfiguration(self) -> ConfigurationModel: def currentConfiguration(self) -> PrinterConfigurationModel:
return self._current_printer_configuration return self._current_printer_configuration
def _onCurrentConfigurationChanged(self) -> None: def _onCurrentConfigurationChanged(self) -> None:
@ -201,13 +200,13 @@ class MachineManager(QObject):
extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != 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.extruderConfigurations.append(extruder_configuration)
# an empty build plate configuration from the network printer is presented as an empty string, so use "" for an # An empty build plate configuration from the network printer is presented as an empty string, so use "" for an
# empty build plate. # 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._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() self.currentConfigurationChanged.emit()
@pyqtSlot(QObject, result = bool) @pyqtSlot(QObject, result = bool)
def matchesConfiguration(self, configuration: ConfigurationModel) -> bool: def matchesConfiguration(self, configuration: PrinterConfigurationModel) -> bool:
return self._current_printer_configuration == configuration return self._current_printer_configuration == configuration
@pyqtProperty("QVariantList", notify = outputDevicesChanged) @pyqtProperty("QVariantList", notify = outputDevicesChanged)
@ -247,7 +246,7 @@ class MachineManager(QObject):
self.updateNumberExtrudersEnabled() self.updateNumberExtrudersEnabled()
self.globalContainerChanged.emit() self.globalContainerChanged.emit()
# after switching the global stack we reconnect all the signals and set the variant and material references # After switching the global stack we reconnect all the signals and set the variant and material references
if self._global_container_stack: if self._global_container_stack:
self._application.getPreferences().setValue("cura/active_machine", self._global_container_stack.getId()) self._application.getPreferences().setValue("cura/active_machine", self._global_container_stack.getId())
@ -261,7 +260,7 @@ class MachineManager(QObject):
if global_variant.getMetaDataEntry("hardware_type") != "buildplate": if global_variant.getMetaDataEntry("hardware_type") != "buildplate":
self._global_container_stack.setVariant(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 # 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 global_material = self._global_container_stack.material
if global_material != empty_material_container: if global_material != empty_material_container:
self._global_container_stack.setMaterial(empty_material_container) self._global_container_stack.setMaterial(empty_material_container)
@ -271,30 +270,19 @@ class MachineManager(QObject):
extruder_stack.propertyChanged.connect(self._onPropertyChanged) extruder_stack.propertyChanged.connect(self._onPropertyChanged)
extruder_stack.containersChanged.connect(self._onContainersChanged) extruder_stack.containersChanged.connect(self._onContainersChanged)
if self._global_container_stack.getId() in self.machine_extruder_material_update_dict:
for func in self.machine_extruder_material_update_dict[self._global_container_stack.getId()]:
self._application.callLater(func)
del self.machine_extruder_material_update_dict[self._global_container_stack.getId()]
self.activeQualityGroupChanged.emit() self.activeQualityGroupChanged.emit()
def _onActiveExtruderStackChanged(self) -> None: def _onActiveExtruderStackChanged(self) -> None:
self.blurSettings.emit() # Ensure no-one has focus. self.blurSettings.emit() # Ensure no-one has focus.
old_active_container_stack = self._active_container_stack
self._active_container_stack = ExtruderManager.getInstance().getActiveExtruderStack() self._active_container_stack = ExtruderManager.getInstance().getActiveExtruderStack()
if old_active_container_stack != self._active_container_stack:
# Many methods and properties related to the active quality actually depend
# on _active_container_stack. If it changes, then the properties change.
self.activeQualityChanged.emit()
def __emitChangedSignals(self) -> None: def __emitChangedSignals(self) -> None:
self.activeQualityChanged.emit() self.activeQualityChanged.emit()
self.activeVariantChanged.emit() self.activeVariantChanged.emit()
self.activeMaterialChanged.emit() self.activeMaterialChanged.emit()
self.rootMaterialChanged.emit() self.rootMaterialChanged.emit()
self.numberExtrudersEnabledChanged.emit()
def _onContainersChanged(self, container: ContainerInterface) -> None: def _onContainersChanged(self, container: ContainerInterface) -> None:
self._instance_container_timer.start() self._instance_container_timer.start()
@ -370,18 +358,23 @@ class MachineManager(QObject):
# Make sure that the default machine actions for this machine have been added # Make sure that the default machine actions for this machine have been added
self._application.getMachineActionManager().addDefaultMachineActions(global_stack) self._application.getMachineActionManager().addDefaultMachineActions(global_stack)
ExtruderManager.getInstance()._fixSingleExtrusionMachineExtruderDefinition(global_stack) ExtruderManager.getInstance().fixSingleExtrusionMachineExtruderDefinition(global_stack)
if not global_stack.isValid(): if not global_stack.isValid():
# Mark global stack as invalid # Mark global stack as invalid
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId()) ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
return # We're done here return # We're done here
ExtruderManager.getInstance().setActiveExtruderIndex(0) # Switch to first extruder
self._global_container_stack = global_stack self._global_container_stack = global_stack
self._application.setGlobalContainerStack(global_stack) self._application.setGlobalContainerStack(global_stack)
ExtruderManager.getInstance()._globalContainerStackChanged() ExtruderManager.getInstance()._globalContainerStackChanged()
self._initMachineState(global_stack) self._initMachineState(global_stack)
self._onGlobalContainerChanged() self._onGlobalContainerChanged()
# Switch to the first enabled extruder
self.updateDefaultExtruder()
default_extruder_position = int(self.defaultExtruderPosition)
ExtruderManager.getInstance().setActiveExtruderIndex(default_extruder_position)
self.__emitChangedSignals() self.__emitChangedSignals()
## Given a definition id, return the machine with this id. ## Given a definition id, return the machine with this id.
@ -398,9 +391,17 @@ class MachineManager(QObject):
return machine return machine
return None return None
@pyqtSlot(str)
@pyqtSlot(str, str) @pyqtSlot(str, str)
def addMachine(self, name: str, definition_id: str) -> None: def addMachine(self, definition_id: str, name: Optional[str] = None) -> None:
new_stack = CuraStackBuilder.createMachine(name, definition_id) if name is None:
definitions = CuraContainerRegistry.getInstance().findDefinitionContainers(id = definition_id)
if definitions:
name = definitions[0].getName()
else:
name = definition_id
new_stack = CuraStackBuilder.createMachine(cast(str, name), definition_id)
if new_stack: if new_stack:
# Instead of setting the global container stack here, we set the active machine and so the signals are emitted # Instead of setting the global container stack here, we set the active machine and so the signals are emitted
self.setActiveMachine(new_stack.getId()) self.setActiveMachine(new_stack.getId())
@ -419,7 +420,7 @@ class MachineManager(QObject):
# Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are # Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value") machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks() extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
count = 1 # we start with the global stack count = 1 # We start with the global stack
for stack in extruder_stacks: for stack in extruder_stacks:
md = stack.getMetaData() md = stack.getMetaData()
if "position" in md and int(md["position"]) >= machine_extruder_count: if "position" in md and int(md["position"]) >= machine_extruder_count:
@ -438,12 +439,12 @@ class MachineManager(QObject):
if not self._global_container_stack: if not self._global_container_stack:
return False return False
if self._global_container_stack.getTop().findInstances(): if self._global_container_stack.getTop().getNumInstances() != 0:
return True return True
stacks = ExtruderManager.getInstance().getActiveExtruderStacks() stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
for stack in stacks: for stack in stacks:
if stack.getTop().findInstances(): if stack.getTop().getNumInstances() != 0:
return True return True
return False return False
@ -453,10 +454,10 @@ class MachineManager(QObject):
if not self._global_container_stack: if not self._global_container_stack:
return 0 return 0
num_user_settings = 0 num_user_settings = 0
num_user_settings += len(self._global_container_stack.getTop().findInstances()) num_user_settings += self._global_container_stack.getTop().getNumInstances()
stacks = ExtruderManager.getInstance().getActiveExtruderStacks() stacks = self._global_container_stack.extruderList
for stack in stacks: for stack in stacks:
num_user_settings += len(stack.getTop().findInstances()) num_user_settings += stack.getTop().getNumInstances()
return num_user_settings return num_user_settings
## Delete a user setting from the global stack and all extruder stacks. ## Delete a user setting from the global stack and all extruder stacks.
@ -498,28 +499,78 @@ class MachineManager(QObject):
return bool(self._stacks_have_errors) return bool(self._stacks_have_errors)
@pyqtProperty(str, notify = globalContainerChanged) @pyqtProperty(str, notify = globalContainerChanged)
@deprecated("use Cura.MachineManager.activeMachine.definition.name instead", "4.1")
def activeMachineDefinitionName(self) -> str: def activeMachineDefinitionName(self) -> str:
if self._global_container_stack: if self._global_container_stack:
return self._global_container_stack.definition.getName() return self._global_container_stack.definition.getName()
return "" return ""
@pyqtProperty(str, notify = globalContainerChanged) @pyqtProperty(str, notify = globalContainerChanged)
@deprecated("use Cura.MachineManager.activeMachine.name instead", "4.1")
def activeMachineName(self) -> str: def activeMachineName(self) -> str:
if self._global_container_stack: if self._global_container_stack:
return self._global_container_stack.getName() return self._global_container_stack.getMetaDataEntry("group_name", self._global_container_stack.getName())
return "" return ""
@pyqtProperty(str, notify = globalContainerChanged) @pyqtProperty(str, notify = globalContainerChanged)
@deprecated("use Cura.MachineManager.activeMachine.id instead", "4.1")
def activeMachineId(self) -> str: def activeMachineId(self) -> str:
if self._global_container_stack: if self._global_container_stack:
return self._global_container_stack.getId() return self._global_container_stack.getId()
return "" return ""
@pyqtProperty(str, notify = globalContainerChanged)
def activeMachineFirmwareVersion(self) -> str:
if not self._printer_output_devices:
return ""
return self._printer_output_devices[0].firmwareVersion
@pyqtProperty(str, notify = globalContainerChanged)
def activeMachineAddress(self) -> str:
if not self._printer_output_devices:
return ""
return self._printer_output_devices[0].address
@pyqtProperty(bool, notify = printerConnectedStatusChanged) @pyqtProperty(bool, notify = printerConnectedStatusChanged)
def printerConnected(self): def printerConnected(self) -> bool:
return bool(self._printer_output_devices) return bool(self._printer_output_devices)
@pyqtProperty(str, notify = printerConnectedStatusChanged) @pyqtProperty(bool, notify = printerConnectedStatusChanged)
def activeMachineHasRemoteConnection(self) -> bool:
if self._global_container_stack:
has_remote_connection = False
for connection_type in self._global_container_stack.configuredConnectionTypes:
has_remote_connection |= connection_type in [ConnectionType.NetworkConnection.value,
ConnectionType.CloudConnection.value]
return has_remote_connection
return False
@pyqtProperty("QVariantList", notify=globalContainerChanged)
@deprecated("use Cura.MachineManager.activeMachine.configuredConnectionTypes instead", "4.1")
def activeMachineConfiguredConnectionTypes(self):
if self._global_container_stack:
return self._global_container_stack.configuredConnectionTypes
return []
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
def activeMachineIsGroup(self) -> bool:
return bool(self._printer_output_devices) and len(self._printer_output_devices[0].printers) > 1
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
def activeMachineHasNetworkConnection(self) -> bool:
# A network connection is only available if any output device is actually a network connected device.
return any(d.connectionType == ConnectionType.NetworkConnection for d in self._printer_output_devices)
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
def activeMachineHasCloudConnection(self) -> bool:
# A cloud connection is only available if any output device actually is a cloud connected device.
return any(d.connectionType == ConnectionType.CloudConnection for d in self._printer_output_devices)
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
def activeMachineIsUsingCloudConnection(self) -> bool:
return self.activeMachineHasCloudConnection and not self.activeMachineHasNetworkConnection
def activeMachineNetworkKey(self) -> str: def activeMachineNetworkKey(self) -> str:
if self._global_container_stack: if self._global_container_stack:
return self._global_container_stack.getMetaDataEntry("um_network_key", "") return self._global_container_stack.getMetaDataEntry("um_network_key", "")
@ -528,7 +579,7 @@ class MachineManager(QObject):
@pyqtProperty(str, notify = printerConnectedStatusChanged) @pyqtProperty(str, notify = printerConnectedStatusChanged)
def activeMachineNetworkGroupName(self) -> str: def activeMachineNetworkGroupName(self) -> str:
if self._global_container_stack: if self._global_container_stack:
return self._global_container_stack.getMetaDataEntry("connect_group_name", "") return self._global_container_stack.getMetaDataEntry("group_name", "")
return "" return ""
@pyqtProperty(QObject, notify = globalContainerChanged) @pyqtProperty(QObject, notify = globalContainerChanged)
@ -616,6 +667,14 @@ class MachineManager(QObject):
is_supported = self._current_quality_group.is_available is_supported = self._current_quality_group.is_available
return is_supported return is_supported
@pyqtProperty(bool, notify = activeQualityGroupChanged)
def isActiveQualityExperimental(self) -> bool:
is_experimental = False
if self._global_container_stack:
if self._current_quality_group:
is_experimental = self._current_quality_group.is_experimental
return is_experimental
## Returns whether there is anything unsupported in the current set-up. ## Returns whether there is anything unsupported in the current set-up.
# #
# The current set-up signifies the global stack and all extruder stacks, # The current set-up signifies the global stack and all extruder stacks,
@ -633,11 +692,6 @@ class MachineManager(QObject):
return False return False
return True return True
## Check if a container is read_only
@pyqtSlot(str, result = bool)
def isReadOnly(self, container_id: str) -> bool:
return CuraContainerRegistry.getInstance().isReadOnly(container_id)
## Copy the value of the setting of the current extruder to all other extruders as well as the global container. ## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
@pyqtSlot(str) @pyqtSlot(str)
def copyValueToExtruders(self, key: str) -> None: def copyValueToExtruders(self, key: str) -> None:
@ -646,7 +700,7 @@ class MachineManager(QObject):
new_value = self._active_container_stack.getProperty(key, "value") new_value = self._active_container_stack.getProperty(key, "value")
extruder_stacks = [stack for stack in ExtruderManager.getInstance().getActiveExtruderStacks()] extruder_stacks = [stack for stack in ExtruderManager.getInstance().getActiveExtruderStacks()]
# check in which stack the value has to be replaced # Check in which stack the value has to be replaced
for extruder_stack in extruder_stacks: for extruder_stack in extruder_stacks:
if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value: if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved
@ -662,10 +716,11 @@ class MachineManager(QObject):
for key in self._active_container_stack.userChanges.getAllKeys(): for key in self._active_container_stack.userChanges.getAllKeys():
new_value = self._active_container_stack.getProperty(key, "value") new_value = self._active_container_stack.getProperty(key, "value")
# check if the value has to be replaced # Check if the value has to be replaced
extruder_stack.userChanges.setProperty(key, "value", new_value) extruder_stack.userChanges.setProperty(key, "value", new_value)
@pyqtProperty(str, notify = activeVariantChanged) @pyqtProperty(str, notify = activeVariantChanged)
@deprecated("use Cura.activeStack.variant.name instead", "4.1")
def activeVariantName(self) -> str: def activeVariantName(self) -> str:
if self._active_container_stack: if self._active_container_stack:
variant = self._active_container_stack.variant variant = self._active_container_stack.variant
@ -675,6 +730,7 @@ class MachineManager(QObject):
return "" return ""
@pyqtProperty(str, notify = activeVariantChanged) @pyqtProperty(str, notify = activeVariantChanged)
@deprecated("use Cura.activeStack.variant.id instead", "4.1")
def activeVariantId(self) -> str: def activeVariantId(self) -> str:
if self._active_container_stack: if self._active_container_stack:
variant = self._active_container_stack.variant variant = self._active_container_stack.variant
@ -684,6 +740,7 @@ class MachineManager(QObject):
return "" return ""
@pyqtProperty(str, notify = activeVariantChanged) @pyqtProperty(str, notify = activeVariantChanged)
@deprecated("use Cura.activeMachine.variant.name instead", "4.1")
def activeVariantBuildplateName(self) -> str: def activeVariantBuildplateName(self) -> str:
if self._global_container_stack: if self._global_container_stack:
variant = self._global_container_stack.variant variant = self._global_container_stack.variant
@ -693,6 +750,7 @@ class MachineManager(QObject):
return "" return ""
@pyqtProperty(str, notify = globalContainerChanged) @pyqtProperty(str, notify = globalContainerChanged)
@deprecated("use Cura.activeMachine.definition.id instead", "4.1")
def activeDefinitionId(self) -> str: def activeDefinitionId(self) -> str:
if self._global_container_stack: if self._global_container_stack:
return self._global_container_stack.definition.id return self._global_container_stack.definition.id
@ -731,7 +789,7 @@ class MachineManager(QObject):
# If the machine that is being removed is the currently active machine, set another machine as the active machine. # If the machine that is being removed is the currently active machine, set another machine as the active machine.
activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id) activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id)
# activate a new machine before removing a machine because this is safer # Activate a new machine before removing a machine because this is safer
if activate_new_machine: if activate_new_machine:
machine_stacks = CuraContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine") machine_stacks = CuraContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine")
other_machine_stacks = [s for s in machine_stacks if s["id"] != machine_id] other_machine_stacks = [s for s in machine_stacks if s["id"] != machine_id]
@ -739,7 +797,7 @@ class MachineManager(QObject):
self.setActiveMachine(other_machine_stacks[0]["id"]) self.setActiveMachine(other_machine_stacks[0]["id"])
metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0] metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0]
network_key = metadata["um_network_key"] if "um_network_key" in metadata else None network_key = metadata.get("um_network_key", None)
ExtruderManager.getInstance().removeMachineExtruders(machine_id) ExtruderManager.getInstance().removeMachineExtruders(machine_id)
containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id) containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
for container in containers: for container in containers:
@ -757,19 +815,19 @@ class MachineManager(QObject):
@pyqtProperty(bool, notify = globalContainerChanged) @pyqtProperty(bool, notify = globalContainerChanged)
def hasMaterials(self) -> bool: def hasMaterials(self) -> bool:
if self._global_container_stack: if self._global_container_stack:
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)) return self._global_container_stack.getHasMaterials()
return False return False
@pyqtProperty(bool, notify = globalContainerChanged) @pyqtProperty(bool, notify = globalContainerChanged)
def hasVariants(self) -> bool: def hasVariants(self) -> bool:
if self._global_container_stack: if self._global_container_stack:
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_variants", False)) return self._global_container_stack.getHasVariants()
return False return False
@pyqtProperty(bool, notify = globalContainerChanged) @pyqtProperty(bool, notify = globalContainerChanged)
def hasVariantBuildplates(self) -> bool: def hasVariantBuildplates(self) -> bool:
if self._global_container_stack: if self._global_container_stack:
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_variant_buildplates", False)) return self._global_container_stack.getHasVariantsBuildPlates()
return False return False
## The selected buildplate is compatible if it is compatible with all the materials in all the extruders ## The selected buildplate is compatible if it is compatible with all the materials in all the extruders
@ -864,7 +922,7 @@ class MachineManager(QObject):
caution_message = Message(catalog.i18nc( caution_message = Message(catalog.i18nc(
"@info:generic", "@info:generic",
"Settings have been changed to match the current availability of extruders: [%s]" % ", ".join(add_user_changes)), "Settings have been changed to match the current availability of extruders: [%s]" % ", ".join(add_user_changes)),
lifetime=0, lifetime = 0,
title = catalog.i18nc("@info:title", "Settings updated")) title = catalog.i18nc("@info:title", "Settings updated"))
caution_message.show() caution_message.show()
@ -909,21 +967,18 @@ class MachineManager(QObject):
# After CURA-4482 this should not be the case anymore, but we still want to support older project files. # After CURA-4482 this should not be the case anymore, but we still want to support older project files.
global_user_container = self._global_container_stack.userChanges global_user_container = self._global_container_stack.userChanges
# Make sure extruder_stacks exists
extruder_stacks = [] #type: List[ExtruderStack]
if previous_extruder_count == 1:
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
global_user_container = self._global_container_stack.userChanges
for setting_instance in global_user_container.findInstances(): for setting_instance in global_user_container.findInstances():
setting_key = setting_instance.definition.key setting_key = setting_instance.definition.key
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder") settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
if settable_per_extruder: if settable_per_extruder:
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder")) limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
extruder_stack = extruder_stacks[max(0, limit_to_extruder)] extruder_position = max(0, limit_to_extruder)
extruder_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value")) extruder_stack = self.getExtruder(extruder_position)
if extruder_stack:
extruder_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
else:
Logger.log("e", "Unable to find extruder on position %s", extruder_position)
global_user_container.removeInstance(setting_key) global_user_container.removeInstance(setting_key)
# Signal that the global stack has changed # Signal that the global stack has changed
@ -932,10 +987,9 @@ class MachineManager(QObject):
@pyqtSlot(int, result = QObject) @pyqtSlot(int, result = QObject)
def getExtruder(self, position: int) -> Optional[ExtruderStack]: def getExtruder(self, position: int) -> Optional[ExtruderStack]:
extruder = None
if self._global_container_stack: if self._global_container_stack:
extruder = self._global_container_stack.extruders.get(str(position)) return self._global_container_stack.extruders.get(str(position))
return extruder return None
def updateDefaultExtruder(self) -> None: def updateDefaultExtruder(self) -> None:
if self._global_container_stack is None: if self._global_container_stack is None:
@ -1001,12 +1055,12 @@ class MachineManager(QObject):
if not enabled and position == ExtruderManager.getInstance().activeExtruderIndex: if not enabled and position == ExtruderManager.getInstance().activeExtruderIndex:
ExtruderManager.getInstance().setActiveExtruderIndex(int(self._default_extruder_position)) ExtruderManager.getInstance().setActiveExtruderIndex(int(self._default_extruder_position))
# ensure that the quality profile is compatible with current combination, or choose a compatible one if available # Ensure that the quality profile is compatible with current combination, or choose a compatible one if available
self._updateQualityWithMaterial() self._updateQualityWithMaterial()
self.extruderChanged.emit() self.extruderChanged.emit()
# update material compatibility color # Update material compatibility color
self.activeQualityGroupChanged.emit() self.activeQualityGroupChanged.emit()
# update items in SettingExtruder # Update items in SettingExtruder
ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId()) ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId())
# Make sure the front end reflects changes # Make sure the front end reflects changes
self.forceUpdateAllSettings() self.forceUpdateAllSettings()
@ -1019,9 +1073,6 @@ class MachineManager(QObject):
def _onMaterialNameChanged(self) -> None: def _onMaterialNameChanged(self) -> None:
self.activeMaterialChanged.emit() self.activeMaterialChanged.emit()
def _onQualityNameChanged(self) -> None:
self.activeQualityChanged.emit()
def _getContainerChangedSignals(self) -> List[Signal]: def _getContainerChangedSignals(self) -> List[Signal]:
if self._global_container_stack is None: if self._global_container_stack is None:
return [] return []
@ -1080,7 +1131,6 @@ class MachineManager(QObject):
return result return result
#
# Sets all quality and quality_changes containers to empty_quality and empty_quality_changes containers # Sets all quality and quality_changes containers to empty_quality and empty_quality_changes containers
# for all stacks in the currently active machine. # for all stacks in the currently active machine.
# #
@ -1139,7 +1189,7 @@ class MachineManager(QObject):
def _setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None: def _setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
if self._global_container_stack is None: if self._global_container_stack is None:
return #Can't change that. return # Can't change that.
quality_type = quality_changes_group.quality_type quality_type = quality_changes_group.quality_type
# A custom quality can be created based on "not supported". # A custom quality can be created based on "not supported".
# In that case, do not set quality containers to empty. # In that case, do not set quality containers to empty.
@ -1209,7 +1259,7 @@ class MachineManager(QObject):
self.rootMaterialChanged.emit() self.rootMaterialChanged.emit()
def activeMaterialsCompatible(self) -> bool: def activeMaterialsCompatible(self) -> bool:
# check material - variant compatibility # Check material - variant compatibility
if self._global_container_stack is not None: if self._global_container_stack is not None:
if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)): if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)):
for position, extruder in self._global_container_stack.extruders.items(): for position, extruder in self._global_container_stack.extruders.items():
@ -1310,17 +1360,18 @@ class MachineManager(QObject):
# Get the definition id corresponding to this machine name # Get the definition id corresponding to this machine name
machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId() machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()
# Try to find a machine with the same network key # Try to find a machine with the same network key
new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey}) new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey()})
# If there is no machine, then create a new one and set it to the non-hidden instance # If there is no machine, then create a new one and set it to the non-hidden instance
if not new_machine: if not new_machine:
new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id) new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id)
if not new_machine: if not new_machine:
return return
new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey) new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey())
new_machine.setMetaDataEntry("connect_group_name", self.activeMachineNetworkGroupName) new_machine.setMetaDataEntry("group_name", self.activeMachineNetworkGroupName)
new_machine.setMetaDataEntry("hidden", False) new_machine.setMetaDataEntry("hidden", False)
new_machine.setMetaDataEntry("connection_type", self._global_container_stack.getMetaDataEntry("connection_type"))
else: else:
Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey) Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey())
new_machine.setMetaDataEntry("hidden", False) new_machine.setMetaDataEntry("hidden", False)
# Set the current printer instance to hidden (the metadata entry must exist) # Set the current printer instance to hidden (the metadata entry must exist)
@ -1329,31 +1380,65 @@ class MachineManager(QObject):
self.setActiveMachine(new_machine.getId()) self.setActiveMachine(new_machine.getId())
@pyqtSlot(QObject) @pyqtSlot(QObject)
def applyRemoteConfiguration(self, configuration: ConfigurationModel) -> None: def applyRemoteConfiguration(self, configuration: PrinterConfigurationModel) -> None:
if self._global_container_stack is None: if self._global_container_stack is None:
return return
self.blurSettings.emit() self.blurSettings.emit()
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self.switchPrinterType(configuration.printerType) self.switchPrinterType(configuration.printerType)
disabled_used_extruder_position_set = set()
extruders_to_disable = set()
# If an extruder that's currently used to print a model gets disabled due to the syncing, we need to show
# a message explaining why.
need_to_show_message = False
for extruder_configuration in configuration.extruderConfigurations:
# We support "" or None, since the cloud uses None instead of empty strings
extruder_has_hotend = extruder_configuration.hotendID and extruder_configuration.hotendID != ""
extruder_has_material = extruder_configuration.material.guid and extruder_configuration.material.guid != ""
# If the machine doesn't have a hotend or material, disable this extruder
if not extruder_has_hotend or not extruder_has_material:
extruders_to_disable.add(extruder_configuration.position)
# If there's no material and/or nozzle on the printer, enable the first extruder and disable the rest.
if len(extruders_to_disable) == len(self._global_container_stack.extruders):
extruders_to_disable.remove(min(extruders_to_disable))
for extruder_configuration in configuration.extruderConfigurations: for extruder_configuration in configuration.extruderConfigurations:
position = str(extruder_configuration.position) position = str(extruder_configuration.position)
variant_container_node = self._variant_manager.getVariantNode(self._global_container_stack.definition.getId(), extruder_configuration.hotendID)
material_container_node = self._material_manager.getMaterialNodeByType(self._global_container_stack,
position,
extruder_configuration.hotendID,
configuration.buildplateConfiguration,
extruder_configuration.material.guid)
if variant_container_node: # If the machine doesn't have a hotend or material, disable this extruder
self._setVariantNode(position, variant_container_node) if int(position) in extruders_to_disable:
else: self._global_container_stack.extruders[position].setEnabled(False)
self._global_container_stack.extruders[position].variant = empty_variant_container
need_to_show_message = True
disabled_used_extruder_position_set.add(int(position))
if material_container_node:
self._setMaterial(position, material_container_node)
else: else:
self._global_container_stack.extruders[position].material = empty_material_container variant_container_node = self._variant_manager.getVariantNode(self._global_container_stack.definition.getId(),
self.updateMaterialWithVariant(position) extruder_configuration.hotendID)
material_container_node = self._material_manager.getMaterialNodeByType(self._global_container_stack,
position,
extruder_configuration.hotendID,
configuration.buildplateConfiguration,
extruder_configuration.material.guid)
if variant_container_node:
self._setVariantNode(position, variant_container_node)
else:
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 = empty_material_container
self._global_container_stack.extruders[position].setEnabled(True)
self.updateMaterialWithVariant(position)
self.updateDefaultExtruder()
self.updateNumberExtrudersEnabled()
if configuration.buildplateConfiguration is not None: if configuration.buildplateConfiguration is not None:
global_variant_container_node = self._variant_manager.getBuildplateVariantNode(self._global_container_stack.definition.getId(), configuration.buildplateConfiguration) global_variant_container_node = self._variant_manager.getBuildplateVariantNode(self._global_container_stack.definition.getId(), configuration.buildplateConfiguration)
@ -1365,35 +1450,25 @@ class MachineManager(QObject):
self._global_container_stack.variant = empty_variant_container self._global_container_stack.variant = empty_variant_container
self._updateQualityWithMaterial() self._updateQualityWithMaterial()
if need_to_show_message:
msg_str = "{extruders} is disabled because there is no material loaded. Please load a material or use custom configurations."
# Show human-readable extruder names such as "Extruder Left", "Extruder Front" instead of "Extruder 1, 2, 3".
extruder_names = []
for extruder_position in sorted(disabled_used_extruder_position_set):
extruder_stack = self._global_container_stack.extruders[str(extruder_position)]
extruder_name = extruder_stack.definition.getName()
extruder_names.append(extruder_name)
extruders_str = ", ".join(extruder_names)
msg_str = msg_str.format(extruders = extruders_str)
message = Message(catalog.i18nc("@info:status", msg_str),
title = catalog.i18nc("@info:title", "Extruder(s) Disabled"))
message.show()
# See if we need to show the Discard or Keep changes screen # See if we need to show the Discard or Keep changes screen
if self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1: if self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1:
self._application.discardOrKeepProfileChanges() self._application.discardOrKeepProfileChanges()
## Find all container stacks that has the pair 'key = value' in its metadata and replaces the value with 'new_value'
def replaceContainersMetadata(self, key: str, value: str, new_value: str) -> None:
machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine")
for machine in machines:
if machine.getMetaDataEntry(key) == value:
machine.setMetaDataEntry(key, new_value)
## This method checks if the name of the group stored in the definition container is correct.
# After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group
# then all the container stacks are updated, both the current and the hidden ones.
def checkCorrectGroupName(self, device_id: str, group_name: str) -> None:
if self._global_container_stack and device_id == self.activeMachineNetworkKey:
# Check if the connect_group_name is correct. If not, update all the containers connected to the same printer
if self.activeMachineNetworkGroupName != group_name:
metadata_filter = {"um_network_key": self.activeMachineNetworkKey}
containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
for container in containers:
container.setMetaDataEntry("connect_group_name", group_name)
## This method checks if there is an instance connected to the given network_key
def existNetworkInstances(self, network_key: str) -> bool:
metadata_filter = {"um_network_key": network_key}
containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
return bool(containers)
@pyqtSlot("QVariant") @pyqtSlot("QVariant")
def setGlobalVariant(self, container_node: "ContainerNode") -> None: def setGlobalVariant(self, container_node: "ContainerNode") -> None:
self.blurSettings.emit() self.blurSettings.emit()
@ -1419,7 +1494,7 @@ class MachineManager(QObject):
material_diameter, root_material_id) material_diameter, root_material_id)
self.setMaterial(position, material_node) self.setMaterial(position, material_node)
## global_stack: if you want to provide your own global_stack instead of the current active one ## Global_stack: if you want to provide your own global_stack instead of the current active one
# if you update an active machine, special measures have to be taken. # if you update an active machine, special measures have to be taken.
@pyqtSlot(str, "QVariant") @pyqtSlot(str, "QVariant")
def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None: def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None:
@ -1522,6 +1597,10 @@ class MachineManager(QObject):
def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]: def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]:
return self._current_quality_changes_group return self._current_quality_changes_group
@pyqtProperty(bool, notify = activeQualityChangesGroupChanged)
def hasCustomQuality(self) -> bool:
return self._current_quality_changes_group is not None
@pyqtProperty(str, notify = activeQualityGroupChanged) @pyqtProperty(str, notify = activeQualityGroupChanged)
def activeQualityOrQualityChangesName(self) -> str: def activeQualityOrQualityChangesName(self) -> str:
name = empty_quality_container.getName() name = empty_quality_container.getName()
@ -1531,9 +1610,40 @@ class MachineManager(QObject):
name = self._current_quality_group.name name = self._current_quality_group.name
return name return name
@pyqtProperty(bool, notify = activeQualityGroupChanged)
def hasNotSupportedQuality(self) -> bool:
return self._current_quality_group is None and self._current_quality_changes_group is None
def _updateUponMaterialMetadataChange(self) -> None: def _updateUponMaterialMetadataChange(self) -> None:
if self._global_container_stack is None: if self._global_container_stack is None:
return return
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue): with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self.updateMaterialWithVariant(None) self.updateMaterialWithVariant(None)
self._updateQualityWithMaterial() self._updateQualityWithMaterial()
## This function will translate any printer type name to an abbreviated printer type name
@pyqtSlot(str, result = str)
def getAbbreviatedMachineName(self, machine_type_name: str) -> str:
abbr_machine = ""
for word in re.findall(r"[\w']+", machine_type_name):
if word.lower() == "ultimaker":
abbr_machine += "UM"
elif word.isdigit():
abbr_machine += word
else:
stripped_word = "".join(char for char in unicodedata.normalize("NFD", word.upper()) if unicodedata.category(char) != "Mn")
# - use only the first character if the word is too long (> 3 characters)
# - use the whole word if it's not too long (<= 3 characters)
if len(stripped_word) > 3:
stripped_word = stripped_word[0]
abbr_machine += stripped_word
return abbr_machine
@pyqtSlot(str, result = str)
def getMachineTypeNameFromId(self, machine_type_id: str) -> str:
machine_type_name = ""
results = self._container_registry.findDefinitionContainersMetadata(id = machine_type_id)
if results:
machine_type_name = results[0]["name"]
return machine_type_name

View File

@ -34,7 +34,7 @@ class PerObjectContainerStack(CuraContainerStack):
if limit_to_extruder is not None: if limit_to_extruder is not None:
limit_to_extruder = str(limit_to_extruder) limit_to_extruder = str(limit_to_extruder)
# if this stack has the limit_to_extruder "not overriden", use the original limit_to_extruder as the current # if this stack has the limit_to_extruder "not overridden", use the original limit_to_extruder as the current
# limit_to_extruder, so the values retrieved will be from the perspective of the original limit_to_extruder # limit_to_extruder, so the values retrieved will be from the perspective of the original limit_to_extruder
# stack. # stack.
if limit_to_extruder == "-1": if limit_to_extruder == "-1":
@ -42,7 +42,7 @@ class PerObjectContainerStack(CuraContainerStack):
limit_to_extruder = context.context["original_limit_to_extruder"] limit_to_extruder = context.context["original_limit_to_extruder"]
if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in global_stack.extruders: if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in global_stack.extruders:
# set the original limit_to_extruder if this is the first stack that has a non-overriden limit_to_extruder # set the original limit_to_extruder if this is the first stack that has a non-overridden limit_to_extruder
if "original_limit_to_extruder" not in context.context: if "original_limit_to_extruder" not in context.context:
context.context["original_limit_to_extruder"] = limit_to_extruder context.context["original_limit_to_extruder"] = limit_to_extruder

View File

@ -73,8 +73,8 @@ class SettingOverrideDecorator(SceneNodeDecorator):
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh" # use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
# has not been updated yet. # has not been updated yet.
deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh() deep_copy._is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
deep_copy._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh() deep_copy._is_non_thumbnail_visible_mesh = self._evaluateIsNonThumbnailVisibleMesh()
return deep_copy return deep_copy
@ -102,23 +102,32 @@ class SettingOverrideDecorator(SceneNodeDecorator):
def isNonPrintingMesh(self): def isNonPrintingMesh(self):
return self._is_non_printing_mesh return self._is_non_printing_mesh
def evaluateIsNonPrintingMesh(self): def _evaluateIsNonPrintingMesh(self):
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings) return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
def isNonThumbnailVisibleMesh(self): def isNonThumbnailVisibleMesh(self):
return self._is_non_thumbnail_visible_mesh return self._is_non_thumbnail_visible_mesh
def evaluateIsNonThumbnailVisibleMesh(self): def _evaluateIsNonThumbnailVisibleMesh(self):
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_thumbnail_visible_settings) return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_thumbnail_visible_settings)
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function def _onSettingChanged(self, setting_key, property_name): # Reminder: 'property' is a built-in function
if property_name == "value": # We're only interested in a few settings and only if it's value changed.
if property_name == "value" and (setting_key in self._non_printing_mesh_settings or setting_key in self._non_thumbnail_visible_settings):
# Trigger slice/need slicing if the value has changed. # Trigger slice/need slicing if the value has changed.
self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh() new_is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
self._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh() new_is_non_thumbnail_visible_mesh = self._evaluateIsNonThumbnailVisibleMesh()
changed = False
if self._is_non_printing_mesh != new_is_non_printing_mesh:
self._is_non_printing_mesh = new_is_non_printing_mesh
self._node.setCalculateBoundingBox(not self._is_non_printing_mesh)
changed = True
if self._is_non_thumbnail_visible_mesh != new_is_non_thumbnail_visible_mesh:
changed = True
Application.getInstance().getBackend().needsSlicing() if changed:
Application.getInstance().getBackend().tickle() Application.getInstance().getBackend().needsSlicing()
Application.getInstance().getBackend().tickle()
## Makes sure that the stack upon which the container stack is placed is ## Makes sure that the stack upon which the container stack is placed is
# kept up to date. # kept up to date.

View File

@ -1,7 +1,8 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from typing import Set
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
from UM.Application import Application from UM.Application import Application
@ -16,15 +17,11 @@ class SimpleModeSettingsManager(QObject):
self._is_profile_user_created = False # True when profile was custom created by user self._is_profile_user_created = False # True when profile was custom created by user
self._machine_manager.activeStackValueChanged.connect(self._updateIsProfileCustomized) self._machine_manager.activeStackValueChanged.connect(self._updateIsProfileCustomized)
self._machine_manager.activeQualityGroupChanged.connect(self._updateIsProfileUserCreated)
self._machine_manager.activeQualityChangesGroupChanged.connect(self._updateIsProfileUserCreated)
# update on create as the activeQualityChanged signal is emitted before this manager is created when Cura starts # update on create as the activeQualityChanged signal is emitted before this manager is created when Cura starts
self._updateIsProfileCustomized() self._updateIsProfileCustomized()
self._updateIsProfileUserCreated()
isProfileCustomizedChanged = pyqtSignal() isProfileCustomizedChanged = pyqtSignal()
isProfileUserCreatedChanged = pyqtSignal()
@pyqtProperty(bool, notify = isProfileCustomizedChanged) @pyqtProperty(bool, notify = isProfileCustomizedChanged)
def isProfileCustomized(self): def isProfileCustomized(self):
@ -57,33 +54,6 @@ class SimpleModeSettingsManager(QObject):
self._is_profile_customized = has_customized_user_settings self._is_profile_customized = has_customized_user_settings
self.isProfileCustomizedChanged.emit() self.isProfileCustomizedChanged.emit()
@pyqtProperty(bool, notify = isProfileUserCreatedChanged)
def isProfileUserCreated(self):
return self._is_profile_user_created
def _updateIsProfileUserCreated(self):
quality_changes_keys = set()
if not self._machine_manager.activeMachine:
return False
global_stack = self._machine_manager.activeMachine
# check quality changes settings in the global stack
quality_changes_keys.update(global_stack.qualityChanges.getAllKeys())
# check quality changes settings in the extruder stacks
if global_stack.extruders:
for extruder_stack in global_stack.extruders.values():
quality_changes_keys.update(extruder_stack.qualityChanges.getAllKeys())
# check if the qualityChanges container is not empty (meaning it is a user created profile)
has_quality_changes = len(quality_changes_keys) > 0
if has_quality_changes != self._is_profile_user_created:
self._is_profile_user_created = has_quality_changes
self.isProfileUserCreatedChanged.emit()
# These are the settings included in the Simple ("Recommended") Mode, so only when the other settings have been # These are the settings included in the Simple ("Recommended") Mode, so only when the other settings have been
# changed, we consider it as a user customized profile in the Simple ("Recommended") Mode. # changed, we consider it as a user customized profile in the Simple ("Recommended") Mode.
__ignored_custom_setting_keys = ["support_enable", __ignored_custom_setting_keys = ["support_enable",

View File

@ -41,6 +41,22 @@ empty_quality_changes_container.setMetaDataEntry("type", "quality_changes")
empty_quality_changes_container.setMetaDataEntry("quality_type", "not_supported") empty_quality_changes_container.setMetaDataEntry("quality_type", "not_supported")
# All empty container IDs set
ALL_EMPTY_CONTAINER_ID_SET = {
EMPTY_CONTAINER_ID,
EMPTY_DEFINITION_CHANGES_CONTAINER_ID,
EMPTY_VARIANT_CONTAINER_ID,
EMPTY_MATERIAL_CONTAINER_ID,
EMPTY_QUALITY_CONTAINER_ID,
EMPTY_QUALITY_CHANGES_CONTAINER_ID,
}
# Convenience function to check if a container ID represents an empty container.
def isEmptyContainer(container_id: str) -> bool:
return container_id in ALL_EMPTY_CONTAINER_ID_SET
__all__ = ["EMPTY_CONTAINER_ID", __all__ = ["EMPTY_CONTAINER_ID",
"empty_container", # For convenience "empty_container", # For convenience
"EMPTY_DEFINITION_CHANGES_CONTAINER_ID", "EMPTY_DEFINITION_CHANGES_CONTAINER_ID",
@ -52,5 +68,7 @@ __all__ = ["EMPTY_CONTAINER_ID",
"EMPTY_QUALITY_CHANGES_CONTAINER_ID", "EMPTY_QUALITY_CHANGES_CONTAINER_ID",
"empty_quality_changes_container", "empty_quality_changes_container",
"EMPTY_QUALITY_CONTAINER_ID", "EMPTY_QUALITY_CONTAINER_ID",
"empty_quality_container" "empty_quality_container",
"ALL_EMPTY_CONTAINER_ID_SET",
"isEmptyContainer",
] ]

View File

@ -1,23 +1,32 @@
# Copyright (c) 2017 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, QUrl
from PyQt5.QtCore import pyqtProperty, QUrl
from UM.Stage import Stage
from UM.Stage import Stage
class CuraStage(Stage):
# Since Cura has a few pre-defined "space claims" for the locations of certain components, we've provided some structure
def __init__(self, parent = None): # to indicate this.
super().__init__(parent) # * The StageMenuComponent is the horizontal area below the stage bar. This should be used to show stage specific
# buttons and elements. This component will be drawn over the bar & main component.
@pyqtProperty(str, constant = True) # * The MainComponent is the component that will be drawn starting from the bottom of the stageBar and fills the rest
def stageId(self): # of the screen.
return self.getPluginId() class CuraStage(Stage):
def __init__(self, parent = None) -> None:
@pyqtProperty(QUrl, constant = True) super().__init__(parent)
def mainComponent(self):
return self.getDisplayComponent("main") @pyqtProperty(str, constant = True)
def stageId(self) -> str:
@pyqtProperty(QUrl, constant = True) return self.getPluginId()
def sidebarComponent(self):
return self.getDisplayComponent("sidebar") @pyqtProperty(QUrl, constant = True)
def mainComponent(self) -> QUrl:
return self.getDisplayComponent("main")
@pyqtProperty(QUrl, constant = True)
def stageMenuComponent(self) -> QUrl:
return self.getDisplayComponent("menu")
__all__ = ["CuraStage"]

View File

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

View File

@ -0,0 +1,30 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from .WelcomePagesModel import WelcomePagesModel
#
# This Qt ListModel is more or less the same the WelcomePagesModel, except that this model is only for adding a printer,
# so only the steps for adding a printer is included.
#
class AddPrinterPagesModel(WelcomePagesModel):
def initialize(self) -> None:
self._pages.append({"id": "add_network_or_local_printer",
"page_url": self._getBuiltinWelcomePagePath("AddNetworkOrLocalPrinterContent.qml"),
"next_page_id": "machine_actions",
"next_page_button_text": self._catalog.i18nc("@action:button", "Add"),
})
self._pages.append({"id": "add_printer_by_ip",
"page_url": self._getBuiltinWelcomePagePath("AddPrinterByIpContent.qml"),
"next_page_id": "machine_actions",
})
self._pages.append({"id": "machine_actions",
"page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"),
"should_show_function": self.shouldShowMachineActions,
})
self.setItems(self._pages)
__all__ = ["AddPrinterPagesModel"]

View File

@ -12,7 +12,7 @@ from UM.PluginRegistry import PluginRegistry # So MachineAction can be added as
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
from cura.Settings.GlobalStack import GlobalStack from cura.Settings.GlobalStack import GlobalStack
from .MachineAction import MachineAction from cura.MachineAction import MachineAction
## Raised when trying to add an unknown machine action as a required action ## Raised when trying to add an unknown machine action as a required action
@ -136,7 +136,7 @@ class MachineActionManager(QObject):
# action multiple times). # action multiple times).
# \param definition_id The ID of the definition that you want to get the "on added" actions for. # \param definition_id The ID of the definition that you want to get the "on added" actions for.
# \returns List of actions. # \returns List of actions.
@pyqtSlot(str, result="QVariantList") @pyqtSlot(str, result = "QVariantList")
def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]: def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]:
if definition_id in self._first_start_actions: if definition_id in self._first_start_actions:
return self._first_start_actions[definition_id] return self._first_start_actions[definition_id]

View File

@ -0,0 +1,82 @@
# Copyright (c) 2019 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, pyqtSlot
from UM.i18n import i18nCatalog
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
#
# This manager provides (convenience) functions to the Machine Settings Dialog QML to update certain machine settings.
#
class MachineSettingsManager(QObject):
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._i18n_catalog = i18nCatalog("cura")
self._application = application
# Force rebuilding the build volume by reloading the global container stack. This is a bit of a hack, but it seems
# quite enough.
@pyqtSlot()
def forceUpdate(self) -> None:
self._application.getMachineManager().globalContainerChanged.emit()
# Function for the Machine Settings panel (QML) to update the compatible material diameter after a user has changed
# an extruder's compatible material diameter. This ensures that after the modification, changes can be notified
# and updated right away.
@pyqtSlot(int)
def updateMaterialForDiameter(self, extruder_position: int) -> None:
# Updates the material container to a material that matches the material diameter set for the printer
self._application.getMachineManager().updateMaterialWithVariant(str(extruder_position))
@pyqtSlot(int)
def setMachineExtruderCount(self, extruder_count: int) -> None:
# Note: this method was in this class before, but since it's quite generic and other plugins also need it
# it was moved to the machine manager instead. Now this method just calls the machine manager.
self._application.getMachineManager().setActiveMachineExtruderCount(extruder_count)
# Function for the Machine Settings panel (QML) to update after the usre changes "Number of Extruders".
#
# fieldOfView: The Ultimaker 2 family (not 2+) does not have materials in Cura by default, because the material is
# to be set on the printer. But when switching to Marlin flavor, the printer firmware can not change/insert material
# settings on the fly so they need to be configured in Cura. So when switching between gcode flavors, materials may
# need to be enabled/disabled.
@pyqtSlot()
def updateHasMaterialsMetadata(self):
machine_manager = self._application.getMachineManager()
material_manager = self._application.getMaterialManager()
global_stack = machine_manager.activeMachine
definition = global_stack.definition
if definition.getProperty("machine_gcode_flavor", "value") != "UltiGCode" or definition.getMetaDataEntry(
"has_materials", False):
# In other words: only continue for the UM2 (extended), but not for the UM2+
return
extruder_positions = list(global_stack.extruders.keys())
has_materials = global_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
material_node = None
if has_materials:
global_stack.setMetaDataEntry("has_materials", True)
else:
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
if "has_materials" in global_stack.getMetaData():
global_stack.removeMetaDataEntry("has_materials")
# set materials
for position in extruder_positions:
if has_materials:
material_node = material_manager.getDefaultMaterial(global_stack, position, None)
machine_manager.setMaterial(position, material_node)
self.forceUpdate()

View File

@ -1,54 +1,64 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from collections import defaultdict
from typing import Dict
from PyQt5.QtCore import QTimer from PyQt5.QtCore import QTimer
from UM.Application import Application from UM.Application import Application
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.Scene.Camera import Camera
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection from UM.Scene.Selection import Selection
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from collections import defaultdict
catalog = i18nCatalog("cura") catalog = i18nCatalog("cura")
## Keep track of all objects in the project ## Keep track of all objects in the project
class ObjectsModel(ListModel): class ObjectsModel(ListModel):
def __init__(self): def __init__(self) -> None:
super().__init__() super().__init__()
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateDelayed) Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSceneDelayed)
Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed) Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed)
self._update_timer = QTimer() self._update_timer = QTimer()
self._update_timer.setInterval(100) self._update_timer.setInterval(200)
self._update_timer.setSingleShot(True) self._update_timer.setSingleShot(True)
self._update_timer.timeout.connect(self._update) self._update_timer.timeout.connect(self._update)
self._build_plate_number = -1 self._build_plate_number = -1
def setActiveBuildPlate(self, nr): def setActiveBuildPlate(self, nr: int) -> None:
self._build_plate_number = nr if self._build_plate_number != nr:
self._update() self._build_plate_number = nr
self._update()
def _updateDelayed(self, *args): def _updateSceneDelayed(self, source) -> None:
if not isinstance(source, Camera):
self._update_timer.start()
def _updateDelayed(self, *args) -> None:
self._update_timer.start() self._update_timer.start()
def _update(self, *args): def _update(self, *args) -> None:
nodes = [] nodes = []
filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate") filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate")
active_build_plate_number = self._build_plate_number active_build_plate_number = self._build_plate_number
group_nr = 1 group_nr = 1
name_count_dict = defaultdict(int) name_count_dict = defaultdict(int) # type: Dict[str, int]
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): # type: ignore
if not isinstance(node, SceneNode): if not isinstance(node, SceneNode):
continue continue
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"): if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
continue continue
if node.getParent() and node.getParent().callDecoration("isGroup"):
parent = node.getParent()
if parent and parent.callDecoration("isGroup"):
continue # Grouped nodes don't need resetting as their parent (the group) is resetted) continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"): if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue continue
@ -64,7 +74,7 @@ class ObjectsModel(ListModel):
group_nr += 1 group_nr += 1
if hasattr(node, "isOutsideBuildArea"): if hasattr(node, "isOutsideBuildArea"):
is_outside_build_area = node.isOutsideBuildArea() is_outside_build_area = node.isOutsideBuildArea() # type: ignore
else: else:
is_outside_build_area = False is_outside_build_area = False

View File

@ -5,8 +5,7 @@ import json
import math import math
import os import os
import unicodedata import unicodedata
import re # To create abbreviations for printer names. from typing import Dict, List, Optional, TYPE_CHECKING
from typing import Dict, List, Optional
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
@ -14,10 +13,7 @@ from UM.Logger import Logger
from UM.Qt.Duration import Duration from UM.Qt.Duration import Duration
from UM.Scene.SceneNode import SceneNode from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog from UM.i18n import i18nCatalog
from UM.MimeTypeDatabase import MimeTypeDatabase from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
from typing import TYPE_CHECKING
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication from cura.CuraApplication import CuraApplication
@ -70,10 +66,8 @@ class PrintInformation(QObject):
self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged) self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged) self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
self._onActiveMaterialsChanged()
self._material_amounts = [] # type: List[float] self._material_amounts = [] # type: List[float]
self._onActiveMaterialsChanged()
def initializeCuraMessagePrintTimeProperties(self) -> None: def initializeCuraMessagePrintTimeProperties(self) -> None:
self._current_print_time = {} # type: Dict[int, Duration] self._current_print_time = {} # type: Dict[int, Duration]
@ -221,6 +215,7 @@ class PrintInformation(QObject):
material_guid = material.getMetaDataEntry("GUID") material_guid = material.getMetaDataEntry("GUID")
material_name = material.getName() material_name = material.getName()
if material_guid in material_preference_values: if material_guid in material_preference_values:
material_values = material_preference_values[material_guid] material_values = material_preference_values[material_guid]
@ -361,7 +356,7 @@ class PrintInformation(QObject):
try: try:
mime_type = MimeTypeDatabase.getMimeTypeForFile(name) mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
data = mime_type.stripExtension(name) data = mime_type.stripExtension(name)
except: except MimeTypeNotFoundError:
Logger.log("w", "Unsupported Mime Type Database file extension %s", name) Logger.log("w", "Unsupported Mime Type Database file extension %s", name)
if data is not None and check_name is not None: if data is not None and check_name is not None:
@ -369,6 +364,16 @@ class PrintInformation(QObject):
else: else:
self._base_name = "" 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() self._updateJobName()
@pyqtProperty(str, fset = setBaseName, notify = baseNameChanged) @pyqtProperty(str, fset = setBaseName, notify = baseNameChanged)
@ -385,28 +390,14 @@ class PrintInformation(QObject):
return return
active_machine_type_name = global_container_stack.definition.getName() active_machine_type_name = global_container_stack.definition.getName()
abbr_machine = "" self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name)
for word in re.findall(r"[\w']+", active_machine_type_name):
if word.lower() == "ultimaker":
abbr_machine += "UM"
elif word.isdigit():
abbr_machine += word
else:
stripped_word = self._stripAccents(word.upper())
# - use only the first character if the word is too long (> 3 characters)
# - use the whole word if it's not too long (<= 3 characters)
if len(stripped_word) > 3:
stripped_word = stripped_word[0]
abbr_machine += stripped_word
self._abbr_machine = abbr_machine
## Utility method that strips accents from characters (eg: â -> a) ## Utility method that strips accents from characters (eg: â -> a)
def _stripAccents(self, to_strip: str) -> str: def _stripAccents(self, to_strip: str) -> str:
return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn') return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')
@pyqtSlot(result = "QVariantMap") @pyqtSlot(result = "QVariantMap")
def getFeaturePrintTimes(self): def getFeaturePrintTimes(self) -> Dict[str, Duration]:
result = {} result = {}
if self._active_build_plate not in self._print_times_per_feature: if self._active_build_plate not in self._print_times_per_feature:
self._initPrintTimesPerFeature(self._active_build_plate) self._initPrintTimesPerFeature(self._active_build_plate)

69
cura/UI/TextManager.py Normal file
View File

@ -0,0 +1,69 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import collections
from typing import Optional, Dict, List, cast
from PyQt5.QtCore import QObject, pyqtSlot
from UM.Resources import Resources
from UM.Version import Version
#
# This manager provides means to load texts to QML.
#
class TextManager(QObject):
def __init__(self, parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._change_log_text = ""
@pyqtSlot(result = str)
def getChangeLogText(self) -> str:
if not self._change_log_text:
self._change_log_text = self._loadChangeLogText()
return self._change_log_text
def _loadChangeLogText(self) -> str:
# Load change log texts and organize them with a dict
file_path = Resources.getPath(Resources.Texts, "change_log.txt")
change_logs_dict = {} # type: Dict[Version, Dict[str, List[str]]]
with open(file_path, "r", encoding = "utf-8") as f:
open_version = None # type: Optional[Version]
open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog
for line in f:
line = line.replace("\n", "")
if "[" in line and "]" in line:
line = line.replace("[", "")
line = line.replace("]", "")
open_version = Version(line)
if open_version > Version([14, 99, 99]): # Bit of a hack: We released the 15.x.x versions before 2.x
open_version = Version([0, open_version.getMinor(), open_version.getRevision(), open_version.getPostfixVersion()])
open_header = ""
change_logs_dict[open_version] = collections.OrderedDict()
elif line.startswith("*"):
open_header = line.replace("*", "")
change_logs_dict[cast(Version, open_version)][open_header] = []
elif line != "":
if open_header not in change_logs_dict[cast(Version, open_version)]:
change_logs_dict[cast(Version, open_version)][open_header] = []
change_logs_dict[cast(Version, open_version)][open_header].append(line)
# Format changelog text
content = ""
for version in sorted(change_logs_dict.keys(), reverse = True):
text_version = version
if version < Version([1, 0, 0]): # Bit of a hack: We released the 15.x.x versions before 2.x
text_version = Version([15, version.getMinor(), version.getRevision(), version.getPostfixVersion()])
content += "<h1>" + str(text_version) + "</h1><br>"
content += ""
for change in change_logs_dict[version]:
if str(change) != "":
content += "<b>" + str(change) + "</b><br>"
for line in change_logs_dict[version][change]:
content += str(line) + "<br>"
content += "<br>"
return content

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