mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-05-21 20:19:32 +08:00
commit
62c0633bcc
43
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
43
.github/ISSUE_TEMPLATE/bug-report.md
vendored
Normal file
@ -0,0 +1,43 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us fix issues.
|
||||
title: ''
|
||||
labels: 'Type: Bug'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Processing an issue will go much faster when this is filled out, and issues which do not use this template WILL BE REMOVED and no fix will be considered!
|
||||
|
||||
Before filing, PLEASE check if the issue already exists (either open or closed) by using the search bar on the issues page. If it does, comment there. Even if it's closed, we can reopen it based on your comment.
|
||||
|
||||
Also, please note the application version in the title of the issue. For example: "[3.2.1] Cannot connect to 3rd-party printer". Please do NOT write things like "Request:" or "[BUG]" in the title; this is what labels are for.
|
||||
|
||||
It is also helpful to attach a project (.3mf or .curaproject) file and Cura log file so we can debug issues quicker. Information about how to find the log file can be found at https://github.com/Ultimaker/Cura#logging-issues
|
||||
|
||||
To upload a project, try changing the extension to e.g. .curaproject.3mf.zip so that GitHub accepts uploading the file. Otherwise, we recommend http://wetransfer.com, but other file hosts like Google Drive or Dropbox work well too.
|
||||
|
||||
Thank you for using Cura!
|
||||
-->
|
||||
|
||||
**Application version**
|
||||
<!-- The version of the application this issue occurs with -->
|
||||
|
||||
**Platform**
|
||||
<!-- Information about the operating system the issue occurs on. Include at least the operating system. In the case of visual glitches/issues, also include information about your graphics drivers and GPU. -->
|
||||
|
||||
**Printer**
|
||||
<!-- Which printer was selected in Cura? If possible, please attach project file as .curaproject.3mf.zip -->
|
||||
|
||||
**Reproduction steps**
|
||||
<!-- How did you encounter the bug? -->
|
||||
|
||||
**Actual results**
|
||||
<!-- What happens after the above steps have been followed -->
|
||||
|
||||
**Expected results**
|
||||
<!-- What should happen after the above steps have been followed -->
|
||||
|
||||
**Additional information**
|
||||
<!-- Extra information relevant to the issue, like screenshots. Don't forget to attach the log files with this issue report. -->
|
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: 'Type: New Feature'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
|
||||
|
||||
**Describe the solution you'd like**
|
||||
<!--A clear and concise description of what you want to happen. If possible, describe why you think this is a good solution.-->
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
<!-- A clear and concise description of any alternative solutions or features you've considered. Again, if possible, think about why these alternatives are not working out. -->
|
||||
|
||||
**Affected users and/or printers**
|
||||
<!-- Who do you think will benefit from this? Is everyone going to benefit from these changes? Only a few people? -->
|
||||
**Additional context**
|
||||
<!-- Add any other context or screenshots about the feature request here. -->
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -35,7 +35,7 @@ cura.desktop
|
||||
.pydevproject
|
||||
.settings
|
||||
|
||||
#Externally located plug-ins.
|
||||
#Externally located plug-ins commonly installed by our devs.
|
||||
plugins/cura-big-flame-graph
|
||||
plugins/cura-god-mode-plugin
|
||||
plugins/cura-siemensnx-plugin
|
||||
@ -52,6 +52,7 @@ plugins/FlatProfileExporter
|
||||
plugins/GodMode
|
||||
plugins/OctoPrintPlugin
|
||||
plugins/ProfileFlattener
|
||||
plugins/SettingsGuide
|
||||
plugins/X3GWriter
|
||||
|
||||
#Build stuff
|
||||
@ -71,3 +72,7 @@ run.sh
|
||||
.scannerwork/
|
||||
CuraEngine
|
||||
|
||||
/.coverage
|
||||
|
||||
#Prevents import failures when plugin running tests
|
||||
plugins/__init__.py
|
||||
|
16
.gitlab-ci.yml
Normal file
16
.gitlab-ci.yml
Normal file
@ -0,0 +1,16 @@
|
||||
image: registry.gitlab.com/ultimaker/cura/cura-build-environment:centos7
|
||||
|
||||
stages:
|
||||
- build
|
||||
|
||||
build and test linux:
|
||||
stage: build
|
||||
tags:
|
||||
- cura
|
||||
- docker
|
||||
- linux
|
||||
script:
|
||||
- docker/build.sh
|
||||
artifacts:
|
||||
paths:
|
||||
- build
|
@ -1,11 +1,10 @@
|
||||
project(cura NONE)
|
||||
cmake_minimum_required(VERSION 2.8.12)
|
||||
|
||||
set(CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake/
|
||||
${CMAKE_MODULE_PATH})
|
||||
project(cura)
|
||||
cmake_minimum_required(VERSION 3.6)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
list(APPEND CMAKE_MODULE_PATH ${CMAKE_SOURCE_DIR}/cmake)
|
||||
|
||||
set(URANIUM_DIR "${CMAKE_SOURCE_DIR}/../Uranium" CACHE DIRECTORY "The location of the Uranium repository")
|
||||
set(URANIUM_SCRIPTS_DIR "${URANIUM_DIR}/scripts" CACHE DIRECTORY "The location of the scripts directory of the Uranium repository")
|
||||
|
||||
@ -21,13 +20,34 @@ set(CURA_APP_NAME "cura" CACHE STRING "Short name of Cura, used for configuratio
|
||||
set(CURA_APP_DISPLAY_NAME "Ultimaker Cura" CACHE STRING "Display name of Cura")
|
||||
set(CURA_VERSION "master" CACHE STRING "Version name of Cura")
|
||||
set(CURA_BUILDTYPE "" CACHE STRING "Build type of Cura, eg. 'PPA'")
|
||||
set(CURA_SDK_VERSION "" CACHE STRING "SDK version of Cura")
|
||||
set(CURA_CLOUD_API_ROOT "" CACHE STRING "Alternative Cura cloud API root")
|
||||
set(CURA_CLOUD_API_VERSION "" CACHE STRING "Alternative Cura cloud API version")
|
||||
set(CURA_CLOUD_ACCOUNT_API_ROOT "" CACHE STRING "Alternative Cura cloud account 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 +60,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 +73,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 +98,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()
|
||||
|
@ -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 --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}
|
||||
)
|
||||
)
|
||||
|
@ -13,6 +13,6 @@ TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
|
||||
Icon=cura-icon
|
||||
Terminal=false
|
||||
Type=Application
|
||||
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;text/x-gcode;
|
||||
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;text/x-gcode;application/x-amf;application/x-ply;application/x-ctm;model/vnd.collada+xml;model/gltf-binary;model/gltf+json;model/vnd.collada+xml+zip;
|
||||
Categories=Graphics;
|
||||
Keywords=3D;Printing;Slicer;
|
||||
|
@ -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()
|
||||
|
||||
|
@ -9,7 +9,7 @@ DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
|
||||
DEFAULT_CURA_VERSION = "master"
|
||||
DEFAULT_CURA_BUILD_TYPE = ""
|
||||
DEFAULT_CURA_DEBUG_MODE = False
|
||||
DEFAULT_CURA_SDK_VERSION = "6.0.0"
|
||||
DEFAULT_CURA_SDK_VERSION = "6.2.0"
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraAppName # type: ignore
|
||||
@ -42,9 +42,7 @@ try:
|
||||
except ImportError:
|
||||
CuraDebugMode = DEFAULT_CURA_DEBUG_MODE
|
||||
|
||||
try:
|
||||
from cura.CuraVersion import CuraSDKVersion # type: ignore
|
||||
if CuraSDKVersion == "":
|
||||
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION
|
||||
except ImportError:
|
||||
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION
|
||||
# Each release has a fixed SDK version coupled with it. It doesn't make sense to make it configurable because, for
|
||||
# example Cura 3.2 with SDK version 6.1 will not work. So the SDK version is hard-coded here and left out of the
|
||||
# CuraVersion.py.in template.
|
||||
CuraSDKVersion = "6.2.0"
|
||||
|
@ -1,12 +1,19 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import numpy
|
||||
import copy
|
||||
from typing import Optional, Tuple, TYPE_CHECKING
|
||||
|
||||
from UM.Math.Polygon import Polygon
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
||||
|
||||
## Polygon representation as an array for use with Arrange
|
||||
class ShapeArray:
|
||||
def __init__(self, arr, offset_x, offset_y, scale = 1):
|
||||
def __init__(self, arr: numpy.array, offset_x: float, offset_y: float, scale: float = 1) -> None:
|
||||
self.arr = arr
|
||||
self.offset_x = offset_x
|
||||
self.offset_y = offset_y
|
||||
@ -16,7 +23,7 @@ class ShapeArray:
|
||||
# \param vertices
|
||||
# \param scale scale the coordinates
|
||||
@classmethod
|
||||
def fromPolygon(cls, vertices, scale = 1):
|
||||
def fromPolygon(cls, vertices: numpy.array, scale: float = 1) -> "ShapeArray":
|
||||
# scale
|
||||
vertices = vertices * scale
|
||||
# flip y, x -> x, y
|
||||
@ -42,7 +49,7 @@ class ShapeArray:
|
||||
# \param min_offset offset for the offset ShapeArray
|
||||
# \param scale scale the coordinates
|
||||
@classmethod
|
||||
def fromNode(cls, node, min_offset, scale = 0.5, include_children = False):
|
||||
def fromNode(cls, node: "SceneNode", min_offset: float, scale: float = 0.5, include_children: bool = False) -> Tuple[Optional["ShapeArray"], Optional["ShapeArray"]]:
|
||||
transform = node._transformation
|
||||
transform_x = transform._data[0][3]
|
||||
transform_y = transform._data[2][3]
|
||||
@ -88,14 +95,16 @@ class ShapeArray:
|
||||
# \param shape numpy format shape, [x-size, y-size]
|
||||
# \param vertices
|
||||
@classmethod
|
||||
def arrayFromPolygon(cls, shape, vertices):
|
||||
def arrayFromPolygon(cls, shape: Tuple[int, int], vertices: numpy.array) -> numpy.array:
|
||||
base_array = numpy.zeros(shape, dtype = numpy.int32) # Initialize your array of zeros
|
||||
|
||||
fill = numpy.ones(base_array.shape) * True # Initialize boolean array defining shape fill
|
||||
|
||||
# Create check array for each edge segment, combine into fill array
|
||||
for k in range(vertices.shape[0]):
|
||||
fill = numpy.all([fill, cls._check(vertices[k - 1], vertices[k], base_array)], axis=0)
|
||||
check_array = cls._check(vertices[k - 1], vertices[k], base_array)
|
||||
if check_array is not None:
|
||||
fill = numpy.all([fill, check_array], axis=0)
|
||||
|
||||
# Set all values inside polygon to one
|
||||
base_array[fill] = 1
|
||||
@ -111,9 +120,9 @@ class ShapeArray:
|
||||
# \param p2 2-tuple with x, y for point 2
|
||||
# \param base_array boolean array to project the line on
|
||||
@classmethod
|
||||
def _check(cls, p1, p2, base_array):
|
||||
def _check(cls, p1: numpy.array, p2: numpy.array, base_array: numpy.array) -> Optional[numpy.array]:
|
||||
if p1[0] == p2[0] and p1[1] == p2[1]:
|
||||
return
|
||||
return None
|
||||
idxs = numpy.indices(base_array.shape) # Create 3D array of indices
|
||||
|
||||
p1 = p1.astype(float)
|
||||
@ -131,5 +140,4 @@ class ShapeArray:
|
||||
|
||||
max_col_idx = (idxs[0] - p1[0]) / (p2[0] - p1[0]) * (p2[1] - p1[1]) + p1[1]
|
||||
sign = numpy.sign(p2[0] - p1[0])
|
||||
return idxs[1] * sign <= max_col_idx * sign
|
||||
|
||||
return idxs[1] * sign <= max_col_idx * sign
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2016 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
@ -16,9 +16,10 @@ class AutoSave:
|
||||
self._application.getPreferences().addPreference("cura/autosave_delay", 1000 * 10)
|
||||
|
||||
self._change_timer = QTimer()
|
||||
self._change_timer.setInterval(self._application.getPreferences().getValue("cura/autosave_delay"))
|
||||
self._change_timer.setInterval(int(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)
|
||||
|
@ -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()
|
||||
@ -147,5 +148,9 @@ class Backup:
|
||||
Logger.log("d", "Removing current data in location: %s", target_path)
|
||||
Resources.factoryReset()
|
||||
Logger.log("d", "Extracting backup to location: %s", target_path)
|
||||
archive.extractall(target_path)
|
||||
try:
|
||||
archive.extractall(target_path)
|
||||
except PermissionError:
|
||||
Logger.logException("e", "Unable to extract the backup due to permission errors")
|
||||
return False
|
||||
return True
|
||||
|
@ -51,8 +51,18 @@ 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)
|
||||
auto_save = self._application.getAutoSave()
|
||||
# The auto save is only not created if the application has not yet started.
|
||||
if auto_save:
|
||||
auto_save.setEnabled(False)
|
||||
else:
|
||||
Logger.log("e", "Unable to disable the autosave as application init has not been completed")
|
||||
|
||||
## Re-enable auto-save after we're done.
|
||||
def _enableAutoSave(self) -> None:
|
||||
self._application.setSaveDataEnabled(True)
|
||||
auto_save = self._application.getAutoSave()
|
||||
# The auto save is only not created if the application has not yet started.
|
||||
if auto_save:
|
||||
auto_save.setEnabled(True)
|
||||
else:
|
||||
Logger.log("e", "Unable to enable the autosave as application init has not been completed")
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -12,9 +12,10 @@ import json
|
||||
import ssl
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
import shutil
|
||||
|
||||
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, Qt, QUrl
|
||||
import certifi
|
||||
|
||||
from PyQt5.QtCore import QT_VERSION_STR, PYQT_VERSION_STR, QUrl
|
||||
from PyQt5.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout, QLabel, QTextEdit, QGroupBox, QCheckBox, QPushButton
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
|
||||
@ -22,7 +23,6 @@ from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.View.GL.OpenGL import OpenGL
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Platform import Platform
|
||||
from UM.Resources import Resources
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
@ -319,7 +319,8 @@ class CrashHandler:
|
||||
|
||||
def _userDescriptionWidget(self):
|
||||
group = QGroupBox()
|
||||
group.setTitle(catalog.i18nc("@title:groupbox", "User description"))
|
||||
group.setTitle(catalog.i18nc("@title:groupbox", "User description" +
|
||||
" (Note: Developers may not speak your language, please use English if possible)"))
|
||||
layout = QVBoxLayout()
|
||||
|
||||
# When sending the report, the user comments will be collected
|
||||
@ -351,11 +352,13 @@ class CrashHandler:
|
||||
# Convert data to bytes
|
||||
binary_data = json.dumps(self.data).encode("utf-8")
|
||||
|
||||
# CURA-6698 Create an SSL context and use certifi CA certificates for verification.
|
||||
context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLSv1_2)
|
||||
context.load_verify_locations(cafile = certifi.where())
|
||||
# Submit data
|
||||
kwoptions = {"data": binary_data, "timeout": 5}
|
||||
|
||||
if Platform.isOSX():
|
||||
kwoptions["context"] = ssl._create_unverified_context()
|
||||
kwoptions = {"data": binary_data,
|
||||
"timeout": 5,
|
||||
"context": context}
|
||||
|
||||
Logger.log("i", "Sending crash report info to [%s]...", self.crash_url)
|
||||
if not self.has_started:
|
||||
|
@ -3,15 +3,17 @@
|
||||
|
||||
from PyQt5.QtCore import QObject, QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
from typing import List, TYPE_CHECKING, cast
|
||||
from typing import List, Optional, cast
|
||||
|
||||
from UM.Event import CallFunctionEvent
|
||||
from UM.FlameProfiler import pyqtSlot
|
||||
from UM.Math.Quaternion import Quaternion
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||
from UM.Operations.RotateOperation import RotateOperation
|
||||
from UM.Operations.TranslateOperation import TranslateOperation
|
||||
|
||||
import cura.CuraApplication
|
||||
@ -23,9 +25,8 @@ from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.Operations.SetBuildPlateNumberOperation import SetBuildPlateNumberOperation
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
|
||||
class CuraActions(QObject):
|
||||
def __init__(self, parent: QObject = None) -> None:
|
||||
@ -74,6 +75,39 @@ class CuraActions(QObject):
|
||||
operation.addOperation(center_operation)
|
||||
operation.push()
|
||||
|
||||
# Rotate the selection, so that the face that the mouse-pointer is on, faces the build-plate.
|
||||
@pyqtSlot()
|
||||
def bottomFaceSelection(self) -> None:
|
||||
selected_face = Selection.getSelectedFace()
|
||||
if not selected_face:
|
||||
Logger.log("e", "Bottom face operation shouldn't have been called without a selected face.")
|
||||
return
|
||||
|
||||
original_node, face_id = selected_face
|
||||
meshdata = original_node.getMeshDataTransformed()
|
||||
if not meshdata or face_id < 0 or face_id > Selection.getMaxFaceSelectionId():
|
||||
return
|
||||
|
||||
rotation_point, face_normal = meshdata.getFacePlane(face_id)
|
||||
rotation_point_vector = Vector(rotation_point[0], rotation_point[1], rotation_point[2])
|
||||
face_normal_vector = Vector(face_normal[0], face_normal[1], face_normal[2])
|
||||
rotation_quaternion = Quaternion.rotationTo(face_normal_vector.normalized(), Vector(0.0, -1.0, 0.0))
|
||||
|
||||
operation = GroupedOperation()
|
||||
current_node = None # type: Optional[SceneNode]
|
||||
for node in Selection.getAllSelectedObjects():
|
||||
current_node = node
|
||||
parent_node = current_node.getParent()
|
||||
while parent_node and parent_node.callDecoration("isGroup"):
|
||||
current_node = parent_node
|
||||
parent_node = current_node.getParent()
|
||||
if current_node is None:
|
||||
return
|
||||
|
||||
rotate_operation = RotateOperation(current_node, rotation_quaternion, rotation_point_vector)
|
||||
operation.addOperation(rotate_operation)
|
||||
operation.push()
|
||||
|
||||
## Multiply all objects in the selection
|
||||
#
|
||||
# \param count The number of times to multiply the selection.
|
||||
|
@ -13,113 +13,124 @@ 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.GCodeListDecorator import GCodeListDecorator
|
||||
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.RecommendedMode import RecommendedMode
|
||||
from cura.UI.WelcomePagesModel import WelcomePagesModel
|
||||
from cura.UI.WhatsNewPagesModel import WhatsNewPagesModel
|
||||
|
||||
from cura.Utils.NetworkingUtil import NetworkingUtil
|
||||
|
||||
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
|
||||
@ -134,7 +145,7 @@ class CuraApplication(QtApplication):
|
||||
# SettingVersion represents the set of settings available in the machine/extruder definitions.
|
||||
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
|
||||
# changes of the settings.
|
||||
SettingVersion = 7
|
||||
SettingVersion = 9
|
||||
|
||||
Created = False
|
||||
|
||||
@ -208,6 +219,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(self, parent = self)
|
||||
self._first_start_machine_actions_model = FirstStartMachineActionsModel(self, parent = self)
|
||||
self._welcome_pages_model = WelcomePagesModel(self, parent = self)
|
||||
self._add_printer_pages_model = AddPrinterPagesModel(self, parent = self)
|
||||
self._whats_new_pages_model = WhatsNewPagesModel(self, parent = self)
|
||||
self._text_manager = TextManager(parent = self)
|
||||
|
||||
self._quality_profile_drop_down_menu_model = None
|
||||
self._custom_quality_profile_drop_down_menu_model = None
|
||||
self._cura_API = CuraAPI(self)
|
||||
@ -237,15 +257,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
|
||||
self._auto_save = None # type: Optional[AutoSave]
|
||||
|
||||
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry
|
||||
self._container_registry_class = CuraContainerRegistry
|
||||
@ -405,7 +422,7 @@ class CuraApplication(QtApplication):
|
||||
# Add empty variant, material and quality containers.
|
||||
# Since they are empty, they should never be serialized and instead just programmatically created.
|
||||
# We need them to simplify the switching between materials.
|
||||
self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container # type: EmptyInstanceContainer
|
||||
self.empty_container = cura.Settings.cura_empty_instance_containers.empty_container
|
||||
|
||||
self._container_registry.addContainer(
|
||||
cura.Settings.cura_empty_instance_containers.empty_definition_changes_container)
|
||||
@ -450,7 +467,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.
|
||||
@ -509,8 +525,13 @@ class CuraApplication(QtApplication):
|
||||
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
|
||||
preferences.addPreference("cura/choice_on_open_project", "always_ask")
|
||||
preferences.addPreference("cura/use_multi_build_plate", False)
|
||||
preferences.addPreference("cura/show_list_of_objects", 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 +543,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 +566,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 +677,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 +770,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()
|
||||
@ -810,7 +840,6 @@ class CuraApplication(QtApplication):
|
||||
if diagonal < 1: #No printer added yet. Set a default camera distance for normal-sized printers.
|
||||
diagonal = 375
|
||||
camera.setPosition(Vector(-80, 250, 700) * diagonal / 375)
|
||||
camera.setPerspective(True)
|
||||
camera.lookAt(Vector(0, 0, 0))
|
||||
controller.getScene().setActiveCamera("3d")
|
||||
|
||||
@ -839,10 +868,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)
|
||||
@ -874,7 +931,7 @@ class CuraApplication(QtApplication):
|
||||
|
||||
def getObjectsModel(self, *args):
|
||||
if self._object_manager is None:
|
||||
self._object_manager = ObjectsModel.createObjectsModel()
|
||||
self._object_manager = ObjectsModel(self)
|
||||
return self._object_manager
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
@ -933,7 +990,7 @@ class CuraApplication(QtApplication):
|
||||
|
||||
return super().event(event)
|
||||
|
||||
def getAutoSave(self):
|
||||
def getAutoSave(self) -> Optional[AutoSave]:
|
||||
return self._auto_save
|
||||
|
||||
## Get print information (duration / material used)
|
||||
@ -975,9 +1032,17 @@ class CuraApplication(QtApplication):
|
||||
qmlRegisterSingletonType(SimpleModeSettingsManager, "Cura", 1, 0, "SimpleModeSettingsManager", self.getSimpleModeSettingsManager)
|
||||
qmlRegisterSingletonType(MachineActionManager.MachineActionManager, "Cura", 1, 0, "MachineActionManager", self.getMachineActionManager)
|
||||
|
||||
qmlRegisterType(NetworkingUtil, "Cura", 1, 5, "NetworkingUtil")
|
||||
|
||||
qmlRegisterType(WelcomePagesModel, "Cura", 1, 0, "WelcomePagesModel")
|
||||
qmlRegisterType(WhatsNewPagesModel, "Cura", 1, 0, "WhatsNewPagesModel")
|
||||
qmlRegisterType(AddPrinterPagesModel, "Cura", 1, 0, "AddPrinterPagesModel")
|
||||
qmlRegisterType(TextManager, "Cura", 1, 0, "TextManager")
|
||||
qmlRegisterType(RecommendedMode, "Cura", 1, 0, "RecommendedMode")
|
||||
|
||||
qmlRegisterType(NetworkMJPGImage, "Cura", 1, 0, "NetworkMJPGImage")
|
||||
|
||||
qmlRegisterSingletonType(ObjectsModel, "Cura", 1, 0, "ObjectsModel", self.getObjectsModel)
|
||||
qmlRegisterType(ObjectsModel, "Cura", 1, 0, "ObjectsModel")
|
||||
qmlRegisterType(BuildPlateModel, "Cura", 1, 0, "BuildPlateModel")
|
||||
qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
|
||||
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
|
||||
@ -988,7 +1053,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 +1065,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 +1122,6 @@ class CuraApplication(QtApplication):
|
||||
self._camera_animation.setTarget(Selection.getSelectedObject(0).getWorldPosition())
|
||||
self._camera_animation.start()
|
||||
|
||||
requestAddPrinter = pyqtSignal()
|
||||
activityChanged = pyqtSignal()
|
||||
sceneBoundingBoxChanged = pyqtSignal()
|
||||
|
||||
@ -1197,7 +1263,7 @@ class CuraApplication(QtApplication):
|
||||
@pyqtSlot()
|
||||
def arrangeObjectsToAllBuildPlates(self) -> None:
|
||||
nodes_to_arrange = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
|
||||
@ -1224,7 +1290,7 @@ class CuraApplication(QtApplication):
|
||||
def arrangeAll(self) -> None:
|
||||
nodes_to_arrange = []
|
||||
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
|
||||
@ -1262,7 +1328,13 @@ class CuraApplication(QtApplication):
|
||||
Logger.log("i", "Reloading all loaded mesh data.")
|
||||
nodes = []
|
||||
has_merged_nodes = False
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||
gcode_filename = None # type: Optional[str]
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
# Objects loaded from Gcode should also be included.
|
||||
gcode_filename = node.callDecoration("getGcodeFileName")
|
||||
if gcode_filename is not None:
|
||||
break
|
||||
|
||||
if not isinstance(node, CuraSceneNode) or not node.getMeshData():
|
||||
if node.getName() == "MergedMesh":
|
||||
has_merged_nodes = True
|
||||
@ -1270,13 +1342,18 @@ class CuraApplication(QtApplication):
|
||||
|
||||
nodes.append(node)
|
||||
|
||||
# We can open only one gcode file at the same time. If the current view has a gcode file open, just reopen it
|
||||
# for reloading.
|
||||
if gcode_filename:
|
||||
self._openFile(gcode_filename)
|
||||
|
||||
if not nodes:
|
||||
return
|
||||
|
||||
for node in nodes:
|
||||
file_name = node.getMeshData().getFileName()
|
||||
if file_name:
|
||||
job = ReadMeshJob(file_name)
|
||||
mesh_data = node.getMeshData()
|
||||
if mesh_data and mesh_data.getFileName():
|
||||
job = ReadMeshJob(mesh_data.getFileName())
|
||||
job._node = node # type: ignore
|
||||
job.finished.connect(self._reloadMeshFinished)
|
||||
if has_merged_nodes:
|
||||
@ -1715,3 +1792,69 @@ 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
|
||||
|
||||
@pyqtSlot()
|
||||
def deleteAll(self, only_selectable: bool = True) -> None:
|
||||
super().deleteAll(only_selectable = only_selectable)
|
||||
|
||||
# Also remove nodes with LayerData
|
||||
self._removeNodesWithLayerData(only_selectable = only_selectable)
|
||||
|
||||
def _removeNodesWithLayerData(self, only_selectable: bool = True) -> None:
|
||||
Logger.log("i", "Clearing scene")
|
||||
nodes = []
|
||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||
if not isinstance(node, SceneNode):
|
||||
continue
|
||||
if not node.isEnabled():
|
||||
continue
|
||||
if (not node.getMeshData() and not node.callDecoration("getLayerData")) and not node.callDecoration("isGroup"):
|
||||
continue # Node that doesnt have a mesh and is not a group.
|
||||
if only_selectable and not node.isSelectable():
|
||||
continue # Only remove nodes that are selectable.
|
||||
if not node.callDecoration("isSliceable") and not node.callDecoration("getLayerData") and not node.callDecoration("isGroup"):
|
||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
nodes.append(node)
|
||||
if nodes:
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
op = GroupedOperation()
|
||||
|
||||
for node in nodes:
|
||||
from UM.Operations.RemoveSceneNodeOperation import RemoveSceneNodeOperation
|
||||
op.addOperation(RemoveSceneNodeOperation(node))
|
||||
|
||||
# Reset the print information
|
||||
self.getController().getScene().sceneChanged.emit(node)
|
||||
|
||||
op.push()
|
||||
from UM.Scene.Selection import Selection
|
||||
Selection.clear()
|
||||
|
@ -6,7 +6,6 @@ CuraAppDisplayName = "@CURA_APP_DISPLAY_NAME@"
|
||||
CuraVersion = "@CURA_VERSION@"
|
||||
CuraBuildType = "@CURA_BUILDTYPE@"
|
||||
CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
||||
CuraSDKVersion = "@CURA_SDK_VERSION@"
|
||||
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
||||
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
||||
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
|
||||
|
@ -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.fromLocalFile(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
|
||||
|
@ -1,14 +1,20 @@
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import List
|
||||
import numpy
|
||||
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
from UM.Mesh.MeshData import MeshData
|
||||
from cura.LayerPolygon import LayerPolygon
|
||||
|
||||
|
||||
class Layer:
|
||||
def __init__(self, layer_id):
|
||||
def __init__(self, layer_id: int) -> None:
|
||||
self._id = layer_id
|
||||
self._height = 0.0
|
||||
self._thickness = 0.0
|
||||
self._polygons = []
|
||||
self._polygons = [] # type: List[LayerPolygon]
|
||||
self._element_count = 0
|
||||
|
||||
@property
|
||||
@ -20,7 +26,7 @@ class Layer:
|
||||
return self._thickness
|
||||
|
||||
@property
|
||||
def polygons(self):
|
||||
def polygons(self) -> List[LayerPolygon]:
|
||||
return self._polygons
|
||||
|
||||
@property
|
||||
@ -33,14 +39,14 @@ class Layer:
|
||||
def setThickness(self, thickness):
|
||||
self._thickness = thickness
|
||||
|
||||
def lineMeshVertexCount(self):
|
||||
def lineMeshVertexCount(self) -> int:
|
||||
result = 0
|
||||
for polygon in self._polygons:
|
||||
result += polygon.lineMeshVertexCount()
|
||||
|
||||
return result
|
||||
|
||||
def lineMeshElementCount(self):
|
||||
def lineMeshElementCount(self) -> int:
|
||||
result = 0
|
||||
for polygon in self._polygons:
|
||||
result += polygon.lineMeshElementCount()
|
||||
@ -57,18 +63,18 @@ class Layer:
|
||||
result_index_offset += polygon.lineMeshElementCount()
|
||||
self._element_count += polygon.elementCount
|
||||
|
||||
return (result_vertex_offset, result_index_offset)
|
||||
return result_vertex_offset, result_index_offset
|
||||
|
||||
def createMesh(self):
|
||||
def createMesh(self) -> MeshData:
|
||||
return self.createMeshOrJumps(True)
|
||||
|
||||
def createJumps(self):
|
||||
def createJumps(self) -> MeshData:
|
||||
return self.createMeshOrJumps(False)
|
||||
|
||||
# Defines the two triplets of local point indices to use to draw the two faces for each line segment in createMeshOrJump
|
||||
__index_pattern = numpy.array([[0, 3, 2, 0, 1, 3]], dtype = numpy.int32 )
|
||||
|
||||
def createMeshOrJumps(self, make_mesh):
|
||||
def createMeshOrJumps(self, make_mesh: bool) -> MeshData:
|
||||
builder = MeshBuilder()
|
||||
|
||||
line_count = 0
|
||||
@ -79,14 +85,14 @@ class Layer:
|
||||
for polygon in self._polygons:
|
||||
line_count += polygon.jumpCount
|
||||
|
||||
# Reserve the neccesary space for the data upfront
|
||||
# Reserve the necessary space for the data upfront
|
||||
builder.reserveFaceAndVertexCount(2 * line_count, 4 * line_count)
|
||||
|
||||
for polygon in self._polygons:
|
||||
# Filter out the types of lines we are not interesed in depending on whether we are drawing the mesh or the jumps.
|
||||
# Filter out the types of lines we are not interested in depending on whether we are drawing the mesh or the jumps.
|
||||
index_mask = numpy.logical_not(polygon.jumpMask) if make_mesh else polygon.jumpMask
|
||||
|
||||
# Create an array with rows [p p+1] and only keep those we whant to draw based on make_mesh
|
||||
# Create an array with rows [p p+1] and only keep those we want to draw based on make_mesh
|
||||
points = numpy.concatenate((polygon.data[:-1], polygon.data[1:]), 1)[index_mask.ravel()]
|
||||
# Line types of the points we want to draw
|
||||
line_types = polygon.types[index_mask]
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Application import Application
|
||||
@ -20,7 +20,7 @@ class LayerPolygon:
|
||||
MoveCombingType = 8
|
||||
MoveRetractionType = 9
|
||||
SupportInterfaceType = 10
|
||||
PrimeTower = 11
|
||||
PrimeTowerType = 11
|
||||
__number_of_types = 12
|
||||
|
||||
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType)
|
||||
@ -61,19 +61,19 @@ class LayerPolygon:
|
||||
|
||||
# When type is used as index returns true if type == LayerPolygon.InfillType or type == LayerPolygon.SkinType or type == LayerPolygon.SupportInfillType
|
||||
# Should be generated in better way, not hardcoded.
|
||||
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1], dtype=numpy.bool)
|
||||
self._isInfillOrSkinTypeMap = numpy.array([0, 0, 0, 1, 0, 0, 1, 1, 0, 0, 1, 0], dtype = numpy.bool)
|
||||
|
||||
self._build_cache_line_mesh_mask = None # type: Optional[numpy.ndarray]
|
||||
self._build_cache_needed_points = None # type: Optional[numpy.ndarray]
|
||||
|
||||
def buildCache(self) -> None:
|
||||
# For the line mesh we do not draw Infill or Jumps. Therefore those lines are filtered out.
|
||||
self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype=bool)
|
||||
self._build_cache_line_mesh_mask = numpy.ones(self._jump_mask.shape, dtype = bool)
|
||||
mesh_line_count = numpy.sum(self._build_cache_line_mesh_mask)
|
||||
self._index_begin = 0
|
||||
self._index_end = mesh_line_count
|
||||
|
||||
self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype=numpy.bool)
|
||||
self._build_cache_needed_points = numpy.ones((len(self._types), 2), dtype = numpy.bool)
|
||||
# Only if the type of line segment changes do we need to add an extra vertex to change colors
|
||||
self._build_cache_needed_points[1:, 0][:, numpy.newaxis] = self._types[1:] != self._types[:-1]
|
||||
# Mark points as unneeded if they are of types we don't want in the line mesh according to the calculated mask
|
||||
@ -136,9 +136,9 @@ class LayerPolygon:
|
||||
self._index_begin += index_offset
|
||||
self._index_end += index_offset
|
||||
|
||||
indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype=numpy.int32).reshape((-1, 1))
|
||||
indices[self._index_begin:self._index_end, :] = numpy.arange(self._index_end-self._index_begin, dtype = numpy.int32).reshape((-1, 1))
|
||||
# When the line type changes the index needs to be increased by 2.
|
||||
indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype=numpy.int32).reshape((-1, 1))
|
||||
indices[self._index_begin:self._index_end, :] += numpy.cumsum(needed_points_list[line_mesh_mask.ravel(), 0], dtype = numpy.int32).reshape((-1, 1))
|
||||
# Each line segment goes from it's starting point p to p+1, offset by the vertex index.
|
||||
# The -1 is to compensate for the neccecarily True value of needed_points_list[0,0] which causes an unwanted +1 in cumsum above.
|
||||
indices[self._index_begin:self._index_end, :] += numpy.array([self._vertex_begin - 1, self._vertex_begin])
|
||||
@ -245,7 +245,7 @@ class LayerPolygon:
|
||||
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
|
||||
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
|
||||
theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType
|
||||
theme.getColor("layerview_prime_tower").getRgbF()
|
||||
theme.getColor("layerview_prime_tower").getRgbF() # PrimeTowerType
|
||||
])
|
||||
|
||||
return cls.__color_map
|
||||
|
@ -2,8 +2,9 @@
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
from PyQt5.QtCore import QObject, QUrl, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.PluginObject import PluginObject
|
||||
@ -33,6 +34,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
|
||||
@ -66,18 +73,26 @@ class MachineAction(QObject, PluginObject):
|
||||
return self._finished
|
||||
|
||||
## Protected helper to create a view object based on provided QML.
|
||||
def _createViewFromQML(self) -> None:
|
||||
def _createViewFromQML(self) -> Optional["QObject"]:
|
||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||
if plugin_path is None:
|
||||
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
|
||||
return
|
||||
return None
|
||||
path = os.path.join(plugin_path, self._qml_url)
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
self._view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
|
||||
view = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
|
||||
return view
|
||||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
def displayItem(self):
|
||||
if not self._view:
|
||||
self._createViewFromQML()
|
||||
return self._view
|
||||
@pyqtProperty(QUrl, constant = True)
|
||||
def qmlPath(self) -> "QUrl":
|
||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
||||
if plugin_path is None:
|
||||
Logger.log("e", "Cannot create QML view: cannot find plugin path for plugin [%s]", self.getPluginId())
|
||||
return QUrl("")
|
||||
path = os.path.join(plugin_path, self._qml_url)
|
||||
return QUrl.fromLocalFile(path)
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
def getDisplayItem(self) -> Optional["QObject"]:
|
||||
return self._createViewFromQML()
|
||||
|
@ -127,7 +127,7 @@ class MachineErrorChecker(QObject):
|
||||
|
||||
# Populate the (stack, key) tuples to check
|
||||
self._stacks_and_keys_to_check = deque()
|
||||
for stack in [global_stack] + list(global_stack.extruders.values()):
|
||||
for stack in global_stack.extruders.values():
|
||||
for key in stack.getAllKeys():
|
||||
self._stacks_and_keys_to_check.append((stack, key))
|
||||
|
||||
@ -168,7 +168,7 @@ class MachineErrorChecker(QObject):
|
||||
if validator_type:
|
||||
validator = validator_type(key)
|
||||
validation_state = validator(stack)
|
||||
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError):
|
||||
if validation_state in (ValidatorState.Exception, ValidatorState.MaximumError, ValidatorState.MinimumError, ValidatorState.Invalid):
|
||||
# Finish
|
||||
self._setResult(True)
|
||||
return
|
||||
|
@ -93,7 +93,7 @@ class MaterialManager(QObject):
|
||||
self._container_registry.findContainersMetadata(type = "material") if
|
||||
metadata.get("GUID")} # type: Dict[str, Dict[str, Any]]
|
||||
|
||||
self._material_group_map = dict() # type: Dict[str, MaterialGroup]
|
||||
self._material_group_map = dict()
|
||||
|
||||
# Map #1
|
||||
# root_material_id -> MaterialGroup
|
||||
@ -103,6 +103,8 @@ class MaterialManager(QObject):
|
||||
continue
|
||||
|
||||
root_material_id = material_metadata.get("base_file", "")
|
||||
if root_material_id not in material_metadatas: #Not a registered material profile. Don't store this in the look-up tables.
|
||||
continue
|
||||
if root_material_id not in self._material_group_map:
|
||||
self._material_group_map[root_material_id] = MaterialGroup(root_material_id, MaterialNode(material_metadatas[root_material_id]))
|
||||
self._material_group_map[root_material_id].is_read_only = self._container_registry.isReadOnly(root_material_id)
|
||||
@ -118,7 +120,7 @@ class MaterialManager(QObject):
|
||||
|
||||
# Map #1.5
|
||||
# GUID -> material group list
|
||||
self._guid_material_groups_map = defaultdict(list) # type: Dict[str, List[MaterialGroup]]
|
||||
self._guid_material_groups_map = defaultdict(list)
|
||||
for root_material_id, material_group in self._material_group_map.items():
|
||||
guid = material_group.root_material_node.getMetaDataEntry("GUID", "")
|
||||
self._guid_material_groups_map[guid].append(material_group)
|
||||
@ -200,7 +202,7 @@ class MaterialManager(QObject):
|
||||
|
||||
# Map #4
|
||||
# "machine" -> "nozzle name" -> "buildplate name" -> "root material ID" -> specific material InstanceContainer
|
||||
self._diameter_machine_nozzle_buildplate_material_map = dict() # type: Dict[str, Dict[str, MaterialNode]]
|
||||
self._diameter_machine_nozzle_buildplate_material_map = dict()
|
||||
for material_metadata in material_metadatas.values():
|
||||
self.__addMaterialMetadataIntoLookupTree(material_metadata)
|
||||
|
||||
|
264
cura/Machines/Models/DiscoveredPrintersModel.py
Normal file
264
cura/Machines/Models/DiscoveredPrintersModel.py
Normal file
@ -0,0 +1,264 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Callable, Dict, List, Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, pyqtProperty, pyqtSignal, QObject, QTimer
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Util import parseBool
|
||||
from UM.OutputDevice.OutputDeviceManager import ManualDeviceAdditionAttempt
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt5.QtCore import QObject
|
||||
from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice
|
||||
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
class DiscoveredPrinter(QObject):
|
||||
|
||||
def __init__(self, ip_address: str, key: str, name: str, create_callback: Callable[[str], None], machine_type: str,
|
||||
device: "NetworkedPrinterOutputDevice", parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self._ip_address = ip_address
|
||||
self._key = key
|
||||
self._name = name
|
||||
self.create_callback = create_callback
|
||||
self._machine_type = machine_type
|
||||
self._device = device
|
||||
|
||||
nameChanged = pyqtSignal()
|
||||
|
||||
def getKey(self) -> str:
|
||||
return self._key
|
||||
|
||||
@pyqtProperty(str, notify = nameChanged)
|
||||
def name(self) -> str:
|
||||
return self._name
|
||||
|
||||
def setName(self, name: str) -> None:
|
||||
if self._name != name:
|
||||
self._name = name
|
||||
self.nameChanged.emit()
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def address(self) -> str:
|
||||
return self._ip_address
|
||||
|
||||
machineTypeChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(str, notify = machineTypeChanged)
|
||||
def machineType(self) -> str:
|
||||
return self._machine_type
|
||||
|
||||
def setMachineType(self, machine_type: str) -> None:
|
||||
if self._machine_type != machine_type:
|
||||
self._machine_type = machine_type
|
||||
self.machineTypeChanged.emit()
|
||||
|
||||
# Checks if the given machine type name in the available machine list.
|
||||
# The machine type is a code name such as "ultimaker_3", while the machine type name is the human-readable name of
|
||||
# the machine type, which is "Ultimaker 3" for "ultimaker_3".
|
||||
def _hasHumanReadableMachineTypeName(self, machine_type_name: str) -> bool:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
results = CuraApplication.getInstance().getContainerRegistry().findDefinitionContainersMetadata(name = machine_type_name)
|
||||
return len(results) > 0
|
||||
|
||||
# Human readable machine type string
|
||||
@pyqtProperty(str, notify = machineTypeChanged)
|
||||
def readableMachineType(self) -> str:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
machine_manager = CuraApplication.getInstance().getMachineManager()
|
||||
# In NetworkOutputDevice, 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 self._hasHumanReadableMachineTypeName(self._machine_type):
|
||||
readable_type = self._machine_type
|
||||
else:
|
||||
readable_type = self._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:
|
||||
if self._hasHumanReadableMachineTypeName(self._machine_type):
|
||||
readable_type = self._machine_type
|
||||
else:
|
||||
readable_type = self._getMachineTypeNameFromId(self._machine_type)
|
||||
return not readable_type
|
||||
|
||||
def _getMachineTypeNameFromId(self, machine_type_id: str) -> str:
|
||||
machine_type_name = ""
|
||||
from cura.CuraApplication import CuraApplication
|
||||
results = CuraApplication.getInstance().getContainerRegistry().findDefinitionContainersMetadata(id = machine_type_id)
|
||||
if results:
|
||||
machine_type_name = results[0]["name"]
|
||||
return machine_type_name
|
||||
|
||||
@pyqtProperty(QObject, constant = True)
|
||||
def device(self) -> "NetworkedPrinterOutputDevice":
|
||||
return self._device
|
||||
|
||||
@pyqtProperty(bool, constant = True)
|
||||
def isHostOfGroup(self) -> bool:
|
||||
return getattr(self._device, "clusterSize", 1) > 0
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def sectionName(self) -> str:
|
||||
if self.isUnknownMachineType or not self.isHostOfGroup:
|
||||
return catalog.i18nc("@label", "The printer(s) below cannot be connected because they are part of a group")
|
||||
else:
|
||||
return catalog.i18nc("@label", "Available networked printers")
|
||||
|
||||
|
||||
#
|
||||
# Discovered printers are all the printers that were found on the network, which provide a more convenient way
|
||||
# to add networked printers (Plugin finds a bunch of printers, user can select one from the list, plugin can then
|
||||
# add that printer to Cura as the active one).
|
||||
#
|
||||
class DiscoveredPrintersModel(QObject):
|
||||
|
||||
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self._application = application
|
||||
self._discovered_printer_by_ip_dict = dict() # type: Dict[str, DiscoveredPrinter]
|
||||
|
||||
self._plugin_for_manual_device = None # type: Optional[OutputDevicePlugin]
|
||||
self._manual_device_address = ""
|
||||
|
||||
self._manual_device_request_timeout_in_seconds = 5 # timeout for adding a manual device in seconds
|
||||
self._manual_device_request_timer = QTimer()
|
||||
self._manual_device_request_timer.setInterval(self._manual_device_request_timeout_in_seconds * 1000)
|
||||
self._manual_device_request_timer.setSingleShot(True)
|
||||
self._manual_device_request_timer.timeout.connect(self._onManualRequestTimeout)
|
||||
|
||||
discoveredPrintersChanged = pyqtSignal()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def checkManualDevice(self, address: str) -> None:
|
||||
if self.hasManualDeviceRequestInProgress:
|
||||
Logger.log("i", "A manual device request for address [%s] is still in progress, do nothing",
|
||||
self._manual_device_address)
|
||||
return
|
||||
|
||||
priority_order = [
|
||||
ManualDeviceAdditionAttempt.PRIORITY,
|
||||
ManualDeviceAdditionAttempt.POSSIBLE,
|
||||
] # type: List[ManualDeviceAdditionAttempt]
|
||||
|
||||
all_plugins_dict = self._application.getOutputDeviceManager().getAllOutputDevicePlugins()
|
||||
|
||||
can_add_manual_plugins = [item for item in filter(
|
||||
lambda plugin_item: plugin_item.canAddManualDevice(address) in priority_order,
|
||||
all_plugins_dict.values())]
|
||||
|
||||
if not can_add_manual_plugins:
|
||||
Logger.log("d", "Could not find a plugin to accept adding %s manually via address.", address)
|
||||
return
|
||||
|
||||
plugin = max(can_add_manual_plugins, key = lambda p: priority_order.index(p.canAddManualDevice(address)))
|
||||
self._plugin_for_manual_device = plugin
|
||||
self._plugin_for_manual_device.addManualDevice(address, callback = self._onManualDeviceRequestFinished)
|
||||
self._manual_device_address = address
|
||||
self._manual_device_request_timer.start()
|
||||
self.hasManualDeviceRequestInProgressChanged.emit()
|
||||
|
||||
@pyqtSlot()
|
||||
def cancelCurrentManualDeviceRequest(self) -> None:
|
||||
self._manual_device_request_timer.stop()
|
||||
|
||||
if self._manual_device_address:
|
||||
if self._plugin_for_manual_device is not None:
|
||||
self._plugin_for_manual_device.removeManualDevice(self._manual_device_address, address = self._manual_device_address)
|
||||
self._manual_device_address = ""
|
||||
self._plugin_for_manual_device = None
|
||||
self.hasManualDeviceRequestInProgressChanged.emit()
|
||||
self.manualDeviceRequestFinished.emit(False)
|
||||
|
||||
def _onManualRequestTimeout(self) -> None:
|
||||
Logger.log("w", "Manual printer [%s] request timed out. Cancel the current request.", self._manual_device_address)
|
||||
self.cancelCurrentManualDeviceRequest()
|
||||
|
||||
hasManualDeviceRequestInProgressChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = hasManualDeviceRequestInProgressChanged)
|
||||
def hasManualDeviceRequestInProgress(self) -> bool:
|
||||
return self._manual_device_address != ""
|
||||
|
||||
manualDeviceRequestFinished = pyqtSignal(bool, arguments = ["success"])
|
||||
|
||||
def _onManualDeviceRequestFinished(self, success: bool, address: str) -> None:
|
||||
self._manual_device_request_timer.stop()
|
||||
if address == self._manual_device_address:
|
||||
self._manual_device_address = ""
|
||||
self.hasManualDeviceRequestInProgressChanged.emit()
|
||||
self.manualDeviceRequestFinished.emit(success)
|
||||
|
||||
@pyqtProperty("QVariantMap", notify = discoveredPrintersChanged)
|
||||
def discoveredPrintersByAddress(self) -> Dict[str, DiscoveredPrinter]:
|
||||
return self._discovered_printer_by_ip_dict
|
||||
|
||||
@pyqtProperty("QVariantList", notify = discoveredPrintersChanged)
|
||||
def discoveredPrinters(self) -> List["DiscoveredPrinter"]:
|
||||
item_list = list(
|
||||
x for x in self._discovered_printer_by_ip_dict.values() if not parseBool(x.device.getProperty("temporary")))
|
||||
|
||||
# Split the printers into 2 lists and sort them ascending based on names.
|
||||
available_list = []
|
||||
not_available_list = []
|
||||
for item in item_list:
|
||||
if item.isUnknownMachineType or getattr(item.device, "clusterSize", 1) < 1:
|
||||
not_available_list.append(item)
|
||||
else:
|
||||
available_list.append(item)
|
||||
|
||||
available_list.sort(key = lambda x: x.device.name)
|
||||
not_available_list.sort(key = lambda x: x.device.name)
|
||||
|
||||
return available_list + not_available_list
|
||||
|
||||
def addDiscoveredPrinter(self, ip_address: str, key: str, name: str, create_callback: Callable[[str], None],
|
||||
machine_type: str, device: "NetworkedPrinterOutputDevice") -> None:
|
||||
if ip_address in self._discovered_printer_by_ip_dict:
|
||||
Logger.log("e", "Printer with ip [%s] has already been added", ip_address)
|
||||
return
|
||||
|
||||
discovered_printer = DiscoveredPrinter(ip_address, key, name, create_callback, machine_type, device, parent = self)
|
||||
self._discovered_printer_by_ip_dict[ip_address] = discovered_printer
|
||||
self.discoveredPrintersChanged.emit()
|
||||
|
||||
def updateDiscoveredPrinter(self, ip_address: str,
|
||||
name: Optional[str] = None,
|
||||
machine_type: Optional[str] = None) -> None:
|
||||
if ip_address not in self._discovered_printer_by_ip_dict:
|
||||
Logger.log("w", "Printer with ip [%s] is not known", ip_address)
|
||||
return
|
||||
|
||||
item = self._discovered_printer_by_ip_dict[ip_address]
|
||||
|
||||
if name is not None:
|
||||
item.setName(name)
|
||||
if machine_type is not None:
|
||||
item.setMachineType(machine_type)
|
||||
|
||||
def removeDiscoveredPrinter(self, ip_address: str) -> None:
|
||||
if ip_address not in self._discovered_printer_by_ip_dict:
|
||||
Logger.log("w", "Key [%s] does not exist in the discovered printers list.", ip_address)
|
||||
return
|
||||
|
||||
del self._discovered_printer_by_ip_dict[ip_address]
|
||||
self.discoveredPrintersChanged.emit()
|
||||
|
||||
# A convenience function for QML to create a machine (GlobalStack) out of the given discovered printer.
|
||||
# This function invokes the given discovered printer's "create_callback" to do this.
|
||||
@pyqtSlot("QVariant")
|
||||
def createMachineFromDiscoveredPrinter(self, discovered_printer: "DiscoveredPrinter") -> None:
|
||||
discovered_printer.create_callback(discovered_printer.getKey())
|
@ -2,23 +2,25 @@
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
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
|
||||
|
112
cura/Machines/Models/FirstStartMachineActionsModel.py
Normal file
112
cura/Machines/Models/FirstStartMachineActionsModel.py
Normal file
@ -0,0 +1,112 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional, Dict, Any, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QObject, Qt, pyqtProperty, pyqtSignal, pyqtSlot
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
#
|
||||
# This model holds all first-start machine actions for the currently active machine. It has 2 roles:
|
||||
# - title : the title/name of the action
|
||||
# - content : the QObject of the QML content of the action
|
||||
# - action : the MachineAction object itself
|
||||
#
|
||||
class FirstStartMachineActionsModel(ListModel):
|
||||
|
||||
TitleRole = Qt.UserRole + 1
|
||||
ContentRole = Qt.UserRole + 2
|
||||
ActionRole = Qt.UserRole + 3
|
||||
|
||||
def __init__(self, application: "CuraApplication", parent: Optional[QObject] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.addRoleName(self.TitleRole, "title")
|
||||
self.addRoleName(self.ContentRole, "content")
|
||||
self.addRoleName(self.ActionRole, "action")
|
||||
|
||||
self._current_action_index = 0
|
||||
|
||||
self._application = application
|
||||
self._application.initializationFinished.connect(self._initialize)
|
||||
|
||||
self._previous_global_stack = None
|
||||
|
||||
def _initialize(self) -> None:
|
||||
self._application.getMachineManager().globalContainerChanged.connect(self._update)
|
||||
self._update()
|
||||
|
||||
currentActionIndexChanged = pyqtSignal()
|
||||
allFinished = pyqtSignal() # Emitted when all actions have been finished.
|
||||
|
||||
@pyqtProperty(int, notify = currentActionIndexChanged)
|
||||
def currentActionIndex(self) -> int:
|
||||
return self._current_action_index
|
||||
|
||||
@pyqtProperty("QVariantMap", notify = currentActionIndexChanged)
|
||||
def currentItem(self) -> Optional[Dict[str, Any]]:
|
||||
if self._current_action_index >= self.count:
|
||||
return dict()
|
||||
else:
|
||||
return self.getItem(self._current_action_index)
|
||||
|
||||
@pyqtProperty(bool, notify = currentActionIndexChanged)
|
||||
def hasMoreActions(self) -> bool:
|
||||
return self._current_action_index < self.count - 1
|
||||
|
||||
@pyqtSlot()
|
||||
def goToNextAction(self) -> None:
|
||||
# finish the current item
|
||||
if "action" in self.currentItem:
|
||||
self.currentItem["action"].setFinished()
|
||||
|
||||
if not self.hasMoreActions:
|
||||
self.allFinished.emit()
|
||||
self.reset()
|
||||
return
|
||||
|
||||
self._current_action_index += 1
|
||||
self.currentActionIndexChanged.emit()
|
||||
|
||||
# Resets the current action index to 0 so the wizard panel can show actions from the beginning.
|
||||
@pyqtSlot()
|
||||
def reset(self) -> None:
|
||||
self._current_action_index = 0
|
||||
self.currentActionIndexChanged.emit()
|
||||
|
||||
if self.count == 0:
|
||||
self.allFinished.emit()
|
||||
|
||||
def _update(self) -> None:
|
||||
global_stack = self._application.getMachineManager().activeMachine
|
||||
if global_stack is None:
|
||||
self.setItems([])
|
||||
return
|
||||
|
||||
# Do not update if the machine has not been switched. This can cause the SettingProviders on the Machine
|
||||
# Setting page to do a force update, but they can use potential outdated cached values.
|
||||
if self._previous_global_stack is not None and global_stack.getId() == self._previous_global_stack.getId():
|
||||
return
|
||||
self._previous_global_stack = global_stack
|
||||
|
||||
definition_id = global_stack.definition.getId()
|
||||
first_start_actions = self._application.getMachineActionManager().getFirstStartActions(definition_id)
|
||||
|
||||
item_list = []
|
||||
for item in first_start_actions:
|
||||
item_list.append({"title": item.label,
|
||||
"content": item.getDisplayItem(),
|
||||
"action": item,
|
||||
})
|
||||
item.reset()
|
||||
|
||||
self.setItems(item_list)
|
||||
self.reset()
|
||||
|
||||
|
||||
__all__ = ["FirstStartMachineActionsModel"]
|
@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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):
|
||||
|
@ -1,14 +1,14 @@
|
||||
# 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 UM.Util import parseBool
|
||||
|
||||
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 +18,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,35 +40,38 @@ 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:
|
||||
items = []
|
||||
|
||||
container_stacks = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
||||
|
||||
for container_stack in container_stacks:
|
||||
has_remote_connection = False
|
||||
|
||||
for connection_type in container_stack.configuredConnectionTypes:
|
||||
has_remote_connection |= connection_type in [ConnectionType.NetworkConnection.value, ConnectionType.CloudConnection.value]
|
||||
has_remote_connection |= connection_type in [ConnectionType.NetworkConnection.value,
|
||||
ConnectionType.CloudConnection.value]
|
||||
|
||||
if container_stack.getMetaDataEntry("hidden", False) in ["True", True]:
|
||||
if parseBool(container_stack.getMetaDataEntry("hidden", False)):
|
||||
continue
|
||||
|
||||
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"], i["name"]))
|
||||
self.setItems(items)
|
@ -1,82 +0,0 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.Qt.ListModel import ListModel
|
||||
|
||||
from PyQt5.QtCore import Qt
|
||||
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
#
|
||||
# This the QML model for the quality management page.
|
||||
#
|
||||
class MachineManagementModel(ListModel):
|
||||
NameRole = Qt.UserRole + 1
|
||||
IdRole = Qt.UserRole + 2
|
||||
MetaDataRole = Qt.UserRole + 3
|
||||
GroupRole = Qt.UserRole + 4
|
||||
|
||||
def __init__(self, parent = None):
|
||||
super().__init__(parent)
|
||||
self.addRoleName(self.NameRole, "name")
|
||||
self.addRoleName(self.IdRole, "id")
|
||||
self.addRoleName(self.MetaDataRole, "metadata")
|
||||
self.addRoleName(self.GroupRole, "group")
|
||||
self._local_container_stacks = []
|
||||
self._network_container_stacks = []
|
||||
|
||||
# Listen to changes
|
||||
ContainerRegistry.getInstance().containerAdded.connect(self._onContainerChanged)
|
||||
ContainerRegistry.getInstance().containerMetaDataChanged.connect(self._onContainerChanged)
|
||||
ContainerRegistry.getInstance().containerRemoved.connect(self._onContainerChanged)
|
||||
self._filter_dict = {}
|
||||
self._update()
|
||||
|
||||
## Handler for container added/removed events from registry
|
||||
def _onContainerChanged(self, container):
|
||||
# We only need to update when the added / removed container is a stack.
|
||||
if isinstance(container, ContainerStack) and container.getMetaDataEntry("type") == "machine":
|
||||
self._update()
|
||||
|
||||
## Private convenience function to reset & repopulate the model.
|
||||
def _update(self):
|
||||
items = []
|
||||
|
||||
# Get first the network enabled printers
|
||||
network_filter_printers = {"type": "machine",
|
||||
"um_network_key": "*",
|
||||
"hidden": "False"}
|
||||
self._network_container_stacks = ContainerRegistry.getInstance().findContainerStacks(**network_filter_printers)
|
||||
self._network_container_stacks.sort(key = lambda i: i.getMetaDataEntry("group_name", ""))
|
||||
|
||||
for container in self._network_container_stacks:
|
||||
metadata = container.getMetaData().copy()
|
||||
if container.getBottom():
|
||||
metadata["definition_name"] = container.getBottom().getName()
|
||||
|
||||
items.append({"name": metadata.get("group_name", ""),
|
||||
"id": container.getId(),
|
||||
"metadata": metadata,
|
||||
"group": catalog.i18nc("@info:title", "Network enabled printers")})
|
||||
|
||||
# Get now the local printers
|
||||
local_filter_printers = {"type": "machine", "um_network_key": None}
|
||||
self._local_container_stacks = ContainerRegistry.getInstance().findContainerStacks(**local_filter_printers)
|
||||
self._local_container_stacks.sort(key = lambda i: i.getName())
|
||||
|
||||
for container in self._local_container_stacks:
|
||||
metadata = container.getMetaData().copy()
|
||||
if container.getBottom():
|
||||
metadata["definition_name"] = container.getBottom().getName()
|
||||
|
||||
items.append({"name": container.getName(),
|
||||
"id": container.getId(),
|
||||
"metadata": metadata,
|
||||
"group": catalog.i18nc("@info:title", "Local printers")})
|
||||
|
||||
self.setItems(items)
|
@ -1,9 +1,8 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# 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):
|
||||
|
@ -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
|
||||
|
||||
|
@ -202,9 +202,6 @@ class QualityManager(QObject):
|
||||
def getQualityGroups(self, machine: "GlobalStack") -> Dict[str, QualityGroup]:
|
||||
machine_definition_id = getMachineDefinitionIDForQualitySearch(machine.definition)
|
||||
|
||||
# This determines if we should only get the global qualities for the global stack and skip the global qualities for the extruder stacks
|
||||
has_machine_specific_qualities = machine.getHasMachineQuality()
|
||||
|
||||
# To find the quality container for the GlobalStack, check in the following fall-back manner:
|
||||
# (1) the machine-specific node
|
||||
# (2) the generic node
|
||||
@ -442,7 +439,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:
|
||||
|
@ -85,7 +85,14 @@ class VariantManager:
|
||||
if variant_definition not in self._machine_to_buildplate_dict_map:
|
||||
self._machine_to_buildplate_dict_map[variant_definition] = OrderedDict()
|
||||
|
||||
variant_container = self._container_registry.findContainers(type = "variant", id = variant_metadata["id"])[0]
|
||||
try:
|
||||
variant_container = self._container_registry.findContainers(type = "variant", id = variant_metadata["id"])[0]
|
||||
except IndexError as e:
|
||||
# It still needs to break, but we want to know what variant ID made it break.
|
||||
msg = "Unable to find build plate variant with the id [%s]" % variant_metadata["id"]
|
||||
Logger.logException("e", msg)
|
||||
raise IndexError(msg)
|
||||
|
||||
buildplate_type = variant_container.getProperty("machine_buildplate_type", "value")
|
||||
if buildplate_type not in self._machine_to_buildplate_dict_map[variant_definition]:
|
||||
self._machine_to_variant_dict_map[variant_definition][buildplate_type] = dict()
|
||||
|
@ -2,10 +2,12 @@
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import copy
|
||||
from typing import List
|
||||
|
||||
from UM.Job import Job
|
||||
from UM.Operations.GroupedOperation import GroupedOperation
|
||||
from UM.Message import Message
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
@ -23,7 +25,7 @@ class MultiplyObjectsJob(Job):
|
||||
self._count = count
|
||||
self._min_offset = min_offset
|
||||
|
||||
def run(self):
|
||||
def run(self) -> None:
|
||||
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
|
||||
dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Objects"))
|
||||
status_message.show()
|
||||
@ -33,13 +35,15 @@ class MultiplyObjectsJob(Job):
|
||||
current_progress = 0
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if global_container_stack is None:
|
||||
return # We can't do anything in this case.
|
||||
machine_width = global_container_stack.getProperty("machine_width", "value")
|
||||
machine_depth = global_container_stack.getProperty("machine_depth", "value")
|
||||
|
||||
root = scene.getRoot()
|
||||
scale = 0.5
|
||||
arranger = Arrange.create(x = machine_width, y = machine_depth, scene_root = root, scale = scale, min_offset = self._min_offset)
|
||||
processed_nodes = []
|
||||
processed_nodes = [] # type: List[SceneNode]
|
||||
nodes = []
|
||||
|
||||
not_fit_count = 0
|
||||
@ -67,7 +71,11 @@ class MultiplyObjectsJob(Job):
|
||||
new_node = copy.deepcopy(node)
|
||||
solution_found = False
|
||||
if not node_too_big:
|
||||
solution_found = arranger.findNodePlacement(new_node, offset_shape_arr, hull_shape_arr)
|
||||
if offset_shape_arr is not None and hull_shape_arr is not None:
|
||||
solution_found = arranger.findNodePlacement(new_node, offset_shape_arr, hull_shape_arr)
|
||||
else:
|
||||
# The node has no shape, so no need to arrange it. The solution is simple: Do nothing.
|
||||
solution_found = True
|
||||
|
||||
if node_too_big or not solution_found:
|
||||
found_solution_for_all = False
|
||||
|
@ -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 "",
|
||||
|
@ -25,6 +25,10 @@ class AuthorizationRequestHandler(BaseHTTPRequestHandler):
|
||||
self.authorization_callback = None # type: Optional[Callable[[AuthenticationResponse], None]]
|
||||
self.verification_code = None # type: Optional[str]
|
||||
|
||||
# CURA-6609: Some browser seems to issue a HEAD instead of GET request as the callback.
|
||||
def do_HEAD(self) -> None:
|
||||
self.do_GET()
|
||||
|
||||
def do_GET(self) -> None:
|
||||
# Extract values from the query string.
|
||||
parsed_url = urlparse(self.path)
|
||||
|
@ -2,15 +2,19 @@
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import json
|
||||
import webbrowser
|
||||
from datetime import datetime, timedelta
|
||||
import os
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import requests.exceptions
|
||||
|
||||
from PyQt5.QtCore import QUrl
|
||||
from PyQt5.QtGui import QDesktopServices
|
||||
|
||||
from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Platform import Platform
|
||||
from UM.Signal import Signal
|
||||
|
||||
from cura.OAuth2.LocalAuthorizationServer import LocalAuthorizationServer
|
||||
@ -34,6 +38,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 +74,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 +90,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 +98,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 +136,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)
|
||||
@ -154,7 +167,7 @@ class AuthorizationService:
|
||||
})
|
||||
|
||||
# Open the authorization page in a new browser window.
|
||||
webbrowser.open_new("{}?{}".format(self._auth_url, query_string))
|
||||
QDesktopServices.openUrl(QUrl("{}?{}".format(self._auth_url, query_string)))
|
||||
|
||||
# Start a local web server to receive the callback URL on.
|
||||
self._server.start(verification_code)
|
||||
@ -194,6 +207,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 +220,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()
|
||||
|
@ -63,6 +63,10 @@ class LocalAuthorizationServer:
|
||||
Logger.log("d", "Stopping local oauth2 web server...")
|
||||
|
||||
if self._web_server:
|
||||
self._web_server.server_close()
|
||||
try:
|
||||
self._web_server.server_close()
|
||||
except OSError:
|
||||
# OS error can happen if the socket was already closed. We really don't care about that case.
|
||||
pass
|
||||
self._web_server = None
|
||||
self._web_server_thread = None
|
||||
|
@ -1,100 +0,0 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Scene.Camera import Camera
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.i18n import i18nCatalog
|
||||
from collections import defaultdict
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
## Keep track of all objects in the project
|
||||
class ObjectsModel(ListModel):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSceneDelayed)
|
||||
Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed)
|
||||
|
||||
self._update_timer = QTimer()
|
||||
self._update_timer.setInterval(200)
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self._update)
|
||||
|
||||
self._build_plate_number = -1
|
||||
|
||||
def setActiveBuildPlate(self, nr):
|
||||
if self._build_plate_number != nr:
|
||||
self._build_plate_number = nr
|
||||
self._update()
|
||||
|
||||
def _updateSceneDelayed(self, source):
|
||||
if not isinstance(source, Camera):
|
||||
self._update_timer.start()
|
||||
|
||||
def _updateDelayed(self, *args):
|
||||
self._update_timer.start()
|
||||
|
||||
def _update(self, *args):
|
||||
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)
|
||||
|
||||
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()):
|
||||
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"):
|
||||
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
|
||||
node_build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||
if filter_current_build_plate and node_build_plate_number != active_build_plate_number:
|
||||
continue
|
||||
|
||||
if not node.callDecoration("isGroup"):
|
||||
name = node.getName()
|
||||
|
||||
else:
|
||||
name = catalog.i18nc("@label", "Group #{group_nr}").format(group_nr = str(group_nr))
|
||||
group_nr += 1
|
||||
|
||||
if hasattr(node, "isOutsideBuildArea"):
|
||||
is_outside_build_area = node.isOutsideBuildArea()
|
||||
else:
|
||||
is_outside_build_area = False
|
||||
|
||||
#check if we already have an instance of the object based on name
|
||||
name_count_dict[name] += 1
|
||||
name_count = name_count_dict[name]
|
||||
|
||||
if name_count > 1:
|
||||
name = "{0}({1})".format(name, name_count-1)
|
||||
node.setName(name)
|
||||
|
||||
nodes.append({
|
||||
"name": name,
|
||||
"isSelected": Selection.isSelected(node),
|
||||
"isOutsideBuildArea": is_outside_build_area,
|
||||
"buildPlateNumber": node_build_plate_number,
|
||||
"node": node
|
||||
})
|
||||
|
||||
nodes = sorted(nodes, key=lambda n: n["name"])
|
||||
self.setItems(nodes)
|
||||
|
||||
self.itemsChanged.emit()
|
||||
|
||||
@staticmethod
|
||||
def createObjectsModel():
|
||||
return ObjectsModel()
|
@ -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)
|
||||
|
@ -49,18 +49,20 @@ class PlatformPhysics:
|
||||
return
|
||||
|
||||
root = self._controller.getScene().getRoot()
|
||||
build_volume = Application.getInstance().getBuildVolume()
|
||||
build_volume.updateNodeBoundaryCheck()
|
||||
|
||||
# Keep a list of nodes that are moving. We use this so that we don't move two intersecting objects in the
|
||||
# same direction.
|
||||
transformed_nodes = []
|
||||
|
||||
# We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
|
||||
# By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
|
||||
nodes = list(BreadthFirstIterator(root))
|
||||
|
||||
# Only check nodes inside build area.
|
||||
nodes = [node for node in nodes if (hasattr(node, "_outside_buildarea") and not node._outside_buildarea)]
|
||||
|
||||
# We try to shuffle all the nodes to prevent "locked" situations, where iteration B inverts iteration A.
|
||||
# By shuffling the order of the nodes, this might happen a few times, but at some point it will resolve.
|
||||
random.shuffle(nodes)
|
||||
for node in nodes:
|
||||
if node is root or not isinstance(node, SceneNode) or node.getBoundingBox() is None:
|
||||
@ -76,7 +78,7 @@ class PlatformPhysics:
|
||||
move_vector = move_vector.set(y = -bbox.bottom + z_offset)
|
||||
|
||||
# If there is no convex hull for the node, start calculating it and continue.
|
||||
if not node.getDecorator(ConvexHullDecorator):
|
||||
if not node.getDecorator(ConvexHullDecorator) and not node.callDecoration("isNonPrintingMesh"):
|
||||
node.addDecorator(ConvexHullDecorator())
|
||||
|
||||
# only push away objects if this node is a printing mesh
|
||||
@ -160,7 +162,6 @@ class PlatformPhysics:
|
||||
op.push()
|
||||
|
||||
# After moving, we have to evaluate the boundary checks for nodes
|
||||
build_volume = Application.getInstance().getBuildVolume()
|
||||
build_volume.updateNodeBoundaryCheck()
|
||||
|
||||
def _onToolOperationStarted(self, tool):
|
||||
|
@ -1,7 +1,8 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from typing import Optional, TYPE_CHECKING, cast
|
||||
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Resources import Resources
|
||||
@ -12,6 +13,7 @@ from UM.View.RenderBatch import RenderBatch
|
||||
|
||||
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.View.GL.ShaderProgram import ShaderProgram
|
||||
@ -44,9 +46,9 @@ class PreviewPass(RenderPass):
|
||||
|
||||
self._renderer = Application.getInstance().getRenderer()
|
||||
|
||||
self._shader = None #type: Optional[ShaderProgram]
|
||||
self._non_printing_shader = None #type: Optional[ShaderProgram]
|
||||
self._support_mesh_shader = None #type: Optional[ShaderProgram]
|
||||
self._shader = None # type: Optional[ShaderProgram]
|
||||
self._non_printing_shader = None # type: Optional[ShaderProgram]
|
||||
self._support_mesh_shader = None # type: Optional[ShaderProgram]
|
||||
self._scene = Application.getInstance().getController().getScene()
|
||||
|
||||
# Set the camera to be used by this render pass
|
||||
@ -83,30 +85,31 @@ class PreviewPass(RenderPass):
|
||||
batch_support_mesh = RenderBatch(self._support_mesh_shader)
|
||||
|
||||
# Fill up the batch with objects that can be sliced.
|
||||
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.isVisible():
|
||||
per_mesh_stack = node.callDecoration("getStack")
|
||||
if node.callDecoration("isNonThumbnailVisibleMesh"):
|
||||
# Non printing mesh
|
||||
continue
|
||||
elif per_mesh_stack is not None and per_mesh_stack.getProperty("support_mesh", "value"):
|
||||
# Support mesh
|
||||
uniforms = {}
|
||||
shade_factor = 0.6
|
||||
diffuse_color = node.getDiffuseColor()
|
||||
diffuse_color2 = [
|
||||
diffuse_color[0] * shade_factor,
|
||||
diffuse_color[1] * shade_factor,
|
||||
diffuse_color[2] * shade_factor,
|
||||
1.0]
|
||||
uniforms["diffuse_color"] = prettier_color(diffuse_color)
|
||||
uniforms["diffuse_color_2"] = diffuse_color2
|
||||
batch_support_mesh.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
|
||||
else:
|
||||
# Normal scene node
|
||||
uniforms = {}
|
||||
uniforms["diffuse_color"] = prettier_color(node.getDiffuseColor())
|
||||
batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
|
||||
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||
if hasattr(node, "_outside_buildarea") and not getattr(node, "_outside_buildarea"):
|
||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible():
|
||||
per_mesh_stack = node.callDecoration("getStack")
|
||||
if node.callDecoration("isNonThumbnailVisibleMesh"):
|
||||
# Non printing mesh
|
||||
continue
|
||||
elif per_mesh_stack is not None and per_mesh_stack.getProperty("support_mesh", "value"):
|
||||
# Support mesh
|
||||
uniforms = {}
|
||||
shade_factor = 0.6
|
||||
diffuse_color = cast(CuraSceneNode, node).getDiffuseColor()
|
||||
diffuse_color2 = [
|
||||
diffuse_color[0] * shade_factor,
|
||||
diffuse_color[1] * shade_factor,
|
||||
diffuse_color[2] * shade_factor,
|
||||
1.0]
|
||||
uniforms["diffuse_color"] = prettier_color(diffuse_color)
|
||||
uniforms["diffuse_color_2"] = diffuse_color2
|
||||
batch_support_mesh.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
|
||||
else:
|
||||
# Normal scene node
|
||||
uniforms = {}
|
||||
uniforms["diffuse_color"] = prettier_color(cast(CuraSceneNode, node).getDiffuseColor())
|
||||
batch.addItem(node.getWorldTransformation(), node.getMeshData(), uniforms = uniforms)
|
||||
|
||||
self.bind()
|
||||
|
||||
|
@ -9,7 +9,7 @@ from typing import Union
|
||||
|
||||
MYPY = False
|
||||
if MYPY:
|
||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||
|
||||
class FirmwareUpdater(QObject):
|
||||
firmwareProgressChanged = pyqtSignal()
|
||||
|
@ -3,14 +3,15 @@
|
||||
|
||||
from typing import TYPE_CHECKING, Set, Union, Optional
|
||||
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
from PyQt5.QtCore import QTimer
|
||||
|
||||
from .PrinterOutputController import PrinterOutputController
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice
|
||||
from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
|
||||
from .Models.PrintJobOutputModel import PrintJobOutputModel
|
||||
from .Models.PrinterOutputModel import PrinterOutputModel
|
||||
from .PrinterOutputDevice import PrinterOutputDevice
|
||||
from .Models.ExtruderOutputModel import ExtruderOutputModel
|
||||
|
||||
|
||||
class GenericOutputController(PrinterOutputController):
|
||||
@ -54,7 +55,7 @@ class GenericOutputController(PrinterOutputController):
|
||||
self._preheat_hotends_timer.stop()
|
||||
for extruder in self._preheat_hotends:
|
||||
extruder.updateIsPreheating(False)
|
||||
self._preheat_hotends = set() # type: Set[ExtruderOutputModel]
|
||||
self._preheat_hotends = set()
|
||||
|
||||
def moveHead(self, printer: "PrinterOutputModel", x, y, z, speed) -> None:
|
||||
self._output_device.sendCommand("G91")
|
||||
@ -158,7 +159,7 @@ class GenericOutputController(PrinterOutputController):
|
||||
def _onPreheatHotendsTimerFinished(self) -> None:
|
||||
for extruder in self._preheat_hotends:
|
||||
self.setTargetHotendTemperature(extruder.getPrinter(), extruder.getPosition(), 0)
|
||||
self._preheat_hotends = set() #type: Set[ExtruderOutputModel]
|
||||
self._preheat_hotends = set()
|
||||
|
||||
# Cancel any ongoing preheating timers, without setting back the temperature to 0
|
||||
# This can be used eg at the start of a print
|
||||
@ -166,7 +167,7 @@ class GenericOutputController(PrinterOutputController):
|
||||
if self._preheat_hotends_timer.isActive():
|
||||
for extruder in self._preheat_hotends:
|
||||
extruder.updateIsPreheating(False)
|
||||
self._preheat_hotends = set() #type: Set[ExtruderOutputModel]
|
||||
self._preheat_hotends = set()
|
||||
|
||||
self._preheat_hotends_timer.stop()
|
||||
|
||||
|
@ -1,34 +0,0 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, QVariant, pyqtSlot
|
||||
|
||||
|
||||
class MaterialOutputModel(QObject):
|
||||
def __init__(self, guid, type, color, brand, name, parent = None):
|
||||
super().__init__(parent)
|
||||
self._guid = guid
|
||||
self._type = type
|
||||
self._color = color
|
||||
self._brand = brand
|
||||
self._name = name
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def guid(self):
|
||||
return self._guid
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def brand(self):
|
||||
return self._brand
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def color(self):
|
||||
return self._color
|
||||
|
||||
@pyqtProperty(str, constant=True)
|
||||
def name(self):
|
||||
return self._name
|
@ -4,7 +4,7 @@ from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal
|
||||
|
||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||
from .MaterialOutputModel import MaterialOutputModel
|
||||
|
||||
|
||||
class ExtruderConfigurationModel(QObject):
|
||||
@ -25,7 +25,7 @@ class ExtruderConfigurationModel(QObject):
|
||||
return self._position
|
||||
|
||||
def setMaterial(self, material: Optional[MaterialOutputModel]) -> None:
|
||||
if self._hotend_id != material:
|
||||
if self._material != material:
|
||||
self._material = material
|
||||
self.extruderConfigurationChanged.emit()
|
||||
|
||||
@ -33,7 +33,7 @@ class ExtruderConfigurationModel(QObject):
|
||||
def activeMaterial(self) -> Optional[MaterialOutputModel]:
|
||||
return self._material
|
||||
|
||||
@pyqtProperty(QObject, fset=setMaterial, notify=extruderConfigurationChanged)
|
||||
@pyqtProperty(QObject, fset = setMaterial, notify = extruderConfigurationChanged)
|
||||
def material(self) -> Optional[MaterialOutputModel]:
|
||||
return self._material
|
||||
|
||||
@ -62,9 +62,26 @@ class ExtruderConfigurationModel(QObject):
|
||||
return " ".join(message_chunks)
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
return hash(self) == hash(other)
|
||||
if not isinstance(other, ExtruderConfigurationModel):
|
||||
return False
|
||||
|
||||
if self._position != other.position:
|
||||
return False
|
||||
# Empty materials should be ignored for comparison
|
||||
if self.activeMaterial is not None and other.activeMaterial is not None:
|
||||
if self.activeMaterial.guid != other.activeMaterial.guid:
|
||||
if self.activeMaterial.guid != "" and other.activeMaterial.guid != "":
|
||||
return False
|
||||
else:
|
||||
# At this point there is no material, so it doesn't matter what the hotend is.
|
||||
return True
|
||||
|
||||
if self.hotendID != other.hotendID:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
# Calculating a hash function using the position of the extruder, the material GUID and the hotend id to check if is
|
||||
# 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)
|
@ -1,14 +1,15 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
||||
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
|
||||
|
||||
from .ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||
from .MaterialOutputModel import MaterialOutputModel
|
||||
from .PrinterOutputModel import PrinterOutputModel
|
||||
|
||||
|
||||
class ExtruderOutputModel(QObject):
|
36
cura/PrinterOutput/Models/MaterialOutputModel.py
Normal file
36
cura/PrinterOutput/Models/MaterialOutputModel.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, QObject
|
||||
|
||||
|
||||
class MaterialOutputModel(QObject):
|
||||
def __init__(self, guid: Optional[str], type: str, color: str, brand: str, name: str, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._guid = guid
|
||||
self._type = type
|
||||
self._color = color
|
||||
self._brand = brand
|
||||
self._name = name
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def guid(self) -> str:
|
||||
return self._guid if self._guid else ""
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def type(self) -> str:
|
||||
return self._type
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def brand(self) -> str:
|
||||
return self._brand
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def color(self) -> str:
|
||||
return self._color
|
||||
|
||||
@pyqtProperty(str, constant = True)
|
||||
def name(self) -> str:
|
||||
return self._name
|
171
cura/PrinterOutput/Models/PrintJobOutputModel.py
Normal file
171
cura/PrinterOutput/Models/PrintJobOutputModel.py
Normal file
@ -0,0 +1,171 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional, TYPE_CHECKING, List
|
||||
|
||||
from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot, QUrl
|
||||
from PyQt5.QtGui import QImage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
|
||||
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
||||
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||
|
||||
|
||||
class PrintJobOutputModel(QObject):
|
||||
stateChanged = pyqtSignal()
|
||||
timeTotalChanged = pyqtSignal()
|
||||
timeElapsedChanged = pyqtSignal()
|
||||
nameChanged = pyqtSignal()
|
||||
keyChanged = pyqtSignal()
|
||||
assignedPrinterChanged = pyqtSignal()
|
||||
ownerChanged = pyqtSignal()
|
||||
configurationChanged = pyqtSignal()
|
||||
previewImageChanged = pyqtSignal()
|
||||
compatibleMachineFamiliesChanged = pyqtSignal()
|
||||
|
||||
def __init__(self, output_controller: "PrinterOutputController", key: str = "", name: str = "", parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._output_controller = output_controller
|
||||
self._state = ""
|
||||
self._time_total = 0
|
||||
self._time_elapsed = 0
|
||||
self._name = name # Human readable name
|
||||
self._key = key # Unique identifier
|
||||
self._assigned_printer = None # type: Optional[PrinterOutputModel]
|
||||
self._owner = "" # Who started/owns the print job?
|
||||
|
||||
self._configuration = None # type: Optional[PrinterConfigurationModel]
|
||||
self._compatible_machine_families = [] # type: List[str]
|
||||
self._preview_image_id = 0
|
||||
|
||||
self._preview_image = None # type: Optional[QImage]
|
||||
|
||||
@pyqtProperty("QStringList", notify=compatibleMachineFamiliesChanged)
|
||||
def compatibleMachineFamilies(self):
|
||||
# Hack; Some versions of cluster will return a family more than once...
|
||||
return list(set(self._compatible_machine_families))
|
||||
|
||||
def setCompatibleMachineFamilies(self, compatible_machine_families: List[str]) -> None:
|
||||
if self._compatible_machine_families != compatible_machine_families:
|
||||
self._compatible_machine_families = compatible_machine_families
|
||||
self.compatibleMachineFamiliesChanged.emit()
|
||||
|
||||
@pyqtProperty(QUrl, notify=previewImageChanged)
|
||||
def previewImageUrl(self):
|
||||
self._preview_image_id += 1
|
||||
# There is an image provider that is called "print_job_preview". In order to ensure that the image qml object, that
|
||||
# requires a QUrl to function, updates correctly we add an increasing number. This causes to see the QUrl
|
||||
# as new (instead of relying on cached version and thus forces an update.
|
||||
temp = "image://print_job_preview/" + str(self._preview_image_id) + "/" + self._key
|
||||
return QUrl(temp, QUrl.TolerantMode)
|
||||
|
||||
def getPreviewImage(self) -> Optional[QImage]:
|
||||
return self._preview_image
|
||||
|
||||
def updatePreviewImage(self, preview_image: Optional[QImage]) -> None:
|
||||
if self._preview_image != preview_image:
|
||||
self._preview_image = preview_image
|
||||
self.previewImageChanged.emit()
|
||||
|
||||
@pyqtProperty(QObject, notify=configurationChanged)
|
||||
def configuration(self) -> Optional["PrinterConfigurationModel"]:
|
||||
return self._configuration
|
||||
|
||||
def updateConfiguration(self, configuration: Optional["PrinterConfigurationModel"]) -> None:
|
||||
if self._configuration != configuration:
|
||||
self._configuration = configuration
|
||||
self.configurationChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=ownerChanged)
|
||||
def owner(self):
|
||||
return self._owner
|
||||
|
||||
def updateOwner(self, owner):
|
||||
if self._owner != owner:
|
||||
self._owner = owner
|
||||
self.ownerChanged.emit()
|
||||
|
||||
@pyqtProperty(QObject, notify=assignedPrinterChanged)
|
||||
def assignedPrinter(self):
|
||||
return self._assigned_printer
|
||||
|
||||
def updateAssignedPrinter(self, assigned_printer: Optional["PrinterOutputModel"]) -> None:
|
||||
if self._assigned_printer != assigned_printer:
|
||||
old_printer = self._assigned_printer
|
||||
self._assigned_printer = assigned_printer
|
||||
if old_printer is not None:
|
||||
# If the previously assigned printer is set, this job is moved away from it.
|
||||
old_printer.updateActivePrintJob(None)
|
||||
self.assignedPrinterChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify=keyChanged)
|
||||
def key(self):
|
||||
return self._key
|
||||
|
||||
def updateKey(self, key: str):
|
||||
if self._key != key:
|
||||
self._key = key
|
||||
self.keyChanged.emit()
|
||||
|
||||
@pyqtProperty(str, notify = nameChanged)
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def updateName(self, name: str):
|
||||
if self._name != name:
|
||||
self._name = name
|
||||
self.nameChanged.emit()
|
||||
|
||||
@pyqtProperty(int, notify = timeTotalChanged)
|
||||
def timeTotal(self) -> int:
|
||||
return self._time_total
|
||||
|
||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||
def timeElapsed(self) -> int:
|
||||
return self._time_elapsed
|
||||
|
||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||
def timeRemaining(self) -> int:
|
||||
# Never get a negative time remaining
|
||||
return max(self.timeTotal - self.timeElapsed, 0)
|
||||
|
||||
@pyqtProperty(float, notify = timeElapsedChanged)
|
||||
def progress(self) -> float:
|
||||
result = float(self.timeElapsed) / max(self.timeTotal, 1.0) # Prevent a division by zero exception.
|
||||
return min(result, 1.0) # Never get a progress past 1.0
|
||||
|
||||
@pyqtProperty(str, notify=stateChanged)
|
||||
def state(self) -> str:
|
||||
return self._state
|
||||
|
||||
@pyqtProperty(bool, notify=stateChanged)
|
||||
def isActive(self) -> bool:
|
||||
inactive_states = [
|
||||
"pausing",
|
||||
"paused",
|
||||
"resuming",
|
||||
"wait_cleanup"
|
||||
]
|
||||
if self.state in inactive_states and self.timeRemaining > 0:
|
||||
return False
|
||||
return True
|
||||
|
||||
def updateTimeTotal(self, new_time_total):
|
||||
if self._time_total != new_time_total:
|
||||
self._time_total = new_time_total
|
||||
self.timeTotalChanged.emit()
|
||||
|
||||
def updateTimeElapsed(self, new_time_elapsed):
|
||||
if self._time_elapsed != new_time_elapsed:
|
||||
self._time_elapsed = new_time_elapsed
|
||||
self.timeElapsedChanged.emit()
|
||||
|
||||
def updateState(self, new_state):
|
||||
if self._state != new_state:
|
||||
self._state = new_state
|
||||
self.stateChanged.emit()
|
||||
|
||||
@pyqtSlot(str)
|
||||
def setState(self, state):
|
||||
self._output_controller.setJobState(self, state)
|
@ -6,10 +6,10 @@ from typing import List
|
||||
|
||||
MYPY = False
|
||||
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()
|
||||
|
||||
@ -58,6 +58,14 @@ class ConfigurationModel(QObject):
|
||||
return False
|
||||
return self._printer_type != ""
|
||||
|
||||
def hasAnyMaterialLoaded(self) -> bool:
|
||||
if not self.isValid():
|
||||
return False
|
||||
for configuration in self._extruder_configurations:
|
||||
if configuration.activeMaterial and configuration.activeMaterial.type != "empty":
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
message_chunks = []
|
||||
message_chunks.append("Printer type: " + self._printer_type)
|
||||
@ -71,7 +79,23 @@ class ConfigurationModel(QObject):
|
||||
return "\n".join(message_chunks)
|
||||
|
||||
def __eq__(self, other):
|
||||
return hash(self) == hash(other)
|
||||
if not isinstance(other, PrinterConfigurationModel):
|
||||
return False
|
||||
|
||||
if self.printerType != other.printerType:
|
||||
return False
|
||||
|
||||
if self.buildplateConfiguration != other.buildplateConfiguration:
|
||||
return False
|
||||
|
||||
if len(self.extruderConfigurations) != len(other.extruderConfigurations):
|
||||
return False
|
||||
|
||||
for self_extruder, other_extruder in zip(sorted(self._extruder_configurations, key=lambda x: x.position), sorted(other.extruderConfigurations, key=lambda x: x.position)):
|
||||
if self_extruder != other_extruder:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
## The hash function is used to compare and create unique sets. The configuration is unique if the configuration
|
||||
# of the extruders is unique (the order of the extruders matters), and the type and buildplate is the same.
|
||||
@ -86,4 +110,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)
|
340
cura/PrinterOutput/Models/PrinterOutputModel.py
Normal file
340
cura/PrinterOutput/Models/PrinterOutputModel.py
Normal file
@ -0,0 +1,340 @@
|
||||
# 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, TYPE_CHECKING
|
||||
from UM.Math.Vector import Vector
|
||||
from cura.PrinterOutput.Peripheral import Peripheral
|
||||
from cura.PrinterOutput.Models.PrinterConfigurationModel import PrinterConfigurationModel
|
||||
from cura.PrinterOutput.Models.ExtruderOutputModel import ExtruderOutputModel
|
||||
from UM.Logger import Logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
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._active_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._peripherals = [] # type: List[Peripheral]
|
||||
|
||||
self._active_printer_configuration.extruderConfigurations = [extruder.extruderConfiguration for extruder in
|
||||
self._extruders]
|
||||
self._active_printer_configuration.configurationChanged.connect(self.configurationChanged)
|
||||
self._available_printer_configurations = [] # type: List[PrinterConfigurationModel]
|
||||
|
||||
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._active_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._active_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 active configuration (material, variant and buildplate) of the current printer
|
||||
@pyqtProperty(QObject, notify = configurationChanged)
|
||||
def printerConfiguration(self) -> Optional[PrinterConfigurationModel]:
|
||||
if self._active_printer_configuration.isValid():
|
||||
return self._active_printer_configuration
|
||||
return None
|
||||
|
||||
peripheralsChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(str, notify = peripheralsChanged)
|
||||
def peripherals(self) -> str:
|
||||
return ", ".join([peripheral.name for peripheral in self._peripherals])
|
||||
|
||||
def addPeripheral(self, peripheral: Peripheral) -> None:
|
||||
self._peripherals.append(peripheral)
|
||||
self.peripheralsChanged.emit()
|
||||
|
||||
def removePeripheral(self, peripheral: Peripheral) -> None:
|
||||
self._peripherals.remove(peripheral)
|
||||
self.peripheralsChanged.emit()
|
||||
|
||||
availableConfigurationsChanged = pyqtSignal()
|
||||
|
||||
# The availableConfigurations are configuration options that a printer can switch to, but doesn't currently have
|
||||
# active (eg; Automatic tool changes, material loaders, etc).
|
||||
@pyqtProperty("QVariantList", notify = availableConfigurationsChanged)
|
||||
def availableConfigurations(self) -> List[PrinterConfigurationModel]:
|
||||
return self._available_printer_configurations
|
||||
|
||||
def addAvailableConfiguration(self, new_configuration: PrinterConfigurationModel) -> None:
|
||||
if new_configuration not in self._available_printer_configurations:
|
||||
self._available_printer_configurations.append(new_configuration)
|
||||
self.availableConfigurationsChanged.emit()
|
||||
|
||||
def removeAvailableConfiguration(self, config_to_remove: PrinterConfigurationModel) -> None:
|
||||
try:
|
||||
self._available_printer_configurations.remove(config_to_remove)
|
||||
except ValueError:
|
||||
Logger.log("w", "Unable to remove configuration that isn't in the list of available configurations")
|
||||
else:
|
||||
self.availableConfigurationsChanged.emit()
|
||||
|
||||
def setAvailableConfigurations(self, new_configurations: List[PrinterConfigurationModel]) -> None:
|
||||
self._available_printer_configurations = new_configurations
|
||||
self.availableConfigurationsChanged.emit()
|
0
cura/PrinterOutput/Models/__init__.py
Normal file
0
cura/PrinterOutput/Models/__init__.py
Normal file
@ -7,7 +7,7 @@ from UM.Scene.SceneNode import SceneNode #For typing.
|
||||
from cura.API import Account
|
||||
from cura.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
|
||||
@ -33,8 +35,6 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType = ConnectionType.NetworkConnection, parent: QObject = None) -> None:
|
||||
super().__init__(device_id = device_id, connection_type = connection_type, parent = parent)
|
||||
self._manager = None # type: Optional[QNetworkAccessManager]
|
||||
self._last_manager_create_time = None # type: Optional[float]
|
||||
self._recreate_network_manager_time = 30
|
||||
self._timeout_time = 10 # After how many seconds of no response should a timeout occur?
|
||||
|
||||
self._last_response_time = None # type: Optional[float]
|
||||
@ -58,8 +58,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
self._gcode = [] # type: List[str]
|
||||
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
|
||||
|
||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||
file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
||||
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||
file_handler: Optional["FileHandler"] = None, filter_by_machine: bool = False, **kwargs) -> None:
|
||||
raise NotImplementedError("requestWrite needs to be implemented")
|
||||
|
||||
def setAuthenticationState(self, authentication_state: AuthState) -> None:
|
||||
@ -131,12 +131,6 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
|
||||
self.setConnectionState(ConnectionState.Closed)
|
||||
|
||||
# We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to
|
||||
# sleep.
|
||||
if time_since_last_response > self._recreate_network_manager_time:
|
||||
if self._last_manager_create_time is None or time() - self._last_manager_create_time > self._recreate_network_manager_time:
|
||||
self._createNetworkManager()
|
||||
assert(self._manager is not None)
|
||||
elif self._connection_state == ConnectionState.Closed:
|
||||
# Go out of timeout.
|
||||
if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here
|
||||
@ -315,16 +309,30 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||
|
||||
self._manager = QNetworkAccessManager()
|
||||
self._manager.finished.connect(self._handleOnFinished)
|
||||
self._last_manager_create_time = time()
|
||||
self._manager.authenticationRequired.connect(self._onAuthenticationRequired)
|
||||
|
||||
if self._properties.get(b"temporary", b"false") != b"true":
|
||||
CuraApplication.getInstance().getMachineManager().checkCorrectGroupName(self.getId(), self.name)
|
||||
self._checkCorrectGroupName(self.getId(), self.name)
|
||||
|
||||
def _registerOnFinishedCallback(self, reply: QNetworkReply, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||
if on_finished is not None:
|
||||
self._onFinishedCallbacks[reply.url().toString() + str(reply.operation())] = on_finished
|
||||
|
||||
## 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.
|
||||
|
16
cura/PrinterOutput/Peripheral.py
Normal file
16
cura/PrinterOutput/Peripheral.py
Normal file
@ -0,0 +1,16 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
|
||||
## Data class that represents a peripheral for a printer.
|
||||
#
|
||||
# Output device plug-ins may specify that the printer has a certain set of
|
||||
# peripherals. This set is then possibly shown in the interface of the monitor
|
||||
# stage.
|
||||
class Peripheral:
|
||||
## Constructs the peripheral.
|
||||
# \param type A unique ID for the type of peripheral.
|
||||
# \param name A human-readable name for the peripheral.
|
||||
def __init__(self, peripheral_type: str, name: str) -> None:
|
||||
self.type = peripheral_type
|
||||
self.name = name
|
@ -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 instead", DeprecationWarning, stacklevel=2)
|
||||
# We moved the the models to one submodule deeper
|
||||
from cura.PrinterOutput.Models.PrintJobOutputModel import PrintJobOutputModel
|
@ -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:
|
||||
|
266
cura/PrinterOutput/PrinterOutputDevice.py
Normal file
266
cura/PrinterOutput/PrinterOutputDevice.py
Normal file
@ -0,0 +1,266 @@
|
||||
# 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, filter_by_machine: bool = False, **kwargs) -> 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:
|
||||
all_configurations = set()
|
||||
for printer in self._printers:
|
||||
if printer.printerConfiguration is not None and printer.printerConfiguration.hasAnyMaterialLoaded():
|
||||
all_configurations.add(printer.printerConfiguration)
|
||||
all_configurations.update(printer.availableConfigurations)
|
||||
new_configurations = sorted(all_configurations, key = lambda config: config.printerType)
|
||||
if new_configurations != self._unique_configurations:
|
||||
self._unique_configurations = new_configurations
|
||||
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)
|
||||
printer.availableConfigurationsChanged.connect(self._updateUniqueConfigurations)
|
||||
|
||||
# At this point there may be non-updated configurations
|
||||
self._updateUniqueConfigurations()
|
||||
|
||||
## Set the device firmware name
|
||||
#
|
||||
# \param name The name of the firmware.
|
||||
def _setFirmwareName(self, name: str) -> None:
|
||||
self._firmware_name = name
|
||||
|
||||
## Get the name of device firmware
|
||||
#
|
||||
# This name can be used to define device type
|
||||
def getFirmwareName(self) -> Optional[str]:
|
||||
return self._firmware_name
|
||||
|
||||
def getFirmwareUpdater(self) -> Optional["FirmwareUpdater"]:
|
||||
return self._firmware_updater
|
||||
|
||||
@pyqtSlot(str)
|
||||
def updateFirmware(self, firmware_file: Union[str, QUrl]) -> None:
|
||||
if not self._firmware_updater:
|
||||
return
|
||||
|
||||
self._firmware_updater.updateFirmware(firmware_file)
|
@ -1,297 +1,4 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# 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 instead", DeprecationWarning, stacklevel=2)
|
||||
# We moved the the models to one submodule deeper
|
||||
from cura.PrinterOutput.Models.PrinterOutputModel import PrinterOutputModel
|
@ -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 instead", DeprecationWarning, stacklevel=2)
|
||||
# We moved the PrinterOutput device to it's own submodule.
|
||||
from cura.PrinterOutput.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
@ -60,13 +60,15 @@ 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)
|
||||
|
||||
per_object_stack = node.callDecoration("getStack")
|
||||
if per_object_stack:
|
||||
per_object_stack.propertyChanged.connect(self._onSettingValueChanged)
|
||||
|
||||
self._onChanged()
|
||||
|
||||
@ -78,7 +80,8 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
def getConvexHull(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
if self._node.callDecoration("isNonPrintingMesh"):
|
||||
return None
|
||||
hull = self._compute2DConvexHull()
|
||||
|
||||
if self._global_stack and self._node is not None and hull is not None:
|
||||
@ -108,7 +111,8 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
def getConvexHullHead(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
if self._node.callDecoration("isNonPrintingMesh"):
|
||||
return None
|
||||
if self._global_stack:
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||
head_with_fans = self._compute2DConvexHeadMin()
|
||||
@ -124,6 +128,9 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
def getConvexHullBoundary(self) -> Optional[Polygon]:
|
||||
if self._node is None:
|
||||
return None
|
||||
|
||||
if self._node.callDecoration("isNonPrintingMesh"):
|
||||
return None
|
||||
|
||||
if self._global_stack:
|
||||
if self._global_stack.getProperty("print_sequence", "value") == "one_at_a_time" and not self.hasGroupAsParent(self._node):
|
||||
@ -400,4 +407,4 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||
## Settings that change the convex hull.
|
||||
#
|
||||
# If these settings change, the convex hull should be recalculated.
|
||||
_influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width"}
|
||||
_influencing_settings = {"xy_offset", "xy_offset_layer_0", "mold_enabled", "mold_width", "anti_overhang_mesh", "infill_mesh", "cutting_mesh"}
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,4 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from copy import deepcopy
|
||||
@ -6,13 +6,14 @@ from typing import cast, Dict, List, Optional
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Math.AxisAlignedBox import AxisAlignedBox
|
||||
from UM.Math.Polygon import Polygon #For typing.
|
||||
from UM.Math.Polygon import Polygon # For typing.
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator #To cast the deepcopy of every decorator back to SceneNodeDecorator.
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator # To cast the deepcopy of every decorator back to SceneNodeDecorator.
|
||||
|
||||
import cura.CuraApplication # To get the build plate.
|
||||
from cura.Settings.ExtruderStack import ExtruderStack # For typing.
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator # For per-object settings.
|
||||
|
||||
import cura.CuraApplication #To get the build plate.
|
||||
from cura.Settings.ExtruderStack import ExtruderStack #For typing.
|
||||
from cura.Settings.SettingOverrideDecorator import SettingOverrideDecorator #For per-object settings.
|
||||
|
||||
## Scene nodes that are models are only seen when selecting the corresponding build plate
|
||||
# Note that many other nodes can just be UM SceneNode objects.
|
||||
@ -20,7 +21,7 @@ class CuraSceneNode(SceneNode):
|
||||
def __init__(self, parent: Optional["SceneNode"] = None, visible: bool = True, name: str = "", no_setting_override: bool = False) -> None:
|
||||
super().__init__(parent = parent, visible = visible, name = name)
|
||||
if not no_setting_override:
|
||||
self.addDecorator(SettingOverrideDecorator()) # now we always have a getActiveExtruderPosition, unless explicitly disabled
|
||||
self.addDecorator(SettingOverrideDecorator()) # Now we always have a getActiveExtruderPosition, unless explicitly disabled
|
||||
self._outside_buildarea = False
|
||||
|
||||
def setOutsideBuildArea(self, new_value: bool) -> None:
|
||||
@ -58,7 +59,7 @@ class CuraSceneNode(SceneNode):
|
||||
if extruder_id is not None:
|
||||
if extruder_id == extruder.getId():
|
||||
return extruder
|
||||
else: # If the id is unknown, then return the extruder in the position 0
|
||||
else: # If the id is unknown, then return the extruder in the position 0
|
||||
try:
|
||||
if extruder.getMetaDataEntry("position", default = "0") == "0": # Check if the position is zero
|
||||
return extruder
|
||||
@ -85,24 +86,14 @@ class CuraSceneNode(SceneNode):
|
||||
1.0
|
||||
]
|
||||
|
||||
## Return if the provided bbox collides with the bbox of this scene node
|
||||
def collidesWithBbox(self, check_bbox: AxisAlignedBox) -> bool:
|
||||
bbox = self.getBoundingBox()
|
||||
if bbox is not None:
|
||||
# Mark the node as outside the build volume if the bounding box test fails.
|
||||
if check_bbox.intersectsBox(bbox) != AxisAlignedBox.IntersectionResult.FullIntersection:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
## Return if any area collides with the convex hull of this scene node
|
||||
def collidesWithArea(self, areas: List[Polygon]) -> bool:
|
||||
def collidesWithAreas(self, areas: List[Polygon]) -> bool:
|
||||
convex_hull = self.callDecoration("getConvexHull")
|
||||
if convex_hull:
|
||||
if not convex_hull.isValid():
|
||||
return False
|
||||
|
||||
# Check for collisions between disallowed areas and the object
|
||||
# Check for collisions between provided areas and the object
|
||||
for area in areas:
|
||||
overlap = convex_hull.intersectsPolygon(area)
|
||||
if overlap is None:
|
||||
@ -112,21 +103,24 @@ 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())
|
||||
self._aabb = self._mesh_data.getExtents(self.getWorldTransformation())
|
||||
else: # If there is no mesh_data, use a boundingbox that encompasses the local (0,0,0)
|
||||
position = self.getWorldPosition()
|
||||
aabb = AxisAlignedBox(minimum = position, maximum = position)
|
||||
self._aabb = AxisAlignedBox(minimum=position, maximum=position)
|
||||
|
||||
for child in self._children:
|
||||
for child in self.getAllChildren():
|
||||
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.getMeshData():
|
||||
# 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":
|
||||
|
@ -1,11 +1,18 @@
|
||||
from UM.Scene.SceneNodeDecorator import SceneNodeDecorator
|
||||
from typing import List
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class GCodeListDecorator(SceneNodeDecorator):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._gcode_list = [] # type: List[str]
|
||||
self._filename = None # type: Optional[str]
|
||||
|
||||
def getGcodeFileName(self) -> Optional[str]:
|
||||
return self._filename
|
||||
|
||||
def setGcodeFileName(self, filename: str) -> None:
|
||||
self._filename = filename
|
||||
|
||||
def getGCodeList(self) -> List[str]:
|
||||
return self._gcode_list
|
||||
|
@ -4,7 +4,7 @@
|
||||
import os
|
||||
import urllib.parse
|
||||
import uuid
|
||||
from typing import Dict, Union, Any, TYPE_CHECKING, List
|
||||
from typing import Dict, Union, Any, TYPE_CHECKING, List, cast
|
||||
|
||||
from PyQt5.QtCore import QObject, QUrl
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
@ -438,7 +438,7 @@ class ContainerManager(QObject):
|
||||
if not path:
|
||||
return
|
||||
|
||||
container_list = [n.getContainer() for n in quality_changes_group.getAllNodes() if n.getContainer() is not None]
|
||||
container_list = [cast(InstanceContainer, n.getContainer()) for n in quality_changes_group.getAllNodes() if n.getContainer() is not None]
|
||||
self._container_registry.exportQualityProfile(container_list, path, file_type)
|
||||
|
||||
__instance = None # type: ContainerManager
|
||||
|
@ -5,7 +5,7 @@ import os
|
||||
import re
|
||||
import configparser
|
||||
|
||||
from typing import Any, cast, Dict, Optional
|
||||
from typing import Any, cast, Dict, Optional, List, Union
|
||||
from PyQt5.QtWidgets import QMessageBox
|
||||
|
||||
from UM.Decorators import override
|
||||
@ -20,13 +20,14 @@ from UM.Logger import Logger
|
||||
from UM.Message import Message
|
||||
from UM.Platform import Platform
|
||||
from UM.PluginRegistry import PluginRegistry # For getting the possible profile writers to write with.
|
||||
from UM.Util import parseBool
|
||||
from UM.Resources import Resources
|
||||
from cura.ReaderWriters.ProfileWriter import ProfileWriter
|
||||
|
||||
from . import ExtruderStack
|
||||
from . import GlobalStack
|
||||
|
||||
import cura.CuraApplication
|
||||
from cura.Settings.cura_empty_instance_containers import empty_quality_container
|
||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||
from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader
|
||||
|
||||
@ -50,10 +51,10 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
# This will also try to convert a ContainerStack to either Extruder or
|
||||
# Global stack based on metadata information.
|
||||
@override(ContainerRegistry)
|
||||
def addContainer(self, container):
|
||||
def addContainer(self, container: ContainerInterface) -> None:
|
||||
# Note: Intentional check with type() because we want to ignore subclasses
|
||||
if type(container) == ContainerStack:
|
||||
container = self._convertContainerStack(container)
|
||||
container = self._convertContainerStack(cast(ContainerStack, container))
|
||||
|
||||
if isinstance(container, InstanceContainer) and type(container) != type(self.getEmptyInstanceContainer()):
|
||||
# Check against setting version of the definition.
|
||||
@ -61,7 +62,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
actual_setting_version = int(container.getMetaDataEntry("setting_version", default = 0))
|
||||
if required_setting_version != actual_setting_version:
|
||||
Logger.log("w", "Instance container {container_id} is outdated. Its setting version is {actual_setting_version} but it should be {required_setting_version}.".format(container_id = container.getId(), actual_setting_version = actual_setting_version, required_setting_version = required_setting_version))
|
||||
return #Don't add.
|
||||
return # Don't add.
|
||||
|
||||
super().addContainer(container)
|
||||
|
||||
@ -71,9 +72,9 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
# \param new_name \type{string} Base name, which may not be unique
|
||||
# \param fallback_name \type{string} Name to use when (stripped) new_name is empty
|
||||
# \return \type{string} Name that is unique for the specified type and name/id
|
||||
def createUniqueName(self, container_type, current_name, new_name, fallback_name):
|
||||
def createUniqueName(self, container_type: str, current_name: str, new_name: str, fallback_name: str) -> str:
|
||||
new_name = new_name.strip()
|
||||
num_check = re.compile("(.*?)\s*#\d+$").match(new_name)
|
||||
num_check = re.compile(r"(.*?)\s*#\d+$").match(new_name)
|
||||
if num_check:
|
||||
new_name = num_check.group(1)
|
||||
if new_name == "":
|
||||
@ -92,7 +93,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
# Both the id and the name are checked, because they may not be the same and it is better if they are both unique
|
||||
# \param container_type \type{string} Type of the container (machine, quality, ...)
|
||||
# \param container_name \type{string} Name to check
|
||||
def _containerExists(self, container_type, container_name):
|
||||
def _containerExists(self, container_type: str, container_name: str):
|
||||
container_class = ContainerStack if container_type == "machine" else InstanceContainer
|
||||
|
||||
return self.findContainersMetadata(container_type = container_class, id = container_name, type = container_type, ignore_case = True) or \
|
||||
@ -100,16 +101,17 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
|
||||
## Exports an profile to a file
|
||||
#
|
||||
# \param instance_ids \type{list} the IDs of the profiles to export.
|
||||
# \param container_list \type{list} the containers to export
|
||||
# \param file_name \type{str} the full path and filename to export to.
|
||||
# \param file_type \type{str} the file type with the format "<description> (*.<extension>)"
|
||||
def exportQualityProfile(self, container_list, file_name, file_type):
|
||||
# \return True if the export succeeded, false otherwise.
|
||||
def exportQualityProfile(self, container_list: List[InstanceContainer], file_name: str, file_type: str) -> bool:
|
||||
# Parse the fileType to deduce what plugin can save the file format.
|
||||
# fileType has the format "<description> (*.<extension>)"
|
||||
split = file_type.rfind(" (*.") # Find where the description ends and the extension starts.
|
||||
if split < 0: # Not found. Invalid format.
|
||||
Logger.log("e", "Invalid file format identifier %s", file_type)
|
||||
return
|
||||
return False
|
||||
description = file_type[:split]
|
||||
extension = file_type[split + 4:-1] # Leave out the " (*." and ")".
|
||||
if not file_name.endswith("." + extension): # Auto-fill the extension if the user did not provide any.
|
||||
@ -121,10 +123,12 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
result = QMessageBox.question(None, catalog.i18nc("@title:window", "File Already Exists"),
|
||||
catalog.i18nc("@label Don't translate the XML tag <filename>!", "The file <filename>{0}</filename> already exists. Are you sure you want to overwrite it?").format(file_name))
|
||||
if result == QMessageBox.No:
|
||||
return
|
||||
return False
|
||||
|
||||
profile_writer = self._findProfileWriter(extension, description)
|
||||
try:
|
||||
if profile_writer is None:
|
||||
raise Exception("Unable to find a profile writer")
|
||||
success = profile_writer.write(file_name, container_list)
|
||||
except Exception as e:
|
||||
Logger.log("e", "Failed to export profile to %s: %s", file_name, str(e))
|
||||
@ -132,23 +136,24 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
lifetime = 0,
|
||||
title = catalog.i18nc("@info:title", "Error"))
|
||||
m.show()
|
||||
return
|
||||
return False
|
||||
if not success:
|
||||
Logger.log("w", "Failed to export profile to %s: Writer plugin reported failure.", file_name)
|
||||
m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Failed to export profile to <filename>{0}</filename>: Writer plugin reported failure.", file_name),
|
||||
lifetime = 0,
|
||||
title = catalog.i18nc("@info:title", "Error"))
|
||||
m.show()
|
||||
return
|
||||
return False
|
||||
m = Message(catalog.i18nc("@info:status Don't translate the XML tag <filename>!", "Exported profile to <filename>{0}</filename>", file_name),
|
||||
title = catalog.i18nc("@info:title", "Export succeeded"))
|
||||
m.show()
|
||||
return True
|
||||
|
||||
## Gets the plugin object matching the criteria
|
||||
# \param extension
|
||||
# \param description
|
||||
# \return The plugin object matching the given extension and description.
|
||||
def _findProfileWriter(self, extension, description):
|
||||
def _findProfileWriter(self, extension: str, description: str) -> Optional[ProfileWriter]:
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
for plugin_id, meta_data in self._getIOPlugins("profile_writer"):
|
||||
for supported_type in meta_data["profile_writer"]: # All file types this plugin can supposedly write.
|
||||
@ -156,7 +161,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
if supported_extension == extension: # This plugin supports a file type with the same extension.
|
||||
supported_description = supported_type.get("description", None)
|
||||
if supported_description == description: # The description is also identical. Assume it's the same file type.
|
||||
return plugin_registry.getPluginObject(plugin_id)
|
||||
return cast(ProfileWriter, plugin_registry.getPluginObject(plugin_id))
|
||||
return None
|
||||
|
||||
## Imports a profile from a file
|
||||
@ -169,9 +174,6 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
if not file_name:
|
||||
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>: {1}", file_name, "Invalid path")}
|
||||
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
extension = file_name.split(".")[-1]
|
||||
|
||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||
if not global_stack:
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Can't import profile from <filename>{0}</filename> before a printer is added.", file_name)}
|
||||
@ -180,6 +182,9 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
for position in sorted(global_stack.extruders):
|
||||
machine_extruders.append(global_stack.extruders[position])
|
||||
|
||||
plugin_registry = PluginRegistry.getInstance()
|
||||
extension = file_name.split(".")[-1]
|
||||
|
||||
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
||||
if meta_data["profile_reader"][0]["extension"] != extension:
|
||||
continue
|
||||
@ -236,12 +241,11 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
|
||||
# And check if the profile_definition matches either one (showing error if not):
|
||||
if profile_definition != expected_machine_definition:
|
||||
Logger.log("e", "Profile [%s] is for machine [%s] but the current active machine is [%s]. Will not import the profile", file_name, profile_definition, expected_machine_definition)
|
||||
return { "status": "error",
|
||||
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "The machine defined in profile <filename>{0}</filename> ({1}) doesn't match with your current machine ({2}), could not import it.", file_name, profile_definition, expected_machine_definition)}
|
||||
Logger.log("d", "Profile {file_name} is for machine {profile_definition}, but the current active machine is {expected_machine_definition}. Changing profile's definition.".format(file_name = file_name, profile_definition = profile_definition, expected_machine_definition = expected_machine_definition))
|
||||
global_profile.setMetaDataEntry("definition", expected_machine_definition)
|
||||
for extruder_profile in extruder_profiles:
|
||||
extruder_profile.setMetaDataEntry("definition", expected_machine_definition)
|
||||
|
||||
# Fix the global quality profile's definition field in case it's not correct
|
||||
global_profile.setMetaDataEntry("definition", expected_machine_definition)
|
||||
quality_name = global_profile.getName()
|
||||
quality_type = global_profile.getMetaDataEntry("quality_type")
|
||||
|
||||
@ -281,7 +285,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
profile.addInstance(new_instance)
|
||||
profile.setDirty(True)
|
||||
|
||||
global_profile.removeInstance(qc_setting_key, postpone_emit=True)
|
||||
global_profile.removeInstance(qc_setting_key, postpone_emit = True)
|
||||
extruder_profiles.append(profile)
|
||||
|
||||
for profile in extruder_profiles:
|
||||
@ -309,9 +313,9 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
result = self._configureProfile(profile, profile_id, new_name, expected_machine_definition)
|
||||
if result is not None:
|
||||
return {"status": "error", "message": catalog.i18nc(
|
||||
"@info:status Don't translate the XML tags <filename> or <message>!",
|
||||
"@info:status Don't translate the XML tag <filename>!",
|
||||
"Failed to import profile from <filename>{0}</filename>:",
|
||||
file_name) + " <message>" + result + "</message>"}
|
||||
file_name) + " " + result}
|
||||
|
||||
return {"status": "ok", "message": catalog.i18nc("@info:status", "Successfully imported profile {0}", profile_or_list[0].getName())}
|
||||
|
||||
@ -322,7 +326,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
return {"status": "error", "message": catalog.i18nc("@info:status", "Profile {0} has an unknown file type or is corrupted.", file_name)}
|
||||
|
||||
@override(ContainerRegistry)
|
||||
def load(self):
|
||||
def load(self) -> None:
|
||||
super().load()
|
||||
self._registerSingleExtrusionMachinesExtruderStacks()
|
||||
self._connectUpgradedExtruderStacksToMachines()
|
||||
@ -383,13 +387,33 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
# successfully imported but then fail to show up.
|
||||
quality_manager = cura.CuraApplication.CuraApplication.getInstance()._quality_manager
|
||||
quality_group_dict = quality_manager.getQualityGroupsForMachineDefinition(global_stack)
|
||||
if quality_type not in quality_group_dict:
|
||||
# "not_supported" profiles can be imported.
|
||||
if quality_type != empty_quality_container.getMetaDataEntry("quality_type") and quality_type not in quality_group_dict:
|
||||
return catalog.i18nc("@info:status", "Could not find a quality type {0} for the current configuration.", quality_type)
|
||||
|
||||
ContainerRegistry.getInstance().addContainer(profile)
|
||||
|
||||
return None
|
||||
|
||||
@override(ContainerRegistry)
|
||||
def saveDirtyContainers(self) -> None:
|
||||
# Lock file for "more" atomically loading and saving to/from config dir.
|
||||
with self.lockFile():
|
||||
# Save base files first
|
||||
for instance in self.findDirtyContainers(container_type=InstanceContainer):
|
||||
if instance.getMetaDataEntry("removed"):
|
||||
continue
|
||||
if instance.getId() == instance.getMetaData().get("base_file"):
|
||||
self.saveContainer(instance)
|
||||
|
||||
for instance in self.findDirtyContainers(container_type=InstanceContainer):
|
||||
if instance.getMetaDataEntry("removed"):
|
||||
continue
|
||||
self.saveContainer(instance)
|
||||
|
||||
for stack in self.findContainerStacks():
|
||||
self.saveContainer(stack)
|
||||
|
||||
## Gets a list of profile writer plugins
|
||||
# \return List of tuples of (plugin_id, meta_data).
|
||||
def _getIOPlugins(self, io_type):
|
||||
@ -404,7 +428,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
return result
|
||||
|
||||
## Convert an "old-style" pure ContainerStack to either an Extruder or Global stack.
|
||||
def _convertContainerStack(self, container):
|
||||
def _convertContainerStack(self, container: ContainerStack) -> Union[ExtruderStack.ExtruderStack, GlobalStack.GlobalStack]:
|
||||
assert type(container) == ContainerStack
|
||||
|
||||
container_type = container.getMetaDataEntry("type")
|
||||
@ -428,14 +452,14 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
|
||||
return new_stack
|
||||
|
||||
def _registerSingleExtrusionMachinesExtruderStacks(self):
|
||||
def _registerSingleExtrusionMachinesExtruderStacks(self) -> None:
|
||||
machines = self.findContainerStacks(type = "machine", machine_extruder_trains = {"0": "fdmextruder"})
|
||||
for machine in machines:
|
||||
extruder_stacks = self.findContainerStacks(type = "extruder_train", machine = machine.getId())
|
||||
if not extruder_stacks:
|
||||
self.addExtruderStackForSingleExtrusionMachine(machine, "fdmextruder")
|
||||
|
||||
def _onContainerAdded(self, container):
|
||||
def _onContainerAdded(self, container: ContainerInterface) -> None:
|
||||
# We don't have all the machines loaded in the beginning, so in order to add the missing extruder stack
|
||||
# for single extrusion machines, we subscribe to the containerAdded signal, and whenever a global stack
|
||||
# is added, we check to see if an extruder stack needs to be added.
|
||||
@ -669,7 +693,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
|
||||
return extruder_stack
|
||||
|
||||
def _findQualityChangesContainerInCuraFolder(self, name):
|
||||
def _findQualityChangesContainerInCuraFolder(self, name: str) -> Optional[InstanceContainer]:
|
||||
quality_changes_dir = Resources.getPath(cura.CuraApplication.CuraApplication.ResourceTypes.QualityChangesInstanceContainer)
|
||||
|
||||
instance_container = None
|
||||
@ -682,7 +706,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
parser = configparser.ConfigParser(interpolation = None)
|
||||
try:
|
||||
parser.read([file_path])
|
||||
except:
|
||||
except Exception:
|
||||
# Skip, it is not a valid stack file
|
||||
continue
|
||||
|
||||
@ -714,7 +738,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||
# due to problems with loading order, some stacks may not have the proper next stack
|
||||
# set after upgrading, because the proper global stack was not yet loaded. This method
|
||||
# makes sure those extruders also get the right stack set.
|
||||
def _connectUpgradedExtruderStacksToMachines(self):
|
||||
def _connectUpgradedExtruderStacksToMachines(self) -> None:
|
||||
extruder_stacks = self.findContainers(container_type = ExtruderStack.ExtruderStack)
|
||||
for extruder_stack in extruder_stacks:
|
||||
if extruder_stack.getNextStack():
|
||||
|
@ -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)
|
||||
|
@ -129,8 +129,9 @@ class CuraStackBuilder:
|
||||
extruder_definition = registry.findDefinitionContainers(id = extruder_definition_id)[0]
|
||||
except IndexError as e:
|
||||
# It still needs to break, but we want to know what extruder ID made it break.
|
||||
Logger.log("e", "Unable to find extruder with the id %s", extruder_definition_id)
|
||||
raise e
|
||||
msg = "Unable to find extruder definition with the id [%s]" % extruder_definition_id
|
||||
Logger.logException("e", msg)
|
||||
raise IndexError(msg)
|
||||
|
||||
# get material container for extruders
|
||||
material_container = application.empty_material_container
|
||||
|
@ -12,7 +12,7 @@ from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.Scene.Iterator.BreadthFirstIterator import BreadthFirstIterator
|
||||
from UM.Settings.ContainerRegistry import ContainerRegistry # Finding containers by ID.
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Decorators import deprecated
|
||||
|
||||
from typing import Any, cast, Dict, List, Optional, TYPE_CHECKING, Union
|
||||
|
||||
@ -95,6 +95,7 @@ class ExtruderManager(QObject):
|
||||
#
|
||||
# \param index The index of the extruder whose name to get.
|
||||
@pyqtSlot(int, result = str)
|
||||
@deprecated("Use Cura.MachineManager.activeMachine.extruders[index].name instead", "4.3")
|
||||
def getExtruderName(self, index: int) -> str:
|
||||
try:
|
||||
return self.getActiveExtruderStacks()[index].getName()
|
||||
@ -114,7 +115,7 @@ class ExtruderManager(QObject):
|
||||
selected_nodes = [] # type: List["SceneNode"]
|
||||
for node in Selection.getAllSelectedObjects():
|
||||
if node.callDecoration("isGroup"):
|
||||
for grouped_node in BreadthFirstIterator(node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||
for grouped_node in BreadthFirstIterator(node):
|
||||
if grouped_node.callDecoration("isGroup"):
|
||||
continue
|
||||
|
||||
@ -131,7 +132,7 @@ class ExtruderManager(QObject):
|
||||
elif current_extruder_trains:
|
||||
object_extruders.add(current_extruder_trains[0].getId())
|
||||
|
||||
self._selected_object_extruders = list(object_extruders) # type: List[Union[str, "ExtruderStack"]]
|
||||
self._selected_object_extruders = list(object_extruders)
|
||||
|
||||
return self._selected_object_extruders
|
||||
|
||||
@ -140,7 +141,7 @@ class ExtruderManager(QObject):
|
||||
# This will trigger a recalculation of the extruders used for the
|
||||
# selection.
|
||||
def resetSelectedObjectExtruders(self) -> None:
|
||||
self._selected_object_extruders = [] # type: List[Union[str, "ExtruderStack"]]
|
||||
self._selected_object_extruders = []
|
||||
self.selectedObjectExtrudersChanged.emit()
|
||||
|
||||
@pyqtSlot(result = QObject)
|
||||
@ -180,7 +181,7 @@ class ExtruderManager(QObject):
|
||||
# \param setting_key \type{str} The setting to get the property of.
|
||||
# \param property \type{str} The property to get.
|
||||
# \return \type{List} the list of results
|
||||
def getAllExtruderSettings(self, setting_key: str, prop: str) -> List:
|
||||
def getAllExtruderSettings(self, setting_key: str, prop: str) -> List[Any]:
|
||||
result = []
|
||||
|
||||
for extruder_stack in self.getActiveExtruderStacks():
|
||||
@ -205,7 +206,7 @@ class ExtruderManager(QObject):
|
||||
# list.
|
||||
#
|
||||
# \return A list of extruder stacks.
|
||||
def getUsedExtruderStacks(self) -> List["ContainerStack"]:
|
||||
def getUsedExtruderStacks(self) -> List["ExtruderStack"]:
|
||||
global_stack = self._application.getGlobalContainerStack()
|
||||
container_registry = ContainerRegistry.getInstance()
|
||||
|
||||
@ -224,7 +225,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
|
||||
@ -270,7 +280,8 @@ class ExtruderManager(QObject):
|
||||
extruder_str_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value"))
|
||||
if extruder_str_nr == "-1":
|
||||
extruder_str_nr = self._application.getMachineManager().defaultExtruderPosition
|
||||
used_extruder_stack_ids.add(self.extruderIds[extruder_str_nr])
|
||||
if extruder_str_nr in self.extruderIds:
|
||||
used_extruder_stack_ids.add(self.extruderIds[extruder_str_nr])
|
||||
|
||||
try:
|
||||
return [container_registry.findContainerStacks(id = stack_id)[0] for stack_id in used_extruder_stack_ids]
|
||||
@ -370,7 +381,13 @@ class ExtruderManager(QObject):
|
||||
elif extruder_stack_0.definition.getId() != expected_extruder_definition_0_id:
|
||||
Logger.log("e", "Single extruder printer [{printer}] expected extruder [{expected}], but got [{got}]. I'm making it [{expected}].".format(
|
||||
printer = global_stack.getId(), expected = expected_extruder_definition_0_id, got = extruder_stack_0.definition.getId()))
|
||||
extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0]
|
||||
try:
|
||||
extruder_definition = container_registry.findDefinitionContainers(id = expected_extruder_definition_0_id)[0]
|
||||
except IndexError as e:
|
||||
# It still needs to break, but we want to know what extruder ID made it break.
|
||||
msg = "Unable to find extruder definition with the id [%s]" % expected_extruder_definition_0_id
|
||||
Logger.logException("e", msg)
|
||||
raise IndexError(msg)
|
||||
extruder_stack_0.definition = extruder_definition
|
||||
|
||||
## Get all extruder values for a certain setting.
|
||||
|
@ -1,12 +1,14 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from collections import defaultdict
|
||||
import threading
|
||||
from typing import Any, Dict, Optional, Set, TYPE_CHECKING, List
|
||||
import uuid
|
||||
|
||||
from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
|
||||
|
||||
from UM.Decorators import override
|
||||
from UM.Decorators import deprecated, override
|
||||
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||
from UM.Settings.ContainerStack import ContainerStack
|
||||
from UM.Settings.SettingInstance import InstanceState
|
||||
@ -34,6 +36,12 @@ class GlobalStack(CuraContainerStack):
|
||||
|
||||
self.setMetaDataEntry("type", "machine") # For backward compatibility
|
||||
|
||||
# TL;DR: If Cura is looking for printers that belong to the same group, it should use "group_id".
|
||||
# Each GlobalStack by default belongs to a group which is identified via "group_id". This group_id is used to
|
||||
# figure out which GlobalStacks are in the printer cluster for example without knowing the implementation
|
||||
# details such as the um_network_key or some other identifier that's used by the underlying device plugin.
|
||||
self.setMetaDataEntry("group_id", str(uuid.uuid4())) # Assign a new GlobalStack to a unique group by default
|
||||
|
||||
self._extruders = {} # type: Dict[str, "ExtruderStack"]
|
||||
|
||||
# This property is used to track which settings we are calculating the "resolve" for
|
||||
@ -53,17 +61,26 @@ class GlobalStack(CuraContainerStack):
|
||||
#
|
||||
# \return The extruders registered with this stack.
|
||||
@pyqtProperty("QVariantMap", notify = extrudersChanged)
|
||||
@deprecated("Please use extruderList instead.", "4.4")
|
||||
def extruders(self) -> Dict[str, "ExtruderStack"]:
|
||||
return self._extruders
|
||||
|
||||
@pyqtProperty("QVariantList", notify = extrudersChanged)
|
||||
def extruderList(self) -> List["ExtruderStack"]:
|
||||
result_tuple_list = sorted(list(self.extruders.items()), key=lambda x: int(x[0]))
|
||||
result_tuple_list = sorted(list(self._extruders.items()), key=lambda x: int(x[0]))
|
||||
result_list = [item[1] for item in result_tuple_list]
|
||||
|
||||
machine_extruder_count = self.getProperty("machine_extruder_count", "value")
|
||||
return result_list[:machine_extruder_count]
|
||||
|
||||
@pyqtProperty(int, constant = True)
|
||||
def maxExtruderCount(self):
|
||||
return len(self.getMetaDataEntry("machine_extruder_trains"))
|
||||
|
||||
@pyqtProperty(bool, notify=configuredConnectionTypesChanged)
|
||||
def supportsNetworkConnection(self):
|
||||
return self.getMetaDataEntry("supports_network_connection", False)
|
||||
|
||||
@classmethod
|
||||
def getLoadingPriority(cls) -> int:
|
||||
return 2
|
||||
@ -81,7 +98,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:
|
||||
@ -94,7 +119,7 @@ class GlobalStack(CuraContainerStack):
|
||||
## \sa configuredConnectionTypes
|
||||
def removeConfiguredConnectionType(self, connection_type: int) -> None:
|
||||
configured_connection_types = self.configuredConnectionTypes
|
||||
if connection_type in self.configured_connection_types:
|
||||
if connection_type in configured_connection_types:
|
||||
# Store the values as a string.
|
||||
configured_connection_types.remove(connection_type)
|
||||
self.setMetaDataEntry("connection_type", ",".join([str(c_type) for c_type in configured_connection_types]))
|
||||
@ -200,7 +225,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
|
||||
|
||||
@ -240,14 +265,17 @@ class GlobalStack(CuraContainerStack):
|
||||
def getHeadAndFansCoordinates(self):
|
||||
return self.getProperty("machine_head_with_fans_polygon", "value")
|
||||
|
||||
def getHasMaterials(self) -> bool:
|
||||
@pyqtProperty(int, constant=True)
|
||||
def hasMaterials(self):
|
||||
return parseBool(self.getMetaDataEntry("has_materials", False))
|
||||
|
||||
def getHasVariants(self) -> bool:
|
||||
@pyqtProperty(int, constant=True)
|
||||
def hasVariants(self):
|
||||
return parseBool(self.getMetaDataEntry("has_variants", False))
|
||||
|
||||
def getHasMachineQuality(self) -> bool:
|
||||
return parseBool(self.getMetaDataEntry("has_machine_quality", False))
|
||||
@pyqtProperty(int, constant=True)
|
||||
def hasVariantBuildplates(self) -> bool:
|
||||
return parseBool(self.getMetaDataEntry("has_variant_buildplates", False))
|
||||
|
||||
## Get default firmware file name if one is specified in the firmware
|
||||
@pyqtSlot(result = str)
|
||||
|
@ -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
|
||||
@ -37,11 +38,10 @@ from .CuraStackBuilder import CuraStackBuilder
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.CuraContainerStack import CuraContainerStack
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from cura.Machines.MaterialManager import MaterialManager
|
||||
from cura.Machines.QualityManager import QualityManager
|
||||
from cura.Machines.VariantManager import VariantManager
|
||||
@ -106,7 +106,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 +157,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 +172,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 +205,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 +362,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 +369,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.
|
||||
@ -383,12 +387,21 @@ class MachineManager(QObject):
|
||||
machines = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
||||
for machine in machines:
|
||||
if machine.definition.getId() == definition_id:
|
||||
return machine
|
||||
return cast(GlobalStack, 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:
|
||||
Logger.log("i", "Trying to add a machine with the definition id [%s]", definition_id)
|
||||
if name is None:
|
||||
definitions = CuraContainerRegistry.getInstance().findDefinitionContainers(id = definition_id)
|
||||
if definitions:
|
||||
name = definitions[0].getName()
|
||||
else:
|
||||
name = definition_id
|
||||
|
||||
new_stack = CuraStackBuilder.createMachine(cast(str, name), definition_id)
|
||||
if new_stack:
|
||||
# Instead of setting the global container stack here, we set the active machine and so the signals are emitted
|
||||
self.setActiveMachine(new_stack.getId())
|
||||
@ -451,6 +464,7 @@ class MachineManager(QObject):
|
||||
# \param key \type{str} the name of the key to delete
|
||||
@pyqtSlot(str)
|
||||
def clearUserSettingAllCurrentStacks(self, key: str) -> None:
|
||||
Logger.log("i", "Clearing the setting [%s] from all stacks", key)
|
||||
if not self._global_container_stack:
|
||||
return
|
||||
|
||||
@ -486,18 +500,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()
|
||||
@ -520,6 +537,7 @@ class MachineManager(QObject):
|
||||
return bool(self._printer_output_devices)
|
||||
|
||||
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.configuredConnectionTypes instead", "4.2")
|
||||
def activeMachineHasRemoteConnection(self) -> bool:
|
||||
if self._global_container_stack:
|
||||
has_remote_connection = False
|
||||
@ -531,6 +549,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
|
||||
@ -579,7 +598,7 @@ class MachineManager(QObject):
|
||||
def activeStack(self) -> Optional["ExtruderStack"]:
|
||||
return self._active_container_stack
|
||||
|
||||
@pyqtProperty(str, notify=activeMaterialChanged)
|
||||
@pyqtProperty(str, notify = activeMaterialChanged)
|
||||
def activeMaterialId(self) -> str:
|
||||
if self._active_container_stack:
|
||||
material = self._active_container_stack.material
|
||||
@ -675,11 +694,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 +722,7 @@ class MachineManager(QObject):
|
||||
extruder_stack.userChanges.setProperty(key, "value", new_value)
|
||||
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
@deprecated("use Cura.MachineManager.activeStack.variant.name instead", "4.1")
|
||||
def activeVariantName(self) -> str:
|
||||
if self._active_container_stack:
|
||||
variant = self._active_container_stack.variant
|
||||
@ -717,6 +732,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
@deprecated("use Cura.MachineManager.activeStack.variant.id instead", "4.1")
|
||||
def activeVariantId(self) -> str:
|
||||
if self._active_container_stack:
|
||||
variant = self._active_container_stack.variant
|
||||
@ -726,6 +742,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = activeVariantChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.variant.name instead", "4.1")
|
||||
def activeVariantBuildplateName(self) -> str:
|
||||
if self._global_container_stack:
|
||||
variant = self._global_container_stack.variant
|
||||
@ -735,6 +752,7 @@ class MachineManager(QObject):
|
||||
return ""
|
||||
|
||||
@pyqtProperty(str, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.definition.id instead", "4.1")
|
||||
def activeDefinitionId(self) -> str:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.definition.id
|
||||
@ -770,6 +788,7 @@ class MachineManager(QObject):
|
||||
|
||||
@pyqtSlot(str)
|
||||
def removeMachine(self, machine_id: str) -> None:
|
||||
Logger.log("i", "Attempting to remove a machine with the id [%s]", machine_id)
|
||||
# If the machine that is being removed is the currently active machine, set another machine as the active machine.
|
||||
activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id)
|
||||
|
||||
@ -781,7 +800,6 @@ class MachineManager(QObject):
|
||||
self.setActiveMachine(other_machine_stacks[0]["id"])
|
||||
|
||||
metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0]
|
||||
network_key = metadata.get("um_network_key", None)
|
||||
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
||||
containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
|
||||
for container in containers:
|
||||
@ -789,29 +807,33 @@ class MachineManager(QObject):
|
||||
CuraContainerRegistry.getInstance().removeContainer(machine_id)
|
||||
|
||||
# If the printer that is being removed is a network printer, the hidden printers have to be also removed
|
||||
if network_key:
|
||||
metadata_filter = {"um_network_key": network_key}
|
||||
group_id = metadata.get("group_id", None)
|
||||
if group_id:
|
||||
metadata_filter = {"group_id": group_id}
|
||||
hidden_containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
||||
if hidden_containers:
|
||||
# This reuses the method and remove all printers recursively
|
||||
self.removeMachine(hidden_containers[0].getId())
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.hasMaterials instead", "4.2")
|
||||
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.hasMaterials
|
||||
return False
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.hasVariants instead", "4.2")
|
||||
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.hasVariants
|
||||
return False
|
||||
|
||||
@pyqtProperty(bool, notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.hasVariantBuildplates instead", "4.2")
|
||||
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.hasVariantBuildplates
|
||||
return False
|
||||
|
||||
## The selected buildplate is compatible if it is compatible with all the materials in all the extruders
|
||||
@ -874,17 +896,12 @@ class MachineManager(QObject):
|
||||
result = [] # type: List[str]
|
||||
for setting_instance in container.findInstances():
|
||||
setting_key = setting_instance.definition.key
|
||||
setting_enabled = self._global_container_stack.getProperty(setting_key, "enabled")
|
||||
if not setting_enabled:
|
||||
# A setting is not visible anymore
|
||||
result.append(setting_key)
|
||||
Logger.log("d", "Reset setting [%s] from [%s] because the setting is no longer enabled", setting_key, container)
|
||||
continue
|
||||
|
||||
if not self._global_container_stack.getProperty(setting_key, "type") in ("extruder", "optional_extruder"):
|
||||
continue
|
||||
|
||||
old_value = container.getProperty(setting_key, "value")
|
||||
if isinstance(old_value, SettingFunction):
|
||||
old_value = old_value(self._global_container_stack)
|
||||
if int(old_value) < 0:
|
||||
continue
|
||||
if int(old_value) >= extruder_count or not self._global_container_stack.extruders[str(old_value)].isEnabled:
|
||||
@ -903,9 +920,8 @@ class MachineManager(QObject):
|
||||
# Apply quality changes that are incompatible to user changes, so we do not change the quality changes itself.
|
||||
self._global_container_stack.userChanges.setProperty(setting_key, "value", self._default_extruder_position)
|
||||
if add_user_changes:
|
||||
caution_message = Message(catalog.i18nc(
|
||||
"@info:generic",
|
||||
"Settings have been changed to match the current availability of extruders: [%s]" % ", ".join(add_user_changes)),
|
||||
caution_message = Message(
|
||||
catalog.i18nc("@info:message Followed by a list of settings.", "Settings have been changed to match the current availability of extruders:") + " [{settings_list}]".format(settings_list = ", ".join(add_user_changes)),
|
||||
lifetime = 0,
|
||||
title = catalog.i18nc("@info:title", "Settings updated"))
|
||||
caution_message.show()
|
||||
@ -933,7 +949,7 @@ class MachineManager(QObject):
|
||||
|
||||
# Check to see if any objects are set to print with an extruder that will no longer exist
|
||||
root_node = self._application.getController().getScene().getRoot()
|
||||
for node in DepthFirstIterator(root_node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||
for node in DepthFirstIterator(root_node):
|
||||
if node.getMeshData():
|
||||
extruder_nr = node.callDecoration("getActiveExtruderPosition")
|
||||
|
||||
@ -971,6 +987,11 @@ class MachineManager(QObject):
|
||||
|
||||
@pyqtSlot(int, result = QObject)
|
||||
def getExtruder(self, position: int) -> Optional[ExtruderStack]:
|
||||
return self._getExtruder(position)
|
||||
|
||||
# This is a workaround for the deprecated decorator and the pyqtSlot not playing well together.
|
||||
@deprecated("use Cura.MachineManager.activeMachine.extruders instead", "4.2")
|
||||
def _getExtruder(self, position) -> Optional[ExtruderStack]:
|
||||
if self._global_container_stack:
|
||||
return self._global_container_stack.extruders.get(str(position))
|
||||
return None
|
||||
@ -1057,9 +1078,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 []
|
||||
@ -1086,6 +1104,7 @@ class MachineManager(QObject):
|
||||
container.removeInstance(setting_name)
|
||||
|
||||
@pyqtProperty("QVariantList", notify = globalContainerChanged)
|
||||
@deprecated("use Cura.MachineManager.activeMachine.extruders instead", "4.2")
|
||||
def currentExtruderPositions(self) -> List[str]:
|
||||
if self._global_container_stack is None:
|
||||
return []
|
||||
@ -1095,9 +1114,17 @@ class MachineManager(QObject):
|
||||
def _onRootMaterialChanged(self) -> None:
|
||||
self._current_root_material_id = {}
|
||||
|
||||
changed = False
|
||||
|
||||
if self._global_container_stack:
|
||||
for position in self._global_container_stack.extruders:
|
||||
self._current_root_material_id[position] = self._global_container_stack.extruders[position].material.getMetaDataEntry("base_file")
|
||||
material_id = self._global_container_stack.extruders[position].material.getMetaDataEntry("base_file")
|
||||
if position not in self._current_root_material_id or material_id != self._current_root_material_id[position]:
|
||||
changed = True
|
||||
self._current_root_material_id[position] = material_id
|
||||
|
||||
if changed:
|
||||
self.activeMaterialChanged.emit()
|
||||
|
||||
@pyqtProperty("QVariant", notify = rootMaterialChanged)
|
||||
def currentRootMaterialId(self) -> Dict[str, str]:
|
||||
@ -1250,8 +1277,8 @@ class MachineManager(QObject):
|
||||
if self._global_container_stack is not None:
|
||||
if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)):
|
||||
for position, extruder in self._global_container_stack.extruders.items():
|
||||
if extruder.isEnabled and not extruder.material.getMetaDataEntry("compatible"):
|
||||
return False
|
||||
if not extruder.isEnabled:
|
||||
continue
|
||||
if not extruder.material.getMetaDataEntry("compatible"):
|
||||
return False
|
||||
return True
|
||||
@ -1260,7 +1287,7 @@ class MachineManager(QObject):
|
||||
def _updateQualityWithMaterial(self, *args: Any) -> None:
|
||||
if self._global_container_stack is None:
|
||||
return
|
||||
Logger.log("i", "Updating quality/quality_changes due to material change")
|
||||
Logger.log("d", "Updating quality/quality_changes due to material change")
|
||||
current_quality_type = None
|
||||
if self._current_quality_group:
|
||||
current_quality_type = self._current_quality_group.quality_type
|
||||
@ -1341,33 +1368,37 @@ class MachineManager(QObject):
|
||||
# instance with the same network key.
|
||||
@pyqtSlot(str)
|
||||
def switchPrinterType(self, machine_name: str) -> None:
|
||||
Logger.log("i", "Attempting to switch the printer type to [%s]", machine_name)
|
||||
# Don't switch if the user tries to change to the same type of printer
|
||||
if self._global_container_stack is None or self.activeMachineDefinitionName == machine_name:
|
||||
return
|
||||
# Get the definition id corresponding to this machine name
|
||||
machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()
|
||||
# Try to find a machine with the same network key
|
||||
new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey()})
|
||||
metadata_filter = {"group_id": self._global_container_stack.getMetaDataEntry("group_id"),
|
||||
"um_network_key": self.activeMachineNetworkKey(),
|
||||
}
|
||||
new_machine = self.getMachine(machine_definition_id, metadata_filter = metadata_filter)
|
||||
# If there is no machine, then create a new one and set it to the non-hidden instance
|
||||
if not new_machine:
|
||||
new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id)
|
||||
if not new_machine:
|
||||
return
|
||||
new_machine.setMetaDataEntry("group_id", self._global_container_stack.getMetaDataEntry("group_id"))
|
||||
new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey())
|
||||
new_machine.setMetaDataEntry("group_name", self.activeMachineNetworkGroupName)
|
||||
new_machine.setMetaDataEntry("hidden", False)
|
||||
new_machine.setMetaDataEntry("connection_type", self._global_container_stack.getMetaDataEntry("connection_type"))
|
||||
else:
|
||||
Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey())
|
||||
new_machine.setMetaDataEntry("hidden", False)
|
||||
|
||||
# Set the current printer instance to hidden (the metadata entry must exist)
|
||||
new_machine.setMetaDataEntry("hidden", False)
|
||||
self._global_container_stack.setMetaDataEntry("hidden", True)
|
||||
|
||||
self.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 +1455,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 +1487,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 +1657,7 @@ class MachineManager(QObject):
|
||||
abbr_machine += stripped_word
|
||||
|
||||
return abbr_machine
|
||||
|
||||
# Gets all machines that belong to the given group_id.
|
||||
def getMachinesInGroup(self, group_id: str) -> List["GlobalStack"]:
|
||||
return self._container_registry.findContainerStacks(type = "machine", group_id = group_id)
|
||||
|
@ -12,6 +12,10 @@ from .CuraContainerStack import CuraContainerStack
|
||||
|
||||
|
||||
class PerObjectContainerStack(CuraContainerStack):
|
||||
def isDirty(self):
|
||||
# This stack should never be auto saved, so always return that there is nothing to save.
|
||||
return False
|
||||
|
||||
@override(CuraContainerStack)
|
||||
def getProperty(self, key: str, property_name: str, context: Optional[PropertyEvaluationContext] = None) -> Any:
|
||||
if context is None:
|
||||
@ -34,7 +38,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 +46,7 @@ class PerObjectContainerStack(CuraContainerStack):
|
||||
limit_to_extruder = context.context["original_limit_to_extruder"]
|
||||
|
||||
if limit_to_extruder is not None and limit_to_extruder != "-1" and limit_to_extruder in global_stack.extruders:
|
||||
# set the original limit_to_extruder if this is the first stack that has a non-overriden limit_to_extruder
|
||||
# set the original limit_to_extruder if this is the first stack that has a non-overridden limit_to_extruder
|
||||
if "original_limit_to_extruder" not in context.context:
|
||||
context.context["original_limit_to_extruder"] = limit_to_extruder
|
||||
|
||||
|
@ -73,8 +73,8 @@ class SettingOverrideDecorator(SceneNodeDecorator):
|
||||
|
||||
# use value from the stack because there can be a delay in signal triggering and "_is_non_printing_mesh"
|
||||
# has not been updated yet.
|
||||
deep_copy._is_non_printing_mesh = self.evaluateIsNonPrintingMesh()
|
||||
deep_copy._is_non_thumbnail_visible_mesh = self.evaluateIsNonThumbnailVisibleMesh()
|
||||
deep_copy._is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
|
||||
deep_copy._is_non_thumbnail_visible_mesh = self._evaluateIsNonThumbnailVisibleMesh()
|
||||
|
||||
return deep_copy
|
||||
|
||||
@ -102,21 +102,21 @@ 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()
|
||||
|
||||
self._is_non_printing_mesh = self._evaluateIsNonPrintingMesh()
|
||||
self._is_non_thumbnail_visible_mesh = self._evaluateIsNonThumbnailVisibleMesh()
|
||||
Application.getInstance().getBackend().needsSlicing()
|
||||
Application.getInstance().getBackend().tickle()
|
||||
|
||||
|
@ -1,9 +1,11 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import copy
|
||||
|
||||
from UM.Settings.constant_instance_containers import EMPTY_CONTAINER_ID, empty_container
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
# Empty definition changes
|
||||
@ -28,7 +30,7 @@ empty_material_container.setMetaDataEntry("type", "material")
|
||||
EMPTY_QUALITY_CONTAINER_ID = "empty_quality"
|
||||
empty_quality_container = copy.deepcopy(empty_container)
|
||||
empty_quality_container.setMetaDataEntry("id", EMPTY_QUALITY_CONTAINER_ID)
|
||||
empty_quality_container.setName("Not Supported")
|
||||
empty_quality_container.setName(catalog.i18nc("@info:not supported profile", "Not supported"))
|
||||
empty_quality_container.setMetaDataEntry("quality_type", "not_supported")
|
||||
empty_quality_container.setMetaDataEntry("type", "quality")
|
||||
empty_quality_container.setMetaDataEntry("supported", False)
|
||||
@ -41,6 +43,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 +70,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",
|
||||
]
|
||||
|
@ -48,12 +48,12 @@ class Snapshot:
|
||||
# determine zoom and look at
|
||||
bbox = None
|
||||
for node in DepthFirstIterator(root):
|
||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonThumbnailVisibleMesh"):
|
||||
if bbox is None:
|
||||
bbox = node.getBoundingBox()
|
||||
else:
|
||||
bbox = bbox + node.getBoundingBox()
|
||||
|
||||
if not getattr(node, "_outside_buildarea", False):
|
||||
if node.callDecoration("isSliceable") and node.getMeshData() and node.isVisible() and not node.callDecoration("isNonThumbnailVisibleMesh"):
|
||||
if bbox is None:
|
||||
bbox = node.getBoundingBox()
|
||||
else:
|
||||
bbox = bbox + node.getBoundingBox()
|
||||
# If there is no bounding box, it means that there is no model in the buildplate
|
||||
if bbox is None:
|
||||
return None
|
||||
@ -66,7 +66,7 @@ class Snapshot:
|
||||
looking_from_offset = Vector(-1, 1, 2)
|
||||
if size > 0:
|
||||
# determine the watch distance depending on the size
|
||||
looking_from_offset = looking_from_offset * size * 1.3
|
||||
looking_from_offset = looking_from_offset * size * 1.75
|
||||
camera.setPosition(look_at + looking_from_offset)
|
||||
camera.lookAt(look_at)
|
||||
|
||||
|
@ -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"]
|
||||
|
@ -1,2 +0,0 @@
|
||||
# Copyright (c) 2017 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
31
cura/UI/AddPrinterPagesModel.py
Normal file
31
cura/UI/AddPrinterPagesModel.py
Normal file
@ -0,0 +1,31 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from .WelcomePagesModel import WelcomePagesModel
|
||||
|
||||
|
||||
#
|
||||
# This Qt ListModel is more or less the same the WelcomePagesModel, except that this model is only for adding a printer,
|
||||
# so only the steps for adding a printer is included.
|
||||
#
|
||||
class AddPrinterPagesModel(WelcomePagesModel):
|
||||
|
||||
def initialize(self) -> None:
|
||||
self._pages.append({"id": "add_network_or_local_printer",
|
||||
"page_url": self._getBuiltinWelcomePagePath("AddNetworkOrLocalPrinterContent.qml"),
|
||||
"next_page_id": "machine_actions",
|
||||
"next_page_button_text": self._catalog.i18nc("@action:button", "Add"),
|
||||
"previous_page_button_text": self._catalog.i18nc("@action:button", "Cancel"),
|
||||
})
|
||||
self._pages.append({"id": "add_printer_by_ip",
|
||||
"page_url": self._getBuiltinWelcomePagePath("AddPrinterByIpContent.qml"),
|
||||
"next_page_id": "machine_actions",
|
||||
})
|
||||
self._pages.append({"id": "machine_actions",
|
||||
"page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"),
|
||||
"should_show_function": self.shouldShowMachineActions,
|
||||
})
|
||||
self.setItems(self._pages)
|
||||
|
||||
|
||||
__all__ = ["AddPrinterPagesModel"]
|
@ -12,7 +12,7 @@ from UM.PluginRegistry import PluginRegistry # So MachineAction can be added as
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
from .MachineAction import MachineAction
|
||||
from cura.MachineAction import MachineAction
|
||||
|
||||
|
||||
## Raised when trying to add an unknown machine action as a required action
|
||||
@ -136,7 +136,7 @@ class MachineActionManager(QObject):
|
||||
# action multiple times).
|
||||
# \param definition_id The ID of the definition that you want to get the "on added" actions for.
|
||||
# \returns List of actions.
|
||||
@pyqtSlot(str, result="QVariantList")
|
||||
@pyqtSlot(str, result = "QVariantList")
|
||||
def getFirstStartActions(self, definition_id: str) -> List["MachineAction"]:
|
||||
if definition_id in self._first_start_actions:
|
||||
return self._first_start_actions[definition_id]
|
82
cura/UI/MachineSettingsManager.py
Normal file
82
cura/UI/MachineSettingsManager.py
Normal file
@ -0,0 +1,82 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
#
|
||||
# This manager provides (convenience) functions to the Machine Settings Dialog QML to update certain machine settings.
|
||||
#
|
||||
class MachineSettingsManager(QObject):
|
||||
|
||||
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
self._i18n_catalog = i18nCatalog("cura")
|
||||
|
||||
self._application = application
|
||||
|
||||
# Force rebuilding the build volume by reloading the global container stack. This is a bit of a hack, but it seems
|
||||
# quite enough.
|
||||
@pyqtSlot()
|
||||
def forceUpdate(self) -> None:
|
||||
self._application.getMachineManager().globalContainerChanged.emit()
|
||||
|
||||
# Function for the Machine Settings panel (QML) to update the compatible material diameter after a user has changed
|
||||
# an extruder's compatible material diameter. This ensures that after the modification, changes can be notified
|
||||
# and updated right away.
|
||||
@pyqtSlot(int)
|
||||
def updateMaterialForDiameter(self, extruder_position: int) -> None:
|
||||
# Updates the material container to a material that matches the material diameter set for the printer
|
||||
self._application.getMachineManager().updateMaterialWithVariant(str(extruder_position))
|
||||
|
||||
@pyqtSlot(int)
|
||||
def setMachineExtruderCount(self, extruder_count: int) -> None:
|
||||
# Note: this method was in this class before, but since it's quite generic and other plugins also need it
|
||||
# it was moved to the machine manager instead. Now this method just calls the machine manager.
|
||||
self._application.getMachineManager().setActiveMachineExtruderCount(extruder_count)
|
||||
|
||||
# Function for the Machine Settings panel (QML) to update after the usre changes "Number of Extruders".
|
||||
#
|
||||
# fieldOfView: The Ultimaker 2 family (not 2+) does not have materials in Cura by default, because the material is
|
||||
# to be set on the printer. But when switching to Marlin flavor, the printer firmware can not change/insert material
|
||||
# settings on the fly so they need to be configured in Cura. So when switching between gcode flavors, materials may
|
||||
# need to be enabled/disabled.
|
||||
@pyqtSlot()
|
||||
def updateHasMaterialsMetadata(self):
|
||||
machine_manager = self._application.getMachineManager()
|
||||
material_manager = self._application.getMaterialManager()
|
||||
|
||||
global_stack = machine_manager.activeMachine
|
||||
|
||||
definition = global_stack.definition
|
||||
if definition.getProperty("machine_gcode_flavor", "value") != "UltiGCode" or definition.getMetaDataEntry(
|
||||
"has_materials", False):
|
||||
# In other words: only continue for the UM2 (extended), but not for the UM2+
|
||||
return
|
||||
|
||||
extruder_positions = list(global_stack.extruders.keys())
|
||||
has_materials = global_stack.getProperty("machine_gcode_flavor", "value") != "UltiGCode"
|
||||
|
||||
material_node = None
|
||||
if has_materials:
|
||||
global_stack.setMetaDataEntry("has_materials", True)
|
||||
else:
|
||||
# The metadata entry is stored in an ini, and ini files are parsed as strings only.
|
||||
# Because any non-empty string evaluates to a boolean True, we have to remove the entry to make it False.
|
||||
if "has_materials" in global_stack.getMetaData():
|
||||
global_stack.removeMetaDataEntry("has_materials")
|
||||
|
||||
# set materials
|
||||
for position in extruder_positions:
|
||||
if has_materials:
|
||||
material_node = material_manager.getDefaultMaterial(global_stack, position, None)
|
||||
machine_manager.setMaterial(position, material_node)
|
||||
|
||||
self.forceUpdate()
|
184
cura/UI/ObjectsModel.py
Normal file
184
cura/UI/ObjectsModel.py
Normal file
@ -0,0 +1,184 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from UM.Logger import Logger
|
||||
import re
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
from PyQt5.QtCore import QTimer, Qt
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Scene.Camera import Camera
|
||||
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
|
||||
from UM.Scene.SceneNode import SceneNode
|
||||
from UM.Scene.Selection import Selection
|
||||
from UM.i18n import i18nCatalog
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
|
||||
# Simple convenience class to keep stuff together. Since we're still stuck on python 3.5, we can't use the full
|
||||
# typed named tuple, so we have to do it like this.
|
||||
# Once we are at python 3.6, feel free to change this to a named tuple.
|
||||
class _NodeInfo:
|
||||
def __init__(self, index_to_node: Optional[Dict[int, SceneNode]] = None, nodes_to_rename: Optional[List[SceneNode]] = None, is_group: bool = False) -> None:
|
||||
if index_to_node is None:
|
||||
index_to_node = {}
|
||||
if nodes_to_rename is None:
|
||||
nodes_to_rename = []
|
||||
self.index_to_node = index_to_node # type: Dict[int, SceneNode]
|
||||
self.nodes_to_rename = nodes_to_rename # type: List[SceneNode]
|
||||
self.is_group = is_group # type: bool
|
||||
|
||||
|
||||
## Keep track of all objects in the project
|
||||
class ObjectsModel(ListModel):
|
||||
NameRole = Qt.UserRole + 1
|
||||
SelectedRole = Qt.UserRole + 2
|
||||
OutsideAreaRole = Qt.UserRole + 3
|
||||
BuilplateNumberRole = Qt.UserRole + 4
|
||||
NodeRole = Qt.UserRole + 5
|
||||
|
||||
def __init__(self, parent = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.addRoleName(self.NameRole, "name")
|
||||
self.addRoleName(self.SelectedRole, "selected")
|
||||
self.addRoleName(self.OutsideAreaRole, "outside_build_area")
|
||||
self.addRoleName(self.BuilplateNumberRole, "buildplate_number")
|
||||
self.addRoleName(self.NodeRole, "node")
|
||||
|
||||
Application.getInstance().getController().getScene().sceneChanged.connect(self._updateSceneDelayed)
|
||||
Application.getInstance().getPreferences().preferenceChanged.connect(self._updateDelayed)
|
||||
Selection.selectionChanged.connect(self._updateDelayed)
|
||||
|
||||
self._update_timer = QTimer()
|
||||
self._update_timer.setInterval(200)
|
||||
self._update_timer.setSingleShot(True)
|
||||
self._update_timer.timeout.connect(self._update)
|
||||
|
||||
self._build_plate_number = -1
|
||||
|
||||
self._group_name_template = catalog.i18nc("@label", "Group #{group_nr}")
|
||||
self._group_name_prefix = self._group_name_template.split("#")[0]
|
||||
|
||||
self._naming_regex = re.compile("^(.+)\(([0-9]+)\)$")
|
||||
|
||||
def setActiveBuildPlate(self, nr: int) -> None:
|
||||
if self._build_plate_number != nr:
|
||||
self._build_plate_number = nr
|
||||
self._update()
|
||||
|
||||
def _updateSceneDelayed(self, source) -> None:
|
||||
if not isinstance(source, Camera):
|
||||
self._update_timer.start()
|
||||
|
||||
def _updateDelayed(self, *args) -> None:
|
||||
self._update_timer.start()
|
||||
|
||||
def _shouldNodeBeHandled(self, node: SceneNode) -> bool:
|
||||
is_group = bool(node.callDecoration("isGroup"))
|
||||
if not node.callDecoration("isSliceable") and not is_group:
|
||||
return False
|
||||
|
||||
parent = node.getParent()
|
||||
if parent and parent.callDecoration("isGroup"):
|
||||
return False # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||
|
||||
node_build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||
if Application.getInstance().getPreferences().getValue("view/filter_current_build_plate") and node_build_plate_number != self._build_plate_number:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def _renameNodes(self, node_info_dict: Dict[str, _NodeInfo]) -> List[SceneNode]:
|
||||
# Go through all names and find out the names for all nodes that need to be renamed.
|
||||
all_nodes = [] # type: List[SceneNode]
|
||||
for name, node_info in node_info_dict.items():
|
||||
# First add the ones that do not need to be renamed.
|
||||
for node in node_info.index_to_node.values():
|
||||
all_nodes.append(node)
|
||||
|
||||
# Generate new names for the nodes that need to be renamed
|
||||
current_index = 0
|
||||
for node in node_info.nodes_to_rename:
|
||||
current_index += 1
|
||||
while current_index in node_info.index_to_node:
|
||||
current_index += 1
|
||||
|
||||
if not node_info.is_group:
|
||||
new_group_name = "{0}({1})".format(name, current_index)
|
||||
else:
|
||||
new_group_name = "{0}#{1}".format(name, current_index)
|
||||
|
||||
old_name = node.getName()
|
||||
node.setName(new_group_name)
|
||||
Logger.log("d", "Node [%s] renamed to [%s]", old_name, new_group_name)
|
||||
all_nodes.append(node)
|
||||
return all_nodes
|
||||
|
||||
def _update(self, *args) -> None:
|
||||
nodes = [] # type: List[Dict[str, Union[str, int, bool, SceneNode]]]
|
||||
name_to_node_info_dict = {} # type: Dict[str, _NodeInfo]
|
||||
for node in DepthFirstIterator(Application.getInstance().getController().getScene().getRoot()): # type: ignore
|
||||
if not self._shouldNodeBeHandled(node):
|
||||
continue
|
||||
|
||||
is_group = bool(node.callDecoration("isGroup"))
|
||||
|
||||
force_rename = False
|
||||
if not is_group:
|
||||
# Handle names for individual nodes
|
||||
name = node.getName()
|
||||
|
||||
name_match = self._naming_regex.fullmatch(name)
|
||||
if name_match is None:
|
||||
original_name = name
|
||||
name_index = 0
|
||||
else:
|
||||
original_name = name_match.groups()[0]
|
||||
name_index = int(name_match.groups()[1])
|
||||
else:
|
||||
# Handle names for grouped nodes
|
||||
original_name = self._group_name_prefix
|
||||
|
||||
current_name = node.getName()
|
||||
if current_name.startswith(self._group_name_prefix):
|
||||
name_index = int(current_name.split("#")[-1])
|
||||
else:
|
||||
# Force rename this group because this node has not been named as a group yet, probably because
|
||||
# it's a newly created group.
|
||||
name_index = 0
|
||||
force_rename = True
|
||||
|
||||
if original_name not in name_to_node_info_dict:
|
||||
# Keep track of 2 things:
|
||||
# - known indices for nodes which doesn't need to be renamed
|
||||
# - a list of nodes that need to be renamed. When renaming then, we should avoid using the known indices.
|
||||
name_to_node_info_dict[original_name] = _NodeInfo(is_group = is_group)
|
||||
node_info = name_to_node_info_dict[original_name]
|
||||
if not force_rename and name_index not in node_info.index_to_node:
|
||||
node_info.index_to_node[name_index] = node
|
||||
else:
|
||||
node_info.nodes_to_rename.append(node)
|
||||
|
||||
all_nodes = self._renameNodes(name_to_node_info_dict)
|
||||
|
||||
for node in all_nodes:
|
||||
if hasattr(node, "isOutsideBuildArea"):
|
||||
is_outside_build_area = node.isOutsideBuildArea() # type: ignore
|
||||
else:
|
||||
is_outside_build_area = False
|
||||
|
||||
node_build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||
|
||||
nodes.append({
|
||||
"name": node.getName(),
|
||||
"selected": Selection.isSelected(node),
|
||||
"outside_build_area": is_outside_build_area,
|
||||
"buildplate_number": node_build_plate_number,
|
||||
"node": node
|
||||
})
|
||||
|
||||
nodes = sorted(nodes, key=lambda n: n["name"])
|
||||
self.setItems(nodes)
|
@ -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
|
||||
|
||||
@ -84,6 +81,7 @@ class PrintInformation(QObject):
|
||||
"support_interface": catalog.i18nc("@tooltip", "Support Interface"),
|
||||
"support": catalog.i18nc("@tooltip", "Support"),
|
||||
"skirt": catalog.i18nc("@tooltip", "Skirt"),
|
||||
"prime_tower": catalog.i18nc("@tooltip", "Prime Tower"),
|
||||
"travel": catalog.i18nc("@tooltip", "Travel"),
|
||||
"retract": catalog.i18nc("@tooltip", "Retractions"),
|
||||
"none": catalog.i18nc("@tooltip", "Other")
|
49
cura/UI/RecommendedMode.py
Normal file
49
cura/UI/RecommendedMode.py
Normal file
@ -0,0 +1,49 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
from cura import CuraApplication
|
||||
|
||||
#
|
||||
# This object contains helper/convenience functions for Recommended mode.
|
||||
#
|
||||
class RecommendedMode(QObject):
|
||||
|
||||
# Sets to use the adhesion or not for the "Adhesion" CheckBox in Recommended mode.
|
||||
@pyqtSlot(bool)
|
||||
def setAdhesion(self, checked: bool) -> None:
|
||||
application = CuraApplication.CuraApplication.getInstance()
|
||||
global_stack = application.getMachineManager().activeMachine
|
||||
if global_stack is None:
|
||||
return
|
||||
|
||||
# Remove the adhesion type value set by the user.
|
||||
adhesion_type_key = "adhesion_type"
|
||||
user_changes_container = global_stack.userChanges
|
||||
if adhesion_type_key in user_changes_container.getAllKeys():
|
||||
user_changes_container.removeInstance(adhesion_type_key)
|
||||
|
||||
# Get the default value of adhesion type after user's value has been removed.
|
||||
# skirt and none are counted as "no adhesion", the others are considered as "with adhesion". The conditions are
|
||||
# as the following:
|
||||
# - if the user checks the adhesion checkbox, get the default value (including the custom quality) for adhesion
|
||||
# type.
|
||||
# (1) If the default value is "skirt" or "none" (no adhesion), set adhesion_type to "brim".
|
||||
# (2) If the default value is "with adhesion", do nothing.
|
||||
# - if the user unchecks the adhesion checkbox, get the default value (including the custom quality) for
|
||||
# adhesion type.
|
||||
# (1) If the default value is "skirt" or "none" (no adhesion), do nothing.
|
||||
# (2) Otherwise, set adhesion_type to "skirt".
|
||||
value = global_stack.getProperty(adhesion_type_key, "value")
|
||||
if checked:
|
||||
if value in ("skirt", "none"):
|
||||
value = "brim"
|
||||
else:
|
||||
if value not in ("skirt", "none"):
|
||||
value = "skirt"
|
||||
|
||||
user_changes_container.setProperty(adhesion_type_key, "value", value)
|
||||
|
||||
|
||||
__all__ = ["RecommendedMode"]
|
69
cura/UI/TextManager.py
Normal file
69
cura/UI/TextManager.py
Normal file
@ -0,0 +1,69 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import collections
|
||||
from typing import Optional, Dict, List, cast
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
from UM.Resources import Resources
|
||||
from UM.Version import Version
|
||||
|
||||
|
||||
#
|
||||
# This manager provides means to load texts to QML.
|
||||
#
|
||||
class TextManager(QObject):
|
||||
|
||||
def __init__(self, parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self._change_log_text = ""
|
||||
|
||||
@pyqtSlot(result = str)
|
||||
def getChangeLogText(self) -> str:
|
||||
if not self._change_log_text:
|
||||
self._change_log_text = self._loadChangeLogText()
|
||||
return self._change_log_text
|
||||
|
||||
def _loadChangeLogText(self) -> str:
|
||||
# Load change log texts and organize them with a dict
|
||||
file_path = Resources.getPath(Resources.Texts, "change_log.txt")
|
||||
change_logs_dict = {} # type: Dict[Version, Dict[str, List[str]]]
|
||||
with open(file_path, "r", encoding = "utf-8") as f:
|
||||
open_version = None # type: Optional[Version]
|
||||
open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog
|
||||
for line in f:
|
||||
line = line.replace("\n", "")
|
||||
if "[" in line and "]" in line:
|
||||
line = line.replace("[", "")
|
||||
line = line.replace("]", "")
|
||||
open_version = Version(line)
|
||||
if open_version > Version([14, 99, 99]): # Bit of a hack: We released the 15.x.x versions before 2.x
|
||||
open_version = Version([0, open_version.getMinor(), open_version.getRevision(), open_version.getPostfixVersion()])
|
||||
open_header = ""
|
||||
change_logs_dict[open_version] = collections.OrderedDict()
|
||||
elif line.startswith("*"):
|
||||
open_header = line.replace("*", "")
|
||||
change_logs_dict[cast(Version, open_version)][open_header] = []
|
||||
elif line != "":
|
||||
if open_header not in change_logs_dict[cast(Version, open_version)]:
|
||||
change_logs_dict[cast(Version, open_version)][open_header] = []
|
||||
change_logs_dict[cast(Version, open_version)][open_header].append(line)
|
||||
|
||||
# Format changelog text
|
||||
content = ""
|
||||
for version in sorted(change_logs_dict.keys(), reverse = True):
|
||||
text_version = version
|
||||
if version < Version([1, 0, 0]): # Bit of a hack: We released the 15.x.x versions before 2.x
|
||||
text_version = Version([15, version.getMinor(), version.getRevision(), version.getPostfixVersion()])
|
||||
content += "<h1>" + str(text_version) + "</h1><br>"
|
||||
content += ""
|
||||
for change in change_logs_dict[version]:
|
||||
if str(change) != "":
|
||||
content += "<b>" + str(change) + "</b><br>"
|
||||
for line in change_logs_dict[version][change]:
|
||||
content += str(line) + "<br>"
|
||||
content += "<br>"
|
||||
|
||||
return content
|
294
cura/UI/WelcomePagesModel.py
Normal file
294
cura/UI/WelcomePagesModel.py
Normal file
@ -0,0 +1,294 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
from collections import deque
|
||||
import os
|
||||
from typing import TYPE_CHECKING, Optional, List, Dict, Any
|
||||
|
||||
from PyQt5.QtCore import QUrl, Qt, pyqtSlot, pyqtProperty, pyqtSignal
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Logger import Logger
|
||||
from UM.Qt.ListModel import ListModel
|
||||
from UM.Resources import Resources
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from PyQt5.QtCore import QObject
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
#
|
||||
# This is the Qt ListModel that contains all welcome pages data. Each page is a page that can be shown as a step in the
|
||||
# welcome wizard dialog. Each item in this ListModel represents a page, which contains the following fields:
|
||||
#
|
||||
# - id : A unique page_id which can be used in function goToPage(page_id)
|
||||
# - page_url : The QUrl to the QML file that contains the content of this page
|
||||
# - next_page_id : (OPTIONAL) The next page ID to go to when this page finished. This is optional. If this is not
|
||||
# provided, it will go to the page with the current index + 1
|
||||
# - next_page_button_text: (OPTIONAL) The text to show for the "next" button, by default it's the translated text of
|
||||
# "Next". Note that each step QML can decide whether to use this text or not, so it's not
|
||||
# mandatory.
|
||||
# - should_show_function : (OPTIONAL) An optional function that returns True/False indicating if this page should be
|
||||
# shown. By default all pages should be shown. If a function returns False, that page will
|
||||
# be skipped and its next page will be shown.
|
||||
#
|
||||
# Note that in any case, a page that has its "should_show_function" == False will ALWAYS be skipped.
|
||||
#
|
||||
class WelcomePagesModel(ListModel):
|
||||
|
||||
IdRole = Qt.UserRole + 1 # Page ID
|
||||
PageUrlRole = Qt.UserRole + 2 # URL to the page's QML file
|
||||
NextPageIdRole = Qt.UserRole + 3 # The next page ID it should go to
|
||||
NextPageButtonTextRole = Qt.UserRole + 4 # The text for the next page button
|
||||
PreviousPageButtonTextRole = Qt.UserRole + 5 # The text for the previous page button
|
||||
|
||||
def __init__(self, application: "CuraApplication", parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent)
|
||||
|
||||
self.addRoleName(self.IdRole, "id")
|
||||
self.addRoleName(self.PageUrlRole, "page_url")
|
||||
self.addRoleName(self.NextPageIdRole, "next_page_id")
|
||||
self.addRoleName(self.NextPageButtonTextRole, "next_page_button_text")
|
||||
self.addRoleName(self.PreviousPageButtonTextRole, "previous_page_button_text")
|
||||
|
||||
self._application = application
|
||||
self._catalog = i18nCatalog("cura")
|
||||
|
||||
self._default_next_button_text = self._catalog.i18nc("@action:button", "Next")
|
||||
|
||||
self._pages = [] # type: List[Dict[str, Any]]
|
||||
|
||||
self._current_page_index = 0
|
||||
# Store all the previous page indices so it can go back.
|
||||
self._previous_page_indices_stack = deque() # type: deque
|
||||
|
||||
# If the welcome flow should be shown. It can show the complete flow or just the changelog depending on the
|
||||
# specific case. See initialize() for how this variable is set.
|
||||
self._should_show_welcome_flow = False
|
||||
|
||||
allFinished = pyqtSignal() # emitted when all steps have been finished
|
||||
currentPageIndexChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(int, notify = currentPageIndexChanged)
|
||||
def currentPageIndex(self) -> int:
|
||||
return self._current_page_index
|
||||
|
||||
# Returns a float number in [0, 1] which indicates the current progress.
|
||||
@pyqtProperty(float, notify = currentPageIndexChanged)
|
||||
def currentProgress(self) -> float:
|
||||
if len(self._items) == 0:
|
||||
return 0
|
||||
else:
|
||||
return self._current_page_index / len(self._items)
|
||||
|
||||
# Indicates if the current page is the last page.
|
||||
@pyqtProperty(bool, notify = currentPageIndexChanged)
|
||||
def isCurrentPageLast(self) -> bool:
|
||||
return self._current_page_index == len(self._items) - 1
|
||||
|
||||
def _setCurrentPageIndex(self, page_index: int) -> None:
|
||||
if page_index != self._current_page_index:
|
||||
self._previous_page_indices_stack.append(self._current_page_index)
|
||||
self._current_page_index = page_index
|
||||
self.currentPageIndexChanged.emit()
|
||||
|
||||
# Ends the Welcome-Pages. Put as a separate function for cases like the 'decline' in the User-Agreement.
|
||||
@pyqtSlot()
|
||||
def atEnd(self) -> None:
|
||||
self.allFinished.emit()
|
||||
self.resetState()
|
||||
|
||||
# Goes to the next page.
|
||||
# If "from_index" is given, it will look for the next page to show starting from the "from_index" page instead of
|
||||
# the "self._current_page_index".
|
||||
@pyqtSlot()
|
||||
def goToNextPage(self, from_index: Optional[int] = None) -> None:
|
||||
# Look for the next page that should be shown
|
||||
current_index = self._current_page_index if from_index is None else from_index
|
||||
while True:
|
||||
page_item = self._items[current_index]
|
||||
|
||||
# Check if there's a "next_page_id" assigned. If so, go to that page. Otherwise, go to the page with the
|
||||
# current index + 1.
|
||||
next_page_id = page_item.get("next_page_id")
|
||||
next_page_index = current_index + 1
|
||||
if next_page_id:
|
||||
idx = self.getPageIndexById(next_page_id)
|
||||
if idx is None:
|
||||
# FIXME: If we cannot find the next page, we cannot do anything here.
|
||||
Logger.log("e", "Cannot find page with ID [%s]", next_page_id)
|
||||
return
|
||||
next_page_index = idx
|
||||
|
||||
# If we have reached the last page, emit allFinished signal and reset.
|
||||
if next_page_index == len(self._items):
|
||||
self.atEnd()
|
||||
return
|
||||
|
||||
# Check if the this page should be shown (default yes), if not, keep looking for the next one.
|
||||
next_page_item = self.getItem(next_page_index)
|
||||
if self._shouldPageBeShown(next_page_index):
|
||||
break
|
||||
|
||||
Logger.log("d", "Page [%s] should not be displayed, look for the next page.", next_page_item["id"])
|
||||
current_index = next_page_index
|
||||
|
||||
# Move to the next page
|
||||
self._setCurrentPageIndex(next_page_index)
|
||||
|
||||
# Goes to the previous page. If there's no previous page, do nothing.
|
||||
@pyqtSlot()
|
||||
def goToPreviousPage(self) -> None:
|
||||
if len(self._previous_page_indices_stack) == 0:
|
||||
Logger.log("i", "No previous page, do nothing")
|
||||
return
|
||||
|
||||
previous_page_index = self._previous_page_indices_stack.pop()
|
||||
self._current_page_index = previous_page_index
|
||||
self.currentPageIndexChanged.emit()
|
||||
|
||||
# Sets the current page to the given page ID. If the page ID is not found, do nothing.
|
||||
@pyqtSlot(str)
|
||||
def goToPage(self, page_id: str) -> None:
|
||||
page_index = self.getPageIndexById(page_id)
|
||||
if page_index is None:
|
||||
# FIXME: If we cannot find the next page, we cannot do anything here.
|
||||
Logger.log("e", "Cannot find page with ID [%s], go to the next page by default", page_index)
|
||||
self.goToNextPage()
|
||||
return
|
||||
|
||||
if self._shouldPageBeShown(page_index):
|
||||
# Move to that page if it should be shown
|
||||
self._setCurrentPageIndex(page_index)
|
||||
else:
|
||||
# Find the next page to show starting from the "page_index"
|
||||
self.goToNextPage(from_index = page_index)
|
||||
|
||||
# Checks if the page with the given index should be shown by calling the "should_show_function" associated with it.
|
||||
# If the function is not present, returns True (show page by default).
|
||||
def _shouldPageBeShown(self, page_index: int) -> bool:
|
||||
next_page_item = self.getItem(page_index)
|
||||
should_show_function = next_page_item.get("should_show_function", lambda: True)
|
||||
return should_show_function()
|
||||
|
||||
# Resets the state of the WelcomePagesModel. This functions does the following:
|
||||
# - Resets current_page_index to 0
|
||||
# - Clears the previous page indices stack
|
||||
@pyqtSlot()
|
||||
def resetState(self) -> None:
|
||||
self._current_page_index = 0
|
||||
self._previous_page_indices_stack.clear()
|
||||
|
||||
self.currentPageIndexChanged.emit()
|
||||
|
||||
shouldShowWelcomeFlowChanged = pyqtSignal()
|
||||
|
||||
@pyqtProperty(bool, notify = shouldShowWelcomeFlowChanged)
|
||||
def shouldShowWelcomeFlow(self) -> bool:
|
||||
return self._should_show_welcome_flow
|
||||
|
||||
# Gets the page index with the given page ID. If the page ID doesn't exist, returns None.
|
||||
def getPageIndexById(self, page_id: str) -> Optional[int]:
|
||||
page_idx = None
|
||||
for idx, page_item in enumerate(self._items):
|
||||
if page_item["id"] == page_id:
|
||||
page_idx = idx
|
||||
break
|
||||
return page_idx
|
||||
|
||||
# Convenience function to get QUrl path to pages that's located in "resources/qml/WelcomePages".
|
||||
def _getBuiltinWelcomePagePath(self, page_filename: str) -> "QUrl":
|
||||
from cura.CuraApplication import CuraApplication
|
||||
return QUrl.fromLocalFile(Resources.getPath(CuraApplication.ResourceTypes.QmlFiles,
|
||||
os.path.join("WelcomePages", page_filename)))
|
||||
|
||||
# FIXME: HACKs for optimization that we don't update the model every time the active machine gets changed.
|
||||
def _onActiveMachineChanged(self) -> None:
|
||||
self._application.getMachineManager().globalContainerChanged.disconnect(self._onActiveMachineChanged)
|
||||
self._initialize(update_should_show_flag = False)
|
||||
|
||||
def initialize(self) -> None:
|
||||
self._application.getMachineManager().globalContainerChanged.connect(self._onActiveMachineChanged)
|
||||
self._initialize()
|
||||
|
||||
def _initialize(self, update_should_show_flag: bool = True) -> None:
|
||||
show_whatsnew_only = False
|
||||
if update_should_show_flag:
|
||||
has_active_machine = self._application.getMachineManager().activeMachine is not None
|
||||
has_app_just_upgraded = self._application.hasJustUpdatedFromOldVersion()
|
||||
|
||||
# Only show the what's new dialog if there's no machine and we have just upgraded
|
||||
show_complete_flow = not has_active_machine
|
||||
show_whatsnew_only = has_active_machine and has_app_just_upgraded
|
||||
|
||||
# FIXME: This is a hack. Because of the circular dependency between MachineManager, ExtruderManager, and
|
||||
# possibly some others, setting the initial active machine is not done when the MachineManager gets initialized.
|
||||
# So at this point, we don't know if there will be an active machine or not. It could be that the active machine
|
||||
# files are corrupted so we cannot rely on Preferences either. This makes sure that once the active machine
|
||||
# gets changed, this model updates the flags, so it can decide whether to show the welcome flow or not.
|
||||
should_show_welcome_flow = show_complete_flow or show_whatsnew_only
|
||||
if should_show_welcome_flow != self._should_show_welcome_flow:
|
||||
self._should_show_welcome_flow = should_show_welcome_flow
|
||||
self.shouldShowWelcomeFlowChanged.emit()
|
||||
|
||||
# All pages
|
||||
all_pages_list = [{"id": "welcome",
|
||||
"page_url": self._getBuiltinWelcomePagePath("WelcomeContent.qml"),
|
||||
},
|
||||
{"id": "user_agreement",
|
||||
"page_url": self._getBuiltinWelcomePagePath("UserAgreementContent.qml"),
|
||||
},
|
||||
{"id": "whats_new",
|
||||
"page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"),
|
||||
},
|
||||
{"id": "data_collections",
|
||||
"page_url": self._getBuiltinWelcomePagePath("DataCollectionsContent.qml"),
|
||||
},
|
||||
{"id": "add_network_or_local_printer",
|
||||
"page_url": self._getBuiltinWelcomePagePath("AddNetworkOrLocalPrinterContent.qml"),
|
||||
"next_page_id": "machine_actions",
|
||||
},
|
||||
{"id": "add_printer_by_ip",
|
||||
"page_url": self._getBuiltinWelcomePagePath("AddPrinterByIpContent.qml"),
|
||||
"next_page_id": "machine_actions",
|
||||
},
|
||||
{"id": "machine_actions",
|
||||
"page_url": self._getBuiltinWelcomePagePath("FirstStartMachineActionsContent.qml"),
|
||||
"next_page_id": "cloud",
|
||||
"should_show_function": self.shouldShowMachineActions,
|
||||
},
|
||||
{"id": "cloud",
|
||||
"page_url": self._getBuiltinWelcomePagePath("CloudContent.qml"),
|
||||
},
|
||||
]
|
||||
|
||||
pages_to_show = all_pages_list
|
||||
if show_whatsnew_only:
|
||||
pages_to_show = list(filter(lambda x: x["id"] == "whats_new", all_pages_list))
|
||||
|
||||
self._pages = pages_to_show
|
||||
self.setItems(self._pages)
|
||||
|
||||
# For convenience, inject the default "next" button text to each item if it's not present.
|
||||
def setItems(self, items: List[Dict[str, Any]]) -> None:
|
||||
for item in items:
|
||||
if "next_page_button_text" not in item:
|
||||
item["next_page_button_text"] = self._default_next_button_text
|
||||
|
||||
super().setItems(items)
|
||||
|
||||
# Indicates if the machine action panel should be shown by checking if there's any first start machine actions
|
||||
# available.
|
||||
def shouldShowMachineActions(self) -> bool:
|
||||
global_stack = self._application.getMachineManager().activeMachine
|
||||
if global_stack is None:
|
||||
return False
|
||||
|
||||
definition_id = global_stack.definition.getId()
|
||||
first_start_actions = self._application.getMachineActionManager().getFirstStartActions(definition_id)
|
||||
return len([action for action in first_start_actions if action.needsUserInteraction()]) > 0
|
||||
|
||||
def addPage(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
__all__ = ["WelcomePagesModel"]
|
22
cura/UI/WhatsNewPagesModel.py
Normal file
22
cura/UI/WhatsNewPagesModel.py
Normal file
@ -0,0 +1,22 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from .WelcomePagesModel import WelcomePagesModel
|
||||
|
||||
|
||||
#
|
||||
# This Qt ListModel is more or less the same the WelcomePagesModel, except that this model is only for showing the
|
||||
# "what's new" page. This is also used in the "Help" menu to show the changes log.
|
||||
#
|
||||
class WhatsNewPagesModel(WelcomePagesModel):
|
||||
|
||||
def initialize(self) -> None:
|
||||
self._pages = []
|
||||
self._pages.append({"id": "whats_new",
|
||||
"page_url": self._getBuiltinWelcomePagePath("WhatsNewContent.qml"),
|
||||
"next_page_button_text": self._catalog.i18nc("@action:button", "Close"),
|
||||
})
|
||||
self.setItems(self._pages)
|
||||
|
||||
|
||||
__all__ = ["WhatsNewPagesModel"]
|
0
cura/UI/__init__.py
Normal file
0
cura/UI/__init__.py
Normal file
44
cura/Utils/NetworkingUtil.py
Normal file
44
cura/Utils/NetworkingUtil.py
Normal file
@ -0,0 +1,44 @@
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import socket
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtCore import QObject, pyqtSlot
|
||||
|
||||
|
||||
#
|
||||
# This is a QObject because some of the functions can be used (and are useful) in QML.
|
||||
#
|
||||
class NetworkingUtil(QObject):
|
||||
|
||||
def __init__(self, parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(parent = parent)
|
||||
|
||||
# Checks if the given string is a valid IPv4 address.
|
||||
@pyqtSlot(str, result = bool)
|
||||
def isIPv4(self, address: str) -> bool:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET, address)
|
||||
result = True
|
||||
except:
|
||||
result = False
|
||||
return result
|
||||
|
||||
# Checks if the given string is a valid IPv6 address.
|
||||
@pyqtSlot(str, result = bool)
|
||||
def isIPv6(self, address: str) -> bool:
|
||||
try:
|
||||
socket.inet_pton(socket.AF_INET6, address)
|
||||
result = True
|
||||
except:
|
||||
result = False
|
||||
return result
|
||||
|
||||
# Checks if the given string is a valid IPv4 or IPv6 address.
|
||||
@pyqtSlot(str, result = bool)
|
||||
def isValidIP(self, address: str) -> bool:
|
||||
return self.isIPv4(address) or self.isIPv6(address)
|
||||
|
||||
|
||||
__all__ = ["NetworkingUtil"]
|
48
cura_app.py
48
cura_app.py
@ -23,13 +23,17 @@ 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():
|
||||
return os.path.expanduser("~/Library/Logs/" + CuraAppName)
|
||||
|
||||
if hasattr(sys, "frozen"):
|
||||
# Do not redirect stdout and stderr to files if we are running CLI.
|
||||
if hasattr(sys, "frozen") and "cli" not in os.path.basename(sys.argv[0]).lower():
|
||||
dirpath = get_cura_dir_path()
|
||||
os.makedirs(dirpath, exist_ok = True)
|
||||
sys.stdout = open(os.path.join(dirpath, "stdout.log"), "w", encoding = "utf-8")
|
||||
@ -56,6 +60,14 @@ if Platform.isWindows() and hasattr(sys, "frozen"):
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# GITHUB issue #6194: https://github.com/Ultimaker/Cura/issues/6194
|
||||
# With AppImage 2 on Linux, the current working directory will be somewhere in /tmp/<rand>/usr, which is owned
|
||||
# by root. For some reason, QDesktopServices.openUrl() requires to have a usable current working directory,
|
||||
# otherwise it doesn't work. This is a workaround on Linux that before we call QDesktopServices.openUrl(), we
|
||||
# switch to a directory where the user has the ownership.
|
||||
if Platform.isLinux() and hasattr(sys, "frozen"):
|
||||
os.chdir(os.path.expanduser("~"))
|
||||
|
||||
# WORKAROUND: GITHUB-704 GITHUB-708
|
||||
# It looks like setuptools creates a .pth file in
|
||||
# the default /usr/lib which causes the default site-packages
|
||||
@ -129,5 +141,37 @@ import Arcus #@UnusedImport
|
||||
import Savitar #@UnusedImport
|
||||
from cura.CuraApplication import CuraApplication
|
||||
|
||||
|
||||
# WORKAROUND: CURA-6739
|
||||
# The CTM file loading module in Trimesh requires the OpenCTM library to be dynamically loaded. It uses
|
||||
# ctypes.util.find_library() to find libopenctm.dylib, but this doesn't seem to look in the ".app" application folder
|
||||
# on Mac OS X. Adding the search path to environment variables such as DYLD_LIBRARY_PATH and DYLD_FALLBACK_LIBRARY_PATH
|
||||
# makes it work. The workaround here uses DYLD_FALLBACK_LIBRARY_PATH.
|
||||
if Platform.isOSX() and getattr(sys, "frozen", False):
|
||||
old_env = os.environ.get("DYLD_FALLBACK_LIBRARY_PATH", "")
|
||||
# This is where libopenctm.so is in the .app folder.
|
||||
search_path = os.path.join(CuraApplication.getInstallPrefix(), "MacOS")
|
||||
path_list = old_env.split(":")
|
||||
if search_path not in path_list:
|
||||
path_list.append(search_path)
|
||||
os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = ":".join(path_list)
|
||||
import trimesh.exchange.load
|
||||
os.environ["DYLD_FALLBACK_LIBRARY_PATH"] = old_env
|
||||
|
||||
# WORKAROUND: CURA-6739
|
||||
# Similar CTM file loading fix for Linux, but NOTE THAT this doesn't work directly with Python 3.5.7. There's a fix
|
||||
# for ctypes.util.find_library() in Python 3.6 and 3.7. That fix makes sure that find_library() will check
|
||||
# LD_LIBRARY_PATH. With Python 3.5, that fix needs to be backported to make this workaround work.
|
||||
if Platform.isLinux() and getattr(sys, "frozen", False):
|
||||
old_env = os.environ.get("LD_LIBRARY_PATH", "")
|
||||
# This is where libopenctm.so is in the AppImage.
|
||||
search_path = os.path.join(CuraApplication.getInstallPrefix(), "bin")
|
||||
path_list = old_env.split(":")
|
||||
if search_path not in path_list:
|
||||
path_list.append(search_path)
|
||||
os.environ["LD_LIBRARY_PATH"] = ":".join(path_list)
|
||||
import trimesh.exchange.load
|
||||
os.environ["LD_LIBRARY_PATH"] = old_env
|
||||
|
||||
app = CuraApplication()
|
||||
app.run()
|
||||
|
43
docker/build.sh
Executable file
43
docker/build.sh
Executable file
@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Abort at the first error.
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||
PROJECT_DIR="$( cd "${SCRIPT_DIR}/.." && pwd )"
|
||||
|
||||
# Make sure that environment variables are set properly
|
||||
source /opt/rh/devtoolset-7/enable
|
||||
export PATH="${CURA_BUILD_ENV_PATH}/bin:${PATH}"
|
||||
export PKG_CONFIG_PATH="${CURA_BUILD_ENV_PATH}/lib/pkgconfig:${PKG_CONFIG_PATH}"
|
||||
|
||||
cd "${PROJECT_DIR}"
|
||||
|
||||
#
|
||||
# Clone Uranium and set PYTHONPATH first
|
||||
#
|
||||
|
||||
# Check the branch to use:
|
||||
# 1. Use the Uranium branch with the branch same if it exists.
|
||||
# 2. Otherwise, use the default branch name "master"
|
||||
URANIUM_BRANCH="${CI_COMMIT_REF_NAME:-master}"
|
||||
output="$(git ls-remote --heads https://github.com/Ultimaker/Uranium.git "${URANIUM_BRANCH}")"
|
||||
if [ -z "${output}" ]; then
|
||||
echo "Could not find Uranium banch ${URANIUM_BRANCH}, fallback to use master."
|
||||
URANIUM_BRANCH="master"
|
||||
fi
|
||||
|
||||
echo "Using Uranium branch ${URANIUM_BRANCH} ..."
|
||||
git clone --depth=1 -b "${URANIUM_BRANCH}" https://github.com/Ultimaker/Uranium.git "${PROJECT_DIR}"/Uranium
|
||||
export PYTHONPATH="${PROJECT_DIR}/Uranium:.:${PYTHONPATH}"
|
||||
|
||||
mkdir build
|
||||
cd build
|
||||
cmake3 \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_PREFIX_PATH="${CURA_BUILD_ENV_PATH}" \
|
||||
-DURANIUM_DIR="${PROJECT_DIR}/Uranium" \
|
||||
-DBUILD_TESTS=ON \
|
||||
..
|
||||
make
|
||||
ctest3 --output-on-failure -T Test
|
@ -1,7 +1,7 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Copyright (c) 2019 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import Optional
|
||||
from typing import List, Optional, Union, TYPE_CHECKING
|
||||
import os.path
|
||||
import zipfile
|
||||
|
||||
@ -9,15 +9,16 @@ import numpy
|
||||
|
||||
import Savitar
|
||||
|
||||
from UM.Application import Application
|
||||
from UM.Logger import Logger
|
||||
from UM.Math.Matrix import Matrix
|
||||
from UM.Math.Vector import Vector
|
||||
from UM.Mesh.MeshBuilder import MeshBuilder
|
||||
from UM.Mesh.MeshReader import MeshReader
|
||||
from UM.Scene.GroupDecorator import GroupDecorator
|
||||
from UM.Scene.SceneNode import SceneNode #For typing.
|
||||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
||||
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Settings.ExtruderManager import ExtruderManager
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||
@ -25,11 +26,9 @@ from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
from cura.Scene.ZOffsetDecorator import ZOffsetDecorator
|
||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||
|
||||
MYPY = False
|
||||
|
||||
|
||||
try:
|
||||
if not MYPY:
|
||||
if not TYPE_CHECKING:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
Logger.log("w", "Unable to load cElementTree, switching to slower version")
|
||||
@ -55,7 +54,7 @@ class ThreeMFReader(MeshReader):
|
||||
self._unit = None
|
||||
self._object_count = 0 # Used to name objects as there is no node name yet.
|
||||
|
||||
def _createMatrixFromTransformationString(self, transformation):
|
||||
def _createMatrixFromTransformationString(self, transformation: str) -> Matrix:
|
||||
if transformation == "":
|
||||
return Matrix()
|
||||
|
||||
@ -85,13 +84,13 @@ class ThreeMFReader(MeshReader):
|
||||
|
||||
return temp_mat
|
||||
|
||||
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a Uranium scene node.
|
||||
# \returns Uranium scene node.
|
||||
def _convertSavitarNodeToUMNode(self, savitar_node):
|
||||
## Convenience function that converts a SceneNode object (as obtained from libSavitar) to a scene node.
|
||||
# \returns Scene node.
|
||||
def _convertSavitarNodeToUMNode(self, savitar_node: Savitar.SceneNode) -> Optional[SceneNode]:
|
||||
self._object_count += 1
|
||||
node_name = "Object %s" % self._object_count
|
||||
|
||||
active_build_plate = Application.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||
active_build_plate = CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate
|
||||
|
||||
um_node = CuraSceneNode() # This adds a SettingOverrideDecorator
|
||||
um_node.addDecorator(BuildPlateDecorator(active_build_plate))
|
||||
@ -122,7 +121,7 @@ class ThreeMFReader(MeshReader):
|
||||
|
||||
# Add the setting override decorator, so we can add settings to this node.
|
||||
if settings:
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
|
||||
# Ensure the correct next container for the SettingOverride decorator is set.
|
||||
if global_container_stack:
|
||||
@ -161,7 +160,7 @@ class ThreeMFReader(MeshReader):
|
||||
um_node.addDecorator(sliceable_decorator)
|
||||
return um_node
|
||||
|
||||
def _read(self, file_name):
|
||||
def _read(self, file_name: str) -> Union[SceneNode, List[SceneNode]]:
|
||||
result = []
|
||||
self._object_count = 0 # Used to name objects as there is no node name yet.
|
||||
# The base object of 3mf is a zipped archive.
|
||||
@ -181,12 +180,13 @@ class ThreeMFReader(MeshReader):
|
||||
mesh_data = um_node.getMeshData()
|
||||
if mesh_data is not None:
|
||||
extents = mesh_data.getExtents()
|
||||
center_vector = Vector(extents.center.x, extents.center.y, extents.center.z)
|
||||
transform_matrix.setByTranslation(center_vector)
|
||||
if extents is not None:
|
||||
center_vector = Vector(extents.center.x, extents.center.y, extents.center.z)
|
||||
transform_matrix.setByTranslation(center_vector)
|
||||
transform_matrix.multiply(um_node.getLocalTransformation())
|
||||
um_node.setTransformation(transform_matrix)
|
||||
|
||||
global_container_stack = Application.getInstance().getGlobalContainerStack()
|
||||
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
|
||||
|
||||
# Create a transformation Matrix to convert from 3mf worldspace into ours.
|
||||
# First step: flip the y and z axis.
|
||||
@ -215,17 +215,20 @@ class ThreeMFReader(MeshReader):
|
||||
um_node.setTransformation(um_node.getLocalTransformation().preMultiply(transformation_matrix))
|
||||
|
||||
# Check if the model is positioned below the build plate and honor that when loading project files.
|
||||
if um_node.getMeshData() is not None:
|
||||
minimum_z_value = um_node.getMeshData().getExtents(um_node.getWorldTransformation()).minimum.y # y is z in transformation coordinates
|
||||
if minimum_z_value < 0:
|
||||
um_node.addDecorator(ZOffsetDecorator())
|
||||
um_node.callDecoration("setZOffset", minimum_z_value)
|
||||
node_meshdata = um_node.getMeshData()
|
||||
if node_meshdata is not None:
|
||||
aabb = node_meshdata.getExtents(um_node.getWorldTransformation())
|
||||
if aabb is not None:
|
||||
minimum_z_value = aabb.minimum.y # y is z in transformation coordinates
|
||||
if minimum_z_value < 0:
|
||||
um_node.addDecorator(ZOffsetDecorator())
|
||||
um_node.callDecoration("setZOffset", minimum_z_value)
|
||||
|
||||
result.append(um_node)
|
||||
|
||||
except Exception:
|
||||
Logger.logException("e", "An exception occurred in 3mf reader.")
|
||||
return None
|
||||
return []
|
||||
|
||||
return result
|
||||
|
||||
|
@ -33,6 +33,8 @@ from cura.Settings.CuraContainerStack import _ContainerIndexes
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from cura.Utils.Threading import call_on_qt_thread
|
||||
|
||||
from PyQt5.QtCore import QCoreApplication
|
||||
|
||||
from .WorkspaceDialog import WorkspaceDialog
|
||||
|
||||
i18n_catalog = i18nCatalog("cura")
|
||||
@ -59,6 +61,9 @@ class MachineInfo:
|
||||
self.container_id = None
|
||||
self.name = None
|
||||
self.definition_id = None
|
||||
|
||||
self.metadata_dict = {} # type: Dict[str, str]
|
||||
|
||||
self.quality_type = None
|
||||
self.custom_quality_name = None
|
||||
self.quality_changes_info = None
|
||||
@ -227,6 +232,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
else:
|
||||
Logger.log("w", "Unknown definition container type %s for %s",
|
||||
definition_container_type, definition_container_file)
|
||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||
Job.yieldThread()
|
||||
|
||||
if machine_definition_container_count != 1:
|
||||
@ -253,13 +259,14 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
containers_found_dict["material"] = True
|
||||
if not self._container_registry.isReadOnly(container_id): # Only non readonly materials can be in conflict
|
||||
material_conflict = True
|
||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||
Job.yieldThread()
|
||||
|
||||
# Check if any quality_changes instance container is in conflict.
|
||||
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 +304,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")
|
||||
@ -322,7 +329,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
# Ignore certain instance container types
|
||||
Logger.log("w", "Ignoring instance container [%s] with type [%s]", container_id, container_type)
|
||||
continue
|
||||
|
||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||
Job.yieldThread()
|
||||
|
||||
if self._machine_info.quality_changes_info.global_info is None:
|
||||
@ -342,6 +349,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
global_stack_id = self._stripFileToId(global_stack_file)
|
||||
serialized = archive.open(global_stack_file).read().decode("utf-8")
|
||||
machine_name = self._getMachineNameFromSerializedStack(serialized)
|
||||
self._machine_info.metadata_dict = self._getMetaDataDictFromSerializedStack(serialized)
|
||||
|
||||
stacks = self._container_registry.findContainerStacks(name = machine_name, type = "machine")
|
||||
self._is_same_machine_type = True
|
||||
existing_global_stack = None
|
||||
@ -397,7 +406,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
variant_id = parser["containers"][str(_ContainerIndexes.Variant)]
|
||||
if variant_id not in ("empty", "empty_variant"):
|
||||
self._machine_info.variant_info = instance_container_info_dict[variant_id]
|
||||
|
||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||
Job.yieldThread()
|
||||
|
||||
# if the global stack is found, we check if there are conflicts in the extruder stacks
|
||||
@ -419,13 +428,17 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
if parser.has_option("metadata", "enabled"):
|
||||
extruder_info.enabled = parser["metadata"]["enabled"]
|
||||
if variant_id not in ("empty", "empty_variant"):
|
||||
extruder_info.variant_info = instance_container_info_dict[variant_id]
|
||||
if variant_id in instance_container_info_dict:
|
||||
extruder_info.variant_info = instance_container_info_dict[variant_id]
|
||||
|
||||
if material_id not in ("empty", "empty_material"):
|
||||
root_material_id = reverse_material_id_dict[material_id]
|
||||
extruder_info.root_material_id = root_material_id
|
||||
|
||||
definition_changes_id = parser["containers"][str(_ContainerIndexes.DefinitionChanges)]
|
||||
if definition_changes_id not in ("empty", "empty_definition_changes"):
|
||||
extruder_info.definition_changes_info = instance_container_info_dict[definition_changes_id]
|
||||
|
||||
user_changes_id = parser["containers"][str(_ContainerIndexes.UserChanges)]
|
||||
if user_changes_id not in ("empty", "empty_user_changes"):
|
||||
extruder_info.user_changes_info = instance_container_info_dict[user_changes_id]
|
||||
@ -515,7 +528,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)
|
||||
@ -648,6 +661,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
definition_container = self._container_registry.findDefinitionContainers(id = "fdmprinter")[0] #Fall back to defaults.
|
||||
self._container_registry.addContainer(definition_container)
|
||||
Job.yieldThread()
|
||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||
|
||||
Logger.log("d", "Workspace loading is checking materials...")
|
||||
# Get all the material files and check if they exist. If not, add them.
|
||||
@ -697,6 +711,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
material_container.setDirty(True)
|
||||
self._container_registry.addContainer(material_container)
|
||||
Job.yieldThread()
|
||||
QCoreApplication.processEvents() # Ensure that the GUI does not freeze.
|
||||
|
||||
# Handle quality changes if any
|
||||
self._processQualityChanges(global_stack)
|
||||
@ -820,13 +835,16 @@ 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)
|
||||
|
||||
self._machine_info.quality_changes_info.name = quality_changes_name
|
||||
|
||||
def _clearStack(self, stack):
|
||||
@staticmethod
|
||||
def _clearStack(stack):
|
||||
application = CuraApplication.getInstance()
|
||||
|
||||
stack.definitionChanges.clear()
|
||||
@ -903,6 +921,10 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
continue
|
||||
extruder_info = self._machine_info.extruder_info_dict[position]
|
||||
if extruder_info.variant_info is None:
|
||||
# If there is no variant_info, try to use the default variant. Otherwise, leave it be.
|
||||
node = variant_manager.getDefaultVariantNode(global_stack.definition, VariantType.NOZZLE, global_stack)
|
||||
if node is not None and node.getContainer() is not None:
|
||||
extruder_stack.variant = node.getContainer()
|
||||
continue
|
||||
parser = extruder_info.variant_info.parser
|
||||
|
||||
@ -971,6 +993,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
extruder_stack.setMetaDataEntry("enabled", "True")
|
||||
extruder_stack.setMetaDataEntry("enabled", str(extruder_info.enabled))
|
||||
|
||||
# Set metadata fields that are missing from the global stack
|
||||
for key, value in self._machine_info.metadata_dict.items():
|
||||
if key not in global_stack.getMetaData():
|
||||
global_stack.setMetaDataEntry(key, value)
|
||||
|
||||
def _updateActiveMachine(self, global_stack):
|
||||
# Actually change the active machine.
|
||||
machine_manager = Application.getInstance().getMachineManager()
|
||||
@ -983,6 +1010,11 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
|
||||
machine_manager.setActiveMachine(global_stack.getId())
|
||||
|
||||
# Set metadata fields that are missing from the global stack
|
||||
for key, value in self._machine_info.metadata_dict.items():
|
||||
if key not in global_stack.getMetaData():
|
||||
global_stack.setMetaDataEntry(key, value)
|
||||
|
||||
if self._quality_changes_to_apply:
|
||||
quality_changes_group_dict = quality_manager.getQualityChangesGroups(global_stack)
|
||||
if self._quality_changes_to_apply not in quality_changes_group_dict:
|
||||
@ -1009,7 +1041,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
# Notify everything/one that is to notify about changes.
|
||||
global_stack.containersChanged.emit(global_stack.getTop())
|
||||
|
||||
def _stripFileToId(self, file):
|
||||
@staticmethod
|
||||
def _stripFileToId(file):
|
||||
mime_type = MimeTypeDatabase.getMimeTypeForFile(file)
|
||||
file = mime_type.stripExtension(file)
|
||||
return file.replace("Cura/", "")
|
||||
@ -1018,7 +1051,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
return self._container_registry.getContainerForMimeType(MimeTypeDatabase.getMimeType("application/x-ultimaker-material-profile"))
|
||||
|
||||
## Get the list of ID's of all containers in a container stack by partially parsing it's serialized data.
|
||||
def _getContainerIdListFromSerialized(self, serialized):
|
||||
@staticmethod
|
||||
def _getContainerIdListFromSerialized(serialized):
|
||||
parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
|
||||
parser.read_string(serialized)
|
||||
|
||||
@ -1039,12 +1073,20 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||
|
||||
return container_ids
|
||||
|
||||
def _getMachineNameFromSerializedStack(self, serialized):
|
||||
@staticmethod
|
||||
def _getMachineNameFromSerializedStack(serialized):
|
||||
parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
|
||||
parser.read_string(serialized)
|
||||
return parser["general"].get("name", "")
|
||||
|
||||
def _getMaterialLabelFromSerialized(self, serialized):
|
||||
@staticmethod
|
||||
def _getMetaDataDictFromSerializedStack(serialized: str) -> Dict[str, str]:
|
||||
parser = ConfigParser(interpolation = None, empty_lines_in_values = False)
|
||||
parser.read_string(serialized)
|
||||
return dict(parser["metadata"])
|
||||
|
||||
@staticmethod
|
||||
def _getMaterialLabelFromSerialized(serialized):
|
||||
data = ET.fromstring(serialized)
|
||||
metadata = data.iterfind("./um:metadata/um:name/um:label", {"um": "http://www.ultimaker.com/material"})
|
||||
for entry in metadata:
|
||||
|
@ -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)
|
||||
|
173
plugins/AMFReader/AMFReader.py
Normal file
173
plugins/AMFReader/AMFReader.py
Normal file
@ -0,0 +1,173 @@
|
||||
# Copyright (c) 2019 fieldOfView
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
# This AMF parser is based on the AMF parser in legacy cura:
|
||||
# https://github.com/daid/LegacyCura/blob/ad7641e059048c7dcb25da1f47c0a7e95e7f4f7c/Cura/util/meshLoaders/amf.py
|
||||
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
|
||||
from cura.CuraApplication import CuraApplication
|
||||
from UM.Logger import Logger
|
||||
|
||||
from UM.Mesh.MeshData import MeshData, calculateNormalsFromIndexedVertices
|
||||
from UM.Mesh.MeshReader import MeshReader
|
||||
|
||||
from cura.Scene.CuraSceneNode import CuraSceneNode
|
||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||
from cura.Scene.BuildPlateDecorator import BuildPlateDecorator
|
||||
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
||||
from UM.Scene.GroupDecorator import GroupDecorator
|
||||
|
||||
import numpy
|
||||
import trimesh
|
||||
import os.path
|
||||
import zipfile
|
||||
|
||||
MYPY = False
|
||||
try:
|
||||
if not MYPY:
|
||||
import xml.etree.cElementTree as ET
|
||||
except ImportError:
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class AMFReader(MeshReader):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._supported_extensions = [".amf"]
|
||||
self._namespaces = {} # type: Dict[str, str]
|
||||
|
||||
MimeTypeDatabase.addMimeType(
|
||||
MimeType(
|
||||
name="application/x-amf",
|
||||
comment="AMF",
|
||||
suffixes=["amf"]
|
||||
)
|
||||
)
|
||||
|
||||
# Main entry point
|
||||
# Reads the file, returns a SceneNode (possibly with nested ones), or None
|
||||
def _read(self, file_name):
|
||||
base_name = os.path.basename(file_name)
|
||||
try:
|
||||
zipped_file = zipfile.ZipFile(file_name)
|
||||
xml_document = zipped_file.read(zipped_file.namelist()[0])
|
||||
zipped_file.close()
|
||||
except zipfile.BadZipfile:
|
||||
raw_file = open(file_name, "r")
|
||||
xml_document = raw_file.read()
|
||||
raw_file.close()
|
||||
|
||||
try:
|
||||
amf_document = ET.fromstring(xml_document)
|
||||
except ET.ParseError:
|
||||
Logger.log("e", "Could not parse XML in file %s" % base_name)
|
||||
return None
|
||||
|
||||
if "unit" in amf_document.attrib:
|
||||
unit = amf_document.attrib["unit"].lower()
|
||||
else:
|
||||
unit = "millimeter"
|
||||
if unit == "millimeter":
|
||||
scale = 1.0
|
||||
elif unit == "meter":
|
||||
scale = 1000.0
|
||||
elif unit == "inch":
|
||||
scale = 25.4
|
||||
elif unit == "feet":
|
||||
scale = 304.8
|
||||
elif unit == "micron":
|
||||
scale = 0.001
|
||||
else:
|
||||
Logger.log("w", "Unknown unit in amf: %s. Using mm instead." % unit)
|
||||
scale = 1.0
|
||||
|
||||
nodes = []
|
||||
for amf_object in amf_document.iter("object"):
|
||||
for amf_mesh in amf_object.iter("mesh"):
|
||||
amf_mesh_vertices = []
|
||||
for vertices in amf_mesh.iter("vertices"):
|
||||
for vertex in vertices.iter("vertex"):
|
||||
for coordinates in vertex.iter("coordinates"):
|
||||
v = [0.0, 0.0, 0.0]
|
||||
for t in coordinates:
|
||||
if t.tag == "x":
|
||||
v[0] = float(t.text) * scale
|
||||
elif t.tag == "y":
|
||||
v[2] = float(t.text) * scale
|
||||
elif t.tag == "z":
|
||||
v[1] = float(t.text) * scale
|
||||
amf_mesh_vertices.append(v)
|
||||
if not amf_mesh_vertices:
|
||||
continue
|
||||
|
||||
indices = []
|
||||
for volume in amf_mesh.iter("volume"):
|
||||
for triangle in volume.iter("triangle"):
|
||||
f = [0, 0, 0]
|
||||
for t in triangle:
|
||||
if t.tag == "v1":
|
||||
f[0] = int(t.text)
|
||||
elif t.tag == "v2":
|
||||
f[1] = int(t.text)
|
||||
elif t.tag == "v3":
|
||||
f[2] = int(t.text)
|
||||
indices.append(f)
|
||||
|
||||
mesh = trimesh.base.Trimesh(vertices=numpy.array(amf_mesh_vertices, dtype=numpy.float32), faces=numpy.array(indices, dtype=numpy.int32))
|
||||
mesh.merge_vertices()
|
||||
mesh.remove_unreferenced_vertices()
|
||||
mesh.fix_normals()
|
||||
mesh_data = self._toMeshData(mesh)
|
||||
|
||||
new_node = CuraSceneNode()
|
||||
new_node.setSelectable(True)
|
||||
new_node.setMeshData(mesh_data)
|
||||
new_node.setName(base_name if len(nodes)==0 else "%s %d" % (base_name, len(nodes)))
|
||||
new_node.addDecorator(BuildPlateDecorator(CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate))
|
||||
new_node.addDecorator(SliceableObjectDecorator())
|
||||
|
||||
nodes.append(new_node)
|
||||
|
||||
if not nodes:
|
||||
Logger.log("e", "No meshes in file %s" % base_name)
|
||||
return None
|
||||
|
||||
if len(nodes) == 1:
|
||||
return nodes[0]
|
||||
|
||||
# Add all scenenodes to a group so they stay together
|
||||
group_node = CuraSceneNode()
|
||||
group_node.addDecorator(GroupDecorator())
|
||||
group_node.addDecorator(ConvexHullDecorator())
|
||||
group_node.addDecorator(BuildPlateDecorator(CuraApplication.getInstance().getMultiBuildPlateModel().activeBuildPlate))
|
||||
|
||||
for node in nodes:
|
||||
node.setParent(group_node)
|
||||
|
||||
return group_node
|
||||
|
||||
def _toMeshData(self, tri_node: trimesh.base.Trimesh) -> MeshData:
|
||||
tri_faces = tri_node.faces
|
||||
tri_vertices = tri_node.vertices
|
||||
|
||||
indices = []
|
||||
vertices = []
|
||||
|
||||
index_count = 0
|
||||
face_count = 0
|
||||
for tri_face in tri_faces:
|
||||
face = []
|
||||
for tri_index in tri_face:
|
||||
vertices.append(tri_vertices[tri_index])
|
||||
face.append(index_count)
|
||||
index_count += 1
|
||||
indices.append(face)
|
||||
face_count += 1
|
||||
|
||||
vertices = numpy.asarray(vertices, dtype=numpy.float32)
|
||||
indices = numpy.asarray(indices, dtype=numpy.int32)
|
||||
normals = calculateNormalsFromIndexedVertices(vertices, indices, face_count)
|
||||
|
||||
mesh_data = MeshData(vertices=vertices, indices=indices, normals=normals)
|
||||
return mesh_data
|
21
plugins/AMFReader/__init__.py
Normal file
21
plugins/AMFReader/__init__.py
Normal file
@ -0,0 +1,21 @@
|
||||
# Copyright (c) 2019 fieldOfView
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from . import AMFReader
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
i18n_catalog = i18nCatalog("uranium")
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {
|
||||
"mesh_reader": [
|
||||
{
|
||||
"extension": "amf",
|
||||
"description": i18n_catalog.i18nc("@item:inlistbox", "AMF File")
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
def register(app):
|
||||
return {"mesh_reader": AMFReader.AMFReader()}
|
7
plugins/AMFReader/plugin.json
Normal file
7
plugins/AMFReader/plugin.json
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "AMF Reader",
|
||||
"author": "fieldOfView",
|
||||
"version": "1.0.0",
|
||||
"description": "Provides support for reading AMF files.",
|
||||
"api": "6.0.0"
|
||||
}
|
@ -1,109 +0,0 @@
|
||||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from UM.i18n import i18nCatalog
|
||||
from UM.Extension import Extension
|
||||
from UM.Application import Application
|
||||
from UM.PluginRegistry import PluginRegistry
|
||||
from UM.Version import Version
|
||||
|
||||
from PyQt5.QtCore import pyqtSlot, QObject
|
||||
|
||||
import os.path
|
||||
import collections
|
||||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
class ChangeLog(Extension, QObject,):
|
||||
def __init__(self, parent = None):
|
||||
QObject.__init__(self, parent)
|
||||
Extension.__init__(self)
|
||||
self._changelog_window = None
|
||||
self._changelog_context = None
|
||||
version_string = Application.getInstance().getVersion()
|
||||
if version_string is not "master":
|
||||
self._current_app_version = Version(version_string)
|
||||
else:
|
||||
self._current_app_version = None
|
||||
|
||||
self._change_logs = None
|
||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||
Application.getInstance().getPreferences().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium
|
||||
self.setMenuName(catalog.i18nc("@item:inmenu", "Changelog"))
|
||||
self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog)
|
||||
|
||||
def getChangeLogs(self):
|
||||
if not self._change_logs:
|
||||
self.loadChangeLogs()
|
||||
return self._change_logs
|
||||
|
||||
@pyqtSlot(result = str)
|
||||
def getChangeLogString(self):
|
||||
logs = self.getChangeLogs()
|
||||
result = ""
|
||||
for version in logs:
|
||||
result += "<h1>" + str(version) + "</h1><br>"
|
||||
result += ""
|
||||
for change in logs[version]:
|
||||
if str(change) != "":
|
||||
result += "<b>" + str(change) + "</b><br>"
|
||||
for line in logs[version][change]:
|
||||
result += str(line) + "<br>"
|
||||
result += "<br>"
|
||||
|
||||
pass
|
||||
return result
|
||||
|
||||
def loadChangeLogs(self):
|
||||
self._change_logs = collections.OrderedDict()
|
||||
with open(os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.txt"), "r", encoding = "utf-8") as f:
|
||||
open_version = None
|
||||
open_header = "" # Initialise to an empty header in case there is no "*" in the first line of the changelog
|
||||
for line in f:
|
||||
line = line.replace("\n","")
|
||||
if "[" in line and "]" in line:
|
||||
line = line.replace("[","")
|
||||
line = line.replace("]","")
|
||||
open_version = Version(line)
|
||||
open_header = ""
|
||||
self._change_logs[open_version] = collections.OrderedDict()
|
||||
elif line.startswith("*"):
|
||||
open_header = line.replace("*","")
|
||||
self._change_logs[open_version][open_header] = []
|
||||
elif line != "":
|
||||
if open_header not in self._change_logs[open_version]:
|
||||
self._change_logs[open_version][open_header] = []
|
||||
self._change_logs[open_version][open_header].append(line)
|
||||
|
||||
def _onEngineCreated(self):
|
||||
if not self._current_app_version:
|
||||
return #We're on dev branch.
|
||||
|
||||
if Application.getInstance().getPreferences().getValue("general/latest_version_changelog_shown") == "master":
|
||||
latest_version_shown = Version("0.0.0")
|
||||
else:
|
||||
latest_version_shown = Version(Application.getInstance().getPreferences().getValue("general/latest_version_changelog_shown"))
|
||||
|
||||
Application.getInstance().getPreferences().setValue("general/latest_version_changelog_shown", Application.getInstance().getVersion())
|
||||
|
||||
# Do not show the changelog when there is no global container stack
|
||||
# This implies we are running Cura for the first time.
|
||||
if not Application.getInstance().getGlobalContainerStack():
|
||||
return
|
||||
|
||||
if self._current_app_version > latest_version_shown:
|
||||
self.showChangelog()
|
||||
|
||||
def showChangelog(self):
|
||||
if not self._changelog_window:
|
||||
self.createChangelogWindow()
|
||||
|
||||
self._changelog_window.show()
|
||||
|
||||
def hideChangelog(self):
|
||||
if self._changelog_window:
|
||||
self._changelog_window.hide()
|
||||
|
||||
def createChangelogWindow(self):
|
||||
path = os.path.join(PluginRegistry.getInstance().getPluginPath(self.getPluginId()), "ChangeLog.qml")
|
||||
self._changelog_window = Application.getInstance().createQmlComponent(path, {"manager": self})
|
@ -1,41 +0,0 @@
|
||||
// Copyright (c) 2015 Ultimaker B.V.
|
||||
// Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
import QtQuick 2.1
|
||||
import QtQuick.Controls 1.3
|
||||
import QtQuick.Layouts 1.1
|
||||
import QtQuick.Window 2.1
|
||||
|
||||
import UM 1.1 as UM
|
||||
|
||||
UM.Dialog
|
||||
{
|
||||
id: base
|
||||
minimumWidth: (UM.Theme.getSize("modal_window_minimum").width * 0.75) | 0
|
||||
minimumHeight: (UM.Theme.getSize("modal_window_minimum").height * 0.75) | 0
|
||||
width: minimumWidth
|
||||
height: minimumHeight
|
||||
title: catalog.i18nc("@label", "Changelog")
|
||||
|
||||
TextArea
|
||||
{
|
||||
anchors.fill: parent
|
||||
text: manager.getChangeLogString()
|
||||
readOnly: true;
|
||||
textFormat: TextEdit.RichText
|
||||
}
|
||||
|
||||
rightButtons: [
|
||||
Button
|
||||
{
|
||||
UM.I18nCatalog
|
||||
{
|
||||
id: catalog
|
||||
name: "cura"
|
||||
}
|
||||
|
||||
text: catalog.i18nc("@action:button", "Close")
|
||||
onClicked: base.hide()
|
||||
}
|
||||
]
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
# Copyright (c) 2015 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from . import ChangeLog
|
||||
|
||||
|
||||
def getMetaData():
|
||||
return {}
|
||||
|
||||
def register(app):
|
||||
return {"extension": ChangeLog.ChangeLog()}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user