Merge branch 'master' into feature_model_list

This commit is contained in:
Diego Prado Gesto 2019-05-07 11:57:31 +02:00
commit 9e5e57e6c5
514 changed files with 80530 additions and 9839 deletions

1
.gitignore vendored
View file

@ -71,3 +71,4 @@ run.sh
.scannerwork/ .scannerwork/
CuraEngine CuraEngine
/.coverage

12
.gitlab-ci.yml Normal file
View file

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

View file

@ -1,11 +1,10 @@
project(cura NONE) project(cura)
cmake_minimum_required(VERSION 2.8.12) cmake_minimum_required(VERSION 3.6)
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/
${CMAKE_MODULE_PATH})
include(GNUInstallDirs) include(GNUInstallDirs)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository") set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository")
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository") set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
@ -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()

View file

@ -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
View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View 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"]

View file

@ -1,7 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
class GenericMaterialsModel(BaseMaterialsModel): class GenericMaterialsModel(BaseMaterialsModel):

View file

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

View file

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

View file

@ -1,9 +1,8 @@
# Copyright (c) 2018 Ultimaker B.V. # Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher. # Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty from PyQt5.QtCore import Qt, pyqtSignal
from UM.Qt.ListModel import ListModel from UM.Qt.ListModel import ListModel
from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
class MaterialTypesModel(ListModel): class MaterialTypesModel(ListModel):

View file

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

View file

@ -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:

View file

@ -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 "",

View file

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

View file

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

View file

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

View file

@ -3,14 +3,15 @@
from typing import TYPE_CHECKING, Set, Union, Optional from typing import TYPE_CHECKING, Set, Union, Optional
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from PyQt5.QtCore import QTimer from PyQt5.QtCore import QTimer
from .PrinterOutputController import PrinterOutputController
if TYPE_CHECKING: if TYPE_CHECKING:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel from .Models.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel from .Models.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice from .PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel from .Models.ExtruderOutputModel import ExtruderOutputModel
class GenericOutputController(PrinterOutputController): class GenericOutputController(PrinterOutputController):

View file

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

View file

@ -4,7 +4,7 @@ from typing import Optional
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel from .MaterialOutputModel import MaterialOutputModel
class ExtruderConfigurationModel(QObject): class ExtruderConfigurationModel(QObject):
@ -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

View file

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

View file

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

View file

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

View file

@ -6,10 +6,10 @@ from typing import List
MYPY = False MYPY = False
if MYPY: if MYPY:
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
class ConfigurationModel(QObject): class PrinterConfigurationModel(QObject):
configurationChanged = pyqtSignal() configurationChanged = pyqtSignal()
@ -19,14 +19,14 @@ class ConfigurationModel(QObject):
self._extruder_configurations = [] # type: List[ExtruderConfigurationModel] self._extruder_configurations = [] # type: List[ExtruderConfigurationModel]
self._buildplate_configuration = "" self._buildplate_configuration = ""
def setPrinterType(self, printer_type): def setPrinterType(self, printer_type: str) -> None:
self._printer_type = printer_type self._printer_type = printer_type
@pyqtProperty(str, fset = setPrinterType, notify = configurationChanged) @pyqtProperty(str, fset = setPrinterType, notify = configurationChanged)
def printerType(self) -> str: def printerType(self) -> str:
return self._printer_type return self._printer_type
def setExtruderConfigurations(self, extruder_configurations: List["ExtruderConfigurationModel"]): def setExtruderConfigurations(self, extruder_configurations: List["ExtruderConfigurationModel"]) -> None:
if self._extruder_configurations != extruder_configurations: if self._extruder_configurations != extruder_configurations:
self._extruder_configurations = extruder_configurations self._extruder_configurations = extruder_configurations
@ -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.

View 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

View file

View 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.

View file

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

View file

@ -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:

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

View 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

View file

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

View file

@ -60,13 +60,11 @@ class ConvexHullDecorator(SceneNodeDecorator):
previous_node = self._node previous_node = self._node
# Disconnect from previous node signals # Disconnect from previous node signals
if previous_node is not None and node is not previous_node: if previous_node is not None and node is not previous_node:
previous_node.transformationChanged.disconnect(self._onChanged) previous_node.boundingBoxChanged.disconnect(self._onChanged)
previous_node.parentChanged.disconnect(self._onChanged)
super().setNode(node) super().setNode(node)
# Mypy doesn't understand that self._node is no longer optional, so just use the node.
node.transformationChanged.connect(self._onChanged) node.boundingBoxChanged.connect(self._onChanged)
node.parentChanged.connect(self._onChanged)
self._onChanged() self._onChanged()

View file

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

View file

@ -112,21 +112,21 @@ class CuraSceneNode(SceneNode):
## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box ## Override of SceneNode._calculateAABB to exclude non-printing-meshes from bounding box
def _calculateAABB(self) -> None: def _calculateAABB(self) -> None:
self._aabb = None
if self._mesh_data: if self._mesh_data:
aabb = self._mesh_data.getExtents(self.getWorldTransformation()) self._aabb = self._mesh_data.getExtents(self.getWorldTransformation())
else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0)
position = self.getWorldPosition()
aabb = AxisAlignedBox(minimum = position, maximum = position)
for child in self._children: for child in self.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":

View file

@ -47,8 +47,10 @@ class ContainerManager(QObject):
if ContainerManager.__instance is not None: if ContainerManager.__instance is not None:
raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__) raise RuntimeError("Try to create singleton '%s' more than once" % self.__class__.__name__)
ContainerManager.__instance = self ContainerManager.__instance = self
try:
super().__init__(parent = application) super().__init__(parent = application)
except TypeError:
super().__init__()
self._application = application # type: CuraApplication self._application = application # type: CuraApplication
self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry self._plugin_registry = self._application.getPluginRegistry() # type: PluginRegistry

View file

@ -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.

View file

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

View file

@ -125,7 +125,12 @@ class CuraStackBuilder:
extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains") extruder_definition_dict = global_stack.getMetaDataEntry("machine_extruder_trains")
extruder_definition_id = extruder_definition_dict[str(extruder_position)] extruder_definition_id = extruder_definition_dict[str(extruder_position)]
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

View file

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

View file

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

View file

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

View file

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

View file

@ -73,8 +73,8 @@ class SettingOverrideDecorator(SceneNodeDecorator):
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh" # use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
# has not been updated yet. # has not been updated yet.
deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh() deep_copy._is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
deep_copy._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh() deep_copy._is_non_thumbnail_visible_mesh = self._evaluateIsNonThumbnailVisibleMesh()
return deep_copy return deep_copy
@ -102,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()

View file

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

View file

@ -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"]

View file

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

View file

@ -0,0 +1,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"]

View file

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

View file

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

View file

@ -1,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]

View file

@ -5,8 +5,7 @@ import json
import math import math
import os import os
import unicodedata import unicodedata
import re # To create abbreviations for printer names. from typing import Dict, List, Optional, TYPE_CHECKING
from typing import Dict, List, Optional
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
@ -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
View file

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

View 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"]

View 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
View file

View 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"]

View file

@ -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
View 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

View file

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

View file

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

View file

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

View 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

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

View 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"
}

View file

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

View file

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

View file

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

View file

@ -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"
}

View file

@ -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:

View file

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

View file

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

View file

@ -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"]

View file

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

View file

@ -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"

View file

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

View file

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

View file

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

View file

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

View file

@ -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
}
} }

View 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
}
}
}

View 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
}
}
}

View file

@ -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
}
} }
} }
} }

View file

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

View file

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

View file

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

View file

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