Merge branch 'master' into feature_amf_reader

This commit is contained in:
Aldo Hoeben 2019-04-26 11:51:55 +02:00 committed by GitHub
commit 066b90bdea
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
336 changed files with 57051 additions and 9240 deletions

1
.gitignore vendored
View file

@ -71,3 +71,4 @@ run.sh
.scannerwork/
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)
cmake_minimum_required(VERSION 2.8.12)
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/
${CMAKE_MODULE_PATH})
project(cura)
cmake_minimum_required(VERSION 3.6)
include(GNUInstallDirs)
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository")
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
@ -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(cura/CuraVersion.py.in CuraVersion.py @ONLY)
# FIXME: Remove the code for CMake <3.12 once we have switched over completely.
# FindPython3 is a new module since CMake 3.12. It deprecates FindPythonInterp and FindPythonLibs. The FindPython3
# module is copied from the CMake repository here so in CMake <3.12 we can still use it.
if(${CMAKE_VERSION} VERSION_LESS 3.12)
# Use FindPythonInterp and FindPythonLibs for CMake <3.12
find_package(PythonInterp 3 REQUIRED)
set(Python3_EXECUTABLE ${PYTHON_EXECUTABLE})
set(Python3_VERSION ${PYTHON_VERSION_STRING})
set(Python3_VERSION_MAJOR ${PYTHON_VERSION_MAJOR})
set(Python3_VERSION_MINOR ${PYTHON_VERSION_MINOR})
set(Python3_VERSION_PATCH ${PYTHON_VERSION_PATCH})
else()
# Use FindPython3 for CMake >=3.12
find_package(Python3 REQUIRED COMPONENTS Interpreter Development)
endif()
if(NOT ${URANIUM_DIR} STREQUAL "")
set(CMAKE_MODULE_PATH "${URANIUM_DIR}/cmake")
endif()
@ -40,12 +59,12 @@ if(NOT ${URANIUM_SCRIPTS_DIR} STREQUAL "")
CREATE_TRANSLATION_TARGETS()
endif()
find_package(PythonInterp 3.5.0 REQUIRED)
install(DIRECTORY resources
DESTINATION ${CMAKE_INSTALL_DATADIR}/cura)
install(DIRECTORY plugins
DESTINATION lib${LIB_SUFFIX}/cura)
if(NOT APPLE AND NOT WIN32)
install(FILES cura_app.py
DESTINATION ${CMAKE_INSTALL_BINDIR}
@ -53,16 +72,16 @@ if(NOT APPLE AND NOT WIN32)
RENAME cura)
if(EXISTS /etc/debian_version)
install(DIRECTORY cura
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}/dist-packages
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}/dist-packages
FILES_MATCHING PATTERN *.py)
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}/dist-packages/cura)
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}/dist-packages/cura)
else()
install(DIRECTORY cura
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages
FILES_MATCHING PATTERN *.py)
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura)
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages/cura)
endif()
install(FILES ${CMAKE_BINARY_DIR}/cura.desktop
DESTINATION ${CMAKE_INSTALL_DATADIR}/applications)
@ -78,8 +97,8 @@ else()
DESTINATION ${CMAKE_INSTALL_BINDIR}
PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE GROUP_READ GROUP_EXECUTE WORLD_READ WORLD_EXECUTE)
install(DIRECTORY cura
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages
FILES_MATCHING PATTERN *.py)
install(FILES ${CMAKE_BINARY_DIR}/CuraVersion.py
DESTINATION lib${LIB_SUFFIX}/python${PYTHON_VERSION_MAJOR}.${PYTHON_VERSION_MINOR}/site-packages/cura)
DESTINATION lib${LIB_SUFFIX}/python${Python3_VERSION_MAJOR}.${Python3_VERSION_MINOR}/site-packages/cura)
endif()

View file

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

View file

@ -29,6 +29,7 @@ i18n_catalog = i18nCatalog("cura")
class Account(QObject):
# Signal emitted when user logged in or out.
loginStateChanged = pyqtSignal(bool)
accessTokenChanged = pyqtSignal()
def __init__(self, application: "CuraApplication", parent = None) -> None:
super().__init__(parent)
@ -59,8 +60,12 @@ class Account(QObject):
self._authorization_service.initialize(self._application.getPreferences())
self._authorization_service.onAuthStateChanged.connect(self._onLoginStateChanged)
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
self._authorization_service.accessTokenChanged.connect(self._onAccessTokenChanged)
self._authorization_service.loadAuthDataFromPreferences()
def _onAccessTokenChanged(self):
self.accessTokenChanged.emit()
## Returns a boolean indicating whether the given authentication is applied against staging or not.
@property
def is_staging(self) -> bool:
@ -105,7 +110,7 @@ class Account(QObject):
return None
return user_profile.profile_image_url
@pyqtProperty(str, notify=loginStateChanged)
@pyqtProperty(str, notify=accessTokenChanged)
def accessToken(self) -> Optional[str]:
return self._authorization_service.getAccessToken()

View file

@ -19,6 +19,7 @@ class AutoSave:
self._change_timer.setInterval(self._application.getPreferences().getValue("cura/autosave_delay"))
self._change_timer.setSingleShot(True)
self._enabled = True
self._saving = False
def initialize(self):
@ -32,6 +33,13 @@ class AutoSave:
if not self._saving:
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):
if self._global_stack:
self._global_stack.propertyChanged.disconnect(self._triggerTimer)

View file

@ -116,12 +116,13 @@ class Backup:
current_version = self._application.getVersion()
version_to_restore = self.meta_data.get("cura_release", "master")
if current_version != version_to_restore:
# Cannot restore version older or newer than current because settings might have changed.
# Restoring this will cause a lot of issues so we don't allow this for now.
if current_version < version_to_restore:
# Cannot restore version newer than current because settings might have changed.
Logger.log("d", "Tried to restore a Cura backup of version {version_to_restore} with cura version {current_version}".format(version_to_restore = version_to_restore, current_version = current_version))
self._showMessage(
self.catalog.i18nc("@info:backup_failed",
"Tried to restore a Cura backup that does not match your current version."))
"Tried to restore a Cura backup that is higher than the current version."))
return False
version_data_dir = Resources.getDataStoragePath()

View file

@ -51,8 +51,8 @@ class BackupsManager:
## Here we try to disable the auto-save plug-in as it might interfere with
# restoring a back-up.
def _disableAutoSave(self) -> None:
self._application.setSaveDataEnabled(False)
self._application.getAutoSave().setEnabled(False)
## Re-enable auto-save after we're done.
def _enableAutoSave(self) -> None:
self._application.setSaveDataEnabled(True)
self._application.getAutoSave().setEnabled(True)

View file

