mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-13 09:47:50 -06:00
Merge branch 'master' into feature_model_list
This commit is contained in:
commit
9e5e57e6c5
514 changed files with 80530 additions and 9839 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -71,3 +71,4 @@ run.sh
|
||||||
.scannerwork/
|
.scannerwork/
|
||||||
CuraEngine
|
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)
|
project(cura)
|
||||||
cmake_minimum_required(VERSION 2.8.12)
|
cmake_minimum_required(VERSION 3.6)
|
||||||
|
|
||||||
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/
|
|
||||||
${CMAKE_MODULE_PATH})
|
|
||||||
|
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
|
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
|
||||||
|
|
||||||
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository")
|
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository")
|
||||||
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
|
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
|
||||||
|
|
||||||
|
@ -28,6 +27,26 @@ set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
|
||||||
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
|
configure_file(${CMAKE_SOURCE_DIR}/cura.desktop.in ${CMAKE_BINARY_DIR}/cura.desktop @ONLY)
|
||||||
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
|
configure_file(cura/CuraVersion.py.in CuraVersion.py @ONLY)
|
||||||
|
|
||||||
|
|
||||||
|
# FIXME: Remove the code for CMake <3.12 once we have switched over completely.
|
||||||
|
# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
|
||||||
|
# module is copied from the CMake repository here so in CMake <3.12 we can still use it.
|
||||||
|
if(${CMAKE_VERSION} VERSION_LESS 3.12)
|
||||||
|
# Use FindPythonInterp and FindPythonLibs for CMake <3.12
|
||||||
|
find_package(PythonInterp 3 REQUIRED)
|
||||||
|
|
||||||
|
set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
|
||||||
|
|
||||||
|
set(Python3_VERSION ${PYTHON_VERSION_STRING})
|
||||||
|
set(Python3_VERSION_MAJOR ${PYTHON_VERSION_MAJOR})
|
||||||
|
set(Python3_VERSION_MINOR ${PYTHON_VERSION_MINOR})
|
||||||
|
set(Python3_VERSION_PATCH ${PYTHON_VERSION_PATCH})
|
||||||
|
else()
|
||||||
|
# Use FindPython3 for CMake >=3.12
|
||||||
|
find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
|
||||||
if(NOT ${URANIUM_DIR} STREQUAL "")
|
if(NOT ${URANIUM_DIR} STREQUAL "")
|
||||||
set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake")
|
set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake")
|
||||||
endif()
|
endif()
|
||||||
|
@ -40,12 +59,12 @@ if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
|
||||||
CREATE_TRANSLATION_TARGETS()
|
CREATE_TRANSLATION_TARGETS()
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
find_package(PythonInterp 3.5.0 REQUIRED)
|
|
||||||
|
|
||||||
install(DIRECTORY resources
|
install(DIRECTORY resources
|
||||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
|
||||||
install(DIRECTORY plugins
|
install(DIRECTORY plugins
|
||||||
DESTINATION lib${LIB_SUFFIX}/cura)
|
DESTINATION lib${LIB_SUFFIX}/cura)
|
||||||
|
|
||||||
if(NOT APPLE AND NOT WIN32)
|
if(NOT APPLE AND NOT WIN32)
|
||||||
install(FILES cura_app.py
|
install(FILES cura_app.py
|
||||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
|
@ -53,16 +72,16 @@ if(NOT APPLE AND NOT WIN32)
|
||||||
RENAME cura)
|
RENAME cura)
|
||||||
if(EXISTS /etc/debian_version)
|
if(EXISTS /etc/debian_version)
|
||||||
install(DIRECTORY cura
|
install(DIRECTORY cura
|
||||||
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}/dist-packages
|
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}/dist-packages
|
||||||
FILES_MATCHING PATTERN *.py)
|
FILES_MATCHING PATTERN *.py)
|
||||||
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
||||||
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}/dist-packages/cura)
|
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}/dist-packages/cura)
|
||||||
else()
|
else()
|
||||||
install(DIRECTORY cura
|
install(DIRECTORY cura
|
||||||
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages
|
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages
|
||||||
FILES_MATCHING PATTERN *.py)
|
FILES_MATCHING PATTERN *.py)
|
||||||
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
||||||
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura)
|
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages/cura)
|
||||||
endif()
|
endif()
|
||||||
install(FILES ${CMAKE_BINARY_DIR}/cura.desktop
|
install(FILES ${CMAKE_BINARY_DIR}/cura.desktop
|
||||||
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
|
||||||
|
@ -78,8 +97,8 @@ else()
|
||||||
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||||
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
|
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
|
||||||
install(DIRECTORY cura
|
install(DIRECTORY cura
|
||||||
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages
|
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages
|
||||||
FILES_MATCHING PATTERN *.py)
|
FILES_MATCHING PATTERN *.py)
|
||||||
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
|
||||||
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura)
|
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages/cura)
|
||||||
endif()
|
endif()
|
||||||
|
|
|
@ -1,10 +1,21 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
enable_testing()
|
include(CTest)
|
||||||
include(CMakeParseArguments)
|
include(CMakeParseArguments)
|
||||||
|
|
||||||
find_package(PythonInterp 3.5.0 REQUIRED)
|
# FIXME: Remove the code for CMake <3.12 once we have switched over completely.
|
||||||
|
# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
|
||||||
|
# module is copied from the CMake repository here so in CMake <3.12 we can still use it.
|
||||||
|
if(${CMAKE_VERSION} VERSION_LESS 3.12)
|
||||||
|
# Use FindPythonInterp and FindPythonLibs for CMake <3.12
|
||||||
|
find_package(PythonInterp 3 REQUIRED)
|
||||||
|
|
||||||
|
set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
|
||||||
|
else()
|
||||||
|
# Use FindPython3 for CMake >=3.12
|
||||||
|
find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
|
||||||
|
endif()
|
||||||
|
|
||||||
add_custom_target(test-verbose COMMAND ${CMAKE_CTEST_COMMAND} --verbose)
|
add_custom_target(test-verbose COMMAND ${CMAKE_CTEST_COMMAND} --verbose)
|
||||||
|
|
||||||
|
@ -36,7 +47,7 @@ function(cura_add_test)
|
||||||
if (NOT ${test_exists})
|
if (NOT ${test_exists})
|
||||||
add_test(
|
add_test(
|
||||||
NAME ${_NAME}
|
NAME ${_NAME}
|
||||||
COMMAND ${PYTHON_EXECUTABLE} -m pytest --verbose --full-trace --capture=no --no-print-log --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
|
COMMAND ${Python3_EXECUTABLE} -m pytest --verbose --full-trace --capture=no --no-print-log --junitxml=${CMAKE_BINARY_DIR}/junit-${_NAME}.xml ${_DIRECTORY}
|
||||||
)
|
)
|
||||||
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C)
|
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT LANG=C)
|
||||||
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")
|
set_tests_properties(${_NAME} PROPERTIES ENVIRONMENT "PYTHONPATH=${_PYTHONPATH}")
|
||||||
|
@ -59,13 +70,13 @@ endforeach()
|
||||||
#Add code style test.
|
#Add code style test.
|
||||||
add_test(
|
add_test(
|
||||||
NAME "code-style"
|
NAME "code-style"
|
||||||
COMMAND ${PYTHON_EXECUTABLE} run_mypy.py
|
COMMAND ${Python3_EXECUTABLE} run_mypy.py
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
)
|
)
|
||||||
|
|
||||||
#Add test for whether the shortcut alt-keys are unique in every translation.
|
#Add test for whether the shortcut alt-keys are unique in every translation.
|
||||||
add_test(
|
add_test(
|
||||||
NAME "shortcut-keys"
|
NAME "shortcut-keys"
|
||||||
COMMAND ${PYTHON_EXECUTABLE} scripts/check_shortcut_keys.py
|
COMMAND ${Python3_EXECUTABLE} scripts/check_shortcut_keys.py
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
)
|
)
|
19
contributing.md
Normal file
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`.
|
|
@ -29,6 +29,7 @@ i18n_catalog = i18nCatalog("cura")
|
||||||
class Account(QObject):
|
class Account(QObject):
|
||||||
# Signal emitted when user logged in or out.
|
# Signal emitted when user logged in or out.
|
||||||
loginStateChanged = pyqtSignal(bool)
|
loginStateChanged = pyqtSignal(bool)
|
||||||
|
accessTokenChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
def __init__(self, application: "CuraApplication", parent = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
@ -59,8 +60,12 @@ class Account(QObject):
|
||||||
self._authorization_service.initialize(self._application.getPreferences())
|
self._authorization_service.initialize(self._application.getPreferences())
|
||||||
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
|
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
|
||||||
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
|
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
|
||||||
|
self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged)
|
||||||
self._authorization_service.loadAuthDataFromPreferences()
|
self._authorization_service.loadAuthDataFromPreferences()
|
||||||
|
|
||||||
|
def _onAccessTokenChanged(self):
|
||||||
|
self.accessTokenChanged.emit()
|
||||||
|
|
||||||
## Returns a boolean indicating whether the given authentication is applied against staging or not.
|
## Returns a boolean indicating whether the given authentication is applied against staging or not.
|
||||||
@property
|
@property
|
||||||
def is_staging(self) -> bool:
|
def is_staging(self) -> bool:
|
||||||
|
@ -105,7 +110,7 @@ class Account(QObject):
|
||||||
return None
|
return None
|
||||||
return user_profile.profile_image_url
|
return user_profile.profile_image_url
|
||||||
|
|
||||||
@pyqtProperty(str, notify=loginStateChanged)
|
@pyqtProperty(str, notify=accessTokenChanged)
|
||||||
def accessToken(self) -> Optional[str]:
|
def accessToken(self) -> Optional[str]:
|
||||||
return self._authorization_service.getAccessToken()
|
return self._authorization_service.getAccessToken()
|
||||||
|
|
||||||
|
|
|
@ -217,11 +217,6 @@ class Arrange:
|
||||||
prio_slice = self._priority[min_y:max_y, min_x:max_x]
|
prio_slice = self._priority[min_y:max_y, min_x:max_x]
|
||||||
prio_slice[new_occupied] = 999
|
prio_slice[new_occupied] = 999
|
||||||
|
|
||||||
# If you want to see how the rasterized arranger build plate looks like, uncomment this code
|
|
||||||
# numpy.set_printoptions(linewidth=500, edgeitems=200)
|
|
||||||
# print(self._occupied.shape)
|
|
||||||
# print(self._occupied)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def isEmpty(self):
|
def isEmpty(self):
|
||||||
return self._is_empty
|
return self._is_empty
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
@ -48,7 +48,6 @@ class ArrangeArray:
|
||||||
return self._count
|
return self._count
|
||||||
|
|
||||||
def get(self, index):
|
def get(self, index):
|
||||||
print(self._arrange)
|
|
||||||
return self._arrange[index]
|
return self._arrange[index]
|
||||||
|
|
||||||
def getFirstEmpty(self):
|
def getFirstEmpty(self):
|
||||||
|
|
|
@ -19,6 +19,7 @@ class AutoSave:
|
||||||
self._change_timer.setInterval(self._application.getPreferences().getValue("cura/autosave_delay"))
|
self._change_timer.setInterval(self._application.getPreferences().getValue("cura/autosave_delay"))
|
||||||
self._change_timer.setSingleShot(True)
|
self._change_timer.setSingleShot(True)
|
||||||
|
|
||||||
|
self._enabled = True
|
||||||
self._saving = False
|
self._saving = False
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
|
@ -32,6 +33,13 @@ class AutoSave:
|
||||||
if not self._saving:
|
if not self._saving:
|
||||||
self._change_timer.start()
|
self._change_timer.start()
|
||||||
|
|
||||||
|
def setEnabled(self, enabled: bool) -> None:
|
||||||
|
self._enabled = enabled
|
||||||
|
if self._enabled:
|
||||||
|
self._change_timer.start()
|
||||||
|
else:
|
||||||
|
self._change_timer.stop()
|
||||||
|
|
||||||
def _onGlobalStackChanged(self):
|
def _onGlobalStackChanged(self):
|
||||||
if self._global_stack:
|
if self._global_stack:
|
||||||
self._global_stack.propertyChanged.disconnect(self._triggerTimer)
|
self._global_stack.propertyChanged.disconnect(self._triggerTimer)
|
||||||
|
|
|
@ -116,12 +116,13 @@ class Backup:
|
||||||
|
|
||||||
current_version = self._application.getVersion()
|
current_version = self._application.getVersion()
|
||||||
version_to_restore = self.meta_data.get("cura_release", "master")
|
version_to_restore = self.meta_data.get("cura_release", "master")
|
||||||
if current_version != version_to_restore:
|
|
||||||
# Cannot restore version older or newer than current because settings might have changed.
|
if current_version < version_to_restore:
|
||||||
# Restoring this will cause a lot of issues so we don't allow this for now.
|
# Cannot restore version newer than current because settings might have changed.
|
||||||
|
Logger.log("d", "Tried to restore a Cura backup of version {version_to_restore} with cura version {current_version}".format(version_to_restore = version_to_restore, current_version = current_version))
|
||||||
self._showMessage(
|
self._showMessage(
|
||||||
self.catalog.i18nc("@info:backup_failed",
|
self.catalog.i18nc("@info:backup_failed",
|
||||||
"Tried to restore a Cura backup that does not match your current version."))
|
"Tried to restore a Cura backup that is higher than the current version."))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
version_data_dir = Resources.getDataStoragePath()
|
version_data_dir = Resources.getDataStoragePath()
|
||||||
|
|
|
@ -51,8 +51,8 @@ class BackupsManager:
|
||||||
## Here we try to disable the auto-save plug-in as it might interfere with
|
## Here we try to disable the auto-save plug-in as it might interfere with
|
||||||
# restoring a back-up.
|
# restoring a back-up.
|
||||||
def _disableAutoSave(self) -> None:
|
def _disableAutoSave(self) -> None:
|
||||||
self._application.setSaveDataEnabled(False)
|
self._application.getAutoSave().setEnabled(False)
|
||||||
|
|
||||||
## Re-enable auto-save after we're done.
|
## Re-enable auto-save after we're done.
|
||||||
def _enableAutoSave(self) -> None:
|
def _enableAutoSave(self) -> None:
|
||||||
self._application.setSaveDataEnabled(True)
|
self._application.getAutoSave().setEnabled(True)
|
||||||
|
|
|
@ -13,113 +13,122 @@ from PyQt5.QtGui import QColor, QIcon
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
from UM.Decorators import override
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
|
from UM.Platform import Platform
|
||||||
from UM.PluginError import PluginNotFoundError
|
from UM.PluginError import PluginNotFoundError
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Resources import Resources
|
||||||
from UM.Scene.Camera import Camera
|
from UM.Preferences import Preferences
|
||||||
from UM.Math.Vector import Vector
|
from UM.Qt.Bindings import MainWindow
|
||||||
from UM.Math.Quaternion import Quaternion
|
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.AxisAlignedBox import AxisAlignedBox
|
||||||
from UM.Math.Matrix import Matrix
|
from UM.Math.Matrix import Matrix
|
||||||
from UM.Platform import Platform
|
from UM.Math.Quaternion import Quaternion
|
||||||
from UM.Resources import Resources
|
from UM.Math.Vector import Vector
|
||||||
from UM.Scene.ToolHandle import ToolHandle
|
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
|
||||||
from UM.Mesh.ReadMeshJob import ReadMeshJob
|
from UM.Mesh.ReadMeshJob import ReadMeshJob
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.Preferences import Preferences
|
|
||||||
from UM.Qt.QtApplication import QtApplication #The class we're inheriting from.
|
|
||||||
from UM.View.SelectionPass import SelectionPass #For typing.
|
|
||||||
from UM.Scene.Selection import Selection
|
|
||||||
from UM.Scene.GroupDecorator import GroupDecorator
|
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
|
||||||
from UM.Settings.Validator import Validator
|
|
||||||
from UM.Message import Message
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
from UM.Workspace.WorkspaceReader import WorkspaceReader
|
|
||||||
|
|
||||||
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
|
||||||
from UM.Operations.GroupedOperation import GroupedOperation
|
from UM.Operations.GroupedOperation import GroupedOperation
|
||||||
from UM.Operations.SetTransformOperation import SetTransformOperation
|
from UM.Operations.SetTransformOperation import SetTransformOperation
|
||||||
|
|
||||||
|
from UM.Scene.Camera import Camera
|
||||||
|
from UM.Scene.GroupDecorator import GroupDecorator
|
||||||
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
|
from UM.Scene.SceneNode import SceneNode
|
||||||
|
from UM.Scene.Selection import Selection
|
||||||
|
from UM.Scene.ToolHandle import ToolHandle
|
||||||
|
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
|
||||||
|
from UM.Settings.SettingFunction import SettingFunction
|
||||||
|
from UM.Settings.Validator import Validator
|
||||||
|
|
||||||
|
from UM.Workspace.WorkspaceReader import WorkspaceReader
|
||||||
|
|
||||||
from cura.API import CuraAPI
|
from cura.API import CuraAPI
|
||||||
|
|
||||||
from cura.Arranging.Arrange import Arrange
|
from cura.Arranging.Arrange import Arrange
|
||||||
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||||
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
||||||
from cura.Arranging.ShapeArray import ShapeArray
|
from cura.Arranging.ShapeArray import ShapeArray
|
||||||
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
|
||||||
from cura.GlobalStacksModel import GlobalStacksModel
|
|
||||||
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
|
||||||
from cura.Operations.SetParentOperation import SetParentOperation
|
from cura.Operations.SetParentOperation import SetParentOperation
|
||||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
|
||||||
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
|
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
|
||||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
||||||
|
|
||||||
from cura.Scene.CuraSceneController import CuraSceneController
|
from cura.Scene.CuraSceneController import CuraSceneController
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
|
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from cura.Scene import ZOffsetDecorator
|
||||||
from UM.Settings.SettingFunction import SettingFunction
|
|
||||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
|
||||||
from cura.Settings.MachineNameValidator import MachineNameValidator
|
|
||||||
|
|
||||||
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
|
||||||
from cura.Machines.Models.NozzleModel import NozzleModel
|
|
||||||
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
|
|
||||||
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
|
|
||||||
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
|
|
||||||
from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
|
|
||||||
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
|
|
||||||
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
|
|
||||||
from cura.Machines.Models.QualityManagementModel import QualityManagementModel
|
|
||||||
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
|
|
||||||
from cura.Machines.Models.MachineManagementModel import MachineManagementModel
|
|
||||||
|
|
||||||
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
|
|
||||||
|
|
||||||
from cura.Machines.MachineErrorChecker import MachineErrorChecker
|
from cura.Machines.MachineErrorChecker import MachineErrorChecker
|
||||||
|
from cura.Machines.VariantManager import VariantManager
|
||||||
|
|
||||||
|
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
|
||||||
|
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
|
||||||
|
from cura.Machines.Models.DiscoveredPrintersModel import DiscoveredPrintersModel
|
||||||
|
from cura.Machines.Models.ExtrudersModel import ExtrudersModel
|
||||||
|
from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
|
||||||
|
from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachineActionsModel
|
||||||
|
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
|
||||||
|
from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel
|
||||||
|
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
|
||||||
|
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
|
||||||
|
from cura.Machines.Models.NozzleModel import NozzleModel
|
||||||
|
from cura.Machines.Models.QualityManagementModel import QualityManagementModel
|
||||||
|
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
|
||||||
|
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
|
||||||
|
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
|
||||||
|
from cura.Machines.Models.UserChangesModel import UserChangesModel
|
||||||
|
|
||||||
|
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||||
|
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
|
||||||
|
|
||||||
|
import cura.Settings.cura_empty_instance_containers
|
||||||
|
from cura.Settings.ContainerManager import ContainerManager
|
||||||
|
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||||
|
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
from cura.Settings.MachineManager import MachineManager
|
||||||
|
from cura.Settings.MachineNameValidator import MachineNameValidator
|
||||||
|
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
|
||||||
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
|
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
|
||||||
|
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
|
||||||
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
|
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
|
||||||
|
|
||||||
from cura.Machines.VariantManager import VariantManager
|
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
|
||||||
|
|
||||||
|
from cura.UI import CuraSplashScreen, MachineActionManager, PrintInformation
|
||||||
|
from cura.UI.MachineSettingsManager import MachineSettingsManager
|
||||||
|
from cura.UI.ObjectsModel import ObjectsModel
|
||||||
|
from cura.UI.TextManager import TextManager
|
||||||
|
from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
|
||||||
|
from cura.UI.WelcomePagesModel import WelcomePagesModel
|
||||||
|
from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel
|
||||||
|
|
||||||
|
from cura.Utils.NetworkingUtil import NetworkingUtil
|
||||||
|
|
||||||
from .SingleInstance import SingleInstance
|
from .SingleInstance import SingleInstance
|
||||||
from .AutoSave import AutoSave
|
from .AutoSave import AutoSave
|
||||||
from . import PlatformPhysics
|
from . import PlatformPhysics
|
||||||
from . import BuildVolume
|
from . import BuildVolume
|
||||||
from . import CameraAnimation
|
from . import CameraAnimation
|
||||||
from . import PrintInformation
|
|
||||||
from . import CuraActions
|
from . import CuraActions
|
||||||
from cura.Scene import ZOffsetDecorator
|
|
||||||
from . import CuraSplashScreen
|
|
||||||
from . import PrintJobPreviewImageProvider
|
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 cura.PrinterOutputDevice import PrinterOutputDevice
|
|
||||||
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
|
|
||||||
|
|
||||||
from cura import ApplicationMetadata, UltimakerCloudAuthentication
|
from cura import ApplicationMetadata, UltimakerCloudAuthentication
|
||||||
|
|
||||||
from UM.FlameProfiler import pyqtSlot
|
|
||||||
from UM.Decorators import override
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.Machines.MaterialManager import MaterialManager
|
from cura.Machines.MaterialManager import MaterialManager
|
||||||
from cura.Machines.QualityManager import QualityManager
|
from cura.Machines.QualityManager import QualityManager
|
||||||
|
@ -208,6 +217,15 @@ class CuraApplication(QtApplication):
|
||||||
self._cura_scene_controller = None
|
self._cura_scene_controller = None
|
||||||
self._machine_error_checker = None
|
self._machine_error_checker = None
|
||||||
|
|
||||||
|
self._machine_settings_manager = MachineSettingsManager(self, parent = self)
|
||||||
|
|
||||||
|
self._discovered_printer_model = DiscoveredPrintersModel(self, parent = self)
|
||||||
|
self._first_start_machine_actions_model = FirstStartMachineActionsModel(self, parent = self)
|
||||||
|
self._welcome_pages_model = WelcomePagesModel(self, parent = self)
|
||||||
|
self._add_printer_pages_model = AddPrinterPagesModel(self, parent = self)
|
||||||
|
self._whats_new_pages_model = WhatsNewPagesModel(self, parent = self)
|
||||||
|
self._text_manager = TextManager(parent = self)
|
||||||
|
|
||||||
self._quality_profile_drop_down_menu_model = None
|
self._quality_profile_drop_down_menu_model = None
|
||||||
self._custom_quality_profile_drop_down_menu_model = None
|
self._custom_quality_profile_drop_down_menu_model = None
|
||||||
self._cura_API = CuraAPI(self)
|
self._cura_API = CuraAPI(self)
|
||||||
|
@ -237,15 +255,12 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
self._update_platform_activity_timer = None
|
self._update_platform_activity_timer = None
|
||||||
|
|
||||||
self._need_to_show_user_agreement = True
|
|
||||||
|
|
||||||
self._sidebar_custom_menu_items = [] # type: list # Keeps list of custom menu items for the side bar
|
self._sidebar_custom_menu_items = [] # type: list # Keeps list of custom menu items for the side bar
|
||||||
|
|
||||||
self._plugins_loaded = False
|
self._plugins_loaded = False
|
||||||
|
|
||||||
# Backups
|
# Backups
|
||||||
self._auto_save = None
|
self._auto_save = None
|
||||||
self._save_data_enabled = True
|
|
||||||
|
|
||||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||||
self._container_registry_class = CuraContainerRegistry
|
self._container_registry_class = CuraContainerRegistry
|
||||||
|
@ -450,7 +465,6 @@ class CuraApplication(QtApplication):
|
||||||
# Misc.:
|
# Misc.:
|
||||||
"ConsoleLogger", #You want to be able to read the log if something goes wrong.
|
"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.
|
"CuraEngineBackend", #Cura is useless without this one since you can't slice.
|
||||||
"UserAgreement", #Our lawyers want every user to see this at least once.
|
|
||||||
"FileLogger", #You want to be able to read the log if something goes wrong.
|
"FileLogger", #You want to be able to read the log if something goes wrong.
|
||||||
"XmlMaterialProfile", #Cura crashes without this one.
|
"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.
|
"Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
|
||||||
|
@ -512,6 +526,10 @@ class CuraApplication(QtApplication):
|
||||||
preferences.addPreference("cura/show_list_of_files", False)
|
preferences.addPreference("cura/show_list_of_files", False)
|
||||||
preferences.addPreference("view/settings_list_height", 400)
|
preferences.addPreference("view/settings_list_height", 400)
|
||||||
preferences.addPreference("view/settings_visible", False)
|
preferences.addPreference("view/settings_visible", False)
|
||||||
|
preferences.addPreference("view/settings_xpos", 0)
|
||||||
|
preferences.addPreference("view/settings_ypos", 56)
|
||||||
|
preferences.addPreference("view/colorscheme_xpos", 0)
|
||||||
|
preferences.addPreference("view/colorscheme_ypos", 56)
|
||||||
preferences.addPreference("cura/currency", "€")
|
preferences.addPreference("cura/currency", "€")
|
||||||
preferences.addPreference("cura/material_settings", "{}")
|
preferences.addPreference("cura/material_settings", "{}")
|
||||||
|
|
||||||
|
@ -523,7 +541,7 @@ class CuraApplication(QtApplication):
|
||||||
preferences.addPreference("cura/expanded_brands", "")
|
preferences.addPreference("cura/expanded_brands", "")
|
||||||
preferences.addPreference("cura/expanded_types", "")
|
preferences.addPreference("cura/expanded_types", "")
|
||||||
|
|
||||||
self._need_to_show_user_agreement = not preferences.getValue("general/accepted_user_agreement")
|
preferences.addPreference("general/accepted_user_agreement", False)
|
||||||
|
|
||||||
for key in [
|
for key in [
|
||||||
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
|
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
|
||||||
|
@ -546,13 +564,20 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
@pyqtProperty(bool)
|
@pyqtProperty(bool)
|
||||||
def needToShowUserAgreement(self) -> bool:
|
def needToShowUserAgreement(self) -> bool:
|
||||||
return self._need_to_show_user_agreement
|
return not UM.Util.parseBool(self.getPreferences().getValue("general/accepted_user_agreement"))
|
||||||
|
|
||||||
def setNeedToShowUserAgreement(self, set_value = True) -> None:
|
@pyqtSlot(bool)
|
||||||
self._need_to_show_user_agreement = set_value
|
def setNeedToShowUserAgreement(self, set_value: bool = True) -> None:
|
||||||
|
self.getPreferences().setValue("general/accepted_user_agreement", str(not set_value))
|
||||||
|
|
||||||
|
@pyqtSlot(str, str)
|
||||||
|
def writeToLog(self, severity: str, message: str) -> None:
|
||||||
|
Logger.log(severity, message)
|
||||||
|
|
||||||
# DO NOT call this function to close the application, use checkAndExitApplication() instead which will perform
|
# DO NOT call this function to close the application, use checkAndExitApplication() instead which will perform
|
||||||
# pre-exit checks such as checking for in-progress USB printing, etc.
|
# pre-exit checks such as checking for in-progress USB printing, etc.
|
||||||
|
# Except for the 'Decline and close' in the 'User Agreement'-step in the Welcome-pages, that should be a hard exit.
|
||||||
|
@pyqtSlot()
|
||||||
def closeApplication(self) -> None:
|
def closeApplication(self) -> None:
|
||||||
Logger.log("i", "Close application")
|
Logger.log("i", "Close application")
|
||||||
main_window = self.getMainWindow()
|
main_window = self.getMainWindow()
|
||||||
|
@ -651,12 +676,9 @@ class CuraApplication(QtApplication):
|
||||||
self._message_box_callback = None
|
self._message_box_callback = None
|
||||||
self._message_box_callback_arguments = []
|
self._message_box_callback_arguments = []
|
||||||
|
|
||||||
def setSaveDataEnabled(self, enabled: bool) -> None:
|
|
||||||
self._save_data_enabled = enabled
|
|
||||||
|
|
||||||
# Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
|
# Cura has multiple locations where instance containers need to be saved, so we need to handle this differently.
|
||||||
def saveSettings(self):
|
def saveSettings(self):
|
||||||
if not self.started or not self._save_data_enabled:
|
if not self.started:
|
||||||
# Do not do saving during application start or when data should not be saved on quit.
|
# Do not do saving during application start or when data should not be saved on quit.
|
||||||
return
|
return
|
||||||
ContainerRegistry.getInstance().saveDirtyContainers()
|
ContainerRegistry.getInstance().saveDirtyContainers()
|
||||||
|
@ -746,6 +768,11 @@ class CuraApplication(QtApplication):
|
||||||
# Initialize Cura API
|
# Initialize Cura API
|
||||||
self._cura_API.initialize()
|
self._cura_API.initialize()
|
||||||
|
|
||||||
|
self._output_device_manager.start()
|
||||||
|
self._welcome_pages_model.initialize()
|
||||||
|
self._add_printer_pages_model.initialize()
|
||||||
|
self._whats_new_pages_model.initialize()
|
||||||
|
|
||||||
# Detect in which mode to run and execute that mode
|
# Detect in which mode to run and execute that mode
|
||||||
if self._is_headless:
|
if self._is_headless:
|
||||||
self.runWithoutGUI()
|
self.runWithoutGUI()
|
||||||
|
@ -840,10 +867,38 @@ class CuraApplication(QtApplication):
|
||||||
# Hide the splash screen
|
# Hide the splash screen
|
||||||
self.closeSplash()
|
self.closeSplash()
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getDiscoveredPrintersModel(self, *args) -> "DiscoveredPrintersModel":
|
||||||
|
return self._discovered_printer_model
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getFirstStartMachineActionsModel(self, *args) -> "FirstStartMachineActionsModel":
|
||||||
|
return self._first_start_machine_actions_model
|
||||||
|
|
||||||
@pyqtSlot(result = QObject)
|
@pyqtSlot(result = QObject)
|
||||||
def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
|
def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
|
||||||
return self._setting_visibility_presets_model
|
return self._setting_visibility_presets_model
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getWelcomePagesModel(self, *args) -> "WelcomePagesModel":
|
||||||
|
return self._welcome_pages_model
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getAddPrinterPagesModel(self, *args) -> "AddPrinterPagesModel":
|
||||||
|
return self._add_printer_pages_model
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getWhatsNewPagesModel(self, *args) -> "WhatsNewPagesModel":
|
||||||
|
return self._whats_new_pages_model
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getMachineSettingsManager(self, *args) -> "MachineSettingsManager":
|
||||||
|
return self._machine_settings_manager
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getTextManager(self, *args) -> "TextManager":
|
||||||
|
return self._text_manager
|
||||||
|
|
||||||
def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
|
def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
|
||||||
if self._cura_formula_functions is None:
|
if self._cura_formula_functions is None:
|
||||||
self._cura_formula_functions = CuraFormulaFunctions(self)
|
self._cura_formula_functions = CuraFormulaFunctions(self)
|
||||||
|
@ -976,6 +1031,13 @@ class CuraApplication(QtApplication):
|
||||||
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
|
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
|
||||||
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
|
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
|
||||||
|
|
||||||
|
qmlRegisterType(NetworkingUtil, "Cura", 1, 5, "NetworkingUtil")
|
||||||
|
|
||||||
|
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")
|
qmlRegisterType(NetworkMJPGImage, "Cura", 1, 0, "NetworkMJPGImage")
|
||||||
|
|
||||||
qmlRegisterType(ObjectsModel, "Cura", 1, 0, "ObjectsModel")
|
qmlRegisterType(ObjectsModel, "Cura", 1, 0, "ObjectsModel")
|
||||||
|
@ -989,7 +1051,8 @@ class CuraApplication(QtApplication):
|
||||||
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
|
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
|
||||||
qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
|
qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
|
||||||
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
|
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
|
||||||
qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel")
|
|
||||||
|
qmlRegisterType(DiscoveredPrintersModel, "Cura", 1, 0, "DiscoveredPrintersModel")
|
||||||
|
|
||||||
qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
|
qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
|
||||||
"QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
|
"QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
|
||||||
|
@ -1000,6 +1063,7 @@ class CuraApplication(QtApplication):
|
||||||
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
|
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
|
||||||
qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel")
|
qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel")
|
||||||
qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
|
qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
|
||||||
|
qmlRegisterType(FirstStartMachineActionsModel, "Cura", 1, 0, "FirstStartMachineActionsModel")
|
||||||
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
|
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
|
||||||
qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
|
qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
|
||||||
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
|
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
|
||||||
|
@ -1056,7 +1120,6 @@ class CuraApplication(QtApplication):
|
||||||
self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
|
self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
|
||||||
self._camera_animation.start()
|
self._camera_animation.start()
|
||||||
|
|
||||||
requestAddPrinter = pyqtSignal()
|
|
||||||
activityChanged = pyqtSignal()
|
activityChanged = pyqtSignal()
|
||||||
sceneBoundingBoxChanged = pyqtSignal()
|
sceneBoundingBoxChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@ -1716,3 +1779,32 @@ class CuraApplication(QtApplication):
|
||||||
def getSidebarCustomMenuItems(self) -> list:
|
def getSidebarCustomMenuItems(self) -> list:
|
||||||
return self._sidebar_custom_menu_items
|
return self._sidebar_custom_menu_items
|
||||||
|
|
||||||
|
@pyqtSlot(result = bool)
|
||||||
|
def shouldShowWelcomeDialog(self) -> bool:
|
||||||
|
# Only show the complete flow if there is no printer yet.
|
||||||
|
return self._machine_manager.activeMachine is None
|
||||||
|
|
||||||
|
@pyqtSlot(result = bool)
|
||||||
|
def shouldShowWhatsNewDialog(self) -> bool:
|
||||||
|
has_active_machine = self._machine_manager.activeMachine is not None
|
||||||
|
has_app_just_upgraded = self.hasJustUpdatedFromOldVersion()
|
||||||
|
|
||||||
|
# Only show the what's new dialog if there's no machine and we have just upgraded
|
||||||
|
show_whatsnew_only = has_active_machine and has_app_just_upgraded
|
||||||
|
return show_whatsnew_only
|
||||||
|
|
||||||
|
@pyqtSlot(result = int)
|
||||||
|
def appWidth(self) -> int:
|
||||||
|
main_window = QtApplication.getInstance().getMainWindow()
|
||||||
|
if main_window:
|
||||||
|
return main_window.width()
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
@pyqtSlot(result = int)
|
||||||
|
def appHeight(self) -> int:
|
||||||
|
main_window = QtApplication.getInstance().getMainWindow()
|
||||||
|
if main_window:
|
||||||
|
return main_window.height()
|
||||||
|
else:
|
||||||
|
return 0
|
||||||
|
|
|
@ -3,8 +3,11 @@
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, QUrl
|
from PyQt5.QtCore import pyqtProperty, QUrl
|
||||||
|
|
||||||
|
from UM.Resources import Resources
|
||||||
from UM.View.View import View
|
from UM.View.View import View
|
||||||
|
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
|
||||||
# Since Cura has a few pre-defined "space claims" for the locations of certain components, we've provided some structure
|
# Since Cura has a few pre-defined "space claims" for the locations of certain components, we've provided some structure
|
||||||
# to indicate this.
|
# to indicate this.
|
||||||
|
@ -12,13 +15,20 @@ from UM.View.View import View
|
||||||
# the stageMenuComponent returns an item that should be used somehwere in the stage menu. It's up to the active 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.
|
# to actually do something with this.
|
||||||
class CuraView(View):
|
class CuraView(View):
|
||||||
def __init__(self, parent = None) -> None:
|
def __init__(self, parent = None, use_empty_menu_placeholder: bool = False) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self._empty_menu_placeholder_url = QUrl(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles,
|
||||||
|
"EmptyViewMenuComponent.qml"))
|
||||||
|
self._use_empty_menu_placeholder = use_empty_menu_placeholder
|
||||||
|
|
||||||
@pyqtProperty(QUrl, constant = True)
|
@pyqtProperty(QUrl, constant = True)
|
||||||
def mainComponent(self) -> QUrl:
|
def mainComponent(self) -> QUrl:
|
||||||
return self.getDisplayComponent("main")
|
return self.getDisplayComponent("main")
|
||||||
|
|
||||||
@pyqtProperty(QUrl, constant = True)
|
@pyqtProperty(QUrl, constant = True)
|
||||||
def stageMenuComponent(self) -> QUrl:
|
def stageMenuComponent(self) -> QUrl:
|
||||||
return self.getDisplayComponent("menu")
|
url = self.getDisplayComponent("menu")
|
||||||
|
if not url.toString() and self._use_empty_menu_placeholder:
|
||||||
|
url = self._empty_menu_placeholder_url
|
||||||
|
return url
|
||||||
|
|
|
@ -20,7 +20,7 @@ class LayerPolygon:
|
||||||
MoveCombingType = 8
|
MoveCombingType = 8
|
||||||
MoveRetractionType = 9
|
MoveRetractionType = 9
|
||||||
SupportInterfaceType = 10
|
SupportInterfaceType = 10
|
||||||
PrimeTower = 11
|
PrimeTowerType = 11
|
||||||
__number_of_types = 12
|
__number_of_types = 12
|
||||||
|
|
||||||
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType)
|
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType)
|
||||||
|
@ -245,7 +245,7 @@ class LayerPolygon:
|
||||||
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
|
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
|
||||||
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
|
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
|
||||||
theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType
|
theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType
|
||||||
theme.getColor("layerview_prime_tower").getRgbF()
|
theme.getColor("layerview_prime_tower").getRgbF() # PrimeTowerType
|
||||||
])
|
])
|
||||||
|
|
||||||
return cls.__color_map
|
return cls.__color_map
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
from PyQt5.QtCore import QObject, QUrl, pyqtSlot, pyqtProperty, pyqtSignal
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.PluginObject import PluginObject
|
from UM.PluginObject import PluginObject
|
||||||
|
@ -33,6 +34,12 @@ class MachineAction(QObject, PluginObject):
|
||||||
def getKey(self) -> str:
|
def getKey(self) -> str:
|
||||||
return self._key
|
return self._key
|
||||||
|
|
||||||
|
## Whether this action needs to ask the user anything.
|
||||||
|
# If not, we shouldn't present the user with certain screens which otherwise show up.
|
||||||
|
# Defaults to true to be in line with the old behaviour.
|
||||||
|
def needsUserInteraction(self) -> bool:
|
||||||
|
return True
|
||||||
|
|
||||||
@pyqtProperty(str, notify = labelChanged)
|
@pyqtProperty(str, notify = labelChanged)
|
||||||
def label(self) -> str:
|
def label(self) -> str:
|
||||||
return self._label
|
return self._label
|
||||||
|
@ -66,18 +73,26 @@ class MachineAction(QObject, PluginObject):
|
||||||
return self._finished
|
return self._finished
|
||||||
|
|
||||||
## Protected helper to create a view object based on provided QML.
|
## Protected helper to create a view object based on provided QML.
|
||||||
def _createViewFromQML(self) -> None:
|
def _createViewFromQML(self) -> Optional["QObject"]:
|
||||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||||
if plugin_path is None:
|
if plugin_path is None:
|
||||||
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
|
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
|
||||||
return
|
return None
|
||||||
path = os.path.join(plugin_path, self._qml_url)
|
path = os.path.join(plugin_path, self._qml_url)
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
self._view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
|
view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
|
||||||
|
return view
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
@pyqtProperty(QUrl, constant = True)
|
||||||
def displayItem(self):
|
def qmlPath(self) -> "QUrl":
|
||||||
if not self._view:
|
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||||
self._createViewFromQML()
|
if plugin_path is None:
|
||||||
return self._view
|
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
|
||||||
|
return QUrl("")
|
||||||
|
path = os.path.join(plugin_path, self._qml_url)
|
||||||
|
return QUrl.fromLocalFile(path)
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getDisplayItem(self) -> Optional["QObject"]:
|
||||||
|
return self._createViewFromQML()
|
||||||
|
|
|
@ -103,6 +103,8 @@ class MaterialManager(QObject):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
root_material_id = material_metadata.get("base_file", "")
|
root_material_id = material_metadata.get("base_file", "")
|
||||||
|
if root_material_id not in material_metadatas: #Not a registered material profile. Don't store this in the look-up tables.
|
||||||
|
continue
|
||||||
if root_material_id not in self._material_group_map:
|
if root_material_id not in self._material_group_map:
|
||||||
self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
|
self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
|
||||||
self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
|
self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
|
||||||
|
@ -219,7 +221,7 @@ class MaterialManager(QObject):
|
||||||
|
|
||||||
root_material_id = material_metadata["base_file"]
|
root_material_id = material_metadata["base_file"]
|
||||||
definition = material_metadata["definition"]
|
definition = material_metadata["definition"]
|
||||||
approximate_diameter = material_metadata["approximate_diameter"]
|
approximate_diameter = str(material_metadata["approximate_diameter"])
|
||||||
|
|
||||||
if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
|
if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
|
||||||
self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}
|
self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}
|
||||||
|
@ -332,7 +334,6 @@ class MaterialManager(QObject):
|
||||||
buildplate_node = nozzle_node.getChildNode(buildplate_name)
|
buildplate_node = nozzle_node.getChildNode(buildplate_name)
|
||||||
|
|
||||||
nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]
|
nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]
|
||||||
|
|
||||||
# Fallback mechanism of finding materials:
|
# Fallback mechanism of finding materials:
|
||||||
# 1. buildplate-specific material
|
# 1. buildplate-specific material
|
||||||
# 2. nozzle-specific material
|
# 2. nozzle-specific material
|
||||||
|
@ -537,16 +538,40 @@ class MaterialManager(QObject):
|
||||||
return
|
return
|
||||||
|
|
||||||
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
|
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
|
||||||
|
# Sort all nodes with respect to the container ID lengths in the ascending order so the base material container
|
||||||
|
# will be the first one to be removed. We need to do this to ensure that all containers get loaded & deleted.
|
||||||
|
nodes_to_remove = sorted(nodes_to_remove, key = lambda x: len(x.getMetaDataEntry("id", "")))
|
||||||
|
# Try to load all containers first. If there is any faulty ones, they will be put into the faulty container
|
||||||
|
# list, so removeContainer() can ignore those ones.
|
||||||
|
for node in nodes_to_remove:
|
||||||
|
container_id = node.getMetaDataEntry("id", "")
|
||||||
|
results = self._container_registry.findContainers(id = container_id)
|
||||||
|
if not results:
|
||||||
|
self._container_registry.addWrongContainerId(container_id)
|
||||||
for node in nodes_to_remove:
|
for node in nodes_to_remove:
|
||||||
self._container_registry.removeContainer(node.getMetaDataEntry("id", ""))
|
self._container_registry.removeContainer(node.getMetaDataEntry("id", ""))
|
||||||
|
|
||||||
#
|
#
|
||||||
# Methods for GUI
|
# Methods for GUI
|
||||||
#
|
#
|
||||||
|
@pyqtSlot("QVariant", result=bool)
|
||||||
|
def canMaterialBeRemoved(self, material_node: "MaterialNode"):
|
||||||
|
# Check if the material is active in any extruder train. In that case, the material shouldn't be removed!
|
||||||
|
# In the future we might enable this again, but right now, it's causing a ton of issues if we do (since it
|
||||||
|
# corrupts the configuration)
|
||||||
|
root_material_id = material_node.getMetaDataEntry("base_file")
|
||||||
|
material_group = self.getMaterialGroup(root_material_id)
|
||||||
|
if not material_group:
|
||||||
|
return False
|
||||||
|
|
||||||
|
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
|
||||||
|
ids_to_remove = [node.getMetaDataEntry("id", "") for node in nodes_to_remove]
|
||||||
|
|
||||||
|
for extruder_stack in self._container_registry.findContainerStacks(type="extruder_train"):
|
||||||
|
if extruder_stack.material.getId() in ids_to_remove:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
#
|
|
||||||
# Sets the new name for the given material.
|
|
||||||
#
|
|
||||||
@pyqtSlot("QVariant", str)
|
@pyqtSlot("QVariant", str)
|
||||||
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
|
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
|
||||||
root_material_id = material_node.getMetaDataEntry("base_file")
|
root_material_id = material_node.getMetaDataEntry("base_file")
|
||||||
|
|
250
cura/Machines/Models/DiscoveredPrintersModel.py
Normal file
250
cura/Machines/Models/DiscoveredPrintersModel.py
Normal file
|
@ -0,0 +1,250 @@
|
||||||
|
# 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, QTimer
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Util import parseBool
|
||||||
|
from UM.OutputDevice.OutputDeviceManager import ManualDeviceAdditionAttempt
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from PyQt5.QtCore import QObject
|
||||||
|
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
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()
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def address(self) -> str:
|
||||||
|
return self._ip_address
|
||||||
|
|
||||||
|
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
|
||||||
|
machine_manager = CuraApplication.getInstance().getMachineManager()
|
||||||
|
# In ClusterUM3OutputDevice, when it updates a printer information, it updates the machine type using the field
|
||||||
|
# "machine_variant", and for some reason, it's not the machine type ID/codename/... but a human-readable string
|
||||||
|
# like "Ultimaker 3". The code below handles this case.
|
||||||
|
if machine_manager.hasHumanReadableMachineTypeName(self._machine_type):
|
||||||
|
readable_type = self._machine_type
|
||||||
|
else:
|
||||||
|
readable_type = machine_manager.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
|
||||||
|
machine_manager = CuraApplication.getInstance().getMachineManager()
|
||||||
|
if machine_manager.hasHumanReadableMachineTypeName(self._machine_type):
|
||||||
|
readable_type = self._machine_type
|
||||||
|
else:
|
||||||
|
readable_type = machine_manager.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, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self._application = application
|
||||||
|
self._discovered_printer_by_ip_dict = dict() # type: Dict[str, DiscoveredPrinter]
|
||||||
|
|
||||||
|
self._plugin_for_manual_device = None # type: Optional[OutputDevicePlugin]
|
||||||
|
self._manual_device_address = ""
|
||||||
|
|
||||||
|
self._manual_device_request_timeout_in_seconds = 5 # timeout for adding a manual device in seconds
|
||||||
|
self._manual_device_request_timer = QTimer()
|
||||||
|
self._manual_device_request_timer.setInterval(self._manual_device_request_timeout_in_seconds * 1000)
|
||||||
|
self._manual_device_request_timer.setSingleShot(True)
|
||||||
|
self._manual_device_request_timer.timeout.connect(self._onManualRequestTimeout)
|
||||||
|
|
||||||
|
discoveredPrintersChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def checkManualDevice(self, address: str) -> None:
|
||||||
|
if self.hasManualDeviceRequestInProgress:
|
||||||
|
Logger.log("i", "A manual device request for address [%s] is still in progress, do nothing",
|
||||||
|
self._manual_device_address)
|
||||||
|
return
|
||||||
|
|
||||||
|
priority_order = [
|
||||||
|
ManualDeviceAdditionAttempt.PRIORITY,
|
||||||
|
ManualDeviceAdditionAttempt.POSSIBLE,
|
||||||
|
] # type: List[ManualDeviceAdditionAttempt]
|
||||||
|
|
||||||
|
all_plugins_dict = self._application.getOutputDeviceManager().getAllOutputDevicePlugins()
|
||||||
|
|
||||||
|
can_add_manual_plugins = [item for item in filter(
|
||||||
|
lambda plugin_item: plugin_item.canAddManualDevice(address) in priority_order,
|
||||||
|
all_plugins_dict.values())]
|
||||||
|
|
||||||
|
if not can_add_manual_plugins:
|
||||||
|
Logger.log("d", "Could not find a plugin to accept adding %s manually via address.", address)
|
||||||
|
return
|
||||||
|
|
||||||
|
plugin = max(can_add_manual_plugins, key = lambda p: priority_order.index(p.canAddManualDevice(address)))
|
||||||
|
self._plugin_for_manual_device = plugin
|
||||||
|
self._plugin_for_manual_device.addManualDevice(address, callback = self._onManualDeviceRequestFinished)
|
||||||
|
self._manual_device_address = address
|
||||||
|
self._manual_device_request_timer.start()
|
||||||
|
self.hasManualDeviceRequestInProgressChanged.emit()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def cancelCurrentManualDeviceRequest(self) -> None:
|
||||||
|
self._manual_device_request_timer.stop()
|
||||||
|
|
||||||
|
if self._manual_device_address:
|
||||||
|
if self._plugin_for_manual_device is not None:
|
||||||
|
self._plugin_for_manual_device.removeManualDevice(self._manual_device_address, address = self._manual_device_address)
|
||||||
|
self._manual_device_address = ""
|
||||||
|
self._plugin_for_manual_device = None
|
||||||
|
self.hasManualDeviceRequestInProgressChanged.emit()
|
||||||
|
self.manualDeviceRequestFinished.emit(False)
|
||||||
|
|
||||||
|
def _onManualRequestTimeout(self) -> None:
|
||||||
|
Logger.log("w", "Manual printer [%s] request timed out. Cancel the current request.", self._manual_device_address)
|
||||||
|
self.cancelCurrentManualDeviceRequest()
|
||||||
|
|
||||||
|
hasManualDeviceRequestInProgressChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = hasManualDeviceRequestInProgressChanged)
|
||||||
|
def hasManualDeviceRequestInProgress(self) -> bool:
|
||||||
|
return self._manual_device_address != ""
|
||||||
|
|
||||||
|
manualDeviceRequestFinished = pyqtSignal(bool, arguments = ["success"])
|
||||||
|
|
||||||
|
def _onManualDeviceRequestFinished(self, success: bool, address: str) -> None:
|
||||||
|
self._manual_device_request_timer.stop()
|
||||||
|
if address == self._manual_device_address:
|
||||||
|
self._manual_device_address = ""
|
||||||
|
self.hasManualDeviceRequestInProgressChanged.emit()
|
||||||
|
self.manualDeviceRequestFinished.emit(success)
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantMap", notify = discoveredPrintersChanged)
|
||||||
|
def discoveredPrintersByAddress(self) -> Dict[str, DiscoveredPrinter]:
|
||||||
|
return self._discovered_printer_by_ip_dict
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", 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())
|
|
@ -2,23 +2,25 @@
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer
|
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer
|
||||||
from typing import Iterable
|
from typing import Iterable, TYPE_CHECKING
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
import UM.Qt.ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
import UM.FlameProfiler
|
import UM.FlameProfiler
|
||||||
|
|
||||||
from cura.Settings.ExtruderStack import ExtruderStack # To listen to changes on the extruders.
|
if TYPE_CHECKING:
|
||||||
|
from cura.Settings.ExtruderStack import ExtruderStack # To listen to changes on the extruders.
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
## Model that holds extruders.
|
## Model that holds extruders.
|
||||||
#
|
#
|
||||||
# This model is designed for use by any list of extruders, but specifically
|
# This model is designed for use by any list of extruders, but specifically
|
||||||
# intended for drop-down lists of the current machine's extruders in place of
|
# intended for drop-down lists of the current machine's extruders in place of
|
||||||
# settings.
|
# settings.
|
||||||
class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
class ExtrudersModel(ListModel):
|
||||||
# The ID of the container stack for the extruder.
|
# The ID of the container stack for the extruder.
|
||||||
IdRole = Qt.UserRole + 1
|
IdRole = Qt.UserRole + 1
|
||||||
|
|
112
cura/Machines/Models/FirstStartMachineActionsModel.py
Normal file
112
cura/Machines/Models/FirstStartMachineActionsModel.py
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
self._previous_global_stack = None
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
# Do not update if the machine has not been switched. This can cause the SettingProviders on the Machine
|
||||||
|
# Setting page to do a force update, but they can use potential outdated cached values.
|
||||||
|
if self._previous_global_stack is not None and global_stack.getId() == self._previous_global_stack.getId():
|
||||||
|
return
|
||||||
|
self._previous_global_stack = global_stack
|
||||||
|
|
||||||
|
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.getDisplayItem(),
|
||||||
|
"action": item,
|
||||||
|
})
|
||||||
|
item.reset()
|
||||||
|
|
||||||
|
self.setItems(item_list)
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["FirstStartMachineActionsModel"]
|
|
@ -1,7 +1,6 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Logger import Logger
|
|
||||||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||||
|
|
||||||
class GenericMaterialsModel(BaseMaterialsModel):
|
class GenericMaterialsModel(BaseMaterialsModel):
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
|
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Util import parseBool
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, Qt, QTimer
|
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
|
||||||
|
|
||||||
from cura.PrinterOutputDevice import ConnectionType
|
|
||||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||||
|
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
|
|
||||||
|
@ -18,14 +18,18 @@ class GlobalStacksModel(ListModel):
|
||||||
HasRemoteConnectionRole = Qt.UserRole + 3
|
HasRemoteConnectionRole = Qt.UserRole + 3
|
||||||
ConnectionTypeRole = Qt.UserRole + 4
|
ConnectionTypeRole = Qt.UserRole + 4
|
||||||
MetaDataRole = Qt.UserRole + 5
|
MetaDataRole = Qt.UserRole + 5
|
||||||
|
DiscoverySourceRole = Qt.UserRole + 6 # For separating local and remote printers in the machine management page
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self._catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
self.addRoleName(self.NameRole, "name")
|
self.addRoleName(self.NameRole, "name")
|
||||||
self.addRoleName(self.IdRole, "id")
|
self.addRoleName(self.IdRole, "id")
|
||||||
self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection")
|
self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection")
|
||||||
self.addRoleName(self.MetaDataRole, "metadata")
|
self.addRoleName(self.MetaDataRole, "metadata")
|
||||||
self._container_stacks = []
|
self.addRoleName(self.DiscoverySourceRole, "discoverySource")
|
||||||
|
|
||||||
self._change_timer = QTimer()
|
self._change_timer = QTimer()
|
||||||
self._change_timer.setInterval(200)
|
self._change_timer.setInterval(200)
|
||||||
|
@ -36,35 +40,38 @@ class GlobalStacksModel(ListModel):
|
||||||
CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
|
CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
|
||||||
CuraContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
|
CuraContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
|
||||||
CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
|
CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
|
||||||
self._filter_dict = {}
|
|
||||||
self._updateDelayed()
|
self._updateDelayed()
|
||||||
|
|
||||||
## Handler for container added/removed events from registry
|
## Handler for container added/removed events from registry
|
||||||
def _onContainerChanged(self, container):
|
def _onContainerChanged(self, container) -> None:
|
||||||
# We only need to update when the added / removed container GlobalStack
|
# We only need to update when the added / removed container GlobalStack
|
||||||
if isinstance(container, GlobalStack):
|
if isinstance(container, GlobalStack):
|
||||||
self._updateDelayed()
|
self._updateDelayed()
|
||||||
|
|
||||||
def _updateDelayed(self):
|
def _updateDelayed(self) -> None:
|
||||||
self._change_timer.start()
|
self._change_timer.start()
|
||||||
|
|
||||||
def _update(self) -> None:
|
def _update(self) -> None:
|
||||||
items = []
|
items = []
|
||||||
|
|
||||||
container_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
container_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
||||||
|
|
||||||
for container_stack in container_stacks:
|
for container_stack in container_stacks:
|
||||||
has_remote_connection = False
|
has_remote_connection = False
|
||||||
|
|
||||||
for connection_type in container_stack.configuredConnectionTypes:
|
for connection_type in container_stack.configuredConnectionTypes:
|
||||||
has_remote_connection |= connection_type in [ConnectionType.NetworkConnection.value, ConnectionType.CloudConnection.value]
|
has_remote_connection |= connection_type in [ConnectionType.NetworkConnection.value,
|
||||||
|
ConnectionType.CloudConnection.value]
|
||||||
|
|
||||||
if container_stack.getMetaDataEntry("hidden", False) in ["True", True]:
|
if parseBool(container_stack.getMetaDataEntry("hidden", False)):
|
||||||
continue
|
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()),
|
items.append({"name": container_stack.getMetaDataEntry("group_name", container_stack.getName()),
|
||||||
"id": container_stack.getId(),
|
"id": container_stack.getId(),
|
||||||
"hasRemoteConnection": has_remote_connection,
|
"hasRemoteConnection": has_remote_connection,
|
||||||
"metadata": container_stack.getMetaData().copy()})
|
"metadata": container_stack.getMetaData().copy(),
|
||||||
items.sort(key=lambda i: not i["hasRemoteConnection"])
|
"discoverySource": section_name})
|
||||||
|
items.sort(key = lambda i: (not i["hasRemoteConnection"], i["name"]))
|
||||||
self.setItems(items)
|
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("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.get("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.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
from PyQt5.QtCore import Qt, pyqtSignal
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
from UM.Logger import Logger
|
|
||||||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||||
|
|
||||||
class MaterialTypesModel(ListModel):
|
class MaterialTypesModel(ListModel):
|
||||||
|
|
|
@ -10,7 +10,6 @@ from UM.Application import Application
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Settings.SettingFunction import SettingFunction
|
from UM.Settings.SettingFunction import SettingFunction
|
||||||
|
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
|
|
||||||
|
|
|
@ -209,6 +209,7 @@ class QualityManager(QObject):
|
||||||
# (1) the machine-specific node
|
# (1) the machine-specific node
|
||||||
# (2) the generic node
|
# (2) the generic node
|
||||||
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
|
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
|
||||||
|
|
||||||
# Check if this machine has specific quality profiles for its extruders, if so, when looking up extruder
|
# Check if this machine has specific quality profiles for its extruders, if so, when looking up extruder
|
||||||
# qualities, we should not fall back to use the global qualities.
|
# qualities, we should not fall back to use the global qualities.
|
||||||
has_extruder_specific_qualities = False
|
has_extruder_specific_qualities = False
|
||||||
|
@ -441,7 +442,8 @@ class QualityManager(QObject):
|
||||||
quality_changes_group = quality_model_item["quality_changes_group"]
|
quality_changes_group = quality_model_item["quality_changes_group"]
|
||||||
if quality_changes_group is None:
|
if quality_changes_group is None:
|
||||||
# create global quality changes only
|
# create global quality changes only
|
||||||
new_quality_changes = self._createQualityChanges(quality_group.quality_type, quality_changes_name,
|
new_name = self._container_registry.uniqueName(quality_changes_name)
|
||||||
|
new_quality_changes = self._createQualityChanges(quality_group.quality_type, new_name,
|
||||||
global_stack, None)
|
global_stack, None)
|
||||||
self._container_registry.addContainer(new_quality_changes)
|
self._container_registry.addContainer(new_quality_changes)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -50,6 +50,7 @@ class AuthorizationHelpers:
|
||||||
# \param refresh_token:
|
# \param refresh_token:
|
||||||
# \return An AuthenticationResponse object.
|
# \return An AuthenticationResponse object.
|
||||||
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
|
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
|
||||||
|
Logger.log("d", "Refreshing the access token.")
|
||||||
data = {
|
data = {
|
||||||
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
|
||||||
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
|
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",
|
||||||
|
|
|
@ -34,6 +34,8 @@ class AuthorizationService:
|
||||||
# Emit signal when authentication failed.
|
# Emit signal when authentication failed.
|
||||||
onAuthenticationError = Signal()
|
onAuthenticationError = Signal()
|
||||||
|
|
||||||
|
accessTokenChanged = Signal()
|
||||||
|
|
||||||
def __init__(self, settings: "OAuth2Settings", preferences: Optional["Preferences"] = None) -> None:
|
def __init__(self, settings: "OAuth2Settings", preferences: Optional["Preferences"] = None) -> None:
|
||||||
self._settings = settings
|
self._settings = settings
|
||||||
self._auth_helpers = AuthorizationHelpers(settings)
|
self._auth_helpers = AuthorizationHelpers(settings)
|
||||||
|
@ -68,6 +70,7 @@ class AuthorizationService:
|
||||||
self._user_profile = self._parseJWT()
|
self._user_profile = self._parseJWT()
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
# Unable to get connection, can't login.
|
# Unable to get connection, can't login.
|
||||||
|
Logger.logException("w", "Unable to validate user data with the remote server.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not self._user_profile and self._auth_data:
|
if not self._user_profile and self._auth_data:
|
||||||
|
@ -83,6 +86,7 @@ class AuthorizationService:
|
||||||
def _parseJWT(self) -> Optional["UserProfile"]:
|
def _parseJWT(self) -> Optional["UserProfile"]:
|
||||||
if not self._auth_data or self._auth_data.access_token is None:
|
if not self._auth_data or self._auth_data.access_token is None:
|
||||||
# If no auth data exists, we should always log in again.
|
# If no auth data exists, we should always log in again.
|
||||||
|
Logger.log("d", "There was no auth data or access token")
|
||||||
return None
|
return None
|
||||||
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
|
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||||
if user_data:
|
if user_data:
|
||||||
|
@ -90,12 +94,16 @@ class AuthorizationService:
|
||||||
return user_data
|
return user_data
|
||||||
# The JWT was expired or invalid and we should request a new one.
|
# The JWT was expired or invalid and we should request a new one.
|
||||||
if self._auth_data.refresh_token is None:
|
if self._auth_data.refresh_token is None:
|
||||||
|
Logger.log("w", "There was no refresh token in the auth data.")
|
||||||
return None
|
return None
|
||||||
self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
|
self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
|
||||||
if not self._auth_data or self._auth_data.access_token is None:
|
if not self._auth_data or self._auth_data.access_token is None:
|
||||||
|
Logger.log("w", "Unable to use the refresh token to get a new access token.")
|
||||||
# The token could not be refreshed using the refresh token. We should login again.
|
# The token could not be refreshed using the refresh token. We should login again.
|
||||||
return None
|
return None
|
||||||
|
# Ensure it gets stored as otherwise we only have it in memory. The stored refresh token has been deleted
|
||||||
|
# from the server already.
|
||||||
|
self._storeAuthData(self._auth_data)
|
||||||
return self._auth_helpers.parseJWT(self._auth_data.access_token)
|
return self._auth_helpers.parseJWT(self._auth_data.access_token)
|
||||||
|
|
||||||
## Get the access token as provided by the repsonse data.
|
## Get the access token as provided by the repsonse data.
|
||||||
|
@ -124,7 +132,8 @@ class AuthorizationService:
|
||||||
self._storeAuthData(response)
|
self._storeAuthData(response)
|
||||||
self.onAuthStateChanged.emit(logged_in = True)
|
self.onAuthStateChanged.emit(logged_in = True)
|
||||||
else:
|
else:
|
||||||
self.onAuthStateChanged(logged_in = False)
|
Logger.log("w", "Failed to get a new access token from the server.")
|
||||||
|
self.onAuthStateChanged.emit(logged_in = False)
|
||||||
|
|
||||||
## Delete the authentication data that we have stored locally (eg; logout)
|
## Delete the authentication data that we have stored locally (eg; logout)
|
||||||
def deleteAuthData(self) -> None:
|
def deleteAuthData(self) -> None:
|
||||||
|
@ -194,6 +203,7 @@ class AuthorizationService:
|
||||||
|
|
||||||
## Store authentication data in preferences.
|
## Store authentication data in preferences.
|
||||||
def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
|
def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
|
||||||
|
Logger.log("d", "Attempting to store the auth data")
|
||||||
if self._preferences is None:
|
if self._preferences is None:
|
||||||
Logger.log("e", "Unable to save authentication data, since no preference has been set!")
|
Logger.log("e", "Unable to save authentication data, since no preference has been set!")
|
||||||
return
|
return
|
||||||
|
@ -206,6 +216,8 @@ class AuthorizationService:
|
||||||
self._user_profile = None
|
self._user_profile = None
|
||||||
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
|
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
|
||||||
|
|
||||||
|
self.accessTokenChanged.emit()
|
||||||
|
|
||||||
def _onMessageActionTriggered(self, _, action):
|
def _onMessageActionTriggered(self, _, action):
|
||||||
if action == "retry":
|
if action == "retry":
|
||||||
self.loadAuthDataFromPreferences()
|
self.loadAuthDataFromPreferences()
|
||||||
|
|
|
@ -29,4 +29,4 @@ class PlatformPhysicsOperation(Operation):
|
||||||
return group
|
return group
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "PlatformPhysicsOperation(translation = {0})".format(self._translation)
|
return "PlatformPhysicsOp.(trans.={0})".format(self._translation)
|
||||||
|
|
|
@ -9,7 +9,7 @@ from typing import Union
|
||||||
|
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||||
|
|
||||||
class FirmwareUpdater(QObject):
|
class FirmwareUpdater(QObject):
|
||||||
firmwareProgressChanged = pyqtSignal()
|
firmwareProgressChanged = pyqtSignal()
|
||||||
|
|
|
@ -3,14 +3,15 @@
|
||||||
|
|
||||||
from typing import TYPE_CHECKING, Set, Union, Optional
|
from typing import TYPE_CHECKING, Set, Union, Optional
|
||||||
|
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
|
||||||
from PyQt5.QtCore import QTimer
|
from PyQt5.QtCore import QTimer
|
||||||
|
|
||||||
|
from .PrinterOutputController import PrinterOutputController
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
from .Models.PrintJobOutputModel import PrintJobOutputModel
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from .Models.PrinterOutputModel import PrinterOutputModel
|
||||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
from .PrinterOutputDevice import PrinterOutputDevice
|
||||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
from .Models.ExtruderOutputModel import ExtruderOutputModel
|
||||||
|
|
||||||
|
|
||||||
class GenericOutputController(PrinterOutputController):
|
class GenericOutputController(PrinterOutputController):
|
||||||
|
|
|
@ -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 PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
|
||||||
|
|
||||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
from .MaterialOutputModel import MaterialOutputModel
|
||||||
|
|
||||||
|
|
||||||
class ExtruderConfigurationModel(QObject):
|
class ExtruderConfigurationModel(QObject):
|
||||||
|
@ -62,7 +62,24 @@ class ExtruderConfigurationModel(QObject):
|
||||||
return " ".join(message_chunks)
|
return " ".join(message_chunks)
|
||||||
|
|
||||||
def __eq__(self, other) -> bool:
|
def __eq__(self, other) -> bool:
|
||||||
return hash(self) == hash(other)
|
if not isinstance(other, ExtruderConfigurationModel):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self._position != other.position:
|
||||||
|
return False
|
||||||
|
# Empty materials should be ignored for comparison
|
||||||
|
if self.activeMaterial is not None and other.activeMaterial is not None:
|
||||||
|
if self.activeMaterial.guid != other.activeMaterial.guid:
|
||||||
|
if self.activeMaterial.guid != "" and other.activeMaterial.guid != "":
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
# At this point there is no material, so it doesn't matter what the hotend is.
|
||||||
|
return True
|
||||||
|
|
||||||
|
if self.hotendID != other.hotendID:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
# Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is
|
# Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is
|
||||||
# unique within a set
|
# unique within a set
|
|
@ -1,14 +1,15 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
|
||||||
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
|
||||||
|
|
||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
||||||
|
|
||||||
|
from .ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from .MaterialOutputModel import MaterialOutputModel
|
||||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
from .PrinterOutputModel import PrinterOutputModel
|
||||||
|
|
||||||
|
|
||||||
class ExtruderOutputModel(QObject):
|
class ExtruderOutputModel(QObject):
|
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
|
171
cura/PrinterOutput/Models/PrintJobOutputModel.py
Normal file
171
cura/PrinterOutput/Models/PrintJobOutputModel.py
Normal file
|
@ -0,0 +1,171 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from typing import Optional, TYPE_CHECKING, List
|
||||||
|
|
||||||
|
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.Models.PrinterOutputModel import PrinterOutputModel
|
||||||
|
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||||
|
|
||||||
|
|
||||||
|
class PrintJobOutputModel(QObject):
|
||||||
|
stateChanged = pyqtSignal()
|
||||||
|
timeTotalChanged = pyqtSignal()
|
||||||
|
timeElapsedChanged = pyqtSignal()
|
||||||
|
nameChanged = pyqtSignal()
|
||||||
|
keyChanged = pyqtSignal()
|
||||||
|
assignedPrinterChanged = pyqtSignal()
|
||||||
|
ownerChanged = pyqtSignal()
|
||||||
|
configurationChanged = pyqtSignal()
|
||||||
|
previewImageChanged = pyqtSignal()
|
||||||
|
compatibleMachineFamiliesChanged = pyqtSignal()
|
||||||
|
|
||||||
|
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
self._output_controller = output_controller
|
||||||
|
self._state = ""
|
||||||
|
self._time_total = 0
|
||||||
|
self._time_elapsed = 0
|
||||||
|
self._name = name # Human readable name
|
||||||
|
self._key = key # Unique identifier
|
||||||
|
self._assigned_printer = None # type: Optional[PrinterOutputModel]
|
||||||
|
self._owner = "" # Who started/owns the print job?
|
||||||
|
|
||||||
|
self._configuration = None # type: Optional[PrinterConfigurationModel]
|
||||||
|
self._compatible_machine_families = [] # type: List[str]
|
||||||
|
self._preview_image_id = 0
|
||||||
|
|
||||||
|
self._preview_image = None # type: Optional[QImage]
|
||||||
|
|
||||||
|
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
|
||||||
|
def compatibleMachineFamilies(self):
|
||||||
|
# Hack; Some versions of cluster will return a family more than once...
|
||||||
|
return list(set(self._compatible_machine_families))
|
||||||
|
|
||||||
|
def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None:
|
||||||
|
if self._compatible_machine_families != compatible_machine_families:
|
||||||
|
self._compatible_machine_families = compatible_machine_families
|
||||||
|
self.compatibleMachineFamiliesChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(QUrl, notify=previewImageChanged)
|
||||||
|
def previewImageUrl(self):
|
||||||
|
self._preview_image_id += 1
|
||||||
|
# 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
|
||||||
|
return QUrl(temp, QUrl.TolerantMode)
|
||||||
|
|
||||||
|
def getPreviewImage(self) -> Optional[QImage]:
|
||||||
|
return self._preview_image
|
||||||
|
|
||||||
|
def updatePreviewImage(self, preview_image: Optional[QImage]) -> None:
|
||||||
|
if self._preview_image != preview_image:
|
||||||
|
self._preview_image = preview_image
|
||||||
|
self.previewImageChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, notify=configurationChanged)
|
||||||
|
def configuration(self) -> Optional["PrinterConfigurationModel"]:
|
||||||
|
return self._configuration
|
||||||
|
|
||||||
|
def updateConfiguration(self, configuration: Optional["PrinterConfigurationModel"]) -> None:
|
||||||
|
if self._configuration != configuration:
|
||||||
|
self._configuration = configuration
|
||||||
|
self.configurationChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=ownerChanged)
|
||||||
|
def owner(self):
|
||||||
|
return self._owner
|
||||||
|
|
||||||
|
def updateOwner(self, owner):
|
||||||
|
if self._owner != owner:
|
||||||
|
self._owner = owner
|
||||||
|
self.ownerChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, notify=assignedPrinterChanged)
|
||||||
|
def assignedPrinter(self):
|
||||||
|
return self._assigned_printer
|
||||||
|
|
||||||
|
def updateAssignedPrinter(self, assigned_printer: Optional["PrinterOutputModel"]) -> None:
|
||||||
|
if self._assigned_printer != assigned_printer:
|
||||||
|
old_printer = self._assigned_printer
|
||||||
|
self._assigned_printer = assigned_printer
|
||||||
|
if old_printer is not None:
|
||||||
|
# If the previously assigned printer is set, this job is moved away from it.
|
||||||
|
old_printer.updateActivePrintJob(None)
|
||||||
|
self.assignedPrinterChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=keyChanged)
|
||||||
|
def key(self):
|
||||||
|
return self._key
|
||||||
|
|
||||||
|
def updateKey(self, key: str):
|
||||||
|
if self._key != key:
|
||||||
|
self._key = key
|
||||||
|
self.keyChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = nameChanged)
|
||||||
|
def name(self):
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
def updateName(self, name: str):
|
||||||
|
if self._name != name:
|
||||||
|
self._name = name
|
||||||
|
self.nameChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = timeTotalChanged)
|
||||||
|
def timeTotal(self) -> int:
|
||||||
|
return self._time_total
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||||
|
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) -> 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
|
||||||
|
self.timeTotalChanged.emit()
|
||||||
|
|
||||||
|
def updateTimeElapsed(self, new_time_elapsed):
|
||||||
|
if self._time_elapsed != new_time_elapsed:
|
||||||
|
self._time_elapsed = new_time_elapsed
|
||||||
|
self.timeElapsedChanged.emit()
|
||||||
|
|
||||||
|
def updateState(self, new_state):
|
||||||
|
if self._state != new_state:
|
||||||
|
self._state = new_state
|
||||||
|
self.stateChanged.emit()
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def setState(self, state):
|
||||||
|
self._output_controller.setJobState(self, state)
|
|
@ -6,10 +6,10 @@ from typing import List
|
||||||
|
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||||
|
|
||||||
|
|
||||||
class ConfigurationModel(QObject):
|
class PrinterConfigurationModel(QObject):
|
||||||
|
|
||||||
configurationChanged = pyqtSignal()
|
configurationChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@ -19,14 +19,14 @@ class ConfigurationModel(QObject):
|
||||||
self._extruder_configurations = [] # type: List[ExtruderConfigurationModel]
|
self._extruder_configurations = [] # type: List[ExtruderConfigurationModel]
|
||||||
self._buildplate_configuration = ""
|
self._buildplate_configuration = ""
|
||||||
|
|
||||||
def setPrinterType(self, printer_type):
|
def setPrinterType(self, printer_type: str) -> None:
|
||||||
self._printer_type = printer_type
|
self._printer_type = printer_type
|
||||||
|
|
||||||
@pyqtProperty(str, fset = setPrinterType, notify = configurationChanged)
|
@pyqtProperty(str, fset = setPrinterType, notify = configurationChanged)
|
||||||
def printerType(self) -> str:
|
def printerType(self) -> str:
|
||||||
return self._printer_type
|
return self._printer_type
|
||||||
|
|
||||||
def setExtruderConfigurations(self, extruder_configurations: List["ExtruderConfigurationModel"]):
|
def setExtruderConfigurations(self, extruder_configurations: List["ExtruderConfigurationModel"]) -> None:
|
||||||
if self._extruder_configurations != extruder_configurations:
|
if self._extruder_configurations != extruder_configurations:
|
||||||
self._extruder_configurations = extruder_configurations
|
self._extruder_configurations = extruder_configurations
|
||||||
|
|
||||||
|
@ -71,7 +71,23 @@ class ConfigurationModel(QObject):
|
||||||
return "\n".join(message_chunks)
|
return "\n".join(message_chunks)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return hash(self) == hash(other)
|
if not isinstance(other, PrinterConfigurationModel):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.printerType != other.printerType:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.buildplateConfiguration != other.buildplateConfiguration:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if len(self.extruderConfigurations) != len(other.extruderConfigurations):
|
||||||
|
return False
|
||||||
|
|
||||||
|
for self_extruder, other_extruder in zip(sorted(self._extruder_configurations, key=lambda x: x.position), sorted(other.extruderConfigurations, key=lambda x: x.position)):
|
||||||
|
if self_extruder != other_extruder:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
## The hash function is used to compare and create unique sets. The configuration is unique if the configuration
|
## The hash function is used to compare and create unique sets. The configuration is unique if the configuration
|
||||||
# of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same.
|
# of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same.
|
297
cura/PrinterOutput/Models/PrinterOutputModel.py
Normal file
297
cura/PrinterOutput/Models/PrinterOutputModel.py
Normal file
|
@ -0,0 +1,297 @@
|
||||||
|
# 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, QUrl
|
||||||
|
from typing import List, Dict, Optional
|
||||||
|
from UM.Math.Vector import Vector
|
||||||
|
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||||
|
from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel
|
||||||
|
|
||||||
|
MYPY = False
|
||||||
|
if MYPY:
|
||||||
|
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
||||||
|
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||||
|
|
||||||
|
|
||||||
|
class PrinterOutputModel(QObject):
|
||||||
|
bedTemperatureChanged = pyqtSignal()
|
||||||
|
targetBedTemperatureChanged = pyqtSignal()
|
||||||
|
isPreheatingChanged = pyqtSignal()
|
||||||
|
stateChanged = pyqtSignal()
|
||||||
|
activePrintJobChanged = pyqtSignal()
|
||||||
|
nameChanged = pyqtSignal()
|
||||||
|
headPositionChanged = pyqtSignal()
|
||||||
|
keyChanged = pyqtSignal()
|
||||||
|
typeChanged = pyqtSignal()
|
||||||
|
buildplateChanged = 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 # 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 = 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 = ""
|
||||||
|
|
||||||
|
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
|
||||||
|
self._extruders]
|
||||||
|
|
||||||
|
self._camera_url = QUrl() # type: QUrl
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def firmwareVersion(self) -> str:
|
||||||
|
return self._firmware_version
|
||||||
|
|
||||||
|
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:
|
||||||
|
self._is_preheating = pre_heating
|
||||||
|
self.isPreheatingChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=isPreheatingChanged)
|
||||||
|
def isPreheating(self) -> bool:
|
||||||
|
return self._is_preheating
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = typeChanged)
|
||||||
|
def type(self) -> str:
|
||||||
|
return self._printer_type
|
||||||
|
|
||||||
|
def updateType(self, printer_type: str) -> None:
|
||||||
|
if self._printer_type != printer_type:
|
||||||
|
self._printer_type = printer_type
|
||||||
|
self._printer_configuration.printerType = self._printer_type
|
||||||
|
self.typeChanged.emit()
|
||||||
|
self.configurationChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = buildplateChanged)
|
||||||
|
def buildplate(self) -> str:
|
||||||
|
return self._buildplate
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify=keyChanged)
|
||||||
|
def key(self) -> str:
|
||||||
|
return self._key
|
||||||
|
|
||||||
|
def updateKey(self, key: str) -> None:
|
||||||
|
if self._key != key:
|
||||||
|
self._key = key
|
||||||
|
self.keyChanged.emit()
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def homeHead(self) -> None:
|
||||||
|
self._controller.homeHead(self)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def homeBed(self) -> None:
|
||||||
|
self._controller.homeBed(self)
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def sendRawCommand(self, command: str) -> None:
|
||||||
|
self._controller.sendRawCommand(self, command)
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", constant = True)
|
||||||
|
def extruders(self) -> List["ExtruderOutputModel"]:
|
||||||
|
return self._extruders
|
||||||
|
|
||||||
|
@pyqtProperty(QVariant, notify = headPositionChanged)
|
||||||
|
def headPosition(self) -> Dict[str, float]:
|
||||||
|
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position.z}
|
||||||
|
|
||||||
|
def updateHeadPosition(self, x: float, y: float, z: float) -> None:
|
||||||
|
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:
|
||||||
|
self._head_position = Vector(x, y, z)
|
||||||
|
self.headPositionChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(float, float, float)
|
||||||
|
@pyqtProperty(float, float, float, float)
|
||||||
|
def setHeadPosition(self, x: float, y: float, z: float, speed: float = 3000) -> None:
|
||||||
|
self.updateHeadPosition(x, y, z)
|
||||||
|
self._controller.setHeadPosition(self, x, y, z, speed)
|
||||||
|
|
||||||
|
@pyqtProperty(float)
|
||||||
|
@pyqtProperty(float, float)
|
||||||
|
def setHeadX(self, x: float, speed: float = 3000) -> None:
|
||||||
|
self.updateHeadPosition(x, self._head_position.y, self._head_position.z)
|
||||||
|
self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed)
|
||||||
|
|
||||||
|
@pyqtProperty(float)
|
||||||
|
@pyqtProperty(float, float)
|
||||||
|
def setHeadY(self, y: float, speed: float = 3000) -> None:
|
||||||
|
self.updateHeadPosition(self._head_position.x, y, self._head_position.z)
|
||||||
|
self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed)
|
||||||
|
|
||||||
|
@pyqtProperty(float)
|
||||||
|
@pyqtProperty(float, float)
|
||||||
|
def setHeadZ(self, z: float, speed:float = 3000) -> None:
|
||||||
|
self.updateHeadPosition(self._head_position.x, self._head_position.y, z)
|
||||||
|
self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed)
|
||||||
|
|
||||||
|
@pyqtSlot(float, float, float)
|
||||||
|
@pyqtSlot(float, float, float, float)
|
||||||
|
def moveHead(self, x: float = 0, y: float = 0, z: float = 0, speed: float = 3000) -> None:
|
||||||
|
self._controller.moveHead(self, x, y, z, speed)
|
||||||
|
|
||||||
|
## Pre-heats the heated bed of the printer.
|
||||||
|
#
|
||||||
|
# \param temperature The temperature to heat the bed to, in degrees
|
||||||
|
# Celsius.
|
||||||
|
# \param duration How long the bed should stay warm, in seconds.
|
||||||
|
@pyqtSlot(float, float)
|
||||||
|
def preheatBed(self, temperature: float, duration: float) -> None:
|
||||||
|
self._controller.preheatBed(self, temperature, duration)
|
||||||
|
|
||||||
|
@pyqtSlot()
|
||||||
|
def cancelPreheatBed(self) -> None:
|
||||||
|
self._controller.cancelPreheatBed(self)
|
||||||
|
|
||||||
|
def getController(self) -> "PrinterOutputController":
|
||||||
|
return self._controller
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = nameChanged)
|
||||||
|
def name(self) -> str:
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
def setName(self, name: str) -> None:
|
||||||
|
self.updateName(name)
|
||||||
|
|
||||||
|
def updateName(self, name: str) -> None:
|
||||||
|
if self._name != name:
|
||||||
|
self._name = name
|
||||||
|
self.nameChanged.emit()
|
||||||
|
|
||||||
|
## Update the bed temperature. This only changes it locally.
|
||||||
|
def updateBedTemperature(self, temperature: float) -> None:
|
||||||
|
if self._bed_temperature != temperature:
|
||||||
|
self._bed_temperature = temperature
|
||||||
|
self.bedTemperatureChanged.emit()
|
||||||
|
|
||||||
|
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(float)
|
||||||
|
def setTargetBedTemperature(self, temperature: float) -> None:
|
||||||
|
self._controller.setTargetBedTemperature(self, temperature)
|
||||||
|
self.updateTargetBedTemperature(temperature)
|
||||||
|
|
||||||
|
def updateActivePrintJob(self, print_job: Optional["PrintJobOutputModel"]) -> None:
|
||||||
|
if self._active_print_job != print_job:
|
||||||
|
old_print_job = self._active_print_job
|
||||||
|
|
||||||
|
if print_job is not None:
|
||||||
|
print_job.updateAssignedPrinter(self)
|
||||||
|
self._active_print_job = print_job
|
||||||
|
|
||||||
|
if old_print_job is not None:
|
||||||
|
old_print_job.updateAssignedPrinter(None)
|
||||||
|
self.activePrintJobChanged.emit()
|
||||||
|
|
||||||
|
def updateState(self, printer_state: str) -> None:
|
||||||
|
if self._printer_state != printer_state:
|
||||||
|
self._printer_state = printer_state
|
||||||
|
self.stateChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, notify = activePrintJobChanged)
|
||||||
|
def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
|
||||||
|
return self._active_print_job
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = stateChanged)
|
||||||
|
def state(self) -> str:
|
||||||
|
return self._printer_state
|
||||||
|
|
||||||
|
@pyqtProperty(float, notify = bedTemperatureChanged)
|
||||||
|
def bedTemperature(self) -> float:
|
||||||
|
return self._bed_temperature
|
||||||
|
|
||||||
|
@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)
|
||||||
|
def canPreHeatBed(self) -> bool:
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_pre_heat_bed
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the printer support pre-heating the bed at all
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def canPreHeatHotends(self) -> bool:
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_pre_heat_hotends
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the printer support sending raw G-code at all
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def canSendRawGcode(self) -> bool:
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_send_raw_gcode
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the printer support pause at all
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def canPause(self) -> bool:
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_pause
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the printer support abort at all
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def canAbort(self) -> bool:
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_abort
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the printer support manual control at all
|
||||||
|
@pyqtProperty(bool, constant = True)
|
||||||
|
def canControlManually(self) -> bool:
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_control_manually
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Does the printer support upgrading firmware
|
||||||
|
@pyqtProperty(bool, notify = canUpdateFirmwareChanged)
|
||||||
|
def canUpdateFirmware(self) -> bool:
|
||||||
|
if self._controller:
|
||||||
|
return self._controller.can_update_firmware
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Stub to connect UM.Signal to pyqtSignal
|
||||||
|
def _onControllerCanUpdateFirmwareChanged(self) -> None:
|
||||||
|
self.canUpdateFirmwareChanged.emit()
|
||||||
|
|
||||||
|
# Returns the configuration (material, variant and buildplate) of the current printer
|
||||||
|
@pyqtProperty(QObject, notify = configurationChanged)
|
||||||
|
def printerConfiguration(self) -> 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
|
@ -7,7 +7,7 @@ from UM.Scene.SceneNode import SceneNode #For typing.
|
||||||
from cura.API import Account
|
from cura.API import Account
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType
|
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType
|
||||||
|
|
||||||
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator
|
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
|
||||||
|
@ -18,6 +18,8 @@ from enum import IntEnum
|
||||||
import os # To get the username
|
import os # To get the username
|
||||||
import gzip
|
import gzip
|
||||||
|
|
||||||
|
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||||
|
|
||||||
|
|
||||||
class AuthState(IntEnum):
|
class AuthState(IntEnum):
|
||||||
NotAuthenticated = 1
|
NotAuthenticated = 1
|
||||||
|
@ -319,12 +321,27 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
|
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
|
||||||
|
|
||||||
if self._properties.get(b"temporary", b"false") != b"true":
|
if self._properties.get(b"temporary", b"false") != b"true":
|
||||||
CuraApplication.getInstance().getMachineManager().checkCorrectGroupName(self.getId(), self.name)
|
self._checkCorrectGroupName(self.getId(), self.name)
|
||||||
|
|
||||||
def _registerOnFinishedCallback(self, reply: QNetworkReply, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
def _registerOnFinishedCallback(self, reply: QNetworkReply, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||||
if on_finished is not None:
|
if on_finished is not None:
|
||||||
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished
|
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished
|
||||||
|
|
||||||
|
## 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:
|
def _handleOnFinished(self, reply: QNetworkReply) -> None:
|
||||||
# Due to garbage collection, we need to cache certain bits of post operations.
|
# Due to garbage collection, we need to cache certain bits of post operations.
|
||||||
# As we don't want to keep them around forever, delete them if we get a reply.
|
# As we don't want to keep them around forever, delete them if we get a reply.
|
||||||
|
|
|
@ -1,172 +1,4 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
import warnings
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
warnings.warn("Importing cura.PrinterOutput.PrintJobOutputModel has been deprecated since 4.1, use cura.PrinterOutput.Models.PrintJobOutputModel inststad", DeprecationWarning, stacklevel=2)
|
||||||
|
# We moved the the models to one submodule deeper
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
||||||
from typing import Optional, TYPE_CHECKING, List
|
|
||||||
|
|
||||||
from PyQt5.QtCore import 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
|
|
||||||
|
|
||||||
|
|
||||||
class PrintJobOutputModel(QObject):
|
|
||||||
stateChanged = pyqtSignal()
|
|
||||||
timeTotalChanged = pyqtSignal()
|
|
||||||
timeElapsedChanged = pyqtSignal()
|
|
||||||
nameChanged = pyqtSignal()
|
|
||||||
keyChanged = pyqtSignal()
|
|
||||||
assignedPrinterChanged = pyqtSignal()
|
|
||||||
ownerChanged = pyqtSignal()
|
|
||||||
configurationChanged = pyqtSignal()
|
|
||||||
previewImageChanged = pyqtSignal()
|
|
||||||
compatibleMachineFamiliesChanged = pyqtSignal()
|
|
||||||
|
|
||||||
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent=None) -> None:
|
|
||||||
super().__init__(parent)
|
|
||||||
self._output_controller = output_controller
|
|
||||||
self._state = ""
|
|
||||||
self._time_total = 0
|
|
||||||
self._time_elapsed = 0
|
|
||||||
self._name = name # Human readable name
|
|
||||||
self._key = key # Unique identifier
|
|
||||||
self._assigned_printer = None # type: Optional[PrinterOutputModel]
|
|
||||||
self._owner = "" # Who started/owns the print job?
|
|
||||||
|
|
||||||
self._configuration = None # type: Optional[ConfigurationModel]
|
|
||||||
self._compatible_machine_families = [] # type: List[str]
|
|
||||||
self._preview_image_id = 0
|
|
||||||
|
|
||||||
self._preview_image = None # type: Optional[QImage]
|
|
||||||
|
|
||||||
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
|
|
||||||
def compatibleMachineFamilies(self):
|
|
||||||
# Hack; Some versions of cluster will return a family more than once...
|
|
||||||
return list(set(self._compatible_machine_families))
|
|
||||||
|
|
||||||
def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None:
|
|
||||||
if self._compatible_machine_families != compatible_machine_families:
|
|
||||||
self._compatible_machine_families = compatible_machine_families
|
|
||||||
self.compatibleMachineFamiliesChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(QUrl, notify=previewImageChanged)
|
|
||||||
def previewImageUrl(self):
|
|
||||||
self._preview_image_id += 1
|
|
||||||
# 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
|
|
||||||
return QUrl(temp, QUrl.TolerantMode)
|
|
||||||
|
|
||||||
def getPreviewImage(self) -> Optional[QImage]:
|
|
||||||
return self._preview_image
|
|
||||||
|
|
||||||
def updatePreviewImage(self, preview_image: Optional[QImage]) -> None:
|
|
||||||
if self._preview_image != preview_image:
|
|
||||||
self._preview_image = preview_image
|
|
||||||
self.previewImageChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify=configurationChanged)
|
|
||||||
def configuration(self) -> Optional["ConfigurationModel"]:
|
|
||||||
return self._configuration
|
|
||||||
|
|
||||||
def updateConfiguration(self, configuration: Optional["ConfigurationModel"]) -> None:
|
|
||||||
if self._configuration != configuration:
|
|
||||||
self._configuration = configuration
|
|
||||||
self.configurationChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify=ownerChanged)
|
|
||||||
def owner(self):
|
|
||||||
return self._owner
|
|
||||||
|
|
||||||
def updateOwner(self, owner):
|
|
||||||
if self._owner != owner:
|
|
||||||
self._owner = owner
|
|
||||||
self.ownerChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify=assignedPrinterChanged)
|
|
||||||
def assignedPrinter(self):
|
|
||||||
return self._assigned_printer
|
|
||||||
|
|
||||||
def updateAssignedPrinter(self, assigned_printer: Optional["PrinterOutputModel"]) -> None:
|
|
||||||
if self._assigned_printer != assigned_printer:
|
|
||||||
old_printer = self._assigned_printer
|
|
||||||
self._assigned_printer = assigned_printer
|
|
||||||
if old_printer is not None:
|
|
||||||
# If the previously assigned printer is set, this job is moved away from it.
|
|
||||||
old_printer.updateActivePrintJob(None)
|
|
||||||
self.assignedPrinterChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify=keyChanged)
|
|
||||||
def key(self):
|
|
||||||
return self._key
|
|
||||||
|
|
||||||
def updateKey(self, key: str):
|
|
||||||
if self._key != key:
|
|
||||||
self._key = key
|
|
||||||
self.keyChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = nameChanged)
|
|
||||||
def name(self):
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
def updateName(self, name: str):
|
|
||||||
if self._name != name:
|
|
||||||
self._name = name
|
|
||||||
self.nameChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(int, notify = timeTotalChanged)
|
|
||||||
def timeTotal(self) -> int:
|
|
||||||
return self._time_total
|
|
||||||
|
|
||||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
|
||||||
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) -> str:
|
|
||||||
return self._state
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=stateChanged)
|
|
||||||
def isActive(self) -> bool:
|
|
||||||
inactiveStates = [
|
|
||||||
"pausing",
|
|
||||||
"paused",
|
|
||||||
"resuming",
|
|
||||||
"wait_cleanup"
|
|
||||||
]
|
|
||||||
if self.state in inactiveStates 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
|
|
||||||
self.timeTotalChanged.emit()
|
|
||||||
|
|
||||||
def updateTimeElapsed(self, new_time_elapsed):
|
|
||||||
if self._time_elapsed != new_time_elapsed:
|
|
||||||
self._time_elapsed = new_time_elapsed
|
|
||||||
self.timeElapsedChanged.emit()
|
|
||||||
|
|
||||||
def updateState(self, new_state):
|
|
||||||
if self._state != new_state:
|
|
||||||
self._state = new_state
|
|
||||||
self.stateChanged.emit()
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def setState(self, state):
|
|
||||||
self._output_controller.setJobState(self, state)
|
|
|
@ -4,14 +4,12 @@
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Signal import Signal
|
from UM.Signal import Signal
|
||||||
|
|
||||||
from typing import Union
|
|
||||||
|
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
from .Models.PrintJobOutputModel import PrintJobOutputModel
|
||||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
from .Models.ExtruderOutputModel import ExtruderOutputModel
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from .Models.PrinterOutputModel import PrinterOutputModel
|
||||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
from .PrinterOutputDevice import PrinterOutputDevice
|
||||||
|
|
||||||
|
|
||||||
class PrinterOutputController:
|
class PrinterOutputController:
|
||||||
|
|
261
cura/PrinterOutput/PrinterOutputDevice.py
Normal file
261
cura/PrinterOutput/PrinterOutputDevice.py
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
# 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 PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
|
||||||
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Signal import signalemitter
|
||||||
|
from UM.Qt.QtApplication import QtApplication
|
||||||
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
from UM.Decorators import deprecated
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.OutputDevice.OutputDevice import OutputDevice
|
||||||
|
|
||||||
|
MYPY = False
|
||||||
|
if MYPY:
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class ConnectionType(IntEnum):
|
||||||
|
NotConnected = 0
|
||||||
|
UsbConnection = 1
|
||||||
|
NetworkConnection = 2
|
||||||
|
CloudConnection = 3
|
||||||
|
|
||||||
|
|
||||||
|
## Printer output device adds extra interface options on top of output device.
|
||||||
|
#
|
||||||
|
# The assumption is made the printer is a FDM printer.
|
||||||
|
#
|
||||||
|
# Note that a number of settings are marked as "final". This is because decorators
|
||||||
|
# are not inherited by children. To fix this we use the private counter part of those
|
||||||
|
# functions to actually have the implementation.
|
||||||
|
#
|
||||||
|
# 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()
|
||||||
|
|
||||||
|
# Signal to indicate that the material of the active printer on the remote changed.
|
||||||
|
materialIdChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# # Signal to indicate that the hotend of the active printer on the remote changed.
|
||||||
|
hotendIdChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# Signal to indicate that the info text about the connection has changed.
|
||||||
|
connectionTextChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# Signal to indicate that the configuration of one of the printers has changed.
|
||||||
|
uniqueConfigurationsChanged = pyqtSignal()
|
||||||
|
|
||||||
|
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[PrinterConfigurationModel]
|
||||||
|
|
||||||
|
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._accepts_commands = False # type: bool
|
||||||
|
|
||||||
|
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_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.printersChanged.connect(self._onPrintersChanged)
|
||||||
|
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
||||||
|
|
||||||
|
@pyqtProperty(str, notify = connectionTextChanged)
|
||||||
|
def address(self) -> str:
|
||||||
|
return self._address
|
||||||
|
|
||||||
|
def setConnectionText(self, connection_text):
|
||||||
|
if self._connection_text != connection_text:
|
||||||
|
self._connection_text = connection_text
|
||||||
|
self.connectionTextChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant=True)
|
||||||
|
def connectionText(self) -> str:
|
||||||
|
return self._connection_text
|
||||||
|
|
||||||
|
def materialHotendChangedMessage(self, callback: Callable[[int], None]) -> None:
|
||||||
|
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
|
||||||
|
callback(QMessageBox.Yes)
|
||||||
|
|
||||||
|
def isConnected(self) -> bool:
|
||||||
|
return self._connection_state != ConnectionState.Closed and self._connection_state != ConnectionState.Error
|
||||||
|
|
||||||
|
def setConnectionState(self, connection_state: "ConnectionState") -> None:
|
||||||
|
if self._connection_state != connection_state:
|
||||||
|
self._connection_state = connection_state
|
||||||
|
self.connectionStateChanged.emit(self._id)
|
||||||
|
|
||||||
|
@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:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _getPrinterByKey(self, key: str) -> Optional["PrinterOutputModel"]:
|
||||||
|
for printer in self._printers:
|
||||||
|
if printer.key == key:
|
||||||
|
return printer
|
||||||
|
|
||||||
|
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:
|
||||||
|
raise NotImplementedError("requestWrite needs to be implemented")
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, notify = printersChanged)
|
||||||
|
def activePrinter(self) -> Optional["PrinterOutputModel"]:
|
||||||
|
if len(self._printers):
|
||||||
|
return self._printers[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify = printersChanged)
|
||||||
|
def printers(self) -> List["PrinterOutputModel"]:
|
||||||
|
return self._printers
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, constant = True)
|
||||||
|
def monitorItem(self) -> QObject:
|
||||||
|
# Note that we specifically only check if the monitor component is created.
|
||||||
|
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
|
||||||
|
# create the item (and fail) every time.
|
||||||
|
if not self._monitor_component:
|
||||||
|
self._createMonitorViewFromQML()
|
||||||
|
return self._monitor_item
|
||||||
|
|
||||||
|
@pyqtProperty(QObject, constant = True)
|
||||||
|
def controlItem(self) -> QObject:
|
||||||
|
if not self._control_component:
|
||||||
|
self._createControlViewFromQML()
|
||||||
|
return self._control_item
|
||||||
|
|
||||||
|
def _createControlViewFromQML(self) -> None:
|
||||||
|
if not self._control_view_qml_path:
|
||||||
|
return
|
||||||
|
if self._control_item is None:
|
||||||
|
self._control_item = QtApplication.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
|
||||||
|
|
||||||
|
def _createMonitorViewFromQML(self) -> None:
|
||||||
|
if not self._monitor_view_qml_path:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._monitor_item is None:
|
||||||
|
self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
|
||||||
|
|
||||||
|
## Attempt to establish connection
|
||||||
|
def connect(self) -> None:
|
||||||
|
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)
|
||||||
|
|
||||||
|
## Ensure that close gets called when object is destroyed
|
||||||
|
def __del__(self) -> None:
|
||||||
|
self.close()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = acceptsCommandsChanged)
|
||||||
|
def acceptsCommands(self) -> bool:
|
||||||
|
return self._accepts_commands
|
||||||
|
|
||||||
|
@deprecated("Please use the protected function instead", "3.2")
|
||||||
|
def setAcceptsCommands(self, accepts_commands: bool) -> None:
|
||||||
|
self._setAcceptsCommands(accepts_commands)
|
||||||
|
|
||||||
|
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
|
||||||
|
def _setAcceptsCommands(self, accepts_commands: bool) -> None:
|
||||||
|
if self._accepts_commands != accepts_commands:
|
||||||
|
self._accepts_commands = accepts_commands
|
||||||
|
|
||||||
|
self.acceptsCommandsChanged.emit()
|
||||||
|
|
||||||
|
# Returns the unique configurations of the printers within this output device
|
||||||
|
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
|
||||||
|
def uniqueConfigurations(self) -> List["PrinterConfigurationModel"]:
|
||||||
|
return self._unique_configurations
|
||||||
|
|
||||||
|
def _updateUniqueConfigurations(self) -> None:
|
||||||
|
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)
|
||||||
|
|
||||||
|
# At this point there may be non-updated configurations
|
||||||
|
self._updateUniqueConfigurations()
|
||||||
|
|
||||||
|
## Set the device firmware name
|
||||||
|
#
|
||||||
|
# \param name The name of the firmware.
|
||||||
|
def _setFirmwareName(self, name: str) -> None:
|
||||||
|
self._firmware_name = name
|
||||||
|
|
||||||
|
## Get the name of device firmware
|
||||||
|
#
|
||||||
|
# This name can be used to define device type
|
||||||
|
def getFirmwareName(self) -> Optional[str]:
|
||||||
|
return self._firmware_name
|
||||||
|
|
||||||
|
def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]:
|
||||||
|
return self._firmware_updater
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None:
|
||||||
|
if not self._firmware_updater:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._firmware_updater.updateFirmware(firmware_file)
|
|
@ -1,297 +1,4 @@
|
||||||
# Copyright (c) 2019 Ultimaker B.V.
|
import warnings
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
warnings.warn("Importing cura.PrinterOutput.PrinterOutputModel has been deprecated since 4.1, use cura.PrinterOutput.Models.PrinterOutputModel inststad", DeprecationWarning, stacklevel=2)
|
||||||
|
# We moved the the models to one submodule deeper
|
||||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot, QUrl
|
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
||||||
from typing import List, Dict, Optional
|
|
||||||
from UM.Math.Vector import Vector
|
|
||||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
|
||||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
|
||||||
|
|
||||||
MYPY = False
|
|
||||||
if MYPY:
|
|
||||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
|
||||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
|
||||||
|
|
||||||
|
|
||||||
class PrinterOutputModel(QObject):
|
|
||||||
bedTemperatureChanged = pyqtSignal()
|
|
||||||
targetBedTemperatureChanged = pyqtSignal()
|
|
||||||
isPreheatingChanged = pyqtSignal()
|
|
||||||
stateChanged = pyqtSignal()
|
|
||||||
activePrintJobChanged = pyqtSignal()
|
|
||||||
nameChanged = pyqtSignal()
|
|
||||||
headPositionChanged = pyqtSignal()
|
|
||||||
keyChanged = pyqtSignal()
|
|
||||||
typeChanged = pyqtSignal()
|
|
||||||
buildplateChanged = 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 # 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._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 = ""
|
|
||||||
|
|
||||||
self._printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
|
|
||||||
self._extruders]
|
|
||||||
|
|
||||||
self._camera_url = QUrl() # type: QUrl
|
|
||||||
|
|
||||||
@pyqtProperty(str, constant = True)
|
|
||||||
def firmwareVersion(self) -> str:
|
|
||||||
return self._firmware_version
|
|
||||||
|
|
||||||
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:
|
|
||||||
self._is_preheating = pre_heating
|
|
||||||
self.isPreheatingChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=isPreheatingChanged)
|
|
||||||
def isPreheating(self) -> bool:
|
|
||||||
return self._is_preheating
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = typeChanged)
|
|
||||||
def type(self) -> str:
|
|
||||||
return self._printer_type
|
|
||||||
|
|
||||||
def updateType(self, printer_type: str) -> None:
|
|
||||||
if self._printer_type != printer_type:
|
|
||||||
self._printer_type = printer_type
|
|
||||||
self._printer_configuration.printerType = self._printer_type
|
|
||||||
self.typeChanged.emit()
|
|
||||||
self.configurationChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = buildplateChanged)
|
|
||||||
def buildplate(self) -> str:
|
|
||||||
return self._buildplate
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify=keyChanged)
|
|
||||||
def key(self) -> str:
|
|
||||||
return self._key
|
|
||||||
|
|
||||||
def updateKey(self, key: str) -> None:
|
|
||||||
if self._key != key:
|
|
||||||
self._key = key
|
|
||||||
self.keyChanged.emit()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def homeHead(self) -> None:
|
|
||||||
self._controller.homeHead(self)
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def homeBed(self) -> None:
|
|
||||||
self._controller.homeBed(self)
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def sendRawCommand(self, command: str) -> None:
|
|
||||||
self._controller.sendRawCommand(self, command)
|
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", constant = True)
|
|
||||||
def extruders(self) -> List["ExtruderOutputModel"]:
|
|
||||||
return self._extruders
|
|
||||||
|
|
||||||
@pyqtProperty(QVariant, notify = headPositionChanged)
|
|
||||||
def headPosition(self) -> Dict[str, float]:
|
|
||||||
return {"x": self._head_position.x, "y": self._head_position.y, "z": self.head_position.z}
|
|
||||||
|
|
||||||
def updateHeadPosition(self, x: float, y: float, z: float) -> None:
|
|
||||||
if self._head_position.x != x or self._head_position.y != y or self._head_position.z != z:
|
|
||||||
self._head_position = Vector(x, y, z)
|
|
||||||
self.headPositionChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(float, float, float)
|
|
||||||
@pyqtProperty(float, float, float, float)
|
|
||||||
def setHeadPosition(self, x: float, y: float, z: float, speed: float = 3000) -> None:
|
|
||||||
self.updateHeadPosition(x, y, z)
|
|
||||||
self._controller.setHeadPosition(self, x, y, z, speed)
|
|
||||||
|
|
||||||
@pyqtProperty(float)
|
|
||||||
@pyqtProperty(float, float)
|
|
||||||
def setHeadX(self, x: float, speed: float = 3000) -> None:
|
|
||||||
self.updateHeadPosition(x, self._head_position.y, self._head_position.z)
|
|
||||||
self._controller.setHeadPosition(self, x, self._head_position.y, self._head_position.z, speed)
|
|
||||||
|
|
||||||
@pyqtProperty(float)
|
|
||||||
@pyqtProperty(float, float)
|
|
||||||
def setHeadY(self, y: float, speed: float = 3000) -> None:
|
|
||||||
self.updateHeadPosition(self._head_position.x, y, self._head_position.z)
|
|
||||||
self._controller.setHeadPosition(self, self._head_position.x, y, self._head_position.z, speed)
|
|
||||||
|
|
||||||
@pyqtProperty(float)
|
|
||||||
@pyqtProperty(float, float)
|
|
||||||
def setHeadZ(self, z: float, speed:float = 3000) -> None:
|
|
||||||
self.updateHeadPosition(self._head_position.x, self._head_position.y, z)
|
|
||||||
self._controller.setHeadPosition(self, self._head_position.x, self._head_position.y, z, speed)
|
|
||||||
|
|
||||||
@pyqtSlot(float, float, float)
|
|
||||||
@pyqtSlot(float, float, float, float)
|
|
||||||
def moveHead(self, x: float = 0, y: float = 0, z: float = 0, speed: float = 3000) -> None:
|
|
||||||
self._controller.moveHead(self, x, y, z, speed)
|
|
||||||
|
|
||||||
## Pre-heats the heated bed of the printer.
|
|
||||||
#
|
|
||||||
# \param temperature The temperature to heat the bed to, in degrees
|
|
||||||
# Celsius.
|
|
||||||
# \param duration How long the bed should stay warm, in seconds.
|
|
||||||
@pyqtSlot(float, float)
|
|
||||||
def preheatBed(self, temperature: float, duration: float) -> None:
|
|
||||||
self._controller.preheatBed(self, temperature, duration)
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def cancelPreheatBed(self) -> None:
|
|
||||||
self._controller.cancelPreheatBed(self)
|
|
||||||
|
|
||||||
def getController(self) -> "PrinterOutputController":
|
|
||||||
return self._controller
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = nameChanged)
|
|
||||||
def name(self) -> str:
|
|
||||||
return self._name
|
|
||||||
|
|
||||||
def setName(self, name: str) -> None:
|
|
||||||
self.updateName(name)
|
|
||||||
|
|
||||||
def updateName(self, name: str) -> None:
|
|
||||||
if self._name != name:
|
|
||||||
self._name = name
|
|
||||||
self.nameChanged.emit()
|
|
||||||
|
|
||||||
## Update the bed temperature. This only changes it locally.
|
|
||||||
def updateBedTemperature(self, temperature: float) -> None:
|
|
||||||
if self._bed_temperature != temperature:
|
|
||||||
self._bed_temperature = temperature
|
|
||||||
self.bedTemperatureChanged.emit()
|
|
||||||
|
|
||||||
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(float)
|
|
||||||
def setTargetBedTemperature(self, temperature: float) -> None:
|
|
||||||
self._controller.setTargetBedTemperature(self, temperature)
|
|
||||||
self.updateTargetBedTemperature(temperature)
|
|
||||||
|
|
||||||
def updateActivePrintJob(self, print_job: Optional["PrintJobOutputModel"]) -> None:
|
|
||||||
if self._active_print_job != print_job:
|
|
||||||
old_print_job = self._active_print_job
|
|
||||||
|
|
||||||
if print_job is not None:
|
|
||||||
print_job.updateAssignedPrinter(self)
|
|
||||||
self._active_print_job = print_job
|
|
||||||
|
|
||||||
if old_print_job is not None:
|
|
||||||
old_print_job.updateAssignedPrinter(None)
|
|
||||||
self.activePrintJobChanged.emit()
|
|
||||||
|
|
||||||
def updateState(self, printer_state: str) -> None:
|
|
||||||
if self._printer_state != printer_state:
|
|
||||||
self._printer_state = printer_state
|
|
||||||
self.stateChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = activePrintJobChanged)
|
|
||||||
def activePrintJob(self) -> Optional["PrintJobOutputModel"]:
|
|
||||||
return self._active_print_job
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = stateChanged)
|
|
||||||
def state(self) -> str:
|
|
||||||
return self._printer_state
|
|
||||||
|
|
||||||
@pyqtProperty(float, notify = bedTemperatureChanged)
|
|
||||||
def bedTemperature(self) -> float:
|
|
||||||
return self._bed_temperature
|
|
||||||
|
|
||||||
@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)
|
|
||||||
def canPreHeatBed(self) -> bool:
|
|
||||||
if self._controller:
|
|
||||||
return self._controller.can_pre_heat_bed
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Does the printer support pre-heating the bed at all
|
|
||||||
@pyqtProperty(bool, constant = True)
|
|
||||||
def canPreHeatHotends(self) -> bool:
|
|
||||||
if self._controller:
|
|
||||||
return self._controller.can_pre_heat_hotends
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Does the printer support sending raw G-code at all
|
|
||||||
@pyqtProperty(bool, constant = True)
|
|
||||||
def canSendRawGcode(self) -> bool:
|
|
||||||
if self._controller:
|
|
||||||
return self._controller.can_send_raw_gcode
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Does the printer support pause at all
|
|
||||||
@pyqtProperty(bool, constant = True)
|
|
||||||
def canPause(self) -> bool:
|
|
||||||
if self._controller:
|
|
||||||
return self._controller.can_pause
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Does the printer support abort at all
|
|
||||||
@pyqtProperty(bool, constant = True)
|
|
||||||
def canAbort(self) -> bool:
|
|
||||||
if self._controller:
|
|
||||||
return self._controller.can_abort
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Does the printer support manual control at all
|
|
||||||
@pyqtProperty(bool, constant = True)
|
|
||||||
def canControlManually(self) -> bool:
|
|
||||||
if self._controller:
|
|
||||||
return self._controller.can_control_manually
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Does the printer support upgrading firmware
|
|
||||||
@pyqtProperty(bool, notify = canUpdateFirmwareChanged)
|
|
||||||
def canUpdateFirmware(self) -> bool:
|
|
||||||
if self._controller:
|
|
||||||
return self._controller.can_update_firmware
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Stub to connect UM.Signal to pyqtSignal
|
|
||||||
def _onControllerCanUpdateFirmwareChanged(self) -> None:
|
|
||||||
self.canUpdateFirmwareChanged.emit()
|
|
||||||
|
|
||||||
# Returns the configuration (material, variant and buildplate) of the current printer
|
|
||||||
@pyqtProperty(QObject, notify = configurationChanged)
|
|
||||||
def printerConfiguration(self) -> Optional[ConfigurationModel]:
|
|
||||||
if self._printer_configuration.isValid():
|
|
||||||
return self._printer_configuration
|
|
||||||
return None
|
|
|
@ -1,261 +1,4 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
import warnings
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
warnings.warn("Importing cura.PrinterOutputDevice has been deprecated since 4.1, use cura.PrinterOutput.PrinterOutputDevice inststad", DeprecationWarning, stacklevel=2)
|
||||||
from enum import IntEnum
|
# We moved the PrinterOutput device to it's own submodule.
|
||||||
from typing import Callable, List, Optional, Union
|
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.Signal import signalemitter
|
|
||||||
from UM.Qt.QtApplication import QtApplication
|
|
||||||
from UM.FlameProfiler import pyqtSlot
|
|
||||||
|
|
||||||
MYPY = False
|
|
||||||
if MYPY:
|
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
|
||||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
|
||||||
from cura.PrinterOutput.FirmwareUpdater import FirmwareUpdater
|
|
||||||
from UM.FileHandler.FileHandler import FileHandler
|
|
||||||
from UM.Scene.SceneNode import SceneNode
|
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
|
||||||
|
|
||||||
|
|
||||||
## The current processing state of the backend.
|
|
||||||
class ConnectionState(IntEnum):
|
|
||||||
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.
|
|
||||||
#
|
|
||||||
# The assumption is made the printer is a FDM printer.
|
|
||||||
#
|
|
||||||
# Note that a number of settings are marked as "final". This is because decorators
|
|
||||||
# are not inherited by children. To fix this we use the private counter part of those
|
|
||||||
# functions to actually have the implementation.
|
|
||||||
#
|
|
||||||
# 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()
|
|
||||||
|
|
||||||
# Signal to indicate that the material of the active printer on the remote changed.
|
|
||||||
materialIdChanged = pyqtSignal()
|
|
||||||
|
|
||||||
# # Signal to indicate that the hotend of the active printer on the remote changed.
|
|
||||||
hotendIdChanged = pyqtSignal()
|
|
||||||
|
|
||||||
# Signal to indicate that the info text about the connection has changed.
|
|
||||||
connectionTextChanged = pyqtSignal()
|
|
||||||
|
|
||||||
# Signal to indicate that the configuration of one of the printers has changed.
|
|
||||||
uniqueConfigurationsChanged = pyqtSignal()
|
|
||||||
|
|
||||||
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._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._accepts_commands = False # type: bool
|
|
||||||
|
|
||||||
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_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.printersChanged.connect(self._onPrintersChanged)
|
|
||||||
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = connectionTextChanged)
|
|
||||||
def address(self) -> str:
|
|
||||||
return self._address
|
|
||||||
|
|
||||||
def setConnectionText(self, connection_text):
|
|
||||||
if self._connection_text != connection_text:
|
|
||||||
self._connection_text = connection_text
|
|
||||||
self.connectionTextChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(str, constant=True)
|
|
||||||
def connectionText(self) -> str:
|
|
||||||
return self._connection_text
|
|
||||||
|
|
||||||
def materialHotendChangedMessage(self, callback: Callable[[int], None]) -> None:
|
|
||||||
Logger.log("w", "materialHotendChangedMessage needs to be implemented, returning 'Yes'")
|
|
||||||
callback(QMessageBox.Yes)
|
|
||||||
|
|
||||||
def isConnected(self) -> bool:
|
|
||||||
return self._connection_state != ConnectionState.Closed and self._connection_state != ConnectionState.Error
|
|
||||||
|
|
||||||
def setConnectionState(self, connection_state: "ConnectionState") -> None:
|
|
||||||
if self._connection_state != connection_state:
|
|
||||||
self._connection_state = connection_state
|
|
||||||
self.connectionStateChanged.emit(self._id)
|
|
||||||
|
|
||||||
@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:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def _getPrinterByKey(self, key: str) -> Optional["PrinterOutputModel"]:
|
|
||||||
for printer in self._printers:
|
|
||||||
if printer.key == key:
|
|
||||||
return printer
|
|
||||||
|
|
||||||
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:
|
|
||||||
raise NotImplementedError("requestWrite needs to be implemented")
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = printersChanged)
|
|
||||||
def activePrinter(self) -> Optional["PrinterOutputModel"]:
|
|
||||||
if len(self._printers):
|
|
||||||
return self._printers[0]
|
|
||||||
return None
|
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = printersChanged)
|
|
||||||
def printers(self) -> List["PrinterOutputModel"]:
|
|
||||||
return self._printers
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def monitorItem(self) -> QObject:
|
|
||||||
# Note that we specifically only check if the monitor component is created.
|
|
||||||
# It could be that it failed to actually create the qml item! If we check if the item was created, it will try to
|
|
||||||
# create the item (and fail) every time.
|
|
||||||
if not self._monitor_component:
|
|
||||||
self._createMonitorViewFromQML()
|
|
||||||
return self._monitor_item
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def controlItem(self) -> QObject:
|
|
||||||
if not self._control_component:
|
|
||||||
self._createControlViewFromQML()
|
|
||||||
return self._control_item
|
|
||||||
|
|
||||||
def _createControlViewFromQML(self) -> None:
|
|
||||||
if not self._control_view_qml_path:
|
|
||||||
return
|
|
||||||
if self._control_item is None:
|
|
||||||
self._control_item = QtApplication.getInstance().createQmlComponent(self._control_view_qml_path, {"OutputDevice": self})
|
|
||||||
|
|
||||||
def _createMonitorViewFromQML(self) -> None:
|
|
||||||
if not self._monitor_view_qml_path:
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._monitor_item is None:
|
|
||||||
self._monitor_item = QtApplication.getInstance().createQmlComponent(self._monitor_view_qml_path, {"OutputDevice": self})
|
|
||||||
|
|
||||||
## Attempt to establish connection
|
|
||||||
def connect(self) -> None:
|
|
||||||
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)
|
|
||||||
|
|
||||||
## Ensure that close gets called when object is destroyed
|
|
||||||
def __del__(self) -> None:
|
|
||||||
self.close()
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = acceptsCommandsChanged)
|
|
||||||
def acceptsCommands(self) -> bool:
|
|
||||||
return self._accepts_commands
|
|
||||||
|
|
||||||
@deprecated("Please use the protected function instead", "3.2")
|
|
||||||
def setAcceptsCommands(self, accepts_commands: bool) -> None:
|
|
||||||
self._setAcceptsCommands(accepts_commands)
|
|
||||||
|
|
||||||
## Set a flag to signal the UI that the printer is not (yet) ready to receive commands
|
|
||||||
def _setAcceptsCommands(self, accepts_commands: bool) -> None:
|
|
||||||
if self._accepts_commands != accepts_commands:
|
|
||||||
self._accepts_commands = accepts_commands
|
|
||||||
|
|
||||||
self.acceptsCommandsChanged.emit()
|
|
||||||
|
|
||||||
# Returns the unique configurations of the printers within this output device
|
|
||||||
@pyqtProperty("QVariantList", notify = uniqueConfigurationsChanged)
|
|
||||||
def uniqueConfigurations(self) -> List["ConfigurationModel"]:
|
|
||||||
return self._unique_configurations
|
|
||||||
|
|
||||||
def _updateUniqueConfigurations(self) -> None:
|
|
||||||
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)
|
|
||||||
|
|
||||||
# At this point there may be non-updated configurations
|
|
||||||
self._updateUniqueConfigurations()
|
|
||||||
|
|
||||||
## Set the device firmware name
|
|
||||||
#
|
|
||||||
# \param name The name of the firmware.
|
|
||||||
def _setFirmwareName(self, name: str) -> None:
|
|
||||||
self._firmware_name = name
|
|
||||||
|
|
||||||
## Get the name of device firmware
|
|
||||||
#
|
|
||||||
# This name can be used to define device type
|
|
||||||
def getFirmwareName(self) -> Optional[str]:
|
|
||||||
return self._firmware_name
|
|
||||||
|
|
||||||
def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]:
|
|
||||||
return self._firmware_updater
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None:
|
|
||||||
if not self._firmware_updater:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._firmware_updater.updateFirmware(firmware_file)
|
|
|
@ -60,13 +60,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
previous_node = self._node
|
previous_node = self._node
|
||||||
# Disconnect from previous node signals
|
# Disconnect from previous node signals
|
||||||
if previous_node is not None and node is not previous_node:
|
if previous_node is not None and node is not previous_node:
|
||||||
previous_node.transformationChanged.disconnect(self._onChanged)
|
previous_node.boundingBoxChanged.disconnect(self._onChanged)
|
||||||
previous_node.parentChanged.disconnect(self._onChanged)
|
|
||||||
|
|
||||||
super().setNode(node)
|
super().setNode(node)
|
||||||
# Mypy doesn't understand that self._node is no longer optional, so just use the node.
|
|
||||||
node.transformationChanged.connect(self._onChanged)
|
node.boundingBoxChanged.connect(self._onChanged)
|
||||||
node.parentChanged.connect(self._onChanged)
|
|
||||||
|
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ from PyQt5.QtCore import Qt, pyqtSlot, QObject
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt5.QtWidgets import QApplication
|
||||||
|
|
||||||
from UM.Scene.Camera import Camera
|
from UM.Scene.Camera import Camera
|
||||||
from cura.ObjectsModel import ObjectsModel
|
from cura.UI.ObjectsModel import ObjectsModel
|
||||||
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
|
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
|
@ -112,21 +112,21 @@ class CuraSceneNode(SceneNode):
|
||||||
|
|
||||||
## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box
|
## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box
|
||||||
def _calculateAABB(self) -> None:
|
def _calculateAABB(self) -> None:
|
||||||
|
self._aabb = None
|
||||||
if self._mesh_data:
|
if self._mesh_data:
|
||||||
aabb = self._mesh_data.getExtents(self.getWorldTransformation())
|
self._aabb = self._mesh_data.getExtents(self.getWorldTransformation())
|
||||||
else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0)
|
|
||||||
position = self.getWorldPosition()
|
|
||||||
aabb = AxisAlignedBox(minimum = position, maximum = position)
|
|
||||||
|
|
||||||
for child in self._children:
|
for child in self.getAllChildren():
|
||||||
if child.callDecoration("isNonPrintingMesh"):
|
if child.callDecoration("isNonPrintingMesh"):
|
||||||
# Non-printing-meshes inside a group should not affect push apart or drop to build plate
|
# Non-printing-meshes inside a group should not affect push apart or drop to build plate
|
||||||
continue
|
continue
|
||||||
if aabb is None:
|
if not child.getMeshData():
|
||||||
aabb = child.getBoundingBox()
|
# Nodes without mesh data should not affect bounding boxes of their parents.
|
||||||
|
continue
|
||||||
|
if self._aabb is None:
|
||||||
|
self._aabb = child.getBoundingBox()
|
||||||
else:
|
else:
|
||||||
aabb = aabb + child.getBoundingBox()
|
self._aabb = self._aabb + child.getBoundingBox()
|
||||||
self._aabb = aabb
|
|
||||||
|
|
||||||
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
|
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
|
||||||
def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode":
|
def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode":
|
||||||
|
|
|
@ -47,8 +47,10 @@ class ContainerManager(QObject):
|
||||||
if ContainerManager.__instance is not None:
|
if ContainerManager.__instance is not None:
|
||||||
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
|
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
|
||||||
ContainerManager.__instance = self
|
ContainerManager.__instance = self
|
||||||
|
try:
|
||||||
super().__init__(parent = application)
|
super().__init__(parent = application)
|
||||||
|
except TypeError:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
self._application = application # type: CuraApplication
|
self._application = application # type: CuraApplication
|
||||||
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
|
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import configparser
|
import configparser
|
||||||
|
|
||||||
from typing import cast, Dict, Optional
|
from typing import Any, cast, Dict, Optional
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
|
@ -327,6 +327,23 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
self._registerSingleExtrusionMachinesExtruderStacks()
|
self._registerSingleExtrusionMachinesExtruderStacks()
|
||||||
self._connectUpgradedExtruderStacksToMachines()
|
self._connectUpgradedExtruderStacksToMachines()
|
||||||
|
|
||||||
|
## Check if the metadata for a container is okay before adding it.
|
||||||
|
#
|
||||||
|
# This overrides the one from UM.Settings.ContainerRegistry because we
|
||||||
|
# also require that the setting_version is correct.
|
||||||
|
@override(ContainerRegistry)
|
||||||
|
def _isMetadataValid(self, metadata: Optional[Dict[str, Any]]) -> bool:
|
||||||
|
if metadata is None:
|
||||||
|
return False
|
||||||
|
if "setting_version" not in metadata:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
if int(metadata["setting_version"]) != cura.CuraApplication.CuraApplication.SettingVersion:
|
||||||
|
return False
|
||||||
|
except ValueError: #Not parsable as int.
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
## Update an imported profile to match the current machine configuration.
|
## Update an imported profile to match the current machine configuration.
|
||||||
#
|
#
|
||||||
# \param profile The profile to configure.
|
# \param profile The profile to configure.
|
||||||
|
|
|
@ -42,7 +42,14 @@ class CuraFormulaFunctions:
|
||||||
try:
|
try:
|
||||||
extruder_stack = global_stack.extruders[str(extruder_position)]
|
extruder_stack = global_stack.extruders[str(extruder_position)]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
Logger.log("w", "Value for %s of extruder %s was requested, but that extruder is not available" % (property_key, extruder_position))
|
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
|
return None
|
||||||
|
|
||||||
value = extruder_stack.getRawProperty(property_key, "value", context = context)
|
value = extruder_stack.getRawProperty(property_key, "value", context = context)
|
||||||
|
|
|
@ -125,7 +125,12 @@ class CuraStackBuilder:
|
||||||
|
|
||||||
extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains")
|
extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains")
|
||||||
extruder_definition_id = extruder_definition_dict[str(extruder_position)]
|
extruder_definition_id = extruder_definition_dict[str(extruder_position)]
|
||||||
|
try:
|
||||||
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
|
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
|
||||||
|
except IndexError as e:
|
||||||
|
# It still needs to break, but we want to know what extruder ID made it break.
|
||||||
|
Logger.log("e", "Unable to find extruder with the id %s", extruder_definition_id)
|
||||||
|
raise e
|
||||||
|
|
||||||
# get material container for extruders
|
# get material container for extruders
|
||||||
material_container = application.empty_material_container
|
material_container = application.empty_material_container
|
||||||
|
|
|
@ -224,7 +224,16 @@ class ExtruderManager(QObject):
|
||||||
|
|
||||||
# Get the extruders of all printable meshes in the scene
|
# Get the extruders of all printable meshes in the scene
|
||||||
meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()] #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()] #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
|
|
||||||
|
# Exclude anti-overhang meshes
|
||||||
|
mesh_list = []
|
||||||
for mesh in meshes:
|
for mesh in meshes:
|
||||||
|
stack = mesh.callDecoration("getStack")
|
||||||
|
if stack is not None and (stack.getProperty("anti_overhang_mesh", "value") or stack.getProperty("support_mesh", "value")):
|
||||||
|
continue
|
||||||
|
mesh_list.append(mesh)
|
||||||
|
|
||||||
|
for mesh in mesh_list:
|
||||||
extruder_stack_id = mesh.callDecoration("getActiveExtruder")
|
extruder_stack_id = mesh.callDecoration("getActiveExtruder")
|
||||||
if not extruder_stack_id:
|
if not extruder_stack_id:
|
||||||
# No per-object settings for this node
|
# No per-object settings for this node
|
||||||
|
@ -338,7 +347,7 @@ class ExtruderManager(QObject):
|
||||||
extruder_train.setNextStack(global_stack)
|
extruder_train.setNextStack(global_stack)
|
||||||
extruders_changed = True
|
extruders_changed = True
|
||||||
|
|
||||||
self._fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
self.fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||||
if extruders_changed:
|
if extruders_changed:
|
||||||
self.extrudersChanged.emit(global_stack_id)
|
self.extrudersChanged.emit(global_stack_id)
|
||||||
self.setActiveExtruderIndex(0)
|
self.setActiveExtruderIndex(0)
|
||||||
|
@ -346,7 +355,7 @@ class ExtruderManager(QObject):
|
||||||
|
|
||||||
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
|
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
|
||||||
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
|
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
|
||||||
def _fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
|
def fixSingleExtrusionMachineExtruderDefinition(self, global_stack: "GlobalStack") -> None:
|
||||||
container_registry = ContainerRegistry.getInstance()
|
container_registry = ContainerRegistry.getInstance()
|
||||||
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
|
expected_extruder_definition_0_id = global_stack.getMetaDataEntry("machine_extruder_trains")["0"]
|
||||||
extruder_stack_0 = global_stack.extruders.get("0")
|
extruder_stack_0 = global_stack.extruders.get("0")
|
||||||
|
|
|
@ -4,6 +4,8 @@
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Dict, Optional, Set, TYPE_CHECKING, List
|
from typing import Any, Dict, Optional, Set, TYPE_CHECKING, List
|
||||||
|
import uuid
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
|
from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
|
||||||
|
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
|
@ -34,6 +36,12 @@ class GlobalStack(CuraContainerStack):
|
||||||
|
|
||||||
self.setMetaDataEntry("type", "machine") # For backward compatibility
|
self.setMetaDataEntry("type", "machine") # For backward compatibility
|
||||||
|
|
||||||
|
# TL;DR: If Cura is looking for printers that belong to the same group, it should use "group_id".
|
||||||
|
# Each GlobalStack by default belongs to a group which is identified via "group_id". This group_id is used to
|
||||||
|
# figure out which GlobalStacks are in the printer cluster for example without knowing the implementation
|
||||||
|
# details such as the um_network_key or some other identifier that's used by the underlying device plugin.
|
||||||
|
self.setMetaDataEntry("group_id", str(uuid.uuid4())) # Assign a new GlobalStack to a unique group by default
|
||||||
|
|
||||||
self._extruders = {} # type: Dict[str, "ExtruderStack"]
|
self._extruders = {} # type: Dict[str, "ExtruderStack"]
|
||||||
|
|
||||||
# This property is used to track which settings we are calculating the "resolve" for
|
# This property is used to track which settings we are calculating the "resolve" for
|
||||||
|
@ -64,6 +72,14 @@ class GlobalStack(CuraContainerStack):
|
||||||
machine_extruder_count = self.getProperty("machine_extruder_count", "value")
|
machine_extruder_count = self.getProperty("machine_extruder_count", "value")
|
||||||
return result_list[:machine_extruder_count]
|
return result_list[:machine_extruder_count]
|
||||||
|
|
||||||
|
@pyqtProperty(int, constant = True)
|
||||||
|
def maxExtruderCount(self):
|
||||||
|
return len(self.getMetaDataEntry("machine_extruder_trains"))
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify=configuredConnectionTypesChanged)
|
||||||
|
def supportsNetworkConnection(self):
|
||||||
|
return self.getMetaDataEntry("supports_network_connection", False)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def getLoadingPriority(cls) -> int:
|
def getLoadingPriority(cls) -> int:
|
||||||
return 2
|
return 2
|
||||||
|
@ -81,7 +97,15 @@ class GlobalStack(CuraContainerStack):
|
||||||
# Requesting it from the metadata actually gets them as strings (as that's what you get from serializing).
|
# 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)
|
# 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(",")
|
connection_types = self.getMetaDataEntry("connection_type", "").split(",")
|
||||||
return [int(connection_type) for connection_type in connection_types if connection_type != ""]
|
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
|
## \sa configuredConnectionTypes
|
||||||
def addConfiguredConnectionType(self, connection_type: int) -> None:
|
def addConfiguredConnectionType(self, connection_type: int) -> None:
|
||||||
|
@ -200,7 +224,7 @@ class GlobalStack(CuraContainerStack):
|
||||||
# Determine whether or not we should try to get the "resolve" property instead of the
|
# Determine whether or not we should try to get the "resolve" property instead of the
|
||||||
# requested property.
|
# requested property.
|
||||||
def _shouldResolve(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> bool:
|
def _shouldResolve(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> bool:
|
||||||
if property_name is not "value":
|
if property_name != "value":
|
||||||
# Do not try to resolve anything but the "value" property
|
# Do not try to resolve anything but the "value" property
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
@ -246,6 +270,9 @@ class GlobalStack(CuraContainerStack):
|
||||||
def getHasVariants(self) -> bool:
|
def getHasVariants(self) -> bool:
|
||||||
return parseBool(self.getMetaDataEntry("has_variants", False))
|
return parseBool(self.getMetaDataEntry("has_variants", False))
|
||||||
|
|
||||||
|
def getHasVariantsBuildPlates(self) -> bool:
|
||||||
|
return parseBool(self.getMetaDataEntry("has_variant_buildplates", False))
|
||||||
|
|
||||||
def getHasMachineQuality(self) -> bool:
|
def getHasMachineQuality(self) -> bool:
|
||||||
return parseBool(self.getMetaDataEntry("has_machine_quality", False))
|
return parseBool(self.getMetaDataEntry("has_machine_quality", False))
|
||||||
|
|
||||||
|
|
|
@ -6,13 +6,14 @@ import re
|
||||||
import unicodedata
|
import unicodedata
|
||||||
from typing import Any, List, Dict, TYPE_CHECKING, Optional, cast
|
from typing import Any, List, Dict, TYPE_CHECKING, Optional, cast
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer
|
||||||
|
|
||||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||||
|
from UM.Decorators import deprecated
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
from UM.Settings.Interfaces import ContainerInterface
|
from UM.Settings.Interfaces import ContainerInterface
|
||||||
from UM.Signal import Signal
|
from UM.Signal import Signal
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer
|
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
from UM import Util
|
from UM import Util
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
@ -22,10 +23,10 @@ from UM.Settings.SettingFunction import SettingFunction
|
||||||
from UM.Signal import postponeSignals, CompressTechnique
|
from UM.Signal import postponeSignals, CompressTechnique
|
||||||
|
|
||||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||||
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionType
|
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionType
|
||||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||||
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
|
||||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from cura.Settings.ExtruderStack import ExtruderStack
|
from cura.Settings.ExtruderStack import ExtruderStack
|
||||||
|
@ -106,7 +107,7 @@ class MachineManager(QObject):
|
||||||
# There might already be some output devices by the time the signal is connected
|
# There might already be some output devices by the time the signal is connected
|
||||||
self._onOutputDevicesChanged()
|
self._onOutputDevicesChanged()
|
||||||
|
|
||||||
self._current_printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
|
self._current_printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer
|
||||||
self.activeMaterialChanged.connect(self._onCurrentConfigurationChanged)
|
self.activeMaterialChanged.connect(self._onCurrentConfigurationChanged)
|
||||||
self.activeVariantChanged.connect(self._onCurrentConfigurationChanged)
|
self.activeVariantChanged.connect(self._onCurrentConfigurationChanged)
|
||||||
# Force to compute the current configuration
|
# Force to compute the current configuration
|
||||||
|
@ -157,6 +158,7 @@ class MachineManager(QObject):
|
||||||
printerConnectedStatusChanged = pyqtSignal() # Emitted every time the active machine change or the outputdevices change
|
printerConnectedStatusChanged = pyqtSignal() # Emitted every time the active machine change or the outputdevices change
|
||||||
|
|
||||||
rootMaterialChanged = pyqtSignal()
|
rootMaterialChanged = pyqtSignal()
|
||||||
|
discoveredPrintersChanged = pyqtSignal()
|
||||||
|
|
||||||
def setInitialActiveMachine(self) -> None:
|
def setInitialActiveMachine(self) -> None:
|
||||||
active_machine_id = self._application.getPreferences().getValue("cura/active_machine")
|
active_machine_id = self._application.getPreferences().getValue("cura/active_machine")
|
||||||
|
@ -171,10 +173,9 @@ class MachineManager(QObject):
|
||||||
self._printer_output_devices.append(printer_output_device)
|
self._printer_output_devices.append(printer_output_device)
|
||||||
|
|
||||||
self.outputDevicesChanged.emit()
|
self.outputDevicesChanged.emit()
|
||||||
self.printerConnectedStatusChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = currentConfigurationChanged)
|
@pyqtProperty(QObject, notify = currentConfigurationChanged)
|
||||||
def currentConfiguration(self) -> ConfigurationModel:
|
def currentConfiguration(self) -> PrinterConfigurationModel:
|
||||||
return self._current_printer_configuration
|
return self._current_printer_configuration
|
||||||
|
|
||||||
def _onCurrentConfigurationChanged(self) -> None:
|
def _onCurrentConfigurationChanged(self) -> None:
|
||||||
|
@ -205,7 +206,7 @@ class MachineManager(QObject):
|
||||||
self.currentConfigurationChanged.emit()
|
self.currentConfigurationChanged.emit()
|
||||||
|
|
||||||
@pyqtSlot(QObject, result = bool)
|
@pyqtSlot(QObject, result = bool)
|
||||||
def matchesConfiguration(self, configuration: ConfigurationModel) -> bool:
|
def matchesConfiguration(self, configuration: PrinterConfigurationModel) -> bool:
|
||||||
return self._current_printer_configuration == configuration
|
return self._current_printer_configuration == configuration
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = outputDevicesChanged)
|
@pyqtProperty("QVariantList", notify = outputDevicesChanged)
|
||||||
|
@ -357,12 +358,11 @@ class MachineManager(QObject):
|
||||||
# Make sure that the default machine actions for this machine have been added
|
# Make sure that the default machine actions for this machine have been added
|
||||||
self._application.getMachineActionManager().addDefaultMachineActions(global_stack)
|
self._application.getMachineActionManager().addDefaultMachineActions(global_stack)
|
||||||
|
|
||||||
ExtruderManager.getInstance()._fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
ExtruderManager.getInstance().fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||||
if not global_stack.isValid():
|
if not global_stack.isValid():
|
||||||
# Mark global stack as invalid
|
# Mark global stack as invalid
|
||||||
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
|
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
|
||||||
return # We're done here
|
return # We're done here
|
||||||
ExtruderManager.getInstance().setActiveExtruderIndex(0) # Switch to first extruder
|
|
||||||
|
|
||||||
self._global_container_stack = global_stack
|
self._global_container_stack = global_stack
|
||||||
self._application.setGlobalContainerStack(global_stack)
|
self._application.setGlobalContainerStack(global_stack)
|
||||||
|
@ -370,6 +370,11 @@ class MachineManager(QObject):
|
||||||
self._initMachineState(global_stack)
|
self._initMachineState(global_stack)
|
||||||
self._onGlobalContainerChanged()
|
self._onGlobalContainerChanged()
|
||||||
|
|
||||||
|
# Switch to the first enabled extruder
|
||||||
|
self.updateDefaultExtruder()
|
||||||
|
default_extruder_position = int(self.defaultExtruderPosition)
|
||||||
|
ExtruderManager.getInstance().setActiveExtruderIndex(default_extruder_position)
|
||||||
|
|
||||||
self.__emitChangedSignals()
|
self.__emitChangedSignals()
|
||||||
|
|
||||||
## Given a definition id, return the machine with this id.
|
## Given a definition id, return the machine with this id.
|
||||||
|
@ -386,9 +391,17 @@ class MachineManager(QObject):
|
||||||
return machine
|
return machine
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@pyqtSlot(str)
|
||||||
@pyqtSlot(str, str)
|
@pyqtSlot(str, str)
|
||||||
def addMachine(self, name: str, definition_id: str) -> None:
|
def addMachine(self, definition_id: str, name: Optional[str] = None) -> None:
|
||||||
new_stack = CuraStackBuilder.createMachine(name, definition_id)
|
if name is None:
|
||||||
|
definitions = CuraContainerRegistry.getInstance().findDefinitionContainers(id = definition_id)
|
||||||
|
if definitions:
|
||||||
|
name = definitions[0].getName()
|
||||||
|
else:
|
||||||
|
name = definition_id
|
||||||
|
|
||||||
|
new_stack = CuraStackBuilder.createMachine(cast(str, name), definition_id)
|
||||||
if new_stack:
|
if new_stack:
|
||||||
# Instead of setting the global container stack here, we set the active machine and so the signals are emitted
|
# Instead of setting the global container stack here, we set the active machine and so the signals are emitted
|
||||||
self.setActiveMachine(new_stack.getId())
|
self.setActiveMachine(new_stack.getId())
|
||||||
|
@ -486,18 +499,21 @@ class MachineManager(QObject):
|
||||||
return bool(self._stacks_have_errors)
|
return bool(self._stacks_have_errors)
|
||||||
|
|
||||||
@pyqtProperty(str, notify = globalContainerChanged)
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
|
@deprecated("use Cura.MachineManager.activeMachine.definition.name instead", "4.1")
|
||||||
def activeMachineDefinitionName(self) -> str:
|
def activeMachineDefinitionName(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self._global_container_stack.definition.getName()
|
return self._global_container_stack.definition.getName()
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(str, notify = globalContainerChanged)
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
|
@deprecated("use Cura.MachineManager.activeMachine.name instead", "4.1")
|
||||||
def activeMachineName(self) -> str:
|
def activeMachineName(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self._global_container_stack.getMetaDataEntry("group_name", self._global_container_stack.getName())
|
return self._global_container_stack.getMetaDataEntry("group_name", self._global_container_stack.getName())
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(str, notify = globalContainerChanged)
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
|
@deprecated("use Cura.MachineManager.activeMachine.id instead", "4.1")
|
||||||
def activeMachineId(self) -> str:
|
def activeMachineId(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self._global_container_stack.getId()
|
return self._global_container_stack.getId()
|
||||||
|
@ -531,6 +547,7 @@ class MachineManager(QObject):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify=globalContainerChanged)
|
@pyqtProperty("QVariantList", notify=globalContainerChanged)
|
||||||
|
@deprecated("use Cura.MachineManager.activeMachine.configuredConnectionTypes instead", "4.1")
|
||||||
def activeMachineConfiguredConnectionTypes(self):
|
def activeMachineConfiguredConnectionTypes(self):
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self._global_container_stack.configuredConnectionTypes
|
return self._global_container_stack.configuredConnectionTypes
|
||||||
|
@ -675,11 +692,6 @@ class MachineManager(QObject):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
## Check if a container is read_only
|
|
||||||
@pyqtSlot(str, result = bool)
|
|
||||||
def isReadOnly(self, container_id: str) -> bool:
|
|
||||||
return CuraContainerRegistry.getInstance().isReadOnly(container_id)
|
|
||||||
|
|
||||||
## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
|
## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def copyValueToExtruders(self, key: str) -> None:
|
def copyValueToExtruders(self, key: str) -> None:
|
||||||
|
@ -708,6 +720,7 @@ class MachineManager(QObject):
|
||||||
extruder_stack.userChanges.setProperty(key, "value", new_value)
|
extruder_stack.userChanges.setProperty(key, "value", new_value)
|
||||||
|
|
||||||
@pyqtProperty(str, notify = activeVariantChanged)
|
@pyqtProperty(str, notify = activeVariantChanged)
|
||||||
|
@deprecated("use Cura.MachineManager.activeStack.variant.name instead", "4.1")
|
||||||
def activeVariantName(self) -> str:
|
def activeVariantName(self) -> str:
|
||||||
if self._active_container_stack:
|
if self._active_container_stack:
|
||||||
variant = self._active_container_stack.variant
|
variant = self._active_container_stack.variant
|
||||||
|
@ -717,6 +730,7 @@ class MachineManager(QObject):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(str, notify = activeVariantChanged)
|
@pyqtProperty(str, notify = activeVariantChanged)
|
||||||
|
@deprecated("use Cura.MachineManager.activeStack.variant.id instead", "4.1")
|
||||||
def activeVariantId(self) -> str:
|
def activeVariantId(self) -> str:
|
||||||
if self._active_container_stack:
|
if self._active_container_stack:
|
||||||
variant = self._active_container_stack.variant
|
variant = self._active_container_stack.variant
|
||||||
|
@ -726,6 +740,7 @@ class MachineManager(QObject):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(str, notify = activeVariantChanged)
|
@pyqtProperty(str, notify = activeVariantChanged)
|
||||||
|
@deprecated("use Cura.MachineManager.activeMachine.variant.name instead", "4.1")
|
||||||
def activeVariantBuildplateName(self) -> str:
|
def activeVariantBuildplateName(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
variant = self._global_container_stack.variant
|
variant = self._global_container_stack.variant
|
||||||
|
@ -735,6 +750,7 @@ class MachineManager(QObject):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(str, notify = globalContainerChanged)
|
@pyqtProperty(str, notify = globalContainerChanged)
|
||||||
|
@deprecated("use Cura.MachineManager.activeMachine.definition.id instead", "4.1")
|
||||||
def activeDefinitionId(self) -> str:
|
def activeDefinitionId(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self._global_container_stack.definition.id
|
return self._global_container_stack.definition.id
|
||||||
|
@ -781,7 +797,6 @@ class MachineManager(QObject):
|
||||||
self.setActiveMachine(other_machine_stacks[0]["id"])
|
self.setActiveMachine(other_machine_stacks[0]["id"])
|
||||||
|
|
||||||
metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0]
|
metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0]
|
||||||
network_key = metadata.get("um_network_key", None)
|
|
||||||
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
||||||
containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
|
containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
|
||||||
for container in containers:
|
for container in containers:
|
||||||
|
@ -789,8 +804,9 @@ class MachineManager(QObject):
|
||||||
CuraContainerRegistry.getInstance().removeContainer(machine_id)
|
CuraContainerRegistry.getInstance().removeContainer(machine_id)
|
||||||
|
|
||||||
# If the printer that is being removed is a network printer, the hidden printers have to be also removed
|
# If the printer that is being removed is a network printer, the hidden printers have to be also removed
|
||||||
if network_key:
|
group_id = metadata.get("group_id", None)
|
||||||
metadata_filter = {"um_network_key": network_key}
|
if group_id:
|
||||||
|
metadata_filter = {"group_id": group_id}
|
||||||
hidden_containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
hidden_containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
||||||
if hidden_containers:
|
if hidden_containers:
|
||||||
# This reuses the method and remove all printers recursively
|
# This reuses the method and remove all printers recursively
|
||||||
|
@ -799,19 +815,19 @@ class MachineManager(QObject):
|
||||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||||
def hasMaterials(self) -> bool:
|
def hasMaterials(self) -> bool:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False))
|
return self._global_container_stack.getHasMaterials()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||||
def hasVariants(self) -> bool:
|
def hasVariants(self) -> bool:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_variants", False))
|
return self._global_container_stack.getHasVariants()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||||
def hasVariantBuildplates(self) -> bool:
|
def hasVariantBuildplates(self) -> bool:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_variant_buildplates", False))
|
return self._global_container_stack.getHasVariantsBuildPlates()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
## The selected buildplate is compatible if it is compatible with all the materials in all the extruders
|
## The selected buildplate is compatible if it is compatible with all the materials in all the extruders
|
||||||
|
@ -1057,9 +1073,6 @@ class MachineManager(QObject):
|
||||||
def _onMaterialNameChanged(self) -> None:
|
def _onMaterialNameChanged(self) -> None:
|
||||||
self.activeMaterialChanged.emit()
|
self.activeMaterialChanged.emit()
|
||||||
|
|
||||||
def _onQualityNameChanged(self) -> None:
|
|
||||||
self.activeQualityChanged.emit()
|
|
||||||
|
|
||||||
def _getContainerChangedSignals(self) -> List[Signal]:
|
def _getContainerChangedSignals(self) -> List[Signal]:
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None:
|
||||||
return []
|
return []
|
||||||
|
@ -1347,27 +1360,30 @@ class MachineManager(QObject):
|
||||||
# Get the definition id corresponding to this machine name
|
# Get the definition id corresponding to this machine name
|
||||||
machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()
|
machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()
|
||||||
# Try to find a machine with the same network key
|
# Try to find a machine with the same network key
|
||||||
new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey()})
|
metadata_filter = {"group_id": self._global_container_stack.getMetaDataEntry("group_id"),
|
||||||
|
"um_network_key": self.activeMachineNetworkKey(),
|
||||||
|
}
|
||||||
|
new_machine = self.getMachine(machine_definition_id, metadata_filter = metadata_filter)
|
||||||
# If there is no machine, then create a new one and set it to the non-hidden instance
|
# If there is no machine, then create a new one and set it to the non-hidden instance
|
||||||
if not new_machine:
|
if not new_machine:
|
||||||
new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id)
|
new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id)
|
||||||
if not new_machine:
|
if not new_machine:
|
||||||
return
|
return
|
||||||
|
new_machine.setMetaDataEntry("group_id", self._global_container_stack.getMetaDataEntry("group_id"))
|
||||||
new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey())
|
new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey())
|
||||||
new_machine.setMetaDataEntry("group_name", self.activeMachineNetworkGroupName)
|
new_machine.setMetaDataEntry("group_name", self.activeMachineNetworkGroupName)
|
||||||
new_machine.setMetaDataEntry("hidden", False)
|
|
||||||
new_machine.setMetaDataEntry("connection_type", self._global_container_stack.getMetaDataEntry("connection_type"))
|
new_machine.setMetaDataEntry("connection_type", self._global_container_stack.getMetaDataEntry("connection_type"))
|
||||||
else:
|
else:
|
||||||
Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey())
|
Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey())
|
||||||
new_machine.setMetaDataEntry("hidden", False)
|
|
||||||
|
|
||||||
# Set the current printer instance to hidden (the metadata entry must exist)
|
# Set the current printer instance to hidden (the metadata entry must exist)
|
||||||
|
new_machine.setMetaDataEntry("hidden", False)
|
||||||
self._global_container_stack.setMetaDataEntry("hidden", True)
|
self._global_container_stack.setMetaDataEntry("hidden", True)
|
||||||
|
|
||||||
self.setActiveMachine(new_machine.getId())
|
self.setActiveMachine(new_machine.getId())
|
||||||
|
|
||||||
@pyqtSlot(QObject)
|
@pyqtSlot(QObject)
|
||||||
def applyRemoteConfiguration(self, configuration: ConfigurationModel) -> None:
|
def applyRemoteConfiguration(self, configuration: PrinterConfigurationModel) -> None:
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None:
|
||||||
return
|
return
|
||||||
self.blurSettings.emit()
|
self.blurSettings.emit()
|
||||||
|
@ -1382,8 +1398,9 @@ class MachineManager(QObject):
|
||||||
need_to_show_message = False
|
need_to_show_message = False
|
||||||
|
|
||||||
for extruder_configuration in configuration.extruderConfigurations:
|
for extruder_configuration in configuration.extruderConfigurations:
|
||||||
extruder_has_hotend = extruder_configuration.hotendID != ""
|
# We support "" or None, since the cloud uses None instead of empty strings
|
||||||
extruder_has_material = extruder_configuration.material.guid != ""
|
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 the machine doesn't have a hotend or material, disable this extruder
|
||||||
if not extruder_has_hotend or not extruder_has_material:
|
if not extruder_has_hotend or not extruder_has_material:
|
||||||
|
@ -1423,6 +1440,7 @@ class MachineManager(QObject):
|
||||||
self._global_container_stack.extruders[position].setEnabled(True)
|
self._global_container_stack.extruders[position].setEnabled(True)
|
||||||
self.updateMaterialWithVariant(position)
|
self.updateMaterialWithVariant(position)
|
||||||
|
|
||||||
|
self.updateDefaultExtruder()
|
||||||
self.updateNumberExtrudersEnabled()
|
self.updateNumberExtrudersEnabled()
|
||||||
|
|
||||||
if configuration.buildplateConfiguration is not None:
|
if configuration.buildplateConfiguration is not None:
|
||||||
|
@ -1454,31 +1472,6 @@ class MachineManager(QObject):
|
||||||
if self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1:
|
if self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1:
|
||||||
self._application.discardOrKeepProfileChanges()
|
self._application.discardOrKeepProfileChanges()
|
||||||
|
|
||||||
## Find all container stacks that has the pair 'key = value' in its metadata and replaces the value with 'new_value'
|
|
||||||
def replaceContainersMetadata(self, key: str, value: str, new_value: str) -> None:
|
|
||||||
machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
|
||||||
for machine in machines:
|
|
||||||
if machine.getMetaDataEntry(key) == value:
|
|
||||||
machine.setMetaDataEntry(key, new_value)
|
|
||||||
|
|
||||||
## This method checks if the name of the group stored in the definition container is correct.
|
|
||||||
# After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group
|
|
||||||
# then all the container stacks are updated, both the current and the hidden ones.
|
|
||||||
def checkCorrectGroupName(self, device_id: str, group_name: str) -> None:
|
|
||||||
if self._global_container_stack and device_id == self.activeMachineNetworkKey():
|
|
||||||
# Check if the 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("group_name", group_name)
|
|
||||||
|
|
||||||
## This method checks if there is an instance connected to the given network_key
|
|
||||||
def existNetworkInstances(self, network_key: str) -> bool:
|
|
||||||
metadata_filter = {"um_network_key": network_key}
|
|
||||||
containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
|
||||||
return bool(containers)
|
|
||||||
|
|
||||||
@pyqtSlot("QVariant")
|
@pyqtSlot("QVariant")
|
||||||
def setGlobalVariant(self, container_node: "ContainerNode") -> None:
|
def setGlobalVariant(self, container_node: "ContainerNode") -> None:
|
||||||
self.blurSettings.emit()
|
self.blurSettings.emit()
|
||||||
|
@ -1649,3 +1642,22 @@ class MachineManager(QObject):
|
||||||
abbr_machine += stripped_word
|
abbr_machine += stripped_word
|
||||||
|
|
||||||
return abbr_machine
|
return abbr_machine
|
||||||
|
|
||||||
|
# Checks if the given machine type name in the available machine list.
|
||||||
|
# The machine type is a code name such as "ultimaker_3", while the machine type name is the human-readable name of
|
||||||
|
# the machine type, which is "Ultimaker 3" for "ultimaker_3".
|
||||||
|
def hasHumanReadableMachineTypeName(self, machine_type_name: str) -> bool:
|
||||||
|
results = self._container_registry.findDefinitionContainersMetadata(name = machine_type_name)
|
||||||
|
return len(results) > 0
|
||||||
|
|
||||||
|
@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
|
||||||
|
|
||||||
|
# Gets all machines that belong to the given group_id.
|
||||||
|
def getMachinesInGroup(self, group_id: str) -> List["GlobalStack"]:
|
||||||
|
return self._container_registry.findContainerStacks(type = "machine", group_id = group_id)
|
||||||
|
|
|
@ -34,7 +34,7 @@ class PerObjectContainerStack(CuraContainerStack):
|
||||||
if limit_to_extruder is not None:
|
if limit_to_extruder is not None:
|
||||||
limit_to_extruder = str(limit_to_extruder)
|
limit_to_extruder = str(limit_to_extruder)
|
||||||
|
|
||||||
# if this stack has the limit_to_extruder "not overriden", use the original limit_to_extruder as the current
|
# if this stack has the limit_to_extruder "not overridden", use the original limit_to_extruder as the current
|
||||||
# limit_to_extruder, so the values retrieved will be from the perspective of the original limit_to_extruder
|
# limit_to_extruder, so the values retrieved will be from the perspective of the original limit_to_extruder
|
||||||
# stack.
|
# stack.
|
||||||
if limit_to_extruder == "-1":
|
if limit_to_extruder == "-1":
|
||||||
|
@ -42,7 +42,7 @@ class PerObjectContainerStack(CuraContainerStack):
|
||||||
limit_to_extruder = context.context["original_limit_to_extruder"]
|
limit_to_extruder = context.context["original_limit_to_extruder"]
|
||||||
|
|
||||||
if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in global_stack.extruders:
|
if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in global_stack.extruders:
|
||||||
# set the original limit_to_extruder if this is the first stack that has a non-overriden limit_to_extruder
|
# set the original limit_to_extruder if this is the first stack that has a non-overridden limit_to_extruder
|
||||||
if "original_limit_to_extruder" not in context.context:
|
if "original_limit_to_extruder" not in context.context:
|
||||||
context.context["original_limit_to_extruder"] = limit_to_extruder
|
context.context["original_limit_to_extruder"] = limit_to_extruder
|
||||||
|
|
||||||
|
|
|
@ -73,8 +73,8 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
|
|
||||||
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
|
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
|
||||||
# has not been updated yet.
|
# has not been updated yet.
|
||||||
deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
deep_copy._is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
|
||||||
deep_copy._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
|
deep_copy._is_non_thumbnail_visible_mesh = self._evaluateIsNonThumbnailVisibleMesh()
|
||||||
|
|
||||||
return deep_copy
|
return deep_copy
|
||||||
|
|
||||||
|
@ -102,20 +102,25 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||||
def isNonPrintingMesh(self):
|
def isNonPrintingMesh(self):
|
||||||
return self._is_non_printing_mesh
|
return self._is_non_printing_mesh
|
||||||
|
|
||||||
def evaluateIsNonPrintingMesh(self):
|
def _evaluateIsNonPrintingMesh(self):
|
||||||
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
|
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
|
||||||
|
|
||||||
def isNonThumbnailVisibleMesh(self):
|
def isNonThumbnailVisibleMesh(self):
|
||||||
return self._is_non_thumbnail_visible_mesh
|
return self._is_non_thumbnail_visible_mesh
|
||||||
|
|
||||||
def evaluateIsNonThumbnailVisibleMesh(self):
|
def _evaluateIsNonThumbnailVisibleMesh(self):
|
||||||
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_thumbnail_visible_settings)
|
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_thumbnail_visible_settings)
|
||||||
|
|
||||||
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
|
def _onSettingChanged(self, setting_key, property_name): # Reminder: 'property' is a built-in function
|
||||||
|
# We're only interested in a few settings and only if it's value changed.
|
||||||
if property_name == "value":
|
if property_name == "value":
|
||||||
|
if setting_key in self._non_printing_mesh_settings or setting_key in self._non_thumbnail_visible_settings:
|
||||||
# Trigger slice/need slicing if the value has changed.
|
# Trigger slice/need slicing if the value has changed.
|
||||||
self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
new_is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
|
||||||
self._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
|
self._is_non_thumbnail_visible_mesh = self._evaluateIsNonThumbnailVisibleMesh()
|
||||||
|
|
||||||
|
if self._is_non_printing_mesh != new_is_non_printing_mesh:
|
||||||
|
self._is_non_printing_mesh = new_is_non_printing_mesh
|
||||||
|
|
||||||
Application.getInstance().getBackend().needsSlicing()
|
Application.getInstance().getBackend().needsSlicing()
|
||||||
Application.getInstance().getBackend().tickle()
|
Application.getInstance().getBackend().tickle()
|
||||||
|
|
|
@ -41,6 +41,22 @@ empty_quality_changes_container.setMetaDataEntry("type", "quality_changes")
|
||||||
empty_quality_changes_container.setMetaDataEntry("quality_type", "not_supported")
|
empty_quality_changes_container.setMetaDataEntry("quality_type", "not_supported")
|
||||||
|
|
||||||
|
|
||||||
|
# All empty container IDs set
|
||||||
|
ALL_EMPTY_CONTAINER_ID_SET = {
|
||||||
|
EMPTY_CONTAINER_ID,
|
||||||
|
EMPTY_DEFINITION_CHANGES_CONTAINER_ID,
|
||||||
|
EMPTY_VARIANT_CONTAINER_ID,
|
||||||
|
EMPTY_MATERIAL_CONTAINER_ID,
|
||||||
|
EMPTY_QUALITY_CONTAINER_ID,
|
||||||
|
EMPTY_QUALITY_CHANGES_CONTAINER_ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Convenience function to check if a container ID represents an empty container.
|
||||||
|
def isEmptyContainer(container_id: str) -> bool:
|
||||||
|
return container_id in ALL_EMPTY_CONTAINER_ID_SET
|
||||||
|
|
||||||
|
|
||||||
__all__ = ["EMPTY_CONTAINER_ID",
|
__all__ = ["EMPTY_CONTAINER_ID",
|
||||||
"empty_container", # For convenience
|
"empty_container", # For convenience
|
||||||
"EMPTY_DEFINITION_CHANGES_CONTAINER_ID",
|
"EMPTY_DEFINITION_CHANGES_CONTAINER_ID",
|
||||||
|
@ -52,5 +68,7 @@ __all__ = ["EMPTY_CONTAINER_ID",
|
||||||
"EMPTY_QUALITY_CHANGES_CONTAINER_ID",
|
"EMPTY_QUALITY_CHANGES_CONTAINER_ID",
|
||||||
"empty_quality_changes_container",
|
"empty_quality_changes_container",
|
||||||
"EMPTY_QUALITY_CONTAINER_ID",
|
"EMPTY_QUALITY_CONTAINER_ID",
|
||||||
"empty_quality_container"
|
"empty_quality_container",
|
||||||
|
"ALL_EMPTY_CONTAINER_ID_SET",
|
||||||
|
"isEmptyContainer",
|
||||||
]
|
]
|
||||||
|
|
|
@ -27,3 +27,6 @@ class CuraStage(Stage):
|
||||||
@pyqtProperty(QUrl, constant = True)
|
@pyqtProperty(QUrl, constant = True)
|
||||||
def stageMenuComponent(self) -> QUrl:
|
def stageMenuComponent(self) -> QUrl:
|
||||||
return self.getDisplayComponent("menu")
|
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.
|
|
31
cura/UI/AddPrinterPagesModel.py
Normal file
31
cura/UI/AddPrinterPagesModel.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
# 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"),
|
||||||
|
"previous_page_button_text": self._catalog.i18nc("@action:button", "Cancel"),
|
||||||
|
})
|
||||||
|
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:
|
if TYPE_CHECKING:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
from .MachineAction import MachineAction
|
from cura.MachineAction import MachineAction
|
||||||
|
|
||||||
|
|
||||||
## Raised when trying to add an unknown machine action as a required action
|
## Raised when trying to add an unknown machine action as a required action
|
||||||
|
@ -136,7 +136,7 @@ class MachineActionManager(QObject):
|
||||||
# action multiple times).
|
# action multiple times).
|
||||||
# \param definition_id The ID of the definition that you want to get the "on added" actions for.
|
# \param definition_id The ID of the definition that you want to get the "on added" actions for.
|
||||||
# \returns List of actions.
|
# \returns List of actions.
|
||||||
@pyqtSlot(str, result="QVariantList")
|
@pyqtSlot(str, result = "QVariantList")
|
||||||
def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]:
|
def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]:
|
||||||
if definition_id in self._first_start_actions:
|
if definition_id in self._first_start_actions:
|
||||||
return self._first_start_actions[definition_id]
|
return self._first_start_actions[definition_id]
|
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,6 +1,9 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
from PyQt5.QtCore import QTimer, Qt
|
from PyQt5.QtCore import QTimer, Qt
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
@ -10,7 +13,6 @@ from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Scene.Selection import Selection
|
from UM.Scene.Selection import Selection
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
@ -23,7 +25,7 @@ class ObjectsModel(ListModel):
|
||||||
BuilplateNumberRole = Qt.UserRole + 4
|
BuilplateNumberRole = Qt.UserRole + 4
|
||||||
NodeRole = Qt.UserRole + 5
|
NodeRole = Qt.UserRole + 5
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.addRoleName(self.NameRole, "name")
|
self.addRoleName(self.NameRole, "name")
|
||||||
|
@ -42,31 +44,33 @@ class ObjectsModel(ListModel):
|
||||||
|
|
||||||
self._build_plate_number = -1
|
self._build_plate_number = -1
|
||||||
|
|
||||||
def setActiveBuildPlate(self, nr):
|
def setActiveBuildPlate(self, nr: int) -> None:
|
||||||
if self._build_plate_number != nr:
|
if self._build_plate_number != nr:
|
||||||
self._build_plate_number = nr
|
self._build_plate_number = nr
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def _updateSceneDelayed(self, source):
|
def _updateSceneDelayed(self, source) -> None:
|
||||||
if not isinstance(source, Camera):
|
if not isinstance(source, Camera):
|
||||||
self._update_timer.start()
|
self._update_timer.start()
|
||||||
|
|
||||||
def _updateDelayed(self, *args):
|
def _updateDelayed(self, *args) -> None:
|
||||||
self._update_timer.start()
|
self._update_timer.start()
|
||||||
|
|
||||||
def _update(self, *args):
|
def _update(self, *args) -> None:
|
||||||
nodes = []
|
nodes = []
|
||||||
filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate")
|
filter_current_build_plate = Application.getInstance().getPreferences().getValue("view/filter_current_build_plate")
|
||||||
active_build_plate_number = self._build_plate_number
|
active_build_plate_number = self._build_plate_number
|
||||||
group_nr = 1
|
group_nr = 1
|
||||||
name_count_dict = defaultdict(int)
|
name_count_dict = defaultdict(int) # type: Dict[str, int]
|
||||||
|
|
||||||
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
|
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): # type: ignore
|
||||||
if not isinstance(node, SceneNode):
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
|
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
|
||||||
continue
|
continue
|
||||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
|
||||||
|
parent = node.getParent()
|
||||||
|
if parent and parent.callDecoration("isGroup"):
|
||||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||||
continue
|
continue
|
||||||
|
@ -82,11 +86,11 @@ class ObjectsModel(ListModel):
|
||||||
group_nr += 1
|
group_nr += 1
|
||||||
|
|
||||||
if hasattr(node, "isOutsideBuildArea"):
|
if hasattr(node, "isOutsideBuildArea"):
|
||||||
is_outside_build_area = node.isOutsideBuildArea()
|
is_outside_build_area = node.isOutsideBuildArea() # type: ignore
|
||||||
else:
|
else:
|
||||||
is_outside_build_area = False
|
is_outside_build_area = False
|
||||||
|
|
||||||
#check if we already have an instance of the object based on name
|
# Check if we already have an instance of the object based on name
|
||||||
name_count_dict[name] += 1
|
name_count_dict[name] += 1
|
||||||
name_count = name_count_dict[name]
|
name_count = name_count_dict[name]
|
||||||
|
|
|
@ -5,8 +5,7 @@ import json
|
||||||
import math
|
import math
|
||||||
import os
|
import os
|
||||||
import unicodedata
|
import unicodedata
|
||||||
import re # To create abbreviations for printer names.
|
from typing import Dict, List, Optional, TYPE_CHECKING
|
||||||
from typing import Dict, List, Optional
|
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
||||||
|
|
||||||
|
@ -16,8 +15,6 @@ from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
@ -84,6 +81,7 @@ class PrintInformation(QObject):
|
||||||
"support_interface": catalog.i18nc("@tooltip", "Support Interface"),
|
"support_interface": catalog.i18nc("@tooltip", "Support Interface"),
|
||||||
"support": catalog.i18nc("@tooltip", "Support"),
|
"support": catalog.i18nc("@tooltip", "Support"),
|
||||||
"skirt": catalog.i18nc("@tooltip", "Skirt"),
|
"skirt": catalog.i18nc("@tooltip", "Skirt"),
|
||||||
|
"prime_tower": catalog.i18nc("@tooltip", "Prime Tower"),
|
||||||
"travel": catalog.i18nc("@tooltip", "Travel"),
|
"travel": catalog.i18nc("@tooltip", "Travel"),
|
||||||
"retract": catalog.i18nc("@tooltip", "Retractions"),
|
"retract": catalog.i18nc("@tooltip", "Retractions"),
|
||||||
"none": catalog.i18nc("@tooltip", "Other")
|
"none": catalog.i18nc("@tooltip", "Other")
|
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
|
294
cura/UI/WelcomePagesModel.py
Normal file
294
cura/UI/WelcomePagesModel.py
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from collections import deque
|
||||||
|
import os
|
||||||
|
from typing import TYPE_CHECKING, Optional, List, Dict, Any
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QUrl, Qt, pyqtSlot, pyqtProperty, pyqtSignal
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Qt.ListModel import ListModel
|
||||||
|
from UM.Resources import Resources
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from PyQt5.QtCore import QObject
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# This is the Qt ListModel that contains all welcome pages data. Each page is a page that can be shown as a step in the
|
||||||
|
# welcome wizard dialog. Each item in this ListModel represents a page, which contains the following fields:
|
||||||
|
#
|
||||||
|
# - id : A unique page_id which can be used in function goToPage(page_id)
|
||||||
|
# - page_url : The QUrl to the QML file that contains the content of this page
|
||||||
|
# - next_page_id : (OPTIONAL) The next page ID to go to when this page finished. This is optional. If this is not
|
||||||
|
# provided, it will go to the page with the current index + 1
|
||||||
|
# - next_page_button_text: (OPTIONAL) The text to show for the "next" button, by default it's the translated text of
|
||||||
|
# "Next". Note that each step QML can decide whether to use this text or not, so it's not
|
||||||
|
# mandatory.
|
||||||
|
# - should_show_function : (OPTIONAL) An optional function that returns True/False indicating if this page should be
|
||||||
|
# shown. By default all pages should be shown. If a function returns False, that page will
|
||||||
|
# be skipped and its next page will be shown.
|
||||||
|
#
|
||||||
|
# Note that in any case, a page that has its "should_show_function" == False will ALWAYS be skipped.
|
||||||
|
#
|
||||||
|
class WelcomePagesModel(ListModel):
|
||||||
|
|
||||||
|
IdRole = Qt.UserRole + 1 # Page ID
|
||||||
|
PageUrlRole = Qt.UserRole + 2 # URL to the page's QML file
|
||||||
|
NextPageIdRole = Qt.UserRole + 3 # The next page ID it should go to
|
||||||
|
NextPageButtonTextRole = Qt.UserRole + 4 # The text for the next page button
|
||||||
|
PreviousPageButtonTextRole = Qt.UserRole + 5 # The text for the previous page button
|
||||||
|
|
||||||
|
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
self.addRoleName(self.IdRole, "id")
|
||||||
|
self.addRoleName(self.PageUrlRole, "page_url")
|
||||||
|
self.addRoleName(self.NextPageIdRole, "next_page_id")
|
||||||
|
self.addRoleName(self.NextPageButtonTextRole, "next_page_button_text")
|
||||||
|
self.addRoleName(self.PreviousPageButtonTextRole, "previous_page_button_text")
|
||||||
|
|
||||||
|
self._application = application
|
||||||
|
self._catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
self._default_next_button_text = self._catalog.i18nc("@action:button", "Next")
|
||||||
|
|
||||||
|
self._pages = [] # type: List[Dict[str, Any]]
|
||||||
|
|
||||||
|
self._current_page_index = 0
|
||||||
|
# Store all the previous page indices so it can go back.
|
||||||
|
self._previous_page_indices_stack = deque() # type: deque
|
||||||
|
|
||||||
|
# If the welcome flow should be shown. It can show the complete flow or just the changelog depending on the
|
||||||
|
# specific case. See initialize() for how this variable is set.
|
||||||
|
self._should_show_welcome_flow = False
|
||||||
|
|
||||||
|
allFinished = pyqtSignal() # emitted when all steps have been finished
|
||||||
|
currentPageIndexChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = currentPageIndexChanged)
|
||||||
|
def currentPageIndex(self) -> int:
|
||||||
|
return self._current_page_index
|
||||||
|
|
||||||
|
# Returns a float number in [0, 1] which indicates the current progress.
|
||||||
|
@pyqtProperty(float, notify = currentPageIndexChanged)
|
||||||
|
def currentProgress(self) -> float:
|
||||||
|
if len(self._items) == 0:
|
||||||
|
return 0
|
||||||
|
else:
|
||||||
|
return self._current_page_index / len(self._items)
|
||||||
|
|
||||||
|
# Indicates if the current page is the last page.
|
||||||
|
@pyqtProperty(bool, notify = currentPageIndexChanged)
|
||||||
|
def isCurrentPageLast(self) -> bool:
|
||||||
|
return self._current_page_index == len(self._items) - 1
|
||||||
|
|
||||||
|
def _setCurrentPageIndex(self, page_index: int) -> None:
|
||||||
|
if page_index != self._current_page_index:
|
||||||
|
self._previous_page_indices_stack.append(self._current_page_index)
|
||||||
|
self._current_page_index = page_index
|
||||||
|
self.currentPageIndexChanged.emit()
|
||||||
|
|
||||||
|
# Ends the Welcome-Pages. Put as a separate function for cases like the 'decline' in the User-Agreement.
|
||||||
|
@pyqtSlot()
|
||||||
|
def atEnd(self) -> None:
|
||||||
|
self.allFinished.emit()
|
||||||
|
self.resetState()
|
||||||
|
|
||||||
|
# Goes to the next page.
|
||||||
|
# If "from_index" is given, it will look for the next page to show starting from the "from_index" page instead of
|
||||||
|
# the "self._current_page_index".
|
||||||
|
@pyqtSlot()
|
||||||
|
def goToNextPage(self, from_index: Optional[int] = None) -> None:
|
||||||
|
# Look for the next page that should be shown
|
||||||
|
current_index = self._current_page_index if from_index is None else from_index
|
||||||
|
while True:
|
||||||
|
page_item = self._items[current_index]
|
||||||
|
|
||||||
|
# Check if there's a "next_page_id" assigned. If so, go to that page. Otherwise, go to the page with the
|
||||||
|
# current index + 1.
|
||||||
|
next_page_id = page_item.get("next_page_id")
|
||||||
|
next_page_index = current_index + 1
|
||||||
|
if next_page_id:
|
||||||
|
idx = self.getPageIndexById(next_page_id)
|
||||||
|
if idx is None:
|
||||||
|
# FIXME: If we cannot find the next page, we cannot do anything here.
|
||||||
|
Logger.log("e", "Cannot find page with ID [%s]", next_page_id)
|
||||||
|
return
|
||||||
|
next_page_index = idx
|
||||||
|
|
||||||
|
# If we have reached the last page, emit allFinished signal and reset.
|
||||||
|
if next_page_index == len(self._items):
|
||||||
|
self.atEnd()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Check if the this page should be shown (default yes), if not, keep looking for the next one.
|
||||||
|
next_page_item = self.getItem(next_page_index)
|
||||||
|
if self._shouldPageBeShown(next_page_index):
|
||||||
|
break
|
||||||
|
|
||||||
|
Logger.log("d", "Page [%s] should not be displayed, look for the next page.", next_page_item["id"])
|
||||||
|
current_index = next_page_index
|
||||||
|
|
||||||
|
# Move to the next page
|
||||||
|
self._setCurrentPageIndex(next_page_index)
|
||||||
|
|
||||||
|
# Goes to the previous page. If there's no previous page, do nothing.
|
||||||
|
@pyqtSlot()
|
||||||
|
def goToPreviousPage(self) -> None:
|
||||||
|
if len(self._previous_page_indices_stack) == 0:
|
||||||
|
Logger.log("i", "No previous page, do nothing")
|
||||||
|
return
|
||||||
|
|
||||||
|
previous_page_index = self._previous_page_indices_stack.pop()
|
||||||
|
self._current_page_index = previous_page_index
|
||||||
|
self.currentPageIndexChanged.emit()
|
||||||
|
|
||||||
|
# Sets the current page to the given page ID. If the page ID is not found, do nothing.
|
||||||
|
@pyqtSlot(str)
|
||||||
|
def goToPage(self, page_id: str) -> None:
|
||||||
|
page_index = self.getPageIndexById(page_id)
|
||||||
|
if page_index is None:
|
||||||
|
# FIXME: If we cannot find the next page, we cannot do anything here.
|
||||||
|
Logger.log("e", "Cannot find page with ID [%s], go to the next page by default", page_index)
|
||||||
|
self.goToNextPage()
|
||||||
|
return
|
||||||
|
|
||||||
|
if self._shouldPageBeShown(page_index):
|
||||||
|
# Move to that page if it should be shown
|
||||||
|
self._setCurrentPageIndex(page_index)
|
||||||
|
else:
|
||||||
|
# Find the next page to show starting from the "page_index"
|
||||||
|
self.goToNextPage(from_index = page_index)
|
||||||
|
|
||||||
|
# Checks if the page with the given index should be shown by calling the "should_show_function" associated with it.
|
||||||
|
# If the function is not present, returns True (show page by default).
|
||||||
|
def _shouldPageBeShown(self, page_index: int) -> bool:
|
||||||
|
next_page_item = self.getItem(page_index)
|
||||||
|
should_show_function = next_page_item.get("should_show_function", lambda: True)
|
||||||
|
return should_show_function()
|
||||||
|
|
||||||
|
# Resets the state of the WelcomePagesModel. This functions does the following:
|
||||||
|
# - Resets current_page_index to 0
|
||||||
|
# - Clears the previous page indices stack
|
||||||
|
@pyqtSlot()
|
||||||
|
def resetState(self) -> None:
|
||||||
|
self._current_page_index = 0
|
||||||
|
self._previous_page_indices_stack.clear()
|
||||||
|
|
||||||
|
self.currentPageIndexChanged.emit()
|
||||||
|
|
||||||
|
shouldShowWelcomeFlowChanged = pyqtSignal()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = shouldShowWelcomeFlowChanged)
|
||||||
|
def shouldShowWelcomeFlow(self) -> bool:
|
||||||
|
return self._should_show_welcome_flow
|
||||||
|
|
||||||
|
# Gets the page index with the given page ID. If the page ID doesn't exist, returns None.
|
||||||
|
def getPageIndexById(self, page_id: str) -> Optional[int]:
|
||||||
|
page_idx = None
|
||||||
|
for idx, page_item in enumerate(self._items):
|
||||||
|
if page_item["id"] == page_id:
|
||||||
|
page_idx = idx
|
||||||
|
break
|
||||||
|
return page_idx
|
||||||
|
|
||||||
|
# Convenience function to get QUrl path to pages that's located in "resources/qml/WelcomePages".
|
||||||
|
def _getBuiltinWelcomePagePath(self, page_filename: str) -> "QUrl":
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
return QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles,
|
||||||
|
os.path.join("WelcomePages", page_filename)))
|
||||||
|
|
||||||
|
# FIXME: HACKs for optimization that we don't update the model every time the active machine gets changed.
|
||||||
|
def _onActiveMachineChanged(self) -> None:
|
||||||
|
self._application.getMachineManager().globalContainerChanged.disconnect(self._onActiveMachineChanged)
|
||||||
|
self._initialize(update_should_show_flag = False)
|
||||||
|
|
||||||
|
def initialize(self) -> None:
|
||||||
|
self._application.getMachineManager().globalContainerChanged.connect(self._onActiveMachineChanged)
|
||||||
|
self._initialize()
|
||||||
|
|
||||||
|
def _initialize(self, update_should_show_flag: bool = True) -> None:
|
||||||
|
show_whatsnew_only = False
|
||||||
|
if update_should_show_flag:
|
||||||
|
has_active_machine = self._application.getMachineManager().activeMachine is not None
|
||||||
|
has_app_just_upgraded = self._application.hasJustUpdatedFromOldVersion()
|
||||||
|
|
||||||
|
# Only show the what's new dialog if there's no machine and we have just upgraded
|
||||||
|
show_complete_flow = not has_active_machine
|
||||||
|
show_whatsnew_only = has_active_machine and has_app_just_upgraded
|
||||||
|
|
||||||
|
# FIXME: This is a hack. Because of the circular dependency between MachineManager, ExtruderManager, and
|
||||||
|
# possibly some others, setting the initial active machine is not done when the MachineManager gets initialized.
|
||||||
|
# So at this point, we don't know if there will be an active machine or not. It could be that the active machine
|
||||||
|
# files are corrupted so we cannot rely on Preferences either. This makes sure that once the active machine
|
||||||
|
# gets changed, this model updates the flags, so it can decide whether to show the welcome flow or not.
|
||||||
|
should_show_welcome_flow = show_complete_flow or show_whatsnew_only
|
||||||
|
if should_show_welcome_flow != self._should_show_welcome_flow:
|
||||||
|
self._should_show_welcome_flow = should_show_welcome_flow
|
||||||
|
self.shouldShowWelcomeFlowChanged.emit()
|
||||||
|
|
||||||
|
# All pages
|
||||||
|
all_pages_list = [{"id": "welcome",
|
||||||
|
"page_url": self._getBuiltinWelcomePagePath("WelcomeContent.qml"),
|
||||||
|
},
|
||||||
|
{"id": "user_agreement",
|
||||||
|
"page_url": self._getBuiltinWelcomePagePath("UserAgreementContent.qml"),
|
||||||
|
},
|
||||||
|
{"id": "whats_new",
|
||||||
|
"page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"),
|
||||||
|
},
|
||||||
|
{"id": "data_collections",
|
||||||
|
"page_url": self._getBuiltinWelcomePagePath("DataCollectionsContent.qml"),
|
||||||
|
},
|
||||||
|
{"id": "add_network_or_local_printer",
|
||||||
|
"page_url": self._getBuiltinWelcomePagePath("AddNetworkOrLocalPrinterContent.qml"),
|
||||||
|
"next_page_id": "machine_actions",
|
||||||
|
},
|
||||||
|
{"id": "add_printer_by_ip",
|
||||||
|
"page_url": self._getBuiltinWelcomePagePath("AddPrinterByIpContent.qml"),
|
||||||
|
"next_page_id": "machine_actions",
|
||||||
|
},
|
||||||
|
{"id": "machine_actions",
|
||||||
|
"page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"),
|
||||||
|
"next_page_id": "cloud",
|
||||||
|
"should_show_function": self.shouldShowMachineActions,
|
||||||
|
},
|
||||||
|
{"id": "cloud",
|
||||||
|
"page_url": self._getBuiltinWelcomePagePath("CloudContent.qml"),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
pages_to_show = all_pages_list
|
||||||
|
if show_whatsnew_only:
|
||||||
|
pages_to_show = list(filter(lambda x: x["id"] == "whats_new", all_pages_list))
|
||||||
|
|
||||||
|
self._pages = pages_to_show
|
||||||
|
self.setItems(self._pages)
|
||||||
|
|
||||||
|
# For convenience, inject the default "next" button text to each item if it's not present.
|
||||||
|
def setItems(self, items: List[Dict[str, Any]]) -> None:
|
||||||
|
for item in items:
|
||||||
|
if "next_page_button_text" not in item:
|
||||||
|
item["next_page_button_text"] = self._default_next_button_text
|
||||||
|
|
||||||
|
super().setItems(items)
|
||||||
|
|
||||||
|
# Indicates if the machine action panel should be shown by checking if there's any first start machine actions
|
||||||
|
# available.
|
||||||
|
def shouldShowMachineActions(self) -> bool:
|
||||||
|
global_stack = self._application.getMachineManager().activeMachine
|
||||||
|
if global_stack is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
definition_id = global_stack.definition.getId()
|
||||||
|
first_start_actions = self._application.getMachineActionManager().getFirstStartActions(definition_id)
|
||||||
|
return len([action for action in first_start_actions if action.needsUserInteraction()]) > 0
|
||||||
|
|
||||||
|
def addPage(self) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["WelcomePagesModel"]
|
22
cura/UI/WhatsNewPagesModel.py
Normal file
22
cura/UI/WhatsNewPagesModel.py
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
# 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 showing the
|
||||||
|
# "what's new" page. This is also used in the "Help" menu to show the changes log.
|
||||||
|
#
|
||||||
|
class WhatsNewPagesModel(WelcomePagesModel):
|
||||||
|
|
||||||
|
def initialize(self) -> None:
|
||||||
|
self._pages = []
|
||||||
|
self._pages.append({"id": "whats_new",
|
||||||
|
"page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"),
|
||||||
|
"next_page_button_text": self._catalog.i18nc("@action:button", "Close"),
|
||||||
|
})
|
||||||
|
self.setItems(self._pages)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["WhatsNewPagesModel"]
|
0
cura/UI/__init__.py
Normal file
0
cura/UI/__init__.py
Normal file
44
cura/Utils/NetworkingUtil.py
Normal file
44
cura/Utils/NetworkingUtil.py
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import socket
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSlot
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# This is a QObject because some of the functions can be used (and are useful) in QML.
|
||||||
|
#
|
||||||
|
class NetworkingUtil(QObject):
|
||||||
|
|
||||||
|
def __init__(self, parent: Optional["QObject"] = None) -> None:
|
||||||
|
super().__init__(parent = parent)
|
||||||
|
|
||||||
|
# Checks if the given string is a valid IPv4 address.
|
||||||
|
@pyqtSlot(str, result = bool)
|
||||||
|
def isIPv4(self, address: str) -> bool:
|
||||||
|
try:
|
||||||
|
socket.inet_pton(socket.AF_INET, address)
|
||||||
|
result = True
|
||||||
|
except:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Checks if the given string is a valid IPv6 address.
|
||||||
|
@pyqtSlot(str, result = bool)
|
||||||
|
def isIPv6(self, address: str) -> bool:
|
||||||
|
try:
|
||||||
|
socket.inet_pton(socket.AF_INET6, address)
|
||||||
|
result = True
|
||||||
|
except:
|
||||||
|
result = False
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Checks if the given string is a valid IPv4 or IPv6 address.
|
||||||
|
@pyqtSlot(str, result = bool)
|
||||||
|
def isValidIP(self, address: str) -> bool:
|
||||||
|
return self.isIPv4(address) or self.isIPv6(address)
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ["NetworkingUtil"]
|
|
@ -23,7 +23,10 @@ known_args = vars(parser.parse_known_args()[0])
|
||||||
if not known_args["debug"]:
|
if not known_args["debug"]:
|
||||||
def get_cura_dir_path():
|
def get_cura_dir_path():
|
||||||
if Platform.isWindows():
|
if Platform.isWindows():
|
||||||
return os.path.expanduser("~/AppData/Roaming/" + CuraAppName)
|
appdata_path = os.getenv("APPDATA")
|
||||||
|
if not appdata_path: #Defensive against the environment variable missing (should never happen).
|
||||||
|
appdata_path = "."
|
||||||
|
return os.path.join(appdata_path, CuraAppName)
|
||||||
elif Platform.isLinux():
|
elif Platform.isLinux():
|
||||||
return os.path.expanduser("~/.local/share/" + CuraAppName)
|
return os.path.expanduser("~/.local/share/" + CuraAppName)
|
||||||
elif Platform.isOSX():
|
elif Platform.isOSX():
|
||||||
|
|
43
docker/build.sh
Executable file
43
docker/build.sh
Executable file
|
@ -0,0 +1,43 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
# Abort at the first error.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||||
|
PROJECT_DIR="$( cd "${SCRIPT_DIR}/.." && pwd )"
|
||||||
|
|
||||||
|
# Make sure that environment variables are set properly
|
||||||
|
source /opt/rh/devtoolset-7/enable
|
||||||
|
export PATH="${CURA_BUILD_ENV_PATH}/bin:${PATH}"
|
||||||
|
export PKG_CONFIG_PATH="${CURA_BUILD_ENV_PATH}/lib/pkgconfig:${PKG_CONFIG_PATH}"
|
||||||
|
|
||||||
|
cd "${PROJECT_DIR}"
|
||||||
|
|
||||||
|
#
|
||||||
|
# Clone Uranium and set PYTHONPATH first
|
||||||
|
#
|
||||||
|
|
||||||
|
# Check the branch to use:
|
||||||
|
# 1. Use the Uranium branch with the branch same if it exists.
|
||||||
|
# 2. Otherwise, use the default branch name "master"
|
||||||
|
URANIUM_BRANCH="${CI_COMMIT_REF_NAME:-master}"
|
||||||
|
output="$(git ls-remote --heads https://github.com/Ultimaker/Uranium.git "${URANIUM_BRANCH}")"
|
||||||
|
if [ -z "${output}" ]; then
|
||||||
|
echo "Could not find Uranium banch ${URANIUM_BRANCH}, fallback to use master."
|
||||||
|
URANIUM_BRANCH="master"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Using Uranium branch ${URANIUM_BRANCH} ..."
|
||||||
|
git clone --depth=1 -b "${URANIUM_BRANCH}" https://github.com/Ultimaker/Uranium.git "${PROJECT_DIR}"/Uranium
|
||||||
|
export PYTHONPATH="${PROJECT_DIR}/Uranium:.:${PYTHONPATH}"
|
||||||
|
|
||||||
|
mkdir build
|
||||||
|
cd build
|
||||||
|
cmake3 \
|
||||||
|
-DCMAKE_BUILD_TYPE=Debug \
|
||||||
|
-DCMAKE_PREFIX_PATH="${CURA_BUILD_ENV_PATH}" \
|
||||||
|
-DURANIUM_DIR="${PROJECT_DIR}/Uranium" \
|
||||||
|
-DBUILD_TESTS=ON \
|
||||||
|
..
|
||||||
|
make
|
||||||
|
ctest3 --output-on-failure -T Test
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Optional
|
from typing import List, Optional, Union, TYPE_CHECKING
|
||||||
import os.path
|
import os.path
|
||||||
import zipfile
|
import zipfile
|
||||||
|
|
||||||
|
@ -9,15 +9,16 @@ import numpy
|
||||||
|
|
||||||
import Savitar
|
import Savitar
|
||||||
|
|
||||||
from UM.Application import Application
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Math.Matrix import Matrix
|
from UM.Math.Matrix import Matrix
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||||
from UM.Mesh.MeshReader import MeshReader
|
from UM.Mesh.MeshReader import MeshReader
|
||||||
from UM.Scene.GroupDecorator import GroupDecorator
|
from UM.Scene.GroupDecorator import GroupDecorator
|
||||||
|
from UM.Scene.SceneNode import SceneNode #For typing.
|
||||||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
||||||
|
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||||
|
@ -25,11 +26,9 @@ from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||||
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
|
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
|
||||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||||
|
|
||||||
MYPY = False
|
|
||||||
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if not MYPY:
|
if not TYPE_CHECKING:
|
||||||
import xml.etree.cElementTree as ET
|
import xml.etree.cElementTree as ET
|
||||||
except ImportError:
|
except ImportError:
|
||||||
Logger.log("w", "Unable to load cElementTree, switching to slower version")
|
Logger.log("w", "Unable to load cElementTree, switching to slower version")
|
||||||
|
@ -55,7 +54,7 @@ class ThreeMFReader(MeshReader):
|
||||||
self._unit = None
|
self._unit = None
|
||||||
self._object_count = 0 # Used to name objects as there is no node name yet.
|
self._object_count = 0 # Used to name objects as there is no node name yet.
|
||||||
|
|
||||||
def _createMatrixFromTransformationString(self, transformation):
|
def _createMatrixFromTransformationString(self, transformation: str) -> Matrix:
|
||||||
if transformation == "":
|
if transformation == "":
|
||||||
return Matrix()
|
return Matrix()
|
||||||
|
|
||||||
|
@ -85,13 +84,13 @@ class ThreeMFReader(MeshReader):
|
||||||
|
|
||||||
return temp_mat
|
return temp_mat
|
||||||
|
|
||||||
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a Uranium scene node.
|
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
|
||||||
# \returns Uranium scene node.
|
# \returns Scene node.
|
||||||
def _convertSavitarNodeToUMNode(self, savitar_node):
|
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode) -> Optional[SceneNode]:
|
||||||
self._object_count += 1
|
self._object_count += 1
|
||||||
node_name = "Object %s" % self._object_count
|
node_name = "Object %s" % self._object_count
|
||||||
|
|
||||||
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||||
|
|
||||||
um_node = CuraSceneNode() # This adds a SettingOverrideDecorator
|
um_node = CuraSceneNode() # This adds a SettingOverrideDecorator
|
||||||
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
|
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
|
||||||
|
@ -122,7 +121,7 @@ class ThreeMFReader(MeshReader):
|
||||||
|
|
||||||
# Add the setting override decorator, so we can add settings to this node.
|
# Add the setting override decorator, so we can add settings to this node.
|
||||||
if settings:
|
if settings:
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
|
|
||||||
# Ensure the correct next container for the SettingOverride decorator is set.
|
# Ensure the correct next container for the SettingOverride decorator is set.
|
||||||
if global_container_stack:
|
if global_container_stack:
|
||||||
|
@ -161,7 +160,7 @@ class ThreeMFReader(MeshReader):
|
||||||
um_node.addDecorator(sliceable_decorator)
|
um_node.addDecorator(sliceable_decorator)
|
||||||
return um_node
|
return um_node
|
||||||
|
|
||||||
def _read(self, file_name):
|
def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]:
|
||||||
result = []
|
result = []
|
||||||
self._object_count = 0 # Used to name objects as there is no node name yet.
|
self._object_count = 0 # Used to name objects as there is no node name yet.
|
||||||
# The base object of 3mf is a zipped archive.
|
# The base object of 3mf is a zipped archive.
|
||||||
|
@ -181,12 +180,13 @@ class ThreeMFReader(MeshReader):
|
||||||
mesh_data = um_node.getMeshData()
|
mesh_data = um_node.getMeshData()
|
||||||
if mesh_data is not None:
|
if mesh_data is not None:
|
||||||
extents = mesh_data.getExtents()
|
extents = mesh_data.getExtents()
|
||||||
|
if extents is not None:
|
||||||
center_vector = Vector(extents.center.x, extents.center.y, extents.center.z)
|
center_vector = Vector(extents.center.x, extents.center.y, extents.center.z)
|
||||||
transform_matrix.setByTranslation(center_vector)
|
transform_matrix.setByTranslation(center_vector)
|
||||||
transform_matrix.multiply(um_node.getLocalTransformation())
|
transform_matrix.multiply(um_node.getLocalTransformation())
|
||||||
um_node.setTransformation(transform_matrix)
|
um_node.setTransformation(transform_matrix)
|
||||||
|
|
||||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
|
|
||||||
# Create a transformation Matrix to convert from 3mf worldspace into ours.
|
# Create a transformation Matrix to convert from 3mf worldspace into ours.
|
||||||
# First step: flip the y and z axis.
|
# First step: flip the y and z axis.
|
||||||
|
@ -215,8 +215,11 @@ class ThreeMFReader(MeshReader):
|
||||||
um_node.setTransformation(um_node.getLocalTransformation().preMultiply(transformation_matrix))
|
um_node.setTransformation(um_node.getLocalTransformation().preMultiply(transformation_matrix))
|
||||||
|
|
||||||
# Check if the model is positioned below the build plate and honor that when loading project files.
|
# Check if the model is positioned below the build plate and honor that when loading project files.
|
||||||
if um_node.getMeshData() is not None:
|
node_meshdata = um_node.getMeshData()
|
||||||
minimum_z_value = um_node.getMeshData().getExtents(um_node.getWorldTransformation()).minimum.y # y is z in transformation coordinates
|
if node_meshdata is not None:
|
||||||
|
aabb = node_meshdata.getExtents(um_node.getWorldTransformation())
|
||||||
|
if aabb is not None:
|
||||||
|
minimum_z_value = aabb.minimum.y # y is z in transformation coordinates
|
||||||
if minimum_z_value < 0:
|
if minimum_z_value < 0:
|
||||||
um_node.addDecorator(ZOffsetDecorator())
|
um_node.addDecorator(ZOffsetDecorator())
|
||||||
um_node.callDecoration("setZOffset", minimum_z_value)
|
um_node.callDecoration("setZOffset", minimum_z_value)
|
||||||
|
@ -225,7 +228,7 @@ class ThreeMFReader(MeshReader):
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
Logger.logException("e", "An exception occurred in 3mf reader.")
|
Logger.logException("e", "An exception occurred in 3mf reader.")
|
||||||
return None
|
return []
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
|
@ -26,6 +26,7 @@ from UM.Preferences import Preferences
|
||||||
|
|
||||||
from cura.Machines.VariantType import VariantType
|
from cura.Machines.VariantType import VariantType
|
||||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||||
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
from cura.Settings.ExtruderStack import ExtruderStack
|
from cura.Settings.ExtruderStack import ExtruderStack
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
from cura.Settings.CuraContainerStack import _ContainerIndexes
|
from cura.Settings.CuraContainerStack import _ContainerIndexes
|
||||||
|
@ -258,7 +259,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
|
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
|
||||||
quality_name = ""
|
quality_name = ""
|
||||||
custom_quality_name = ""
|
custom_quality_name = ""
|
||||||
num_settings_overriden_by_quality_changes = 0 # How many settings are changed by the quality changes
|
num_settings_overridden_by_quality_changes = 0 # How many settings are changed by the quality changes
|
||||||
num_user_settings = 0
|
num_user_settings = 0
|
||||||
quality_changes_conflict = False
|
quality_changes_conflict = False
|
||||||
|
|
||||||
|
@ -296,7 +297,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
|
|
||||||
custom_quality_name = parser["general"]["name"]
|
custom_quality_name = parser["general"]["name"]
|
||||||
values = parser["values"] if parser.has_section("values") else dict()
|
values = parser["values"] if parser.has_section("values") else dict()
|
||||||
num_settings_overriden_by_quality_changes += len(values)
|
num_settings_overridden_by_quality_changes += len(values)
|
||||||
# Check if quality changes already exists.
|
# Check if quality changes already exists.
|
||||||
quality_changes = self._container_registry.findInstanceContainers(name = custom_quality_name,
|
quality_changes = self._container_registry.findInstanceContainers(name = custom_quality_name,
|
||||||
type = "quality_changes")
|
type = "quality_changes")
|
||||||
|
@ -514,7 +515,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
self._dialog.setNumVisibleSettings(num_visible_settings)
|
self._dialog.setNumVisibleSettings(num_visible_settings)
|
||||||
self._dialog.setQualityName(quality_name)
|
self._dialog.setQualityName(quality_name)
|
||||||
self._dialog.setQualityType(quality_type)
|
self._dialog.setQualityType(quality_type)
|
||||||
self._dialog.setNumSettingsOverridenByQualityChanges(num_settings_overriden_by_quality_changes)
|
self._dialog.setNumSettingsOverriddenByQualityChanges(num_settings_overridden_by_quality_changes)
|
||||||
self._dialog.setNumUserSettings(num_user_settings)
|
self._dialog.setNumUserSettings(num_user_settings)
|
||||||
self._dialog.setActiveMode(active_mode)
|
self._dialog.setActiveMode(active_mode)
|
||||||
self._dialog.setMachineName(machine_name)
|
self._dialog.setMachineName(machine_name)
|
||||||
|
@ -781,6 +782,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
if not quality_changes_info.extruder_info_dict:
|
if not quality_changes_info.extruder_info_dict:
|
||||||
container_info = ContainerInfo(None, None, None)
|
container_info = ContainerInfo(None, None, None)
|
||||||
quality_changes_info.extruder_info_dict["0"] = container_info
|
quality_changes_info.extruder_info_dict["0"] = container_info
|
||||||
|
# If the global stack we're "targeting" has never been active, but was updated from Cura 3.4,
|
||||||
|
# it might not have it's extruders set properly.
|
||||||
|
if not global_stack.extruders:
|
||||||
|
ExtruderManager.getInstance().fixSingleExtrusionMachineExtruderDefinition(global_stack)
|
||||||
extruder_stack = global_stack.extruders["0"]
|
extruder_stack = global_stack.extruders["0"]
|
||||||
|
|
||||||
container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name,
|
container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name,
|
||||||
|
@ -815,6 +820,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name,
|
container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name,
|
||||||
global_stack, extruder_stack)
|
global_stack, extruder_stack)
|
||||||
container_info.container = container
|
container_info.container = container
|
||||||
|
container.setDirty(True)
|
||||||
|
self._container_registry.addContainer(container)
|
||||||
|
|
||||||
for key, value in container_info.parser["values"].items():
|
for key, value in container_info.parser["values"].items():
|
||||||
container_info.container.setProperty(key, "value", value)
|
container_info.container.setProperty(key, "value", value)
|
||||||
|
|
|
@ -41,7 +41,7 @@ class WorkspaceDialog(QObject):
|
||||||
self._num_user_settings = 0
|
self._num_user_settings = 0
|
||||||
self._active_mode = ""
|
self._active_mode = ""
|
||||||
self._quality_name = ""
|
self._quality_name = ""
|
||||||
self._num_settings_overriden_by_quality_changes = 0
|
self._num_settings_overridden_by_quality_changes = 0
|
||||||
self._quality_type = ""
|
self._quality_type = ""
|
||||||
self._machine_name = ""
|
self._machine_name = ""
|
||||||
self._machine_type = ""
|
self._machine_type = ""
|
||||||
|
@ -151,10 +151,10 @@ class WorkspaceDialog(QObject):
|
||||||
|
|
||||||
@pyqtProperty(int, notify=numSettingsOverridenByQualityChangesChanged)
|
@pyqtProperty(int, notify=numSettingsOverridenByQualityChangesChanged)
|
||||||
def numSettingsOverridenByQualityChanges(self):
|
def numSettingsOverridenByQualityChanges(self):
|
||||||
return self._num_settings_overriden_by_quality_changes
|
return self._num_settings_overridden_by_quality_changes
|
||||||
|
|
||||||
def setNumSettingsOverridenByQualityChanges(self, num_settings_overriden_by_quality_changes):
|
def setNumSettingsOverriddenByQualityChanges(self, num_settings_overridden_by_quality_changes):
|
||||||
self._num_settings_overriden_by_quality_changes = num_settings_overriden_by_quality_changes
|
self._num_settings_overridden_by_quality_changes = num_settings_overridden_by_quality_changes
|
||||||
self.numSettingsOverridenByQualityChangesChanged.emit()
|
self.numSettingsOverridenByQualityChangesChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(str, notify=qualityNameChanged)
|
@pyqtProperty(str, notify=qualityNameChanged)
|
||||||
|
|
173
plugins/AMFReader/AMFReader.py
Normal file
173
plugins/AMFReader/AMFReader.py
Normal file
|
@ -0,0 +1,173 @@
|
||||||
|
# Copyright (c) 2019 fieldOfView
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
# This AMF parser is based on the AMF parser in legacy cura:
|
||||||
|
# https://github.com/daid/LegacyCura/blob/ad7641e059048c7dcb25da1f47c0a7e95e7f4f7c/Cura/util/meshLoaders/amf.py
|
||||||
|
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
from UM.Mesh.MeshData import MeshData, calculateNormalsFromIndexedVertices
|
||||||
|
from UM.Mesh.MeshReader import MeshReader
|
||||||
|
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
|
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||||
|
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||||
|
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
||||||
|
from UM.Scene.GroupDecorator import GroupDecorator
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
import trimesh
|
||||||
|
import os.path
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
MYPY = False
|
||||||
|
try:
|
||||||
|
if not MYPY:
|
||||||
|
import xml.etree.cElementTree as ET
|
||||||
|
except ImportError:
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
|
||||||
|
class AMFReader(MeshReader):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._supported_extensions = [".amf"]
|
||||||
|
self._namespaces = {} # type: Dict[str, str]
|
||||||
|
|
||||||
|
MimeTypeDatabase.addMimeType(
|
||||||
|
MimeType(
|
||||||
|
name="application/x-amf",
|
||||||
|
comment="AMF",
|
||||||
|
suffixes=["amf"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Main entry point
|
||||||
|
# Reads the file, returns a SceneNode (possibly with nested ones), or None
|
||||||
|
def _read(self, file_name):
|
||||||
|
base_name = os.path.basename(file_name)
|
||||||
|
try:
|
||||||
|
zipped_file = zipfile.ZipFile(file_name)
|
||||||
|
xml_document = zipped_file.read(zipped_file.namelist()[0])
|
||||||
|
zipped_file.close()
|
||||||
|
except zipfile.BadZipfile:
|
||||||
|
raw_file = open(file_name, "r")
|
||||||
|
xml_document = raw_file.read()
|
||||||
|
raw_file.close()
|
||||||
|
|
||||||
|
try:
|
||||||
|
amf_document = ET.fromstring(xml_document)
|
||||||
|
except ET.ParseError:
|
||||||
|
Logger.log("e", "Could not parse XML in file %s" % base_name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if "unit" in amf_document.attrib:
|
||||||
|
unit = amf_document.attrib["unit"].lower()
|
||||||
|
else:
|
||||||
|
unit = "millimeter"
|
||||||
|
if unit == "millimeter":
|
||||||
|
scale = 1.0
|
||||||
|
elif unit == "meter":
|
||||||
|
scale = 1000.0
|
||||||
|
elif unit == "inch":
|
||||||
|
scale = 25.4
|
||||||
|
elif unit == "feet":
|
||||||
|
scale = 304.8
|
||||||
|
elif unit == "micron":
|
||||||
|
scale = 0.001
|
||||||
|
else:
|
||||||
|
Logger.log("w", "Unknown unit in amf: %s. Using mm instead." % unit)
|
||||||
|
scale = 1.0
|
||||||
|
|
||||||
|
nodes = []
|
||||||
|
for amf_object in amf_document.iter("object"):
|
||||||
|
for amf_mesh in amf_object.iter("mesh"):
|
||||||
|
amf_mesh_vertices = []
|
||||||
|
for vertices in amf_mesh.iter("vertices"):
|
||||||
|
for vertex in vertices.iter("vertex"):
|
||||||
|
for coordinates in vertex.iter("coordinates"):
|
||||||
|
v = [0.0, 0.0, 0.0]
|
||||||
|
for t in coordinates:
|
||||||
|
if t.tag == "x":
|
||||||
|
v[0] = float(t.text) * scale
|
||||||
|
elif t.tag == "y":
|
||||||
|
v[2] = float(t.text) * scale
|
||||||
|
elif t.tag == "z":
|
||||||
|
v[1] = float(t.text) * scale
|
||||||
|
amf_mesh_vertices.append(v)
|
||||||
|
if not amf_mesh_vertices:
|
||||||
|
continue
|
||||||
|
|
||||||
|
indices = []
|
||||||
|
for volume in amf_mesh.iter("volume"):
|
||||||
|
for triangle in volume.iter("triangle"):
|
||||||
|
f = [0, 0, 0]
|
||||||
|
for t in triangle:
|
||||||
|
if t.tag == "v1":
|
||||||
|
f[0] = int(t.text)
|
||||||
|
elif t.tag == "v2":
|
||||||
|
f[1] = int(t.text)
|
||||||
|
elif t.tag == "v3":
|
||||||
|
f[2] = int(t.text)
|
||||||
|
indices.append(f)
|
||||||
|
|
||||||
|
mesh = trimesh.base.Trimesh(vertices=numpy.array(amf_mesh_vertices, dtype=numpy.float32), faces=numpy.array(indices, dtype=numpy.int32))
|
||||||
|
mesh.merge_vertices()
|
||||||
|
mesh.remove_unreferenced_vertices()
|
||||||
|
mesh.fix_normals()
|
||||||
|
mesh_data = self._toMeshData(mesh)
|
||||||
|
|
||||||
|
new_node = CuraSceneNode()
|
||||||
|
new_node.setSelectable(True)
|
||||||
|
new_node.setMeshData(mesh_data)
|
||||||
|
new_node.setName(base_name if len(nodes)==0 else "%s %d" % (base_name, len(nodes)))
|
||||||
|
new_node.addDecorator(BuildPlateDecorator(CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate))
|
||||||
|
new_node.addDecorator(SliceableObjectDecorator())
|
||||||
|
|
||||||
|
nodes.append(new_node)
|
||||||
|
|
||||||
|
if not nodes:
|
||||||
|
Logger.log("e", "No meshes in file %s" % base_name)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if len(nodes) == 1:
|
||||||
|
return nodes[0]
|
||||||
|
|
||||||
|
# Add all scenenodes to a group so they stay together
|
||||||
|
group_node = CuraSceneNode()
|
||||||
|
group_node.addDecorator(GroupDecorator())
|
||||||
|
group_node.addDecorator(ConvexHullDecorator())
|
||||||
|
group_node.addDecorator(BuildPlateDecorator(CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate))
|
||||||
|
|
||||||
|
for node in nodes:
|
||||||
|
node.setParent(group_node)
|
||||||
|
|
||||||
|
return group_node
|
||||||
|
|
||||||
|
def _toMeshData(self, tri_node: trimesh.base.Trimesh) -> MeshData:
|
||||||
|
tri_faces = tri_node.faces
|
||||||
|
tri_vertices = tri_node.vertices
|
||||||
|
|
||||||
|
indices = []
|
||||||
|
vertices = []
|
||||||
|
|
||||||
|
index_count = 0
|
||||||
|
face_count = 0
|
||||||
|
for tri_face in tri_faces:
|
||||||
|
face = []
|
||||||
|
for tri_index in tri_face:
|
||||||
|
vertices.append(tri_vertices[tri_index])
|
||||||
|
face.append(index_count)
|
||||||
|
index_count += 1
|
||||||
|
indices.append(face)
|
||||||
|
face_count += 1
|
||||||
|
|
||||||
|
vertices = numpy.asarray(vertices, dtype=numpy.float32)
|
||||||
|
indices = numpy.asarray(indices, dtype=numpy.int32)
|
||||||
|
normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count)
|
||||||
|
|
||||||
|
mesh_data = MeshData(vertices=vertices, indices=indices, normals=normals)
|
||||||
|
return mesh_data
|
21
plugins/AMFReader/__init__.py
Normal file
21
plugins/AMFReader/__init__.py
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
# Copyright (c) 2019 fieldOfView
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from . import AMFReader
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
i18n_catalog = i18nCatalog("uranium")
|
||||||
|
|
||||||
|
|
||||||
|
def getMetaData():
|
||||||
|
return {
|
||||||
|
"mesh_reader": [
|
||||||
|
{
|
||||||
|
"extension": "amf",
|
||||||
|
"description": i18n_catalog.i18nc("@item:inlistbox", "AMF File")
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
return {"mesh_reader": AMFReader.AMFReader()}
|
7
plugins/AMFReader/plugin.json
Normal file
7
plugins/AMFReader/plugin.json
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"name": "AMF Reader",
|
||||||
|
"author": "fieldOfView",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "Provides support for reading AMF files.",
|
||||||
|
"api": "6.0.0"
|
||||||
|
}
|
|
@ -1,109 +0,0 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
from UM.Extension import Extension
|
|
||||||
from UM.Application import Application
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
|
||||||
from UM.Version import Version
|
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtSlot, QObject
|
|
||||||
|
|
||||||
import os.path
|
|
||||||
import collections
|
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
|
||||||
|
|
||||||
class ChangeLog(Extension, QObject,):
|
|
||||||
def __init__(self, parent = None):
|
|
||||||
QObject.__init__(self, parent)
|
|
||||||
Extension.__init__(self)
|
|
||||||
self._changelog_window = None
|
|
||||||
self._changelog_context = None
|
|
||||||
version_string = Application.getInstance().getVersion()
|
|
||||||
if version_string is not "master":
|
|
||||||
self._current_app_version = Version(version_string)
|
|
||||||
else:
|
|
||||||
self._current_app_version = None
|
|
||||||
|
|
||||||
self._change_logs = None
|
|
||||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
|
||||||
Application.getInstance().getPreferences().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium
|
|
||||||
self.setMenuName(catalog.i18nc("@item:inmenu", "Changelog"))
|
|
||||||
self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog)
|
|
||||||
|
|
||||||
def getChangeLogs(self):
|
|
||||||
if not self._change_logs:
|
|
||||||
self.loadChangeLogs()
|
|
||||||
return self._change_logs
|
|
||||||
|
|
||||||
@pyqtSlot(result = str)
|
|
||||||
def getChangeLogString(self):
|
|
||||||
logs = self.getChangeLogs()
|
|
||||||
result = ""
|
|
||||||
for version in logs:
|
|
||||||
result += "<h1>" + str(version) + "</h1><br>"
|
|
||||||
result += ""
|
|
||||||
for change in logs[version]:
|
|
||||||
if str(change) != "":
|
|
||||||
result += "<b>" + str(change) + "</b><br>"
|
|
||||||
for line in logs[version][change]:
|
|
||||||
result += str(line) + "<br>"
|
|
||||||
result += "<br>"
|
|
||||||
|
|
||||||
pass
|
|
||||||
return result
|
|
||||||
|
|
||||||
def loadChangeLogs(self):
|
|
||||||
self._change_logs = collections.OrderedDict()
|
|
||||||
with open(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.txt"), "r", encoding = "utf-8") as f:
|
|
||||||
open_version = None
|
|
||||||
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)
|
|
||||||
open_header = ""
|
|
||||||
self._change_logs[open_version] = collections.OrderedDict()
|
|
||||||
elif line.startswith("*"):
|
|
||||||
open_header = line.replace("*","")
|
|
||||||
self._change_logs[open_version][open_header] = []
|
|
||||||
elif line != "":
|
|
||||||
if open_header not in self._change_logs[open_version]:
|
|
||||||
self._change_logs[open_version][open_header] = []
|
|
||||||
self._change_logs[open_version][open_header].append(line)
|
|
||||||
|
|
||||||
def _onEngineCreated(self):
|
|
||||||
if not self._current_app_version:
|
|
||||||
return #We're on dev branch.
|
|
||||||
|
|
||||||
if Application.getInstance().getPreferences().getValue("general/latest_version_changelog_shown") == "master":
|
|
||||||
latest_version_shown = Version("0.0.0")
|
|
||||||
else:
|
|
||||||
latest_version_shown = Version(Application.getInstance().getPreferences().getValue("general/latest_version_changelog_shown"))
|
|
||||||
|
|
||||||
Application.getInstance().getPreferences().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion())
|
|
||||||
|
|
||||||
# Do not show the changelog when there is no global container stack
|
|
||||||
# This implies we are running Cura for the first time.
|
|
||||||
if not Application.getInstance().getGlobalContainerStack():
|
|
||||||
return
|
|
||||||
|
|
||||||
if self._current_app_version > latest_version_shown:
|
|
||||||
self.showChangelog()
|
|
||||||
|
|
||||||
def showChangelog(self):
|
|
||||||
if not self._changelog_window:
|
|
||||||
self.createChangelogWindow()
|
|
||||||
|
|
||||||
self._changelog_window.show()
|
|
||||||
|
|
||||||
def hideChangelog(self):
|
|
||||||
if self._changelog_window:
|
|
||||||
self._changelog_window.hide()
|
|
||||||
|
|
||||||
def createChangelogWindow(self):
|
|
||||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.qml")
|
|
||||||
self._changelog_window = Application.getInstance().createQmlComponent(path, {"manager": self})
|
|
|
@ -1,41 +0,0 @@
|
||||||
// Copyright (c) 2015 Ultimaker B.V.
|
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import QtQuick 2.1
|
|
||||||
import QtQuick.Controls 1.3
|
|
||||||
import QtQuick.Layouts 1.1
|
|
||||||
import QtQuick.Window 2.1
|
|
||||||
|
|
||||||
import UM 1.1 as UM
|
|
||||||
|
|
||||||
UM.Dialog
|
|
||||||
{
|
|
||||||
id: base
|
|
||||||
minimumWidth: (UM.Theme.getSize("modal_window_minimum").width * 0.75) | 0
|
|
||||||
minimumHeight: (UM.Theme.getSize("modal_window_minimum").height * 0.75) | 0
|
|
||||||
width: minimumWidth
|
|
||||||
height: minimumHeight
|
|
||||||
title: catalog.i18nc("@label", "Changelog")
|
|
||||||
|
|
||||||
TextArea
|
|
||||||
{
|
|
||||||
anchors.fill: parent
|
|
||||||
text: manager.getChangeLogString()
|
|
||||||
readOnly: true;
|
|
||||||
textFormat: TextEdit.RichText
|
|
||||||
}
|
|
||||||
|
|
||||||
rightButtons: [
|
|
||||||
Button
|
|
||||||
{
|
|
||||||
UM.I18nCatalog
|
|
||||||
{
|
|
||||||
id: catalog
|
|
||||||
name: "cura"
|
|
||||||
}
|
|
||||||
|
|
||||||
text: catalog.i18nc("@action:button", "Close")
|
|
||||||
onClicked: base.hide()
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
from . import ChangeLog
|
|
||||||
|
|
||||||
|
|
||||||
def getMetaData():
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def register(app):
|
|
||||||
return {"extension": ChangeLog.ChangeLog()}
|
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Changelog",
|
|
||||||
"author": "Ultimaker B.V.",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"description": "Shows changes since latest checked version.",
|
|
||||||
"api": "6.0",
|
|
||||||
"i18n-catalog": "cura"
|
|
||||||
}
|
|
|
@ -45,7 +45,7 @@ class DriveApiService:
|
||||||
"Authorization": "Bearer {}".format(access_token)
|
"Authorization": "Bearer {}".format(access_token)
|
||||||
})
|
})
|
||||||
except requests.exceptions.ConnectionError:
|
except requests.exceptions.ConnectionError:
|
||||||
Logger.log("w", "Unable to connect with the server.")
|
Logger.logException("w", "Unable to connect with the server.")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# HTTP status 300s mean redirection. 400s and 500s are errors.
|
# HTTP status 300s mean redirection. 400s and 500s are errors.
|
||||||
|
@ -98,7 +98,12 @@ class DriveApiService:
|
||||||
# If there is no download URL, we can't restore the backup.
|
# If there is no download URL, we can't restore the backup.
|
||||||
return self._emitRestoreError()
|
return self._emitRestoreError()
|
||||||
|
|
||||||
|
try:
|
||||||
download_package = requests.get(download_url, stream = True)
|
download_package = requests.get(download_url, stream = True)
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
Logger.logException("e", "Unable to connect with the server")
|
||||||
|
return self._emitRestoreError()
|
||||||
|
|
||||||
if download_package.status_code >= 300:
|
if download_package.status_code >= 300:
|
||||||
# Something went wrong when attempting to download the backup.
|
# Something went wrong when attempting to download the backup.
|
||||||
Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text)
|
Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text)
|
||||||
|
@ -142,9 +147,14 @@ class DriveApiService:
|
||||||
Logger.log("w", "Could not get access token.")
|
Logger.log("w", "Could not get access token.")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
|
delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
|
||||||
"Authorization": "Bearer {}".format(access_token)
|
"Authorization": "Bearer {}".format(access_token)
|
||||||
})
|
})
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
Logger.logException("e", "Unable to connect with the server")
|
||||||
|
return False
|
||||||
|
|
||||||
if delete_backup.status_code >= 300:
|
if delete_backup.status_code >= 300:
|
||||||
Logger.log("w", "Could not delete backup: %s", delete_backup.text)
|
Logger.log("w", "Could not delete backup: %s", delete_backup.text)
|
||||||
return False
|
return False
|
||||||
|
@ -159,15 +169,19 @@ class DriveApiService:
|
||||||
if not access_token:
|
if not access_token:
|
||||||
Logger.log("w", "Could not get access token.")
|
Logger.log("w", "Could not get access token.")
|
||||||
return None
|
return None
|
||||||
|
try:
|
||||||
backup_upload_request = requests.put(self.BACKUP_URL, json = {
|
backup_upload_request = requests.put(
|
||||||
"data": {
|
self.BACKUP_URL,
|
||||||
"backup_size": backup_size,
|
json = {"data": {"backup_size": backup_size,
|
||||||
"metadata": backup_metadata
|
"metadata": backup_metadata
|
||||||
}
|
}
|
||||||
}, headers = {
|
},
|
||||||
|
headers = {
|
||||||
"Authorization": "Bearer {}".format(access_token)
|
"Authorization": "Bearer {}".format(access_token)
|
||||||
})
|
})
|
||||||
|
except requests.exceptions.ConnectionError:
|
||||||
|
Logger.logException("e", "Unable to connect with the server")
|
||||||
|
return None
|
||||||
|
|
||||||
# Any status code of 300 or above indicates an error.
|
# Any status code of 300 or above indicates an error.
|
||||||
if backup_upload_request.status_code >= 300:
|
if backup_upload_request.status_code >= 300:
|
||||||
|
|
|
@ -10,20 +10,17 @@ from time import time
|
||||||
from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING
|
from typing import Any, cast, Dict, List, Optional, Set, TYPE_CHECKING
|
||||||
|
|
||||||
from UM.Backend.Backend import Backend, BackendState
|
from UM.Backend.Backend import Backend, BackendState
|
||||||
from UM.Scene.Camera import Camera
|
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Signal import Signal
|
from UM.Signal import Signal
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from UM.PluginRegistry import PluginRegistry
|
from UM.PluginRegistry import PluginRegistry
|
||||||
from UM.Resources import Resources
|
|
||||||
from UM.Platform import Platform
|
from UM.Platform import Platform
|
||||||
from UM.Qt.Duration import DurationFormat
|
from UM.Qt.Duration import DurationFormat
|
||||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||||
from UM.Settings.Interfaces import DefinitionContainerInterface
|
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||||
from UM.Settings.SettingInstance import SettingInstance #For typing.
|
from UM.Settings.SettingInstance import SettingInstance #For typing.
|
||||||
from UM.Tool import Tool #For typing.
|
from UM.Tool import Tool #For typing.
|
||||||
from UM.Mesh.MeshData import MeshData #For typing.
|
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
@ -738,6 +735,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
"support_interface": message.time_support_interface,
|
"support_interface": message.time_support_interface,
|
||||||
"support": message.time_support,
|
"support": message.time_support,
|
||||||
"skirt": message.time_skirt,
|
"skirt": message.time_skirt,
|
||||||
|
"prime_tower": message.time_prime_tower,
|
||||||
"travel": message.time_travel,
|
"travel": message.time_travel,
|
||||||
"retract": message.time_retract,
|
"retract": message.time_retract,
|
||||||
"none": message.time_none
|
"none": message.time_none
|
||||||
|
|
|
@ -24,7 +24,7 @@ from cura import LayerPolygon
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
from time import time
|
from time import time
|
||||||
from cura.Settings.ExtrudersModel import ExtrudersModel
|
from cura.Machines.Models.ExtrudersModel import ExtrudersModel
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -196,10 +196,7 @@ class StartSliceJob(Job):
|
||||||
has_printing_mesh = False
|
has_printing_mesh = False
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.getMeshData().getVertices() is not None:
|
if node.callDecoration("isSliceable") and node.getMeshData() and node.getMeshData().getVertices() is not None:
|
||||||
per_object_stack = node.callDecoration("getStack")
|
is_non_printing_mesh = bool(node.callDecoration("isNonPrintingMesh"))
|
||||||
is_non_printing_mesh = False
|
|
||||||
if per_object_stack:
|
|
||||||
is_non_printing_mesh = any(per_object_stack.getProperty(key, "value") for key in NON_PRINTING_MESH_SETTINGS)
|
|
||||||
|
|
||||||
# Find a reason not to add the node
|
# Find a reason not to add the node
|
||||||
if node.callDecoration("getBuildPlateNumber") != self._build_plate_number:
|
if node.callDecoration("getBuildPlateNumber") != self._build_plate_number:
|
||||||
|
@ -259,10 +256,7 @@ class StartSliceJob(Job):
|
||||||
self._buildGlobalInheritsStackMessage(stack)
|
self._buildGlobalInheritsStackMessage(stack)
|
||||||
|
|
||||||
# Build messages for extruder stacks
|
# Build messages for extruder stacks
|
||||||
# Send the extruder settings in the order of extruder positions. Somehow, if you send e.g. extruder 3 first,
|
for extruder_stack in global_stack.extruderList:
|
||||||
# then CuraEngine can slice with the wrong settings. This I think should be fixed in CuraEngine as well.
|
|
||||||
extruder_stack_list = sorted(list(global_stack.extruders.items()), key = lambda item: int(item[0]))
|
|
||||||
for _, extruder_stack in extruder_stack_list:
|
|
||||||
self._buildExtruderMessage(extruder_stack)
|
self._buildExtruderMessage(extruder_stack)
|
||||||
|
|
||||||
for group in filtered_object_groups:
|
for group in filtered_object_groups:
|
||||||
|
@ -326,6 +320,7 @@ class StartSliceJob(Job):
|
||||||
|
|
||||||
result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings.
|
result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings.
|
||||||
result["print_temperature"] = result["material_print_temperature"]
|
result["print_temperature"] = result["material_print_temperature"]
|
||||||
|
result["travel_speed"] = result["speed_travel"]
|
||||||
result["time"] = time.strftime("%H:%M:%S") #Some extra settings.
|
result["time"] = time.strftime("%H:%M:%S") #Some extra settings.
|
||||||
result["date"] = time.strftime("%d-%m-%Y")
|
result["date"] = time.strftime("%d-%m-%Y")
|
||||||
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
|
result["day"] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][int(time.strftime("%w"))]
|
||||||
|
@ -336,25 +331,29 @@ class StartSliceJob(Job):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
## Replace setting tokens in a piece of g-code.
|
def _cacheAllExtruderSettings(self):
|
||||||
# \param value A piece of g-code to replace tokens in.
|
|
||||||
# \param default_extruder_nr Stack nr to use when no stack nr is specified, defaults to the global stack
|
|
||||||
def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1) -> str:
|
|
||||||
if not self._all_extruders_settings:
|
|
||||||
global_stack = cast(ContainerStack, CuraApplication.getInstance().getGlobalContainerStack())
|
global_stack = cast(ContainerStack, CuraApplication.getInstance().getGlobalContainerStack())
|
||||||
|
|
||||||
# NB: keys must be strings for the string formatter
|
# NB: keys must be strings for the string formatter
|
||||||
self._all_extruders_settings = {
|
self._all_extruders_settings = {
|
||||||
"-1": self._buildReplacementTokens(global_stack)
|
"-1": self._buildReplacementTokens(global_stack)
|
||||||
}
|
}
|
||||||
|
|
||||||
for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
|
for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
|
||||||
extruder_nr = extruder_stack.getProperty("extruder_nr", "value")
|
extruder_nr = extruder_stack.getProperty("extruder_nr", "value")
|
||||||
self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack)
|
self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack)
|
||||||
|
|
||||||
|
## Replace setting tokens in a piece of g-code.
|
||||||
|
# \param value A piece of g-code to replace tokens in.
|
||||||
|
# \param default_extruder_nr Stack nr to use when no stack nr is specified, defaults to the global stack
|
||||||
|
def _expandGcodeTokens(self, value: str, default_extruder_nr: int = -1) -> str:
|
||||||
|
if not self._all_extruders_settings:
|
||||||
|
self._cacheAllExtruderSettings()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# any setting can be used as a token
|
# any setting can be used as a token
|
||||||
fmt = GcodeStartEndFormatter(default_extruder_nr = default_extruder_nr)
|
fmt = GcodeStartEndFormatter(default_extruder_nr = default_extruder_nr)
|
||||||
|
if self._all_extruders_settings is None:
|
||||||
|
return ""
|
||||||
settings = self._all_extruders_settings.copy()
|
settings = self._all_extruders_settings.copy()
|
||||||
settings["default_extruder_nr"] = default_extruder_nr
|
settings["default_extruder_nr"] = default_extruder_nr
|
||||||
return str(fmt.format(value, **settings))
|
return str(fmt.format(value, **settings))
|
||||||
|
@ -366,8 +365,14 @@ class StartSliceJob(Job):
|
||||||
def _buildExtruderMessage(self, stack: ContainerStack) -> None:
|
def _buildExtruderMessage(self, stack: ContainerStack) -> None:
|
||||||
message = self._slice_message.addRepeatedMessage("extruders")
|
message = self._slice_message.addRepeatedMessage("extruders")
|
||||||
message.id = int(stack.getMetaDataEntry("position"))
|
message.id = int(stack.getMetaDataEntry("position"))
|
||||||
|
if not self._all_extruders_settings:
|
||||||
|
self._cacheAllExtruderSettings()
|
||||||
|
|
||||||
settings = self._buildReplacementTokens(stack)
|
if self._all_extruders_settings is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
extruder_nr = stack.getProperty("extruder_nr", "value")
|
||||||
|
settings = self._all_extruders_settings[str(extruder_nr)].copy()
|
||||||
|
|
||||||
# Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it.
|
# Also send the material GUID. This is a setting in fdmprinter, but we have no interface for it.
|
||||||
settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "")
|
settings["material_guid"] = stack.material.getMetaDataEntry("GUID", "")
|
||||||
|
@ -391,7 +396,13 @@ class StartSliceJob(Job):
|
||||||
# The settings are taken from the global stack. This does not include any
|
# The settings are taken from the global stack. This does not include any
|
||||||
# per-extruder settings or per-object settings.
|
# per-extruder settings or per-object settings.
|
||||||
def _buildGlobalSettingsMessage(self, stack: ContainerStack) -> None:
|
def _buildGlobalSettingsMessage(self, stack: ContainerStack) -> None:
|
||||||
settings = self._buildReplacementTokens(stack)
|
if not self._all_extruders_settings:
|
||||||
|
self._cacheAllExtruderSettings()
|
||||||
|
|
||||||
|
if self._all_extruders_settings is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
settings = self._all_extruders_settings["-1"].copy()
|
||||||
|
|
||||||
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
|
# Pre-compute material material_bed_temp_prepend and material_print_temp_prepend
|
||||||
start_gcode = settings["machine_start_gcode"]
|
start_gcode = settings["machine_start_gcode"]
|
||||||
|
|
|
@ -1,31 +1,33 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import math
|
||||||
|
import re
|
||||||
|
from typing import Dict, List, NamedTuple, Optional, Union
|
||||||
|
|
||||||
|
import numpy
|
||||||
|
|
||||||
from UM.Backend import Backend
|
from UM.Backend import Backend
|
||||||
from UM.Job import Job
|
from UM.Job import Job
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Math.Vector import Vector
|
from UM.Math.Vector import Vector
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
|
||||||
|
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
from cura.LayerDataBuilder import LayerDataBuilder
|
from cura.LayerDataBuilder import LayerDataBuilder
|
||||||
from cura.LayerDataDecorator import LayerDataDecorator
|
from cura.LayerDataDecorator import LayerDataDecorator
|
||||||
from cura.LayerPolygon import LayerPolygon
|
from cura.LayerPolygon import LayerPolygon
|
||||||
|
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||||
from cura.Scene.GCodeListDecorator import GCodeListDecorator
|
from cura.Scene.GCodeListDecorator import GCodeListDecorator
|
||||||
from cura.Settings.ExtruderManager import ExtruderManager
|
from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
import numpy
|
catalog = i18nCatalog("cura")
|
||||||
import math
|
|
||||||
import re
|
|
||||||
from typing import Dict, List, NamedTuple, Optional, Union
|
|
||||||
|
|
||||||
PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optional[float]), ("z", Optional[float]), ("f", Optional[float]), ("e", Optional[float])])
|
PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optional[float]), ("z", Optional[float]), ("f", Optional[float]), ("e", Optional[float])])
|
||||||
Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])])
|
Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])])
|
||||||
|
|
||||||
|
|
||||||
## This parser is intended to interpret the common firmware codes among all the
|
## This parser is intended to interpret the common firmware codes among all the
|
||||||
# different flavors
|
# different flavors
|
||||||
class FlavorParser:
|
class FlavorParser:
|
||||||
|
@ -33,7 +35,7 @@ class FlavorParser:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage)
|
CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage)
|
||||||
self._cancelled = False
|
self._cancelled = False
|
||||||
self._message = None
|
self._message = None # type: Optional[Message]
|
||||||
self._layer_number = 0
|
self._layer_number = 0
|
||||||
self._extruder_number = 0
|
self._extruder_number = 0
|
||||||
self._clearValues()
|
self._clearValues()
|
||||||
|
@ -368,6 +370,8 @@ class FlavorParser:
|
||||||
self._layer_type = LayerPolygon.InfillType
|
self._layer_type = LayerPolygon.InfillType
|
||||||
elif type == "SUPPORT-INTERFACE":
|
elif type == "SUPPORT-INTERFACE":
|
||||||
self._layer_type = LayerPolygon.SupportInterfaceType
|
self._layer_type = LayerPolygon.SupportInterfaceType
|
||||||
|
elif type == "PRIME-TOWER":
|
||||||
|
self._layer_type = LayerPolygon.PrimeTowerType
|
||||||
else:
|
else:
|
||||||
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
|
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
|
||||||
|
|
||||||
|
@ -425,6 +429,7 @@ class FlavorParser:
|
||||||
|
|
||||||
if line.startswith("M"):
|
if line.startswith("M"):
|
||||||
M = self._getInt(line, "M")
|
M = self._getInt(line, "M")
|
||||||
|
if M is not None:
|
||||||
self.processMCode(M, line, current_position, current_path)
|
self.processMCode(M, line, current_position, current_path)
|
||||||
|
|
||||||
# "Flush" leftovers. Last layer paths are still stored
|
# "Flush" leftovers. Last layer paths are still stored
|
||||||
|
@ -463,7 +468,7 @@ class FlavorParser:
|
||||||
Logger.log("w", "File doesn't contain any valid layers")
|
Logger.log("w", "File doesn't contain any valid layers")
|
||||||
|
|
||||||
settings = CuraApplication.getInstance().getGlobalContainerStack()
|
settings = CuraApplication.getInstance().getGlobalContainerStack()
|
||||||
if not settings.getProperty("machine_center_is_zero", "value"):
|
if settings is not None and not settings.getProperty("machine_center_is_zero", "value"):
|
||||||
machine_width = settings.getProperty("machine_width", "value")
|
machine_width = settings.getProperty("machine_width", "value")
|
||||||
machine_depth = settings.getProperty("machine_depth", "value")
|
machine_depth = settings.getProperty("machine_depth", "value")
|
||||||
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
|
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))
|
||||||
|
|
|
@ -12,9 +12,6 @@ catalog = i18nCatalog("cura")
|
||||||
from . import MarlinFlavorParser, RepRapFlavorParser
|
from . import MarlinFlavorParser, RepRapFlavorParser
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Class for loading and parsing G-code files
|
# Class for loading and parsing G-code files
|
||||||
class GCodeReader(MeshReader):
|
class GCodeReader(MeshReader):
|
||||||
_flavor_default = "Marlin"
|
_flavor_default = "Marlin"
|
||||||
|
|
|
@ -123,7 +123,7 @@ UM.Dialog
|
||||||
UM.TooltipArea {
|
UM.TooltipArea {
|
||||||
Layout.fillWidth:true
|
Layout.fillWidth:true
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
text: catalog.i18nc("@info:tooltip","By default, white pixels represent high points on the mesh and black pixels represent low points on the mesh. Change this option to reverse the behavior such that black pixels represent high points on the mesh and white pixels represent low points on the mesh.")
|
text: catalog.i18nc("@info:tooltip","For lithophanes dark pixels should correspond to thicker locations in order to block more light coming through. For height maps lighter pixels signify higher terrain, so lighter pixels should correspond to thicker locations in the generated 3D model.")
|
||||||
Row {
|
Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|
||||||
|
@ -134,9 +134,9 @@ UM.Dialog
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
ComboBox {
|
ComboBox {
|
||||||
id: image_color_invert
|
id: lighter_is_higher
|
||||||
objectName: "Image_Color_Invert"
|
objectName: "Lighter_Is_Higher"
|
||||||
model: [ catalog.i18nc("@item:inlistbox","Lighter is higher"), catalog.i18nc("@item:inlistbox","Darker is higher") ]
|
model: [ catalog.i18nc("@item:inlistbox","Darker is higher"), catalog.i18nc("@item:inlistbox","Lighter is higher") ]
|
||||||
width: 180 * screenScaleFactor
|
width: 180 * screenScaleFactor
|
||||||
onCurrentIndexChanged: { manager.onImageColorInvertChanged(currentIndex) }
|
onCurrentIndexChanged: { manager.onImageColorInvertChanged(currentIndex) }
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,9 +46,9 @@ class ImageReader(MeshReader):
|
||||||
|
|
||||||
def _read(self, file_name):
|
def _read(self, file_name):
|
||||||
size = max(self._ui.getWidth(), self._ui.getDepth())
|
size = max(self._ui.getWidth(), self._ui.getDepth())
|
||||||
return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.image_color_invert)
|
return self._generateSceneNode(file_name, size, self._ui.peak_height, self._ui.base_height, self._ui.smoothing, 512, self._ui.lighter_is_higher)
|
||||||
|
|
||||||
def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, image_color_invert):
|
def _generateSceneNode(self, file_name, xz_size, peak_height, base_height, blur_iterations, max_size, lighter_is_higher):
|
||||||
scene_node = SceneNode()
|
scene_node = SceneNode()
|
||||||
|
|
||||||
mesh = MeshBuilder()
|
mesh = MeshBuilder()
|
||||||
|
@ -104,7 +104,7 @@ class ImageReader(MeshReader):
|
||||||
|
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
|
|
||||||
if image_color_invert:
|
if not lighter_is_higher:
|
||||||
height_data = 1 - height_data
|
height_data = 1 - height_data
|
||||||
|
|
||||||
for _ in range(0, blur_iterations):
|
for _ in range(0, blur_iterations):
|
||||||
|
|
|
@ -30,10 +30,10 @@ class ImageReaderUI(QObject):
|
||||||
self._width = self.default_width
|
self._width = self.default_width
|
||||||
self._depth = self.default_depth
|
self._depth = self.default_depth
|
||||||
|
|
||||||
self.base_height = 1
|
self.base_height = 0.4
|
||||||
self.peak_height = 10
|
self.peak_height = 2.5
|
||||||
self.smoothing = 1
|
self.smoothing = 1
|
||||||
self.image_color_invert = False;
|
self.lighter_is_higher = False;
|
||||||
|
|
||||||
self._ui_lock = threading.Lock()
|
self._ui_lock = threading.Lock()
|
||||||
self._cancelled = False
|
self._cancelled = False
|
||||||
|
@ -143,4 +143,4 @@ class ImageReaderUI(QObject):
|
||||||
|
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def onImageColorInvertChanged(self, value):
|
def onImageColorInvertChanged(self, value):
|
||||||
self.image_color_invert = (value == 1)
|
self.lighter_is_higher = (value == 1)
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal
|
from typing import Optional, TYPE_CHECKING
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtProperty
|
||||||
|
|
||||||
import UM.i18n
|
import UM.i18n
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
from UM.Application import Application
|
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
from UM.Settings.DefinitionContainer import DefinitionContainer
|
||||||
|
|
||||||
from cura.MachineAction import MachineAction
|
from cura.MachineAction import MachineAction
|
||||||
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
from cura.Settings.CuraStackBuilder import CuraStackBuilder
|
||||||
|
from cura.Settings.cura_empty_instance_containers import isEmptyContainer
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from PyQt5.QtCore import QObject
|
||||||
|
|
||||||
catalog = UM.i18n.i18nCatalog("cura")
|
catalog = UM.i18n.i18nCatalog("cura")
|
||||||
|
|
||||||
|
@ -18,139 +23,102 @@ catalog = UM.i18n.i18nCatalog("cura")
|
||||||
## This action allows for certain settings that are "machine only") to be modified.
|
## This action allows for certain settings that are "machine only") to be modified.
|
||||||
# It automatically detects machine definitions that it knows how to change and attaches itself to those.
|
# It automatically detects machine definitions that it knows how to change and attaches itself to those.
|
||||||
class MachineSettingsAction(MachineAction):
|
class MachineSettingsAction(MachineAction):
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent: Optional["QObject"] = None) -> None:
|
||||||
super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings"))
|
super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings"))
|
||||||
self._qml_url = "MachineSettingsAction.qml"
|
self._qml_url = "MachineSettingsAction.qml"
|
||||||
|
|
||||||
self._application = Application.getInstance()
|
from cura.CuraApplication import CuraApplication
|
||||||
|
self._application = CuraApplication.getInstance()
|
||||||
self._global_container_stack = None
|
|
||||||
|
|
||||||
from cura.Settings.CuraContainerStack import _ContainerIndexes
|
from cura.Settings.CuraContainerStack import _ContainerIndexes
|
||||||
self._container_index = _ContainerIndexes.DefinitionChanges
|
self._store_container_index = _ContainerIndexes.DefinitionChanges
|
||||||
|
|
||||||
self._container_registry = ContainerRegistry.getInstance()
|
self._container_registry = ContainerRegistry.getInstance()
|
||||||
self._container_registry.containerAdded.connect(self._onContainerAdded)
|
self._container_registry.containerAdded.connect(self._onContainerAdded)
|
||||||
self._container_registry.containerRemoved.connect(self._onContainerRemoved)
|
|
||||||
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
|
||||||
|
|
||||||
|
# The machine settings dialog blocks auto-slicing when it's shown, and re-enables it when it's finished.
|
||||||
self._backend = self._application.getBackend()
|
self._backend = self._application.getBackend()
|
||||||
|
self.onFinished.connect(self._onFinished)
|
||||||
|
|
||||||
self._empty_definition_container_id_list = []
|
# Which container index in a stack to store machine setting changes.
|
||||||
|
@pyqtProperty(int, constant = True)
|
||||||
def _isEmptyDefinitionChanges(self, container_id: str):
|
def storeContainerIndex(self) -> int:
|
||||||
if not self._empty_definition_container_id_list:
|
return self._store_container_index
|
||||||
self._empty_definition_container_id_list = [self._application.empty_container.getId(),
|
|
||||||
self._application.empty_definition_changes_container.getId()]
|
|
||||||
return container_id in self._empty_definition_container_id_list
|
|
||||||
|
|
||||||
def _onContainerAdded(self, container):
|
def _onContainerAdded(self, container):
|
||||||
# Add this action as a supported action to all machine definitions
|
# Add this action as a supported action to all machine definitions
|
||||||
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine":
|
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine":
|
||||||
self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
|
self._application.getMachineActionManager().addSupportedAction(container.getId(), self.getKey())
|
||||||
|
|
||||||
def _onContainerRemoved(self, container):
|
|
||||||
# Remove definition_changes containers when a stack is removed
|
|
||||||
if container.getMetaDataEntry("type") in ["machine", "extruder_train"]:
|
|
||||||
definition_changes_id = container.definitionChanges.getId()
|
|
||||||
if self._isEmptyDefinitionChanges(definition_changes_id):
|
|
||||||
return
|
|
||||||
|
|
||||||
def _reset(self):
|
def _reset(self):
|
||||||
if not self._global_container_stack:
|
global_stack = self._application.getMachineManager().activeMachine
|
||||||
|
if not global_stack:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Make sure there is a definition_changes container to store the machine settings
|
# Make sure there is a definition_changes container to store the machine settings
|
||||||
definition_changes_id = self._global_container_stack.definitionChanges.getId()
|
definition_changes_id = global_stack.definitionChanges.getId()
|
||||||
if self._isEmptyDefinitionChanges(definition_changes_id):
|
if isEmptyContainer(definition_changes_id):
|
||||||
CuraStackBuilder.createDefinitionChangesContainer(self._global_container_stack,
|
CuraStackBuilder.createDefinitionChangesContainer(global_stack,
|
||||||
self._global_container_stack.getName() + "_settings")
|
global_stack.getName() + "_settings")
|
||||||
|
|
||||||
# Notify the UI in which container to store the machine settings data
|
|
||||||
from cura.Settings.CuraContainerStack import _ContainerIndexes
|
|
||||||
|
|
||||||
container_index = _ContainerIndexes.DefinitionChanges
|
|
||||||
if container_index != self._container_index:
|
|
||||||
self._container_index = container_index
|
|
||||||
self.containerIndexChanged.emit()
|
|
||||||
|
|
||||||
# Disable auto-slicing while the MachineAction is showing
|
# Disable auto-slicing while the MachineAction is showing
|
||||||
if self._backend: # This sometimes triggers before backend is loaded.
|
if self._backend: # This sometimes triggers before backend is loaded.
|
||||||
self._backend.disableTimer()
|
self._backend.disableTimer()
|
||||||
|
|
||||||
@pyqtSlot()
|
def _onFinished(self):
|
||||||
def onFinishAction(self):
|
# Restore auto-slicing when the machine action is dismissed
|
||||||
# Restore autoslicing when the machineaction is dismissed
|
|
||||||
if self._backend and self._backend.determineAutoSlicing():
|
if self._backend and self._backend.determineAutoSlicing():
|
||||||
|
self._backend.enableTimer()
|
||||||
self._backend.tickle()
|
self._backend.tickle()
|
||||||
|
|
||||||
containerIndexChanged = pyqtSignal()
|
|
||||||
|
|
||||||
@pyqtProperty(int, notify = containerIndexChanged)
|
|
||||||
def containerIndex(self):
|
|
||||||
return self._container_index
|
|
||||||
|
|
||||||
def _onGlobalContainerChanged(self):
|
|
||||||
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
|
|
||||||
|
|
||||||
# This additional emit is needed because we cannot connect a UM.Signal directly to a pyqtSignal
|
|
||||||
self.globalContainerChanged.emit()
|
|
||||||
|
|
||||||
globalContainerChanged = pyqtSignal()
|
|
||||||
|
|
||||||
@pyqtProperty(int, notify = globalContainerChanged)
|
|
||||||
def definedExtruderCount(self):
|
|
||||||
if not self._global_container_stack:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
return len(self._global_container_stack.getMetaDataEntry("machine_extruder_trains"))
|
|
||||||
|
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def setMachineExtruderCount(self, extruder_count):
|
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
|
# 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.
|
# it was moved to the machine manager instead. Now this method just calls the machine manager.
|
||||||
self._application.getMachineManager().setActiveMachineExtruderCount(extruder_count)
|
self._application.getMachineManager().setActiveMachineExtruderCount(extruder_count)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def forceUpdate(self):
|
def forceUpdate(self) -> None:
|
||||||
# Force rebuilding the build volume by reloading the global container stack.
|
# Force rebuilding the build volume by reloading the global container stack.
|
||||||
# This is a bit of a hack, but it seems quick enough.
|
# This is a bit of a hack, but it seems quick enough.
|
||||||
self._application.globalContainerStackChanged.emit()
|
self._application.getMachineManager().globalContainerChanged.emit()
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def updateHasMaterialsMetadata(self):
|
def updateHasMaterialsMetadata(self) -> None:
|
||||||
|
global_stack = self._application.getMachineManager().activeMachine
|
||||||
|
|
||||||
# Updates the has_materials metadata flag after switching gcode flavor
|
# Updates the has_materials metadata flag after switching gcode flavor
|
||||||
if not self._global_container_stack:
|
if not global_stack:
|
||||||
return
|
return
|
||||||
|
|
||||||
definition = self._global_container_stack.getBottom()
|
definition = global_stack.getDefinition()
|
||||||
if definition.getProperty("machine_gcode_flavor", "value") != "UltiGCode" or definition.getMetaDataEntry("has_materials", False):
|
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+
|
# In other words: only continue for the UM2 (extended), but not for the UM2+
|
||||||
return
|
return
|
||||||
|
|
||||||
machine_manager = self._application.getMachineManager()
|
machine_manager = self._application.getMachineManager()
|
||||||
material_manager = self._application.getMaterialManager()
|
material_manager = self._application.getMaterialManager()
|
||||||
extruder_positions = list(self._global_container_stack.extruders.keys())
|
extruder_positions = list(global_stack.extruders.keys())
|
||||||
has_materials = self._global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
|
has_materials = global_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
|
||||||
|
|
||||||
material_node = None
|
material_node = None
|
||||||
if has_materials:
|
if has_materials:
|
||||||
self._global_container_stack.setMetaDataEntry("has_materials", True)
|
global_stack.setMetaDataEntry("has_materials", True)
|
||||||
else:
|
else:
|
||||||
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
|
# 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.
|
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
|
||||||
if "has_materials" in self._global_container_stack.getMetaData():
|
if "has_materials" in global_stack.getMetaData():
|
||||||
self._global_container_stack.removeMetaDataEntry("has_materials")
|
global_stack.removeMetaDataEntry("has_materials")
|
||||||
|
|
||||||
# set materials
|
# set materials
|
||||||
for position in extruder_positions:
|
for position in extruder_positions:
|
||||||
if has_materials:
|
if has_materials:
|
||||||
material_node = material_manager.getDefaultMaterial(self._global_container_stack, position, None)
|
material_node = material_manager.getDefaultMaterial(global_stack, position, None)
|
||||||
machine_manager.setMaterial(position, material_node)
|
machine_manager.setMaterial(position, material_node)
|
||||||
|
|
||||||
self._application.globalContainerStackChanged.emit()
|
self._application.globalContainerStackChanged.emit()
|
||||||
|
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def updateMaterialForDiameter(self, extruder_position: int):
|
def updateMaterialForDiameter(self, extruder_position: int) -> None:
|
||||||
# Updates the material container to a material that matches the material diameter set for the printer
|
# Updates the material container to a material that matches the material diameter set for the printer
|
||||||
self._application.getMachineManager().updateMaterialWithVariant(str(extruder_position))
|
self._application.getMachineManager().updateMaterialWithVariant(str(extruder_position))
|
||||||
|
|
|
@ -1,939 +1,103 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
import QtQuick 2.2
|
import QtQuick 2.10
|
||||||
import QtQuick.Controls 1.1
|
import QtQuick.Controls 2.3
|
||||||
import QtQuick.Layouts 1.1
|
import QtQuick.Layouts 1.3
|
||||||
import QtQuick.Window 2.1
|
|
||||||
|
|
||||||
import UM 1.2 as UM
|
import UM 1.3 as UM
|
||||||
import Cura 1.0 as Cura
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// This component contains the content for the "Welcome" page of the welcome on-boarding process.
|
||||||
|
//
|
||||||
Cura.MachineAction
|
Cura.MachineAction
|
||||||
{
|
{
|
||||||
id: base
|
UM.I18nCatalog { id: catalog; name: "cura" }
|
||||||
property var extrudersModel: Cura.ExtrudersModel{} // Do not retrieve the Model from a backend. Otherwise the tabs
|
|
||||||
// in tabView will not removed/updated. Probably QML bug
|
|
||||||
property int extruderTabsCount: 0
|
|
||||||
|
|
||||||
property var activeMachineId: Cura.MachineManager.activeMachine != null ? Cura.MachineManager.activeMachine.id : ""
|
anchors.fill: parent
|
||||||
|
|
||||||
|
property var extrudersModel: Cura.ExtrudersModel {}
|
||||||
|
|
||||||
|
// If we create a TabButton for "Printer" and use Repeater for extruders, for some reason, once the component
|
||||||
|
// finishes it will automatically change "currentIndex = 1", and it is VERY difficult to change "currentIndex = 0"
|
||||||
|
// after that. Using a model and a Repeater to create both "Printer" and extruder TabButtons seem to solve this
|
||||||
|
// problem.
|
||||||
Connections
|
Connections
|
||||||
{
|
{
|
||||||
target: base.extrudersModel
|
target: extrudersModel
|
||||||
onModelChanged:
|
onItemsChanged: tabNameModel.update()
|
||||||
{
|
|
||||||
var extruderCount = base.extrudersModel.count;
|
|
||||||
base.extruderTabsCount = extruderCount;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections
|
ListModel
|
||||||
{
|
{
|
||||||
target: dialog ? dialog : null
|
id: tabNameModel
|
||||||
ignoreUnknownSignals: true
|
|
||||||
// Any which way this action dialog is dismissed, make sure it is properly finished
|
|
||||||
onNextClicked: finishAction()
|
|
||||||
onBackClicked: finishAction()
|
|
||||||
onAccepted: finishAction()
|
|
||||||
onRejected: finishAction()
|
|
||||||
onClosing: finishAction()
|
|
||||||
}
|
|
||||||
|
|
||||||
function finishAction()
|
Component.onCompleted: update()
|
||||||
|
|
||||||
|
function update()
|
||||||
{
|
{
|
||||||
forceActiveFocus();
|
clear()
|
||||||
manager.onFinishAction();
|
append({ name: catalog.i18nc("@title:tab", "Printer") })
|
||||||
}
|
for (var i = 0; i < extrudersModel.count; i++)
|
||||||
|
|
||||||
anchors.fill: parent;
|
|
||||||
Item
|
|
||||||
{
|
{
|
||||||
id: machineSettingsAction
|
const m = extrudersModel.getItem(i)
|
||||||
anchors.fill: parent;
|
append({ name: m.name })
|
||||||
|
|
||||||
UM.I18nCatalog { id: catalog; name: "cura"; }
|
|
||||||
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
id: pageTitle
|
|
||||||
width: parent.width
|
|
||||||
text: catalog.i18nc("@title", "Machine Settings")
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
font.pointSize: 18;
|
|
||||||
}
|
|
||||||
|
|
||||||
TabView
|
|
||||||
{
|
|
||||||
id: settingsTabs
|
|
||||||
height: parent.height - y
|
|
||||||
width: parent.width
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.top: pageTitle.bottom
|
|
||||||
anchors.topMargin: UM.Theme.getSize("default_margin").height
|
|
||||||
|
|
||||||
property real columnWidth: Math.round((width - 3 * UM.Theme.getSize("default_margin").width) / 2)
|
|
||||||
property real labelColumnWidth: Math.round(columnWidth / 2)
|
|
||||||
|
|
||||||
Tab
|
|
||||||
{
|
|
||||||
title: catalog.i18nc("@title:tab", "Printer");
|
|
||||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
|
||||||
|
|
||||||
Column
|
|
||||||
{
|
|
||||||
spacing: UM.Theme.getSize("default_margin").height
|
|
||||||
|
|
||||||
Row
|
|
||||||
{
|
|
||||||
width: parent.width
|
|
||||||
spacing: UM.Theme.getSize("default_margin").height
|
|
||||||
|
|
||||||
Column
|
|
||||||
{
|
|
||||||
width: settingsTabs.columnWidth
|
|
||||||
spacing: UM.Theme.getSize("default_lining").height
|
|
||||||
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "Printer Settings")
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { width: UM.Theme.getSize("default_margin").width; height: UM.Theme.getSize("default_margin").height }
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: buildAreaWidthField
|
|
||||||
sourceComponent: numericTextFieldWithUnit
|
|
||||||
property string settingKey: "machine_width"
|
|
||||||
property string label: catalog.i18nc("@label", "X (Width)")
|
|
||||||
property string unit: catalog.i18nc("@label", "mm")
|
|
||||||
property bool forceUpdateOnChange: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: buildAreaDepthField
|
|
||||||
sourceComponent: numericTextFieldWithUnit
|
|
||||||
property string settingKey: "machine_depth"
|
|
||||||
property string label: catalog.i18nc("@label", "Y (Depth)")
|
|
||||||
property string unit: catalog.i18nc("@label", "mm")
|
|
||||||
property bool forceUpdateOnChange: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: buildAreaHeightField
|
|
||||||
sourceComponent: numericTextFieldWithUnit
|
|
||||||
property string settingKey: "machine_height"
|
|
||||||
property string label: catalog.i18nc("@label", "Z (Height)")
|
|
||||||
property string unit: catalog.i18nc("@label", "mm")
|
|
||||||
property bool forceUpdateOnChange: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { width: UM.Theme.getSize("default_margin").width; height: UM.Theme.getSize("default_margin").height }
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: shapeComboBox
|
|
||||||
sourceComponent: comboBoxWithOptions
|
|
||||||
property string settingKey: "machine_shape"
|
|
||||||
property string label: catalog.i18nc("@label", "Build plate shape")
|
|
||||||
property bool forceUpdateOnChange: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: centerIsZeroCheckBox
|
|
||||||
sourceComponent: simpleCheckBox
|
|
||||||
property string settingKey: "machine_center_is_zero"
|
|
||||||
property string label: catalog.i18nc("@option:check", "Origin at center")
|
|
||||||
property bool forceUpdateOnChange: true
|
|
||||||
}
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: heatedBedCheckBox
|
|
||||||
sourceComponent: simpleCheckBox
|
|
||||||
property var settingKey: "machine_heated_bed"
|
|
||||||
property string label: catalog.i18nc("@option:check", "Heated bed")
|
|
||||||
property bool forceUpdateOnChange: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { width: UM.Theme.getSize("default_margin").width; height: UM.Theme.getSize("default_margin").height }
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: gcodeFlavorComboBox
|
|
||||||
sourceComponent: comboBoxWithOptions
|
|
||||||
property string settingKey: "machine_gcode_flavor"
|
|
||||||
property string label: catalog.i18nc("@label", "G-code flavor")
|
|
||||||
property bool forceUpdateOnChange: true
|
|
||||||
property var afterOnActivate: manager.updateHasMaterialsMetadata
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column
|
|
||||||
{
|
|
||||||
width: settingsTabs.columnWidth
|
|
||||||
spacing: UM.Theme.getSize("default_lining").height
|
|
||||||
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "Printhead Settings")
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { width: UM.Theme.getSize("default_margin").width; height: UM.Theme.getSize("default_margin").height }
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: printheadXMinField
|
|
||||||
sourceComponent: headPolygonTextField
|
|
||||||
property string label: catalog.i18nc("@label", "X min")
|
|
||||||
property string tooltip: catalog.i18nc("@tooltip", "Distance from the left of the printhead to the center of the nozzle. Used to prevent colissions between previous prints and the printhead when printing \"One at a Time\".")
|
|
||||||
property string axis: "x"
|
|
||||||
property string side: "min"
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: printheadYMinField
|
|
||||||
sourceComponent: headPolygonTextField
|
|
||||||
property string label: catalog.i18nc("@label", "Y min")
|
|
||||||
property string tooltip: catalog.i18nc("@tooltip", "Distance from the front of the printhead to the center of the nozzle. Used to prevent colissions between previous prints and the printhead when printing \"One at a Time\".")
|
|
||||||
property string axis: "y"
|
|
||||||
property string side: "min"
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: printheadXMaxField
|
|
||||||
sourceComponent: headPolygonTextField
|
|
||||||
property string label: catalog.i18nc("@label", "X max")
|
|
||||||
property string tooltip: catalog.i18nc("@tooltip", "Distance from the right of the printhead to the center of the nozzle. Used to prevent colissions between previous prints and the printhead when printing \"One at a Time\".")
|
|
||||||
property string axis: "x"
|
|
||||||
property string side: "max"
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: printheadYMaxField
|
|
||||||
sourceComponent: headPolygonTextField
|
|
||||||
property string label: catalog.i18nc("@label", "Y max")
|
|
||||||
property string tooltip: catalog.i18nc("@tooltip", "Distance from the rear of the printhead to the center of the nozzle. Used to prevent colissions between previous prints and the printhead when printing \"One at a Time\".")
|
|
||||||
property string axis: "y"
|
|
||||||
property string side: "max"
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { width: UM.Theme.getSize("default_margin").width; height: UM.Theme.getSize("default_margin").height }
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: gantryHeightField
|
|
||||||
sourceComponent: numericTextFieldWithUnit
|
|
||||||
property string settingKey: "gantry_height"
|
|
||||||
property string label: catalog.i18nc("@label", "Gantry height")
|
|
||||||
property string unit: catalog.i18nc("@label", "mm")
|
|
||||||
property string tooltip: catalog.i18nc("@tooltip", "The height difference between the tip of the nozzle and the gantry system (X and Y axes). Used to prevent collisions between previous prints and the gantry when printing \"One at a Time\".")
|
|
||||||
property bool forceUpdateOnChange: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { width: UM.Theme.getSize("default_margin").width; height: UM.Theme.getSize("default_margin").height }
|
|
||||||
|
|
||||||
UM.TooltipArea
|
|
||||||
{
|
|
||||||
height: childrenRect.height
|
|
||||||
width: childrenRect.width
|
|
||||||
text: machineExtruderCountProvider.properties.description
|
|
||||||
visible: extruderCountModel.count >= 2
|
|
||||||
|
|
||||||
Row
|
|
||||||
{
|
|
||||||
spacing: UM.Theme.getSize("default_margin").width
|
|
||||||
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "Number of Extruders")
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: Math.max(0, settingsTabs.labelColumnWidth)
|
|
||||||
anchors.verticalCenter: extruderCountComboBox.verticalCenter
|
|
||||||
}
|
|
||||||
ComboBox
|
|
||||||
{
|
|
||||||
id: extruderCountComboBox
|
|
||||||
model: ListModel
|
|
||||||
{
|
|
||||||
id: extruderCountModel
|
|
||||||
Component.onCompleted:
|
|
||||||
{
|
|
||||||
for(var i = 0; i < manager.definedExtruderCount; i++)
|
|
||||||
{
|
|
||||||
extruderCountModel.append({text: String(i + 1), value: i});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections
|
Cura.RoundedRectangle
|
||||||
{
|
{
|
||||||
target: manager
|
anchors
|
||||||
onDefinedExtruderCountChanged:
|
|
||||||
{
|
{
|
||||||
extruderCountModel.clear();
|
top: tabBar.bottom
|
||||||
for(var i = 0; i < manager.definedExtruderCount; ++i)
|
topMargin: -UM.Theme.getSize("default_lining").height
|
||||||
|
bottom: parent.bottom
|
||||||
|
left: parent.left
|
||||||
|
right: parent.right
|
||||||
|
}
|
||||||
|
cornerSide: Cura.RoundedRectangle.Direction.Down
|
||||||
|
border.color: UM.Theme.getColor("lining")
|
||||||
|
border.width: UM.Theme.getSize("default_lining").width
|
||||||
|
radius: UM.Theme.getSize("default_radius").width
|
||||||
|
color: UM.Theme.getColor("main_background")
|
||||||
|
StackLayout
|
||||||
{
|
{
|
||||||
extruderCountModel.append({text: String(i + 1), value: i});
|
id: tabStack
|
||||||
}
|
anchors.fill: parent
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentIndex: machineExtruderCountProvider.properties.value - 1
|
currentIndex: tabBar.currentIndex
|
||||||
onActivated:
|
|
||||||
{
|
|
||||||
manager.setMachineExtruderCount(index + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row
|
MachineSettingsPrinterTab
|
||||||
{
|
{
|
||||||
spacing: UM.Theme.getSize("default_margin").width
|
id: printerTab
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
height: parent.height - y
|
|
||||||
Column
|
|
||||||
{
|
|
||||||
height: parent.height
|
|
||||||
width: settingsTabs.columnWidth
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "Start G-code")
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: machineStartGcodeField
|
|
||||||
sourceComponent: gcodeTextArea
|
|
||||||
property int areaWidth: parent.width
|
|
||||||
property int areaHeight: parent.height - y
|
|
||||||
property string settingKey: "machine_start_gcode"
|
|
||||||
property string tooltip: catalog.i18nc("@tooltip", "G-code commands to be executed at the very start.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
height: parent.height
|
|
||||||
width: settingsTabs.columnWidth
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "End G-code")
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: machineEndGcodeField
|
|
||||||
sourceComponent: gcodeTextArea
|
|
||||||
property int areaWidth: parent.width
|
|
||||||
property int areaHeight: parent.height - y
|
|
||||||
property string settingKey: "machine_end_gcode"
|
|
||||||
property string tooltip: catalog.i18nc("@tooltip", "G-code commands to be executed at the very end.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onCurrentIndexChanged:
|
|
||||||
{
|
|
||||||
if(currentIndex > 0)
|
|
||||||
{
|
|
||||||
contentItem.forceActiveFocus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Repeater
|
Repeater
|
||||||
{
|
{
|
||||||
id: extruderTabsRepeater
|
model: extrudersModel
|
||||||
model: base.extruderTabsCount
|
delegate: MachineSettingsExtruderTab
|
||||||
|
|
||||||
Tab
|
|
||||||
{
|
{
|
||||||
title: base.extrudersModel.getItem(index).name
|
id: discoverTab
|
||||||
anchors.margins: UM.Theme.getSize("default_margin").width
|
extruderPosition: model.index
|
||||||
|
extruderStackId: model.id
|
||||||
Column
|
|
||||||
{
|
|
||||||
spacing: UM.Theme.getSize("default_lining").width
|
|
||||||
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "Nozzle Settings")
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { width: UM.Theme.getSize("default_margin").width; height: UM.Theme.getSize("default_margin").height }
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: extruderNozzleSizeField
|
|
||||||
visible: !Cura.MachineManager.hasVariants
|
|
||||||
sourceComponent: numericTextFieldWithUnit
|
|
||||||
property string settingKey: "machine_nozzle_size"
|
|
||||||
property string label: catalog.i18nc("@label", "Nozzle size")
|
|
||||||
property string unit: catalog.i18nc("@label", "mm")
|
|
||||||
function afterOnEditingFinished()
|
|
||||||
{
|
|
||||||
// Somehow the machine_nozzle_size dependent settings are not updated otherwise
|
|
||||||
Cura.MachineManager.forceUpdateAllSettings()
|
|
||||||
}
|
|
||||||
property bool isExtruderSetting: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: materialDiameterField
|
|
||||||
visible: Cura.MachineManager.hasMaterials
|
|
||||||
sourceComponent: numericTextFieldWithUnit
|
|
||||||
property string settingKey: "material_diameter"
|
|
||||||
property string label: catalog.i18nc("@label", "Compatible material diameter")
|
|
||||||
property string unit: catalog.i18nc("@label", "mm")
|
|
||||||
property string tooltip: catalog.i18nc("@tooltip", "The nominal diameter of filament supported by the printer. The exact diameter will be overridden by the material and/or the profile.")
|
|
||||||
function afterOnEditingFinished()
|
|
||||||
{
|
|
||||||
if (settingsTabs.currentIndex > 0)
|
|
||||||
{
|
|
||||||
manager.updateMaterialForDiameter(settingsTabs.currentIndex - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function setValueFunction(value)
|
|
||||||
{
|
|
||||||
if (settingsTabs.currentIndex > 0)
|
|
||||||
{
|
|
||||||
const extruderIndex = index.toString()
|
|
||||||
Cura.MachineManager.activeMachine.extruders[extruderIndex].compatibleMaterialDiameter = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
property bool isExtruderSetting: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: extruderOffsetXField
|
|
||||||
sourceComponent: numericTextFieldWithUnit
|
|
||||||
property string settingKey: "machine_nozzle_offset_x"
|
|
||||||
property string label: catalog.i18nc("@label", "Nozzle offset X")
|
|
||||||
property string unit: catalog.i18nc("@label", "mm")
|
|
||||||
property bool isExtruderSetting: true
|
|
||||||
property bool forceUpdateOnChange: true
|
|
||||||
property bool allowNegative: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: extruderOffsetYField
|
|
||||||
sourceComponent: numericTextFieldWithUnit
|
|
||||||
property string settingKey: "machine_nozzle_offset_y"
|
|
||||||
property string label: catalog.i18nc("@label", "Nozzle offset Y")
|
|
||||||
property string unit: catalog.i18nc("@label", "mm")
|
|
||||||
property bool isExtruderSetting: true
|
|
||||||
property bool forceUpdateOnChange: true
|
|
||||||
property bool allowNegative: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: extruderCoolingFanNumberField
|
|
||||||
sourceComponent: numericTextFieldWithUnit
|
|
||||||
property string settingKey: "machine_extruder_cooling_fan_number"
|
|
||||||
property string label: catalog.i18nc("@label", "Cooling Fan Number")
|
|
||||||
property string unit: catalog.i18nc("@label", "")
|
|
||||||
property bool isExtruderSetting: true
|
|
||||||
property bool forceUpdateOnChange: true
|
|
||||||
property bool allowNegative: false
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { width: UM.Theme.getSize("default_margin").width; height: UM.Theme.getSize("default_margin").height }
|
|
||||||
|
|
||||||
Row
|
|
||||||
{
|
|
||||||
spacing: UM.Theme.getSize("default_margin").width
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
height: parent.height - y
|
|
||||||
Column
|
|
||||||
{
|
|
||||||
height: parent.height
|
|
||||||
width: settingsTabs.columnWidth
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "Extruder Start G-code")
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: extruderStartGcodeField
|
|
||||||
sourceComponent: gcodeTextArea
|
|
||||||
property int areaWidth: parent.width
|
|
||||||
property int areaHeight: parent.height - y
|
|
||||||
property string settingKey: "machine_extruder_start_code"
|
|
||||||
property bool isExtruderSetting: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Column {
|
|
||||||
height: parent.height
|
|
||||||
width: settingsTabs.columnWidth
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "Extruder End G-code")
|
|
||||||
font.bold: true
|
|
||||||
}
|
|
||||||
Loader
|
|
||||||
{
|
|
||||||
id: extruderEndGcodeField
|
|
||||||
sourceComponent: gcodeTextArea
|
|
||||||
property int areaWidth: parent.width
|
|
||||||
property int areaHeight: parent.height - y
|
|
||||||
property string settingKey: "machine_extruder_end_code"
|
|
||||||
property bool isExtruderSetting: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component
|
|
||||||
{
|
|
||||||
id: simpleCheckBox
|
|
||||||
UM.TooltipArea
|
|
||||||
{
|
|
||||||
height: checkBox.height
|
|
||||||
width: checkBox.width
|
|
||||||
text: _tooltip
|
|
||||||
|
|
||||||
property bool _isExtruderSetting: (typeof(isExtruderSetting) === 'undefined') ? false: isExtruderSetting
|
|
||||||
property bool _forceUpdateOnChange: (typeof(forceUpdateOnChange) === 'undefined') ? false: forceUpdateOnChange
|
|
||||||
property string _tooltip: (typeof(tooltip) === 'undefined') ? propertyProvider.properties.description : tooltip
|
|
||||||
|
|
||||||
UM.SettingPropertyProvider
|
|
||||||
{
|
|
||||||
id: propertyProvider
|
|
||||||
|
|
||||||
containerStackId: {
|
|
||||||
if(_isExtruderSetting)
|
|
||||||
{
|
|
||||||
if(settingsTabs.currentIndex > 0)
|
|
||||||
{
|
|
||||||
return Cura.ExtruderManager.extruderIds[String(settingsTabs.currentIndex - 1)];
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return base.activeMachineId
|
|
||||||
}
|
|
||||||
key: settingKey
|
|
||||||
watchedProperties: [ "value", "description" ]
|
|
||||||
storeIndex: manager.containerIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
CheckBox
|
|
||||||
{
|
|
||||||
id: checkBox
|
|
||||||
text: label
|
|
||||||
checked: String(propertyProvider.properties.value).toLowerCase() != 'false'
|
|
||||||
onClicked:
|
|
||||||
{
|
|
||||||
propertyProvider.setPropertyValue("value", checked);
|
|
||||||
if(_forceUpdateOnChange)
|
|
||||||
{
|
|
||||||
manager.forceUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component
|
|
||||||
{
|
|
||||||
id: numericTextFieldWithUnit
|
|
||||||
UM.TooltipArea
|
|
||||||
{
|
|
||||||
height: childrenRect.height
|
|
||||||
width: childrenRect.width
|
|
||||||
text: _tooltip
|
|
||||||
|
|
||||||
property bool _isExtruderSetting: (typeof(isExtruderSetting) === 'undefined') ? false: isExtruderSetting
|
|
||||||
property bool _allowNegative: (typeof(allowNegative) === 'undefined') ? false : allowNegative
|
|
||||||
property var _afterOnEditingFinished: (typeof(afterOnEditingFinished) === 'undefined') ? undefined : afterOnEditingFinished
|
|
||||||
property bool _forceUpdateOnChange: (typeof(forceUpdateOnChange) === 'undefined') ? false : forceUpdateOnChange
|
|
||||||
property string _label: (typeof(label) === 'undefined') ? "" : label
|
|
||||||
property string _tooltip: (typeof(tooltip) === 'undefined') ? propertyProvider.properties.description : tooltip
|
|
||||||
property var _setValueFunction: (typeof(setValueFunction) === 'undefined') ? undefined : setValueFunction
|
|
||||||
|
|
||||||
UM.SettingPropertyProvider
|
|
||||||
{
|
|
||||||
id: propertyProvider
|
|
||||||
|
|
||||||
containerStackId: {
|
|
||||||
if(_isExtruderSetting)
|
|
||||||
{
|
|
||||||
if(settingsTabs.currentIndex > 0)
|
|
||||||
{
|
|
||||||
return Cura.ExtruderManager.extruderIds[String(settingsTabs.currentIndex - 1)];
|
|
||||||
}
|
}
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
return base.activeMachineId
|
|
||||||
}
|
}
|
||||||
key: settingKey
|
|
||||||
watchedProperties: [ "value", "description" ]
|
|
||||||
storeIndex: manager.containerIndex
|
|
||||||
}
|
}
|
||||||
|
UM.TabRow
|
||||||
Row
|
|
||||||
{
|
{
|
||||||
spacing: UM.Theme.getSize("default_margin").width
|
id: tabBar
|
||||||
|
width: parent.width
|
||||||
Label
|
Repeater
|
||||||
{
|
{
|
||||||
text: _label
|
model: tabNameModel
|
||||||
visible: _label != ""
|
delegate: UM.TabRowButton
|
||||||
elide: Text.ElideRight
|
|
||||||
width: Math.max(0, settingsTabs.labelColumnWidth)
|
|
||||||
anchors.verticalCenter: textFieldWithUnit.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item
|
|
||||||
{
|
|
||||||
width: textField.width
|
|
||||||
height: textField.height
|
|
||||||
|
|
||||||
id: textFieldWithUnit
|
|
||||||
TextField
|
|
||||||
{
|
|
||||||
id: textField
|
|
||||||
text: {
|
|
||||||
const value = propertyProvider.properties.value;
|
|
||||||
return value ? value : "";
|
|
||||||
}
|
|
||||||
validator: RegExpValidator { regExp: _allowNegative ? /-?[0-9\.,]{0,6}/ : /[0-9\.,]{0,6}/ }
|
|
||||||
onEditingFinished:
|
|
||||||
{
|
|
||||||
if (propertyProvider && text != propertyProvider.properties.value)
|
|
||||||
{
|
|
||||||
// For some properties like the extruder-compatible material diameter, they need to
|
|
||||||
// trigger many updates, such as the available materials, the current material may
|
|
||||||
// need to be switched, etc. Although setting the diameter can be done directly via
|
|
||||||
// the provider, all the updates that need to be triggered then need to depend on
|
|
||||||
// the metadata update, a signal that can be fired way too often. The update functions
|
|
||||||
// can have if-checks to filter out the irrelevant updates, but still it incurs unnecessary
|
|
||||||
// overhead.
|
|
||||||
// The ExtruderStack class has a dedicated function for this call "setCompatibleMaterialDiameter()",
|
|
||||||
// and it triggers the diameter update signals only when it is needed. Here it is optionally
|
|
||||||
// choose to use setCompatibleMaterialDiameter() or other more specific functions that
|
|
||||||
// are available.
|
|
||||||
if (_setValueFunction !== undefined)
|
|
||||||
{
|
{
|
||||||
_setValueFunction(text)
|
text: model.name
|
||||||
}
|
}
|
||||||
else
|
|
||||||
{
|
|
||||||
propertyProvider.setPropertyValue("value", text)
|
|
||||||
}
|
|
||||||
if(_forceUpdateOnChange)
|
|
||||||
{
|
|
||||||
manager.forceUpdate()
|
|
||||||
}
|
|
||||||
if(_afterOnEditingFinished)
|
|
||||||
{
|
|
||||||
_afterOnEditingFinished()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
text: unit
|
|
||||||
anchors.right: textField.right
|
|
||||||
anchors.rightMargin: y - textField.y
|
|
||||||
anchors.verticalCenter: textField.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component
|
|
||||||
{
|
|
||||||
id: comboBoxWithOptions
|
|
||||||
UM.TooltipArea
|
|
||||||
{
|
|
||||||
height: childrenRect.height
|
|
||||||
width: childrenRect.width
|
|
||||||
text: _tooltip
|
|
||||||
|
|
||||||
property bool _isExtruderSetting: (typeof(isExtruderSetting) === 'undefined') ? false : isExtruderSetting
|
|
||||||
property bool _forceUpdateOnChange: (typeof(forceUpdateOnChange) === 'undefined') ? false : forceUpdateOnChange
|
|
||||||
property var _afterOnActivate: (typeof(afterOnActivate) === 'undefined') ? undefined : afterOnActivate
|
|
||||||
property string _label: (typeof(label) === 'undefined') ? "" : label
|
|
||||||
property string _tooltip: (typeof(tooltip) === 'undefined') ? propertyProvider.properties.description : tooltip
|
|
||||||
|
|
||||||
UM.SettingPropertyProvider
|
|
||||||
{
|
|
||||||
id: propertyProvider
|
|
||||||
|
|
||||||
containerStackId: {
|
|
||||||
if(_isExtruderSetting)
|
|
||||||
{
|
|
||||||
if(settingsTabs.currentIndex > 0)
|
|
||||||
{
|
|
||||||
return Cura.ExtruderManager.extruderIds[String(settingsTabs.currentIndex - 1)];
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return base.activeMachineId
|
|
||||||
}
|
|
||||||
key: settingKey
|
|
||||||
watchedProperties: [ "value", "options", "description" ]
|
|
||||||
storeIndex: manager.containerIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
Row
|
|
||||||
{
|
|
||||||
spacing: UM.Theme.getSize("default_margin").width
|
|
||||||
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
text: _label
|
|
||||||
visible: _label != ""
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: Math.max(0, settingsTabs.labelColumnWidth)
|
|
||||||
anchors.verticalCenter: comboBox.verticalCenter
|
|
||||||
}
|
|
||||||
ComboBox
|
|
||||||
{
|
|
||||||
id: comboBox
|
|
||||||
model: ListModel
|
|
||||||
{
|
|
||||||
id: optionsModel
|
|
||||||
Component.onCompleted:
|
|
||||||
{
|
|
||||||
// Options come in as a string-representation of an OrderedDict
|
|
||||||
var options = propertyProvider.properties.options.match(/^OrderedDict\(\[\((.*)\)\]\)$/);
|
|
||||||
if(options)
|
|
||||||
{
|
|
||||||
options = options[1].split("), (")
|
|
||||||
for(var i = 0; i < options.length; i++)
|
|
||||||
{
|
|
||||||
var option = options[i].substring(1, options[i].length - 1).split("', '")
|
|
||||||
optionsModel.append({text: option[1], value: option[0]});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
currentIndex:
|
|
||||||
{
|
|
||||||
var currentValue = propertyProvider.properties.value;
|
|
||||||
var index = 0;
|
|
||||||
for(var i = 0; i < optionsModel.count; i++)
|
|
||||||
{
|
|
||||||
if(optionsModel.get(i).value == currentValue) {
|
|
||||||
index = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
onActivated:
|
|
||||||
{
|
|
||||||
if(propertyProvider.properties.value != optionsModel.get(index).value)
|
|
||||||
{
|
|
||||||
propertyProvider.setPropertyValue("value", optionsModel.get(index).value);
|
|
||||||
if(_forceUpdateOnChange)
|
|
||||||
{
|
|
||||||
manager.forceUpdate();
|
|
||||||
}
|
|
||||||
if(_afterOnActivate)
|
|
||||||
{
|
|
||||||
_afterOnActivate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component
|
|
||||||
{
|
|
||||||
id: gcodeTextArea
|
|
||||||
|
|
||||||
UM.TooltipArea
|
|
||||||
{
|
|
||||||
height: gcodeArea.height
|
|
||||||
width: gcodeArea.width
|
|
||||||
text: _tooltip
|
|
||||||
|
|
||||||
property bool _isExtruderSetting: (typeof(isExtruderSetting) === 'undefined') ? false : isExtruderSetting
|
|
||||||
property string _tooltip: (typeof(tooltip) === 'undefined') ? propertyProvider.properties.description : tooltip
|
|
||||||
|
|
||||||
UM.SettingPropertyProvider
|
|
||||||
{
|
|
||||||
id: propertyProvider
|
|
||||||
|
|
||||||
containerStackId: {
|
|
||||||
if(_isExtruderSetting)
|
|
||||||
{
|
|
||||||
if(settingsTabs.currentIndex > 0)
|
|
||||||
{
|
|
||||||
return Cura.ExtruderManager.extruderIds[String(settingsTabs.currentIndex - 1)];
|
|
||||||
}
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
return base.activeMachineId
|
|
||||||
}
|
|
||||||
key: settingKey
|
|
||||||
watchedProperties: [ "value", "description" ]
|
|
||||||
storeIndex: manager.containerIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
TextArea
|
|
||||||
{
|
|
||||||
id: gcodeArea
|
|
||||||
width: areaWidth
|
|
||||||
height: areaHeight
|
|
||||||
font: UM.Theme.getFont("fixed")
|
|
||||||
text: (propertyProvider.properties.value) ? propertyProvider.properties.value : ""
|
|
||||||
onActiveFocusChanged:
|
|
||||||
{
|
|
||||||
if(!activeFocus)
|
|
||||||
{
|
|
||||||
propertyProvider.setPropertyValue("value", gcodeArea.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Component.onCompleted:
|
|
||||||
{
|
|
||||||
wrapMode = TextEdit.NoWrap;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component
|
|
||||||
{
|
|
||||||
id: headPolygonTextField
|
|
||||||
UM.TooltipArea
|
|
||||||
{
|
|
||||||
height: textField.height
|
|
||||||
width: textField.width
|
|
||||||
text: tooltip
|
|
||||||
|
|
||||||
property string _label: (typeof(label) === 'undefined') ? "" : label
|
|
||||||
|
|
||||||
Row
|
|
||||||
{
|
|
||||||
spacing: UM.Theme.getSize("default_margin").width
|
|
||||||
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
text: _label
|
|
||||||
visible: _label != ""
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: Math.max(0, settingsTabs.labelColumnWidth)
|
|
||||||
anchors.verticalCenter: textFieldWithUnit.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item
|
|
||||||
{
|
|
||||||
id: textFieldWithUnit
|
|
||||||
width: textField.width
|
|
||||||
height: textField.height
|
|
||||||
|
|
||||||
TextField
|
|
||||||
{
|
|
||||||
id: textField
|
|
||||||
text:
|
|
||||||
{
|
|
||||||
var polygon = JSON.parse(machineHeadPolygonProvider.properties.value);
|
|
||||||
var item = (axis == "x") ? 0 : 1
|
|
||||||
var result = polygon[0][item];
|
|
||||||
for(var i = 1; i < polygon.length; i++) {
|
|
||||||
if (side == "min") {
|
|
||||||
result = Math.min(result, polygon[i][item]);
|
|
||||||
} else {
|
|
||||||
result = Math.max(result, polygon[i][item]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
result = Math.abs(result);
|
|
||||||
printHeadPolygon[axis][side] = result;
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
validator: RegExpValidator { regExp: /[0-9\.,]{0,6}/ }
|
|
||||||
onEditingFinished:
|
|
||||||
{
|
|
||||||
printHeadPolygon[axis][side] = parseFloat(textField.text.replace(',','.'));
|
|
||||||
var polygon = [];
|
|
||||||
polygon.push([-printHeadPolygon["x"]["min"], printHeadPolygon["y"]["max"]]);
|
|
||||||
polygon.push([-printHeadPolygon["x"]["min"],-printHeadPolygon["y"]["min"]]);
|
|
||||||
polygon.push([ printHeadPolygon["x"]["max"], printHeadPolygon["y"]["max"]]);
|
|
||||||
polygon.push([ printHeadPolygon["x"]["max"],-printHeadPolygon["y"]["min"]]);
|
|
||||||
var polygon_string = JSON.stringify(polygon);
|
|
||||||
if(polygon_string != machineHeadPolygonProvider.properties.value)
|
|
||||||
{
|
|
||||||
machineHeadPolygonProvider.setPropertyValue("value", polygon_string);
|
|
||||||
manager.forceUpdate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "mm")
|
|
||||||
anchors.right: textField.right
|
|
||||||
anchors.rightMargin: y - textField.y
|
|
||||||
anchors.verticalCenter: textField.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
property var printHeadPolygon:
|
|
||||||
{
|
|
||||||
"x": {
|
|
||||||
"min": 0,
|
|
||||||
"max": 0,
|
|
||||||
},
|
|
||||||
"y": {
|
|
||||||
"min": 0,
|
|
||||||
"max": 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
UM.SettingPropertyProvider
|
|
||||||
{
|
|
||||||
id: machineExtruderCountProvider
|
|
||||||
|
|
||||||
containerStackId: base.activeMachineId
|
|
||||||
key: "machine_extruder_count"
|
|
||||||
watchedProperties: [ "value", "description" ]
|
|
||||||
storeIndex: manager.containerIndex
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.SettingPropertyProvider
|
|
||||||
{
|
|
||||||
id: machineHeadPolygonProvider
|
|
||||||
|
|
||||||
containerStackId: base.activeMachineId
|
|
||||||
key: "machine_head_with_fans_polygon"
|
|
||||||
watchedProperties: [ "value" ]
|
|
||||||
storeIndex: manager.containerIndex
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
180
plugins/MachineSettingsAction/MachineSettingsExtruderTab.qml
Normal file
180
plugins/MachineSettingsAction/MachineSettingsExtruderTab.qml
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.10
|
||||||
|
import QtQuick.Controls 2.3
|
||||||
|
|
||||||
|
import UM 1.3 as UM
|
||||||
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// This component contains the content for the "Welcome" page of the welcome on-boarding process.
|
||||||
|
//
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
id: base
|
||||||
|
UM.I18nCatalog { id: catalog; name: "cura" }
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
|
||||||
|
property int labelWidth: 210 * screenScaleFactor
|
||||||
|
property int controlWidth: (UM.Theme.getSize("setting_control").width * 3 / 4) | 0
|
||||||
|
property var labelFont: UM.Theme.getFont("medium")
|
||||||
|
|
||||||
|
property int columnWidth: ((parent.width - 2 * UM.Theme.getSize("default_margin").width) / 2) | 0
|
||||||
|
property int columnSpacing: 3 * screenScaleFactor
|
||||||
|
property int propertyStoreIndex: manager ? manager.storeContainerIndex : 1 // definition_changes
|
||||||
|
|
||||||
|
property string extruderStackId: ""
|
||||||
|
property int extruderPosition: 0
|
||||||
|
property var forceUpdateFunction: manager.forceUpdate
|
||||||
|
|
||||||
|
function updateMaterialDiameter()
|
||||||
|
{
|
||||||
|
manager.updateMaterialForDiameter(extruderPosition)
|
||||||
|
}
|
||||||
|
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
id: upperBlock
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
|
height: childrenRect.height
|
||||||
|
|
||||||
|
// =======================================
|
||||||
|
// Left-side column "Nozzle Settings"
|
||||||
|
// =======================================
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
width: parent.width * 2 / 3
|
||||||
|
|
||||||
|
spacing: base.columnSpacing
|
||||||
|
|
||||||
|
Label // Title Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@title:label", "Nozzle Settings")
|
||||||
|
font: UM.Theme.getFont("medium_bold")
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.NumericTextFieldWithUnit // "Nozzle size"
|
||||||
|
{
|
||||||
|
id: extruderNozzleSizeField
|
||||||
|
visible: !Cura.MachineManager.hasVariants
|
||||||
|
containerStackId: base.extruderStackId
|
||||||
|
settingKey: "machine_nozzle_size"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
labelText: catalog.i18nc("@label", "Nozzle size")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
unitText: catalog.i18nc("@label", "mm")
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.NumericTextFieldWithUnit // "Compatible material diameter"
|
||||||
|
{
|
||||||
|
id: extruderCompatibleMaterialDiameterField
|
||||||
|
containerStackId: base.extruderStackId
|
||||||
|
settingKey: "material_diameter"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
labelText: catalog.i18nc("@label", "Compatible material diameter")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
unitText: catalog.i18nc("@label", "mm")
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
// Other modules won't automatically respond after the user changes the value, so we need to force it.
|
||||||
|
afterOnEditingFinishedFunction: updateMaterialDiameter
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.NumericTextFieldWithUnit // "Nozzle offset X"
|
||||||
|
{
|
||||||
|
id: extruderNozzleOffsetXField
|
||||||
|
containerStackId: base.extruderStackId
|
||||||
|
settingKey: "machine_nozzle_offset_x"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
labelText: catalog.i18nc("@label", "Nozzle offset X")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
unitText: catalog.i18nc("@label", "mm")
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.NumericTextFieldWithUnit // "Nozzle offset Y"
|
||||||
|
{
|
||||||
|
id: extruderNozzleOffsetYField
|
||||||
|
containerStackId: base.extruderStackId
|
||||||
|
settingKey: "machine_nozzle_offset_y"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
labelText: catalog.i18nc("@label", "Nozzle offset Y")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
unitText: catalog.i18nc("@label", "mm")
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.NumericTextFieldWithUnit // "Cooling Fan Number"
|
||||||
|
{
|
||||||
|
id: extruderNozzleCoolingFanNumberField
|
||||||
|
containerStackId: base.extruderStackId
|
||||||
|
settingKey: "machine_extruder_cooling_fan_number"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
labelText: catalog.i18nc("@label", "Cooling Fan Number")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
unitText: ""
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item // Extruder Start and End G-code
|
||||||
|
{
|
||||||
|
id: lowerBlock
|
||||||
|
anchors.top: upperBlock.bottom
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
|
Cura.GcodeTextArea // "Extruder Start G-code"
|
||||||
|
{
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
|
||||||
|
anchors.left: parent.left
|
||||||
|
width: base.columnWidth - UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
|
labelText: catalog.i18nc("@title:label", "Extruder Start G-code")
|
||||||
|
containerStackId: base.extruderStackId
|
||||||
|
settingKey: "machine_extruder_start_code"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.GcodeTextArea // "Extruder End G-code"
|
||||||
|
{
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
|
||||||
|
anchors.right: parent.right
|
||||||
|
width: base.columnWidth - UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
|
labelText: catalog.i18nc("@title:label", "Extruder End G-code")
|
||||||
|
containerStackId: base.extruderStackId
|
||||||
|
settingKey: "machine_extruder_end_code"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
353
plugins/MachineSettingsAction/MachineSettingsPrinterTab.qml
Normal file
353
plugins/MachineSettingsAction/MachineSettingsPrinterTab.qml
Normal file
|
@ -0,0 +1,353 @@
|
||||||
|
// Copyright (c) 2019 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.10
|
||||||
|
import QtQuick.Controls 2.3
|
||||||
|
|
||||||
|
import UM 1.3 as UM
|
||||||
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
|
|
||||||
|
//
|
||||||
|
// This the content in the "Printer" tab in the Machine Settings dialog.
|
||||||
|
//
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
id: base
|
||||||
|
UM.I18nCatalog { id: catalog; name: "cura" }
|
||||||
|
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
|
||||||
|
property int labelWidth: 120 * screenScaleFactor
|
||||||
|
property int controlWidth: (UM.Theme.getSize("setting_control").width * 3 / 4) | 0
|
||||||
|
property var labelFont: UM.Theme.getFont("default")
|
||||||
|
|
||||||
|
property int columnWidth: ((parent.width - 2 * UM.Theme.getSize("default_margin").width) / 2) | 0
|
||||||
|
property int columnSpacing: 3 * screenScaleFactor
|
||||||
|
property int propertyStoreIndex: manager ? manager.storeContainerIndex : 1 // definition_changes
|
||||||
|
|
||||||
|
property string machineStackId: Cura.MachineManager.activeMachineId
|
||||||
|
|
||||||
|
property var forceUpdateFunction: manager.forceUpdate
|
||||||
|
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
id: upperBlock
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
|
height: childrenRect.height
|
||||||
|
|
||||||
|
// =======================================
|
||||||
|
// Left-side column for "Printer Settings"
|
||||||
|
// =======================================
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.left: parent.left
|
||||||
|
width: base.columnWidth
|
||||||
|
|
||||||
|
spacing: base.columnSpacing
|
||||||
|
|
||||||
|
Label // Title Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@title:label", "Printer Settings")
|
||||||
|
font: UM.Theme.getFont("medium_bold")
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.NumericTextFieldWithUnit // "X (Width)"
|
||||||
|
{
|
||||||
|
id: machineXWidthField
|
||||||
|
containerStackId: machineStackId
|
||||||
|
settingKey: "machine_width"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
labelText: catalog.i18nc("@label", "X (Width)")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
unitText: catalog.i18nc("@label", "mm")
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.NumericTextFieldWithUnit // "Y (Depth)"
|
||||||
|
{
|
||||||
|
id: machineYDepthField
|
||||||
|
containerStackId: machineStackId
|
||||||
|
settingKey: "machine_depth"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
labelText: catalog.i18nc("@label", "Y (Depth)")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
unitText: catalog.i18nc("@label", "mm")
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.NumericTextFieldWithUnit // "Z (Height)"
|
||||||
|
{
|
||||||
|
id: machineZHeightField
|
||||||
|
containerStackId: machineStackId
|
||||||
|
settingKey: "machine_height"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
labelText: catalog.i18nc("@label", "Z (Height)")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
unitText: catalog.i18nc("@label", "mm")
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.ComboBoxWithOptions // "Build plate shape"
|
||||||
|
{
|
||||||
|
id: buildPlateShapeComboBox
|
||||||
|
containerStackId: machineStackId
|
||||||
|
settingKey: "machine_shape"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
labelText: catalog.i18nc("@label", "Build plate shape")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.SimpleCheckBox // "Origin at center"
|
||||||
|
{
|
||||||
|
id: originAtCenterCheckBox
|
||||||
|
containerStackId: machineStackId
|
||||||
|
settingKey: "machine_center_is_zero"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
labelText: catalog.i18nc("@label", "Origin at center")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.SimpleCheckBox // "Heated bed"
|
||||||
|
{
|
||||||
|
id: heatedBedCheckBox
|
||||||
|
containerStackId: machineStackId
|
||||||
|
settingKey: "machine_heated_bed"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
labelText: catalog.i18nc("@label", "Heated bed")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.ComboBoxWithOptions // "G-code flavor"
|
||||||
|
{
|
||||||
|
id: gcodeFlavorComboBox
|
||||||
|
containerStackId: machineStackId
|
||||||
|
settingKey: "machine_gcode_flavor"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
labelText: catalog.i18nc("@label", "G-code flavor")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
// FIXME(Lipu): better document this.
|
||||||
|
// This has something to do with UM2 and UM2+ regarding "has_material" and the gcode flavor settings.
|
||||||
|
// I don't remember exactly what.
|
||||||
|
afterOnEditingFinishedFunction: manager.updateHasMaterialsMetadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =======================================
|
||||||
|
// Right-side column for "Printhead Settings"
|
||||||
|
// =======================================
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.right: parent.right
|
||||||
|
width: base.columnWidth
|
||||||
|
|
||||||
|
spacing: base.columnSpacing
|
||||||
|
|
||||||
|
Label // Title Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@title:label", "Printhead Settings")
|
||||||
|
font: UM.Theme.getFont("medium_bold")
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.PrintHeadMinMaxTextField // "X min"
|
||||||
|
{
|
||||||
|
id: machineXMinField
|
||||||
|
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
|
||||||
|
labelText: catalog.i18nc("@label", "X min")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
unitText: catalog.i18nc("@label", "mm")
|
||||||
|
|
||||||
|
axisName: "x"
|
||||||
|
axisMinOrMax: "min"
|
||||||
|
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.PrintHeadMinMaxTextField // "Y min"
|
||||||
|
{
|
||||||
|
id: machineYMinField
|
||||||
|
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
|
||||||
|
labelText: catalog.i18nc("@label", "Y min")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
unitText: catalog.i18nc("@label", "mm")
|
||||||
|
|
||||||
|
axisName: "y"
|
||||||
|
axisMinOrMax: "min"
|
||||||
|
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.PrintHeadMinMaxTextField // "X max"
|
||||||
|
{
|
||||||
|
id: machineXMaxField
|
||||||
|
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
|
||||||
|
labelText: catalog.i18nc("@label", "X max")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
unitText: catalog.i18nc("@label", "mm")
|
||||||
|
|
||||||
|
axisName: "x"
|
||||||
|
axisMinOrMax: "max"
|
||||||
|
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.PrintHeadMinMaxTextField // "Y max"
|
||||||
|
{
|
||||||
|
id: machineYMaxField
|
||||||
|
|
||||||
|
containerStackId: machineStackId
|
||||||
|
settingKey: "machine_head_with_fans_polygon"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
|
||||||
|
labelText: catalog.i18nc("@label", "Y max")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
unitText: catalog.i18nc("@label", "mm")
|
||||||
|
|
||||||
|
axisName: "y"
|
||||||
|
axisMinOrMax: "max"
|
||||||
|
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.NumericTextFieldWithUnit // "Gantry Height"
|
||||||
|
{
|
||||||
|
id: machineGantryHeightField
|
||||||
|
containerStackId: machineStackId
|
||||||
|
settingKey: "gantry_height"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
labelText: catalog.i18nc("@label", "Gantry Height")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
unitText: catalog.i18nc("@label", "mm")
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.ComboBoxWithOptions // "Number of Extruders"
|
||||||
|
{
|
||||||
|
id: numberOfExtrudersComboBox
|
||||||
|
containerStackId: machineStackId
|
||||||
|
settingKey: "machine_extruder_count"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
labelText: catalog.i18nc("@label", "Number of Extruders")
|
||||||
|
labelFont: base.labelFont
|
||||||
|
labelWidth: base.labelWidth
|
||||||
|
controlWidth: base.controlWidth
|
||||||
|
forceUpdateOnChangeFunction: forceUpdateFunction
|
||||||
|
// FIXME(Lipu): better document this.
|
||||||
|
// This has something to do with UM2 and UM2+ regarding "has_material" and the gcode flavor settings.
|
||||||
|
// I don't remember exactly what.
|
||||||
|
afterOnEditingFinishedFunction: manager.updateHasMaterialsMetadata
|
||||||
|
setValueFunction: manager.setMachineExtruderCount
|
||||||
|
|
||||||
|
optionModel: ListModel
|
||||||
|
{
|
||||||
|
id: extruderCountModel
|
||||||
|
|
||||||
|
Component.onCompleted:
|
||||||
|
{
|
||||||
|
update()
|
||||||
|
}
|
||||||
|
|
||||||
|
function update()
|
||||||
|
{
|
||||||
|
clear()
|
||||||
|
for (var i = 1; i <= Cura.MachineManager.activeMachine.maxExtruderCount; i++)
|
||||||
|
{
|
||||||
|
// Use String as value. JavaScript only has Number. PropertyProvider.setPropertyValue()
|
||||||
|
// takes a QVariant as value, and Number gets translated into a float. This will cause problem
|
||||||
|
// for integer settings such as "Number of Extruders".
|
||||||
|
append({ text: String(i), value: String(i) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections
|
||||||
|
{
|
||||||
|
target: Cura.MachineManager
|
||||||
|
onGlobalContainerChanged: extruderCountModel.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item // Start and End G-code
|
||||||
|
{
|
||||||
|
id: lowerBlock
|
||||||
|
anchors.top: upperBlock.bottom
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.margins: UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
|
Cura.GcodeTextArea // "Start G-code"
|
||||||
|
{
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
|
||||||
|
anchors.left: parent.left
|
||||||
|
width: base.columnWidth - UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
|
labelText: catalog.i18nc("@title:label", "Start G-code")
|
||||||
|
containerStackId: machineStackId
|
||||||
|
settingKey: "machine_start_gcode"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.GcodeTextArea // "End G-code"
|
||||||
|
{
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
|
||||||
|
anchors.right: parent.right
|
||||||
|
width: base.columnWidth - UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
|
labelText: catalog.i18nc("@title:label", "End G-code")
|
||||||
|
containerStackId: machineStackId
|
||||||
|
settingKey: "machine_end_gcode"
|
||||||
|
settingStoreIndex: propertyStoreIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,15 @@ Rectangle
|
||||||
id: viewportOverlay
|
id: viewportOverlay
|
||||||
|
|
||||||
property bool isConnected: Cura.MachineManager.activeMachineHasNetworkConnection || Cura.MachineManager.activeMachineHasCloudConnection
|
property bool isConnected: Cura.MachineManager.activeMachineHasNetworkConnection || Cura.MachineManager.activeMachineHasCloudConnection
|
||||||
property bool isNetworkConfigurable: ["Ultimaker 3", "Ultimaker 3 Extended", "Ultimaker S5"].indexOf(Cura.MachineManager.activeMachineDefinitionName) > -1
|
property bool isNetworkConfigurable:
|
||||||
|
{
|
||||||
|
if(Cura.MachineManager.activeMachine === null)
|
||||||
|
{
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return Cura.MachineManager.activeMachine.supportsNetworkConnection
|
||||||
|
}
|
||||||
|
|
||||||
property bool isNetworkConfigured:
|
property bool isNetworkConfigured:
|
||||||
{
|
{
|
||||||
// Readability:
|
// Readability:
|
||||||
|
@ -98,7 +106,6 @@ Rectangle
|
||||||
width: contentWidth
|
width: contentWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
// CASE 3: CAN NOT MONITOR
|
|
||||||
Label
|
Label
|
||||||
{
|
{
|
||||||
id: noNetworkLabel
|
id: noNetworkLabel
|
||||||
|
@ -106,24 +113,8 @@ Rectangle
|
||||||
{
|
{
|
||||||
horizontalCenter: parent.horizontalCenter
|
horizontalCenter: parent.horizontalCenter
|
||||||
}
|
}
|
||||||
visible: !isNetworkConfigured
|
|
||||||
text: catalog.i18nc("@info", "Please select a network connected printer to monitor.")
|
|
||||||
font: UM.Theme.getFont("medium")
|
|
||||||
color: UM.Theme.getColor("monitor_text_primary")
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: contentWidth
|
|
||||||
lineHeight: UM.Theme.getSize("monitor_text_line_large").height
|
|
||||||
lineHeightMode: Text.FixedHeight
|
|
||||||
}
|
|
||||||
Label
|
|
||||||
{
|
|
||||||
id: noNetworkUltimakerLabel
|
|
||||||
anchors
|
|
||||||
{
|
|
||||||
horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
visible: !isNetworkConfigured && isNetworkConfigurable
|
visible: !isNetworkConfigured && isNetworkConfigurable
|
||||||
text: catalog.i18nc("@info", "Please connect your Ultimaker printer to your local network.")
|
text: catalog.i18nc("@info", "Please connect your printer to the network.")
|
||||||
font: UM.Theme.getFont("medium")
|
font: UM.Theme.getFont("medium")
|
||||||
color: UM.Theme.getColor("monitor_text_primary")
|
color: UM.Theme.getColor("monitor_text_primary")
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
|
@ -135,7 +126,7 @@ Rectangle
|
||||||
{
|
{
|
||||||
anchors
|
anchors
|
||||||
{
|
{
|
||||||
left: noNetworkUltimakerLabel.left
|
left: noNetworkLabel.left
|
||||||
}
|
}
|
||||||
visible: !isNetworkConfigured && isNetworkConfigurable
|
visible: !isNetworkConfigured && isNetworkConfigurable
|
||||||
height: UM.Theme.getSize("monitor_text_line").height
|
height: UM.Theme.getSize("monitor_text_line").height
|
||||||
|
@ -160,7 +151,7 @@ Rectangle
|
||||||
verticalCenter: externalLinkIcon.verticalCenter
|
verticalCenter: externalLinkIcon.verticalCenter
|
||||||
}
|
}
|
||||||
color: UM.Theme.getColor("monitor_text_link")
|
color: UM.Theme.getColor("monitor_text_link")
|
||||||
font: UM.Theme.getFont("medium") // 14pt, regular
|
font: UM.Theme.getFont("medium")
|
||||||
linkColor: UM.Theme.getColor("monitor_text_link")
|
linkColor: UM.Theme.getColor("monitor_text_link")
|
||||||
text: catalog.i18nc("@label link to technical assistance", "View user manuals online")
|
text: catalog.i18nc("@label link to technical assistance", "View user manuals online")
|
||||||
renderType: Text.NativeRendering
|
renderType: Text.NativeRendering
|
||||||
|
@ -170,14 +161,8 @@ Rectangle
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
onClicked: Qt.openUrlExternally("https://ultimaker.com/en/resources/manuals/ultimaker-3d-printers")
|
onClicked: Qt.openUrlExternally("https://ultimaker.com/en/resources/manuals/ultimaker-3d-printers")
|
||||||
onEntered:
|
onEntered: manageQueueText.font.underline = true
|
||||||
{
|
onExited: manageQueueText.font.underline = false
|
||||||
manageQueueText.font.underline = true
|
|
||||||
}
|
|
||||||
onExited:
|
|
||||||
{
|
|
||||||
manageQueueText.font.underline = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,6 @@
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
import os.path
|
import os.path
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.PluginRegistry import PluginRegistry
|
|
||||||
from UM.Resources import Resources
|
|
||||||
from cura.Stages.CuraStage import CuraStage
|
from cura.Stages.CuraStage import CuraStage
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -162,7 +162,7 @@ class PostProcessingPlugin(QObject, Extension):
|
||||||
loaded_script = importlib.util.module_from_spec(spec)
|
loaded_script = importlib.util.module_from_spec(spec)
|
||||||
if spec.loader is None:
|
if spec.loader is None:
|
||||||
continue
|
continue
|
||||||
spec.loader.exec_module(loaded_script)
|
spec.loader.exec_module(loaded_script) # type: ignore
|
||||||
sys.modules[script_name] = loaded_script #TODO: This could be a security risk. Overwrite any module with a user-provided name?
|
sys.modules[script_name] = loaded_script #TODO: This could be a security risk. Overwrite any module with a user-provided name?
|
||||||
|
|
||||||
loaded_class = getattr(loaded_script, script_name)
|
loaded_class = getattr(loaded_script, script_name)
|
||||||
|
|
|
@ -36,7 +36,7 @@ class DisplayFilenameAndLayerOnLCD(Script):
|
||||||
name = self.getSettingValueByKey("name")
|
name = self.getSettingValueByKey("name")
|
||||||
else:
|
else:
|
||||||
name = Application.getInstance().getPrintInformation().jobName
|
name = Application.getInstance().getPrintInformation().jobName
|
||||||
lcd_text = "M117 " + name + " layer: "
|
lcd_text = "M117 " + name + " layer "
|
||||||
i = 0
|
i = 0
|
||||||
for layer in data:
|
for layer in data:
|
||||||
display_text = lcd_text + str(i)
|
display_text = lcd_text + str(i)
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
# Copyright (c) 2019 Ultimaker B.V.
|
# Copyright (c) 2019 Ultimaker B.V.
|
||||||
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Optional, Tuple
|
from typing import List
|
||||||
|
|
||||||
from UM.Logger import Logger
|
|
||||||
from ..Script import Script
|
from ..Script import Script
|
||||||
|
|
||||||
class FilamentChange(Script):
|
class FilamentChange(Script):
|
||||||
|
@ -65,9 +63,10 @@ class FilamentChange(Script):
|
||||||
}
|
}
|
||||||
}"""
|
}"""
|
||||||
|
|
||||||
def execute(self, data: list):
|
## Inserts the filament change g-code at specific layer numbers.
|
||||||
|
# \param data A list of layers of g-code.
|
||||||
"""data is a list. Each index contains a layer"""
|
# \return A similar list, with filament change commands inserted.
|
||||||
|
def execute(self, data: List[str]):
|
||||||
layer_nums = self.getSettingValueByKey("layer_number")
|
layer_nums = self.getSettingValueByKey("layer_number")
|
||||||
initial_retract = self.getSettingValueByKey("initial_retract")
|
initial_retract = self.getSettingValueByKey("initial_retract")
|
||||||
later_retract = self.getSettingValueByKey("later_retract")
|
later_retract = self.getSettingValueByKey("later_retract")
|
||||||
|
@ -88,32 +87,13 @@ class FilamentChange(Script):
|
||||||
if y_pos is not None:
|
if y_pos is not None:
|
||||||
color_change = color_change + (" Y%.2f" % y_pos)
|
color_change = color_change + (" Y%.2f" % y_pos)
|
||||||
|
|
||||||
color_change = color_change + " ; Generated by FilamentChange plugin"
|
color_change = color_change + " ; Generated by FilamentChange plugin\n"
|
||||||
|
|
||||||
layer_targets = layer_nums.split(",")
|
layer_targets = layer_nums.split(",")
|
||||||
if len(layer_targets) > 0:
|
if len(layer_targets) > 0:
|
||||||
for layer_num in layer_targets:
|
for layer_num in layer_targets:
|
||||||
layer_num = int(layer_num.strip())
|
layer_num = int(layer_num.strip()) + 1
|
||||||
if layer_num <= len(data):
|
if layer_num <= len(data):
|
||||||
index, layer_data = self._searchLayerData(data, layer_num - 1)
|
data[layer_num] = color_change + data[layer_num]
|
||||||
if layer_data is None:
|
|
||||||
Logger.log("e", "Could not found the layer")
|
|
||||||
continue
|
|
||||||
lines = layer_data.split("\n")
|
|
||||||
lines.insert(2, color_change)
|
|
||||||
final_line = "\n".join(lines)
|
|
||||||
data[index] = final_line
|
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
## This method returns the data corresponding with the indicated layer number, looking in the gcode for
|
|
||||||
# the occurrence of this layer number.
|
|
||||||
def _searchLayerData(self, data: list, layer_num: int) -> Tuple[int, Optional[str]]:
|
|
||||||
for index, layer_data in enumerate(data):
|
|
||||||
first_line = layer_data.split("\n")[0]
|
|
||||||
# The first line should contain the layer number at the beginning.
|
|
||||||
if first_line[:len(self._layer_keyword)] == self._layer_keyword:
|
|
||||||
# If found the layer that we are looking for, then return the data
|
|
||||||
if first_line[len(self._layer_keyword):] == str(layer_num):
|
|
||||||
return index, layer_data
|
|
||||||
return 0, None
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue