mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-09-24 13:53:14 +08:00
Merge branch 'master' into feature_z_hop_extruder_switch
This commit is contained in:
commit
fec0272c5f
2
.gitignore
vendored
2
.gitignore
vendored
@ -42,7 +42,6 @@ plugins/cura-siemensnx-plugin
|
||||
plugins/CuraBlenderPlugin
|
||||
plugins/CuraCloudPlugin
|
||||
plugins/CuraDrivePlugin
|
||||
plugins/CuraDrive
|
||||
plugins/CuraLiveScriptingPlugin
|
||||
plugins/CuraOpenSCADPlugin
|
||||
plugins/CuraPrintProfileCreator
|
||||
@ -72,3 +71,4 @@ run.sh
|
||||
.scannerwork/
|
||||
CuraEngine
|
||||
|
||||
/.coverage
|
||||
|
12
.gitlab-ci.yml
Normal file
12
.gitlab-ci.yml
Normal 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
|
@ -1,11 +1,10 @@
|
||||
project(cura NONE)
|
||||
cmake_minimum_required(VERSION 2.8.12)
|
||||
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/
|
||||
${CMAKE_MODULE_PATH})
|
||||
project(cura)
|
||||
cmake_minimum_required(VERSION 3.6)
|
||||
|
||||
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_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")
|
||||
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_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
|
||||
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(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 "")
|
||||
set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake")
|
||||
endif()
|
||||
@ -38,12 +59,12 @@ if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
|
||||
CREATE_TRANSLATION_TARGETS()
|
||||
endif()
|
||||
|
||||
find_package(PythonInterp 3.5.0 REQUIRED)
|
||||
|
||||
install(DIRECTORY resources
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
|
||||
install(DIRECTORY plugins
|
||||
DESTINATION lib${LIB_SUFFIX}/cura)
|
||||
|
||||
if(NOT APPLE AND NOT WIN32)
|
||||
install(FILES cura_app.py
|
||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
@ -51,16 +72,16 @@ if(NOT APPLE AND NOT WIN32)
|
||||
RENAME cura)
|
||||
if(EXISTS /etc/debian_version)
|
||||
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)
|
||||
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()
|
||||
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)
|
||||
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()
|
||||
install(FILES ${CMAKE_BINARY_DIR}/cura.desktop
|
||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||
@ -76,8 +97,8 @@ else()
|
||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
|
||||
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)
|
||||
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()
|
||||
|
43
Jenkinsfile
vendored
43
Jenkinsfile
vendored
@ -38,20 +38,9 @@ parallel_nodes(['linux && cura', 'windows && cura'])
|
||||
{
|
||||
if (isUnix())
|
||||
{
|
||||
// For Linux to show everything
|
||||
def branch = env.BRANCH_NAME
|
||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
|
||||
{
|
||||
branch = "master"
|
||||
}
|
||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
|
||||
|
||||
// For Linux
|
||||
try {
|
||||
sh """
|
||||
cd ..
|
||||
export PYTHONPATH=.:"${uranium_dir}"
|
||||
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/pytest -x --verbose --full-trace --capture=no ./tests
|
||||
"""
|
||||
sh 'make CTEST_OUTPUT_ON_FAILURE=TRUE test'
|
||||
} catch(e)
|
||||
{
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,8 +20,9 @@ Dependencies
|
||||
------------
|
||||
* [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.
|
||||
* [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.
|
||||
* [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
|
||||
-------------
|
||||
|
@ -1,10 +1,23 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
enable_testing()
|
||||
include(CTest)
|
||||
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)
|
||||
set(_single_args NAME DIRECTORY PYTHONPATH)
|
||||
@ -34,7 +47,7 @@ function(cura_add_test)
|
||||
if (NOT ${test_exists})
|
||||
add_test(
|
||||
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 "PYTHONPATH=${_PYTHONPATH}")
|
||||
@ -57,13 +70,13 @@ endforeach()
|
||||
#Add code style test.
|
||||
add_test(
|
||||
NAME "code-style"
|
||||
COMMAND ${PYTHON_EXECUTABLE} run_mypy.py
|
||||
COMMAND ${Python3_EXECUTABLE} run_mypy.py
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
||||
|
||||
#Add test for whether the shortcut alt-keys are unique in every translation.
|
||||
add_test(
|
||||
NAME "shortcut-keys"
|
||||
COMMAND ${PYTHON_EXECUTABLE} scripts/check_shortcut_keys.py
|
||||
COMMAND ${Python3_EXECUTABLE} scripts/check_shortcut_keys.py
|
||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||
)
|
19
contributing.md
Normal file
19
contributing.md
Normal 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`.
|
@ -13,6 +13,6 @@ TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
|
||||
Icon=cura-icon
|
||||
Terminal=false
|
||||
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;
|
||||
Keywords=3D;Printing;Slicer;
|
||||
|
@ -19,4 +19,12 @@
|
||||
<glob-deleteall/>
|
||||
<glob pattern="*.obj"/>
|
||||
</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>
|
@ -6,6 +6,7 @@ from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Message import Message
|
||||
from cura import UltimakerCloudAuthentication
|
||||
|
||||
from cura.OAuth2.AuthorizationService import AuthorizationService
|
||||
from cura.OAuth2.Models import OAuth2Settings
|
||||
@ -37,15 +38,16 @@ class Account(QObject):
|
||||
self._logged_in = False
|
||||
|
||||
self._callback_port = 32118
|
||||
self._oauth_root = "https://account.ultimaker.com"
|
||||
self._cloud_api_root = "https://api.ultimaker.com"
|
||||
self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
|
||||
|
||||
self._oauth_settings = OAuth2Settings(
|
||||
OAUTH_SERVER_URL= self._oauth_root,
|
||||
CALLBACK_PORT=self._callback_port,
|
||||
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
|
||||
CLIENT_ID="um---------------ultimaker_cura_drive_plugin",
|
||||
CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.write",
|
||||
CLIENT_ID="um----------------------------ultimaker_cura",
|
||||
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_SUCCESS_REDIRECT="{}/app/auth-success".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:
|
||||
self._authorization_service.initialize(self._application.getPreferences())
|
||||
|
||||
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
|
||||
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
|
||||
self._authorization_service.loadAuthDataFromPreferences()
|
||||
|
||||
## 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)
|
||||
def isLoggedIn(self) -> bool:
|
||||
return self._logged_in
|
||||
@ -70,6 +76,9 @@ class Account(QObject):
|
||||
self._error_message.hide()
|
||||
self._error_message = Message(error_message, title = i18n_catalog.i18nc("@info:title", "Login failed"))
|
||||
self._error_message.show()
|
||||
self._logged_in = False
|
||||
self.loginStateChanged.emit(False)
|
||||
return
|
||||
|
||||
if self._logged_in != logged_in:
|
||||
self._logged_in = logged_in
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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
|
||||
|
||||
@ -24,12 +24,12 @@ class Backups:
|
||||
## Create a new back-up using the BackupsManager.
|
||||
# \return Tuple containing a ZIP file with the back-up data and a dict
|
||||
# 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()
|
||||
|
||||
## Restore a back-up using the BackupsManager.
|
||||
# \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
|
||||
# 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)
|
||||
|
@ -3,7 +3,6 @@
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.API.Interface.Settings import Settings
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@ -23,9 +22,6 @@ if TYPE_CHECKING:
|
||||
|
||||
class Interface:
|
||||
|
||||
# For now we use the same API version to be consistent.
|
||||
VERSION = PluginRegistry.APIVersion
|
||||
|
||||
def __init__(self, application: "CuraApplication") -> None:
|
||||
# API methods specific to the settings portion of the UI
|
||||
self.settings = Settings(application)
|
||||
|
@ -4,7 +4,6 @@ from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtProperty
|
||||
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from cura.API.Backups import Backups
|
||||
from cura.API.Interface import Interface
|
||||
from cura.API.Account import Account
|
||||
@ -22,7 +21,6 @@ if TYPE_CHECKING:
|
||||
class CuraAPI(QObject):
|
||||
|
||||
# For now we use the same API version to be consistent.
|
||||
VERSION = PluginRegistry.APIVersion
|
||||
__instance = None # type: "CuraAPI"
|
||||
_application = None # type: CuraApplication
|
||||
|
||||
|
50
cura/ApplicationMetadata.py
Normal file
50
cura/ApplicationMetadata.py
Normal 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
|
@ -66,6 +66,11 @@ class Arrange:
|
||||
continue
|
||||
vertices = vertices.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
|
||||
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)
|
||||
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[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
|
||||
def isEmpty(self):
|
||||
return self._is_empty
|
||||
|
@ -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.
|
||||
|
||||
from UM.Application import Application
|
||||
@ -48,7 +48,6 @@ class ArrangeArray:
|
||||
return self._count
|
||||
|
||||
def get(self, index):
|
||||
print(self._arrange)
|
||||
return self._arrange[index]
|
||||
|
||||
def getFirstEmpty(self):
|
||||
|
@ -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.
|
||||
|
||||
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)
|
||||
|
||||
# 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
|
||||
nodes_arr = [] # fill with (size, node, offset_shape_arr, hull_shape_arr)
|
||||
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:
|
||||
Logger.log("w", "Node [%s] could not be converted to an array for arranging...", str(node))
|
||||
continue
|
||||
|
@ -42,7 +42,7 @@ class ShapeArray:
|
||||
# \param min_offset offset for the offset ShapeArray
|
||||
# \param scale scale the coordinates
|
||||
@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_x = transform._data[0][3]
|
||||
transform_y = transform._data[2][3]
|
||||
@ -52,6 +52,21 @@ class ShapeArray:
|
||||
return None, None
|
||||
# For one_at_a_time printing you need the convex hull head.
|
||||
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_points = copy.deepcopy(offset_verts._points) # x, y
|
||||
|
@ -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.
|
||||
# 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 = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
|
||||
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
|
||||
Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
|
||||
shutil.copyfile(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)):
|
||||
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.
|
||||
buffer = io.BytesIO()
|
||||
@ -115,12 +116,13 @@ class Backup:
|
||||
|
||||
current_version = self._application.getVersion()
|
||||
version_to_restore = self.meta_data.get("cura_release", "master")
|
||||
if current_version != version_to_restore:
|
||||
# Cannot restore version older or newer than current because settings might have changed.
|
||||
# Restoring this will cause a lot of issues so we don't allow this for now.
|
||||
|
||||
if current_version < version_to_restore:
|
||||
# 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.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
|
||||
|
||||
version_data_dir = Resources.getDataStoragePath()
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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.Settings.ExtruderManager import ExtruderManager
|
||||
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"))
|
||||
|
||||
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._onStackChanged()
|
||||
|
||||
self._engine_ready = False
|
||||
@ -122,7 +129,9 @@ class BuildVolume(SceneNode):
|
||||
|
||||
def _onSceneChanged(self, source):
|
||||
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):
|
||||
root = self._application.getController().getScene().getRoot()
|
||||
@ -139,7 +148,7 @@ class BuildVolume(SceneNode):
|
||||
if active_extruder_changed is not None:
|
||||
node.callDecoration("getActiveExtruderChangedSignal").disconnect(self._updateDisallowedAreasAndRebuild)
|
||||
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._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)
|
||||
)
|
||||
|
||||
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds
|
||||
|
||||
self.updateNodeBoundaryCheck()
|
||||
|
||||
def getBoundingBox(self) -> AxisAlignedBox:
|
||||
@ -489,7 +500,9 @@ class BuildVolume(SceneNode):
|
||||
|
||||
def _updateRaftThickness(self):
|
||||
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
|
||||
if self._adhesion_type == "raft":
|
||||
self._raft_thickness = (
|
||||
@ -522,8 +535,11 @@ class BuildVolume(SceneNode):
|
||||
if extra_z != self._extra_z_clearance:
|
||||
self._extra_z_clearance = extra_z
|
||||
|
||||
## Update the build volume visualization
|
||||
def _onStackChanged(self):
|
||||
self._stack_change_timer.start()
|
||||
|
||||
## Update the build volume visualization
|
||||
def _onStackChangeTimerFinished(self):
|
||||
if self._global_container_stack:
|
||||
self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
||||
extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
@ -651,6 +667,7 @@ class BuildVolume(SceneNode):
|
||||
# ``_updateDisallowedAreas`` method itself shouldn't call ``rebuild``,
|
||||
# since there may be other changes before it needs to be rebuilt, which
|
||||
# would hit performance.
|
||||
|
||||
def _updateDisallowedAreasAndRebuild(self):
|
||||
self._updateDisallowedAreas()
|
||||
self._updateRaftThickness()
|
||||
@ -723,13 +740,17 @@ class BuildVolume(SceneNode):
|
||||
prime_tower_collision = False
|
||||
prime_tower_areas = self._computeDisallowedAreasPrinted(used_extruders)
|
||||
for extruder_id in prime_tower_areas:
|
||||
for prime_tower_area in prime_tower_areas[extruder_id]:
|
||||
for i_area, prime_tower_area in enumerate(prime_tower_areas[extruder_id]):
|
||||
for area in result_areas[extruder_id]:
|
||||
if prime_tower_area.intersectsPolygon(area) is not None:
|
||||
prime_tower_collision = True
|
||||
break
|
||||
if prime_tower_collision: #Already found a collision.
|
||||
break
|
||||
if (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:
|
||||
result_areas[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_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"):
|
||||
radius = prime_tower_size / 2
|
||||
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.
|
||||
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")
|
||||
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:
|
||||
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_enabled = self._global_container_stack.getProperty("support_enable", "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"]
|
||||
_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"]
|
||||
_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"]
|
||||
_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.
|
||||
|
@ -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)
|
@ -36,18 +36,14 @@ else:
|
||||
except ImportError:
|
||||
CuraDebugMode = False # [CodeStyle: Reflecting imported value]
|
||||
|
||||
# List of exceptions that should be considered "fatal" and abort the program.
|
||||
# These are primarily some exception types that we simply cannot really recover from
|
||||
# (MemoryError and SystemError) and exceptions that indicate grave errors in the
|
||||
# code that cause the Python interpreter to fail (SyntaxError, ImportError).
|
||||
fatal_exception_types = [
|
||||
MemoryError,
|
||||
SyntaxError,
|
||||
ImportError,
|
||||
SystemError,
|
||||
# List of exceptions that should not be considered "fatal" and abort the program.
|
||||
# These are primarily some exception types that we simply skip
|
||||
skip_exception_types = [
|
||||
SystemExit,
|
||||
KeyboardInterrupt,
|
||||
GeneratorExit
|
||||
]
|
||||
|
||||
|
||||
class CrashHandler:
|
||||
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 not fully started yet, we always show the early crash dialog. Otherwise, Cura will just crash
|
||||
# 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
|
||||
|
||||
if not has_started:
|
||||
@ -387,7 +383,7 @@ class CrashHandler:
|
||||
Application.getInstance().callLater(self._show)
|
||||
|
||||
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:
|
||||
self.dialog.exec_()
|
||||
os._exit(1)
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
from PyQt5.QtCore import QObject, QUrl
|
||||
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.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.
|
||||
# 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.
|
||||
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)
|
||||
|
||||
@pyqtSlot()
|
||||
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)
|
||||
|
||||
## Reset camera position and direction to default
|
||||
@ -61,8 +61,10 @@ class CuraActions(QObject):
|
||||
operation = GroupedOperation()
|
||||
for node in Selection.getAllSelectedObjects():
|
||||
current_node = node
|
||||
while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
|
||||
current_node = current_node.getParent()
|
||||
parent_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
|
||||
# unpredictable matrix deconstruction it was possible that mirrors
|
||||
@ -150,13 +152,13 @@ class CuraActions(QObject):
|
||||
|
||||
root = cura.CuraApplication.CuraApplication.getInstance().getController().getScene().getRoot()
|
||||
|
||||
nodes_to_change = []
|
||||
nodes_to_change = [] # type: List[SceneNode]
|
||||
for node in Selection.getAllSelectedObjects():
|
||||
parent_node = node # Find the parent node to change instead
|
||||
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)
|
||||
|
||||
if not nodes_to_change:
|
||||
|
@ -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.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from typing import cast, TYPE_CHECKING, Optional, Callable
|
||||
from typing import cast, TYPE_CHECKING, Optional, Callable, List
|
||||
|
||||
import numpy
|
||||
|
||||
@ -13,109 +13,118 @@ from PyQt5.QtGui import QColor, QIcon
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
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.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Camera import Camera
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Math.Quaternion import Quaternion
|
||||
from UM.Resources import Resources
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
|
||||
import UM.Util
|
||||
from UM.View.SelectionPass import SelectionPass # For typing.
|
||||
|
||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
from UM.Math.Matrix import Matrix
|
||||
from UM.Platform import Platform
|
||||
from UM.Resources import Resources
|
||||
from UM.Scene.ToolHandle import ToolHandle
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Math.Quaternion import Quaternion
|
||||
from UM.Math.Vector import Vector
|
||||
|
||||
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.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
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.Arranging.Arrange import Arrange
|
||||
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
||||
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.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
|
||||
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
|
||||
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 UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
from cura.Settings.MachineNameValidator import MachineNameValidator
|
||||
|
||||
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
||||
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.Scene.CuraSceneNode import CuraSceneNode
|
||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
from cura.Scene import ZOffsetDecorator
|
||||
|
||||
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.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
|
||||
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 .AutoSave import AutoSave
|
||||
from . import PlatformPhysics
|
||||
from . import BuildVolume
|
||||
from . import CameraAnimation
|
||||
from . import PrintInformation
|
||||
from . import CuraActions
|
||||
from cura.Scene import ZOffsetDecorator
|
||||
from . import CuraSplashScreen
|
||||
from . import CameraImageProvider
|
||||
from . import PrintJobPreviewImageProvider
|
||||
from . import MachineActionManager
|
||||
|
||||
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
|
||||
|
||||
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
|
||||
from cura import ApplicationMetadata, UltimakerCloudAuthentication
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.Machines.MaterialManager import MaterialManager
|
||||
@ -126,20 +135,12 @@ if TYPE_CHECKING:
|
||||
|
||||
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):
|
||||
# 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
|
||||
# changes of the settings.
|
||||
SettingVersion = 5
|
||||
SettingVersion = 7
|
||||
|
||||
Created = False
|
||||
|
||||
@ -159,10 +160,12 @@ class CuraApplication(QtApplication):
|
||||
Q_ENUMS(ResourceTypes)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(name = "cura",
|
||||
version = CuraVersion,
|
||||
buildtype = CuraBuildType,
|
||||
is_debug_mode = CuraDebugMode,
|
||||
super().__init__(name = ApplicationMetadata.CuraAppName,
|
||||
app_display_name = ApplicationMetadata.CuraAppDisplayName,
|
||||
version = ApplicationMetadata.CuraVersion,
|
||||
api_version = ApplicationMetadata.CuraSDKVersion,
|
||||
buildtype = ApplicationMetadata.CuraBuildType,
|
||||
is_debug_mode = ApplicationMetadata.CuraDebugMode,
|
||||
tray_icon_name = "cura-icon-32.png",
|
||||
**kwargs)
|
||||
|
||||
@ -177,7 +180,6 @@ class CuraApplication(QtApplication):
|
||||
# Variables set from CLI
|
||||
self._files_to_open = []
|
||||
self._use_single_instance = False
|
||||
self._trigger_early_crash = False # For debug only
|
||||
|
||||
self._single_instance = None
|
||||
|
||||
@ -202,6 +204,8 @@ class CuraApplication(QtApplication):
|
||||
self._container_manager = None
|
||||
|
||||
self._object_manager = None
|
||||
self._extruders_model = None
|
||||
self._extruders_model_with_optional = None
|
||||
self._build_plate_model = None
|
||||
self._multi_build_plate_model = None
|
||||
self._setting_visibility_presets_model = None
|
||||
@ -210,6 +214,15 @@ class CuraApplication(QtApplication):
|
||||
self._cura_scene_controller = 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._custom_quality_profile_drop_down_menu_model = None
|
||||
self._cura_API = CuraAPI(self)
|
||||
@ -239,8 +252,6 @@ class CuraApplication(QtApplication):
|
||||
|
||||
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._plugins_loaded = False
|
||||
@ -256,6 +267,14 @@ class CuraApplication(QtApplication):
|
||||
from cura.CuraPackageManager import 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
|
||||
# before the pre-start.
|
||||
def addCommandLineOptions(self):
|
||||
@ -288,7 +307,10 @@ class CuraApplication(QtApplication):
|
||||
sys.exit(0)
|
||||
|
||||
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:
|
||||
self._files_to_open.append(os.path.abspath(filename))
|
||||
|
||||
@ -298,7 +320,8 @@ class CuraApplication(QtApplication):
|
||||
super().initialize()
|
||||
|
||||
self.__sendCommandToSingleInstance()
|
||||
self.__initializeSettingDefinitionsAndFunctions()
|
||||
self._initializeSettingDefinitions()
|
||||
self._initializeSettingFunctions()
|
||||
self.__addAllResourcesAndContainerResources()
|
||||
self.__addAllEmptyContainers()
|
||||
self.__setLatestResouceVersionsForVersionUpgrade()
|
||||
@ -327,31 +350,40 @@ class CuraApplication(QtApplication):
|
||||
resource_path = os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "resources")
|
||||
Resources.addSearchPath(resource_path)
|
||||
|
||||
# Adds custom property types, settings types, and extra operators (functions) that need to be registered in
|
||||
# SettingDefinition and SettingFunction.
|
||||
def __initializeSettingDefinitionsAndFunctions(self):
|
||||
self._cura_formula_functions = CuraFormulaFunctions(self)
|
||||
|
||||
@classmethod
|
||||
def _initializeSettingDefinitions(cls):
|
||||
# Need to do this before ContainerRegistry tries to load the machines
|
||||
SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default = True, read_only = True)
|
||||
SettingDefinition.addSupportedProperty("settable_per_extruder", DefinitionPropertyType.Any, default = True, read_only = True)
|
||||
SettingDefinition.addSupportedProperty("settable_per_mesh", DefinitionPropertyType.Any, default=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
|
||||
SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default = True, read_only = True)
|
||||
SettingDefinition.addSupportedProperty("settable_globally", DefinitionPropertyType.Any, default = True, read_only = True)
|
||||
SettingDefinition.addSupportedProperty("settable_per_meshgroup", DefinitionPropertyType.Any, default=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)
|
||||
# AND for settings which are not settable_per_mesh:
|
||||
# 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:
|
||||
# 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("optional_extruder", 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("extruderValues", self._cura_formula_functions.getValuesInAllExtruders)
|
||||
SettingFunction.registerOperator("resolveOrValue", self._cura_formula_functions.getResolveOrValue)
|
||||
@ -424,38 +456,35 @@ class CuraApplication(QtApplication):
|
||||
def startSplashWindowPhase(self) -> None:
|
||||
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([
|
||||
# Misc.:
|
||||
"ConsoleLogger",
|
||||
"CuraEngineBackend",
|
||||
"UserAgreement",
|
||||
"FileLogger",
|
||||
"XmlMaterialProfile",
|
||||
"Toolbox",
|
||||
"PrepareStage",
|
||||
"MonitorStage",
|
||||
"LocalFileOutputDevice",
|
||||
"LocalContainerProvider",
|
||||
"ConsoleLogger", #You want to be able to read the log if something goes wrong.
|
||||
"CuraEngineBackend", #Cura is useless without this one since you can't slice.
|
||||
"FileLogger", #You want to be able to read the log if something goes wrong.
|
||||
"XmlMaterialProfile", #Cura crashes without this one.
|
||||
"Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
|
||||
"PrepareStage", #Cura is useless without this one since you can't load models.
|
||||
"PreviewStage", #This shows the list of the plugin views that are installed in Cura.
|
||||
"MonitorStage", #Major part of Cura's functionality.
|
||||
"LocalFileOutputDevice", #Major part of Cura's functionality.
|
||||
"LocalContainerProvider", #Cura is useless without any profiles or setting definitions.
|
||||
|
||||
# Views:
|
||||
"SimpleView",
|
||||
"SimulationView",
|
||||
"SolidView",
|
||||
"SimpleView", #Dependency of SolidView.
|
||||
"SolidView", #Displays models. Cura is useless without it.
|
||||
|
||||
# Readers & Writers:
|
||||
"GCodeWriter",
|
||||
"STLReader",
|
||||
"3MFWriter",
|
||||
"GCodeWriter", #Cura is useless if it can't write its output.
|
||||
"STLReader", #Most common model format, so disabling this makes Cura 90% useless.
|
||||
"3MFWriter", #Required for writing project files.
|
||||
|
||||
# Tools:
|
||||
"CameraTool",
|
||||
"MirrorTool",
|
||||
"RotateTool",
|
||||
"ScaleTool",
|
||||
"SelectionTool",
|
||||
"TranslateTool",
|
||||
"CameraTool", #Needed to see the scene. Cura is useless without it.
|
||||
"SelectionTool", #Dependency of the rest of the tools.
|
||||
"TranslateTool", #You'll need this for almost every print.
|
||||
])
|
||||
self._i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
@ -492,7 +521,8 @@ class CuraApplication(QtApplication):
|
||||
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
|
||||
preferences.addPreference("cura/choice_on_open_project", "always_ask")
|
||||
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/material_settings", "{}")
|
||||
|
||||
@ -504,7 +534,7 @@ class CuraApplication(QtApplication):
|
||||
preferences.addPreference("cura/expanded_brands", "")
|
||||
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 [
|
||||
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
|
||||
@ -523,18 +553,24 @@ class CuraApplication(QtApplication):
|
||||
CuraApplication.Created = True
|
||||
|
||||
def _onEngineCreated(self):
|
||||
self._qml_engine.addImageProvider("camera", CameraImageProvider.CameraImageProvider())
|
||||
self._qml_engine.addImageProvider("print_job_preview", PrintJobPreviewImageProvider.PrintJobPreviewImageProvider())
|
||||
|
||||
@pyqtProperty(bool)
|
||||
def needToShowUserAgreement(self) -> 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:
|
||||
self._need_to_show_user_agreement = set_value
|
||||
@pyqtSlot(bool)
|
||||
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
|
||||
# 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:
|
||||
Logger.log("i", "Close application")
|
||||
main_window = self.getMainWindow()
|
||||
@ -633,8 +669,6 @@ class CuraApplication(QtApplication):
|
||||
self._message_box_callback = None
|
||||
self._message_box_callback_arguments = []
|
||||
|
||||
showPrintMonitor = pyqtSignal(bool, arguments = ["show"])
|
||||
|
||||
def setSaveDataEnabled(self, enabled: bool) -> None:
|
||||
self._save_data_enabled = enabled
|
||||
|
||||
@ -660,12 +694,12 @@ class CuraApplication(QtApplication):
|
||||
|
||||
## Handle loading of all plugin types (and the backend explicitly)
|
||||
# \sa PluginRegistry
|
||||
def _loadPlugins(self):
|
||||
def _loadPlugins(self) -> None:
|
||||
self._plugin_registry.addType("profile_reader", self._addProfileReader)
|
||||
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
|
||||
|
||||
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:
|
||||
lib_suffixes = {""}
|
||||
for suffix in lib_suffixes:
|
||||
@ -730,6 +764,11 @@ class CuraApplication(QtApplication):
|
||||
# Initialize Cura API
|
||||
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
|
||||
if self._is_headless:
|
||||
self.runWithoutGUI()
|
||||
@ -824,10 +863,38 @@ class CuraApplication(QtApplication):
|
||||
# Hide the splash screen
|
||||
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)
|
||||
def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
|
||||
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":
|
||||
if self._cura_formula_functions is None:
|
||||
self._cura_formula_functions = CuraFormulaFunctions(self)
|
||||
@ -862,6 +929,19 @@ class CuraApplication(QtApplication):
|
||||
self._object_manager = ObjectsModel.createObjectsModel()
|
||||
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)
|
||||
def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel:
|
||||
if self._multi_build_plate_model is None:
|
||||
@ -936,7 +1016,7 @@ class CuraApplication(QtApplication):
|
||||
engine.rootContext().setContextProperty("CuraApplication", self)
|
||||
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
|
||||
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")
|
||||
|
||||
@ -947,17 +1027,26 @@ class CuraApplication(QtApplication):
|
||||
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
|
||||
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)
|
||||
qmlRegisterType(BuildPlateModel, "Cura", 1, 0, "BuildPlateModel")
|
||||
qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
|
||||
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
|
||||
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
|
||||
qmlRegisterType(GlobalStacksModel, "Cura", 1, 0, "GlobalStacksModel")
|
||||
|
||||
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
|
||||
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
|
||||
qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
|
||||
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
|
||||
qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel")
|
||||
|
||||
qmlRegisterType(DiscoveredPrintersModel, "Cura", 1, 0, "DiscoveredPrintersModel")
|
||||
|
||||
qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
|
||||
"QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
|
||||
@ -968,11 +1057,14 @@ class CuraApplication(QtApplication):
|
||||
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
|
||||
qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel")
|
||||
qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
|
||||
qmlRegisterType(FirstStartMachineActionsModel, "Cura", 1, 0, "FirstStartMachineActionsModel")
|
||||
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
|
||||
qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
|
||||
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
|
||||
qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
|
||||
|
||||
qmlRegisterType(PrinterOutputDevice, "Cura", 1, 0, "PrinterOutputDevice")
|
||||
|
||||
from cura.API import CuraAPI
|
||||
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.start()
|
||||
|
||||
requestAddPrinter = pyqtSignal()
|
||||
activityChanged = pyqtSignal()
|
||||
sceneBoundingBoxChanged = pyqtSignal()
|
||||
|
||||
@ -1083,88 +1174,6 @@ class CuraApplication(QtApplication):
|
||||
self._platform_activity = True if count > 0 else False
|
||||
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.
|
||||
@pyqtSlot()
|
||||
def selectAll(self):
|
||||
@ -1244,62 +1253,75 @@ class CuraApplication(QtApplication):
|
||||
|
||||
## Arrange all objects.
|
||||
@pyqtSlot()
|
||||
def arrangeObjectsToAllBuildPlates(self):
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
def arrangeObjectsToAllBuildPlates(self) -> None:
|
||||
nodes_to_arrange = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
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"):
|
||||
continue # i.e. node with layer data
|
||||
|
||||
bounding_box = node.getBoundingBox()
|
||||
# Skip nodes that are too big
|
||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
||||
nodes.append(node)
|
||||
job = ArrangeObjectsAllBuildPlatesJob(nodes)
|
||||
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
|
||||
nodes_to_arrange.append(node)
|
||||
job = ArrangeObjectsAllBuildPlatesJob(nodes_to_arrange)
|
||||
job.start()
|
||||
self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
|
||||
|
||||
# Single build plate
|
||||
@pyqtSlot()
|
||||
def arrangeAll(self):
|
||||
nodes = []
|
||||
def arrangeAll(self) -> None:
|
||||
nodes_to_arrange = []
|
||||
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):
|
||||
continue
|
||||
|
||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||
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)
|
||||
|
||||
if not node.isSelectable():
|
||||
continue # i.e. node with layer data
|
||||
|
||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||
continue # i.e. node with layer data
|
||||
|
||||
if node.callDecoration("getBuildPlateNumber") == active_build_plate:
|
||||
# Skip nodes that are too big
|
||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
||||
nodes.append(node)
|
||||
self.arrange(nodes, fixed_nodes = [])
|
||||
bounding_box = node.getBoundingBox()
|
||||
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
|
||||
nodes_to_arrange.append(node)
|
||||
self.arrange(nodes_to_arrange, fixed_nodes = [])
|
||||
|
||||
## Arrange a set of nodes given a set of fixed nodes
|
||||
# \param nodes nodes that we have to place
|
||||
# \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
|
||||
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
|
||||
job.start()
|
||||
|
||||
## Reload all mesh data on the screen from file.
|
||||
@pyqtSlot()
|
||||
def reloadAll(self):
|
||||
def reloadAll(self) -> None:
|
||||
Logger.log("i", "Reloading all loaded mesh data.")
|
||||
nodes = []
|
||||
has_merged_nodes = False
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not isinstance(node, CuraSceneNode) or not node.getMeshData() :
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||
if not isinstance(node, CuraSceneNode) or not node.getMeshData():
|
||||
if node.getName() == "MergedMesh":
|
||||
has_merged_nodes = True
|
||||
continue
|
||||
@ -1313,7 +1335,7 @@ class CuraApplication(QtApplication):
|
||||
file_name = node.getMeshData().getFileName()
|
||||
if file_name:
|
||||
job = ReadMeshJob(file_name)
|
||||
job._node = node
|
||||
job._node = node # type: ignore
|
||||
job.finished.connect(self._reloadMeshFinished)
|
||||
if has_merged_nodes:
|
||||
job.finished.connect(self.updateOriginOfMergedMeshes)
|
||||
@ -1322,20 +1344,8 @@ class CuraApplication(QtApplication):
|
||||
else:
|
||||
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")
|
||||
def setExpandedCategories(self, categories):
|
||||
def setExpandedCategories(self, categories: List[str]) -> None:
|
||||
categories = list(set(categories))
|
||||
categories.sort()
|
||||
joined = ";".join(categories)
|
||||
@ -1346,7 +1356,7 @@ class CuraApplication(QtApplication):
|
||||
expandedCategoriesChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty("QStringList", notify = expandedCategoriesChanged)
|
||||
def expandedCategories(self):
|
||||
def expandedCategories(self) -> List[str]:
|
||||
return self.getPreferences().getValue("cura/categories_expanded").split(";")
|
||||
|
||||
@pyqtSlot()
|
||||
@ -1396,13 +1406,12 @@ class CuraApplication(QtApplication):
|
||||
|
||||
|
||||
## Updates origin position of all merged meshes
|
||||
# \param jobNode \type{Job} empty object which passed which is required by JobQueue
|
||||
def updateOriginOfMergedMeshes(self, jobNode):
|
||||
def updateOriginOfMergedMeshes(self, _):
|
||||
group_nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
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():
|
||||
if isinstance(decorator, GroupDecorator):
|
||||
group_nodes.append(node)
|
||||
@ -1446,7 +1455,7 @@ class CuraApplication(QtApplication):
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def groupSelected(self):
|
||||
def groupSelected(self) -> None:
|
||||
# Create a group-node
|
||||
group_node = CuraSceneNode()
|
||||
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
|
||||
selected_nodes = Selection.getAllSelectedObjects().copy()
|
||||
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)
|
||||
|
||||
# Move selected nodes into the group-node
|
||||
@ -1474,7 +1484,7 @@ class CuraApplication(QtApplication):
|
||||
Selection.add(group_node)
|
||||
|
||||
@pyqtSlot()
|
||||
def ungroupSelected(self):
|
||||
def ungroupSelected(self) -> None:
|
||||
selected_objects = Selection.getAllSelectedObjects().copy()
|
||||
for node in selected_objects:
|
||||
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,
|
||||
# see GroupDecorator._onChildrenChanged
|
||||
|
||||
def _createSplashScreen(self):
|
||||
def _createSplashScreen(self) -> Optional[CuraSplashScreen.CuraSplashScreen]:
|
||||
if self._is_headless:
|
||||
return None
|
||||
return CuraSplashScreen.CuraSplashScreen()
|
||||
@ -1663,7 +1673,9 @@ class CuraApplication(QtApplication):
|
||||
is_non_sliceable = "." + file_extension in self._non_sliceable_extensions
|
||||
|
||||
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()
|
||||
node.addDecorator(block_slicing_decorator)
|
||||
@ -1761,3 +1773,16 @@ class CuraApplication(QtApplication):
|
||||
def getSidebarCustomMenuItems(self) -> list:
|
||||
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
|
||||
|
@ -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.
|
||||
|
||||
CuraAppName = "@CURA_APP_NAME@"
|
||||
CuraAppDisplayName = "@CURA_APP_DISPLAY_NAME@"
|
||||
CuraVersion = "@CURA_VERSION@"
|
||||
CuraBuildType = "@CURA_BUILDTYPE@"
|
||||
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
||||
CuraSDKVersion = "@CURA_SDK_VERSION@"
|
||||
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
||||
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
||||
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
|
||||
|
24
cura/CuraView.py
Normal file
24
cura/CuraView.py
Normal 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")
|
@ -7,43 +7,36 @@ from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
from .LayerData import LayerData
|
||||
|
||||
import numpy
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
## Builder class for constructing a LayerData object
|
||||
class LayerDataBuilder(MeshBuilder):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._layers = {}
|
||||
self._element_counts = {}
|
||||
self._layers = {} # type: Dict[int, Layer]
|
||||
self._element_counts = {} # type: Dict[int, int]
|
||||
|
||||
def addLayer(self, layer):
|
||||
def addLayer(self, layer: int) -> None:
|
||||
if layer not in self._layers:
|
||||
self._layers[layer] = Layer(layer)
|
||||
|
||||
def addPolygon(self, layer, polygon_type, data, line_width, line_thickness, line_feedrate):
|
||||
if layer not in self._layers:
|
||||
self.addLayer(layer)
|
||||
def getLayer(self, layer: int) -> Optional[Layer]:
|
||||
return self._layers.get(layer)
|
||||
|
||||
p = LayerPolygon(self, polygon_type, data, line_width, line_thickness, line_feedrate)
|
||||
self._layers[layer].polygons.append(p)
|
||||
|
||||
def getLayer(self, layer):
|
||||
if layer in self._layers:
|
||||
return self._layers[layer]
|
||||
|
||||
def getLayers(self):
|
||||
def getLayers(self) -> Dict[int, Layer]:
|
||||
return self._layers
|
||||
|
||||
def getElementCounts(self):
|
||||
def getElementCounts(self) -> Dict[int, int]:
|
||||
return self._element_counts
|
||||
|
||||
def setLayerHeight(self, layer, height):
|
||||
def setLayerHeight(self, layer: int, height: float) -> None:
|
||||
if layer not in self._layers:
|
||||
self.addLayer(layer)
|
||||
|
||||
self._layers[layer].setHeight(height)
|
||||
|
||||
def setLayerThickness(self, layer, thickness):
|
||||
def setLayerThickness(self, layer: int, thickness: float) -> None:
|
||||
if layer not in self._layers:
|
||||
self.addLayer(layer)
|
||||
|
||||
@ -71,7 +64,7 @@ class LayerDataBuilder(MeshBuilder):
|
||||
vertex_offset = 0
|
||||
index_offset = 0
|
||||
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.addVertices(vertices)
|
||||
|
@ -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 cura.LayerData import LayerData
|
||||
|
||||
|
||||
## Simple decorator to indicate a scene node holds layer data.
|
||||
class LayerDataDecorator(SceneNodeDecorator):
|
||||
def __init__(self):
|
||||
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
|
||||
|
||||
def setLayerData(self, layer_data):
|
||||
def setLayerData(self, layer_data: LayerData) -> None:
|
||||
self._layer_data = layer_data
|
||||
|
||||
def __deepcopy__(self, memo) -> "LayerDataDecorator":
|
||||
copied_decorator = LayerDataDecorator()
|
||||
copied_decorator._layer_data = self._layer_data
|
||||
return copied_decorator
|
||||
|
@ -2,9 +2,11 @@
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Application import Application
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
import numpy
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
||||
|
||||
class LayerPolygon:
|
||||
NoneType = 0
|
||||
@ -18,22 +20,24 @@ class LayerPolygon:
|
||||
MoveCombingType = 8
|
||||
MoveRetractionType = 9
|
||||
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)
|
||||
|
||||
## LayerPolygon, used in ProcessSlicedLayersJob
|
||||
# \param extruder
|
||||
# \param extruder The position of the extruder
|
||||
# \param line_types array with line_types
|
||||
# \param data new_points
|
||||
# \param line_widths array with line widths
|
||||
# \param line_thicknesses: array with type as index and thickness as value
|
||||
# \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._types = line_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._data = data
|
||||
self._line_widths = line_widths
|
||||
@ -53,16 +57,16 @@ class LayerPolygon:
|
||||
# Buffering the colors shouldn't be necessary as it is not
|
||||
# re-used and can save alot of memory usage.
|
||||
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
|
||||
# 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._build_cache_line_mesh_mask = None
|
||||
self._build_cache_needed_points = None
|
||||
self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray]
|
||||
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.
|
||||
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)
|
||||
@ -90,10 +94,14 @@ class LayerPolygon:
|
||||
# \param extruders : vertex numpy array to be filled
|
||||
# \param line_types : vertex 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:
|
||||
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
|
||||
needed_points_list = self._build_cache_needed_points
|
||||
|
||||
@ -236,7 +244,8 @@ class LayerPolygon:
|
||||
theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType
|
||||
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
|
||||
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
|
||||
|
@ -33,6 +33,12 @@ class MachineAction(QObject, PluginObject):
|
||||
def getKey(self) -> str:
|
||||
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)
|
||||
def label(self) -> str:
|
||||
return self._label
|
||||
|
@ -64,21 +64,21 @@ class MachineErrorChecker(QObject):
|
||||
|
||||
def _onMachineChanged(self) -> None:
|
||||
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)
|
||||
|
||||
for extruder in self._global_stack.extruders.values():
|
||||
extruder.propertyChanged.disconnect(self.startErrorCheck)
|
||||
extruder.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
|
||||
extruder.containersChanged.disconnect(self.startErrorCheck)
|
||||
|
||||
self._global_stack = self._machine_manager.activeMachine
|
||||
|
||||
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)
|
||||
|
||||
for extruder in self._global_stack.extruders.values():
|
||||
extruder.propertyChanged.connect(self.startErrorCheck)
|
||||
extruder.propertyChanged.connect(self.startErrorCheckPropertyChanged)
|
||||
extruder.containersChanged.connect(self.startErrorCheck)
|
||||
|
||||
hasErrorUpdated = pyqtSignal()
|
||||
@ -93,6 +93,13 @@ class MachineErrorChecker(QObject):
|
||||
def needToWaitForResult(self) -> bool:
|
||||
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.
|
||||
def startErrorCheck(self, *args) -> None:
|
||||
if not self._check_in_progress:
|
||||
|
@ -21,6 +21,7 @@ from .VariantType import VariantType
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from cura.Settings.ExtruderStack import ExtruderStack
|
||||
|
||||
@ -218,7 +219,7 @@ class MaterialManager(QObject):
|
||||
|
||||
root_material_id = material_metadata["base_file"]
|
||||
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:
|
||||
self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}
|
||||
@ -298,9 +299,13 @@ class MaterialManager(QObject):
|
||||
def getRootMaterialIDWithoutDiameter(self, root_material_id: str) -> str:
|
||||
return self._diameter_material_map.get(root_material_id, "")
|
||||
|
||||
def getMaterialGroupListByGUID(self, guid: str) -> Optional[list]:
|
||||
def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]:
|
||||
return self._guid_material_groups_map.get(guid)
|
||||
|
||||
# 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.
|
||||
#
|
||||
@ -327,7 +332,6 @@ class MaterialManager(QObject):
|
||||
buildplate_node = nozzle_node.getChildNode(buildplate_name)
|
||||
|
||||
nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]
|
||||
|
||||
# Fallback mechanism of finding materials:
|
||||
# 1. buildplate-specific material
|
||||
# 2. nozzle-specific material
|
||||
@ -446,6 +450,28 @@ class MaterialManager(QObject):
|
||||
material_diameter, root_material_id)
|
||||
return node
|
||||
|
||||
# There are 2 ways to get fallback materials;
|
||||
# - A fallback by type (@sa getFallbackMaterialIdByMaterialType), which adds the generic version of this material
|
||||
# - A fallback by GUID; If a material has been duplicated, it should also check if the original materials do have
|
||||
# a GUID. This should only be done if the material itself does not have a quality just yet.
|
||||
def getFallBackMaterialIdsByMaterial(self, material: "InstanceContainer") -> List[str]:
|
||||
results = [] # type: List[str]
|
||||
|
||||
material_groups = self.getMaterialGroupListByGUID(material.getMetaDataEntry("GUID"))
|
||||
for material_group in material_groups: # type: ignore
|
||||
if material_group.name != material.getId():
|
||||
# If the material in the group is read only, put it at the front of the list (since that is the most
|
||||
# likely one to get a result)
|
||||
if material_group.is_read_only:
|
||||
results.insert(0, material_group.name)
|
||||
else:
|
||||
results.append(material_group.name)
|
||||
|
||||
fallback = self.getFallbackMaterialIdByMaterialType(material.getMetaDataEntry("material"))
|
||||
if fallback is not None:
|
||||
results.append(fallback)
|
||||
return results
|
||||
|
||||
#
|
||||
# Used by QualityManager. Built-in quality profiles may be based on generic material IDs such as "generic_pla".
|
||||
# For materials such as ultimaker_pla_orange, no quality profiles may be found, so we should fall back to use
|
||||
@ -510,16 +536,40 @@ class MaterialManager(QObject):
|
||||
return
|
||||
|
||||
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:
|
||||
self._container_registry.removeContainer(node.getMetaDataEntry("id", ""))
|
||||
|
||||
#
|
||||
# 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)
|
||||
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
|
||||
root_material_id = material_node.getMetaDataEntry("base_file")
|
||||
@ -602,7 +652,6 @@ class MaterialManager(QObject):
|
||||
container_to_add.setDirty(True)
|
||||
self._container_registry.addContainer(container_to_add)
|
||||
|
||||
|
||||
# if the duplicated material was favorite then the new material should also be added to favorite.
|
||||
if root_material_id in self.getFavorites():
|
||||
self.addFavorite(new_base_id)
|
||||
@ -622,8 +671,10 @@ class MaterialManager(QObject):
|
||||
machine_manager = self._application.getMachineManager()
|
||||
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)
|
||||
root_material_id = "generic_pla"
|
||||
root_material_id = self.getRootMaterialIDForDiameter(root_material_id, approximate_diameter)
|
||||
material_group = self.getMaterialGroup(root_material_id)
|
||||
|
||||
@ -654,7 +705,11 @@ class MaterialManager(QObject):
|
||||
|
||||
@pyqtSlot(str)
|
||||
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()
|
||||
|
||||
# Ensure all settings are saved.
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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 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.
|
||||
# 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
|
||||
from cura.Machines.MaterialNode import MaterialNode
|
||||
|
||||
|
||||
class BaseMaterialsModel(ListModel):
|
||||
|
||||
extruderPositionChanged = pyqtSignal()
|
||||
enabledChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
self._application = CuraApplication.getInstance()
|
||||
@ -54,8 +58,9 @@ class BaseMaterialsModel(ListModel):
|
||||
self._extruder_position = 0
|
||||
self._extruder_stack = None
|
||||
|
||||
self._available_materials = None
|
||||
self._favorite_ids = None
|
||||
self._available_materials = None # type: Optional[Dict[str, MaterialNode]]
|
||||
self._favorite_ids = set() # type: Set[str]
|
||||
self._enabled = True
|
||||
|
||||
def _updateExtruderStack(self):
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
@ -82,6 +87,18 @@ class BaseMaterialsModel(ListModel):
|
||||
def extruderPosition(self) -> int:
|
||||
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
|
||||
# models themselves.
|
||||
def _update(self):
|
||||
@ -93,7 +110,7 @@ class BaseMaterialsModel(ListModel):
|
||||
def _canUpdate(self):
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
|
||||
if global_stack is None:
|
||||
if global_stack is None or not self._enabled:
|
||||
return False
|
||||
|
||||
extruder_position = str(self._extruder_position)
|
||||
@ -102,7 +119,6 @@ class BaseMaterialsModel(ListModel):
|
||||
return False
|
||||
|
||||
extruder_stack = global_stack.extruders[extruder_position]
|
||||
|
||||
self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack)
|
||||
if self._available_materials is None:
|
||||
return False
|
||||
|
167
cura/Machines/Models/DiscoveredPrintersModel.py
Normal file
167
cura/Machines/Models/DiscoveredPrintersModel.py
Normal 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])
|
@ -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.
|
||||
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, pyqtProperty, QTimer
|
||||
from typing import Iterable
|
||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer
|
||||
from typing import Iterable, TYPE_CHECKING
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
import UM.Qt.ListModel
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Application import Application
|
||||
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")
|
||||
|
||||
|
||||
## Model that holds extruders.
|
||||
#
|
||||
# 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
|
||||
# settings.
|
||||
class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
class ExtrudersModel(ListModel):
|
||||
# The ID of the container stack for the extruder.
|
||||
IdRole = Qt.UserRole + 1
|
||||
|
||||
## Human-readable name of the extruder.
|
||||
NameRole = Qt.UserRole + 2
|
||||
## Is the extruder enabled?
|
||||
EnabledRole = Qt.UserRole + 9
|
||||
|
||||
## Colour of the material loaded in the extruder.
|
||||
ColorRole = Qt.UserRole + 3
|
||||
@ -47,6 +47,12 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
VariantRole = Qt.UserRole + 7
|
||||
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
|
||||
# colour.
|
||||
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.VariantRole, "variant")
|
||||
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.setInterval(100)
|
||||
self._update_extruder_timer.setSingleShot(True)
|
||||
self._update_extruder_timer.timeout.connect(self.__updateExtruders)
|
||||
|
||||
self._simple_names = False
|
||||
|
||||
self._active_machine_extruders = [] # type: Iterable[ExtruderStack]
|
||||
self._add_optional_extruder = False
|
||||
|
||||
@ -96,21 +101,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
def addOptionalExtruder(self):
|
||||
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
|
||||
# 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
|
||||
# signal; it's assumed to be the current printer in that case.
|
||||
def _extrudersChanged(self, machine_id = None):
|
||||
machine_manager = Application.getInstance().getMachineManager()
|
||||
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
|
||||
return
|
||||
if machine_id != Application.getInstance().getGlobalContainerStack().getId():
|
||||
if machine_id != machine_manager.activeMachine.getId():
|
||||
# Not the current machine
|
||||
return
|
||||
|
||||
# Unlink from old extruders
|
||||
for extruder in self._active_machine_extruders:
|
||||
extruder.containersChanged.disconnect(self._onExtruderStackContainersChanged)
|
||||
extruder.enabledChanged.disconnect(self._updateExtruders)
|
||||
|
||||
# Link to new 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.
|
||||
continue
|
||||
extruder.containersChanged.connect(self._onExtruderStackContainersChanged)
|
||||
extruder.enabledChanged.connect(self._updateExtruders)
|
||||
self._active_machine_extruders.append(extruder)
|
||||
|
||||
self._updateExtruders() # Since the new extruders may have different properties, update our own model.
|
||||
|
||||
def _onExtruderStackContainersChanged(self, container):
|
||||
# Update when there is an empty container or material change
|
||||
if container.getMetaDataEntry("type") == "material" or container.getMetaDataEntry("type") is None:
|
||||
# Update when there is an empty container or material or variant change
|
||||
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
|
||||
self._updateExtruders()
|
||||
|
||||
@ -160,7 +153,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
def __updateExtruders(self):
|
||||
extruders_changed = False
|
||||
|
||||
if self.rowCount() != 0:
|
||||
if self.count != 0:
|
||||
extruders_changed = True
|
||||
|
||||
items = []
|
||||
@ -172,7 +165,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
|
||||
|
||||
for extruder in Application.getInstance().getExtruderManager().getActiveExtruderStacks():
|
||||
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
|
||||
position = extruder.getMetaDataEntry("position", default = "0")
|
||||
try:
|
||||
position = int(position)
|
||||
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]
|
||||
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
|
||||
item = {
|
||||
"id": extruder.getId(),
|
||||
@ -195,6 +189,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
"material": extruder.material.getName() if extruder.material else "",
|
||||
"variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core
|
||||
"stack": extruder,
|
||||
"material_brand": material_brand,
|
||||
"color_name": color_name
|
||||
}
|
||||
|
||||
items.append(item)
|
||||
@ -213,9 +209,14 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||
"enabled": True,
|
||||
"color": "#ffffff",
|
||||
"index": -1,
|
||||
"definition": ""
|
||||
"definition": "",
|
||||
"material": "",
|
||||
"variant": "",
|
||||
"stack": None,
|
||||
"material_brand": "",
|
||||
"color_name": "",
|
||||
}
|
||||
items.append(item)
|
||||
|
||||
self.setItems(items)
|
||||
self.modelChanged.emit()
|
||||
if self._items != items:
|
||||
self.setItems(items)
|
||||
self.modelChanged.emit()
|
@ -1,20 +1,16 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Logger import Logger
|
||||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||
|
||||
## Model that shows the list of favorite materials.
|
||||
class FavoriteMaterialsModel(BaseMaterialsModel):
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
|
||||
# Perform standard check and reset if the check fails
|
||||
if not self._canUpdate():
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
# Get updated list of favorites
|
||||
|
104
cura/Machines/Models/FirstStartMachineActionsModel.py
Normal file
104
cura/Machines/Models/FirstStartMachineActionsModel.py
Normal 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"]
|
@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Logger import Logger
|
||||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||
|
||||
class GenericMaterialsModel(BaseMaterialsModel):
|
||||
@ -11,10 +10,7 @@ class GenericMaterialsModel(BaseMaterialsModel):
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
|
||||
# Perform standard check and reset if the check fails
|
||||
if not self._canUpdate():
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
# Get updated list of favorites
|
||||
|
77
cura/Machines/Models/GlobalStacksModel.py
Normal file
77
cura/Machines/Models/GlobalStacksModel.py
Normal 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)
|
@ -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)
|
@ -1,9 +1,8 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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.Logger import Logger
|
||||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||
|
||||
class MaterialTypesModel(ListModel):
|
||||
@ -28,12 +27,8 @@ class MaterialBrandsModel(BaseMaterialsModel):
|
||||
self._update()
|
||||
|
||||
def _update(self):
|
||||
|
||||
# Perform standard check and reset if the check fails
|
||||
if not self._canUpdate():
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
# Get updated list of favorites
|
||||
self._favorite_ids = self._material_manager.getFavorites()
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
from PyQt5.QtCore import QTimer, pyqtSignal, pyqtProperty
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Scene.Camera import Camera
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
@ -34,8 +35,9 @@ class MultiBuildPlateModel(ListModel):
|
||||
self._active_build_plate = -1
|
||||
|
||||
def setMaxBuildPlate(self, max_build_plate):
|
||||
self._max_build_plate = max_build_plate
|
||||
self.maxBuildPlateChanged.emit()
|
||||
if self._max_build_plate != max_build_plate:
|
||||
self._max_build_plate = max_build_plate
|
||||
self.maxBuildPlateChanged.emit()
|
||||
|
||||
## Return the highest build plate number
|
||||
@pyqtProperty(int, notify = maxBuildPlateChanged)
|
||||
@ -43,15 +45,17 @@ class MultiBuildPlateModel(ListModel):
|
||||
return self._max_build_plate
|
||||
|
||||
def setActiveBuildPlate(self, nr):
|
||||
self._active_build_plate = nr
|
||||
self.activeBuildPlateChanged.emit()
|
||||
if self._active_build_plate != nr:
|
||||
self._active_build_plate = nr
|
||||
self.activeBuildPlateChanged.emit()
|
||||
|
||||
@pyqtProperty(int, notify = activeBuildPlateChanged)
|
||||
def activeBuildPlate(self):
|
||||
return self._active_build_plate
|
||||
|
||||
def _updateSelectedObjectBuildPlateNumbersDelayed(self, *args):
|
||||
self._update_timer.start()
|
||||
if not isinstance(args[0], Camera):
|
||||
self._update_timer.start()
|
||||
|
||||
def _updateSelectedObjectBuildPlateNumbers(self, *args):
|
||||
result = set()
|
||||
|
@ -33,8 +33,6 @@ class NozzleModel(ListModel):
|
||||
def _update(self):
|
||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||
|
||||
self.items.clear()
|
||||
|
||||
global_stack = self._machine_manager.activeMachine
|
||||
if global_stack is None:
|
||||
self.setItems([])
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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.Logger import Logger
|
||||
@ -10,6 +10,7 @@ from UM.Settings.SettingFunction import SettingFunction
|
||||
|
||||
from cura.Machines.QualityManager import QualityGroup
|
||||
|
||||
|
||||
#
|
||||
# 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
|
||||
QualityGroupRole = Qt.UserRole + 6
|
||||
QualityChangesGroupRole = Qt.UserRole + 7
|
||||
IsExperimentalRole = Qt.UserRole + 8
|
||||
|
||||
def __init__(self, parent = None):
|
||||
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.QualityGroupRole, "quality_group")
|
||||
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
|
||||
self.addRoleName(self.IsExperimentalRole, "is_experimental")
|
||||
|
||||
self._application = Application.getInstance()
|
||||
self._machine_manager = self._application.getMachineManager()
|
||||
self._quality_manager = Application.getInstance().getQualityManager()
|
||||
|
||||
self._application.globalContainerStackChanged.connect(self._update)
|
||||
self._machine_manager.activeQualityGroupChanged.connect(self._update)
|
||||
self._machine_manager.extruderChanged.connect(self._update)
|
||||
self._quality_manager.qualitiesUpdated.connect(self._update)
|
||||
self._application.globalContainerStackChanged.connect(self._onChange)
|
||||
self._machine_manager.activeQualityGroupChanged.connect(self._onChange)
|
||||
self._machine_manager.extruderChanged.connect(self._onChange)
|
||||
self._quality_manager.qualitiesUpdated.connect(self._onChange)
|
||||
|
||||
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):
|
||||
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_unit": self._layer_height_unit,
|
||||
"available": quality_group.is_available,
|
||||
"quality_group": quality_group}
|
||||
"quality_group": quality_group,
|
||||
"is_experimental": quality_group.is_experimental}
|
||||
|
||||
item_list.append(item)
|
||||
|
||||
|
@ -6,6 +6,7 @@ from typing import Optional, List
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Preferences import Preferences
|
||||
from UM.Resources import Resources
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
@ -18,14 +19,20 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
onItemsChanged = pyqtSignal()
|
||||
activePresetChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, preferences, parent = None):
|
||||
def __init__(self, preferences: Preferences, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self._items = [] # type: List[SettingVisibilityPreset]
|
||||
self._custom_preset = SettingVisibilityPreset(preset_id = "custom", name = "Custom selection", weight = -100)
|
||||
|
||||
self._populate()
|
||||
|
||||
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
|
||||
|
||||
@ -42,7 +49,8 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
visible_settings = self._preferences.getValue("general/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:
|
||||
self._onPreferencesChanged("general/visible_settings")
|
||||
|
||||
@ -59,9 +67,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
def _populate(self) -> None:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
items = [] # type: List[SettingVisibilityPreset]
|
||||
|
||||
custom_preset = SettingVisibilityPreset(preset_id="custom", name ="Custom selection", weight = -100)
|
||||
items.append(custom_preset)
|
||||
items.append(self._custom_preset)
|
||||
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
||||
setting_visibility_preset = SettingVisibilityPreset()
|
||||
try:
|
||||
@ -77,7 +83,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
self.setItems(items)
|
||||
|
||||
@pyqtProperty("QVariantList", notify = onItemsChanged)
|
||||
def items(self):
|
||||
def items(self) -> List[SettingVisibilityPreset]:
|
||||
return self._items
|
||||
|
||||
def setItems(self, items: List[SettingVisibilityPreset]) -> None:
|
||||
@ -87,7 +93,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
|
||||
@pyqtSlot(str)
|
||||
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)
|
||||
return
|
||||
|
||||
@ -96,7 +102,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
|
||||
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:
|
||||
# Save the current visibility settings to custom
|
||||
current_visibility_string = self._preferences.getValue("general/visible_settings")
|
||||
@ -117,7 +123,9 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
|
||||
@pyqtProperty(str, notify = activePresetChanged)
|
||||
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:
|
||||
if name != "general/visible_settings":
|
||||
@ -140,7 +148,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
item_to_set = self._active_preset_item
|
||||
if matching_preset_item is None:
|
||||
# The new visibility setup is "custom" should be custom
|
||||
if self._active_preset_item.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
|
||||
self._preferences.setValue("cura/custom_visible_settings", visibility_string)
|
||||
else:
|
||||
@ -149,7 +157,12 @@ class SettingVisibilityPresetsModel(QObject):
|
||||
else:
|
||||
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:
|
||||
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()
|
||||
|
@ -10,7 +10,6 @@ from UM.Application import Application
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
|
@ -4,6 +4,9 @@
|
||||
from typing import Dict, Optional, List, Set
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
from UM.Util import parseBool
|
||||
|
||||
from cura.Machines.ContainerNode import ContainerNode
|
||||
|
||||
|
||||
@ -29,6 +32,7 @@ class QualityGroup(QObject):
|
||||
self.nodes_for_extruders = {} # type: Dict[int, ContainerNode]
|
||||
self.quality_type = quality_type
|
||||
self.is_available = False
|
||||
self.is_experimental = False
|
||||
|
||||
@pyqtSlot(result = str)
|
||||
def getName(self) -> str:
|
||||
@ -51,3 +55,17 @@ class QualityGroup(QObject):
|
||||
for extruder_node in self.nodes_for_extruders.values():
|
||||
result.append(extruder_node)
|
||||
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
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import TYPE_CHECKING, Optional, cast, Dict, List
|
||||
from typing import TYPE_CHECKING, Optional, cast, Dict, List, Set
|
||||
|
||||
from PyQt5.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot
|
||||
|
||||
@ -16,7 +16,7 @@ from .QualityGroup import QualityGroup
|
||||
from .QualityNode import QualityNode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from .QualityChangesGroup import QualityChangesGroup
|
||||
from cura.CuraApplication import CuraApplication
|
||||
@ -209,6 +209,7 @@ class QualityManager(QObject):
|
||||
# (1) the machine-specific node
|
||||
# (2) the generic node
|
||||
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
|
||||
# qualities, we should not fall back to use the global qualities.
|
||||
has_extruder_specific_qualities = False
|
||||
@ -235,7 +236,7 @@ class QualityManager(QObject):
|
||||
|
||||
for quality_type, quality_node in node.quality_type_map.items():
|
||||
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
|
||||
break
|
||||
|
||||
@ -259,11 +260,15 @@ class QualityManager(QObject):
|
||||
root_material_id = self._material_manager.getRootMaterialIDWithoutDiameter(root_material_id)
|
||||
root_material_id_list.append(root_material_id)
|
||||
|
||||
# Also try to get the fallback material
|
||||
material_type = extruder.material.getMetaDataEntry("material")
|
||||
fallback_root_material_id = self._material_manager.getFallbackMaterialIdByMaterialType(material_type)
|
||||
if fallback_root_material_id:
|
||||
root_material_id_list.append(fallback_root_material_id)
|
||||
# Also try to get the fallback materials
|
||||
fallback_ids = self._material_manager.getFallBackMaterialIdsByMaterial(extruder.material)
|
||||
|
||||
if fallback_ids:
|
||||
root_material_id_list.extend(fallback_ids)
|
||||
|
||||
# Weed out duplicates while preserving the order.
|
||||
seen = set() # type: Set[str]
|
||||
root_material_id_list = [x for x in root_material_id_list if x not in seen and not seen.add(x)] # type: ignore
|
||||
|
||||
# Here we construct a list of nodes we want to look for qualities with the highest priority first.
|
||||
# The use case is that, when we look for qualities for a machine, we first want to search in the following
|
||||
@ -333,7 +338,7 @@ class QualityManager(QObject):
|
||||
|
||||
quality_group = quality_group_dict[quality_type]
|
||||
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
|
||||
# and use the material/variant specific qualities.
|
||||
@ -363,7 +368,7 @@ class QualityManager(QObject):
|
||||
if node and node.quality_type_map:
|
||||
for quality_type, quality_node in node.quality_type_map.items():
|
||||
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
|
||||
break
|
||||
|
||||
@ -437,7 +442,8 @@ class QualityManager(QObject):
|
||||
quality_changes_group = quality_model_item["quality_changes_group"]
|
||||
if quality_changes_group is None:
|
||||
# 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)
|
||||
self._container_registry.addContainer(new_quality_changes)
|
||||
else:
|
||||
@ -534,7 +540,7 @@ class QualityManager(QObject):
|
||||
# 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.
|
||||
#
|
||||
def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainer",
|
||||
def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainerInterface",
|
||||
default_definition_id: str = "fdmprinter") -> str:
|
||||
machine_definition_id = default_definition_id
|
||||
if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)):
|
||||
|
@ -107,7 +107,7 @@ class VariantManager:
|
||||
break
|
||||
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]:
|
||||
machine_definition_id = machine.definition.getId()
|
||||
|
@ -25,7 +25,7 @@ class MultiplyObjectsJob(Job):
|
||||
|
||||
def run(self):
|
||||
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()
|
||||
scene = Application.getInstance().getController().getScene()
|
||||
|
||||
|
@ -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.
|
||||
from datetime import datetime
|
||||
import json
|
||||
import random
|
||||
from hashlib import sha512
|
||||
from base64 import b64encode
|
||||
from typing import Dict, Optional
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
|
||||
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:
|
||||
def __init__(self, settings: "OAuth2Settings") -> None:
|
||||
self._settings = settings
|
||||
self._token_url = "{}/token".format(self._settings.OAUTH_SERVER_URL)
|
||||
|
||||
@property
|
||||
# The OAuth2 settings object.
|
||||
## The OAuth2 settings object.
|
||||
def settings(self) -> "OAuth2Settings":
|
||||
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 verification_code: The verification code needed for the PKCE extension.
|
||||
# \return: An AuthenticationResponse object.
|
||||
# \param verification_code: The verification code needed for the PKCE
|
||||
# extension.
|
||||
# \return An AuthenticationResponse object.
|
||||
def getAccessTokenUsingAuthorizationCode(self, authorization_code: str, verification_code: str) -> "AuthenticationResponse":
|
||||
data = {
|
||||
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
||||
@ -37,12 +41,16 @@ class AuthorizationHelpers:
|
||||
"code_verifier": verification_code,
|
||||
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
||||
}
|
||||
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
||||
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:
|
||||
# \return: An AuthenticationResponse object.
|
||||
# \return An AuthenticationResponse object.
|
||||
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
|
||||
Logger.log("d", "Refreshing the access token.")
|
||||
data = {
|
||||
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
||||
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
|
||||
@ -50,12 +58,15 @@ class AuthorizationHelpers:
|
||||
"refresh_token": refresh_token,
|
||||
"scope": self._settings.CLIENT_SCOPES if self._settings.CLIENT_SCOPES is not None else "",
|
||||
}
|
||||
return self.parseTokenResponse(requests.post(self._token_url, data = data)) # type: ignore
|
||||
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
|
||||
# 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.
|
||||
# \return: An AuthenticationResponse object.
|
||||
# \return An AuthenticationResponse object.
|
||||
def parseTokenResponse(token_response: requests.models.Response) -> "AuthenticationResponse":
|
||||
token_data = None
|
||||
|
||||
@ -65,25 +76,31 @@ class AuthorizationHelpers:
|
||||
Logger.log("w", "Could not parse token response data: %s", token_response.text)
|
||||
|
||||
if not token_data:
|
||||
return AuthenticationResponse(success=False, err_message="Could not read response.")
|
||||
return AuthenticationResponse(success = False, err_message = catalog.i18nc("@message", "Could not read response."))
|
||||
|
||||
if token_response.status_code not in (200, 201):
|
||||
return AuthenticationResponse(success=False, err_message=token_data["error_description"])
|
||||
return AuthenticationResponse(success = False, err_message = token_data["error_description"])
|
||||
|
||||
return AuthenticationResponse(success=True,
|
||||
token_type=token_data["token_type"],
|
||||
access_token=token_data["access_token"],
|
||||
refresh_token=token_data["refresh_token"],
|
||||
expires_in=token_data["expires_in"],
|
||||
scope=token_data["scope"])
|
||||
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.
|
||||
# \return: Dict containing some profile data.
|
||||
# \return Dict containing some profile data.
|
||||
def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
|
||||
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
|
||||
"Authorization": "Bearer {}".format(access_token)
|
||||
})
|
||||
try:
|
||||
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):
|
||||
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
|
||||
return None
|
||||
@ -98,15 +115,15 @@ class AuthorizationHelpers:
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
# Generate a 16-character verification code.
|
||||
## Generate a 16-character verification code.
|
||||
# \param code_length: How long should the code be?
|
||||
def generateVerificationCode(code_length: int = 16) -> str:
|
||||
return "".join(random.choice("0123456789ABCDEF") for i in range(code_length))
|
||||
|
||||
@staticmethod
|
||||
# Generates a base64 encoded sha512 encrypted version of a given string.
|
||||
## Generates a base64 encoded sha512 encrypted version of a given string.
|
||||
# \param verification_code:
|
||||
# \return: The encrypted code in base64 format.
|
||||
# \return The encrypted code in base64 format.
|
||||
def generateVerificationCodeChallenge(verification_code: str) -> str:
|
||||
encoded = sha512(verification_code.encode()).digest()
|
||||
return b64encode(encoded, altchars = b"_-").decode()
|
||||
|
@ -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.
|
||||
from typing import Optional, Callable, Tuple, Dict, Any, List, TYPE_CHECKING
|
||||
|
||||
from http.server import BaseHTTPRequestHandler
|
||||
from typing import Optional, Callable, Tuple, Dict, Any, List, TYPE_CHECKING
|
||||
from urllib.parse import parse_qs, urlparse
|
||||
|
||||
from cura.OAuth2.Models import AuthenticationResponse, ResponseData, HTTP_STATUS
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.OAuth2.Models import ResponseStatus
|
||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
# 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.
|
||||
## This handler handles all HTTP requests on the local web server.
|
||||
# It also requests the access token for the 2nd stage of the OAuth flow.
|
||||
class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
||||
def __init__(self, request, client_address, server) -> None:
|
||||
super().__init__(request, client_address, server)
|
||||
|
||||
# These values will be injected by the HTTPServer that this handler belongs to.
|
||||
self.authorization_helpers = None # type: Optional["AuthorizationHelpers"]
|
||||
self.authorization_helpers = None # type: Optional[AuthorizationHelpers]
|
||||
self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]]
|
||||
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.
|
||||
self.authorization_callback(token_response)
|
||||
|
||||
# Handler for the callback URL redirect.
|
||||
# \param query: Dict containing the HTTP query parameters.
|
||||
# \return: HTTP ResponseData containing a success page to show to the user.
|
||||
## Handler for the callback URL redirect.
|
||||
# \param query Dict containing the HTTP query parameters.
|
||||
# \return HTTP ResponseData containing a success page to show to the user.
|
||||
def _handleCallback(self, query: Dict[Any, List]) -> Tuple[ResponseData, Optional[AuthenticationResponse]]:
|
||||
code = self._queryGet(query, "code")
|
||||
if code and self.authorization_helpers is not None and self.verification_code is not None:
|
||||
@ -60,30 +62,30 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
||||
elif self._queryGet(query, "error_code") == "user_denied":
|
||||
# Otherwise we show an error message (probably the user clicked "Deny" in the auth dialog).
|
||||
token_response = AuthenticationResponse(
|
||||
success=False,
|
||||
err_message="Please give the required permissions when authorizing this application."
|
||||
success = False,
|
||||
err_message = catalog.i18nc("@message", "Please give the required permissions when authorizing this application.")
|
||||
)
|
||||
|
||||
else:
|
||||
# We don't know what went wrong here, so instruct the user to check the logs.
|
||||
token_response = AuthenticationResponse(
|
||||
success=False,
|
||||
error_message="Something unexpected happened when trying to log in, please try again."
|
||||
success = False,
|
||||
error_message = catalog.i18nc("@message", "Something unexpected happened when trying to log in, please try again.")
|
||||
)
|
||||
if self.authorization_helpers is None:
|
||||
return ResponseData(), token_response
|
||||
|
||||
return ResponseData(
|
||||
status=HTTP_STATUS["REDIRECT"],
|
||||
data_stream=b"Redirecting...",
|
||||
redirect_uri=self.authorization_helpers.settings.AUTH_SUCCESS_REDIRECT if token_response.success else
|
||||
status = HTTP_STATUS["REDIRECT"],
|
||||
data_stream = b"Redirecting...",
|
||||
redirect_uri = self.authorization_helpers.settings.AUTH_SUCCESS_REDIRECT if token_response.success else
|
||||
self.authorization_helpers.settings.AUTH_FAILED_REDIRECT
|
||||
), token_response
|
||||
|
||||
## Handle all other non-existing server calls.
|
||||
@staticmethod
|
||||
# Handle all other non-existing server calls.
|
||||
def _handleNotFound() -> ResponseData:
|
||||
return ResponseData(status=HTTP_STATUS["NOT_FOUND"], content_type="text/html", data_stream=b"Not found.")
|
||||
return ResponseData(status = HTTP_STATUS["NOT_FOUND"], content_type = "text/html", data_stream = b"Not found.")
|
||||
|
||||
def _sendHeaders(self, status: "ResponseStatus", content_type: str, redirect_uri: str = None) -> None:
|
||||
self.send_response(status.code, status.message)
|
||||
@ -95,7 +97,7 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
||||
def _sendData(self, data: bytes) -> None:
|
||||
self.wfile.write(data)
|
||||
|
||||
## Convenience helper for getting values from a pre-parsed query string
|
||||
@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]
|
||||
|
@ -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.
|
||||
|
||||
from http.server import HTTPServer
|
||||
from typing import Callable, Any, TYPE_CHECKING
|
||||
|
||||
@ -8,19 +9,19 @@ if TYPE_CHECKING:
|
||||
from cura.OAuth2.AuthorizationHelpers import AuthorizationHelpers
|
||||
|
||||
|
||||
# The authorization request callback handler server.
|
||||
# This subclass is needed to be able to pass some data to the request handler.
|
||||
# This cannot be done on the request handler directly as the HTTPServer creates an instance of the handler after
|
||||
# init.
|
||||
## The authorization request callback handler server.
|
||||
# This subclass is needed to be able to pass some data to the request handler.
|
||||
# This cannot be done on the request handler directly as the HTTPServer
|
||||
# creates an instance of the handler after init.
|
||||
class AuthorizationRequestServer(HTTPServer):
|
||||
# Set the authorization helpers instance on the request handler.
|
||||
## Set the authorization helpers instance on the request handler.
|
||||
def setAuthorizationHelpers(self, authorization_helpers: "AuthorizationHelpers") -> None:
|
||||
self.RequestHandlerClass.authorization_helpers = authorization_helpers # type: ignore
|
||||
|
||||
# Set the authorization callback on the request handler.
|
||||
## Set the authorization callback on the request handler.
|
||||
def setAuthorizationCallback(self, authorization_callback: Callable[["AuthenticationResponse"], Any]) -> None:
|
||||
self.RequestHandlerClass.authorization_callback = authorization_callback # type: ignore
|
||||
|
||||
# Set the verification code on the request handler.
|
||||
## Set the verification code on the request handler.
|
||||
def setVerificationCode(self, verification_code: str) -> None:
|
||||
self.RequestHandlerClass.verification_code = verification_code # type: ignore
|
||||
|
@ -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.
|
||||
|
||||
import json
|
||||
import webbrowser
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from urllib.parse import urlencode
|
||||
import requests.exceptions
|
||||
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Signal import Signal
|
||||
|
||||
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 UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.OAuth2.Models import UserProfile, OAuth2Settings
|
||||
from UM.Preferences import Preferences
|
||||
|
||||
|
||||
## The authorization service is responsible for handling the login flow,
|
||||
# storing user credentials and providing account information.
|
||||
class AuthorizationService:
|
||||
"""
|
||||
The authorization service is responsible for handling the login flow,
|
||||
storing user credentials and providing account information.
|
||||
"""
|
||||
|
||||
# Emit signal when authentication is completed.
|
||||
onAuthStateChanged = Signal()
|
||||
|
||||
@ -38,31 +43,48 @@ class AuthorizationService:
|
||||
self._preferences = preferences
|
||||
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:
|
||||
if preferences is not None:
|
||||
self._preferences = preferences
|
||||
if self._preferences:
|
||||
self._preferences.addPreference(self._settings.AUTH_DATA_PREFERENCE_KEY, "{}")
|
||||
|
||||
# Get the user profile as obtained from the JWT (JSON Web Token).
|
||||
## Get the user profile as obtained from the JWT (JSON Web Token).
|
||||
# If the JWT is not yet parsed, calling this will take care of that.
|
||||
# \return UserProfile if a user is logged in, None otherwise.
|
||||
# \sa _parseJWT
|
||||
def getUserProfile(self) -> Optional["UserProfile"]:
|
||||
if not self._user_profile:
|
||||
# If no user profile was stored locally, we try to get it from JWT.
|
||||
self._user_profile = self._parseJWT()
|
||||
if not self._user_profile:
|
||||
try:
|
||||
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.
|
||||
Logger.log("w", "The user profile could not be loaded. The user must log in again!")
|
||||
self.deleteAuthData()
|
||||
return None
|
||||
|
||||
return self._user_profile
|
||||
|
||||
# Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there.
|
||||
## Tries to parse the JWT (JSON Web Token) data, which it does if all the needed data is there.
|
||||
# \return UserProfile if it was able to parse, None otherwise.
|
||||
def _parseJWT(self) -> Optional["UserProfile"]:
|
||||
if not self._auth_data or self._auth_data.access_token is None:
|
||||
# If no auth data exists, we should always log in again.
|
||||
Logger.log("d", "There was no auth data or access token")
|
||||
return None
|
||||
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||
if user_data:
|
||||
@ -70,41 +92,51 @@ class AuthorizationService:
|
||||
return user_data
|
||||
# The JWT was expired or invalid and we should request a new one.
|
||||
if self._auth_data.refresh_token is None:
|
||||
Logger.log("w", "There was no refresh token in the auth data.")
|
||||
return None
|
||||
self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
|
||||
if not self._auth_data or self._auth_data.access_token is None:
|
||||
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.
|
||||
return None
|
||||
|
||||
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]:
|
||||
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:
|
||||
Logger.log("d", "No auth data to retrieve the access_token from")
|
||||
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:
|
||||
if self._auth_data is None or self._auth_data.refresh_token is None:
|
||||
Logger.log("w", "Unable to refresh access token, since there is no refresh token.")
|
||||
return
|
||||
self._storeAuthData(self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token))
|
||||
self.onAuthStateChanged.emit(logged_in=True)
|
||||
response = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
|
||||
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:
|
||||
if self._auth_data is not None:
|
||||
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:
|
||||
Logger.log("d", "Starting new OAuth2 flow...")
|
||||
|
||||
@ -120,7 +152,7 @@ class AuthorizationService:
|
||||
"redirect_uri": self._settings.CALLBACK_URL,
|
||||
"scope": self._settings.CLIENT_SCOPES,
|
||||
"response_type": "code",
|
||||
"state": "CuraDriveIsAwesome",
|
||||
"state": "(.Y.)",
|
||||
"code_challenge": challenge_code,
|
||||
"code_challenge_method": "S512"
|
||||
})
|
||||
@ -131,16 +163,16 @@ class AuthorizationService:
|
||||
# Start a local web server to receive the callback URL on.
|
||||
self._server.start(verification_code)
|
||||
|
||||
# Callback method for the authentication flow.
|
||||
## Callback method for the authentication flow.
|
||||
def _onAuthStateChanged(self, auth_response: AuthenticationResponse) -> None:
|
||||
if auth_response.success:
|
||||
self._storeAuthData(auth_response)
|
||||
self.onAuthStateChanged.emit(logged_in=True)
|
||||
self.onAuthStateChanged.emit(logged_in = True)
|
||||
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.
|
||||
|
||||
# Load authentication data from preferences.
|
||||
## Load authentication data from preferences.
|
||||
def loadAuthDataFromPreferences(self) -> None:
|
||||
if self._preferences is None:
|
||||
Logger.log("e", "Unable to load authentication data, since no preference has been set!")
|
||||
@ -149,11 +181,22 @@ class AuthorizationService:
|
||||
preferences_data = json.loads(self._preferences.getValue(self._settings.AUTH_DATA_PREFERENCE_KEY))
|
||||
if preferences_data:
|
||||
self._auth_data = AuthenticationResponse(**preferences_data)
|
||||
self.onAuthStateChanged.emit(logged_in=True)
|
||||
# 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:
|
||||
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:
|
||||
if self._preferences is None:
|
||||
Logger.log("e", "Unable to save authentication data, since no preference has been set!")
|
||||
@ -166,3 +209,7 @@ class AuthorizationService:
|
||||
else:
|
||||
self._user_profile = None
|
||||
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
|
||||
|
||||
def _onMessageActionTriggered(self, _, action):
|
||||
if action == "retry":
|
||||
self.loadAuthDataFromPreferences()
|
||||
|
@ -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.
|
||||
|
||||
import threading
|
||||
from typing import Optional, Callable, Any, TYPE_CHECKING
|
||||
|
||||
@ -14,12 +15,15 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class LocalAuthorizationServer:
|
||||
# The local LocalAuthorizationServer takes care of the oauth2 callbacks.
|
||||
# Once the flow is completed, this server should be closed down again by calling stop()
|
||||
# \param auth_helpers: An instance of the authorization helpers class.
|
||||
# \param auth_state_changed_callback: A callback function to be called when the authorization state changes.
|
||||
# \param daemon: Whether the server thread should be run in daemon mode. Note: Daemon threads are abruptly stopped
|
||||
# at shutdown. Their resources (e.g. open files) may never be released.
|
||||
## The local LocalAuthorizationServer takes care of the oauth2 callbacks.
|
||||
# Once the flow is completed, this server should be closed down again by
|
||||
# calling stop()
|
||||
# \param auth_helpers An instance of the authorization helpers class.
|
||||
# \param auth_state_changed_callback A callback function to be called when
|
||||
# the authorization state changes.
|
||||
# \param daemon Whether the server thread should be run in daemon mode.
|
||||
# Note: Daemon threads are abruptly stopped at shutdown. Their resources
|
||||
# (e.g. open files) may never be released.
|
||||
def __init__(self, auth_helpers: "AuthorizationHelpers",
|
||||
auth_state_changed_callback: Callable[["AuthenticationResponse"], Any],
|
||||
daemon: bool) -> None:
|
||||
@ -30,8 +34,8 @@ class LocalAuthorizationServer:
|
||||
self._auth_state_changed_callback = auth_state_changed_callback
|
||||
self._daemon = daemon
|
||||
|
||||
# Starts the local web server to handle the authorization callback.
|
||||
# \param verification_code: The verification code part of the OAuth2 client identification.
|
||||
## Starts the local web server to handle the authorization callback.
|
||||
# \param verification_code The verification code part of the OAuth2 client identification.
|
||||
def start(self, verification_code: str) -> None:
|
||||
if self._web_server:
|
||||
# If the server is already running (because of a previously aborted auth flow), we don't have to start it.
|
||||
@ -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.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:
|
||||
Logger.log("d", "Stopping local oauth2 web server...")
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
@ -7,7 +8,7 @@ class BaseModel:
|
||||
self.__dict__.update(kwargs)
|
||||
|
||||
|
||||
# OAuth OAuth2Settings data template.
|
||||
## OAuth OAuth2Settings data template.
|
||||
class OAuth2Settings(BaseModel):
|
||||
CALLBACK_PORT = None # type: Optional[int]
|
||||
OAUTH_SERVER_URL = None # type: Optional[str]
|
||||
@ -19,14 +20,14 @@ class OAuth2Settings(BaseModel):
|
||||
AUTH_FAILED_REDIRECT = "https://ultimaker.com" # type: str
|
||||
|
||||
|
||||
# User profile data template.
|
||||
## User profile data template.
|
||||
class UserProfile(BaseModel):
|
||||
user_id = None # type: Optional[str]
|
||||
username = None # type: Optional[str]
|
||||
profile_image_url = None # type: Optional[str]
|
||||
|
||||
|
||||
# Authentication data template.
|
||||
## Authentication data template.
|
||||
class AuthenticationResponse(BaseModel):
|
||||
"""Data comes from the token response with success flag and error message added."""
|
||||
success = True # type: bool
|
||||
@ -36,25 +37,25 @@ class AuthenticationResponse(BaseModel):
|
||||
expires_in = None # type: Optional[str]
|
||||
scope = 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):
|
||||
code = 200 # type: int
|
||||
message = "" # type str
|
||||
message = "" # type: str
|
||||
|
||||
|
||||
# Response data template.
|
||||
## Response data template.
|
||||
class ResponseData(BaseModel):
|
||||
status = None # type: ResponseStatus
|
||||
data_stream = None # type: Optional[bytes]
|
||||
redirect_uri = None # type: Optional[str]
|
||||
content_type = "text/html" # type: str
|
||||
|
||||
|
||||
# Possible HTTP responses.
|
||||
## Possible HTTP responses.
|
||||
HTTP_STATUS = {
|
||||
"OK": ResponseStatus(code=200, message="OK"),
|
||||
"NOT_FOUND": ResponseStatus(code=404, message="NOT FOUND"),
|
||||
"REDIRECT": ResponseStatus(code=302, message="REDIRECT")
|
||||
"OK": ResponseStatus(code = 200, message = "OK"),
|
||||
"NOT_FOUND": ResponseStatus(code = 404, message = "NOT FOUND"),
|
||||
"REDIRECT": ResponseStatus(code = 302, message = "REDIRECT")
|
||||
}
|
||||
|
@ -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.
|
||||
|
@ -29,4 +29,4 @@ class PlatformPhysicsOperation(Operation):
|
||||
return group
|
||||
|
||||
def __repr__(self):
|
||||
return "PlatformPhysicsOperation(translation = {0})".format(self._translation)
|
||||
return "PlatformPhysicsOp.(trans.={0})".format(self._translation)
|
||||
|
@ -17,7 +17,6 @@ from cura.Scene import ZOffsetDecorator
|
||||
|
||||
import random # used for list shuffling
|
||||
|
||||
|
||||
class PlatformPhysics:
|
||||
def __init__(self, controller, volume):
|
||||
super().__init__()
|
||||
@ -40,8 +39,9 @@ class PlatformPhysics:
|
||||
Application.getInstance().getPreferences().addPreference("physics/automatic_drop_down", True)
|
||||
|
||||
def _onSceneChanged(self, source):
|
||||
if not source.getMeshData():
|
||||
if not source.callDecoration("isSliceable"):
|
||||
return
|
||||
|
||||
self._change_timer.start()
|
||||
|
||||
def _onChangeTimerFinished(self):
|
||||
|
@ -9,7 +9,7 @@ from typing import Union
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||
|
||||
class FirmwareUpdater(QObject):
|
||||
firmwareProgressChanged = pyqtSignal()
|
||||
|
@ -3,14 +3,15 @@
|
||||
|
||||
from typing import TYPE_CHECKING, Set, Union, Optional
|
||||
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
||||
from .PrinterOutputController import PrinterOutputController
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
||||
from .Models.PrintJobOutputModel import PrintJobOutputModel
|
||||
from .Models.PrinterOutputModel import PrinterOutputModel
|
||||
from .PrinterOutputDevice import PrinterOutputDevice
|
||||
from .Models.ExtruderOutputModel import ExtruderOutputModel
|
||||
|
||||
|
||||
class GenericOutputController(PrinterOutputController):
|
||||
@ -81,8 +82,8 @@ class GenericOutputController(PrinterOutputController):
|
||||
self._output_device.cancelPrint()
|
||||
pass
|
||||
|
||||
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: int) -> None:
|
||||
self._output_device.sendCommand("M140 S%s" % temperature)
|
||||
def setTargetBedTemperature(self, printer: "PrinterOutputModel", temperature: float) -> None:
|
||||
self._output_device.sendCommand("M140 S%s" % round(temperature)) # The API doesn't allow floating point.
|
||||
|
||||
def _onTargetBedTemperatureChanged(self) -> None:
|
||||
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:
|
||||
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.start()
|
||||
self._preheat_printer = printer
|
||||
printer.updateIsPreheating(True)
|
||||
|
||||
def cancelPreheatBed(self, printer: "PrinterOutputModel") -> None:
|
||||
self.setTargetBedTemperature(printer, temperature=0)
|
||||
self.setTargetBedTemperature(printer, temperature = 0)
|
||||
self._preheat_bed_timer.stop()
|
||||
printer.updateIsPreheating(False)
|
||||
|
||||
|
@ -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
|
@ -4,7 +4,7 @@ from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
|
||||
|
||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||
from .MaterialOutputModel import MaterialOutputModel
|
||||
|
||||
|
||||
class ExtruderConfigurationModel(QObject):
|
@ -1,14 +1,15 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
||||
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
||||
|
||||
from .ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||
from .MaterialOutputModel import MaterialOutputModel
|
||||
from .PrinterOutputModel import PrinterOutputModel
|
||||
|
||||
|
||||
class ExtruderOutputModel(QObject):
|
36
cura/PrinterOutput/Models/MaterialOutputModel.py
Normal file
36
cura/PrinterOutput/Models/MaterialOutputModel.py
Normal 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
|
@ -1,16 +1,16 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
||||
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
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||
|
||||
|
||||
class PrintJobOutputModel(QObject):
|
||||
stateChanged = pyqtSignal()
|
||||
@ -24,7 +24,7 @@ class PrintJobOutputModel(QObject):
|
||||
previewImageChanged = 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)
|
||||
self._output_controller = output_controller
|
||||
self._state = ""
|
||||
@ -35,7 +35,7 @@ class PrintJobOutputModel(QObject):
|
||||
self._assigned_printer = None # type: Optional[PrinterOutputModel]
|
||||
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._preview_image_id = 0
|
||||
|
||||
@ -44,7 +44,7 @@ class PrintJobOutputModel(QObject):
|
||||
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
|
||||
def compatibleMachineFamilies(self):
|
||||
# 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:
|
||||
if self._compatible_machine_families != compatible_machine_families:
|
||||
@ -54,7 +54,7 @@ class PrintJobOutputModel(QObject):
|
||||
@pyqtProperty(QUrl, notify=previewImageChanged)
|
||||
def previewImageUrl(self):
|
||||
self._preview_image_id += 1
|
||||
# There is an image provider that is called "camera". In order to ensure that the image qml object, that
|
||||
# There is an image provider that is called "print_job_preview". In order to ensure that the image qml object, that
|
||||
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
|
||||
# as new (instead of relying on cached version and thus forces an update.
|
||||
temp = "image://print_job_preview/" + str(self._preview_image_id) + "/" + self._key
|
||||
@ -69,10 +69,10 @@ class PrintJobOutputModel(QObject):
|
||||
self.previewImageChanged.emit()
|
||||
|
||||
@pyqtProperty(QObject, notify=configurationChanged)
|
||||
def configuration(self) -> Optional["ConfigurationModel"]:
|
||||
def configuration(self) -> Optional["PrinterConfigurationModel"]:
|
||||
return self._configuration
|
||||
|
||||
def updateConfiguration(self, configuration: Optional["ConfigurationModel"]) -> None:
|
||||
def updateConfiguration(self, configuration: Optional["PrinterConfigurationModel"]) -> None:
|
||||
if self._configuration != configuration:
|
||||
self._configuration = configuration
|
||||
self.configurationChanged.emit()
|
||||
@ -118,17 +118,39 @@ class PrintJobOutputModel(QObject):
|
||||
self.nameChanged.emit()
|
||||
|
||||
@pyqtProperty(int, notify = timeTotalChanged)
|
||||
def timeTotal(self):
|
||||
def timeTotal(self) -> int:
|
||||
return self._time_total
|
||||
|
||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||
def timeElapsed(self):
|
||||
def timeElapsed(self) -> int:
|
||||
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)
|
||||
def state(self):
|
||||
def state(self) -> str:
|
||||
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):
|
||||
if self._time_total != new_time_total:
|
||||
self._time_total = new_time_total
|
@ -6,10 +6,10 @@ from typing import List
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||
|
||||
|
||||
class ConfigurationModel(QObject):
|
||||
class PrinterConfigurationModel(QObject):
|
||||
|
||||
configurationChanged = pyqtSignal()
|
||||
|
||||
@ -19,14 +19,14 @@ class ConfigurationModel(QObject):
|
||||
self._extruder_configurations = [] # type: List[ExtruderConfigurationModel]
|
||||
self._buildplate_configuration = ""
|
||||
|
||||
def setPrinterType(self, printer_type):
|
||||
def setPrinterType(self, printer_type: str) -> None:
|
||||
self._printer_type = printer_type
|
||||
|
||||
@pyqtProperty(str, fset = setPrinterType, notify = configurationChanged)
|
||||
def printerType(self) -> str:
|
||||
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:
|
||||
self._extruder_configurations = extruder_configurations
|
||||
|
||||
@ -40,7 +40,9 @@ class ConfigurationModel(QObject):
|
||||
return self._extruder_configurations
|
||||
|
||||
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)
|
||||
def buildplateConfiguration(self) -> str:
|
||||
@ -54,7 +56,7 @@ class ConfigurationModel(QObject):
|
||||
for configuration in self._extruder_configurations:
|
||||
if configuration is None:
|
||||
return False
|
||||
return self._printer_type is not None
|
||||
return self._printer_type != ""
|
||||
|
||||
def __str__(self):
|
||||
message_chunks = []
|
@ -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.
|
||||
|
||||
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 UM.Math.Vector import Vector
|
||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
||||
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||
from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
from cura.PrinterOutput.NetworkCamera import NetworkCamera
|
||||
|
||||
|
||||
class PrinterOutputModel(QObject):
|
||||
@ -23,43 +22,47 @@ class PrinterOutputModel(QObject):
|
||||
nameChanged = pyqtSignal()
|
||||
headPositionChanged = pyqtSignal()
|
||||
keyChanged = pyqtSignal()
|
||||
printerTypeChanged = pyqtSignal()
|
||||
typeChanged = pyqtSignal()
|
||||
buildplateChanged = pyqtSignal()
|
||||
cameraChanged = pyqtSignal()
|
||||
cameraUrlChanged = pyqtSignal()
|
||||
configurationChanged = pyqtSignal()
|
||||
canUpdateFirmwareChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, output_controller: "PrinterOutputController", number_of_extruders: int = 1, parent=None, firmware_version = "") -> None:
|
||||
super().__init__(parent)
|
||||
self._bed_temperature = -1 # Use -1 for no heated bed.
|
||||
self._target_bed_temperature = 0
|
||||
self._bed_temperature = -1 # type: float # Use -1 for no heated bed.
|
||||
self._target_bed_temperature = 0 # type: float
|
||||
self._name = ""
|
||||
self._key = "" # Unique identifier
|
||||
self._controller = output_controller
|
||||
self._controller.canUpdateFirmwareChanged.connect(self._onControllerCanUpdateFirmwareChanged)
|
||||
self._extruders = [ExtruderOutputModel(printer = self, position = i) for i in range(number_of_extruders)]
|
||||
self._printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
|
||||
self._printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer
|
||||
self._head_position = Vector(0, 0, 0)
|
||||
self._active_print_job = None # type: Optional[PrintJobOutputModel]
|
||||
self._firmware_version = firmware_version
|
||||
self._printer_state = "unknown"
|
||||
self._is_preheating = False
|
||||
self._printer_type = ""
|
||||
self._buildplate_name = ""
|
||||
self._buildplate = ""
|
||||
|
||||
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
|
||||
self._extruders]
|
||||
|
||||
self._camera = None # type: Optional[NetworkCamera]
|
||||
self._camera_url = QUrl() # type: QUrl
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def firmwareVersion(self) -> str:
|
||||
return self._firmware_version
|
||||
|
||||
def setCamera(self, camera: Optional["NetworkCamera"]) -> None:
|
||||
if self._camera is not camera:
|
||||
self._camera = camera
|
||||
self.cameraChanged.emit()
|
||||
def setCameraUrl(self, camera_url: "QUrl") -> None:
|
||||
if self._camera_url != camera_url:
|
||||
self._camera_url = camera_url
|
||||
self.cameraUrlChanged.emit()
|
||||
|
||||
@pyqtProperty(QUrl, fset = setCameraUrl, notify = cameraUrlChanged)
|
||||
def cameraUrl(self) -> "QUrl":
|
||||
return self._camera_url
|
||||
|
||||
def updateIsPreheating(self, pre_heating: bool) -> None:
|
||||
if self._is_preheating != pre_heating:
|
||||
@ -70,11 +73,7 @@ class PrinterOutputModel(QObject):
|
||||
def isPreheating(self) -> bool:
|
||||
return self._is_preheating
|
||||
|
||||
@pyqtProperty(QObject, notify=cameraChanged)
|
||||
def camera(self) -> Optional["NetworkCamera"]:
|
||||
return self._camera
|
||||
|
||||
@pyqtProperty(str, notify = printerTypeChanged)
|
||||
@pyqtProperty(str, notify = typeChanged)
|
||||
def type(self) -> str:
|
||||
return self._printer_type
|
||||
|
||||
@ -82,17 +81,17 @@ class PrinterOutputModel(QObject):
|
||||
if self._printer_type != printer_type:
|
||||
self._printer_type = printer_type
|
||||
self._printer_configuration.printerType = self._printer_type
|
||||
self.printerTypeChanged.emit()
|
||||
self.typeChanged.emit()
|
||||
self.configurationChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify = buildplateChanged)
|
||||
def buildplate(self) -> str:
|
||||
return self._buildplate_name
|
||||
return self._buildplate
|
||||
|
||||
def updateBuildplateName(self, buildplate_name: str) -> None:
|
||||
if self._buildplate_name != buildplate_name:
|
||||
self._buildplate_name = buildplate_name
|
||||
self._printer_configuration.buildplateConfiguration = self._buildplate_name
|
||||
def updateBuildplate(self, buildplate: str) -> None:
|
||||
if self._buildplate != buildplate:
|
||||
self._buildplate = buildplate
|
||||
self._printer_configuration.buildplateConfiguration = self._buildplate
|
||||
self.buildplateChanged.emit()
|
||||
self.configurationChanged.emit()
|
||||
|
||||
@ -180,7 +179,6 @@ class PrinterOutputModel(QObject):
|
||||
return self._name
|
||||
|
||||
def setName(self, name: str) -> None:
|
||||
self._setName(name)
|
||||
self.updateName(name)
|
||||
|
||||
def updateName(self, name: str) -> None:
|
||||
@ -189,19 +187,19 @@ class PrinterOutputModel(QObject):
|
||||
self.nameChanged.emit()
|
||||
|
||||
## 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:
|
||||
self._bed_temperature = temperature
|
||||
self.bedTemperatureChanged.emit()
|
||||
|
||||
def updateTargetBedTemperature(self, temperature: int) -> None:
|
||||
def updateTargetBedTemperature(self, temperature: float) -> None:
|
||||
if self._target_bed_temperature != temperature:
|
||||
self._target_bed_temperature = temperature
|
||||
self.targetBedTemperatureChanged.emit()
|
||||
|
||||
## Set the target bed temperature. This ensures that it's actually sent to the remote.
|
||||
@pyqtSlot(int)
|
||||
def setTargetBedTemperature(self, temperature: int) -> None:
|
||||
@pyqtSlot(float)
|
||||
def setTargetBedTemperature(self, temperature: float) -> None:
|
||||
self._controller.setTargetBedTemperature(self, temperature)
|
||||
self.updateTargetBedTemperature(temperature)
|
||||
|
||||
@ -226,55 +224,55 @@ class PrinterOutputModel(QObject):
|
||||
def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
|
||||
return self._active_print_job
|
||||
|
||||
@pyqtProperty(str, notify=stateChanged)
|
||||
@pyqtProperty(str, notify = stateChanged)
|
||||
def state(self) -> str:
|
||||
return self._printer_state
|
||||
|
||||
@pyqtProperty(int, notify=bedTemperatureChanged)
|
||||
def bedTemperature(self) -> int:
|
||||
@pyqtProperty(float, notify = bedTemperatureChanged)
|
||||
def bedTemperature(self) -> float:
|
||||
return self._bed_temperature
|
||||
|
||||
@pyqtProperty(int, notify=targetBedTemperatureChanged)
|
||||
def targetBedTemperature(self) -> int:
|
||||
@pyqtProperty(float, notify = targetBedTemperatureChanged)
|
||||
def targetBedTemperature(self) -> float:
|
||||
return self._target_bed_temperature
|
||||
|
||||
# Does the printer support pre-heating the bed at all
|
||||
@pyqtProperty(bool, constant=True)
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canPreHeatBed(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_pre_heat_bed
|
||||
return False
|
||||
|
||||
# Does the printer support pre-heating the bed at all
|
||||
@pyqtProperty(bool, constant=True)
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canPreHeatHotends(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_pre_heat_hotends
|
||||
return False
|
||||
|
||||
# Does the printer support sending raw G-code at all
|
||||
@pyqtProperty(bool, constant=True)
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canSendRawGcode(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_send_raw_gcode
|
||||
return False
|
||||
|
||||
# Does the printer support pause at all
|
||||
@pyqtProperty(bool, constant=True)
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canPause(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_pause
|
||||
return False
|
||||
|
||||
# Does the printer support abort at all
|
||||
@pyqtProperty(bool, constant=True)
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canAbort(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_abort
|
||||
return False
|
||||
|
||||
# Does the printer support manual control at all
|
||||
@pyqtProperty(bool, constant=True)
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def canControlManually(self) -> bool:
|
||||
if self._controller:
|
||||
return self._controller.can_control_manually
|
||||
@ -293,7 +291,7 @@ class PrinterOutputModel(QObject):
|
||||
|
||||
# Returns the configuration (material, variant and buildplate) of the current printer
|
||||
@pyqtProperty(QObject, notify = configurationChanged)
|
||||
def printerConfiguration(self) -> Optional[ConfigurationModel]:
|
||||
def printerConfiguration(self) -> Optional[PrinterConfigurationModel]:
|
||||
if self._printer_configuration.isValid():
|
||||
return self._printer_configuration
|
||||
return None
|
0
cura/PrinterOutput/Models/__init__.py
Normal file
0
cura/PrinterOutput/Models/__init__.py
Normal 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()
|
153
cura/PrinterOutput/NetworkMJPGImage.py
Normal file
153
cura/PrinterOutput/NetworkMJPGImage.py
Normal file
@ -0,0 +1,153 @@
|
||||
# Copyright (c) 2018 Aldo Hoeben / fieldOfView
|
||||
# NetworkMJPGImage is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QUrl, pyqtProperty, pyqtSignal, pyqtSlot, QRect, QByteArray
|
||||
from PyQt5.QtGui import QImage, QPainter
|
||||
from PyQt5.QtQuick import QQuickPaintedItem
|
||||
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
|
||||
|
||||
from UM.Logger import Logger
|
||||
|
||||
#
|
||||
# A QQuickPaintedItem that progressively downloads a network mjpeg stream,
|
||||
# picks it apart in individual jpeg frames, and paints it.
|
||||
#
|
||||
class NetworkMJPGImage(QQuickPaintedItem):
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
self._stream_buffer = QByteArray()
|
||||
self._stream_buffer_start_index = -1
|
||||
self._network_manager = None # type: QNetworkAccessManager
|
||||
self._image_request = None # type: QNetworkRequest
|
||||
self._image_reply = None # type: QNetworkReply
|
||||
self._image = QImage()
|
||||
self._image_rect = QRect()
|
||||
|
||||
self._source_url = QUrl()
|
||||
self._started = False
|
||||
|
||||
self._mirror = False
|
||||
|
||||
self.setAntialiasing(True)
|
||||
|
||||
## Ensure that close gets called when object is destroyed
|
||||
def __del__(self) -> None:
|
||||
self.stop()
|
||||
|
||||
|
||||
def paint(self, painter: "QPainter") -> None:
|
||||
if self._mirror:
|
||||
painter.drawImage(self.contentsBoundingRect(), self._image.mirrored())
|
||||
return
|
||||
|
||||
painter.drawImage(self.contentsBoundingRect(), self._image)
|
||||
|
||||
|
||||
def setSourceURL(self, source_url: "QUrl") -> None:
|
||||
self._source_url = source_url
|
||||
self.sourceURLChanged.emit()
|
||||
if self._started:
|
||||
self.start()
|
||||
|
||||
def getSourceURL(self) -> "QUrl":
|
||||
return self._source_url
|
||||
|
||||
sourceURLChanged = pyqtSignal()
|
||||
source = pyqtProperty(QUrl, fget = getSourceURL, fset = setSourceURL, notify = sourceURLChanged)
|
||||
|
||||
def setMirror(self, mirror: bool) -> None:
|
||||
if mirror == self._mirror:
|
||||
return
|
||||
self._mirror = mirror
|
||||
self.mirrorChanged.emit()
|
||||
self.update()
|
||||
|
||||
def getMirror(self) -> bool:
|
||||
return self._mirror
|
||||
|
||||
mirrorChanged = pyqtSignal()
|
||||
mirror = pyqtProperty(bool, fget = getMirror, fset = setMirror, notify = mirrorChanged)
|
||||
|
||||
imageSizeChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(int, notify = imageSizeChanged)
|
||||
def imageWidth(self) -> int:
|
||||
return self._image.width()
|
||||
|
||||
@pyqtProperty(int, notify = imageSizeChanged)
|
||||
def imageHeight(self) -> int:
|
||||
return self._image.height()
|
||||
|
||||
|
||||
@pyqtSlot()
|
||||
def start(self) -> None:
|
||||
self.stop() # Ensure that previous requests (if any) are stopped.
|
||||
|
||||
if not self._source_url:
|
||||
Logger.log("w", "Unable to start camera stream without target!")
|
||||
return
|
||||
self._started = True
|
||||
|
||||
self._image_request = QNetworkRequest(self._source_url)
|
||||
if self._network_manager is None:
|
||||
self._network_manager = QNetworkAccessManager()
|
||||
|
||||
self._image_reply = self._network_manager.get(self._image_request)
|
||||
self._image_reply.downloadProgress.connect(self._onStreamDownloadProgress)
|
||||
|
||||
@pyqtSlot()
|
||||
def stop(self) -> None:
|
||||
self._stream_buffer = QByteArray()
|
||||
self._stream_buffer_start_index = -1
|
||||
|
||||
if self._image_reply:
|
||||
try:
|
||||
try:
|
||||
self._image_reply.downloadProgress.disconnect(self._onStreamDownloadProgress)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not self._image_reply.isFinished():
|
||||
self._image_reply.close()
|
||||
except Exception as e: # RuntimeError
|
||||
pass # It can happen that the wrapped c++ object is already deleted.
|
||||
|
||||
self._image_reply = None
|
||||
self._image_request = None
|
||||
|
||||
self._network_manager = None
|
||||
|
||||
self._started = False
|
||||
|
||||
|
||||
def _onStreamDownloadProgress(self, bytes_received: int, bytes_total: int) -> None:
|
||||
# An MJPG stream is (for our purpose) a stream of concatenated JPG images.
|
||||
# JPG images start with the marker 0xFFD8, and end with 0xFFD9
|
||||
if self._image_reply is None:
|
||||
return
|
||||
self._stream_buffer += self._image_reply.readAll()
|
||||
|
||||
if len(self._stream_buffer) > 2000000: # No single camera frame should be 2 Mb or larger
|
||||
Logger.log("w", "MJPEG buffer exceeds reasonable size. Restarting stream...")
|
||||
self.stop() # resets stream buffer and start index
|
||||
self.start()
|
||||
return
|
||||
|
||||
if self._stream_buffer_start_index == -1:
|
||||
self._stream_buffer_start_index = self._stream_buffer.indexOf(b'\xff\xd8')
|
||||
stream_buffer_end_index = self._stream_buffer.lastIndexOf(b'\xff\xd9')
|
||||
# If this happens to be more than a single frame, then so be it; the JPG decoder will
|
||||
# ignore the extra data. We do it like this in order not to get a buildup of frames
|
||||
|
||||
if self._stream_buffer_start_index != -1 and stream_buffer_end_index != -1:
|
||||
jpg_data = self._stream_buffer[self._stream_buffer_start_index:stream_buffer_end_index + 2]
|
||||
self._stream_buffer = self._stream_buffer[stream_buffer_end_index + 2:]
|
||||
self._stream_buffer_start_index = -1
|
||||
self._image.loadFromData(jpg_data)
|
||||
|
||||
if self._image.rect() != self._image_rect:
|
||||
self.imageSizeChanged.emit()
|
||||
|
||||
self.update()
|
@ -4,19 +4,23 @@
|
||||
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
||||
from UM.Logger import Logger
|
||||
from UM.Scene.SceneNode import SceneNode #For typing.
|
||||
from cura.API import Account
|
||||
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.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
|
||||
from time import time
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
from typing import Callable, Dict, List, Optional, Union
|
||||
from enum import IntEnum
|
||||
|
||||
import os # To get the username
|
||||
import gzip
|
||||
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
|
||||
|
||||
class AuthState(IntEnum):
|
||||
NotAuthenticated = 1
|
||||
AuthenticationRequested = 2
|
||||
@ -28,8 +32,8 @@ class AuthState(IntEnum):
|
||||
class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
authenticationStateChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], parent: QObject = None) -> None:
|
||||
super().__init__(device_id = device_id, parent = parent)
|
||||
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, connection_type = connection_type, parent = parent)
|
||||
self._manager = None # type: Optional[QNetworkAccessManager]
|
||||
self._last_manager_create_time = None # type: Optional[float]
|
||||
self._recreate_network_manager_time = 30
|
||||
@ -41,7 +45,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
self._api_prefix = ""
|
||||
self._address = address
|
||||
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._authentication_state = AuthState.NotAuthenticated
|
||||
@ -55,7 +60,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
self._gcode = [] # type: List[str]
|
||||
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")
|
||||
|
||||
def setAuthenticationState(self, authentication_state: AuthState) -> None:
|
||||
@ -125,7 +131,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
if self._connection_state_before_timeout is None:
|
||||
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
|
||||
# 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:
|
||||
self._createNetworkManager()
|
||||
assert(self._manager is not None)
|
||||
elif self._connection_state == ConnectionState.closed:
|
||||
elif self._connection_state == ConnectionState.Closed:
|
||||
# Go out of timeout.
|
||||
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)
|
||||
@ -143,10 +149,15 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
url = QUrl("http://" + self._address + self._api_prefix + target)
|
||||
request = QNetworkRequest(url)
|
||||
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)
|
||||
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:
|
||||
part = QHttpPart()
|
||||
|
||||
@ -160,9 +171,15 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
part.setBody(data)
|
||||
return part
|
||||
|
||||
## Convenience function to get the username from the OS.
|
||||
# The code was copied from the getpass module, as we try to use as little dependencies as possible.
|
||||
## Convenience function to get the username, either from the cloud or from the OS.
|
||||
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"):
|
||||
user = os.environ.get(name)
|
||||
if user:
|
||||
@ -178,49 +195,89 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
self._createNetworkManager()
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
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()
|
||||
request = self._createEmptyRequest(target, content_type=None)
|
||||
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
|
||||
@ -255,22 +312,37 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
def _createNetworkManager(self) -> None:
|
||||
Logger.log("d", "Creating network manager")
|
||||
if self._manager:
|
||||
self._manager.finished.disconnect(self.__handleOnFinished)
|
||||
self._manager.finished.disconnect(self._handleOnFinished)
|
||||
self._manager.authenticationRequired.disconnect(self._onAuthenticationRequired)
|
||||
|
||||
self._manager = QNetworkAccessManager()
|
||||
self._manager.finished.connect(self.__handleOnFinished)
|
||||
self._manager.finished.connect(self._handleOnFinished)
|
||||
self._last_manager_create_time = time()
|
||||
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
|
||||
|
||||
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:
|
||||
if on_finished is not None:
|
||||
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.
|
||||
# As we don't want to keep them around forever, delete them if we get a reply.
|
||||
if reply.operation() == QNetworkAccessManager.PostOperation:
|
||||
@ -282,8 +354,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
|
||||
self._last_response_time = time()
|
||||
|
||||
if self._connection_state == ConnectionState.connecting:
|
||||
self.setConnectionState(ConnectionState.connected)
|
||||
if self._connection_state == ConnectionState.Connecting:
|
||||
self.setConnectionState(ConnectionState.Connected)
|
||||
|
||||
callback_key = reply.url().toString() + str(reply.operation())
|
||||
try:
|
||||
|
@ -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.
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Signal import Signal
|
||||
|
||||
from typing import Union
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||
from .Models.PrintJobOutputModel import PrintJobOutputModel
|
||||
from .Models.ExtruderOutputModel import ExtruderOutputModel
|
||||
from .Models.PrinterOutputModel import PrinterOutputModel
|
||||
from .PrinterOutputDevice import PrinterOutputDevice
|
||||
|
||||
|
||||
class PrinterOutputController:
|
||||
@ -25,10 +23,10 @@ class PrinterOutputController:
|
||||
self.can_update_firmware = False
|
||||
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")
|
||||
|
||||
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")
|
||||
|
||||
def setJobState(self, job: "PrintJobOutputModel", state: str) -> None:
|
||||
|
@ -1,9 +1,8 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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.QtWidgets import QMessageBox
|
||||
|
||||
@ -11,28 +10,35 @@ from UM.Logger import Logger
|
||||
from UM.Signal import signalemitter
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
|
||||
from enum import IntEnum # For the connection state tracking.
|
||||
from typing import Callable, List, Optional, Union
|
||||
from UM.Decorators import deprecated
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdater
|
||||
from UM.FileHandler.FileHandler import FileHandler
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from .Models.PrinterOutputModel import PrinterOutputModel
|
||||
from .Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||
from .FirmwareUpdater import FirmwareUpdater
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## The current processing state of the backend.
|
||||
class ConnectionState(IntEnum):
|
||||
closed = 0
|
||||
connecting = 1
|
||||
connected = 2
|
||||
busy = 3
|
||||
error = 4
|
||||
Closed = 0
|
||||
Connecting = 1
|
||||
Connected = 2
|
||||
Busy = 3
|
||||
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.
|
||||
@ -46,6 +52,7 @@ class ConnectionState(IntEnum):
|
||||
# For all other uses it should be used in the same way as a "regular" OutputDevice.
|
||||
@signalemitter
|
||||
class PrinterOutputDevice(QObject, OutputDevice):
|
||||
|
||||
printersChanged = pyqtSignal()
|
||||
connectionStateChanged = pyqtSignal(str)
|
||||
acceptsCommandsChanged = pyqtSignal()
|
||||
@ -62,33 +69,34 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
# Signal to indicate that the configuration of one of the printers has changed.
|
||||
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
|
||||
|
||||
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_component = None #type: Optional[QObject]
|
||||
self._monitor_item = None #type: Optional[QObject]
|
||||
self._monitor_view_qml_path = "" # type: str
|
||||
self._monitor_component = None # type: Optional[QObject]
|
||||
self._monitor_item = None # type: Optional[QObject]
|
||||
|
||||
self._control_view_qml_path = "" #type: str
|
||||
self._control_component = None #type: Optional[QObject]
|
||||
self._control_item = None #type: Optional[QObject]
|
||||
self._control_view_qml_path = "" # type: str
|
||||
self._control_component = 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.setSingleShot(False)
|
||||
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_name = None #type: Optional[str]
|
||||
self._address = "" #type: str
|
||||
self._connection_text = "" #type: str
|
||||
self._firmware_updater = None # type: Optional[FirmwareUpdater]
|
||||
self._firmware_name = None # type: Optional[str]
|
||||
self._address = "" # type: str
|
||||
self._connection_text = "" # type: str
|
||||
self.printersChanged.connect(self._onPrintersChanged)
|
||||
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
||||
|
||||
@ -110,15 +118,19 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
callback(QMessageBox.Yes)
|
||||
|
||||
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:
|
||||
self._connection_state = connection_state
|
||||
self.connectionStateChanged.emit(self._id)
|
||||
|
||||
@pyqtProperty(str, notify = connectionStateChanged)
|
||||
def connectionState(self) -> ConnectionState:
|
||||
@pyqtProperty(int, constant = True)
|
||||
def connectionType(self) -> "ConnectionType":
|
||||
return self._connection_type
|
||||
|
||||
@pyqtProperty(int, notify = connectionStateChanged)
|
||||
def connectionState(self) -> "ConnectionState":
|
||||
return self._connection_state
|
||||
|
||||
def _update(self) -> None:
|
||||
@ -131,7 +143,8 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
|
||||
return None
|
||||
|
||||
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None:
|
||||
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||
file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None:
|
||||
raise NotImplementedError("requestWrite needs to be implemented")
|
||||
|
||||
@pyqtProperty(QObject, notify = printersChanged)
|
||||
@ -174,13 +187,13 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
|
||||
## Attempt to establish connection
|
||||
def connect(self) -> None:
|
||||
self.setConnectionState(ConnectionState.connecting)
|
||||
self.setConnectionState(ConnectionState.Connecting)
|
||||
self._update_timer.start()
|
||||
|
||||
## Attempt to close the connection
|
||||
def close(self) -> None:
|
||||
self._update_timer.stop()
|
||||
self.setConnectionState(ConnectionState.closed)
|
||||
self.setConnectionState(ConnectionState.Closed)
|
||||
|
||||
## Ensure that close gets called when object is destroyed
|
||||
def __del__(self) -> None:
|
||||
@ -203,14 +216,21 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||
|
||||
# Returns the unique configurations of the printers within this output device
|
||||
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
|
||||
def uniqueConfigurations(self) -> List["ConfigurationModel"]:
|
||||
def uniqueConfigurations(self) -> List["PrinterConfigurationModel"]:
|
||||
return self._unique_configurations
|
||||
|
||||
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.sort(key = lambda k: k.printerType)
|
||||
self._unique_configurations = sorted(
|
||||
{printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None},
|
||||
key=lambda config: config.printerType,
|
||||
)
|
||||
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:
|
||||
for printer in self._printers:
|
||||
printer.configurationChanged.connect(self._updateUniqueConfigurations)
|
@ -60,13 +60,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
previous_node = self._node
|
||||
# Disconnect from previous node signals
|
||||
if previous_node is not None and node is not previous_node:
|
||||
previous_node.transformationChanged.disconnect(self._onChanged)
|
||||
previous_node.parentChanged.disconnect(self._onChanged)
|
||||
previous_node.boundingBoxChanged.disconnect(self._onChanged)
|
||||
|
||||
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.parentChanged.connect(self._onChanged)
|
||||
|
||||
node.boundingBoxChanged.connect(self._onChanged)
|
||||
|
||||
self._onChanged()
|
||||
|
||||
@ -142,6 +140,12 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
controller = Application.getInstance().getController()
|
||||
root = controller.getScene().getRoot()
|
||||
if self._node is None or controller.isToolOperationActive() or not self.__isDescendant(root, self._node):
|
||||
# If the tool operation is still active, we need to compute the convex hull later after the controller is
|
||||
# no longer active.
|
||||
if controller.isToolOperationActive():
|
||||
self.recomputeConvexHullDelayed()
|
||||
return
|
||||
|
||||
if self._convex_hull_node:
|
||||
self._convex_hull_node.setParent(None)
|
||||
self._convex_hull_node = None
|
||||
@ -181,7 +185,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
for child in self._node.getChildren():
|
||||
child_hull = child.callDecoration("_compute2DConvexHull")
|
||||
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:
|
||||
return None
|
||||
@ -233,7 +240,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
|
||||
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
|
||||
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
|
||||
_, idx = numpy.unique(vertex_byte_view, return_index=True)
|
||||
_, idx = numpy.unique(vertex_byte_view, return_index = True)
|
||||
vertex_data = vertex_data[idx] # Select the unique rows by index.
|
||||
|
||||
hull = Polygon(vertex_data)
|
||||
@ -266,7 +273,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
|
||||
|
||||
# Min head hull is used for the push free
|
||||
convex_hull = self._compute2DConvexHeadFull()
|
||||
convex_hull = self._compute2DConvexHull()
|
||||
if convex_hull:
|
||||
return convex_hull.getMinkowskiHull(head_and_fans)
|
||||
return None
|
||||
@ -280,16 +287,21 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
# Add extra margin depending on adhesion type
|
||||
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":
|
||||
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":
|
||||
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":
|
||||
extra_margin = 0
|
||||
elif adhesion_type == "skirt":
|
||||
extra_margin = max(
|
||||
extra_margin = min(max_length_available, max(
|
||||
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:
|
||||
raise Exception("Unknown bed adhesion type. Did you forget to update the convex hull calculations for your new bed adhesion type?")
|
||||
|
||||
|
@ -1,7 +1,10 @@
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from typing import Optional
|
||||
|
||||
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.Resources import Resources
|
||||
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
|
||||
# then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
|
||||
# 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)
|
||||
|
||||
self.setCalculateBoundingBox(False)
|
||||
@ -25,7 +28,11 @@ class ConvexHullNode(SceneNode):
|
||||
|
||||
# Color of the drawn convex hull
|
||||
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:
|
||||
self._color = Color(0, 0, 0)
|
||||
|
||||
@ -47,7 +54,7 @@ class ConvexHullNode(SceneNode):
|
||||
|
||||
if hull_mesh_builder.addConvexPolygonExtrusion(
|
||||
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()
|
||||
self.setMeshData(hull_mesh)
|
||||
@ -75,7 +82,7 @@ class ConvexHullNode(SceneNode):
|
||||
|
||||
return True
|
||||
|
||||
def _onNodeDecoratorsChanged(self, node):
|
||||
def _onNodeDecoratorsChanged(self, node: SceneNode) -> None:
|
||||
convex_hull_head = self._node.callDecoration("getConvexHullHead")
|
||||
if convex_hull_head:
|
||||
convex_hull_head_builder = MeshBuilder()
|
||||
|
@ -3,7 +3,8 @@ from UM.Logger import Logger
|
||||
from PyQt5.QtCore import Qt, pyqtSlot, QObject
|
||||
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 UM.Application import Application
|
||||
@ -33,7 +34,7 @@ class CuraSceneController(QObject):
|
||||
source = args[0]
|
||||
else:
|
||||
source = None
|
||||
if not isinstance(source, SceneNode):
|
||||
if not isinstance(source, SceneNode) or isinstance(source, Camera):
|
||||
return
|
||||
max_build_plate = self._calcMaxBuildPlate()
|
||||
changed = False
|
||||
|
@ -112,21 +112,21 @@ class CuraSceneNode(SceneNode):
|
||||
|
||||
## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box
|
||||
def _calculateAABB(self) -> None:
|
||||
self._aabb = None
|
||||
if self._mesh_data:
|
||||
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)
|
||||
self._aabb = self._mesh_data.getExtents(self.getWorldTransformation())
|
||||
|
||||
for child in self._children:
|
||||
if child.callDecoration("isNonPrintingMesh"):
|
||||
# Non-printing-meshes inside a group should not affect push apart or drop to build plate
|
||||
continue
|
||||
if aabb is None:
|
||||
aabb = child.getBoundingBox()
|
||||
if not child._mesh_data:
|
||||
# Nodes without mesh data should not affect bounding boxes of their parents.
|
||||
continue
|
||||
if self._aabb is None:
|
||||
self._aabb = child.getBoundingBox()
|
||||
else:
|
||||
aabb = aabb + child.getBoundingBox()
|
||||
self._aabb = aabb
|
||||
self._aabb = self._aabb + child.getBoundingBox()
|
||||
|
||||
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
|
||||
def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode":
|
||||
|
@ -10,7 +10,7 @@ class GCodeListDecorator(SceneNodeDecorator):
|
||||
def getGCodeList(self) -> List[str]:
|
||||
return self._gcode_list
|
||||
|
||||
def setGCodeList(self, list: List[str]):
|
||||
def setGCodeList(self, list: List[str]) -> None:
|
||||
self._gcode_list = list
|
||||
|
||||
def __deepcopy__(self, memo) -> "GCodeListDecorator":
|
||||
|
@ -47,8 +47,10 @@ class ContainerManager(QObject):
|
||||
if ContainerManager.__instance is not None:
|
||||
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
|
||||
ContainerManager.__instance = self
|
||||
|
||||
super().__init__(parent = application)
|
||||
try:
|
||||
super().__init__(parent = application)
|
||||
except TypeError:
|
||||
super().__init__()
|
||||
|
||||
self._application = application # type: CuraApplication
|
||||
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
|
||||
@ -419,13 +421,13 @@ class ContainerManager(QObject):
|
||||
self._container_name_filters[name_filter] = entry
|
||||
|
||||
## Import single profile, file_url does not have to end with curaprofile
|
||||
@pyqtSlot(QUrl, result="QVariantMap")
|
||||
def importProfile(self, file_url: QUrl):
|
||||
@pyqtSlot(QUrl, result = "QVariantMap")
|
||||
def importProfile(self, file_url: QUrl) -> Dict[str, str]:
|
||||
if not file_url.isValid():
|
||||
return
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
|
||||
path = file_url.toLocalFile()
|
||||
if not path:
|
||||
return
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
|
||||
return self._container_registry.importProfile(path)
|
||||
|
||||
@pyqtSlot(QObject, QUrl, str)
|
||||
|
@ -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.
|
||||
|
||||
import os
|
||||
import re
|
||||
import configparser
|
||||
|
||||
from typing import cast, Optional
|
||||
|
||||
from typing import Any, cast, Dict, Optional
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from UM.Decorators import override
|
||||
from UM.Settings.ContainerFormatError import ContainerFormatError
|
||||
from UM.Settings.Interfaces import ContainerInterface
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
@ -28,7 +28,7 @@ from . import GlobalStack
|
||||
|
||||
import cura.CuraApplication
|
||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||
from cura.ReaderWriters.ProfileReader import NoProfileException
|
||||
from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
@ -161,20 +161,20 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
|
||||
## Imports a profile from a file
|
||||
#
|
||||
# \param file_name \type{str} 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
|
||||
# containing a message for the user
|
||||
def importProfile(self, file_name):
|
||||
# \param file_name The full path and filename of the profile to import.
|
||||
# \return Dict with a 'status' key containing the string 'ok' or 'error',
|
||||
# and a 'message' key containing a message for the user.
|
||||
def importProfile(self, file_name: str) -> Dict[str, str]:
|
||||
Logger.log("d", "Attempting to import profile %s", 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()
|
||||
extension = file_name.split(".")[-1]
|
||||
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
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 = []
|
||||
for position in sorted(global_stack.extruders):
|
||||
@ -183,7 +183,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
||||
if meta_data["profile_reader"][0]["extension"] != extension:
|
||||
continue
|
||||
profile_reader = plugin_registry.getPluginObject(plugin_id)
|
||||
profile_reader = cast(ProfileReader, plugin_registry.getPluginObject(plugin_id))
|
||||
try:
|
||||
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
|
||||
except NoProfileException:
|
||||
@ -221,13 +221,13 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
# Make sure we have a profile_definition in the file:
|
||||
if profile_definition is None:
|
||||
break
|
||||
machine_definition = self.findDefinitionContainers(id = profile_definition)
|
||||
if not machine_definition:
|
||||
machine_definitions = self.findDefinitionContainers(id = profile_definition)
|
||||
if not machine_definitions:
|
||||
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
|
||||
return {"status": "error",
|
||||
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "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.
|
||||
# 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.setDirty(True)
|
||||
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():
|
||||
settable_per_extruder = global_stack.getProperty(qc_setting_key, "settable_per_extruder")
|
||||
if settable_per_extruder:
|
||||
setting_value = global_profile.getProperty(qc_setting_key, "value")
|
||||
|
||||
setting_definition = global_stack.getSettingDefinition(qc_setting_key)
|
||||
new_instance = SettingInstance(setting_definition, profile)
|
||||
new_instance.setProperty("value", setting_value)
|
||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||
profile.addInstance(new_instance)
|
||||
profile.setDirty(True)
|
||||
if setting_definition is not None:
|
||||
new_instance = SettingInstance(setting_definition, profile)
|
||||
new_instance.setProperty("value", setting_value)
|
||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||
profile.addInstance(new_instance)
|
||||
profile.setDirty(True)
|
||||
|
||||
global_profile.removeInstance(qc_setting_key, postpone_emit=True)
|
||||
extruder_profiles.append(profile)
|
||||
@ -290,7 +291,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
for profile_index, profile in enumerate(profile_or_list):
|
||||
if profile_index == 0:
|
||||
# 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:
|
||||
# This is assumed to be an extruder profile
|
||||
@ -302,8 +303,8 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
profile.setMetaDataEntry("position", extruder_position)
|
||||
profile_id = (extruder_id + "_" + name_seed).lower().replace(" ", "_")
|
||||
|
||||
else: #More extruders in the imported file than in the machine.
|
||||
continue #Delete the additional profiles.
|
||||
else: # More extruders in the imported file than in the machine.
|
||||
continue # Delete the additional profiles.
|
||||
|
||||
result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
|
||||
if result is not None:
|
||||
@ -326,6 +327,23 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
self._registerSingleExtrusionMachinesExtruderStacks()
|
||||
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.
|
||||
#
|
||||
# \param profile The profile to configure.
|
||||
@ -385,30 +403,6 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
result.append( (plugin_id, meta_data) )
|
||||
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.
|
||||
def _convertContainerStack(self, container):
|
||||
assert type(container) == ContainerStack
|
||||
@ -520,7 +514,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
user_container.setMetaDataEntry("position", extruder_stack.getMetaDataEntry("position"))
|
||||
|
||||
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.
|
||||
for user_setting_key in machine.userChanges.getAllKeys():
|
||||
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_stack.qualityChanges = self.findInstanceContainers(id = quality_changes_id)[0]
|
||||
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_id = self.uniqueName(extruder_stack.getId() + "_qc_" + container_name)
|
||||
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]",
|
||||
machine_quality_changes.getName(), extruder_stack.getId())
|
||||
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():
|
||||
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
|
||||
if settable_per_extruder:
|
||||
@ -641,7 +635,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
if qc_name not in qc_groups:
|
||||
qc_groups[qc_name] = []
|
||||
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())
|
||||
if quality_changes_container:
|
||||
qc_groups[qc_name].append(quality_changes_container)
|
||||
@ -655,7 +649,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
else:
|
||||
qc_dict["global"] = qc
|
||||
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():
|
||||
settable_per_extruder = machine.getProperty(qc_setting_key, "settable_per_extruder")
|
||||
if settable_per_extruder:
|
||||
@ -689,17 +683,17 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
try:
|
||||
parser.read([file_path])
|
||||
except:
|
||||
# skip, it is not a valid stack file
|
||||
# Skip, it is not a valid stack file
|
||||
continue
|
||||
|
||||
if not parser.has_option("general", "name"):
|
||||
continue
|
||||
|
||||
if parser["general"]["name"] == name:
|
||||
# load the container
|
||||
# Load the container
|
||||
container_id = os.path.basename(file_path).replace(".inst.cfg", "")
|
||||
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
|
||||
|
||||
instance_container = InstanceContainer(container_id)
|
||||
@ -733,7 +727,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
else:
|
||||
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
|
||||
@override(ContainerRegistry)
|
||||
def getInstance(cls, *args, **kwargs) -> "CuraContainerRegistry":
|
||||
|
@ -5,6 +5,7 @@ from typing import Any, List, Optional, TYPE_CHECKING
|
||||
|
||||
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
|
||||
from UM.Settings.SettingFunction import SettingFunction
|
||||
from UM.Logger import Logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
@ -38,7 +39,18 @@ class CuraFormulaFunctions:
|
||||
extruder_position = int(machine_manager.defaultExtruderPosition)
|
||||
|
||||
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)
|
||||
if isinstance(value, SettingFunction):
|
||||
|
@ -125,7 +125,12 @@ class CuraStackBuilder:
|
||||
|
||||
extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains")
|
||||
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
|
||||
material_container = application.empty_material_container
|
||||
|
@ -63,7 +63,7 @@ class ExtruderManager(QObject):
|
||||
if not self._application.getGlobalContainerStack():
|
||||
return None # No active machine, so no active extruder.
|
||||
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.
|
||||
return None
|
||||
|
||||
@ -83,8 +83,9 @@ class ExtruderManager(QObject):
|
||||
# \param index The index of the new active extruder.
|
||||
@pyqtSlot(int)
|
||||
def setActiveExtruderIndex(self, index: int) -> None:
|
||||
self._active_extruder_index = index
|
||||
self.activeExtruderChanged.emit()
|
||||
if self._active_extruder_index != index:
|
||||
self._active_extruder_index = index
|
||||
self.activeExtruderChanged.emit()
|
||||
|
||||
@pyqtProperty(int, notify = activeExtruderChanged)
|
||||
def activeExtruderIndex(self) -> int:
|
||||
@ -144,7 +145,7 @@ class ExtruderManager(QObject):
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
|
||||
return self.getExtruderStack(self._active_extruder_index)
|
||||
return self.getExtruderStack(self.activeExtruderIndex)
|
||||
|
||||
## Get an extruder stack by index
|
||||
def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
|
||||
@ -223,7 +224,16 @@ class ExtruderManager(QObject):
|
||||
|
||||
# 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.
|
||||
|
||||
# Exclude anti-overhang meshes
|
||||
mesh_list = []
|
||||
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")
|
||||
if not extruder_stack_id:
|
||||
# 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")))])
|
||||
|
||||
# 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"))
|
||||
if extruder_str_nr == "-1":
|
||||
extruder_str_nr = self._application.getMachineManager().defaultExtruderPosition
|
||||
@ -300,12 +312,7 @@ class ExtruderManager(QObject):
|
||||
global_stack = self._application.getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return []
|
||||
|
||||
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]
|
||||
return global_stack.extruderList
|
||||
|
||||
def _globalContainerStackChanged(self) -> None:
|
||||
# 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)
|
||||
extruders_changed = True
|
||||
|
||||
self._fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||
self.fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||
if extruders_changed:
|
||||
self.extrudersChanged.emit(global_stack_id)
|
||||
self.setActiveExtruderIndex(0)
|
||||
self.activeExtruderChanged.emit()
|
||||
|
||||
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
|
||||
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
|
||||
def _fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
|
||||
def fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
|
||||
extruder_stack_0 = global_stack.extruders.get("0")
|
||||
|
@ -52,8 +52,8 @@ class ExtruderStack(CuraContainerStack):
|
||||
return super().getNextStack()
|
||||
|
||||
def setEnabled(self, enabled: bool) -> None:
|
||||
if "enabled" not in self._metadata:
|
||||
self.setMetaDataEntry("enabled", "True")
|
||||
if self.getMetaDataEntry("enabled", True) == enabled: # No change.
|
||||
return # Don't emit a signal then.
|
||||
self.setMetaDataEntry("enabled", str(enabled))
|
||||
self.enabledChanged.emit()
|
||||
|
||||
|
@ -3,8 +3,8 @@
|
||||
|
||||
from collections import defaultdict
|
||||
import threading
|
||||
from typing import Any, Dict, Optional, Set, TYPE_CHECKING
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSlot
|
||||
from typing import Any, Dict, Optional, Set, TYPE_CHECKING, List
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
|
||||
|
||||
from UM.Decorators import override
|
||||
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.
|
||||
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.
|
||||
#
|
||||
# \return The extruders registered with this stack.
|
||||
@pyqtProperty("QVariantMap")
|
||||
@pyqtProperty("QVariantMap", notify = extrudersChanged)
|
||||
def extruders(self) -> Dict[str, "ExtruderStack"]:
|
||||
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
|
||||
def getLoadingPriority(cls) -> int:
|
||||
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
|
||||
def getConfigurationTypeFromSerialized(cls, serialized: str) -> Optional[str]:
|
||||
configuration_type = super().getConfigurationTypeFromSerialized(serialized)
|
||||
@ -87,6 +145,7 @@ class GlobalStack(CuraContainerStack):
|
||||
return
|
||||
|
||||
self._extruders[position] = extruder
|
||||
self.extrudersChanged.emit()
|
||||
Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)
|
||||
|
||||
## 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
|
||||
# requested property.
|
||||
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
|
||||
return False
|
||||
|
||||
@ -199,6 +258,9 @@ class GlobalStack(CuraContainerStack):
|
||||
def getHasVariants(self) -> bool:
|
||||
return parseBool(self.getMetaDataEntry("has_variants", False))
|
||||
|
||||
def getHasVariantsBuildPlates(self) -> bool:
|
||||
return parseBool(self.getMetaDataEntry("has_variant_buildplates", False))
|
||||
|
||||
def getHasMachineQuality(self) -> bool:
|
||||
return parseBool(self.getMetaDataEntry("has_machine_quality", False))
|
||||
|
||||
|
@ -1,17 +1,19 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import collections
|
||||
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.Decorators import deprecated
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Settings.InstanceContainer import InstanceContainer
|
||||
from UM.Settings.Interfaces import ContainerInterface
|
||||
from UM.Signal import Signal
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM import Util
|
||||
from UM.Logger import Logger
|
||||
@ -21,10 +23,10 @@ from UM.Settings.SettingFunction import SettingFunction
|
||||
from UM.Signal import postponeSignals, CompressTechnique
|
||||
|
||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionType
|
||||
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||
from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
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.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.setSingleShot(True)
|
||||
self._instance_container_timer.timeout.connect(self.__emitChangedSignals)
|
||||
@ -74,7 +74,7 @@ class MachineManager(QObject):
|
||||
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||
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.activeVariantChanged)
|
||||
self.globalContainerChanged.connect(self.activeQualityChanged)
|
||||
@ -86,12 +86,14 @@ class MachineManager(QObject):
|
||||
|
||||
self._onGlobalContainerChanged()
|
||||
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
|
||||
extruder_manager = self._application.getExtruderManager()
|
||||
|
||||
extruder_manager.activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
|
||||
self._onActiveExtruderStackChanged()
|
||||
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeMaterialChanged)
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeVariantChanged)
|
||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeQualityChanged)
|
||||
extruder_manager.activeExtruderChanged.connect(self.activeMaterialChanged)
|
||||
extruder_manager.activeExtruderChanged.connect(self.activeVariantChanged)
|
||||
extruder_manager.activeExtruderChanged.connect(self.activeQualityChanged)
|
||||
|
||||
self.globalContainerChanged.connect(self.activeStackChanged)
|
||||
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
|
||||
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.activeVariantChanged.connect(self._onCurrentConfigurationChanged)
|
||||
# Force to compute the current configuration
|
||||
@ -113,17 +115,13 @@ class MachineManager(QObject):
|
||||
|
||||
self._application.callLater(self.setInitialActiveMachine)
|
||||
|
||||
self._material_incompatible_message = Message(catalog.i18nc("@info:status",
|
||||
"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]
|
||||
containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) # type: List[InstanceContainer]
|
||||
if containers:
|
||||
containers[0].nameChanged.connect(self._onMaterialNameChanged)
|
||||
|
||||
self._material_manager = self._application.getMaterialManager() #type: MaterialManager
|
||||
self._variant_manager = self._application.getVariantManager() #type: VariantManager
|
||||
self._quality_manager = self._application.getQualityManager() #type: QualityManager
|
||||
self._material_manager = self._application.getMaterialManager() # type: MaterialManager
|
||||
self._variant_manager = self._application.getVariantManager() # type: VariantManager
|
||||
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
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
rootMaterialChanged = pyqtSignal()
|
||||
discoveredPrintersChanged = pyqtSignal()
|
||||
|
||||
def setInitialActiveMachine(self) -> None:
|
||||
active_machine_id = self._application.getPreferences().getValue("cura/active_machine")
|
||||
@ -176,7 +175,7 @@ class MachineManager(QObject):
|
||||
self.outputDevicesChanged.emit()
|
||||
|
||||
@pyqtProperty(QObject, notify = currentConfigurationChanged)
|
||||
def currentConfiguration(self) -> ConfigurationModel:
|
||||
def currentConfiguration(self) -> PrinterConfigurationModel:
|
||||
return self._current_printer_configuration
|
||||
|
||||
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
|
||||
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.
|
||||
self._current_printer_configuration.buildplateConfiguration = self._global_container_stack.getProperty("machine_buildplate_type", "value") if self._global_container_stack.variant != empty_variant_container else ""
|
||||
self.currentConfigurationChanged.emit()
|
||||
|
||||
@pyqtSlot(QObject, result = bool)
|
||||
def matchesConfiguration(self, configuration: ConfigurationModel) -> bool:
|
||||
def matchesConfiguration(self, configuration: PrinterConfigurationModel) -> bool:
|
||||
return self._current_printer_configuration == configuration
|
||||
|
||||
@pyqtProperty("QVariantList", notify = outputDevicesChanged)
|
||||
@ -247,7 +246,7 @@ class MachineManager(QObject):
|
||||
self.updateNumberExtrudersEnabled()
|
||||
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:
|
||||
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":
|
||||
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
|
||||
if global_material != 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.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()
|
||||
|
||||
def _onActiveExtruderStackChanged(self) -> None:
|
||||
self.blurSettings.emit() # Ensure no-one has focus.
|
||||
old_active_container_stack = self._active_container_stack
|
||||
|
||||
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:
|
||||
self.activeQualityChanged.emit()
|
||||
self.activeVariantChanged.emit()
|
||||
self.activeMaterialChanged.emit()
|
||||
|
||||
self.rootMaterialChanged.emit()
|
||||
self.numberExtrudersEnabledChanged.emit()
|
||||
|
||||
def _onContainersChanged(self, container: ContainerInterface) -> None:
|
||||
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
|
||||
self._application.getMachineActionManager().addDefaultMachineActions(global_stack)
|
||||
|
||||
ExtruderManager.getInstance()._fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||
ExtruderManager.getInstance().fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||
if not global_stack.isValid():
|
||||
# Mark global stack as invalid
|
||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
|
||||
return # We're done here
|
||||
ExtruderManager.getInstance().setActiveExtruderIndex(0) # Switch to first extruder
|
||||
|
||||
self._global_container_stack = global_stack
|
||||
self._application.setGlobalContainerStack(global_stack)
|
||||
ExtruderManager.getInstance()._globalContainerStackChanged()
|
||||
self._initMachineState(global_stack)
|
||||
self._onGlobalContainerChanged()
|
||||
|
||||
# Switch to the first enabled extruder
|
||||
self.updateDefaultExtruder()
|
||||
default_extruder_position = int(self.defaultExtruderPosition)
|
||||
ExtruderManager.getInstance().setActiveExtruderIndex(default_extruder_position)
|
||||
|
||||
self.__emitChangedSignals()
|
||||
|
||||
## Given a definition id, return the machine with this id.
|
||||
@ -398,9 +391,17 @@ class MachineManager(QObject):
|
||||
return machine
|
||||
return None
|
||||
|
||||
@pyqtSlot(str)
|
||||
@pyqtSlot(str, str)
|
||||
def addMachine(self, name: str, definition_id: str) -> None:
|
||||
new_stack = CuraStackBuilder.createMachine(name, definition_id)
|
||||
def addMachine(self, definition_id: str, name: Optional[str] = None) -> None:
|
||||
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:
|
||||
# Instead of setting the global container stack here, we set the active machine and so the signals are emitted
|
||||
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
|
||||
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
|
||||
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:
|
||||
md = stack.getMetaData()
|
||||
if "position" in md and int(md["position"]) >= machine_extruder_count:
|
||||
@ -438,12 +439,12 @@ class MachineManager(QObject):
|
||||
if not self._global_container_stack:
|
||||
return False
|
||||
|
||||
if self._global_container_stack.getTop().findInstances():
|
||||
if self._global_container_stack.getTop().getNumInstances() != 0:
|
||||
return True
|
||||
|
||||
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
for stack in stacks:
|
||||
if stack.getTop().findInstances():
|
||||
if stack.getTop().getNumInstances() != 0:
|
||||
return True
|
||||
|
||||
return False
|
||||
@ -453,10 +454,10 @@ class MachineManager(QObject):
|
||||
if not self._global_container_stack:
|
||||
return 0
|
||||
num_user_settings = 0
|
||||
num_user_settings += len(self._global_container_stack.getTop().findInstances())
|
||||
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||
num_user_settings += self._global_container_stack.getTop().getNumInstances()
|
||||
stacks = self._global_container_stack.extruderList
|
||||
for stack in stacks:
|
||||
num_user_settings += len(stack.getTop().findInstances())
|
||||
num_user_settings += stack.getTop().getNumInstances()
|
||||
return num_user_settings
|
||||
|
||||
## 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)
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.definition.name instead", "4.1")
|
||||
def activeMachineDefinitionName(self) -> str:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.definition.getName()
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.name instead", "4.1")
|
||||
def activeMachineName(self) -> str:
|
||||
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 ""
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.id instead", "4.1")
|
||||
def activeMachineId(self) -> str:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.getId()
|
||||
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)
|
||||
def printerConnected(self):
|
||||
def printerConnected(self) -> bool:
|
||||
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:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.getMetaDataEntry("um_network_key", "")
|
||||
@ -528,7 +579,7 @@ class MachineManager(QObject):
|
||||
@pyqtProperty(str, notify = printerConnectedStatusChanged)
|
||||
def activeMachineNetworkGroupName(self) -> str:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.getMetaDataEntry("connect_group_name", "")
|
||||
return self._global_container_stack.getMetaDataEntry("group_name", "")
|
||||
return ""
|
||||
|
||||
@pyqtProperty(QObject, notify = globalContainerChanged)
|
||||
@ -616,6 +667,14 @@ class MachineManager(QObject):
|
||||
is_supported = self._current_quality_group.is_available
|
||||
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.
|
||||
#
|
||||
# The current set-up signifies the global stack and all extruder stacks,
|
||||
@ -633,11 +692,6 @@ class MachineManager(QObject):
|
||||
return False
|
||||
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.
|
||||
@pyqtSlot(str)
|
||||
def copyValueToExtruders(self, key: str) -> None:
|
||||
@ -646,7 +700,7 @@ class MachineManager(QObject):
|
||||
new_value = self._active_container_stack.getProperty(key, "value")
|
||||
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:
|
||||
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
|
||||
@ -662,10 +716,11 @@ class MachineManager(QObject):
|
||||
for key in self._active_container_stack.userChanges.getAllKeys():
|
||||
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)
|
||||
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
@deprecated("use Cura.activeStack.variant.name instead", "4.1")
|
||||
def activeVariantName(self) -> str:
|
||||
if self._active_container_stack:
|
||||
variant = self._active_container_stack.variant
|
||||
@ -675,6 +730,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
@deprecated("use Cura.activeStack.variant.id instead", "4.1")
|
||||
def activeVariantId(self) -> str:
|
||||
if self._active_container_stack:
|
||||
variant = self._active_container_stack.variant
|
||||
@ -684,6 +740,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
@deprecated("use Cura.activeMachine.variant.name instead", "4.1")
|
||||
def activeVariantBuildplateName(self) -> str:
|
||||
if self._global_container_stack:
|
||||
variant = self._global_container_stack.variant
|
||||
@ -693,6 +750,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.activeMachine.definition.id instead", "4.1")
|
||||
def activeDefinitionId(self) -> str:
|
||||
if self._global_container_stack:
|
||||
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.
|
||||
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:
|
||||
machine_stacks = CuraContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine")
|
||||
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"])
|
||||
|
||||
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)
|
||||
containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
|
||||
for container in containers:
|
||||
@ -757,19 +815,19 @@ class MachineManager(QObject):
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def hasMaterials(self) -> bool:
|
||||
if self._global_container_stack:
|
||||
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False))
|
||||
return self._global_container_stack.getHasMaterials()
|
||||
return False
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def hasVariants(self) -> bool:
|
||||
if self._global_container_stack:
|
||||
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_variants", False))
|
||||
return self._global_container_stack.getHasVariants()
|
||||
return False
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
def hasVariantBuildplates(self) -> bool:
|
||||
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
|
||||
|
||||
## 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(
|
||||
"@info:generic",
|
||||
"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"))
|
||||
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.
|
||||
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():
|
||||
setting_key = setting_instance.definition.key
|
||||
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
|
||||
|
||||
if settable_per_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_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
|
||||
extruder_position = max(0, limit_to_extruder)
|
||||
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)
|
||||
|
||||
# Signal that the global stack has changed
|
||||
@ -932,10 +987,9 @@ class MachineManager(QObject):
|
||||
|
||||
@pyqtSlot(int, result = QObject)
|
||||
def getExtruder(self, position: int) -> Optional[ExtruderStack]:
|
||||
extruder = None
|
||||
if self._global_container_stack:
|
||||
extruder = self._global_container_stack.extruders.get(str(position))
|
||||
return extruder
|
||||
return self._global_container_stack.extruders.get(str(position))
|
||||
return None
|
||||
|
||||
def updateDefaultExtruder(self) -> None:
|
||||
if self._global_container_stack is None:
|
||||
@ -1001,12 +1055,12 @@ class MachineManager(QObject):
|
||||
if not enabled and position == ExtruderManager.getInstance().activeExtruderIndex:
|
||||
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.extruderChanged.emit()
|
||||
# update material compatibility color
|
||||
# Update material compatibility color
|
||||
self.activeQualityGroupChanged.emit()
|
||||
# update items in SettingExtruder
|
||||
# Update items in SettingExtruder
|
||||
ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId())
|
||||
# Make sure the front end reflects changes
|
||||
self.forceUpdateAllSettings()
|
||||
@ -1019,9 +1073,6 @@ class MachineManager(QObject):
|
||||
def _onMaterialNameChanged(self) -> None:
|
||||
self.activeMaterialChanged.emit()
|
||||
|
||||
def _onQualityNameChanged(self) -> None:
|
||||
self.activeQualityChanged.emit()
|
||||
|
||||
def _getContainerChangedSignals(self) -> List[Signal]:
|
||||
if self._global_container_stack is None:
|
||||
return []
|
||||
@ -1080,7 +1131,6 @@ class MachineManager(QObject):
|
||||
|
||||
return result
|
||||
|
||||
#
|
||||
# Sets all quality and quality_changes containers to empty_quality and empty_quality_changes containers
|
||||
# for all stacks in the currently active machine.
|
||||
#
|
||||
@ -1139,7 +1189,7 @@ class MachineManager(QObject):
|
||||
|
||||
def _setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> 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
|
||||
# A custom quality can be created based on "not supported".
|
||||
# In that case, do not set quality containers to empty.
|
||||
@ -1209,7 +1259,7 @@ class MachineManager(QObject):
|
||||
self.rootMaterialChanged.emit()
|
||||
|
||||
def activeMaterialsCompatible(self) -> bool:
|
||||
# check material - variant compatibility
|
||||
# Check material - variant compatibility
|
||||
if self._global_container_stack is not None:
|
||||
if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)):
|
||||
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
|
||||
machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()
|
||||
# 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 not new_machine:
|
||||
new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id)
|
||||
if not new_machine:
|
||||
return
|
||||
new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey)
|
||||
new_machine.setMetaDataEntry("connect_group_name", self.activeMachineNetworkGroupName)
|
||||
new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey())
|
||||
new_machine.setMetaDataEntry("group_name", self.activeMachineNetworkGroupName)
|
||||
new_machine.setMetaDataEntry("hidden", False)
|
||||
new_machine.setMetaDataEntry("connection_type", self._global_container_stack.getMetaDataEntry("connection_type"))
|
||||
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)
|
||||
|
||||
# Set the current printer instance to hidden (the metadata entry must exist)
|
||||
@ -1329,31 +1380,65 @@ class MachineManager(QObject):
|
||||
self.setActiveMachine(new_machine.getId())
|
||||
|
||||
@pyqtSlot(QObject)
|
||||
def applyRemoteConfiguration(self, configuration: ConfigurationModel) -> None:
|
||||
def applyRemoteConfiguration(self, configuration: PrinterConfigurationModel) -> None:
|
||||
if self._global_container_stack is None:
|
||||
return
|
||||
self.blurSettings.emit()
|
||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||
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:
|
||||
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:
|
||||
self._setVariantNode(position, variant_container_node)
|
||||
else:
|
||||
self._global_container_stack.extruders[position].variant = empty_variant_container
|
||||
# If the machine doesn't have a hotend or material, disable this extruder
|
||||
if int(position) in extruders_to_disable:
|
||||
self._global_container_stack.extruders[position].setEnabled(False)
|
||||
|
||||
need_to_show_message = True
|
||||
disabled_used_extruder_position_set.add(int(position))
|
||||
|
||||
if material_container_node:
|
||||
self._setMaterial(position, material_container_node)
|
||||
else:
|
||||
self._global_container_stack.extruders[position].material = empty_material_container
|
||||
self.updateMaterialWithVariant(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:
|
||||
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:
|
||||
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._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
|
||||
if self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1:
|
||||
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")
|
||||
def setGlobalVariant(self, container_node: "ContainerNode") -> None:
|
||||
self.blurSettings.emit()
|
||||
@ -1419,7 +1494,7 @@ class MachineManager(QObject):
|
||||
material_diameter, root_material_id)
|
||||
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.
|
||||
@pyqtSlot(str, "QVariant")
|
||||
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"]:
|
||||
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)
|
||||
def activeQualityOrQualityChangesName(self) -> str:
|
||||
name = empty_quality_container.getName()
|
||||
@ -1531,9 +1610,40 @@ class MachineManager(QObject):
|
||||
name = self._current_quality_group.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:
|
||||
if self._global_container_stack is None:
|
||||
return
|
||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||
self.updateMaterialWithVariant(None)
|
||||
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
|
||||
|
@ -34,7 +34,7 @@ class PerObjectContainerStack(CuraContainerStack):
|
||||
if limit_to_extruder is not None:
|
||||
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
|
||||
# stack.
|
||||
if limit_to_extruder == "-1":
|
||||
@ -42,7 +42,7 @@ class PerObjectContainerStack(CuraContainerStack):
|
||||
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:
|
||||
# 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:
|
||||
context.context["original_limit_to_extruder"] = limit_to_extruder
|
||||
|
||||
|
@ -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"
|
||||
# has not been updated yet.
|
||||
deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
||||
deep_copy._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
|
||||
deep_copy._is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
|
||||
deep_copy._is_non_thumbnail_visible_mesh = self._evaluateIsNonThumbnailVisibleMesh()
|
||||
|
||||
return deep_copy
|
||||
|
||||
@ -102,23 +102,32 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||
def isNonPrintingMesh(self):
|
||||
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)
|
||||
|
||||
def isNonThumbnailVisibleMesh(self):
|
||||
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)
|
||||
|
||||
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
|
||||
if property_name == "value":
|
||||
def _onSettingChanged(self, setting_key, property_name): # Reminder: 'property' is a built-in function
|
||||
# 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.
|
||||
self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
||||
self._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
|
||||
new_is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
|
||||
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()
|
||||
Application.getInstance().getBackend().tickle()
|
||||
if changed:
|
||||
Application.getInstance().getBackend().needsSlicing()
|
||||
Application.getInstance().getBackend().tickle()
|
||||
|
||||
## Makes sure that the stack upon which the container stack is placed is
|
||||
# kept up to date.
|
||||
|
@ -1,7 +1,8 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# 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
|
||||
|
||||
@ -16,15 +17,11 @@ class SimpleModeSettingsManager(QObject):
|
||||
self._is_profile_user_created = False # True when profile was custom created by user
|
||||
|
||||
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
|
||||
self._updateIsProfileCustomized()
|
||||
self._updateIsProfileUserCreated()
|
||||
|
||||
isProfileCustomizedChanged = pyqtSignal()
|
||||
isProfileUserCreatedChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = isProfileCustomizedChanged)
|
||||
def isProfileCustomized(self):
|
||||
@ -57,33 +54,6 @@ class SimpleModeSettingsManager(QObject):
|
||||
self._is_profile_customized = has_customized_user_settings
|
||||
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
|
||||
# changed, we consider it as a user customized profile in the Simple ("Recommended") Mode.
|
||||
__ignored_custom_setting_keys = ["support_enable",
|
||||
|
@ -41,6 +41,22 @@ empty_quality_changes_container.setMetaDataEntry("type", "quality_changes")
|
||||
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",
|
||||
"empty_container", # For convenience
|
||||
"EMPTY_DEFINITION_CHANGES_CONTAINER_ID",
|
||||
@ -52,5 +68,7 @@ __all__ = ["EMPTY_CONTAINER_ID",
|
||||
"EMPTY_QUALITY_CHANGES_CONTAINER_ID",
|
||||
"empty_quality_changes_container",
|
||||
"EMPTY_QUALITY_CONTAINER_ID",
|
||||
"empty_quality_container"
|
||||
"empty_quality_container",
|
||||
"ALL_EMPTY_CONTAINER_ID_SET",
|
||||
"isEmptyContainer",
|
||||
]
|
||||
|
@ -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.
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, QUrl
|
||||
|
||||
from UM.Stage import Stage
|
||||
|
||||
|
||||
# Since Cura has a few pre-defined "space claims" for the locations of certain components, we've provided some structure
|
||||
# to indicate this.
|
||||
# * 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.
|
||||
# * The MainComponent is the component that will be drawn starting from the bottom of the stageBar and fills the rest
|
||||
# of the screen.
|
||||
class CuraStage(Stage):
|
||||
|
||||
def __init__(self, parent = None):
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def stageId(self):
|
||||
def stageId(self) -> str:
|
||||
return self.getPluginId()
|
||||
|
||||
@pyqtProperty(QUrl, constant = True)
|
||||
def mainComponent(self):
|
||||
def mainComponent(self) -> QUrl:
|
||||
return self.getDisplayComponent("main")
|
||||
|
||||
@pyqtProperty(QUrl, constant = True)
|
||||
def sidebarComponent(self):
|
||||
return self.getDisplayComponent("sidebar")
|
||||
def stageMenuComponent(self) -> QUrl:
|
||||
return self.getDisplayComponent("menu")
|
||||
|
||||
|
||||
__all__ = ["CuraStage"]
|
||||
|
@ -1,2 +0,0 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
30
cura/UI/AddPrinterPagesModel.py
Normal file
30
cura/UI/AddPrinterPagesModel.py
Normal 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"]
|
@ -12,7 +12,7 @@ from UM.PluginRegistry import PluginRegistry # So MachineAction can be added as
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
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
|
||||
@ -136,7 +136,7 @@ class MachineActionManager(QObject):
|
||||
# action multiple times).
|
||||
# \param definition_id The ID of the definition that you want to get the "on added" actions for.
|
||||
# \returns List of actions.
|
||||
@pyqtSlot(str, result="QVariantList")
|
||||
@pyqtSlot(str, result = "QVariantList")
|
||||
def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]:
|
||||
if definition_id in self._first_start_actions:
|
||||
return self._first_start_actions[definition_id]
|
82
cura/UI/MachineSettingsManager.py
Normal file
82
cura/UI/MachineSettingsManager.py
Normal 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()
|
@ -1,54 +1,64 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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 UM.Application import Application
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Scene.Camera import Camera
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.i18n import i18nCatalog
|
||||
from collections import defaultdict
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Keep track of all objects in the project
|
||||
class ObjectsModel(ListModel):
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
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)
|
||||
|
||||
self._update_timer = QTimer()
|
||||
self._update_timer.setInterval(100)
|
||||
self._update_timer.setInterval(200)
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self._update)
|
||||
|
||||
self._build_plate_number = -1
|
||||
|
||||
def setActiveBuildPlate(self, nr):
|
||||
self._build_plate_number = nr
|
||||
self._update()
|
||||
def setActiveBuildPlate(self, nr: int) -> None:
|
||||
if self._build_plate_number != nr:
|
||||
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()
|
||||
|
||||
def _update(self, *args):
|
||||
def _update(self, *args) -> None:
|
||||
nodes = []
|
||||
filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate")
|
||||
active_build_plate_number = self._build_plate_number
|
||||
group_nr = 1
|
||||
name_count_dict = defaultdict(int)
|
||||
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):
|
||||
continue
|
||||
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
|
||||
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)
|
||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||
continue
|
||||
@ -64,7 +74,7 @@ class ObjectsModel(ListModel):
|
||||
group_nr += 1
|
||||
|
||||
if hasattr(node, "isOutsideBuildArea"):
|
||||
is_outside_build_area = node.isOutsideBuildArea()
|
||||
is_outside_build_area = node.isOutsideBuildArea() # type: ignore
|
||||
else:
|
||||
is_outside_build_area = False
|
||||
|
@ -5,8 +5,7 @@ import json
|
||||
import math
|
||||
import os
|
||||
import unicodedata
|
||||
import re # To create abbreviations for printer names.
|
||||
from typing import Dict, List, Optional
|
||||
from typing import Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
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.Scene.SceneNode import SceneNode
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.MimeTypeDatabase import MimeTypeDatabase
|
||||
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
@ -70,10 +66,8 @@ class PrintInformation(QObject):
|
||||
self._application.getInstance().getPreferences().preferenceChanged.connect(self._onPreferencesChanged)
|
||||
|
||||
self._multi_build_plate_model.activeBuildPlateChanged.connect(self._onActiveBuildPlateChanged)
|
||||
|
||||
self._onActiveMaterialsChanged()
|
||||
|
||||
self._material_amounts = [] # type: List[float]
|
||||
self._onActiveMaterialsChanged()
|
||||
|
||||
def initializeCuraMessagePrintTimeProperties(self) -> None:
|
||||
self._current_print_time = {} # type: Dict[int, Duration]
|
||||
@ -221,6 +215,7 @@ class PrintInformation(QObject):
|
||||
|
||||
material_guid = material.getMetaDataEntry("GUID")
|
||||
material_name = material.getName()
|
||||
|
||||
if material_guid in material_preference_values:
|
||||
material_values = material_preference_values[material_guid]
|
||||
|
||||
@ -361,7 +356,7 @@ class PrintInformation(QObject):
|
||||
try:
|
||||
mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
|
||||
data = mime_type.stripExtension(name)
|
||||
except:
|
||||
except MimeTypeNotFoundError:
|
||||
Logger.log("w", "Unsupported Mime Type Database file extension %s", name)
|
||||
|
||||
if data is not None and check_name is not None:
|
||||
@ -369,6 +364,16 @@ class PrintInformation(QObject):
|
||||
else:
|
||||
self._base_name = ""
|
||||
|
||||
# Strip the old "curaproject" extension from the name
|
||||
OLD_CURA_PROJECT_EXT = ".curaproject"
|
||||
if self._base_name.lower().endswith(OLD_CURA_PROJECT_EXT):
|
||||
self._base_name = self._base_name[:len(self._base_name) - len(OLD_CURA_PROJECT_EXT)]
|
||||
|
||||
# CURA-5896 Try to strip extra extensions with an infinite amount of ".curaproject.3mf".
|
||||
OLD_CURA_PROJECT_3MF_EXT = ".curaproject.3mf"
|
||||
while self._base_name.lower().endswith(OLD_CURA_PROJECT_3MF_EXT):
|
||||
self._base_name = self._base_name[:len(self._base_name) - len(OLD_CURA_PROJECT_3MF_EXT)]
|
||||
|
||||
self._updateJobName()
|
||||
|
||||
@pyqtProperty(str, fset = setBaseName, notify = baseNameChanged)
|
||||
@ -385,28 +390,14 @@ class PrintInformation(QObject):
|
||||
return
|
||||
active_machine_type_name = global_container_stack.definition.getName()
|
||||
|
||||
abbr_machine = ""
|
||||
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
|
||||
self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name)
|
||||
|
||||
## Utility method that strips accents from characters (eg: â -> a)
|
||||
def _stripAccents(self, to_strip: str) -> str:
|
||||
return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')
|
||||
|
||||
@pyqtSlot(result = "QVariantMap")
|
||||
def getFeaturePrintTimes(self):
|
||||
def getFeaturePrintTimes(self) -> Dict[str, Duration]:
|
||||
result = {}
|
||||
if self._active_build_plate not in self._print_times_per_feature:
|
||||
self._initPrintTimesPerFeature(self._active_build_plate)
|
69
cura/UI/TextManager.py
Normal file
69
cura/UI/TextManager.py
Normal 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
Loading…
x
Reference in New Issue
Block a user