@ -13,113 +13,120 @@ from PyQt5.QtGui import QColor, QIcon
from PyQt5.QtWidgets import QMessageBox
from PyQt5.QtQml import qmlRegisterUncreatableType, qmlRegisterSingletonType, qmlRegisterType
from UM.i18n import i18nCatalog
from UM.Application import Application
from UM.Decorators import override
from UM.FlameProfiler import pyqtSlot
from UM.Logger import Logger
from UM.Message import Message
from UM.Platform import Platform
from UM.PluginError import PluginNotFoundError
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Camera import Camera
from UM.Math.Vector import Vector
from UM.Math.Quaternion import Quaternion
from UM.Resources import Resources
from UM.Preferences import Preferences
from UM.Qt.Bindings import MainWindow
from UM.Qt.QtApplication import QtApplication # The class we're inheriting from.
import UM.Util
from UM.View.SelectionPass import SelectionPass # For typing.
from UM.Math.AxisAlignedBox import AxisAlignedBox
from UM.Math.Matrix import Matrix
from UM.Platform import Platform
from UM.Resources import Resources
from UM.Scene.ToolHandle import ToolHandle
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Math.Quaternion import Quaternion
from UM.Math.Vector import Vector
from UM.Mesh.ReadMeshJob import ReadMeshJob
from UM.Logger import Logger
from UM.Preferences import Preferences
from UM.Qt.QtApplication import QtApplication #The class we're inheriting from.
from UM.View.SelectionPass import SelectionPass #For typing.
from UM.Scene.Selection import Selection
from UM.Scene.GroupDecorator import GroupDecorator
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.Validator import Validator
from UM.Message import Message
from UM.i18n import i18nCatalog
from UM.Workspace.WorkspaceReader import WorkspaceReader
from UM.Operations.AddSceneNodeOperation import AddSceneNodeOperation
from UM.Operations.GroupedOperation import GroupedOperation
from UM.Operations.SetTransformOperation import SetTransformOperation
from UM.Scene.Camera import Camera
from UM.Scene.GroupDecorator import GroupDecorator
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Selection import Selection
from UM.Scene.ToolHandle import ToolHandle
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.ContainerStack import ContainerStack
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
from UM.Settings.SettingFunction import SettingFunction
from UM.Settings.Validator import Validator
from UM.Workspace.WorkspaceReader import WorkspaceReader
from cura.API import CuraAPI
from cura.Arranging.Arrange import Arrange
from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
from cura.Arranging.ShapeArray import ShapeArray
from cura.MultiplyObjectsJob import MultiplyObjectsJob
from cura.GlobalStacksModel import GlobalStacksModel
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
from cura.Operations.SetParentOperation import SetParentOperation
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene.BlockSlicingDecorator import BlockSlicingDecorator
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
from cura.Scene.CuraSceneController import CuraSceneController
from UM.Settings.SettingDefinition import SettingDefinition, DefinitionPropertyType
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.SettingFunction import SettingFunction
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.MachineNameValidator import MachineNameValidator
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
from cura.Machines.Models.NozzleModel import NozzleModel
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
from cura.Machines.Models.QualityManagementModel import QualityManagementModel
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
from cura.Machines.Models.MachineManagementModel import MachineManagementModel
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
from cura.Scene import ZOffsetDecorator
from cura.Machines.MachineErrorChecker import MachineErrorChecker
from cura.Machines.VariantManager import VariantManager
from cura.Machines.Models.BuildPlateModel import BuildPlateModel
from cura.Machines.Models.CustomQualityProfilesDropDownMenuModel import CustomQualityProfilesDropDownMenuModel
from cura.Machines.Models.DiscoveredPrintersModel import DiscoveredPrintersModel
from cura.Machines.Models.ExtrudersModel import ExtrudersModel
from cura.Machines.Models.FavoriteMaterialsModel import FavoriteMaterialsModel
from cura.Machines.Models.FirstStartMachineActionsModel import FirstStartMachineActionsModel
from cura.Machines.Models.GenericMaterialsModel import GenericMaterialsModel
from cura.Machines.Models.GlobalStacksModel import GlobalStacksModel
from cura.Machines.Models.MaterialBrandsModel import MaterialBrandsModel
from cura.Machines.Models.MultiBuildPlateModel import MultiBuildPlateModel
from cura.Machines.Models.NozzleModel import NozzleModel
from cura.Machines.Models.QualityManagementModel import QualityManagementModel
from cura.Machines.Models.QualityProfilesDropDownMenuModel import QualityProfilesDropDownMenuModel
from cura.Machines.Models.QualitySettingsModel import QualitySettingsModel
from cura.Machines.Models.SettingVisibilityPresetsModel import SettingVisibilityPresetsModel
from cura.Machines.Models.UserChangesModel import UserChangesModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
import cura.Settings.cura_empty_instance_containers
from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.MachineManager import MachineManager
from cura.Settings.MachineNameValidator import MachineNameValidator
from cura.Settings.MaterialSettingsVisibilityHandler import MaterialSettingsVisibilityHandler
from cura.Settings.SettingInheritanceManager import SettingInheritanceManager
from cura.Settings.SidebarCustomMenuItemsModel import SidebarCustomMenuItemsModel
from cura.Settings.SimpleModeSettingsManager import SimpleModeSettingsManager
from cura.Machines.VariantManager import VariantManager
from cura.TaskManagement.OnExitCallbackManager import OnExitCallbackManager
from cura.UI import CuraSplashScreen, MachineActionManager, PrintInformation
from cura.UI.MachineSettingsManager import MachineSettingsManager
from cura.UI.ObjectsModel import ObjectsModel
from cura.UI.TextManager import TextManager
from cura.UI.AddPrinterPagesModel import AddPrinterPagesModel
from cura.UI.WelcomePagesModel import WelcomePagesModel
from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel
from .SingleInstance import SingleInstance
from .AutoSave import AutoSave
from . import PlatformPhysics
from . import BuildVolume
from . import CameraAnimation
from . import PrintInformation
from . import CuraActions
from cura.Scene import ZOffsetDecorator
from . import CuraSplashScreen
from . import 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 UM.FlameProfiler import pyqtSlot
from UM.Decorators import override
if TYPE_CHECKING:
from cura.Machines.MaterialManager import MaterialManager
from cura.Machines.QualityManager import QualityManager
@ -208,6 +215,15 @@ class CuraApplication(QtApplication):
self._cura_scene_controller = None
self._machine_error_checker = None
self._machine_settings_manager = MachineSettingsManager(self, parent = self)
self._discovered_printer_model = DiscoveredPrintersModel(parent = self)
self._first_start_machine_actions_model = FirstStartMachineActionsModel(self, parent = self)
self._welcome_pages_model = WelcomePagesModel(self, parent = self)
self._add_printer_pages_model = AddPrinterPagesModel(self, parent = self)
self._whats_new_pages_model = WhatsNewPagesModel(self, parent = self)
self._text_manager = TextManager(parent = self)
self._quality_profile_drop_down_menu_model = None
self._custom_quality_profile_drop_down_menu_model = None
self._cura_API = CuraAPI(self)
@ -237,15 +253,12 @@ class CuraApplication(QtApplication):
self._update_platform_activity_timer = None
self._need_to_show_user_agreement = True
self._sidebar_custom_menu_items = [] # type: list # Keeps list of custom menu items for the side bar
self._plugins_loaded = False
# Backups
self._auto_save = None
self._save_data_enabled = True
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
self._container_registry_class = CuraContainerRegistry
@ -450,7 +463,6 @@ class CuraApplication(QtApplication):
# Misc.:
"ConsoleLogger", #You want to be able to read the log if something goes wrong.
"CuraEngineBackend", #Cura is useless without this one since you can't slice.
"UserAgreement", #Our lawyers want every user to see this at least once.
"FileLogger", #You want to be able to read the log if something goes wrong.
"XmlMaterialProfile", #Cura crashes without this one.
"Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
@ -511,6 +523,10 @@ class CuraApplication(QtApplication):
preferences.addPreference("cura/use_multi_build_plate", False)
preferences.addPreference("view/settings_list_height", 400)
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/material_settings", "{}")
@ -522,7 +538,7 @@ class CuraApplication(QtApplication):
preferences.addPreference("cura/expanded_brands", "")
preferences.addPreference("cura/expanded_types", "")
self._need_to_show_user_agreement = not preferences.getValue("general/accepted_user_agreement")
preferences.addPreference("general/accepted_user_agreement", False)
for key in [
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
@ -545,13 +561,20 @@ class CuraApplication(QtApplication):
@pyqtProperty(bool)
def needToShowUserAgreement(self) -> bool:
return self._need_to_show_user_agreement
return not UM.Util.parseBool(self.getPreferences().getValue("general/accepted_user_agreement"))
def setNeedToShowUserAgreement(self, set_value = True) -> None:
self._need_to_show_user_agreement = set_value
@pyqtSlot(bool)
def setNeedToShowUserAgreement(self, set_value: bool = True) -> None:
self.getPreferences().setValue("general/accepted_user_agreement", str(not set_value))
@pyqtSlot(str, str)
def writeToLog(self, severity: str, message: str) -> None:
Logger.log(severity, message)
# DO NOT call this function to close the application, use checkAndExitApplication() instead which will perform
# pre-exit checks such as checking for in-progress USB printing, etc.
# Except for the 'Decline and close' in the 'User Agreement'-step in the Welcome-pages, that should be a hard exit.
@pyqtSlot()
def closeApplication(self) -> None:
Logger.log("i", "Close application")
main_window = self.getMainWindow()
@ -649,13 +672,10 @@ class CuraApplication(QtApplication):
self._message_box_callback(button, *self._message_box_callback_arguments)
self._message_box_callback = None
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.
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.
return
ContainerRegistry.getInstance().saveDirtyContainers()
@ -745,6 +765,11 @@ class CuraApplication(QtApplication):
# Initialize Cura API
self._cura_API.initialize()
self._output_device_manager.start()
self._welcome_pages_model.initialize()
self._add_printer_pages_model.initialize()
self._whats_new_pages_model.initialize()
# Detect in which mode to run and execute that mode
if self._is_headless:
self.runWithoutGUI()
@ -839,10 +864,38 @@ class CuraApplication(QtApplication):
# Hide the splash screen
self.closeSplash()
@pyqtSlot(result = QObject)
def getDiscoveredPrintersModel(self, *args) -> "DiscoveredPrintersModel":
return self._discovered_printer_model
@pyqtSlot(result = QObject)
def getFirstStartMachineActionsModel(self, *args) -> "FirstStartMachineActionsModel":
return self._first_start_machine_actions_model
@pyqtSlot(result = QObject)
def getSettingVisibilityPresetsModel(self, *args) -> SettingVisibilityPresetsModel:
return self._setting_visibility_presets_model
@pyqtSlot(result = QObject)
def getWelcomePagesModel(self, *args) -> "WelcomePagesModel":
return self._welcome_pages_model
@pyqtSlot(result = QObject)
def getAddPrinterPagesModel(self, *args) -> "AddPrinterPagesModel":
return self._add_printer_pages_model
@pyqtSlot(result = QObject)
def getWhatsNewPagesModel(self, *args) -> "WhatsNewPagesModel":
return self._whats_new_pages_model
@pyqtSlot(result = QObject)
def getMachineSettingsManager(self, *args) -> "MachineSettingsManager":
return self._machine_settings_manager
@pyqtSlot(result = QObject)
def getTextManager(self, *args) -> "TextManager":
return self._text_manager
def getCuraFormulaFunctions(self, *args) -> "CuraFormulaFunctions":
if self._cura_formula_functions is None:
self._cura_formula_functions = CuraFormulaFunctions(self)
@ -975,6 +1028,11 @@ class CuraApplication(QtApplication):
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
qmlRegisterType(WelcomePagesModel, "Cura", 1, 0, "WelcomePagesModel")
qmlRegisterType(WhatsNewPagesModel, "Cura", 1, 0, "WhatsNewPagesModel")
qmlRegisterType(AddPrinterPagesModel, "Cura", 1, 0, "AddPrinterPagesModel")
qmlRegisterType(TextManager, "Cura", 1, 0, "TextManager")
qmlRegisterType(NetworkMJPGImage, "Cura", 1, 0, "NetworkMJPGImage")
qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 0, "ObjectsModel", self.getObjectsModel)
@ -988,7 +1046,8 @@ class CuraApplication(QtApplication):
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
qmlRegisterType(MaterialBrandsModel, "Cura", 1, 0, "MaterialBrandsModel")
qmlRegisterType(QualityManagementModel, "Cura", 1, 0, "QualityManagementModel")
qmlRegisterType(MachineManagementModel, "Cura", 1, 0, "MachineManagementModel")
qmlRegisterType(DiscoveredPrintersModel, "Cura", 1, 0, "DiscoveredPrintersModel")
qmlRegisterSingletonType(QualityProfilesDropDownMenuModel, "Cura", 1, 0,
"QualityProfilesDropDownMenuModel", self.getQualityProfilesDropDownMenuModel)
@ -999,6 +1058,7 @@ class CuraApplication(QtApplication):
qmlRegisterType(MaterialSettingsVisibilityHandler, "Cura", 1, 0, "MaterialSettingsVisibilityHandler")
qmlRegisterType(SettingVisibilityPresetsModel, "Cura", 1, 0, "SettingVisibilityPresetsModel")
qmlRegisterType(QualitySettingsModel, "Cura", 1, 0, "QualitySettingsModel")
qmlRegisterType(FirstStartMachineActionsModel, "Cura", 1, 0, "FirstStartMachineActionsModel")
qmlRegisterType(MachineNameValidator, "Cura", 1, 0, "MachineNameValidator")
qmlRegisterType(UserChangesModel, "Cura", 1, 0, "UserChangesModel")
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
@ -1055,7 +1115,6 @@ class CuraApplication(QtApplication):
self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
self._camera_animation.start()
requestAddPrinter = pyqtSignal()
activityChanged = pyqtSignal()
sceneBoundingBoxChanged = pyqtSignal()
@ -1715,3 +1774,32 @@ class CuraApplication(QtApplication):
def getSidebarCustomMenuItems(self) -> list:
return self._sidebar_custom_menu_items
@pyqtSlot(result = bool)
def shouldShowWelcomeDialog(self) -> bool:
# Only show the complete flow if there is no printer yet.
return self._machine_manager.activeMachine is None
@pyqtSlot(result = bool)
def shouldShowWhatsNewDialog(self) -> bool:
has_active_machine = self._machine_manager.activeMachine is not None
has_app_just_upgraded = self.hasJustUpdatedFromOldVersion()
# Only show the what's new dialog if there's no machine and we have just upgraded
show_whatsnew_only = has_active_machine and has_app_just_upgraded
return show_whatsnew_only
@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 UM.Resources import Resources
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
# 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
# to actually do something with this.
class CuraView(View):
def __init__(self, parent = None) -> None:
def __init__(self, parent = None, use_empty_menu_placeholder: bool = False) -> None:
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)
def mainComponent(self) -> QUrl:
return self.getDisplayComponent("main")
@pyqtProperty(QUrl, constant = True)
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

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

View file

@ -219,7 +219,7 @@ class MaterialManager(QObject):
root_material_id = material_metadata["base_file"]
definition = material_metadata["definition"]
approximate_diameter = material_metadata["approximate_diameter"]
approximate_diameter = str(material_metadata["approximate_diameter"])
if approximate_diameter not in self._diameter_machine_nozzle_buildplate_material_map:
self._diameter_machine_nozzle_buildplate_material_map[approximate_diameter] = {}
@ -332,7 +332,6 @@ class MaterialManager(QObject):
buildplate_node = nozzle_node.getChildNode(buildplate_name)
nodes_to_check = [buildplate_node, nozzle_node, machine_node, default_machine_node]
# Fallback mechanism of finding materials:
# 1. buildplate-specific material
# 2. nozzle-specific material
@ -553,10 +552,24 @@ class MaterialManager(QObject):
#
# Methods for GUI
#
@pyqtSlot("QVariant", result=bool)
def canMaterialBeRemoved(self, material_node: "MaterialNode"):
# Check if the material is active in any extruder train. In that case, the material shouldn't be removed!
# In the future we might enable this again, but right now, it's causing a ton of issues if we do (since it
# corrupts the configuration)
root_material_id = material_node.getMetaDataEntry("base_file")
material_group = self.getMaterialGroup(root_material_id)
if not material_group:
return False
nodes_to_remove = [material_group.root_material_node] + material_group.derived_material_node_list
ids_to_remove = [node.getMetaDataEntry("id", "") for node in nodes_to_remove]
for extruder_stack in self._container_registry.findContainerStacks(type="extruder_train"):
if extruder_stack.material.getId() in ids_to_remove:
return False
return True
#
# Sets the new name for the given material.
#
@pyqtSlot("QVariant", str)
def setMaterialName(self, material_node: "MaterialNode", name: str) -> None:
root_material_id = material_node.getMetaDataEntry("base_file")

View file

@ -0,0 +1,178 @@
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Callable, Dict, List, Optional, TYPE_CHECKING
from PyQt5.QtCore import pyqtSlot, pyqtProperty, pyqtSignal, QObject
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Util import parseBool
if TYPE_CHECKING:
from PyQt5.QtCore import QObject
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice
catalog = i18nCatalog("cura")
class DiscoveredPrinter(QObject):
def __init__(self, ip_address: str, key: str, name: str, create_callback: Callable[[str], None], machine_type: str,
device: "NetworkedPrinterOutputDevice", parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._ip_address = ip_address
self._key = key
self._name = name
self.create_callback = create_callback
self._machine_type = machine_type
self._device = device
nameChanged = pyqtSignal()
def getKey(self) -> str:
return self._key
@pyqtProperty(str, notify = nameChanged)
def name(self) -> str:
return self._name
def setName(self, name: str) -> None:
if self._name != name:
self._name = name
self.nameChanged.emit()
machineTypeChanged = pyqtSignal()
@pyqtProperty(str, notify = machineTypeChanged)
def machineType(self) -> str:
return self._machine_type
def setMachineType(self, machine_type: str) -> None:
if self._machine_type != machine_type:
self._machine_type = machine_type
self.machineTypeChanged.emit()
# Human readable machine type string
@pyqtProperty(str, notify = machineTypeChanged)
def readableMachineType(self) -> str:
from cura.CuraApplication import CuraApplication
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.hasMachineTypeName(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.hasMachineTypeName(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, parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._discovered_printer_by_ip_dict = dict() # type: Dict[str, DiscoveredPrinter]
discoveredPrintersChanged = pyqtSignal()
@pyqtProperty(list, notify = discoveredPrintersChanged)
def discoveredPrinters(self) -> List["DiscoveredPrinter"]:
item_list = list(
x for x in self._discovered_printer_by_ip_dict.values() if not parseBool(x.device.getProperty("temporary")))
# Split the printers into 2 lists and sort them ascending based on names.
available_list = []
not_available_list = []
for item in item_list:
if item.isUnknownMachineType or getattr(item.device, "clusterSize", 1) < 1:
not_available_list.append(item)
else:
available_list.append(item)
available_list.sort(key = lambda x: x.device.name)
not_available_list.sort(key = lambda x: x.device.name)
return available_list + not_available_list
def addDiscoveredPrinter(self, ip_address: str, key: str, name: str, create_callback: Callable[[str], None],
machine_type: str, device: "NetworkedPrinterOutputDevice") -> None:
if ip_address in self._discovered_printer_by_ip_dict:
Logger.log("e", "Printer with ip [%s] has already been added", ip_address)
return
discovered_printer = DiscoveredPrinter(ip_address, key, name, create_callback, machine_type, device, parent = self)
self._discovered_printer_by_ip_dict[ip_address] = discovered_printer
self.discoveredPrintersChanged.emit()
def updateDiscoveredPrinter(self, ip_address: str,
name: Optional[str] = None,
machine_type: Optional[str] = None) -> None:
if ip_address not in self._discovered_printer_by_ip_dict:
Logger.log("w", "Printer with ip [%s] is not known", ip_address)
return
item = self._discovered_printer_by_ip_dict[ip_address]
if name is not None:
item.setName(name)
if machine_type is not None:
item.setMachineType(machine_type)
def removeDiscoveredPrinter(self, ip_address: str) -> None:
if ip_address not in self._discovered_printer_by_ip_dict:
Logger.log("w", "Key [%s] does not exist in the discovered printers list.", ip_address)
return
del self._discovered_printer_by_ip_dict[ip_address]
self.discoveredPrintersChanged.emit()
# A convenience function for QML to create a machine (GlobalStack) out of the given discovered printer.
# This function invokes the given discovered printer's "create_callback" to do this.
@pyqtSlot("QVariant")
def createMachineFromDiscoveredPrinter(self, discovered_printer: "DiscoveredPrinter") -> None:
discovered_printer.create_callback(discovered_printer.getKey())
@pyqtSlot(str)
def createMachineFromDiscoveredPrinterAddress(self, ip_address: str) -> None:
if ip_address not in self._discovered_printer_by_ip_dict:
Logger.log("i", "Key [%s] does not exist in the discovered printers list.", ip_address)
return
self.createMachineFromDiscoveredPrinter(self._discovered_printer_by_ip_dict[ip_address])

View file

@ -2,23 +2,25 @@
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer
from typing import Iterable
from typing import Iterable, TYPE_CHECKING
from UM.i18n import i18nCatalog
import UM.Qt.ListModel
from UM.Qt.ListModel import ListModel
from UM.Application import Application
import UM.FlameProfiler
from cura.Settings.ExtruderStack import ExtruderStack # To listen to changes on the extruders.
if TYPE_CHECKING:
from cura.Settings.ExtruderStack import ExtruderStack # To listen to changes on the extruders.
catalog = i18nCatalog("cura")
## Model that holds extruders.
#
# This model is designed for use by any list of extruders, but specifically
# intended for drop-down lists of the current machine's extruders in place of
# settings.
class ExtrudersModel(UM.Qt.ListModel.ListModel):
class ExtrudersModel(ListModel):
# The ID of the container stack for the extruder.
IdRole = Qt.UserRole + 1

View file

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

View file

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

View file

@ -1,14 +1,13 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import Qt, QTimer
from UM.Qt.ListModel import ListModel
from UM.i18n import i18nCatalog
from PyQt5.QtCore import pyqtProperty, Qt, QTimer
from cura.PrinterOutputDevice import ConnectionType
from cura.PrinterOutput.PrinterOutputDevice import ConnectionType
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.GlobalStack import GlobalStack
@ -18,14 +17,18 @@ class GlobalStacksModel(ListModel):
HasRemoteConnectionRole = Qt.UserRole + 3
ConnectionTypeRole = Qt.UserRole + 4
MetaDataRole = Qt.UserRole + 5
DiscoverySourceRole = Qt.UserRole + 6 # For separating local and remote printers in the machine management page
def __init__(self, parent = None):
def __init__(self, parent = None) -> None:
super().__init__(parent)
self._catalog = i18nCatalog("cura")
self.addRoleName(self.NameRole, "name")
self.addRoleName(self.IdRole, "id")
self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection")
self.addRoleName(self.MetaDataRole, "metadata")
self._container_stacks = []
self.addRoleName(self.DiscoverySourceRole, "discoverySource")
self._change_timer = QTimer()
self._change_timer.setInterval(200)
@ -36,16 +39,15 @@ class GlobalStacksModel(ListModel):
CuraContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
CuraContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
CuraContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
self._filter_dict = {}
self._updateDelayed()
## 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
if isinstance(container, GlobalStack):
self._updateDelayed()
def _updateDelayed(self):
def _updateDelayed(self) -> None:
self._change_timer.start()
def _update(self) -> None:
@ -57,14 +59,19 @@ class GlobalStacksModel(ListModel):
has_remote_connection = False
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]:
continue
section_name = "Network enabled printers" if has_remote_connection else "Local printers"
section_name = self._catalog.i18nc("@info:title", section_name)
items.append({"name": container_stack.getMetaDataEntry("group_name", container_stack.getName()),
"id": container_stack.getId(),
"hasRemoteConnection": has_remote_connection,
"metadata": container_stack.getMetaData().copy()})
items.sort(key=lambda i: not i["hasRemoteConnection"])
"metadata": container_stack.getMetaData().copy(),
"discoverySource": section_name})
items.sort(key = lambda i: not i["hasRemoteConnection"])
self.setItems(items)

View file

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

View file

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

View file

@ -209,6 +209,7 @@ class QualityManager(QObject):
# (1) the machine-specific node
# (2) the generic node
machine_node = self._machine_nozzle_buildplate_material_quality_type_to_quality_dict.get(machine_definition_id)
# Check if this machine has specific quality profiles for its extruders, if so, when looking up extruder
# qualities, we should not fall back to use the global qualities.
has_extruder_specific_qualities = False
@ -441,7 +442,8 @@ class QualityManager(QObject):
quality_changes_group = quality_model_item["quality_changes_group"]
if quality_changes_group is None:
# create global quality changes only
new_quality_changes = self._createQualityChanges(quality_group.quality_type, quality_changes_name,
new_name = self._container_registry.uniqueName(quality_changes_name)
new_quality_changes = self._createQualityChanges(quality_group.quality_type, new_name,
global_stack, None)
self._container_registry.addContainer(new_quality_changes)
else:

View file

@ -50,6 +50,7 @@ class AuthorizationHelpers:
# \param refresh_token:
# \return An AuthenticationResponse object.
def getAccessTokenUsingRefreshToken(self, refresh_token: str) -> "AuthenticationResponse":
Logger.log("d", "Refreshing the access token.")
data = {
"client_id": self._settings.CLIENT_ID if self._settings.CLIENT_ID is not None else "",
"redirect_uri": self._settings.CALLBACK_URL if self._settings.CALLBACK_URL is not None else "",

View file

@ -34,6 +34,8 @@ class AuthorizationService:
# Emit signal when authentication failed.
onAuthenticationError = Signal()
accessTokenChanged = Signal()
def __init__(self, settings: "OAuth2Settings", preferences: Optional["Preferences"] = None) -> None:
self._settings = settings
self._auth_helpers = AuthorizationHelpers(settings)
@ -68,6 +70,7 @@ class AuthorizationService:
self._user_profile = self._parseJWT()
except requests.exceptions.ConnectionError:
# Unable to get connection, can't login.
Logger.logException("w", "Unable to validate user data with the remote server.")
return None
if not self._user_profile and self._auth_data:
@ -83,6 +86,7 @@ class AuthorizationService:
def _parseJWT(self) -> Optional["UserProfile"]:
if not self._auth_data or self._auth_data.access_token is None:
# If no auth data exists, we should always log in again.
Logger.log("d", "There was no auth data or access token")
return None
user_data = self._auth_helpers.parseJWT(self._auth_data.access_token)
if user_data:
@ -90,12 +94,16 @@ class AuthorizationService:
return user_data
# The JWT was expired or invalid and we should request a new one.
if self._auth_data.refresh_token is None:
Logger.log("w", "There was no refresh token in the auth data.")
return None
self._auth_data = self._auth_helpers.getAccessTokenUsingRefreshToken(self._auth_data.refresh_token)
if not self._auth_data or self._auth_data.access_token is None:
Logger.log("w", "Unable to use the refresh token to get a new access token.")
# The token could not be refreshed using the refresh token. We should login again.
return None
# 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)
## Get the access token as provided by the repsonse data.
@ -124,6 +132,7 @@ class AuthorizationService:
self._storeAuthData(response)
self.onAuthStateChanged.emit(logged_in = True)
else:
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)
@ -194,6 +203,7 @@ class AuthorizationService:
## Store authentication data in preferences.
def _storeAuthData(self, auth_data: Optional[AuthenticationResponse] = None) -> None:
Logger.log("d", "Attempting to store the auth data")
if self._preferences is None:
Logger.log("e", "Unable to save authentication data, since no preference has been set!")
return
@ -206,6 +216,8 @@ class AuthorizationService:
self._user_profile = None
self._preferences.resetPreference(self._settings.AUTH_DATA_PREFERENCE_KEY)
self.accessTokenChanged.emit()
def _onMessageActionTriggered(self, _, action):
if action == "retry":
self.loadAuthDataFromPreferences()

View file

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

View file

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

View file

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

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

View file

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

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
if MYPY:
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
class ConfigurationModel(QObject):
class PrinterConfigurationModel(QObject):
configurationChanged = pyqtSignal()
@ -19,14 +19,14 @@ class ConfigurationModel(QObject):
self._extruder_configurations = [] # type: List[ExtruderConfigurationModel]
self._buildplate_configuration = ""
def setPrinterType(self, printer_type):
def setPrinterType(self, printer_type: str) -> None:
self._printer_type = printer_type
@pyqtProperty(str, fset = setPrinterType, notify = configurationChanged)
def printerType(self) -> str:
return self._printer_type
def setExtruderConfigurations(self, extruder_configurations: List["ExtruderConfigurationModel"]):
def setExtruderConfigurations(self, extruder_configurations: List["ExtruderConfigurationModel"]) -> None:
if self._extruder_configurations != extruder_configurations:
self._extruder_configurations = extruder_configurations
@ -40,7 +40,7 @@ class ConfigurationModel(QObject):
return self._extruder_configurations
def setBuildplateConfiguration(self, buildplate_configuration: str) -> None:
if self._buildplate_configuration != buildplate_configuration:
if self._buildplate_configuration != buildplate_configuration:
self._buildplate_configuration = buildplate_configuration
self.configurationChanged.emit()
@ -86,4 +86,4 @@ class ConfigurationModel(QObject):
if first_extruder:
extruder_hash &= hash(first_extruder)
return hash(self._printer_type) ^ extruder_hash ^ hash(self._buildplate_configuration)
return hash(self._printer_type) ^ extruder_hash ^ hash(self._buildplate_configuration)

View file

@ -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.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.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
@ -18,6 +18,8 @@ from enum import IntEnum
import os # To get the username
import gzip
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
class AuthState(IntEnum):
NotAuthenticated = 1
@ -319,12 +321,27 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
if self._properties.get(b"temporary", b"false") != b"true":
CuraApplication.getInstance().getMachineManager().checkCorrectGroupName(self.getId(), self.name)
self._checkCorrectGroupName(self.getId(), self.name)
def _registerOnFinishedCallback(self, reply: QNetworkReply, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
if on_finished is not None:
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished
## This method checks if the name of the group stored in the definition container is correct.
# After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group
# then all the container stacks are updated, both the current and the hidden ones.
def _checkCorrectGroupName(self, device_id: str, group_name: str) -> None:
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
active_machine_network_name = CuraApplication.getInstance().getMachineManager().activeMachineNetworkKey()
if global_container_stack and device_id == active_machine_network_name:
# Check if the group_name is correct. If not, update all the containers connected to the same printer
if CuraApplication.getInstance().getMachineManager().activeMachineNetworkGroupName != group_name:
metadata_filter = {"um_network_key": active_machine_network_name}
containers = CuraContainerRegistry.getInstance().findContainerStacks(type="machine",
**metadata_filter)
for container in containers:
container.setMetaDataEntry("group_name", group_name)
def _handleOnFinished(self, reply: QNetworkReply) -> None:
# Due to garbage collection, we need to cache certain bits of post operations.
# As we don't want to keep them around forever, delete them if we get a reply.

View file

@ -1,172 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
from typing import Optional, TYPE_CHECKING, List
from PyQt5.QtCore import QUrl
from PyQt5.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)
import warnings
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 cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel

View file

@ -4,14 +4,12 @@
from UM.Logger import Logger
from UM.Signal import Signal
from typing import Union
MYPY = False
if MYPY:
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
from .Models.PrintJobOutputModel import PrintJobOutputModel
from .Models.ExtruderOutputModel import ExtruderOutputModel
from .Models.PrinterOutputModel import PrinterOutputModel
from .PrinterOutputDevice import PrinterOutputDevice
class PrinterOutputController:

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.
# 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.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
import warnings
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 cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel

View file

@ -1,261 +1,4 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from enum import IntEnum
from typing import Callable, List, Optional, Union
from UM.Decorators import deprecated
from UM.i18n import i18nCatalog
from UM.OutputDevice.OutputDevice import OutputDevice
from PyQt5.QtCore import pyqtProperty, pyqtSignal, QObject, QTimer, QUrl
from PyQt5.QtWidgets import QMessageBox
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)
import warnings
warnings.warn("Importing cura.PrinterOutputDevice has been deprecated since 4.1, use cura.PrinterOutput.PrinterOutputDevice inststad", DeprecationWarning, stacklevel=2)
# We moved the PrinterOutput device to it's own submodule.
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionState

View file

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

View file

@ -4,7 +4,7 @@ from PyQt5.QtCore import Qt, pyqtSlot, QObject
from PyQt5.QtWidgets import QApplication
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 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
def _calculateAABB(self) -> None:
self._aabb = None
if self._mesh_data:
aabb = self._mesh_data.getExtents(self.getWorldTransformation())
else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0)
position = self.getWorldPosition()
aabb = AxisAlignedBox(minimum = position, maximum = position)
self._aabb = self._mesh_data.getExtents(self.getWorldTransformation())
for child in self._children:
if child.callDecoration("isNonPrintingMesh"):
# Non-printing-meshes inside a group should not affect push apart or drop to build plate
continue
if aabb is None:
aabb = child.getBoundingBox()
if not child._mesh_data:
# Nodes without mesh data should not affect bounding boxes of their parents.
continue
if self._aabb is None:
self._aabb = child.getBoundingBox()
else:
aabb = aabb + child.getBoundingBox()
self._aabb = aabb
self._aabb = self._aabb + child.getBoundingBox()
## Taken from SceneNode, but replaced SceneNode with CuraSceneNode
def __deepcopy__(self, memo: Dict[int, object]) -> "CuraSceneNode":

View file

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

View file

@ -42,7 +42,14 @@ class CuraFormulaFunctions:
try:
extruder_stack = global_stack.extruders[str(extruder_position)]
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
value = extruder_stack.getRawProperty(property_key, "value", context = context)

View file

@ -224,7 +224,16 @@ class ExtruderManager(QObject):
# Get the extruders of all printable meshes in the scene
meshes = [node for node in DepthFirstIterator(scene_root) if isinstance(node, SceneNode) and node.isSelectable()] #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
# Exclude anti-overhang meshes
mesh_list = []
for mesh in meshes:
stack = mesh.callDecoration("getStack")
if stack is not None and (stack.getProperty("anti_overhang_mesh", "value") or stack.getProperty("support_mesh", "value")):
continue
mesh_list.append(mesh)
for mesh in mesh_list:
extruder_stack_id = mesh.callDecoration("getActiveExtruder")
if not extruder_stack_id:
# No per-object settings for this node

View file

@ -64,6 +64,10 @@ class GlobalStack(CuraContainerStack):
machine_extruder_count = self.getProperty("machine_extruder_count", "value")
return result_list[:machine_extruder_count]
@pyqtProperty(int, constant = True)
def maxExtruderCount(self):
return len(self.getMetaDataEntry("machine_extruder_trains"))
@classmethod
def getLoadingPriority(cls) -> int:
return 2
@ -81,7 +85,15 @@ class GlobalStack(CuraContainerStack):
# Requesting it from the metadata actually gets them as strings (as that's what you get from serializing).
# But we do want them returned as a list of ints (so the rest of the code can directly compare)
connection_types = self.getMetaDataEntry("connection_type", "").split(",")
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
def addConfiguredConnectionType(self, connection_type: int) -> None:
@ -200,7 +212,7 @@ class GlobalStack(CuraContainerStack):
# Determine whether or not we should try to get the "resolve" property instead of the
# requested property.
def _shouldResolve(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> bool:
if property_name is not "value":
if property_name != "value":
# Do not try to resolve anything but the "value" property
return False
@ -246,6 +258,9 @@ class GlobalStack(CuraContainerStack):
def getHasVariants(self) -> bool:
return parseBool(self.getMetaDataEntry("has_variants", False))
def getHasVariantsBuildPlates(self) -> bool:
return parseBool(self.getMetaDataEntry("has_variant_buildplates", False))
def getHasMachineQuality(self) -> bool:
return parseBool(self.getMetaDataEntry("has_machine_quality", False))

View file

@ -6,13 +6,14 @@ import re
import unicodedata
from typing import Any, List, Dict, TYPE_CHECKING, Optional, cast
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
from UM.Decorators import deprecated
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Settings.InstanceContainer import InstanceContainer
from UM.Settings.Interfaces import ContainerInterface
from UM.Signal import Signal
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, QTimer
from UM.FlameProfiler import pyqtSlot
from UM import Util
from UM.Logger import Logger
@ -22,10 +23,10 @@ from UM.Settings.SettingFunction import SettingFunction
from UM.Signal import postponeSignals, CompressTechnique
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionType
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionType
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
from cura.PrinterOutput.Models.ExtruderConfigurationModel import ExtruderConfigurationModel
from cura.PrinterOutput.Models.MaterialOutputModel import MaterialOutputModel
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
from cura.Settings.ExtruderManager import ExtruderManager
from cura.Settings.ExtruderStack import ExtruderStack
@ -106,7 +107,7 @@ class MachineManager(QObject):
# There might already be some output devices by the time the signal is connected
self._onOutputDevicesChanged()
self._current_printer_configuration = ConfigurationModel() # Indicates the current configuration setup in this printer
self._current_printer_configuration = PrinterConfigurationModel() # Indicates the current configuration setup in this printer
self.activeMaterialChanged.connect(self._onCurrentConfigurationChanged)
self.activeVariantChanged.connect(self._onCurrentConfigurationChanged)
# Force to compute the current configuration
@ -157,6 +158,7 @@ class MachineManager(QObject):
printerConnectedStatusChanged = pyqtSignal() # Emitted every time the active machine change or the outputdevices change
rootMaterialChanged = pyqtSignal()
discoveredPrintersChanged = pyqtSignal()
def setInitialActiveMachine(self) -> None:
active_machine_id = self._application.getPreferences().getValue("cura/active_machine")
@ -171,10 +173,9 @@ class MachineManager(QObject):
self._printer_output_devices.append(printer_output_device)
self.outputDevicesChanged.emit()
self.printerConnectedStatusChanged.emit()
@pyqtProperty(QObject, notify = currentConfigurationChanged)
def currentConfiguration(self) -> ConfigurationModel:
def currentConfiguration(self) -> PrinterConfigurationModel:
return self._current_printer_configuration
def _onCurrentConfigurationChanged(self) -> None:
@ -205,7 +206,7 @@ class MachineManager(QObject):
self.currentConfigurationChanged.emit()
@pyqtSlot(QObject, result = bool)
def matchesConfiguration(self, configuration: ConfigurationModel) -> bool:
def matchesConfiguration(self, configuration: PrinterConfigurationModel) -> bool:
return self._current_printer_configuration == configuration
@pyqtProperty("QVariantList", notify = outputDevicesChanged)
@ -362,7 +363,6 @@ class MachineManager(QObject):
# Mark global stack as invalid
ConfigurationErrorMessage.getInstance().addFaultyContainers(global_stack.getId())
return # We're done here
ExtruderManager.getInstance().setActiveExtruderIndex(0) # Switch to first extruder
self._global_container_stack = global_stack
self._application.setGlobalContainerStack(global_stack)
@ -370,6 +370,11 @@ class MachineManager(QObject):
self._initMachineState(global_stack)
self._onGlobalContainerChanged()
# Switch to the first enabled extruder
self.updateDefaultExtruder()
default_extruder_position = int(self.defaultExtruderPosition)
ExtruderManager.getInstance().setActiveExtruderIndex(default_extruder_position)
self.__emitChangedSignals()
## Given a definition id, return the machine with this id.
@ -386,9 +391,17 @@ class MachineManager(QObject):
return machine
return None
@pyqtSlot(str)
@pyqtSlot(str, str)
def addMachine(self, name: str, definition_id: str) -> None:
new_stack = CuraStackBuilder.createMachine(name, definition_id)
def addMachine(self, definition_id: str, name: Optional[str] = None) -> None:
if name is None:
definitions = CuraContainerRegistry.getInstance().findDefinitionContainers(id = definition_id)
if definitions:
name = definitions[0].getName()
else:
name = definition_id
new_stack = CuraStackBuilder.createMachine(cast(str, name), definition_id)
if new_stack:
# Instead of setting the global container stack here, we set the active machine and so the signals are emitted
self.setActiveMachine(new_stack.getId())
@ -486,18 +499,21 @@ class MachineManager(QObject):
return bool(self._stacks_have_errors)
@pyqtProperty(str, notify = globalContainerChanged)
@deprecated("use Cura.MachineManager.activeMachine.definition.name instead", "4.1")
def activeMachineDefinitionName(self) -> str:
if self._global_container_stack:
return self._global_container_stack.definition.getName()
return ""
@pyqtProperty(str, notify = globalContainerChanged)
@deprecated("use Cura.MachineManager.activeMachine.name instead", "4.1")
def activeMachineName(self) -> str:
if self._global_container_stack:
return self._global_container_stack.getMetaDataEntry("group_name", self._global_container_stack.getName())
return ""
@pyqtProperty(str, notify = globalContainerChanged)
@deprecated("use Cura.MachineManager.activeMachine.id instead", "4.1")
def activeMachineId(self) -> str:
if self._global_container_stack:
return self._global_container_stack.getId()
@ -531,6 +547,7 @@ class MachineManager(QObject):
return False
@pyqtProperty("QVariantList", notify=globalContainerChanged)
@deprecated("use Cura.MachineManager.activeMachine.configuredConnectionTypes instead", "4.1")
def activeMachineConfiguredConnectionTypes(self):
if self._global_container_stack:
return self._global_container_stack.configuredConnectionTypes
@ -675,11 +692,6 @@ class MachineManager(QObject):
return False
return True
## Check if a container is read_only
@pyqtSlot(str, result = bool)
def isReadOnly(self, container_id: str) -> bool:
return CuraContainerRegistry.getInstance().isReadOnly(container_id)
## Copy the value of the setting of the current extruder to all other extruders as well as the global container.
@pyqtSlot(str)
def copyValueToExtruders(self, key: str) -> None:
@ -708,6 +720,7 @@ class MachineManager(QObject):
extruder_stack.userChanges.setProperty(key, "value", new_value)
@pyqtProperty(str, notify = activeVariantChanged)
@deprecated("use Cura.activeStack.variant.name instead", "4.1")
def activeVariantName(self) -> str:
if self._active_container_stack:
variant = self._active_container_stack.variant
@ -717,6 +730,7 @@ class MachineManager(QObject):
return ""
@pyqtProperty(str, notify = activeVariantChanged)
@deprecated("use Cura.activeStack.variant.id instead", "4.1")
def activeVariantId(self) -> str:
if self._active_container_stack:
variant = self._active_container_stack.variant
@ -726,6 +740,7 @@ class MachineManager(QObject):
return ""
@pyqtProperty(str, notify = activeVariantChanged)
@deprecated("use Cura.activeMachine.variant.name instead", "4.1")
def activeVariantBuildplateName(self) -> str:
if self._global_container_stack:
variant = self._global_container_stack.variant
@ -735,6 +750,7 @@ class MachineManager(QObject):
return ""
@pyqtProperty(str, notify = globalContainerChanged)
@deprecated("use Cura.activeMachine.definition.id instead", "4.1")
def activeDefinitionId(self) -> str:
if self._global_container_stack:
return self._global_container_stack.definition.id
@ -799,19 +815,19 @@ class MachineManager(QObject):
@pyqtProperty(bool, notify = globalContainerChanged)
def hasMaterials(self) -> bool:
if self._global_container_stack:
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False))
return self._global_container_stack.getHasMaterials()
return False
@pyqtProperty(bool, notify = globalContainerChanged)
def hasVariants(self) -> bool:
if self._global_container_stack:
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_variants", False))
return self._global_container_stack.getHasVariants()
return False
@pyqtProperty(bool, notify = globalContainerChanged)
def hasVariantBuildplates(self) -> bool:
if self._global_container_stack:
return Util.parseBool(self._global_container_stack.getMetaDataEntry("has_variant_buildplates", False))
return self._global_container_stack.getHasVariantsBuildPlates()
return False
## The selected buildplate is compatible if it is compatible with all the materials in all the extruders
@ -1057,9 +1073,6 @@ class MachineManager(QObject):
def _onMaterialNameChanged(self) -> None:
self.activeMaterialChanged.emit()
def _onQualityNameChanged(self) -> None:
self.activeQualityChanged.emit()
def _getContainerChangedSignals(self) -> List[Signal]:
if self._global_container_stack is None:
return []
@ -1367,7 +1380,7 @@ class MachineManager(QObject):
self.setActiveMachine(new_machine.getId())
@pyqtSlot(QObject)
def applyRemoteConfiguration(self, configuration: ConfigurationModel) -> None:
def applyRemoteConfiguration(self, configuration: PrinterConfigurationModel) -> None:
if self._global_container_stack is None:
return
self.blurSettings.emit()
@ -1424,6 +1437,7 @@ class MachineManager(QObject):
self._global_container_stack.extruders[position].setEnabled(True)
self.updateMaterialWithVariant(position)
self.updateDefaultExtruder()
self.updateNumberExtrudersEnabled()
if configuration.buildplateConfiguration is not None:
@ -1455,31 +1469,6 @@ class MachineManager(QObject):
if self.hasUserSettings and self._application.getPreferences().getValue("cura/active_mode") == 1:
self._application.discardOrKeepProfileChanges()
## Find all container stacks that has the pair 'key = value' in its metadata and replaces the value with 'new_value'
def replaceContainersMetadata(self, key: str, value: str, new_value: str) -> None:
machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine")
for machine in machines:
if machine.getMetaDataEntry(key) == value:
machine.setMetaDataEntry(key, new_value)
## This method checks if the name of the group stored in the definition container is correct.
# After updating from 3.2 to 3.3 some group names may be temporary. If there is a mismatch in the name of the group
# then all the container stacks are updated, both the current and the hidden ones.
def checkCorrectGroupName(self, device_id: str, group_name: str) -> None:
if self._global_container_stack and device_id == self.activeMachineNetworkKey():
# Check if the 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")
def setGlobalVariant(self, container_node: "ContainerNode") -> None:
self.blurSettings.emit()
@ -1650,3 +1639,15 @@ class MachineManager(QObject):
abbr_machine += stripped_word
return abbr_machine
def hasMachineTypeName(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

View file

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

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"
# has not been updated yet.
deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
deep_copy._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
deep_copy._is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
deep_copy._is_non_thumbnail_visible_mesh = self._evaluateIsNonThumbnailVisibleMesh()
return deep_copy
@ -102,21 +102,26 @@ class SettingOverrideDecorator(SceneNodeDecorator):
def isNonPrintingMesh(self):
return self._is_non_printing_mesh
def evaluateIsNonPrintingMesh(self):
def _evaluateIsNonPrintingMesh(self):
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_printing_mesh_settings)
def isNonThumbnailVisibleMesh(self):
return self._is_non_thumbnail_visible_mesh
def evaluateIsNonThumbnailVisibleMesh(self):
def _evaluateIsNonThumbnailVisibleMesh(self):
return any(bool(self._stack.getProperty(setting, "value")) for setting in self._non_thumbnail_visible_settings)
def _onSettingChanged(self, instance, property_name): # Reminder: 'property' is a built-in function
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":
# Trigger slice/need slicing if the value has changed.
self._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
self._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
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.
new_is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
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().tickle()

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

@ -5,8 +5,7 @@ import json
import math
import os
import unicodedata
import re # To create abbreviations for printer names.
from typing import Dict, List, Optional
from typing import Dict, List, Optional, TYPE_CHECKING
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
@ -16,8 +15,6 @@ from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication

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,292 @@
# 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
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._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

@ -23,7 +23,10 @@ known_args = vars(parser.parse_known_args()[0])
if not known_args["debug"]:
def get_cura_dir_path():
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():
return os.path.expanduser("~/.local/share/" + CuraAppName)
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

@ -259,7 +259,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
instance_container_files = [name for name in cura_file_names if name.endswith(self._instance_container_suffix)]
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
quality_changes_conflict = False
@ -297,7 +297,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
custom_quality_name = parser["general"]["name"]
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.
quality_changes = self._container_registry.findInstanceContainers(name = custom_quality_name,
type = "quality_changes")
@ -515,7 +515,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
self._dialog.setNumVisibleSettings(num_visible_settings)
self._dialog.setQualityName(quality_name)
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.setActiveMode(active_mode)
self._dialog.setMachineName(machine_name)
@ -820,6 +820,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
container = quality_manager._createQualityChanges(quality_changes_quality_type, quality_changes_name,
global_stack, extruder_stack)
container_info.container = container
container.setDirty(True)
self._container_registry.addContainer(container)
for key, value in container_info.parser["values"].items():
container_info.container.setProperty(key, "value", value)

View file

@ -41,7 +41,7 @@ class WorkspaceDialog(QObject):
self._num_user_settings = 0
self._active_mode = ""
self._quality_name = ""
self._num_settings_overriden_by_quality_changes = 0
self._num_settings_overridden_by_quality_changes = 0
self._quality_type = ""
self._machine_name = ""
self._machine_type = ""
@ -151,10 +151,10 @@ class WorkspaceDialog(QObject):
@pyqtProperty(int, notify=numSettingsOverridenByQualityChangesChanged)
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):
self._num_settings_overriden_by_quality_changes = num_settings_overriden_by_quality_changes
def setNumSettingsOverriddenByQualityChanges(self, num_settings_overridden_by_quality_changes):
self._num_settings_overridden_by_quality_changes = num_settings_overridden_by_quality_changes
self.numSettingsOverridenByQualityChangesChanged.emit()
@pyqtProperty(str, notify=qualityNameChanged)

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)
})
except requests.exceptions.ConnectionError:
Logger.log("w", "Unable to connect with the server.")
Logger.logException("w", "Unable to connect with the server.")
return []
# 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.
return self._emitRestoreError()
download_package = requests.get(download_url, stream = True)
try:
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:
# 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)
@ -142,9 +147,14 @@ class DriveApiService:
Logger.log("w", "Could not get access token.")
return False
delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
"Authorization": "Bearer {}".format(access_token)
})
try:
delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
"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:
Logger.log("w", "Could not delete backup: %s", delete_backup.text)
return False
@ -159,15 +169,19 @@ class DriveApiService:
if not access_token:
Logger.log("w", "Could not get access token.")
return None
backup_upload_request = requests.put(self.BACKUP_URL, json = {
"data": {
"backup_size": backup_size,
"metadata": backup_metadata
}
}, headers = {
"Authorization": "Bearer {}".format(access_token)
})
try:
backup_upload_request = requests.put(
self.BACKUP_URL,
json = {"data": {"backup_size": backup_size,
"metadata": backup_metadata
}
},
headers = {
"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.
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 UM.Backend.Backend import Backend, BackendState
from UM.Scene.Camera import Camera
from UM.Scene.SceneNode import SceneNode
from UM.Signal import Signal
from UM.Logger import Logger
from UM.Message import Message
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Platform import Platform
from UM.Qt.Duration import DurationFormat
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Settings.Interfaces import DefinitionContainerInterface
from UM.Settings.SettingInstance import SettingInstance #For typing.
from UM.Tool import Tool #For typing.
from UM.Mesh.MeshData import MeshData #For typing.
from cura.CuraApplication import CuraApplication
from cura.Settings.ExtruderManager import ExtruderManager

View file

@ -24,7 +24,7 @@ from cura import LayerPolygon
import numpy
from time import time
from cura.Settings.ExtrudersModel import ExtrudersModel
from cura.Machines.Models.ExtrudersModel import ExtrudersModel
catalog = i18nCatalog("cura")

View file

@ -196,10 +196,7 @@ class StartSliceJob(Job):
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.
if node.callDecoration("isSliceable") and node.getMeshData() and node.getMeshData().getVertices() is not None:
per_object_stack = node.callDecoration("getStack")
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)
is_non_printing_mesh = bool(node.callDecoration("isNonPrintingMesh"))
# Find a reason not to add the node
if node.callDecoration("getBuildPlateNumber") != self._build_plate_number:

View file

@ -1,31 +1,33 @@
# Copyright (c) 2018 Ultimaker B.V.
# 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.Job import Job
from UM.Logger import Logger
from UM.Math.Vector import Vector
from UM.Message import Message
from cura.Scene.CuraSceneNode import CuraSceneNode
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from cura.CuraApplication import CuraApplication
from cura.LayerDataBuilder import LayerDataBuilder
from cura.LayerDataDecorator import LayerDataDecorator
from cura.LayerPolygon import LayerPolygon
from cura.Scene.CuraSceneNode import CuraSceneNode
from cura.Scene.GCodeListDecorator import GCodeListDecorator
from cura.Settings.ExtruderManager import ExtruderManager
import numpy
import math
import re
from typing import Dict, List, NamedTuple, Optional, Union
catalog = i18nCatalog("cura")
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])])
## This parser is intended to interpret the common firmware codes among all the
# different flavors
class FlavorParser:
@ -33,7 +35,7 @@ class FlavorParser:
def __init__(self) -> None:
CuraApplication.getInstance().hideMessageSignal.connect(self._onHideMessage)
self._cancelled = False
self._message = None
self._message = None # type: Optional[Message]
self._layer_number = 0
self._extruder_number = 0
self._clearValues()
@ -425,7 +427,8 @@ class FlavorParser:
if line.startswith("M"):
M = self._getInt(line, "M")
self.processMCode(M, line, current_position, current_path)
if M is not None:
self.processMCode(M, line, current_position, current_path)
# "Flush" leftovers. Last layer paths are still stored
if len(current_path) > 1:
@ -463,7 +466,7 @@ class FlavorParser:
Logger.log("w", "File doesn't contain any valid layers")
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_depth = settings.getProperty("machine_depth", "value")
scene_node.setPosition(Vector(-machine_width / 2, 0, machine_depth / 2))

View file

@ -12,9 +12,6 @@ catalog = i18nCatalog("cura")
from . import MarlinFlavorParser, RepRapFlavorParser
# Class for loading and parsing G-code files
class GCodeReader(MeshReader):
_flavor_default = "Marlin"

View file

@ -123,7 +123,7 @@ UM.Dialog
UM.TooltipArea {
Layout.fillWidth:true
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 {
width: parent.width
@ -134,9 +134,9 @@ UM.Dialog
anchors.verticalCenter: parent.verticalCenter
}
ComboBox {
id: image_color_invert
objectName: "Image_Color_Invert"
model: [ catalog.i18nc("@item:inlistbox","Lighter is higher"), catalog.i18nc("@item:inlistbox","Darker is higher") ]
id: lighter_is_higher
objectName: "Lighter_Is_Higher"
model: [ catalog.i18nc("@item:inlistbox","Darker is higher"), catalog.i18nc("@item:inlistbox","Lighter is higher") ]
width: 180 * screenScaleFactor
onCurrentIndexChanged: { manager.onImageColorInvertChanged(currentIndex) }
}

View file

@ -46,9 +46,9 @@ class ImageReader(MeshReader):
def _read(self, file_name):
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()
mesh = MeshBuilder()
@ -104,7 +104,7 @@ class ImageReader(MeshReader):
Job.yieldThread()
if image_color_invert:
if not lighter_is_higher:
height_data = 1 - height_data
for _ in range(0, blur_iterations):

View file

@ -30,10 +30,10 @@ class ImageReaderUI(QObject):
self._width = self.default_width
self._depth = self.default_depth
self.base_height = 1
self.peak_height = 10
self.base_height = 0.4
self.peak_height = 2.5
self.smoothing = 1
self.image_color_invert = False;
self.lighter_is_higher = False;
self._ui_lock = threading.Lock()
self._cancelled = False
@ -143,4 +143,4 @@ class ImageReaderUI(QObject):
@pyqtSlot(int)
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.
from PyQt5.QtCore import pyqtProperty, pyqtSignal
from typing import Optional, TYPE_CHECKING
from PyQt5.QtCore import pyqtProperty
import UM.i18n
from UM.FlameProfiler import pyqtSlot
from UM.Application import Application
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.Settings.DefinitionContainer import DefinitionContainer
from cura.MachineAction import MachineAction
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")
@ -18,139 +23,102 @@ catalog = UM.i18n.i18nCatalog("cura")
## 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.
class MachineSettingsAction(MachineAction):
def __init__(self, parent = None):
def __init__(self, parent: Optional["QObject"] = None) -> None:
super().__init__("MachineSettingsAction", catalog.i18nc("@action", "Machine Settings"))
self._qml_url = "MachineSettingsAction.qml"
self._application = Application.getInstance()
self._global_container_stack = None
from cura.CuraApplication import CuraApplication
self._application = CuraApplication.getInstance()
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.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.onFinished.connect(self._onFinished)
self._empty_definition_container_id_list = []
def _isEmptyDefinitionChanges(self, container_id: str):
if not self._empty_definition_container_id_list:
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
# Which container index in a stack to store machine setting changes.
@pyqtProperty(int, constant = True)
def storeContainerIndex(self) -> int:
return self._store_container_index
def _onContainerAdded(self, container):
# Add this action as a supported action to all machine definitions
if isinstance(container, DefinitionContainer) and container.getMetaDataEntry("type") == "machine":
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):
if not self._global_container_stack:
global_stack = self._application.getMachineManager().activeMachine
if not global_stack:
return
# Make sure there is a definition_changes container to store the machine settings
definition_changes_id = self._global_container_stack.definitionChanges.getId()
if self._isEmptyDefinitionChanges(definition_changes_id):
CuraStackBuilder.createDefinitionChangesContainer(self._global_container_stack,
self._global_container_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()
definition_changes_id = global_stack.definitionChanges.getId()
if isEmptyContainer(definition_changes_id):
CuraStackBuilder.createDefinitionChangesContainer(global_stack,
global_stack.getName() + "_settings")
# Disable auto-slicing while the MachineAction is showing
if self._backend: # This sometimes triggers before backend is loaded.
self._backend.disableTimer()
@pyqtSlot()
def onFinishAction(self):
# Restore autoslicing when the machineaction is dismissed
def _onFinished(self):
# Restore auto-slicing when the machine action is dismissed
if self._backend and self._backend.determineAutoSlicing():
self._backend.enableTimer()
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)
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
# it was moved to the machine manager instead. Now this method just calls the machine manager.
self._application.getMachineManager().setActiveMachineExtruderCount(extruder_count)
@pyqtSlot()
def forceUpdate(self):
def forceUpdate(self) -> None:
# Force rebuilding the build volume by reloading the global container stack.
# This is a bit of a hack, but it seems quick enough.
self._application.globalContainerStackChanged.emit()
self._application.getMachineManager().globalContainerChanged.emit()
@pyqtSlot()
def updateHasMaterialsMetadata(self):
def updateHasMaterialsMetadata(self) -> None:
global_stack = self._application.getMachineManager().activeMachine
# Updates the has_materials metadata flag after switching gcode flavor
if not self._global_container_stack:
if not global_stack:
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):
# In other words: only continue for the UM2 (extended), but not for the UM2+
return
machine_manager = self._application.getMachineManager()
material_manager = self._application.getMaterialManager()
extruder_positions = list(self._global_container_stack.extruders.keys())
has_materials = self._global_container_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
extruder_positions = list(global_stack.extruders.keys())
has_materials = global_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
material_node = None
if has_materials:
self._global_container_stack.setMetaDataEntry("has_materials", True)
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 self._global_container_stack.getMetaData():
self._global_container_stack.removeMetaDataEntry("has_materials")
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(self._global_container_stack, position, None)
material_node = material_manager.getDefaultMaterial(global_stack, position, None)
machine_manager.setMaterial(position, material_node)
self._application.globalContainerStackChanged.emit()
@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
self._application.getMachineManager().updateMaterialWithVariant(str(extruder_position))

File diff suppressed because it is too large Load diff

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.storeContainerIndex // 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,341 @@
// 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.storeContainerIndex // 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:
{
extruderCountModel.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".
extruderCountModel.append({ text: String(i), value: String(i) })
}
}
}
}
}
}
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

@ -2,8 +2,6 @@
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
from UM.Application import Application
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from cura.Stages.CuraStage import CuraStage

View file

@ -162,7 +162,7 @@ class PostProcessingPlugin(QObject, Extension):
loaded_script = importlib.util.module_from_spec(spec)
if spec.loader is None:
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?
loaded_class = getattr(loaded_script, script_name)

View file

@ -97,7 +97,7 @@ class FilamentChange(Script):
if layer_num <= len(data):
index, layer_data = self._searchLayerData(data, layer_num - 1)
if layer_data is None:
Logger.log("e", "Could not found the layer")
Logger.log("e", "Could not find the layer {layer_num}".format(layer_num = layer_num))
continue
lines = layer_data.split("\n")
lines.insert(2, color_change)

View file

@ -20,11 +20,19 @@ Item
name: "cura"
}
anchors
{
left: parent.left
right: parent.right
leftMargin: UM.Theme.getSize("wide_margin").width
rightMargin: UM.Theme.getSize("wide_margin").width
}
// Item to ensure that all of the buttons are nicely centered.
Item
{
anchors.horizontalCenter: parent.horizontalCenter
width: openFileButton.width + itemRow.width + UM.Theme.getSize("default_margin").width
width: parent.width - 2 * UM.Theme.getSize("wide_margin").width
height: parent.height
RowLayout
@ -32,9 +40,9 @@ Item
id: itemRow
anchors.left: openFileButton.right
anchors.right: parent.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
width: Math.round(0.9 * prepareMenu.width)
height: parent.height
spacing: 0

View file

@ -20,15 +20,21 @@ Item
name: "cura"
}
anchors
{
left: parent.left
right: parent.right
leftMargin: UM.Theme.getSize("wide_margin").width
rightMargin: UM.Theme.getSize("wide_margin").width
}
Row
{
id: stageMenuRow
anchors.centerIn: parent
height: parent.height
width: childrenRect.width
// We want this row to have a preferred with equals to the 85% of the parent
property int preferredWidth: Math.round(0.85 * previewMenu.width)
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - 2 * UM.Theme.getSize("wide_margin").width
height: parent.height
Cura.ViewsSelector
{
@ -49,12 +55,12 @@ Item
color: UM.Theme.getColor("lining")
}
// This component will grow freely up to complete the preferredWidth of the row.
// This component will grow freely up to complete the width of the row.
Loader
{
id: viewPanel
height: parent.height
width: source != "" ? (stageMenuRow.preferredWidth - viewsSelector.width - printSetupSelectorItem.width - 2 * UM.Theme.getSize("default_lining").width) : 0
width: source != "" ? (previewMenu.width - viewsSelector.width - printSetupSelectorItem.width - 2 * (UM.Theme.getSize("wide_margin").width + UM.Theme.getSize("default_lining").width)) : 0
source: UM.Controller.activeView != null && UM.Controller.activeView.stageMenuComponent != null ? UM.Controller.activeView.stageMenuComponent : ""
}

View file

@ -15,6 +15,8 @@ Cura.ExpandableComponent
{
id: base
dragPreferencesNamePrefix: "view/colorscheme"
contentHeaderTitle: catalog.i18nc("@label", "Color scheme")
Connections
@ -177,7 +179,6 @@ Cura.ExpandableComponent
height: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
width: parent.width
visible: !UM.SimulationView.compatibilityMode
enabled: index < 4
onClicked:
{

View file

@ -1,150 +1,154 @@
// Copyright (c) 2018 Ultimaker B.V.
// Copyright (c) 2019 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick 2.10
import QtQuick.Controls 2.3
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.3 as UM
import Cura 1.0 as Cura
import Cura 1.1 as Cura
UM.Dialog
Window
{
UM.I18nCatalog { id: catalog; name: "cura" }
id: baseDialog
title: catalog.i18nc("@title:window", "More information on anonymous data collection")
visible: false
modality: Qt.ApplicationModal
minimumWidth: 500 * screenScaleFactor
minimumHeight: 400 * screenScaleFactor
width: minimumWidth
height: minimumHeight
property bool allowSendData: true // for saving the user's choice
color: UM.Theme.getColor("main_background")
onAccepted: manager.setSendSliceInfo(allowSendData)
property bool allowSendData: true // for saving the user's choice
onVisibilityChanged:
{
if (visible)
{
baseDialog.allowSendData = UM.Preferences.getValue("info/send_slice_info");
baseDialog.allowSendData = UM.Preferences.getValue("info/send_slice_info")
if (baseDialog.allowSendData)
{
allowSendButton.checked = true;
allowSendButton.checked = true
}
else
{
dontSendButton.checked = true;
dontSendButton.checked = true
}
}
}
// Main content area
Item
{
id: textRow
anchors
{
top: parent.top
bottom: radioButtonsRow.top
bottomMargin: UM.Theme.getSize("default_margin").height
left: parent.left
right: parent.right
}
anchors.fill: parent
anchors.margins: UM.Theme.getSize("default_margin").width
Label
Item // Text part
{
id: headerText
id: textRow
anchors
{
top: parent.top
left: parent.left
right: parent.right
}
text: catalog.i18nc("@text:window", "Cura sends anonymous data to Ultimaker in order to improve the print quality and user experience. Below is an example of all the data that is sent.")
wrapMode: Text.WordWrap
}
TextArea
{
id: exampleData
anchors
{
top: headerText.bottom
topMargin: UM.Theme.getSize("default_margin").height
bottom: parent.bottom
bottom: radioButtonsRow.top
bottomMargin: UM.Theme.getSize("default_margin").height
left: parent.left
right: parent.right
}
text: manager.getExampleData()
readOnly: true
textFormat: TextEdit.PlainText
}
}
Column
{
id: radioButtonsRow
width: parent.width
anchors.bottom: buttonRow.top
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
ExclusiveGroup { id: group }
RadioButton
{
id: dontSendButton
text: catalog.i18nc("@text:window", "I don't want to send this data")
exclusiveGroup: group
onClicked:
Label
{
baseDialog.allowSendData = !checked;
id: headerText
anchors
{
top: parent.top
left: parent.left
right: parent.right
}
text: catalog.i18nc("@text:window", "Cura sends anonymous data to Ultimaker in order to improve the print quality and user experience. Below is an example of all the data that is sent.")
wrapMode: Text.WordWrap
renderType: Text.NativeRendering
}
}
RadioButton
{
id: allowSendButton
text: catalog.i18nc("@text:window", "Allow sending this data to Ultimaker and help us improve Cura")
exclusiveGroup: group
onClicked:
Cura.ScrollableTextArea
{
baseDialog.allowSendData = checked;
}
}
}
anchors
{
top: headerText.bottom
topMargin: UM.Theme.getSize("default_margin").height
bottom: parent.bottom
bottomMargin: UM.Theme.getSize("default_margin").height
left: parent.left
right: parent.right
}
Item
{
id: buttonRow
anchors.bottom: parent.bottom
width: parent.width
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
UM.I18nCatalog { id: catalog; name: "cura" }
Button
{
anchors.right: parent.right
text: catalog.i18nc("@action:button", "OK")
onClicked:
{
baseDialog.accepted()
baseDialog.hide()
textArea.text: manager.getExampleData()
textArea.readOnly: true
}
}
Button
Column // Radio buttons for agree and disagree
{
id: radioButtonsRow
anchors.left: parent.left
text: catalog.i18nc("@action:button", "Cancel")
onClicked:
anchors.right: parent.right
anchors.bottom: buttonRow.top
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
Cura.RadioButton
{
baseDialog.rejected()
baseDialog.hide()
id: dontSendButton
text: catalog.i18nc("@text:window", "I don't want to send this data")
onClicked:
{
baseDialog.allowSendData = !checked
}
}
Cura.RadioButton
{
id: allowSendButton
text: catalog.i18nc("@text:window", "Allow sending this data to Ultimaker and help us improve Cura")
onClicked:
{
baseDialog.allowSendData = checked
}
}
}
Item // Bottom buttons
{
id: buttonRow
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: childrenRect.height
Cura.PrimaryButton
{
anchors.right: parent.right
text: catalog.i18nc("@action:button", "OK")
onClicked:
{
manager.setSendSliceInfo(allowSendData)
baseDialog.hide()
}
}
Cura.SecondaryButton
{
anchors.left: parent.left
text: catalog.i18nc("@action:button", "Cancel")
onClicked:
{
baseDialog.hide()
}
}
}
}

View file

@ -48,20 +48,6 @@ class SliceInfo(QObject, Extension):
def _onAppInitialized(self):
# DO NOT read any preferences values in the constructor because at the time plugins are created, no version
# upgrade has been performed yet because version upgrades are plugins too!
if not self._application.getPreferences().getValue("info/asked_send_slice_info"):
self.send_slice_info_message = Message(catalog.i18nc("@info", "Cura collects anonymized usage statistics."),
lifetime = 0,
dismissable = False,
title = catalog.i18nc("@info:title", "Collecting Data"))
self.send_slice_info_message.addAction("MoreInfo", name = catalog.i18nc("@action:button", "More info"), icon = None,
description = catalog.i18nc("@action:tooltip", "See more information on what data Cura sends."), button_style = Message.ActionButtonStyle.LINK)
self.send_slice_info_message.addAction("Dismiss", name = catalog.i18nc("@action:button", "Allow"), icon = None,
description = catalog.i18nc("@action:tooltip", "Allow Cura to send anonymized usage statistics to help prioritize future improvements to Cura. Some of your preferences and settings are sent, the Cura version and a hash of the models you're slicing."))
self.send_slice_info_message.actionTriggered.connect(self.messageActionTriggered)
self.send_slice_info_message.show()
if self._more_info_dialog is None:
self._more_info_dialog = self._createDialog("MoreInfoWindow.qml")
@ -76,7 +62,7 @@ class SliceInfo(QObject, Extension):
def showMoreInfoDialog(self):
if self._more_info_dialog is None:
self._more_info_dialog = self._createDialog("MoreInfoWindow.qml")
self._more_info_dialog.open()
self._more_info_dialog.show()
def _createDialog(self, qml_name):
Logger.log("d", "Creating dialog [%s]", qml_name)
@ -195,6 +181,8 @@ class SliceInfo(QObject, Extension):
model = dict()
model["hash"] = node.getMeshData().getHash()
bounding_box = node.getBoundingBox()
if not bounding_box:
continue
model["bounding_box"] = {"minimum": {"x": bounding_box.minimum.x,
"y": bounding_box.minimum.y,
"z": bounding_box.minimum.z},

View file

@ -1,4 +1,4 @@
# Copyright (c) 2015 Ultimaker B.V.
# Copyright (c) 2019 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.View.View import View
@ -7,7 +7,6 @@ from UM.Scene.Selection import Selection
from UM.Resources import Resources
from UM.Application import Application
from UM.View.RenderBatch import RenderBatch
from UM.Settings.Validator import ValidatorState
from UM.Math.Color import Color
from UM.View.GL.OpenGL import OpenGL
@ -20,9 +19,9 @@ import math
class SolidView(View):
def __init__(self):
super().__init__()
Application.getInstance().getPreferences().addPreference("view/show_overhang", True)
application = Application.getInstance()
application.getPreferences().addPreference("view/show_overhang", True)
application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
self._enabled_shader = None
self._disabled_shader = None
self._non_printing_shader = None
@ -30,6 +29,38 @@ class SolidView(View):
self._extruders_model = None
self._theme = None
self._support_angle = 90
self._global_stack = None
Application.getInstance().engineCreatedSignal.connect(self._onGlobalContainerChanged)
def _onGlobalContainerChanged(self) -> None:
if self._global_stack:
try:
self._global_stack.propertyChanged.disconnect(self._onPropertyChanged)
except TypeError:
pass
for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
extruder_stack.propertyChanged.disconnect(self._onPropertyChanged)
self._global_stack = Application.getInstance().getGlobalContainerStack()
if self._global_stack:
self._global_stack.propertyChanged.connect(self._onPropertyChanged)
for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
extruder_stack.propertyChanged.connect(self._onPropertyChanged)
self._onPropertyChanged("support_angle", "value") # Force an re-evaluation
def _onPropertyChanged(self, key: str, property_name: str) -> None:
if key != "support_angle" or property_name != "value":
return
# As the rendering is called a *lot* we really, dont want to re-evaluate the property every time. So we store em!
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
support_extruder_nr = global_container_stack.getExtruderPositionValueWithDefault("support_extruder_nr")
support_angle_stack = global_container_stack.extruders.get(str(support_extruder_nr))
if support_angle_stack:
self._support_angle = support_angle_stack.getProperty("support_angle", "value")
def beginRendering(self):
scene = self.getController().getScene()
@ -63,14 +94,10 @@ class SolidView(View):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack:
support_extruder_nr = global_container_stack.getExtruderPositionValueWithDefault("support_extruder_nr")
support_angle_stack = Application.getInstance().getExtruderManager().getExtruderStack(support_extruder_nr)
if support_angle_stack is not None and Application.getInstance().getPreferences().getValue("view/show_overhang"):
angle = support_angle_stack.getProperty("support_angle", "value")
if Application.getInstance().getPreferences().getValue("view/show_overhang"):
# Make sure the overhang angle is valid before passing it to the shader
if angle is not None and angle >= 0 and angle <= 90:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(90 - angle)))
if self._support_angle is not None and self._support_angle >= 0 and self._support_angle <= 90:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(90 - self._support_angle)))
else:
self._enabled_shader.setUniformValue("u_overhangAngle", math.cos(math.radians(0))) #Overhang angle of 0 causes no area at all to be marked as overhang.
else:

View file

@ -65,6 +65,7 @@ Item
{
id: description
text: details.description || ""
font: UM.Theme.getFont("default")
anchors
{
top: title.bottom

View file

@ -26,7 +26,7 @@ UM.Dialog
minimumWidth: 450 * screenScaleFactor
minimumHeight: 150 * screenScaleFactor
modality: UM.Application.platform == "linux" ? Qt.NonModal : Qt.WindowModal
modality: Qt.WindowModal
Column
{

View file

@ -10,7 +10,7 @@ import Cura 1.1 as Cura
Column
{
property bool installed: toolbox.isInstalled(model.id)
property bool canUpdate: toolbox.canUpdate(model.id)
property bool canUpdate: CuraApplication.getPackageManager().packagesWithUpdate.indexOf(model.id) != -1
property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
property var packageData
@ -112,11 +112,9 @@ Column
{
target: toolbox
onInstallChanged: installed = toolbox.isInstalled(model.id)
onMetadataChanged: canUpdate = toolbox.canUpdate(model.id)
onFilterChanged:
{
installed = toolbox.isInstalled(model.id)
canUpdate = toolbox.canUpdate(model.id)
}
}
}

View file

@ -76,7 +76,7 @@ Item
height: (parent.height * 0.4) | 0
anchors
{
bottom: parent.bottomcommi
bottom: parent.bottom
right: parent.right
}
sourceSize.height: height

View file

@ -14,7 +14,7 @@ Rectangle
Column
{
height: childrenRect.height + 2 * padding
spacing: UM.Theme.getSize("toolbox_showcase_spacing").width
spacing: UM.Theme.getSize("default_margin").width
width: parent.width
padding: UM.Theme.getSize("wide_margin").height
Label

View file

@ -1,9 +1,11 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick 2.10
import QtQuick.Controls 1.4
import UM 1.1 as UM
import UM 1.4 as UM
import Cura 1.0 as Cura
Item
{
@ -50,6 +52,7 @@ Item
}
}
}
ToolboxTabButton
{
id: installedTabButton
@ -62,7 +65,25 @@ Item
rightMargin: UM.Theme.getSize("default_margin").width
}
onClicked: toolbox.viewCategory = "installed"
width: UM.Theme.getSize("toolbox_header_tab").width + marketplaceNotificationIcon.width - UM.Theme.getSize("default_margin").width
}
Cura.NotificationIcon
{
id: marketplaceNotificationIcon
visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0
anchors.right: installedTabButton.right
anchors.verticalCenter: installedTabButton.verticalCenter
labelText:
{
const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length
return itemCount > 9 ? "9+" : itemCount
}
}
ToolboxShadow
{
anchors.top: bar.bottom

View file

@ -10,7 +10,7 @@ import Cura 1.1 as Cura
Column
{
property bool canUpdate: false
property bool canUpdate: CuraApplication.getPackageManager().packagesWithUpdate.indexOf(model.id) != -1
property bool canDowngrade: false
property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
width: UM.Theme.getSize("toolbox_action_button").width
@ -83,7 +83,6 @@ Column
target: toolbox
onMetadataChanged:
{
canUpdate = toolbox.canUpdate(model.id)
canDowngrade = toolbox.canDowngrade(model.id)
}
}

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