mirror of
https://github.com/Ultimaker/Cura.git
synced 2025-07-07 15:07:28 -06:00
Merge branch 'master' into feature_curaversion_appname
# Conflicts: # cura/CuraApplication.py
This commit is contained in:
commit
056655e584
1147 changed files with 62054 additions and 12466 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -42,7 +42,6 @@ plugins/cura-siemensnx-plugin
|
||||||
plugins/CuraBlenderPlugin
|
plugins/CuraBlenderPlugin
|
||||||
plugins/CuraCloudPlugin
|
plugins/CuraCloudPlugin
|
||||||
plugins/CuraDrivePlugin
|
plugins/CuraDrivePlugin
|
||||||
plugins/CuraDrive
|
|
||||||
plugins/CuraLiveScriptingPlugin
|
plugins/CuraLiveScriptingPlugin
|
||||||
plugins/CuraOpenSCADPlugin
|
plugins/CuraOpenSCADPlugin
|
||||||
plugins/CuraPrintProfileCreator
|
plugins/CuraPrintProfileCreator
|
||||||
|
|
43
Jenkinsfile
vendored
43
Jenkinsfile
vendored
|
@ -38,20 +38,9 @@ parallel_nodes(['linux && cura', 'windows && cura'])
|
||||||
{
|
{
|
||||||
if (isUnix())
|
if (isUnix())
|
||||||
{
|
{
|
||||||
// For Linux to show everything
|
// For Linux
|
||||||
def branch = env.BRANCH_NAME
|
|
||||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
|
|
||||||
{
|
|
||||||
branch = "master"
|
|
||||||
}
|
|
||||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
sh """
|
sh 'make CTEST_OUTPUT_ON_FAILURE=TRUE test'
|
||||||
cd ..
|
|
||||||
export PYTHONPATH=.:"${uranium_dir}"
|
|
||||||
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/pytest -x --verbose --full-trace --capture=no ./tests
|
|
||||||
"""
|
|
||||||
} catch(e)
|
} catch(e)
|
||||||
{
|
{
|
||||||
currentBuild.result = "UNSTABLE"
|
currentBuild.result = "UNSTABLE"
|
||||||
|
@ -70,34 +59,6 @@ parallel_nodes(['linux && cura', 'windows && cura'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
stage('Code Style')
|
|
||||||
{
|
|
||||||
if (isUnix())
|
|
||||||
{
|
|
||||||
// For Linux to show everything.
|
|
||||||
// CMake also runs this test, but if it fails then the test just shows "failed" without details of what exactly failed.
|
|
||||||
def branch = env.BRANCH_NAME
|
|
||||||
if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
|
|
||||||
{
|
|
||||||
branch = "master"
|
|
||||||
}
|
|
||||||
def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
sh """
|
|
||||||
cd ..
|
|
||||||
export PYTHONPATH=.:"${uranium_dir}"
|
|
||||||
${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/python3 run_mypy.py
|
|
||||||
"""
|
|
||||||
}
|
|
||||||
catch(e)
|
|
||||||
{
|
|
||||||
currentBuild.result = "UNSTABLE"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,8 +20,9 @@ Dependencies
|
||||||
------------
|
------------
|
||||||
* [Uranium](https://github.com/Ultimaker/Uranium) Cura is built on top of the Uranium framework.
|
* [Uranium](https://github.com/Ultimaker/Uranium) Cura is built on top of the Uranium framework.
|
||||||
* [CuraEngine](https://github.com/Ultimaker/CuraEngine) This will be needed at runtime to perform the actual slicing.
|
* [CuraEngine](https://github.com/Ultimaker/CuraEngine) This will be needed at runtime to perform the actual slicing.
|
||||||
|
* [fdm_materials](https://github.com/Ultimaker/fdm_materials) Required to load a printer that has swappable material profiles.
|
||||||
* [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support.
|
* [PySerial](https://github.com/pyserial/pyserial) Only required for USB printing support.
|
||||||
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers
|
* [python-zeroconf](https://github.com/jstasiak/python-zeroconf) Only required to detect mDNS-enabled printers.
|
||||||
|
|
||||||
Build scripts
|
Build scripts
|
||||||
-------------
|
-------------
|
||||||
|
|
|
@ -6,6 +6,8 @@ include(CMakeParseArguments)
|
||||||
|
|
||||||
find_package(PythonInterp 3.5.0 REQUIRED)
|
find_package(PythonInterp 3.5.0 REQUIRED)
|
||||||
|
|
||||||
|
add_custom_target(test-verbose COMMAND ${CMAKE_CTEST_COMMAND} --verbose)
|
||||||
|
|
||||||
function(cura_add_test)
|
function(cura_add_test)
|
||||||
set(_single_args NAME DIRECTORY PYTHONPATH)
|
set(_single_args NAME DIRECTORY PYTHONPATH)
|
||||||
cmake_parse_arguments("" "" "${_single_args}" "" ${ARGN})
|
cmake_parse_arguments("" "" "${_single_args}" "" ${ARGN})
|
||||||
|
|
|
@ -13,6 +13,6 @@ TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
|
||||||
Icon=cura-icon
|
Icon=cura-icon
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;
|
MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;text/x-gcode;
|
||||||
Categories=Graphics;
|
Categories=Graphics;
|
||||||
Keywords=3D;Printing;Slicer;
|
Keywords=3D;Printing;Slicer;
|
||||||
|
|
|
@ -19,4 +19,12 @@
|
||||||
<glob-deleteall/>
|
<glob-deleteall/>
|
||||||
<glob pattern="*.obj"/>
|
<glob pattern="*.obj"/>
|
||||||
</mime-type>
|
</mime-type>
|
||||||
|
<mime-type type="text/x-gcode">
|
||||||
|
<sub-class-of type="text/plain"/>
|
||||||
|
<comment>Gcode file</comment>
|
||||||
|
<icon name="unknown"/>
|
||||||
|
<glob-deleteall/>
|
||||||
|
<glob pattern="*.gcode"/>
|
||||||
|
<glob pattern="*.g"/>
|
||||||
|
</mime-type>
|
||||||
</mime-info>
|
</mime-info>
|
|
@ -6,6 +6,7 @@ from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.Message import Message
|
from UM.Message import Message
|
||||||
|
from cura import UltimakerCloudAuthentication
|
||||||
|
|
||||||
from cura.OAuth2.AuthorizationService import AuthorizationService
|
from cura.OAuth2.AuthorizationService import AuthorizationService
|
||||||
from cura.OAuth2.Models import OAuth2Settings
|
from cura.OAuth2.Models import OAuth2Settings
|
||||||
|
@ -37,15 +38,16 @@ class Account(QObject):
|
||||||
self._logged_in = False
|
self._logged_in = False
|
||||||
|
|
||||||
self._callback_port = 32118
|
self._callback_port = 32118
|
||||||
self._oauth_root = "https://account.ultimaker.com"
|
self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
|
||||||
self._cloud_api_root = "https://api.ultimaker.com"
|
|
||||||
|
|
||||||
self._oauth_settings = OAuth2Settings(
|
self._oauth_settings = OAuth2Settings(
|
||||||
OAUTH_SERVER_URL= self._oauth_root,
|
OAUTH_SERVER_URL= self._oauth_root,
|
||||||
CALLBACK_PORT=self._callback_port,
|
CALLBACK_PORT=self._callback_port,
|
||||||
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
|
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
|
||||||
CLIENT_ID="um---------------ultimaker_cura_drive_plugin",
|
CLIENT_ID="um----------------------------ultimaker_cura",
|
||||||
CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.write",
|
CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download "
|
||||||
|
"packages.rating.read packages.rating.write connect.cluster.read connect.cluster.write "
|
||||||
|
"cura.printjob.read cura.printjob.write cura.mesh.read cura.mesh.write",
|
||||||
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
|
AUTH_DATA_PREFERENCE_KEY="general/ultimaker_auth_data",
|
||||||
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
|
AUTH_SUCCESS_REDIRECT="{}/app/auth-success".format(self._oauth_root),
|
||||||
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
|
AUTH_FAILED_REDIRECT="{}/app/auth-error".format(self._oauth_root)
|
||||||
|
@ -60,6 +62,11 @@ class Account(QObject):
|
||||||
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
|
self._authorization_service.onAuthenticationError.connect(self._onLoginStateChanged)
|
||||||
self._authorization_service.loadAuthDataFromPreferences()
|
self._authorization_service.loadAuthDataFromPreferences()
|
||||||
|
|
||||||
|
## Returns a boolean indicating whether the given authentication is applied against staging or not.
|
||||||
|
@property
|
||||||
|
def is_staging(self) -> bool:
|
||||||
|
return "staging" in self._oauth_root
|
||||||
|
|
||||||
@pyqtProperty(bool, notify=loginStateChanged)
|
@pyqtProperty(bool, notify=loginStateChanged)
|
||||||
def isLoggedIn(self) -> bool:
|
def isLoggedIn(self) -> bool:
|
||||||
return self._logged_in
|
return self._logged_in
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
from typing import Tuple, Optional, TYPE_CHECKING
|
from typing import Tuple, Optional, TYPE_CHECKING, Dict, Any
|
||||||
|
|
||||||
from cura.Backups.BackupsManager import BackupsManager
|
from cura.Backups.BackupsManager import BackupsManager
|
||||||
|
|
||||||
|
@ -24,12 +24,12 @@ class Backups:
|
||||||
## Create a new back-up using the BackupsManager.
|
## Create a new back-up using the BackupsManager.
|
||||||
# \return Tuple containing a ZIP file with the back-up data and a dict
|
# \return Tuple containing a ZIP file with the back-up data and a dict
|
||||||
# with metadata about the back-up.
|
# with metadata about the back-up.
|
||||||
def createBackup(self) -> Tuple[Optional[bytes], Optional[dict]]:
|
def createBackup(self) -> Tuple[Optional[bytes], Optional[Dict[str, Any]]]:
|
||||||
return self.manager.createBackup()
|
return self.manager.createBackup()
|
||||||
|
|
||||||
## Restore a back-up using the BackupsManager.
|
## Restore a back-up using the BackupsManager.
|
||||||
# \param zip_file A ZIP file containing the actual back-up data.
|
# \param zip_file A ZIP file containing the actual back-up data.
|
||||||
# \param meta_data Some metadata needed for restoring a back-up, like the
|
# \param meta_data Some metadata needed for restoring a back-up, like the
|
||||||
# Cura version number.
|
# Cura version number.
|
||||||
def restoreBackup(self, zip_file: bytes, meta_data: dict) -> None:
|
def restoreBackup(self, zip_file: bytes, meta_data: Dict[str, Any]) -> None:
|
||||||
return self.manager.restoreBackup(zip_file, meta_data)
|
return self.manager.restoreBackup(zip_file, meta_data)
|
||||||
|
|
50
cura/ApplicationMetadata.py
Normal file
50
cura/ApplicationMetadata.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
# ---------
|
||||||
|
# Genearl constants used in Cura
|
||||||
|
# ---------
|
||||||
|
DEFAUKT_CURA_APP_NAME = "cura"
|
||||||
|
DEFAULT_CURA_DISPLAY_NAME = "Ultimaker Cura"
|
||||||
|
DEFAULT_CURA_VERSION = "master"
|
||||||
|
DEFAULT_CURA_BUILD_TYPE = ""
|
||||||
|
DEFAULT_CURA_DEBUG_MODE = False
|
||||||
|
DEFAULT_CURA_SDK_VERSION = "6.0.0"
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraAppName # type: ignore
|
||||||
|
if CuraAppName == "":
|
||||||
|
CuraAppName = DEFAULT_CURA_APP_NAME
|
||||||
|
except ImportError:
|
||||||
|
CuraAppName = DEFAULT_CURA_APP_NAME
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraAppDisplayName # type: ignore
|
||||||
|
if CuraAppDisplayName == "":
|
||||||
|
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
|
||||||
|
except ImportError:
|
||||||
|
CuraAppDisplayName = DEFAULT_CURA_DISPLAY_NAME
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraVersion # type: ignore
|
||||||
|
if CuraVersion == "":
|
||||||
|
CuraVersion = DEFAULT_CURA_VERSION
|
||||||
|
except ImportError:
|
||||||
|
CuraVersion = DEFAULT_CURA_VERSION # [CodeStyle: Reflecting imported value]
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraBuildType # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
CuraBuildType = DEFAULT_CURA_BUILD_TYPE
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraDebugMode # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
CuraDebugMode = DEFAULT_CURA_DEBUG_MODE
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraSDKVersion # type: ignore
|
||||||
|
if CuraSDKVersion == "":
|
||||||
|
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION
|
||||||
|
except ImportError:
|
||||||
|
CuraSDKVersion = DEFAULT_CURA_SDK_VERSION
|
|
@ -66,6 +66,11 @@ class Arrange:
|
||||||
continue
|
continue
|
||||||
vertices = vertices.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
|
vertices = vertices.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
|
||||||
points = copy.deepcopy(vertices._points)
|
points = copy.deepcopy(vertices._points)
|
||||||
|
|
||||||
|
# After scaling (like up to 0.1 mm) the node might not have points
|
||||||
|
if len(points) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
shape_arr = ShapeArray.fromPolygon(points, scale = scale)
|
||||||
arranger.place(0, 0, shape_arr)
|
arranger.place(0, 0, shape_arr)
|
||||||
|
|
||||||
|
|
|
@ -46,12 +46,13 @@ class Backup:
|
||||||
|
|
||||||
# We copy the preferences file to the user data directory in Linux as it's in a different location there.
|
# We copy the preferences file to the user data directory in Linux as it's in a different location there.
|
||||||
# When restoring a backup on Linux, we move it back.
|
# When restoring a backup on Linux, we move it back.
|
||||||
if Platform.isLinux():
|
if Platform.isLinux(): #TODO: This should check for the config directory not being the same as the data directory, rather than hard-coding that to Linux systems.
|
||||||
preferences_file_name = self._application.getApplicationName()
|
preferences_file_name = self._application.getApplicationName()
|
||||||
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
|
preferences_file = Resources.getPath(Resources.Preferences, "{}.cfg".format(preferences_file_name))
|
||||||
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
|
backup_preferences_file = os.path.join(version_data_dir, "{}.cfg".format(preferences_file_name))
|
||||||
Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
|
if os.path.exists(preferences_file) and (not os.path.exists(backup_preferences_file) or not os.path.samefile(preferences_file, backup_preferences_file)):
|
||||||
shutil.copyfile(preferences_file, backup_preferences_file)
|
Logger.log("d", "Copying preferences file from %s to %s", preferences_file, backup_preferences_file)
|
||||||
|
shutil.copyfile(preferences_file, backup_preferences_file)
|
||||||
|
|
||||||
# Create an empty buffer and write the archive to it.
|
# Create an empty buffer and write the archive to it.
|
||||||
buffer = io.BytesIO()
|
buffer = io.BytesIO()
|
||||||
|
|
|
@ -83,7 +83,14 @@ class BuildVolume(SceneNode):
|
||||||
" with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
|
" with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
|
||||||
|
|
||||||
self._global_container_stack = None
|
self._global_container_stack = None
|
||||||
|
|
||||||
|
self._stack_change_timer = QTimer()
|
||||||
|
self._stack_change_timer.setInterval(100)
|
||||||
|
self._stack_change_timer.setSingleShot(True)
|
||||||
|
self._stack_change_timer.timeout.connect(self._onStackChangeTimerFinished)
|
||||||
|
|
||||||
self._application.globalContainerStackChanged.connect(self._onStackChanged)
|
self._application.globalContainerStackChanged.connect(self._onStackChanged)
|
||||||
|
|
||||||
self._onStackChanged()
|
self._onStackChanged()
|
||||||
|
|
||||||
self._engine_ready = False
|
self._engine_ready = False
|
||||||
|
@ -105,6 +112,8 @@ class BuildVolume(SceneNode):
|
||||||
self._setting_change_timer.setSingleShot(True)
|
self._setting_change_timer.setSingleShot(True)
|
||||||
self._setting_change_timer.timeout.connect(self._onSettingChangeTimerFinished)
|
self._setting_change_timer.timeout.connect(self._onSettingChangeTimerFinished)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Must be after setting _build_volume_message, apparently that is used in getMachineManager.
|
# Must be after setting _build_volume_message, apparently that is used in getMachineManager.
|
||||||
# activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
|
# activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
|
||||||
# Therefore this works.
|
# Therefore this works.
|
||||||
|
@ -479,6 +488,8 @@ class BuildVolume(SceneNode):
|
||||||
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
|
maximum = Vector(max_w - bed_adhesion_size - 1, max_h - self._raft_thickness - self._extra_z_clearance, max_d - disallowed_area_size + bed_adhesion_size - 1)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds
|
||||||
|
|
||||||
self.updateNodeBoundaryCheck()
|
self.updateNodeBoundaryCheck()
|
||||||
|
|
||||||
def getBoundingBox(self) -> AxisAlignedBox:
|
def getBoundingBox(self) -> AxisAlignedBox:
|
||||||
|
@ -489,7 +500,9 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
def _updateRaftThickness(self):
|
def _updateRaftThickness(self):
|
||||||
old_raft_thickness = self._raft_thickness
|
old_raft_thickness = self._raft_thickness
|
||||||
self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value")
|
if self._global_container_stack.extruders:
|
||||||
|
# This might be called before the extruder stacks have initialised, in which case getting the adhesion_type fails
|
||||||
|
self._adhesion_type = self._global_container_stack.getProperty("adhesion_type", "value")
|
||||||
self._raft_thickness = 0.0
|
self._raft_thickness = 0.0
|
||||||
if self._adhesion_type == "raft":
|
if self._adhesion_type == "raft":
|
||||||
self._raft_thickness = (
|
self._raft_thickness = (
|
||||||
|
@ -522,8 +535,11 @@ class BuildVolume(SceneNode):
|
||||||
if extra_z != self._extra_z_clearance:
|
if extra_z != self._extra_z_clearance:
|
||||||
self._extra_z_clearance = extra_z
|
self._extra_z_clearance = extra_z
|
||||||
|
|
||||||
## Update the build volume visualization
|
|
||||||
def _onStackChanged(self):
|
def _onStackChanged(self):
|
||||||
|
self._stack_change_timer.start()
|
||||||
|
|
||||||
|
## Update the build volume visualization
|
||||||
|
def _onStackChangeTimerFinished(self):
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
|
||||||
extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
|
extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||||
|
|
|
@ -36,18 +36,14 @@ else:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
CuraDebugMode = False # [CodeStyle: Reflecting imported value]
|
CuraDebugMode = False # [CodeStyle: Reflecting imported value]
|
||||||
|
|
||||||
# List of exceptions that should be considered "fatal" and abort the program.
|
# List of exceptions that should not be considered "fatal" and abort the program.
|
||||||
# These are primarily some exception types that we simply cannot really recover from
|
# These are primarily some exception types that we simply skip
|
||||||
# (MemoryError and SystemError) and exceptions that indicate grave errors in the
|
skip_exception_types = [
|
||||||
# code that cause the Python interpreter to fail (SyntaxError, ImportError).
|
SystemExit,
|
||||||
fatal_exception_types = [
|
KeyboardInterrupt,
|
||||||
MemoryError,
|
GeneratorExit
|
||||||
SyntaxError,
|
|
||||||
ImportError,
|
|
||||||
SystemError,
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class CrashHandler:
|
class CrashHandler:
|
||||||
crash_url = "https://stats.ultimaker.com/api/cura"
|
crash_url = "https://stats.ultimaker.com/api/cura"
|
||||||
|
|
||||||
|
@ -70,7 +66,7 @@ class CrashHandler:
|
||||||
# If Cura has fully started, we only show fatal errors.
|
# If Cura has fully started, we only show fatal errors.
|
||||||
# If Cura has not fully started yet, we always show the early crash dialog. Otherwise, Cura will just crash
|
# If Cura has not fully started yet, we always show the early crash dialog. Otherwise, Cura will just crash
|
||||||
# without any information.
|
# without any information.
|
||||||
if has_started and exception_type not in fatal_exception_types:
|
if has_started and exception_type in skip_exception_types:
|
||||||
return
|
return
|
||||||
|
|
||||||
if not has_started:
|
if not has_started:
|
||||||
|
@ -387,7 +383,7 @@ class CrashHandler:
|
||||||
Application.getInstance().callLater(self._show)
|
Application.getInstance().callLater(self._show)
|
||||||
|
|
||||||
def _show(self):
|
def _show(self):
|
||||||
# When the exception is not in the fatal_exception_types list, the dialog is not created, so we don't need to show it
|
# When the exception is in the skip_exception_types list, the dialog is not created, so we don't need to show it
|
||||||
if self.dialog:
|
if self.dialog:
|
||||||
self.dialog.exec_()
|
self.dialog.exec_()
|
||||||
os._exit(1)
|
os._exit(1)
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, QUrl
|
from PyQt5.QtCore import QObject, QUrl
|
||||||
from PyQt5.QtGui import QDesktopServices
|
from PyQt5.QtGui import QDesktopServices
|
||||||
from typing import List, TYPE_CHECKING
|
from typing import List, TYPE_CHECKING, cast
|
||||||
|
|
||||||
from UM.Event import CallFunctionEvent
|
from UM.Event import CallFunctionEvent
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
@ -36,12 +36,12 @@ class CuraActions(QObject):
|
||||||
# Starting a web browser from a signal handler connected to a menu will crash on windows.
|
# Starting a web browser from a signal handler connected to a menu will crash on windows.
|
||||||
# So instead, defer the call to the next run of the event loop, since that does work.
|
# So instead, defer the call to the next run of the event loop, since that does work.
|
||||||
# Note that weirdly enough, only signal handlers that open a web browser fail like that.
|
# Note that weirdly enough, only signal handlers that open a web browser fail like that.
|
||||||
event = CallFunctionEvent(self._openUrl, [QUrl("http://ultimaker.com/en/support/software")], {})
|
event = CallFunctionEvent(self._openUrl, [QUrl("https://ultimaker.com/en/resources/manuals/software")], {})
|
||||||
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def openBugReportPage(self) -> None:
|
def openBugReportPage(self) -> None:
|
||||||
event = CallFunctionEvent(self._openUrl, [QUrl("http://github.com/Ultimaker/Cura/issues")], {})
|
event = CallFunctionEvent(self._openUrl, [QUrl("https://github.com/Ultimaker/Cura/issues")], {})
|
||||||
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
cura.CuraApplication.CuraApplication.getInstance().functionEvent(event)
|
||||||
|
|
||||||
## Reset camera position and direction to default
|
## Reset camera position and direction to default
|
||||||
|
@ -61,8 +61,10 @@ class CuraActions(QObject):
|
||||||
operation = GroupedOperation()
|
operation = GroupedOperation()
|
||||||
for node in Selection.getAllSelectedObjects():
|
for node in Selection.getAllSelectedObjects():
|
||||||
current_node = node
|
current_node = node
|
||||||
while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
|
parent_node = current_node.getParent()
|
||||||
current_node = current_node.getParent()
|
while parent_node and parent_node.callDecoration("isGroup"):
|
||||||
|
current_node = parent_node
|
||||||
|
parent_node = current_node.getParent()
|
||||||
|
|
||||||
# This was formerly done with SetTransformOperation but because of
|
# This was formerly done with SetTransformOperation but because of
|
||||||
# unpredictable matrix deconstruction it was possible that mirrors
|
# unpredictable matrix deconstruction it was possible that mirrors
|
||||||
|
@ -150,13 +152,13 @@ class CuraActions(QObject):
|
||||||
|
|
||||||
root = cura.CuraApplication.CuraApplication.getInstance().getController().getScene().getRoot()
|
root = cura.CuraApplication.CuraApplication.getInstance().getController().getScene().getRoot()
|
||||||
|
|
||||||
nodes_to_change = []
|
nodes_to_change = [] # type: List[SceneNode]
|
||||||
for node in Selection.getAllSelectedObjects():
|
for node in Selection.getAllSelectedObjects():
|
||||||
parent_node = node # Find the parent node to change instead
|
parent_node = node # Find the parent node to change instead
|
||||||
while parent_node.getParent() != root:
|
while parent_node.getParent() != root:
|
||||||
parent_node = parent_node.getParent()
|
parent_node = cast(SceneNode, parent_node.getParent())
|
||||||
|
|
||||||
for single_node in BreadthFirstIterator(parent_node): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for single_node in BreadthFirstIterator(parent_node): # type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
nodes_to_change.append(single_node)
|
nodes_to_change.append(single_node)
|
||||||
|
|
||||||
if not nodes_to_change:
|
if not nodes_to_change:
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from typing import cast, TYPE_CHECKING, Optional, Callable
|
from typing import cast, TYPE_CHECKING, Optional, Callable, List
|
||||||
|
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
|
@ -51,6 +51,7 @@ from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
|
||||||
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
|
||||||
from cura.Arranging.ShapeArray import ShapeArray
|
from cura.Arranging.ShapeArray import ShapeArray
|
||||||
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
from cura.MultiplyObjectsJob import MultiplyObjectsJob
|
||||||
|
from cura.GlobalStacksModel import GlobalStacksModel
|
||||||
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
|
||||||
from cura.Operations.SetParentOperation import SetParentOperation
|
from cura.Operations.SetParentOperation import SetParentOperation
|
||||||
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
|
||||||
|
@ -113,8 +114,11 @@ from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
|
||||||
|
|
||||||
from cura.ObjectsModel import ObjectsModel
|
from cura.ObjectsModel import ObjectsModel
|
||||||
|
|
||||||
|
from cura.PrinterOutputDevice import PrinterOutputDevice
|
||||||
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
|
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
|
||||||
|
|
||||||
|
from cura import ApplicationMetadata, UltimakerCloudAuthentication
|
||||||
|
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
|
|
||||||
|
@ -127,22 +131,12 @@ if TYPE_CHECKING:
|
||||||
|
|
||||||
numpy.seterr(all = "ignore")
|
numpy.seterr(all = "ignore")
|
||||||
|
|
||||||
try:
|
|
||||||
from cura.CuraVersion import CuraAppName, CuraAppDisplayName, CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion # type: ignore
|
|
||||||
except ImportError:
|
|
||||||
CuraAppName = "cura"
|
|
||||||
CuraAppDisplayName = "Ultimaker Cura"
|
|
||||||
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
|
|
||||||
CuraBuildType = ""
|
|
||||||
CuraDebugMode = False
|
|
||||||
CuraSDKVersion = "5.0.0"
|
|
||||||
|
|
||||||
|
|
||||||
class CuraApplication(QtApplication):
|
class CuraApplication(QtApplication):
|
||||||
# SettingVersion represents the set of settings available in the machine/extruder definitions.
|
# 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
|
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
|
||||||
# changes of the settings.
|
# changes of the settings.
|
||||||
SettingVersion = 5
|
SettingVersion = 7
|
||||||
|
|
||||||
Created = False
|
Created = False
|
||||||
|
|
||||||
|
@ -162,12 +156,12 @@ class CuraApplication(QtApplication):
|
||||||
Q_ENUMS(ResourceTypes)
|
Q_ENUMS(ResourceTypes)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(name = CuraAppName,
|
super().__init__(name = ApplicationMetadata.CuraAppName,
|
||||||
app_display_name = CuraAppDisplayName,
|
app_display_name = ApplicationMetadata.CuraAppDisplayName,
|
||||||
version = CuraVersion,
|
version = ApplicationMetadata.CuraVersion,
|
||||||
api_version = CuraSDKVersion,
|
api_version = ApplicationMetadata.CuraSDKVersion,
|
||||||
buildtype = CuraBuildType,
|
buildtype = ApplicationMetadata.CuraBuildType,
|
||||||
is_debug_mode = CuraDebugMode,
|
is_debug_mode = ApplicationMetadata.CuraDebugMode,
|
||||||
tray_icon_name = "cura-icon-32.png",
|
tray_icon_name = "cura-icon-32.png",
|
||||||
**kwargs)
|
**kwargs)
|
||||||
|
|
||||||
|
@ -182,7 +176,6 @@ class CuraApplication(QtApplication):
|
||||||
# Variables set from CLI
|
# Variables set from CLI
|
||||||
self._files_to_open = []
|
self._files_to_open = []
|
||||||
self._use_single_instance = False
|
self._use_single_instance = False
|
||||||
self._trigger_early_crash = False # For debug only
|
|
||||||
|
|
||||||
self._single_instance = None
|
self._single_instance = None
|
||||||
|
|
||||||
|
@ -207,6 +200,8 @@ class CuraApplication(QtApplication):
|
||||||
self._container_manager = None
|
self._container_manager = None
|
||||||
|
|
||||||
self._object_manager = None
|
self._object_manager = None
|
||||||
|
self._extruders_model = None
|
||||||
|
self._extruders_model_with_optional = None
|
||||||
self._build_plate_model = None
|
self._build_plate_model = None
|
||||||
self._multi_build_plate_model = None
|
self._multi_build_plate_model = None
|
||||||
self._setting_visibility_presets_model = None
|
self._setting_visibility_presets_model = None
|
||||||
|
@ -261,6 +256,14 @@ class CuraApplication(QtApplication):
|
||||||
from cura.CuraPackageManager import CuraPackageManager
|
from cura.CuraPackageManager import CuraPackageManager
|
||||||
self._package_manager_class = CuraPackageManager
|
self._package_manager_class = CuraPackageManager
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant=True)
|
||||||
|
def ultimakerCloudApiRootUrl(self) -> str:
|
||||||
|
return UltimakerCloudAuthentication.CuraCloudAPIRoot
|
||||||
|
|
||||||
|
@pyqtProperty(str, constant = True)
|
||||||
|
def ultimakerCloudAccountRootUrl(self) -> str:
|
||||||
|
return UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
|
||||||
|
|
||||||
# Adds command line options to the command line parser. This should be called after the application is created and
|
# Adds command line options to the command line parser. This should be called after the application is created and
|
||||||
# before the pre-start.
|
# before the pre-start.
|
||||||
def addCommandLineOptions(self):
|
def addCommandLineOptions(self):
|
||||||
|
@ -293,7 +296,10 @@ class CuraApplication(QtApplication):
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
self._use_single_instance = self._cli_args.single_instance
|
self._use_single_instance = self._cli_args.single_instance
|
||||||
self._trigger_early_crash = self._cli_args.trigger_early_crash
|
# FOR TESTING ONLY
|
||||||
|
if self._cli_args.trigger_early_crash:
|
||||||
|
assert not "This crash is triggered by the trigger_early_crash command line argument."
|
||||||
|
|
||||||
for filename in self._cli_args.file:
|
for filename in self._cli_args.file:
|
||||||
self._files_to_open.append(os.path.abspath(filename))
|
self._files_to_open.append(os.path.abspath(filename))
|
||||||
|
|
||||||
|
@ -429,7 +435,8 @@ class CuraApplication(QtApplication):
|
||||||
def startSplashWindowPhase(self) -> None:
|
def startSplashWindowPhase(self) -> None:
|
||||||
super().startSplashWindowPhase()
|
super().startSplashWindowPhase()
|
||||||
|
|
||||||
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
|
if not self.getIsHeadLess():
|
||||||
|
self.setWindowIcon(QIcon(Resources.getPath(Resources.Images, "cura-icon.png")))
|
||||||
|
|
||||||
self.setRequiredPlugins([
|
self.setRequiredPlugins([
|
||||||
# Misc.:
|
# Misc.:
|
||||||
|
@ -440,6 +447,7 @@ class CuraApplication(QtApplication):
|
||||||
"XmlMaterialProfile", #Cura crashes without this one.
|
"XmlMaterialProfile", #Cura crashes without this one.
|
||||||
"Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
|
"Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
|
||||||
"PrepareStage", #Cura is useless without this one since you can't load models.
|
"PrepareStage", #Cura is useless without this one since you can't load models.
|
||||||
|
"PreviewStage", #This shows the list of the plugin views that are installed in Cura.
|
||||||
"MonitorStage", #Major part of Cura's functionality.
|
"MonitorStage", #Major part of Cura's functionality.
|
||||||
"LocalFileOutputDevice", #Major part of Cura's functionality.
|
"LocalFileOutputDevice", #Major part of Cura's functionality.
|
||||||
"LocalContainerProvider", #Cura is useless without any profiles or setting definitions.
|
"LocalContainerProvider", #Cura is useless without any profiles or setting definitions.
|
||||||
|
@ -493,7 +501,8 @@ class CuraApplication(QtApplication):
|
||||||
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
|
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
|
||||||
preferences.addPreference("cura/choice_on_open_project", "always_ask")
|
preferences.addPreference("cura/choice_on_open_project", "always_ask")
|
||||||
preferences.addPreference("cura/use_multi_build_plate", False)
|
preferences.addPreference("cura/use_multi_build_plate", False)
|
||||||
|
preferences.addPreference("view/settings_list_height", 400)
|
||||||
|
preferences.addPreference("view/settings_visible", False)
|
||||||
preferences.addPreference("cura/currency", "€")
|
preferences.addPreference("cura/currency", "€")
|
||||||
preferences.addPreference("cura/material_settings", "{}")
|
preferences.addPreference("cura/material_settings", "{}")
|
||||||
|
|
||||||
|
@ -633,8 +642,6 @@ class CuraApplication(QtApplication):
|
||||||
self._message_box_callback = None
|
self._message_box_callback = None
|
||||||
self._message_box_callback_arguments = []
|
self._message_box_callback_arguments = []
|
||||||
|
|
||||||
showPrintMonitor = pyqtSignal(bool, arguments = ["show"])
|
|
||||||
|
|
||||||
def setSaveDataEnabled(self, enabled: bool) -> None:
|
def setSaveDataEnabled(self, enabled: bool) -> None:
|
||||||
self._save_data_enabled = enabled
|
self._save_data_enabled = enabled
|
||||||
|
|
||||||
|
@ -660,12 +667,12 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
## Handle loading of all plugin types (and the backend explicitly)
|
## Handle loading of all plugin types (and the backend explicitly)
|
||||||
# \sa PluginRegistry
|
# \sa PluginRegistry
|
||||||
def _loadPlugins(self):
|
def _loadPlugins(self) -> None:
|
||||||
self._plugin_registry.addType("profile_reader", self._addProfileReader)
|
self._plugin_registry.addType("profile_reader", self._addProfileReader)
|
||||||
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
|
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
|
||||||
|
|
||||||
if Platform.isLinux():
|
if Platform.isLinux():
|
||||||
lib_suffixes = {"", "64", "32", "x32"} #A few common ones on different distributions.
|
lib_suffixes = {"", "64", "32", "x32"} # A few common ones on different distributions.
|
||||||
else:
|
else:
|
||||||
lib_suffixes = {""}
|
lib_suffixes = {""}
|
||||||
for suffix in lib_suffixes:
|
for suffix in lib_suffixes:
|
||||||
|
@ -862,6 +869,19 @@ class CuraApplication(QtApplication):
|
||||||
self._object_manager = ObjectsModel.createObjectsModel()
|
self._object_manager = ObjectsModel.createObjectsModel()
|
||||||
return self._object_manager
|
return self._object_manager
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getExtrudersModel(self, *args) -> "ExtrudersModel":
|
||||||
|
if self._extruders_model is None:
|
||||||
|
self._extruders_model = ExtrudersModel(self)
|
||||||
|
return self._extruders_model
|
||||||
|
|
||||||
|
@pyqtSlot(result = QObject)
|
||||||
|
def getExtrudersModelWithOptional(self, *args) -> "ExtrudersModel":
|
||||||
|
if self._extruders_model_with_optional is None:
|
||||||
|
self._extruders_model_with_optional = ExtrudersModel(self)
|
||||||
|
self._extruders_model_with_optional.setAddOptionalExtruder(True)
|
||||||
|
return self._extruders_model_with_optional
|
||||||
|
|
||||||
@pyqtSlot(result = QObject)
|
@pyqtSlot(result = QObject)
|
||||||
def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel:
|
def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel:
|
||||||
if self._multi_build_plate_model is None:
|
if self._multi_build_plate_model is None:
|
||||||
|
@ -936,7 +956,7 @@ class CuraApplication(QtApplication):
|
||||||
engine.rootContext().setContextProperty("CuraApplication", self)
|
engine.rootContext().setContextProperty("CuraApplication", self)
|
||||||
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
|
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
|
||||||
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
|
engine.rootContext().setContextProperty("CuraActions", self._cura_actions)
|
||||||
engine.rootContext().setContextProperty("CuraSDKVersion", CuraSDKVersion)
|
engine.rootContext().setContextProperty("CuraSDKVersion", ApplicationMetadata.CuraSDKVersion)
|
||||||
|
|
||||||
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
|
qmlRegisterUncreatableType(CuraApplication, "Cura", 1, 0, "ResourceTypes", "Just an Enum type")
|
||||||
|
|
||||||
|
@ -954,6 +974,7 @@ class CuraApplication(QtApplication):
|
||||||
qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
|
qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
|
||||||
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
|
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
|
||||||
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
|
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
|
||||||
|
qmlRegisterType(GlobalStacksModel, "Cura", 1, 0, "GlobalStacksModel")
|
||||||
|
|
||||||
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
|
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
|
||||||
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
|
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
|
||||||
|
@ -975,6 +996,8 @@ class CuraApplication(QtApplication):
|
||||||
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
|
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
|
||||||
qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
|
qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
|
||||||
|
|
||||||
|
qmlRegisterType(PrinterOutputDevice, "Cura", 1, 0, "PrinterOutputDevice")
|
||||||
|
|
||||||
from cura.API import CuraAPI
|
from cura.API import CuraAPI
|
||||||
qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
|
qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
|
||||||
|
|
||||||
|
@ -1085,88 +1108,6 @@ class CuraApplication(QtApplication):
|
||||||
self._platform_activity = True if count > 0 else False
|
self._platform_activity = True if count > 0 else False
|
||||||
self.activityChanged.emit()
|
self.activityChanged.emit()
|
||||||
|
|
||||||
# Remove all selected objects from the scene.
|
|
||||||
@pyqtSlot()
|
|
||||||
@deprecated("Moved to CuraActions", "2.6")
|
|
||||||
def deleteSelection(self):
|
|
||||||
if not self.getController().getToolsEnabled():
|
|
||||||
return
|
|
||||||
removed_group_nodes = []
|
|
||||||
op = GroupedOperation()
|
|
||||||
nodes = Selection.getAllSelectedObjects()
|
|
||||||
for node in nodes:
|
|
||||||
op.addOperation(RemoveSceneNodeOperation(node))
|
|
||||||
group_node = node.getParent()
|
|
||||||
if group_node and group_node.callDecoration("isGroup") and group_node not in removed_group_nodes:
|
|
||||||
remaining_nodes_in_group = list(set(group_node.getChildren()) - set(nodes))
|
|
||||||
if len(remaining_nodes_in_group) == 1:
|
|
||||||
removed_group_nodes.append(group_node)
|
|
||||||
op.addOperation(SetParentOperation(remaining_nodes_in_group[0], group_node.getParent()))
|
|
||||||
op.addOperation(RemoveSceneNodeOperation(group_node))
|
|
||||||
op.push()
|
|
||||||
|
|
||||||
## Remove an object from the scene.
|
|
||||||
# Note that this only removes an object if it is selected.
|
|
||||||
@pyqtSlot("quint64")
|
|
||||||
@deprecated("Use deleteSelection instead", "2.6")
|
|
||||||
def deleteObject(self, object_id):
|
|
||||||
if not self.getController().getToolsEnabled():
|
|
||||||
return
|
|
||||||
|
|
||||||
node = self.getController().getScene().findObject(object_id)
|
|
||||||
|
|
||||||
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
|
|
||||||
node = Selection.getSelectedObject(0)
|
|
||||||
|
|
||||||
if node:
|
|
||||||
op = GroupedOperation()
|
|
||||||
op.addOperation(RemoveSceneNodeOperation(node))
|
|
||||||
|
|
||||||
group_node = node.getParent()
|
|
||||||
if group_node:
|
|
||||||
# Note that at this point the node has not yet been deleted
|
|
||||||
if len(group_node.getChildren()) <= 2 and group_node.callDecoration("isGroup"):
|
|
||||||
op.addOperation(SetParentOperation(group_node.getChildren()[0], group_node.getParent()))
|
|
||||||
op.addOperation(RemoveSceneNodeOperation(group_node))
|
|
||||||
|
|
||||||
op.push()
|
|
||||||
|
|
||||||
## Create a number of copies of existing object.
|
|
||||||
# \param object_id
|
|
||||||
# \param count number of copies
|
|
||||||
# \param min_offset minimum offset to other objects.
|
|
||||||
@pyqtSlot("quint64", int)
|
|
||||||
@deprecated("Use CuraActions::multiplySelection", "2.6")
|
|
||||||
def multiplyObject(self, object_id, count, min_offset = 8):
|
|
||||||
node = self.getController().getScene().findObject(object_id)
|
|
||||||
if not node:
|
|
||||||
node = Selection.getSelectedObject(0)
|
|
||||||
|
|
||||||
while node.getParent() and node.getParent().callDecoration("isGroup"):
|
|
||||||
node = node.getParent()
|
|
||||||
|
|
||||||
job = MultiplyObjectsJob([node], count, min_offset)
|
|
||||||
job.start()
|
|
||||||
return
|
|
||||||
|
|
||||||
## Center object on platform.
|
|
||||||
@pyqtSlot("quint64")
|
|
||||||
@deprecated("Use CuraActions::centerSelection", "2.6")
|
|
||||||
def centerObject(self, object_id):
|
|
||||||
node = self.getController().getScene().findObject(object_id)
|
|
||||||
if not node and object_id != 0: # Workaround for tool handles overlapping the selected object
|
|
||||||
node = Selection.getSelectedObject(0)
|
|
||||||
|
|
||||||
if not node:
|
|
||||||
return
|
|
||||||
|
|
||||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
|
||||||
node = node.getParent()
|
|
||||||
|
|
||||||
if node:
|
|
||||||
op = SetTransformOperation(node, Vector())
|
|
||||||
op.push()
|
|
||||||
|
|
||||||
## Select all nodes containing mesh data in the scene.
|
## Select all nodes containing mesh data in the scene.
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def selectAll(self):
|
def selectAll(self):
|
||||||
|
@ -1246,62 +1187,75 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
## Arrange all objects.
|
## Arrange all objects.
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def arrangeObjectsToAllBuildPlates(self):
|
def arrangeObjectsToAllBuildPlates(self) -> None:
|
||||||
nodes = []
|
nodes_to_arrange = []
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||||
if not isinstance(node, SceneNode):
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||||
continue # Node that doesnt have a mesh and is not a group.
|
continue # Node that doesnt have a mesh and is not a group.
|
||||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
|
||||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
parent_node = node.getParent()
|
||||||
|
if parent_node and parent_node.callDecoration("isGroup"):
|
||||||
|
continue # Grouped nodes don't need resetting as their parent (the group) is reset)
|
||||||
|
|
||||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||||
continue # i.e. node with layer data
|
continue # i.e. node with layer data
|
||||||
|
|
||||||
|
bounding_box = node.getBoundingBox()
|
||||||
# Skip nodes that are too big
|
# Skip nodes that are too big
|
||||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
|
||||||
nodes.append(node)
|
nodes_to_arrange.append(node)
|
||||||
job = ArrangeObjectsAllBuildPlatesJob(nodes)
|
job = ArrangeObjectsAllBuildPlatesJob(nodes_to_arrange)
|
||||||
job.start()
|
job.start()
|
||||||
self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
|
self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
|
||||||
|
|
||||||
# Single build plate
|
# Single build plate
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def arrangeAll(self):
|
def arrangeAll(self) -> None:
|
||||||
nodes = []
|
nodes_to_arrange = []
|
||||||
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
|
active_build_plate = self.getMultiBuildPlateModel().activeBuildPlate
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||||
if not isinstance(node, SceneNode):
|
if not isinstance(node, SceneNode):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
if not node.getMeshData() and not node.callDecoration("isGroup"):
|
||||||
continue # Node that doesnt have a mesh and is not a group.
|
continue # Node that doesnt have a mesh and is not a group.
|
||||||
if node.getParent() and node.getParent().callDecoration("isGroup"):
|
|
||||||
|
parent_node = node.getParent()
|
||||||
|
if parent_node and parent_node.callDecoration("isGroup"):
|
||||||
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
continue # Grouped nodes don't need resetting as their parent (the group) is resetted)
|
||||||
|
|
||||||
if not node.isSelectable():
|
if not node.isSelectable():
|
||||||
continue # i.e. node with layer data
|
continue # i.e. node with layer data
|
||||||
|
|
||||||
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
|
||||||
continue # i.e. node with layer data
|
continue # i.e. node with layer data
|
||||||
|
|
||||||
if node.callDecoration("getBuildPlateNumber") == active_build_plate:
|
if node.callDecoration("getBuildPlateNumber") == active_build_plate:
|
||||||
# Skip nodes that are too big
|
# Skip nodes that are too big
|
||||||
if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
|
bounding_box = node.getBoundingBox()
|
||||||
nodes.append(node)
|
if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
|
||||||
self.arrange(nodes, fixed_nodes = [])
|
nodes_to_arrange.append(node)
|
||||||
|
self.arrange(nodes_to_arrange, fixed_nodes = [])
|
||||||
|
|
||||||
## Arrange a set of nodes given a set of fixed nodes
|
## Arrange a set of nodes given a set of fixed nodes
|
||||||
# \param nodes nodes that we have to place
|
# \param nodes nodes that we have to place
|
||||||
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
|
# \param fixed_nodes nodes that are placed in the arranger before finding spots for nodes
|
||||||
def arrange(self, nodes, fixed_nodes):
|
def arrange(self, nodes: List[SceneNode], fixed_nodes: List[SceneNode]) -> None:
|
||||||
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
min_offset = self.getBuildVolume().getEdgeDisallowedSize() + 2 # Allow for some rounding errors
|
||||||
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
|
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
|
||||||
job.start()
|
job.start()
|
||||||
|
|
||||||
## Reload all mesh data on the screen from file.
|
## Reload all mesh data on the screen from file.
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def reloadAll(self):
|
def reloadAll(self) -> None:
|
||||||
Logger.log("i", "Reloading all loaded mesh data.")
|
Logger.log("i", "Reloading all loaded mesh data.")
|
||||||
nodes = []
|
nodes = []
|
||||||
has_merged_nodes = False
|
has_merged_nodes = False
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
|
||||||
if not isinstance(node, CuraSceneNode) or not node.getMeshData() :
|
if not isinstance(node, CuraSceneNode) or not node.getMeshData():
|
||||||
if node.getName() == "MergedMesh":
|
if node.getName() == "MergedMesh":
|
||||||
has_merged_nodes = True
|
has_merged_nodes = True
|
||||||
continue
|
continue
|
||||||
|
@ -1315,7 +1269,7 @@ class CuraApplication(QtApplication):
|
||||||
file_name = node.getMeshData().getFileName()
|
file_name = node.getMeshData().getFileName()
|
||||||
if file_name:
|
if file_name:
|
||||||
job = ReadMeshJob(file_name)
|
job = ReadMeshJob(file_name)
|
||||||
job._node = node
|
job._node = node # type: ignore
|
||||||
job.finished.connect(self._reloadMeshFinished)
|
job.finished.connect(self._reloadMeshFinished)
|
||||||
if has_merged_nodes:
|
if has_merged_nodes:
|
||||||
job.finished.connect(self.updateOriginOfMergedMeshes)
|
job.finished.connect(self.updateOriginOfMergedMeshes)
|
||||||
|
@ -1324,20 +1278,8 @@ class CuraApplication(QtApplication):
|
||||||
else:
|
else:
|
||||||
Logger.log("w", "Unable to reload data because we don't have a filename.")
|
Logger.log("w", "Unable to reload data because we don't have a filename.")
|
||||||
|
|
||||||
|
|
||||||
## Get logging data of the backend engine
|
|
||||||
# \returns \type{string} Logging data
|
|
||||||
@pyqtSlot(result = str)
|
|
||||||
def getEngineLog(self):
|
|
||||||
log = ""
|
|
||||||
|
|
||||||
for entry in self.getBackend().getLog():
|
|
||||||
log += entry.decode()
|
|
||||||
|
|
||||||
return log
|
|
||||||
|
|
||||||
@pyqtSlot("QStringList")
|
@pyqtSlot("QStringList")
|
||||||
def setExpandedCategories(self, categories):
|
def setExpandedCategories(self, categories: List[str]) -> None:
|
||||||
categories = list(set(categories))
|
categories = list(set(categories))
|
||||||
categories.sort()
|
categories.sort()
|
||||||
joined = ";".join(categories)
|
joined = ";".join(categories)
|
||||||
|
@ -1348,7 +1290,7 @@ class CuraApplication(QtApplication):
|
||||||
expandedCategoriesChanged = pyqtSignal()
|
expandedCategoriesChanged = pyqtSignal()
|
||||||
|
|
||||||
@pyqtProperty("QStringList", notify = expandedCategoriesChanged)
|
@pyqtProperty("QStringList", notify = expandedCategoriesChanged)
|
||||||
def expandedCategories(self):
|
def expandedCategories(self) -> List[str]:
|
||||||
return self.getPreferences().getValue("cura/categories_expanded").split(";")
|
return self.getPreferences().getValue("cura/categories_expanded").split(";")
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
|
@ -1398,13 +1340,12 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
|
|
||||||
## Updates origin position of all merged meshes
|
## Updates origin position of all merged meshes
|
||||||
# \param jobNode \type{Job} empty object which passed which is required by JobQueue
|
def updateOriginOfMergedMeshes(self, _):
|
||||||
def updateOriginOfMergedMeshes(self, jobNode):
|
|
||||||
group_nodes = []
|
group_nodes = []
|
||||||
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
|
||||||
if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh":
|
if isinstance(node, CuraSceneNode) and node.getName() == "MergedMesh":
|
||||||
|
|
||||||
#checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator
|
# Checking by name might be not enough, the merged mesh should has "GroupDecorator" decorator
|
||||||
for decorator in node.getDecorators():
|
for decorator in node.getDecorators():
|
||||||
if isinstance(decorator, GroupDecorator):
|
if isinstance(decorator, GroupDecorator):
|
||||||
group_nodes.append(node)
|
group_nodes.append(node)
|
||||||
|
@ -1448,7 +1389,7 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def groupSelected(self):
|
def groupSelected(self) -> None:
|
||||||
# Create a group-node
|
# Create a group-node
|
||||||
group_node = CuraSceneNode()
|
group_node = CuraSceneNode()
|
||||||
group_decorator = GroupDecorator()
|
group_decorator = GroupDecorator()
|
||||||
|
@ -1464,7 +1405,8 @@ class CuraApplication(QtApplication):
|
||||||
# Remove nodes that are directly parented to another selected node from the selection so they remain parented
|
# Remove nodes that are directly parented to another selected node from the selection so they remain parented
|
||||||
selected_nodes = Selection.getAllSelectedObjects().copy()
|
selected_nodes = Selection.getAllSelectedObjects().copy()
|
||||||
for node in selected_nodes:
|
for node in selected_nodes:
|
||||||
if node.getParent() in selected_nodes and not node.getParent().callDecoration("isGroup"):
|
parent = node.getParent()
|
||||||
|
if parent is not None and parent in selected_nodes and not parent.callDecoration("isGroup"):
|
||||||
Selection.remove(node)
|
Selection.remove(node)
|
||||||
|
|
||||||
# Move selected nodes into the group-node
|
# Move selected nodes into the group-node
|
||||||
|
@ -1476,7 +1418,7 @@ class CuraApplication(QtApplication):
|
||||||
Selection.add(group_node)
|
Selection.add(group_node)
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def ungroupSelected(self):
|
def ungroupSelected(self) -> None:
|
||||||
selected_objects = Selection.getAllSelectedObjects().copy()
|
selected_objects = Selection.getAllSelectedObjects().copy()
|
||||||
for node in selected_objects:
|
for node in selected_objects:
|
||||||
if node.callDecoration("isGroup"):
|
if node.callDecoration("isGroup"):
|
||||||
|
@ -1499,7 +1441,7 @@ class CuraApplication(QtApplication):
|
||||||
# Note: The group removes itself from the scene once all its children have left it,
|
# Note: The group removes itself from the scene once all its children have left it,
|
||||||
# see GroupDecorator._onChildrenChanged
|
# see GroupDecorator._onChildrenChanged
|
||||||
|
|
||||||
def _createSplashScreen(self):
|
def _createSplashScreen(self) -> Optional[CuraSplashScreen.CuraSplashScreen]:
|
||||||
if self._is_headless:
|
if self._is_headless:
|
||||||
return None
|
return None
|
||||||
return CuraSplashScreen.CuraSplashScreen()
|
return CuraSplashScreen.CuraSplashScreen()
|
||||||
|
@ -1665,7 +1607,9 @@ class CuraApplication(QtApplication):
|
||||||
is_non_sliceable = "." + file_extension in self._non_sliceable_extensions
|
is_non_sliceable = "." + file_extension in self._non_sliceable_extensions
|
||||||
|
|
||||||
if is_non_sliceable:
|
if is_non_sliceable:
|
||||||
self.callLater(lambda: self.getController().setActiveView("SimulationView"))
|
# Need to switch first to the preview stage and then to layer view
|
||||||
|
self.callLater(lambda: (self.getController().setActiveStage("PreviewStage"),
|
||||||
|
self.getController().setActiveView("SimulationView")))
|
||||||
|
|
||||||
block_slicing_decorator = BlockSlicingDecorator()
|
block_slicing_decorator = BlockSlicingDecorator()
|
||||||
node.addDecorator(block_slicing_decorator)
|
node.addDecorator(block_slicing_decorator)
|
||||||
|
|
|
@ -9,3 +9,4 @@ CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
|
||||||
CuraSDKVersion = "@CURA_SDK_VERSION@"
|
CuraSDKVersion = "@CURA_SDK_VERSION@"
|
||||||
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
|
||||||
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
|
||||||
|
CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
|
||||||
|
|
24
cura/CuraView.py
Normal file
24
cura/CuraView.py
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from PyQt5.QtCore import pyqtProperty, QUrl
|
||||||
|
|
||||||
|
from UM.View.View import View
|
||||||
|
|
||||||
|
|
||||||
|
# Since Cura has a few pre-defined "space claims" for the locations of certain components, we've provided some structure
|
||||||
|
# to indicate this.
|
||||||
|
# MainComponent works in the same way the MainComponent of a stage.
|
||||||
|
# the stageMenuComponent returns an item that should be used somehwere in the stage menu. It's up to the active stage
|
||||||
|
# to actually do something with this.
|
||||||
|
class CuraView(View):
|
||||||
|
def __init__(self, parent = None) -> None:
|
||||||
|
super().__init__(parent)
|
||||||
|
|
||||||
|
@pyqtProperty(QUrl, constant = True)
|
||||||
|
def mainComponent(self) -> QUrl:
|
||||||
|
return self.getDisplayComponent("main")
|
||||||
|
|
||||||
|
@pyqtProperty(QUrl, constant = True)
|
||||||
|
def stageMenuComponent(self) -> QUrl:
|
||||||
|
return self.getDisplayComponent("menu")
|
63
cura/GlobalStacksModel.py
Normal file
63
cura/GlobalStacksModel.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
# 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 pyqtProperty, Qt, pyqtSignal
|
||||||
|
|
||||||
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
|
|
||||||
|
from cura.PrinterOutputDevice import ConnectionType
|
||||||
|
|
||||||
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
|
|
||||||
|
class GlobalStacksModel(ListModel):
|
||||||
|
NameRole = Qt.UserRole + 1
|
||||||
|
IdRole = Qt.UserRole + 2
|
||||||
|
HasRemoteConnectionRole = Qt.UserRole + 3
|
||||||
|
ConnectionTypeRole = Qt.UserRole + 4
|
||||||
|
MetaDataRole = Qt.UserRole + 5
|
||||||
|
|
||||||
|
def __init__(self, parent = None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.addRoleName(self.NameRole, "name")
|
||||||
|
self.addRoleName(self.IdRole, "id")
|
||||||
|
self.addRoleName(self.HasRemoteConnectionRole, "hasRemoteConnection")
|
||||||
|
self.addRoleName(self.ConnectionTypeRole, "connectionType")
|
||||||
|
self.addRoleName(self.MetaDataRole, "metadata")
|
||||||
|
self._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 GlobalStack
|
||||||
|
if isinstance(container, GlobalStack):
|
||||||
|
self._update()
|
||||||
|
|
||||||
|
def _update(self) -> None:
|
||||||
|
items = []
|
||||||
|
|
||||||
|
container_stacks = ContainerRegistry.getInstance().findContainerStacks(type = "machine")
|
||||||
|
|
||||||
|
for container_stack in container_stacks:
|
||||||
|
connection_type = int(container_stack.getMetaDataEntry("connection_type", ConnectionType.NotConnected.value))
|
||||||
|
has_remote_connection = connection_type in [ConnectionType.NetworkConnection.value, ConnectionType.CloudConnection.value]
|
||||||
|
if container_stack.getMetaDataEntry("hidden", False) in ["True", True]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# TODO: Remove reference to connect group name.
|
||||||
|
items.append({"name": container_stack.getMetaDataEntry("connect_group_name", container_stack.getName()),
|
||||||
|
"id": container_stack.getId(),
|
||||||
|
"hasRemoteConnection": has_remote_connection,
|
||||||
|
"connectionType": connection_type,
|
||||||
|
"metadata": container_stack.getMetaData().copy()})
|
||||||
|
items.sort(key=lambda i: not i["hasRemoteConnection"])
|
||||||
|
self.setItems(items)
|
|
@ -5,6 +5,8 @@ from UM.Application import Application
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import numpy
|
import numpy
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
|
|
||||||
class LayerPolygon:
|
class LayerPolygon:
|
||||||
NoneType = 0
|
NoneType = 0
|
||||||
|
@ -18,7 +20,8 @@ class LayerPolygon:
|
||||||
MoveCombingType = 8
|
MoveCombingType = 8
|
||||||
MoveRetractionType = 9
|
MoveRetractionType = 9
|
||||||
SupportInterfaceType = 10
|
SupportInterfaceType = 10
|
||||||
__number_of_types = 11
|
PrimeTower = 11
|
||||||
|
__number_of_types = 12
|
||||||
|
|
||||||
__jump_map = numpy.logical_or(numpy.logical_or(numpy.arange(__number_of_types) == NoneType, numpy.arange(__number_of_types) == MoveCombingType), numpy.arange(__number_of_types) == MoveRetractionType)
|
__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)
|
||||||
|
|
||||||
|
@ -33,7 +36,8 @@ class LayerPolygon:
|
||||||
self._extruder = extruder
|
self._extruder = extruder
|
||||||
self._types = line_types
|
self._types = line_types
|
||||||
for i in range(len(self._types)):
|
for i in range(len(self._types)):
|
||||||
if self._types[i] >= self.__number_of_types: #Got faulty line data from the engine.
|
if self._types[i] >= self.__number_of_types: # Got faulty line data from the engine.
|
||||||
|
Logger.log("w", "Found an unknown line type: %s", i)
|
||||||
self._types[i] = self.NoneType
|
self._types[i] = self.NoneType
|
||||||
self._data = data
|
self._data = data
|
||||||
self._line_widths = line_widths
|
self._line_widths = line_widths
|
||||||
|
@ -236,7 +240,8 @@ class LayerPolygon:
|
||||||
theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType
|
theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType
|
||||||
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
|
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
|
||||||
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
|
theme.getColor("layerview_move_retraction").getRgbF(), # MoveRetractionType
|
||||||
theme.getColor("layerview_support_interface").getRgbF() # SupportInterfaceType
|
theme.getColor("layerview_support_interface").getRgbF(), # SupportInterfaceType
|
||||||
|
theme.getColor("layerview_prime_tower").getRgbF()
|
||||||
])
|
])
|
||||||
|
|
||||||
return cls.__color_map
|
return cls.__color_map
|
||||||
|
|
|
@ -64,21 +64,21 @@ class MachineErrorChecker(QObject):
|
||||||
|
|
||||||
def _onMachineChanged(self) -> None:
|
def _onMachineChanged(self) -> None:
|
||||||
if self._global_stack:
|
if self._global_stack:
|
||||||
self._global_stack.propertyChanged.disconnect(self.startErrorCheck)
|
self._global_stack.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
|
||||||
self._global_stack.containersChanged.disconnect(self.startErrorCheck)
|
self._global_stack.containersChanged.disconnect(self.startErrorCheck)
|
||||||
|
|
||||||
for extruder in self._global_stack.extruders.values():
|
for extruder in self._global_stack.extruders.values():
|
||||||
extruder.propertyChanged.disconnect(self.startErrorCheck)
|
extruder.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
|
||||||
extruder.containersChanged.disconnect(self.startErrorCheck)
|
extruder.containersChanged.disconnect(self.startErrorCheck)
|
||||||
|
|
||||||
self._global_stack = self._machine_manager.activeMachine
|
self._global_stack = self._machine_manager.activeMachine
|
||||||
|
|
||||||
if self._global_stack:
|
if self._global_stack:
|
||||||
self._global_stack.propertyChanged.connect(self.startErrorCheck)
|
self._global_stack.propertyChanged.connect(self.startErrorCheckPropertyChanged)
|
||||||
self._global_stack.containersChanged.connect(self.startErrorCheck)
|
self._global_stack.containersChanged.connect(self.startErrorCheck)
|
||||||
|
|
||||||
for extruder in self._global_stack.extruders.values():
|
for extruder in self._global_stack.extruders.values():
|
||||||
extruder.propertyChanged.connect(self.startErrorCheck)
|
extruder.propertyChanged.connect(self.startErrorCheckPropertyChanged)
|
||||||
extruder.containersChanged.connect(self.startErrorCheck)
|
extruder.containersChanged.connect(self.startErrorCheck)
|
||||||
|
|
||||||
hasErrorUpdated = pyqtSignal()
|
hasErrorUpdated = pyqtSignal()
|
||||||
|
@ -93,6 +93,13 @@ class MachineErrorChecker(QObject):
|
||||||
def needToWaitForResult(self) -> bool:
|
def needToWaitForResult(self) -> bool:
|
||||||
return self._need_to_check or self._check_in_progress
|
return self._need_to_check or self._check_in_progress
|
||||||
|
|
||||||
|
# Start the error check for property changed
|
||||||
|
# this is seperate from the startErrorCheck because it ignores a number property types
|
||||||
|
def startErrorCheckPropertyChanged(self, key, property_name):
|
||||||
|
if property_name != "value":
|
||||||
|
return
|
||||||
|
self.startErrorCheck()
|
||||||
|
|
||||||
# Starts the error check timer to schedule a new error check.
|
# Starts the error check timer to schedule a new error check.
|
||||||
def startErrorCheck(self, *args) -> None:
|
def startErrorCheck(self, *args) -> None:
|
||||||
if not self._check_in_progress:
|
if not self._check_in_progress:
|
||||||
|
|
|
@ -302,6 +302,10 @@ class MaterialManager(QObject):
|
||||||
def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]:
|
def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]:
|
||||||
return self._guid_material_groups_map.get(guid)
|
return self._guid_material_groups_map.get(guid)
|
||||||
|
|
||||||
|
# Returns a dict of all material groups organized by root_material_id.
|
||||||
|
def getAllMaterialGroups(self) -> Dict[str, "MaterialGroup"]:
|
||||||
|
return self._material_group_map
|
||||||
|
|
||||||
#
|
#
|
||||||
# Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
|
# Return a dict with all root material IDs (k) and ContainerNodes (v) that's suitable for the given setup.
|
||||||
#
|
#
|
||||||
|
@ -679,7 +683,11 @@ class MaterialManager(QObject):
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def removeFavorite(self, root_material_id: str) -> None:
|
def removeFavorite(self, root_material_id: str) -> None:
|
||||||
self._favorites.remove(root_material_id)
|
try:
|
||||||
|
self._favorites.remove(root_material_id)
|
||||||
|
except KeyError:
|
||||||
|
Logger.log("w", "Could not delete material %s from favorites as it was already deleted", root_material_id)
|
||||||
|
return
|
||||||
self.materialsUpdated.emit()
|
self.materialsUpdated.emit()
|
||||||
|
|
||||||
# Ensure all settings are saved.
|
# Ensure all settings are saved.
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from typing import Optional, Dict, Set
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty
|
||||||
from UM.Qt.ListModel import ListModel
|
from UM.Qt.ListModel import ListModel
|
||||||
|
@ -9,6 +10,9 @@ from UM.Qt.ListModel import ListModel
|
||||||
# Those 2 models are used by the material drop down menu to show generic materials and branded materials separately.
|
# Those 2 models are used by the material drop down menu to show generic materials and branded materials separately.
|
||||||
# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top
|
# The extruder position defined here is being used to bound a menu to the correct extruder. This is used in the top
|
||||||
# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
|
# bar menu "Settings" -> "Extruder nr" -> "Material" -> this menu
|
||||||
|
from cura.Machines.MaterialNode import MaterialNode
|
||||||
|
|
||||||
|
|
||||||
class BaseMaterialsModel(ListModel):
|
class BaseMaterialsModel(ListModel):
|
||||||
|
|
||||||
extruderPositionChanged = pyqtSignal()
|
extruderPositionChanged = pyqtSignal()
|
||||||
|
@ -54,8 +58,8 @@ class BaseMaterialsModel(ListModel):
|
||||||
self._extruder_position = 0
|
self._extruder_position = 0
|
||||||
self._extruder_stack = None
|
self._extruder_stack = None
|
||||||
|
|
||||||
self._available_materials = None
|
self._available_materials = None # type: Optional[Dict[str, MaterialNode]]
|
||||||
self._favorite_ids = None
|
self._favorite_ids = set() # type: Set[str]
|
||||||
|
|
||||||
def _updateExtruderStack(self):
|
def _updateExtruderStack(self):
|
||||||
global_stack = self._machine_manager.activeMachine
|
global_stack = self._machine_manager.activeMachine
|
||||||
|
@ -102,7 +106,6 @@ class BaseMaterialsModel(ListModel):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
extruder_stack = global_stack.extruders[extruder_position]
|
extruder_stack = global_stack.extruders[extruder_position]
|
||||||
|
|
||||||
self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack)
|
self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack)
|
||||||
if self._available_materials is None:
|
if self._available_materials is None:
|
||||||
return False
|
return False
|
||||||
|
|
|
@ -1,20 +1,16 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.Logger import Logger
|
|
||||||
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
|
||||||
|
|
||||||
|
## Model that shows the list of favorite materials.
|
||||||
class FavoriteMaterialsModel(BaseMaterialsModel):
|
class FavoriteMaterialsModel(BaseMaterialsModel):
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
|
|
||||||
# Perform standard check and reset if the check fails
|
|
||||||
if not self._canUpdate():
|
if not self._canUpdate():
|
||||||
self.setItems([])
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get updated list of favorites
|
# Get updated list of favorites
|
||||||
|
|
|
@ -11,10 +11,7 @@ class GenericMaterialsModel(BaseMaterialsModel):
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
|
|
||||||
# Perform standard check and reset if the check fails
|
|
||||||
if not self._canUpdate():
|
if not self._canUpdate():
|
||||||
self.setItems([])
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get updated list of favorites
|
# Get updated list of favorites
|
||||||
|
|
|
@ -28,12 +28,8 @@ class MaterialBrandsModel(BaseMaterialsModel):
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
|
|
||||||
# Perform standard check and reset if the check fails
|
|
||||||
if not self._canUpdate():
|
if not self._canUpdate():
|
||||||
self.setItems([])
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get updated list of favorites
|
# Get updated list of favorites
|
||||||
self._favorite_ids = self._material_manager.getFavorites()
|
self._favorite_ids = self._material_manager.getFavorites()
|
||||||
|
|
||||||
|
|
|
@ -33,8 +33,6 @@ class NozzleModel(ListModel):
|
||||||
def _update(self):
|
def _update(self):
|
||||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||||
|
|
||||||
self.items.clear()
|
|
||||||
|
|
||||||
global_stack = self._machine_manager.activeMachine
|
global_stack = self._machine_manager.activeMachine
|
||||||
if global_stack is None:
|
if global_stack is None:
|
||||||
self.setItems([])
|
self.setItems([])
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt5.QtCore import Qt, QTimer
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
@ -21,6 +21,7 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
||||||
AvailableRole = Qt.UserRole + 5
|
AvailableRole = Qt.UserRole + 5
|
||||||
QualityGroupRole = Qt.UserRole + 6
|
QualityGroupRole = Qt.UserRole + 6
|
||||||
QualityChangesGroupRole = Qt.UserRole + 7
|
QualityChangesGroupRole = Qt.UserRole + 7
|
||||||
|
IsExperimentalRole = Qt.UserRole + 8
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
def __init__(self, parent = None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
@ -32,20 +33,29 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
||||||
self.addRoleName(self.AvailableRole, "available") #Whether the quality profile is available in our current nozzle + material.
|
self.addRoleName(self.AvailableRole, "available") #Whether the quality profile is available in our current nozzle + material.
|
||||||
self.addRoleName(self.QualityGroupRole, "quality_group")
|
self.addRoleName(self.QualityGroupRole, "quality_group")
|
||||||
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
|
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
|
||||||
|
self.addRoleName(self.IsExperimentalRole, "is_experimental")
|
||||||
|
|
||||||
self._application = Application.getInstance()
|
self._application = Application.getInstance()
|
||||||
self._machine_manager = self._application.getMachineManager()
|
self._machine_manager = self._application.getMachineManager()
|
||||||
self._quality_manager = Application.getInstance().getQualityManager()
|
self._quality_manager = Application.getInstance().getQualityManager()
|
||||||
|
|
||||||
self._application.globalContainerStackChanged.connect(self._update)
|
self._application.globalContainerStackChanged.connect(self._onChange)
|
||||||
self._machine_manager.activeQualityGroupChanged.connect(self._update)
|
self._machine_manager.activeQualityGroupChanged.connect(self._onChange)
|
||||||
self._machine_manager.extruderChanged.connect(self._update)
|
self._machine_manager.extruderChanged.connect(self._onChange)
|
||||||
self._quality_manager.qualitiesUpdated.connect(self._update)
|
self._quality_manager.qualitiesUpdated.connect(self._onChange)
|
||||||
|
|
||||||
self._layer_height_unit = "" # This is cached
|
self._layer_height_unit = "" # This is cached
|
||||||
|
|
||||||
|
self._update_timer = QTimer() # type: QTimer
|
||||||
|
self._update_timer.setInterval(100)
|
||||||
|
self._update_timer.setSingleShot(True)
|
||||||
|
self._update_timer.timeout.connect(self._update)
|
||||||
|
|
||||||
self._update()
|
self._update()
|
||||||
|
|
||||||
|
def _onChange(self) -> None:
|
||||||
|
self._update_timer.start()
|
||||||
|
|
||||||
def _update(self):
|
def _update(self):
|
||||||
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
|
||||||
|
|
||||||
|
@ -74,7 +84,8 @@ class QualityProfilesDropDownMenuModel(ListModel):
|
||||||
"layer_height": layer_height,
|
"layer_height": layer_height,
|
||||||
"layer_height_unit": self._layer_height_unit,
|
"layer_height_unit": self._layer_height_unit,
|
||||||
"available": quality_group.is_available,
|
"available": quality_group.is_available,
|
||||||
"quality_group": quality_group}
|
"quality_group": quality_group,
|
||||||
|
"is_experimental": quality_group.is_experimental}
|
||||||
|
|
||||||
item_list.append(item)
|
item_list.append(item)
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ from typing import Optional, List
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
|
||||||
|
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
|
from UM.Preferences import Preferences
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
@ -18,14 +19,20 @@ class SettingVisibilityPresetsModel(QObject):
|
||||||
onItemsChanged = pyqtSignal()
|
onItemsChanged = pyqtSignal()
|
||||||
activePresetChanged = pyqtSignal()
|
activePresetChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, preferences, parent = None):
|
def __init__(self, preferences: Preferences, parent = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self._items = [] # type: List[SettingVisibilityPreset]
|
self._items = [] # type: List[SettingVisibilityPreset]
|
||||||
|
self._custom_preset = SettingVisibilityPreset(preset_id = "custom", name = "Custom selection", weight = -100)
|
||||||
|
|
||||||
self._populate()
|
self._populate()
|
||||||
|
|
||||||
basic_item = self.getVisibilityPresetById("basic")
|
basic_item = self.getVisibilityPresetById("basic")
|
||||||
basic_visibile_settings = ";".join(basic_item.settings)
|
if basic_item is not None:
|
||||||
|
basic_visibile_settings = ";".join(basic_item.settings)
|
||||||
|
else:
|
||||||
|
Logger.log("w", "Unable to find the basic visiblity preset.")
|
||||||
|
basic_visibile_settings = ""
|
||||||
|
|
||||||
self._preferences = preferences
|
self._preferences = preferences
|
||||||
|
|
||||||
|
@ -42,7 +49,8 @@ class SettingVisibilityPresetsModel(QObject):
|
||||||
visible_settings = self._preferences.getValue("general/visible_settings")
|
visible_settings = self._preferences.getValue("general/visible_settings")
|
||||||
|
|
||||||
if not visible_settings:
|
if not visible_settings:
|
||||||
self._preferences.setValue("general/visible_settings", ";".join(self._active_preset_item.settings))
|
new_visible_settings = self._active_preset_item.settings if self._active_preset_item is not None else []
|
||||||
|
self._preferences.setValue("general/visible_settings", ";".join(new_visible_settings))
|
||||||
else:
|
else:
|
||||||
self._onPreferencesChanged("general/visible_settings")
|
self._onPreferencesChanged("general/visible_settings")
|
||||||
|
|
||||||
|
@ -59,9 +67,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||||
def _populate(self) -> None:
|
def _populate(self) -> None:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
items = [] # type: List[SettingVisibilityPreset]
|
items = [] # type: List[SettingVisibilityPreset]
|
||||||
|
items.append(self._custom_preset)
|
||||||
custom_preset = SettingVisibilityPreset(preset_id="custom", name ="Custom selection", weight = -100)
|
|
||||||
items.append(custom_preset)
|
|
||||||
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
|
||||||
setting_visibility_preset = SettingVisibilityPreset()
|
setting_visibility_preset = SettingVisibilityPreset()
|
||||||
try:
|
try:
|
||||||
|
@ -77,7 +83,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||||
self.setItems(items)
|
self.setItems(items)
|
||||||
|
|
||||||
@pyqtProperty("QVariantList", notify = onItemsChanged)
|
@pyqtProperty("QVariantList", notify = onItemsChanged)
|
||||||
def items(self):
|
def items(self) -> List[SettingVisibilityPreset]:
|
||||||
return self._items
|
return self._items
|
||||||
|
|
||||||
def setItems(self, items: List[SettingVisibilityPreset]) -> None:
|
def setItems(self, items: List[SettingVisibilityPreset]) -> None:
|
||||||
|
@ -87,7 +93,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||||
|
|
||||||
@pyqtSlot(str)
|
@pyqtSlot(str)
|
||||||
def setActivePreset(self, preset_id: str) -> None:
|
def setActivePreset(self, preset_id: str) -> None:
|
||||||
if preset_id == self._active_preset_item.presetId:
|
if self._active_preset_item is not None and preset_id == self._active_preset_item.presetId:
|
||||||
Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id)
|
Logger.log("d", "Same setting visibility preset [%s] selected, do nothing.", preset_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -96,7 +102,7 @@ class SettingVisibilityPresetsModel(QObject):
|
||||||
Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
|
Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
need_to_save_to_custom = self._active_preset_item.presetId == "custom" and preset_id != "custom"
|
need_to_save_to_custom = self._active_preset_item is None or (self._active_preset_item.presetId == "custom" and preset_id != "custom")
|
||||||
if need_to_save_to_custom:
|
if need_to_save_to_custom:
|
||||||
# Save the current visibility settings to custom
|
# Save the current visibility settings to custom
|
||||||
current_visibility_string = self._preferences.getValue("general/visible_settings")
|
current_visibility_string = self._preferences.getValue("general/visible_settings")
|
||||||
|
@ -117,7 +123,9 @@ class SettingVisibilityPresetsModel(QObject):
|
||||||
|
|
||||||
@pyqtProperty(str, notify = activePresetChanged)
|
@pyqtProperty(str, notify = activePresetChanged)
|
||||||
def activePreset(self) -> str:
|
def activePreset(self) -> str:
|
||||||
return self._active_preset_item.presetId
|
if self._active_preset_item is not None:
|
||||||
|
return self._active_preset_item.presetId
|
||||||
|
return ""
|
||||||
|
|
||||||
def _onPreferencesChanged(self, name: str) -> None:
|
def _onPreferencesChanged(self, name: str) -> None:
|
||||||
if name != "general/visible_settings":
|
if name != "general/visible_settings":
|
||||||
|
@ -149,7 +157,12 @@ class SettingVisibilityPresetsModel(QObject):
|
||||||
else:
|
else:
|
||||||
item_to_set = matching_preset_item
|
item_to_set = matching_preset_item
|
||||||
|
|
||||||
|
# If we didn't find a matching preset, fallback to custom.
|
||||||
|
if item_to_set is None:
|
||||||
|
item_to_set = self._custom_preset
|
||||||
|
|
||||||
if self._active_preset_item is None or self._active_preset_item.presetId != item_to_set.presetId:
|
if self._active_preset_item is None or self._active_preset_item.presetId != item_to_set.presetId:
|
||||||
self._active_preset_item = item_to_set
|
self._active_preset_item = item_to_set
|
||||||
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item.presetId)
|
if self._active_preset_item is not None:
|
||||||
|
self._preferences.setValue("cura/active_setting_visibility_preset", self._active_preset_item.presetId)
|
||||||
self.activePresetChanged.emit()
|
self.activePresetChanged.emit()
|
||||||
|
|
|
@ -4,6 +4,9 @@
|
||||||
from typing import Dict, Optional, List, Set
|
from typing import Dict, Optional, List, Set
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSlot
|
from PyQt5.QtCore import QObject, pyqtSlot
|
||||||
|
|
||||||
|
from UM.Util import parseBool
|
||||||
|
|
||||||
from cura.Machines.ContainerNode import ContainerNode
|
from cura.Machines.ContainerNode import ContainerNode
|
||||||
|
|
||||||
|
|
||||||
|
@ -29,6 +32,7 @@ class QualityGroup(QObject):
|
||||||
self.nodes_for_extruders = {} # type: Dict[int, ContainerNode]
|
self.nodes_for_extruders = {} # type: Dict[int, ContainerNode]
|
||||||
self.quality_type = quality_type
|
self.quality_type = quality_type
|
||||||
self.is_available = False
|
self.is_available = False
|
||||||
|
self.is_experimental = False
|
||||||
|
|
||||||
@pyqtSlot(result = str)
|
@pyqtSlot(result = str)
|
||||||
def getName(self) -> str:
|
def getName(self) -> str:
|
||||||
|
@ -51,3 +55,17 @@ class QualityGroup(QObject):
|
||||||
for extruder_node in self.nodes_for_extruders.values():
|
for extruder_node in self.nodes_for_extruders.values():
|
||||||
result.append(extruder_node)
|
result.append(extruder_node)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def setGlobalNode(self, node: "ContainerNode") -> None:
|
||||||
|
self.node_for_global = node
|
||||||
|
|
||||||
|
# Update is_experimental flag
|
||||||
|
is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
|
||||||
|
self.is_experimental |= is_experimental
|
||||||
|
|
||||||
|
def setExtruderNode(self, position: int, node: "ContainerNode") -> None:
|
||||||
|
self.nodes_for_extruders[position] = node
|
||||||
|
|
||||||
|
# Update is_experimental flag
|
||||||
|
is_experimental = parseBool(node.getMetaDataEntry("is_experimental", False))
|
||||||
|
self.is_experimental |= is_experimental
|
||||||
|
|
|
@ -16,7 +16,7 @@ from .QualityGroup import QualityGroup
|
||||||
from .QualityNode import QualityNode
|
from .QualityNode import QualityNode
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from UM.Settings.DefinitionContainer import DefinitionContainer
|
from UM.Settings.Interfaces import DefinitionContainerInterface
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
from cura.Settings.GlobalStack import GlobalStack
|
||||||
from .QualityChangesGroup import QualityChangesGroup
|
from .QualityChangesGroup import QualityChangesGroup
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
@ -235,7 +235,7 @@ class QualityManager(QObject):
|
||||||
|
|
||||||
for quality_type, quality_node in node.quality_type_map.items():
|
for quality_type, quality_node in node.quality_type_map.items():
|
||||||
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
|
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
|
||||||
quality_group.node_for_global = quality_node
|
quality_group.setGlobalNode(quality_node)
|
||||||
quality_group_dict[quality_type] = quality_group
|
quality_group_dict[quality_type] = quality_group
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -337,7 +337,7 @@ class QualityManager(QObject):
|
||||||
|
|
||||||
quality_group = quality_group_dict[quality_type]
|
quality_group = quality_group_dict[quality_type]
|
||||||
if position not in quality_group.nodes_for_extruders:
|
if position not in quality_group.nodes_for_extruders:
|
||||||
quality_group.nodes_for_extruders[position] = quality_node
|
quality_group.setExtruderNode(position, quality_node)
|
||||||
|
|
||||||
# If the machine has its own specific qualities, for extruders, it should skip the global qualities
|
# If the machine has its own specific qualities, for extruders, it should skip the global qualities
|
||||||
# and use the material/variant specific qualities.
|
# and use the material/variant specific qualities.
|
||||||
|
@ -367,7 +367,7 @@ class QualityManager(QObject):
|
||||||
if node and node.quality_type_map:
|
if node and node.quality_type_map:
|
||||||
for quality_type, quality_node in node.quality_type_map.items():
|
for quality_type, quality_node in node.quality_type_map.items():
|
||||||
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
|
quality_group = QualityGroup(quality_node.getMetaDataEntry("name", ""), quality_type)
|
||||||
quality_group.node_for_global = quality_node
|
quality_group.setGlobalNode(quality_node)
|
||||||
quality_group_dict[quality_type] = quality_group
|
quality_group_dict[quality_type] = quality_group
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -538,7 +538,7 @@ class QualityManager(QObject):
|
||||||
# Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
|
# Example: for an Ultimaker 3 Extended, it has "quality_definition = ultimaker3". This means Ultimaker 3 Extended
|
||||||
# shares the same set of qualities profiles as Ultimaker 3.
|
# shares the same set of qualities profiles as Ultimaker 3.
|
||||||
#
|
#
|
||||||
def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainer",
|
def getMachineDefinitionIDForQualitySearch(machine_definition: "DefinitionContainerInterface",
|
||||||
default_definition_id: str = "fdmprinter") -> str:
|
default_definition_id: str = "fdmprinter") -> str:
|
||||||
machine_definition_id = default_definition_id
|
machine_definition_id = default_definition_id
|
||||||
if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)):
|
if parseBool(machine_definition.getMetaDataEntry("has_machine_quality", False)):
|
||||||
|
|
|
@ -107,7 +107,7 @@ class VariantManager:
|
||||||
break
|
break
|
||||||
return variant_node
|
return variant_node
|
||||||
|
|
||||||
return self._machine_to_variant_dict_map[machine_definition_id].get(variant_type, {}).get(variant_name)
|
return self._machine_to_variant_dict_map.get(machine_definition_id, {}).get(variant_type, {}).get(variant_name)
|
||||||
|
|
||||||
def getVariantNodes(self, machine: "GlobalStack", variant_type: "VariantType") -> Dict[str, ContainerNode]:
|
def getVariantNodes(self, machine: "GlobalStack", variant_type: "VariantType") -> Dict[str, ContainerNode]:
|
||||||
machine_definition_id = machine.definition.getId()
|
machine_definition_id = machine.definition.getId()
|
||||||
|
|
|
@ -25,7 +25,7 @@ class MultiplyObjectsJob(Job):
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
|
status_message = Message(i18n_catalog.i18nc("@info:status", "Multiplying and placing objects"), lifetime=0,
|
||||||
dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Object"))
|
dismissable=False, progress=0, title = i18n_catalog.i18nc("@info:title", "Placing Objects"))
|
||||||
status_message.show()
|
status_message.show()
|
||||||
scene = Application.getInstance().getController().getScene()
|
scene = Application.getInstance().getController().getScene()
|
||||||
|
|
||||||
|
|
|
@ -81,9 +81,14 @@ class AuthorizationHelpers:
|
||||||
# \param access_token: The encoded JWT token.
|
# \param access_token: The encoded JWT token.
|
||||||
# \return: Dict containing some profile data.
|
# \return: Dict containing some profile data.
|
||||||
def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
|
def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
|
||||||
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
|
try:
|
||||||
"Authorization": "Bearer {}".format(access_token)
|
token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
|
||||||
})
|
"Authorization": "Bearer {}".format(access_token)
|
||||||
|
})
|
||||||
|
except ConnectionError:
|
||||||
|
# Connection was suddenly dropped. Nothing we can do about that.
|
||||||
|
Logger.logException("e", "Something failed while attempting to parse the JWT token")
|
||||||
|
return None
|
||||||
if token_request.status_code not in (200, 201):
|
if token_request.status_code not in (200, 201):
|
||||||
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
|
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -52,8 +52,11 @@ class AuthorizationService:
|
||||||
if not self._user_profile:
|
if not self._user_profile:
|
||||||
# If no user profile was stored locally, we try to get it from JWT.
|
# If no user profile was stored locally, we try to get it from JWT.
|
||||||
self._user_profile = self._parseJWT()
|
self._user_profile = self._parseJWT()
|
||||||
if not self._user_profile:
|
|
||||||
|
if not self._user_profile and self._auth_data:
|
||||||
# If there is still no user profile from the JWT, we have to log in again.
|
# If there is still no user profile from the JWT, we have to log in again.
|
||||||
|
Logger.log("w", "The user profile could not be loaded. The user must log in again!")
|
||||||
|
self.deleteAuthData()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._user_profile
|
return self._user_profile
|
||||||
|
@ -83,9 +86,11 @@ class AuthorizationService:
|
||||||
if not self.getUserProfile():
|
if not self.getUserProfile():
|
||||||
# We check if we can get the user profile.
|
# We check if we can get the user profile.
|
||||||
# If we can't get it, that means the access token (JWT) was invalid or expired.
|
# If we can't get it, that means the access token (JWT) was invalid or expired.
|
||||||
|
Logger.log("w", "Unable to get the user profile.")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if self._auth_data is None:
|
if self._auth_data is None:
|
||||||
|
Logger.log("d", "No auth data to retrieve the access_token from")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return self._auth_data.access_token
|
return self._auth_data.access_token
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -14,8 +14,7 @@ from UM.Logger import Logger
|
||||||
from UM.Qt.Duration import Duration
|
from UM.Qt.Duration import Duration
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
from UM.MimeTypeDatabase import MimeTypeDatabase
|
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
|
||||||
|
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
@ -361,7 +360,7 @@ class PrintInformation(QObject):
|
||||||
try:
|
try:
|
||||||
mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
|
mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
|
||||||
data = mime_type.stripExtension(name)
|
data = mime_type.stripExtension(name)
|
||||||
except:
|
except MimeTypeNotFoundError:
|
||||||
Logger.log("w", "Unsupported Mime Type Database file extension %s", name)
|
Logger.log("w", "Unsupported Mime Type Database file extension %s", name)
|
||||||
|
|
||||||
if data is not None and check_name is not None:
|
if data is not None and check_name is not None:
|
||||||
|
@ -395,28 +394,14 @@ class PrintInformation(QObject):
|
||||||
return
|
return
|
||||||
active_machine_type_name = global_container_stack.definition.getName()
|
active_machine_type_name = global_container_stack.definition.getName()
|
||||||
|
|
||||||
abbr_machine = ""
|
self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name)
|
||||||
for word in re.findall(r"[\w']+", active_machine_type_name):
|
|
||||||
if word.lower() == "ultimaker":
|
|
||||||
abbr_machine += "UM"
|
|
||||||
elif word.isdigit():
|
|
||||||
abbr_machine += word
|
|
||||||
else:
|
|
||||||
stripped_word = self._stripAccents(word.upper())
|
|
||||||
# - use only the first character if the word is too long (> 3 characters)
|
|
||||||
# - use the whole word if it's not too long (<= 3 characters)
|
|
||||||
if len(stripped_word) > 3:
|
|
||||||
stripped_word = stripped_word[0]
|
|
||||||
abbr_machine += stripped_word
|
|
||||||
|
|
||||||
self._abbr_machine = abbr_machine
|
|
||||||
|
|
||||||
## Utility method that strips accents from characters (eg: â -> a)
|
## Utility method that strips accents from characters (eg: â -> a)
|
||||||
def _stripAccents(self, to_strip: str) -> str:
|
def _stripAccents(self, to_strip: str) -> str:
|
||||||
return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')
|
return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')
|
||||||
|
|
||||||
@pyqtSlot(result = "QVariantMap")
|
@pyqtSlot(result = "QVariantMap")
|
||||||
def getFeaturePrintTimes(self):
|
def getFeaturePrintTimes(self) -> Dict[str, Duration]:
|
||||||
result = {}
|
result = {}
|
||||||
if self._active_build_plate not in self._print_times_per_feature:
|
if self._active_build_plate not in self._print_times_per_feature:
|
||||||
self._initPrintTimesPerFeature(self._active_build_plate)
|
self._initPrintTimesPerFeature(self._active_build_plate)
|
||||||
|
|
|
@ -54,7 +54,7 @@ class ConfigurationModel(QObject):
|
||||||
for configuration in self._extruder_configurations:
|
for configuration in self._extruder_configurations:
|
||||||
if configuration is None:
|
if configuration is None:
|
||||||
return False
|
return False
|
||||||
return self._printer_type is not None
|
return self._printer_type != ""
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
message_chunks = []
|
message_chunks = []
|
||||||
|
|
|
@ -4,19 +4,21 @@
|
||||||
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
from UM.FileHandler.FileHandler import FileHandler #For typing.
|
||||||
from UM.Logger import Logger
|
from UM.Logger import Logger
|
||||||
from UM.Scene.SceneNode import SceneNode #For typing.
|
from UM.Scene.SceneNode import SceneNode #For typing.
|
||||||
|
from cura.API import Account
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
|
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType
|
||||||
|
|
||||||
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator
|
from PyQt5.QtNetwork import QHttpMultiPart, QHttpPart, QNetworkRequest, QNetworkAccessManager, QNetworkReply, QAuthenticator
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
|
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
|
||||||
from time import time
|
from time import time
|
||||||
from typing import Any, Callable, Dict, List, Optional
|
from typing import Callable, Dict, List, Optional, Union
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
import os # To get the username
|
import os # To get the username
|
||||||
import gzip
|
import gzip
|
||||||
|
|
||||||
|
|
||||||
class AuthState(IntEnum):
|
class AuthState(IntEnum):
|
||||||
NotAuthenticated = 1
|
NotAuthenticated = 1
|
||||||
AuthenticationRequested = 2
|
AuthenticationRequested = 2
|
||||||
|
@ -28,8 +30,8 @@ class AuthState(IntEnum):
|
||||||
class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
authenticationStateChanged = pyqtSignal()
|
authenticationStateChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], parent: QObject = None) -> None:
|
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, parent = parent)
|
super().__init__(device_id = device_id, connection_type = connection_type, parent = parent)
|
||||||
self._manager = None # type: Optional[QNetworkAccessManager]
|
self._manager = None # type: Optional[QNetworkAccessManager]
|
||||||
self._last_manager_create_time = None # type: Optional[float]
|
self._last_manager_create_time = None # type: Optional[float]
|
||||||
self._recreate_network_manager_time = 30
|
self._recreate_network_manager_time = 30
|
||||||
|
@ -41,7 +43,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
self._api_prefix = ""
|
self._api_prefix = ""
|
||||||
self._address = address
|
self._address = address
|
||||||
self._properties = properties
|
self._properties = properties
|
||||||
self._user_agent = "%s/%s " % (CuraApplication.getInstance().getApplicationName(), CuraApplication.getInstance().getVersion())
|
self._user_agent = "%s/%s " % (CuraApplication.getInstance().getApplicationName(),
|
||||||
|
CuraApplication.getInstance().getVersion())
|
||||||
|
|
||||||
self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]]
|
self._onFinishedCallbacks = {} # type: Dict[str, Callable[[QNetworkReply], None]]
|
||||||
self._authentication_state = AuthState.NotAuthenticated
|
self._authentication_state = AuthState.NotAuthenticated
|
||||||
|
@ -55,7 +58,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
self._gcode = [] # type: List[str]
|
self._gcode = [] # type: List[str]
|
||||||
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
|
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
|
||||||
|
|
||||||
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||||
|
file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
|
||||||
raise NotImplementedError("requestWrite needs to be implemented")
|
raise NotImplementedError("requestWrite needs to be implemented")
|
||||||
|
|
||||||
def setAuthenticationState(self, authentication_state: AuthState) -> None:
|
def setAuthenticationState(self, authentication_state: AuthState) -> None:
|
||||||
|
@ -125,7 +129,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
if self._connection_state_before_timeout is None:
|
if self._connection_state_before_timeout is None:
|
||||||
self._connection_state_before_timeout = self._connection_state
|
self._connection_state_before_timeout = self._connection_state
|
||||||
|
|
||||||
self.setConnectionState(ConnectionState.closed)
|
self.setConnectionState(ConnectionState.Closed)
|
||||||
|
|
||||||
# We need to check if the manager needs to be re-created. If we don't, we get some issues when OSX goes to
|
# 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.
|
# sleep.
|
||||||
|
@ -133,7 +137,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
if self._last_manager_create_time is None or time() - self._last_manager_create_time > 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()
|
self._createNetworkManager()
|
||||||
assert(self._manager is not None)
|
assert(self._manager is not None)
|
||||||
elif self._connection_state == ConnectionState.closed:
|
elif self._connection_state == ConnectionState.Closed:
|
||||||
# Go out of timeout.
|
# Go out of timeout.
|
||||||
if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here
|
if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here
|
||||||
self.setConnectionState(self._connection_state_before_timeout)
|
self.setConnectionState(self._connection_state_before_timeout)
|
||||||
|
@ -143,10 +147,15 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
url = QUrl("http://" + self._address + self._api_prefix + target)
|
url = QUrl("http://" + self._address + self._api_prefix + target)
|
||||||
request = QNetworkRequest(url)
|
request = QNetworkRequest(url)
|
||||||
if content_type is not None:
|
if content_type is not None:
|
||||||
request.setHeader(QNetworkRequest.ContentTypeHeader, "application/json")
|
request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
|
||||||
request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
|
request.setHeader(QNetworkRequest.UserAgentHeader, self._user_agent)
|
||||||
return request
|
return request
|
||||||
|
|
||||||
|
## This method was only available privately before, but it was actually called from SendMaterialJob.py.
|
||||||
|
# We now have a public equivalent as well. We did not remove the private one as plugins might be using that.
|
||||||
|
def createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
|
||||||
|
return self._createFormPart(content_header, data, content_type)
|
||||||
|
|
||||||
def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
|
def _createFormPart(self, content_header: str, data: bytes, content_type: Optional[str] = None) -> QHttpPart:
|
||||||
part = QHttpPart()
|
part = QHttpPart()
|
||||||
|
|
||||||
|
@ -160,9 +169,15 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
part.setBody(data)
|
part.setBody(data)
|
||||||
return part
|
return part
|
||||||
|
|
||||||
## Convenience function to get the username from the OS.
|
## Convenience function to get the username, either from the cloud or from the OS.
|
||||||
# The code was copied from the getpass module, as we try to use as little dependencies as possible.
|
|
||||||
def _getUserName(self) -> str:
|
def _getUserName(self) -> str:
|
||||||
|
# check first if we are logged in with the Ultimaker Account
|
||||||
|
account = CuraApplication.getInstance().getCuraAPI().account # type: Account
|
||||||
|
if account and account.isLoggedIn:
|
||||||
|
return account.userName
|
||||||
|
|
||||||
|
# Otherwise get the username from the US
|
||||||
|
# The code below was copied from the getpass module, as we try to use as little dependencies as possible.
|
||||||
for name in ("LOGNAME", "USER", "LNAME", "USERNAME"):
|
for name in ("LOGNAME", "USER", "LNAME", "USERNAME"):
|
||||||
user = os.environ.get(name)
|
user = os.environ.get(name)
|
||||||
if user:
|
if user:
|
||||||
|
@ -178,49 +193,89 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
self._createNetworkManager()
|
self._createNetworkManager()
|
||||||
assert (self._manager is not None)
|
assert (self._manager is not None)
|
||||||
|
|
||||||
def put(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
## Sends a put request to the given path.
|
||||||
|
# \param url: The path after the API prefix.
|
||||||
|
# \param data: The data to be sent in the body
|
||||||
|
# \param content_type: The content type of the body data.
|
||||||
|
# \param on_finished: The function to call when the response is received.
|
||||||
|
# \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
|
||||||
|
def put(self, url: str, data: Union[str, bytes], content_type: Optional[str] = None,
|
||||||
|
on_finished: Optional[Callable[[QNetworkReply], None]] = None,
|
||||||
|
on_progress: Optional[Callable[[int, int], None]] = None) -> None:
|
||||||
self._validateManager()
|
self._validateManager()
|
||||||
request = self._createEmptyRequest(target)
|
|
||||||
self._last_request_time = time()
|
|
||||||
if self._manager is not None:
|
|
||||||
reply = self._manager.put(request, data.encode())
|
|
||||||
self._registerOnFinishedCallback(reply, on_finished)
|
|
||||||
else:
|
|
||||||
Logger.log("e", "Could not find manager.")
|
|
||||||
|
|
||||||
def delete(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
request = self._createEmptyRequest(url, content_type = content_type)
|
||||||
|
self._last_request_time = time()
|
||||||
|
|
||||||
|
if not self._manager:
|
||||||
|
Logger.log("e", "No network manager was created to execute the PUT call with.")
|
||||||
|
return
|
||||||
|
|
||||||
|
body = data if isinstance(data, bytes) else data.encode() # type: bytes
|
||||||
|
reply = self._manager.put(request, body)
|
||||||
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
|
|
||||||
|
if on_progress is not None:
|
||||||
|
reply.uploadProgress.connect(on_progress)
|
||||||
|
|
||||||
|
## Sends a delete request to the given path.
|
||||||
|
# \param url: The path after the API prefix.
|
||||||
|
# \param on_finished: The function to be call when the response is received.
|
||||||
|
def delete(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||||
self._validateManager()
|
self._validateManager()
|
||||||
request = self._createEmptyRequest(target)
|
|
||||||
self._last_request_time = time()
|
|
||||||
if self._manager is not None:
|
|
||||||
reply = self._manager.deleteResource(request)
|
|
||||||
self._registerOnFinishedCallback(reply, on_finished)
|
|
||||||
else:
|
|
||||||
Logger.log("e", "Could not find manager.")
|
|
||||||
|
|
||||||
def get(self, target: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
request = self._createEmptyRequest(url)
|
||||||
|
self._last_request_time = time()
|
||||||
|
|
||||||
|
if not self._manager:
|
||||||
|
Logger.log("e", "No network manager was created to execute the DELETE call with.")
|
||||||
|
return
|
||||||
|
|
||||||
|
reply = self._manager.deleteResource(request)
|
||||||
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
|
|
||||||
|
## Sends a get request to the given path.
|
||||||
|
# \param url: The path after the API prefix.
|
||||||
|
# \param on_finished: The function to be call when the response is received.
|
||||||
|
def get(self, url: str, on_finished: Optional[Callable[[QNetworkReply], None]]) -> None:
|
||||||
self._validateManager()
|
self._validateManager()
|
||||||
request = self._createEmptyRequest(target)
|
|
||||||
self._last_request_time = time()
|
|
||||||
if self._manager is not None:
|
|
||||||
reply = self._manager.get(request)
|
|
||||||
self._registerOnFinishedCallback(reply, on_finished)
|
|
||||||
else:
|
|
||||||
Logger.log("e", "Could not find manager.")
|
|
||||||
|
|
||||||
def post(self, target: str, data: str, on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> None:
|
request = self._createEmptyRequest(url)
|
||||||
|
self._last_request_time = time()
|
||||||
|
|
||||||
|
if not self._manager:
|
||||||
|
Logger.log("e", "No network manager was created to execute the GET call with.")
|
||||||
|
return
|
||||||
|
|
||||||
|
reply = self._manager.get(request)
|
||||||
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
|
|
||||||
|
## Sends a post request to the given path.
|
||||||
|
# \param url: The path after the API prefix.
|
||||||
|
# \param data: The data to be sent in the body
|
||||||
|
# \param on_finished: The function to call when the response is received.
|
||||||
|
# \param on_progress: The function to call when the progress changes. Parameters are bytes_sent / bytes_total.
|
||||||
|
def post(self, url: str, data: Union[str, bytes],
|
||||||
|
on_finished: Optional[Callable[[QNetworkReply], None]],
|
||||||
|
on_progress: Optional[Callable[[int, int], None]] = None) -> None:
|
||||||
self._validateManager()
|
self._validateManager()
|
||||||
request = self._createEmptyRequest(target)
|
|
||||||
self._last_request_time = time()
|
|
||||||
if self._manager is not None:
|
|
||||||
reply = self._manager.post(request, data.encode())
|
|
||||||
if on_progress is not None:
|
|
||||||
reply.uploadProgress.connect(on_progress)
|
|
||||||
self._registerOnFinishedCallback(reply, on_finished)
|
|
||||||
else:
|
|
||||||
Logger.log("e", "Could not find manager.")
|
|
||||||
|
|
||||||
def postFormWithParts(self, target: str, parts: List[QHttpPart], on_finished: Optional[Callable[[QNetworkReply], None]], on_progress: Callable = None) -> QNetworkReply:
|
request = self._createEmptyRequest(url)
|
||||||
|
self._last_request_time = time()
|
||||||
|
|
||||||
|
if not self._manager:
|
||||||
|
Logger.log("e", "Could not find manager.")
|
||||||
|
return
|
||||||
|
|
||||||
|
body = data if isinstance(data, bytes) else data.encode() # type: bytes
|
||||||
|
reply = self._manager.post(request, body)
|
||||||
|
if on_progress is not None:
|
||||||
|
reply.uploadProgress.connect(on_progress)
|
||||||
|
self._registerOnFinishedCallback(reply, on_finished)
|
||||||
|
|
||||||
|
def postFormWithParts(self, target: str, parts: List[QHttpPart],
|
||||||
|
on_finished: Optional[Callable[[QNetworkReply], None]],
|
||||||
|
on_progress: Optional[Callable[[int, int], None]] = None) -> QNetworkReply:
|
||||||
self._validateManager()
|
self._validateManager()
|
||||||
request = self._createEmptyRequest(target, content_type=None)
|
request = self._createEmptyRequest(target, content_type=None)
|
||||||
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
|
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
|
||||||
|
@ -282,8 +337,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
|
||||||
|
|
||||||
self._last_response_time = time()
|
self._last_response_time = time()
|
||||||
|
|
||||||
if self._connection_state == ConnectionState.connecting:
|
if self._connection_state == ConnectionState.Connecting:
|
||||||
self.setConnectionState(ConnectionState.connected)
|
self.setConnectionState(ConnectionState.Connected)
|
||||||
|
|
||||||
callback_key = reply.url().toString() + str(reply.operation())
|
callback_key = reply.url().toString() + str(reply.operation())
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -118,17 +118,40 @@ class PrintJobOutputModel(QObject):
|
||||||
self.nameChanged.emit()
|
self.nameChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(int, notify = timeTotalChanged)
|
@pyqtProperty(int, notify = timeTotalChanged)
|
||||||
def timeTotal(self):
|
def timeTotal(self) -> int:
|
||||||
return self._time_total
|
return self._time_total
|
||||||
|
|
||||||
@pyqtProperty(int, notify = timeElapsedChanged)
|
@pyqtProperty(int, notify = timeElapsedChanged)
|
||||||
def timeElapsed(self):
|
def timeElapsed(self) -> int:
|
||||||
return self._time_elapsed
|
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:
|
||||||
|
time_elapsed = max(float(self.timeElapsed), 1.0) # Prevent a division by zero exception
|
||||||
|
result = time_elapsed / self.timeTotal
|
||||||
|
return min(result, 1.0) # Never get a progress past 1.0
|
||||||
|
|
||||||
@pyqtProperty(str, notify=stateChanged)
|
@pyqtProperty(str, notify=stateChanged)
|
||||||
def state(self):
|
def state(self) -> str:
|
||||||
return self._state
|
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):
|
def updateTimeTotal(self, new_time_total):
|
||||||
if self._time_total != new_time_total:
|
if self._time_total != new_time_total:
|
||||||
self._time_total = new_time_total
|
self._time_total = new_time_total
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from enum import IntEnum
|
||||||
|
from typing import Callable, List, Optional, Union
|
||||||
|
|
||||||
from UM.Decorators import deprecated
|
from UM.Decorators import deprecated
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
@ -12,9 +14,6 @@ from UM.Signal import signalemitter
|
||||||
from UM.Qt.QtApplication import QtApplication
|
from UM.Qt.QtApplication import QtApplication
|
||||||
from UM.FlameProfiler import pyqtSlot
|
from UM.FlameProfiler import pyqtSlot
|
||||||
|
|
||||||
from enum import IntEnum # For the connection state tracking.
|
|
||||||
from typing import Callable, List, Optional, Union
|
|
||||||
|
|
||||||
MYPY = False
|
MYPY = False
|
||||||
if MYPY:
|
if MYPY:
|
||||||
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
|
||||||
|
@ -28,11 +27,18 @@ i18n_catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
## The current processing state of the backend.
|
## The current processing state of the backend.
|
||||||
class ConnectionState(IntEnum):
|
class ConnectionState(IntEnum):
|
||||||
closed = 0
|
Closed = 0
|
||||||
connecting = 1
|
Connecting = 1
|
||||||
connected = 2
|
Connected = 2
|
||||||
busy = 3
|
Busy = 3
|
||||||
error = 4
|
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.
|
## Printer output device adds extra interface options on top of output device.
|
||||||
|
@ -46,6 +52,7 @@ class ConnectionState(IntEnum):
|
||||||
# For all other uses it should be used in the same way as a "regular" OutputDevice.
|
# For all other uses it should be used in the same way as a "regular" OutputDevice.
|
||||||
@signalemitter
|
@signalemitter
|
||||||
class PrinterOutputDevice(QObject, OutputDevice):
|
class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
|
|
||||||
printersChanged = pyqtSignal()
|
printersChanged = pyqtSignal()
|
||||||
connectionStateChanged = pyqtSignal(str)
|
connectionStateChanged = pyqtSignal(str)
|
||||||
acceptsCommandsChanged = pyqtSignal()
|
acceptsCommandsChanged = pyqtSignal()
|
||||||
|
@ -62,33 +69,34 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
# Signal to indicate that the configuration of one of the printers has changed.
|
# Signal to indicate that the configuration of one of the printers has changed.
|
||||||
uniqueConfigurationsChanged = pyqtSignal()
|
uniqueConfigurationsChanged = pyqtSignal()
|
||||||
|
|
||||||
def __init__(self, device_id: str, parent: QObject = None) -> None:
|
def __init__(self, device_id: str, connection_type: "ConnectionType" = ConnectionType.NotConnected, parent: QObject = None) -> None:
|
||||||
super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance
|
super().__init__(device_id = device_id, parent = parent) # type: ignore # MyPy complains with the multiple inheritance
|
||||||
|
|
||||||
self._printers = [] # type: List[PrinterOutputModel]
|
self._printers = [] # type: List[PrinterOutputModel]
|
||||||
self._unique_configurations = [] # type: List[ConfigurationModel]
|
self._unique_configurations = [] # type: List[ConfigurationModel]
|
||||||
|
|
||||||
self._monitor_view_qml_path = "" #type: str
|
self._monitor_view_qml_path = "" # type: str
|
||||||
self._monitor_component = None #type: Optional[QObject]
|
self._monitor_component = None # type: Optional[QObject]
|
||||||
self._monitor_item = None #type: Optional[QObject]
|
self._monitor_item = None # type: Optional[QObject]
|
||||||
|
|
||||||
self._control_view_qml_path = "" #type: str
|
self._control_view_qml_path = "" # type: str
|
||||||
self._control_component = None #type: Optional[QObject]
|
self._control_component = None # type: Optional[QObject]
|
||||||
self._control_item = None #type: Optional[QObject]
|
self._control_item = None # type: Optional[QObject]
|
||||||
|
|
||||||
self._accepts_commands = False #type: bool
|
self._accepts_commands = False # type: bool
|
||||||
|
|
||||||
self._update_timer = QTimer() #type: QTimer
|
self._update_timer = QTimer() # type: QTimer
|
||||||
self._update_timer.setInterval(2000) # TODO; Add preference for update interval
|
self._update_timer.setInterval(2000) # TODO; Add preference for update interval
|
||||||
self._update_timer.setSingleShot(False)
|
self._update_timer.setSingleShot(False)
|
||||||
self._update_timer.timeout.connect(self._update)
|
self._update_timer.timeout.connect(self._update)
|
||||||
|
|
||||||
self._connection_state = ConnectionState.closed #type: ConnectionState
|
self._connection_state = ConnectionState.Closed # type: ConnectionState
|
||||||
|
self._connection_type = connection_type # type: ConnectionType
|
||||||
|
|
||||||
self._firmware_updater = None #type: Optional[FirmwareUpdater]
|
self._firmware_updater = None # type: Optional[FirmwareUpdater]
|
||||||
self._firmware_name = None #type: Optional[str]
|
self._firmware_name = None # type: Optional[str]
|
||||||
self._address = "" #type: str
|
self._address = "" # type: str
|
||||||
self._connection_text = "" #type: str
|
self._connection_text = "" # type: str
|
||||||
self.printersChanged.connect(self._onPrintersChanged)
|
self.printersChanged.connect(self._onPrintersChanged)
|
||||||
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
|
||||||
|
|
||||||
|
@ -110,15 +118,19 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
callback(QMessageBox.Yes)
|
callback(QMessageBox.Yes)
|
||||||
|
|
||||||
def isConnected(self) -> bool:
|
def isConnected(self) -> bool:
|
||||||
return self._connection_state != ConnectionState.closed and self._connection_state != ConnectionState.error
|
return self._connection_state != ConnectionState.Closed and self._connection_state != ConnectionState.Error
|
||||||
|
|
||||||
def setConnectionState(self, connection_state: ConnectionState) -> None:
|
def setConnectionState(self, connection_state: "ConnectionState") -> None:
|
||||||
if self._connection_state != connection_state:
|
if self._connection_state != connection_state:
|
||||||
self._connection_state = connection_state
|
self._connection_state = connection_state
|
||||||
self.connectionStateChanged.emit(self._id)
|
self.connectionStateChanged.emit(self._id)
|
||||||
|
|
||||||
@pyqtProperty(str, notify = connectionStateChanged)
|
@pyqtProperty(int, constant = True)
|
||||||
def connectionState(self) -> ConnectionState:
|
def connectionType(self) -> "ConnectionType":
|
||||||
|
return self._connection_type
|
||||||
|
|
||||||
|
@pyqtProperty(int, notify = connectionStateChanged)
|
||||||
|
def connectionState(self) -> "ConnectionState":
|
||||||
return self._connection_state
|
return self._connection_state
|
||||||
|
|
||||||
def _update(self) -> None:
|
def _update(self) -> None:
|
||||||
|
@ -131,7 +143,8 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None:
|
def requestWrite(self, nodes: List["SceneNode"], file_name: Optional[str] = None, limit_mimetypes: bool = False,
|
||||||
|
file_handler: Optional["FileHandler"] = None, **kwargs: str) -> None:
|
||||||
raise NotImplementedError("requestWrite needs to be implemented")
|
raise NotImplementedError("requestWrite needs to be implemented")
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = printersChanged)
|
@pyqtProperty(QObject, notify = printersChanged)
|
||||||
|
@ -174,13 +187,13 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
|
|
||||||
## Attempt to establish connection
|
## Attempt to establish connection
|
||||||
def connect(self) -> None:
|
def connect(self) -> None:
|
||||||
self.setConnectionState(ConnectionState.connecting)
|
self.setConnectionState(ConnectionState.Connecting)
|
||||||
self._update_timer.start()
|
self._update_timer.start()
|
||||||
|
|
||||||
## Attempt to close the connection
|
## Attempt to close the connection
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
self._update_timer.stop()
|
self._update_timer.stop()
|
||||||
self.setConnectionState(ConnectionState.closed)
|
self.setConnectionState(ConnectionState.Closed)
|
||||||
|
|
||||||
## Ensure that close gets called when object is destroyed
|
## Ensure that close gets called when object is destroyed
|
||||||
def __del__(self) -> None:
|
def __del__(self) -> None:
|
||||||
|
@ -207,10 +220,17 @@ class PrinterOutputDevice(QObject, OutputDevice):
|
||||||
return self._unique_configurations
|
return self._unique_configurations
|
||||||
|
|
||||||
def _updateUniqueConfigurations(self) -> None:
|
def _updateUniqueConfigurations(self) -> None:
|
||||||
self._unique_configurations = list(set([printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None]))
|
self._unique_configurations = sorted(
|
||||||
self._unique_configurations.sort(key = lambda k: k.printerType)
|
{printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None},
|
||||||
|
key=lambda config: config.printerType,
|
||||||
|
)
|
||||||
self.uniqueConfigurationsChanged.emit()
|
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:
|
def _onPrintersChanged(self) -> None:
|
||||||
for printer in self._printers:
|
for printer in self._printers:
|
||||||
printer.configurationChanged.connect(self._updateUniqueConfigurations)
|
printer.configurationChanged.connect(self._updateUniqueConfigurations)
|
||||||
|
|
|
@ -187,7 +187,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
for child in self._node.getChildren():
|
for child in self._node.getChildren():
|
||||||
child_hull = child.callDecoration("_compute2DConvexHull")
|
child_hull = child.callDecoration("_compute2DConvexHull")
|
||||||
if child_hull:
|
if child_hull:
|
||||||
points = numpy.append(points, child_hull.getPoints(), axis = 0)
|
try:
|
||||||
|
points = numpy.append(points, child_hull.getPoints(), axis = 0)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
if points.size < 3:
|
if points.size < 3:
|
||||||
return None
|
return None
|
||||||
|
@ -239,7 +242,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
|
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
|
||||||
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
|
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
|
||||||
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
|
numpy.dtype((numpy.void, vertex_data.dtype.itemsize * vertex_data.shape[1])))
|
||||||
_, idx = numpy.unique(vertex_byte_view, return_index=True)
|
_, idx = numpy.unique(vertex_byte_view, return_index = True)
|
||||||
vertex_data = vertex_data[idx] # Select the unique rows by index.
|
vertex_data = vertex_data[idx] # Select the unique rows by index.
|
||||||
|
|
||||||
hull = Polygon(vertex_data)
|
hull = Polygon(vertex_data)
|
||||||
|
@ -272,7 +275,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
|
||||||
head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
|
head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
|
||||||
|
|
||||||
# Min head hull is used for the push free
|
# Min head hull is used for the push free
|
||||||
convex_hull = self._compute2DConvexHeadFull()
|
convex_hull = self._compute2DConvexHull()
|
||||||
if convex_hull:
|
if convex_hull:
|
||||||
return convex_hull.getMinkowskiHull(head_and_fans)
|
return convex_hull.getMinkowskiHull(head_and_fans)
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2015 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
from UM.Math.Polygon import Polygon
|
||||||
|
from UM.Qt.QtApplication import QtApplication
|
||||||
from UM.Scene.SceneNode import SceneNode
|
from UM.Scene.SceneNode import SceneNode
|
||||||
from UM.Resources import Resources
|
from UM.Resources import Resources
|
||||||
from UM.Math.Color import Color
|
from UM.Math.Color import Color
|
||||||
|
@ -16,7 +19,7 @@ class ConvexHullNode(SceneNode):
|
||||||
# location an object uses on the buildplate. This area (or area's in case of one at a time printing) is
|
# location an object uses on the buildplate. This area (or area's in case of one at a time printing) is
|
||||||
# then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
|
# then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
|
||||||
# to represent the raft as well.
|
# to represent the raft as well.
|
||||||
def __init__(self, node, hull, thickness, parent = None):
|
def __init__(self, node: SceneNode, hull: Optional[Polygon], thickness: float, parent: Optional[SceneNode] = None) -> None:
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.setCalculateBoundingBox(False)
|
self.setCalculateBoundingBox(False)
|
||||||
|
@ -25,7 +28,11 @@ class ConvexHullNode(SceneNode):
|
||||||
|
|
||||||
# Color of the drawn convex hull
|
# Color of the drawn convex hull
|
||||||
if not Application.getInstance().getIsHeadLess():
|
if not Application.getInstance().getIsHeadLess():
|
||||||
self._color = Color(*Application.getInstance().getTheme().getColor("convex_hull").getRgb())
|
theme = QtApplication.getInstance().getTheme()
|
||||||
|
if theme:
|
||||||
|
self._color = Color(*theme.getColor("convex_hull").getRgb())
|
||||||
|
else:
|
||||||
|
self._color = Color(0, 0, 0)
|
||||||
else:
|
else:
|
||||||
self._color = Color(0, 0, 0)
|
self._color = Color(0, 0, 0)
|
||||||
|
|
||||||
|
@ -47,7 +54,7 @@ class ConvexHullNode(SceneNode):
|
||||||
|
|
||||||
if hull_mesh_builder.addConvexPolygonExtrusion(
|
if hull_mesh_builder.addConvexPolygonExtrusion(
|
||||||
self._hull.getPoints()[::-1], # bottom layer is reversed
|
self._hull.getPoints()[::-1], # bottom layer is reversed
|
||||||
self._mesh_height-thickness, self._mesh_height, color=self._color):
|
self._mesh_height - thickness, self._mesh_height, color = self._color):
|
||||||
|
|
||||||
hull_mesh = hull_mesh_builder.build()
|
hull_mesh = hull_mesh_builder.build()
|
||||||
self.setMeshData(hull_mesh)
|
self.setMeshData(hull_mesh)
|
||||||
|
@ -75,7 +82,7 @@ class ConvexHullNode(SceneNode):
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _onNodeDecoratorsChanged(self, node):
|
def _onNodeDecoratorsChanged(self, node: SceneNode) -> None:
|
||||||
convex_hull_head = self._node.callDecoration("getConvexHullHead")
|
convex_hull_head = self._node.callDecoration("getConvexHullHead")
|
||||||
if convex_hull_head:
|
if convex_hull_head:
|
||||||
convex_hull_head_builder = MeshBuilder()
|
convex_hull_head_builder = MeshBuilder()
|
||||||
|
|
|
@ -419,13 +419,13 @@ class ContainerManager(QObject):
|
||||||
self._container_name_filters[name_filter] = entry
|
self._container_name_filters[name_filter] = entry
|
||||||
|
|
||||||
## Import single profile, file_url does not have to end with curaprofile
|
## Import single profile, file_url does not have to end with curaprofile
|
||||||
@pyqtSlot(QUrl, result="QVariantMap")
|
@pyqtSlot(QUrl, result = "QVariantMap")
|
||||||
def importProfile(self, file_url: QUrl):
|
def importProfile(self, file_url: QUrl) -> Dict[str, str]:
|
||||||
if not file_url.isValid():
|
if not file_url.isValid():
|
||||||
return
|
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
|
||||||
path = file_url.toLocalFile()
|
path = file_url.toLocalFile()
|
||||||
if not path:
|
if not path:
|
||||||
return
|
return {"status": "error", "message": catalog.i18nc("@info:status", "Invalid file URL:") + " " + str(file_url)}
|
||||||
return self._container_registry.importProfile(path)
|
return self._container_registry.importProfile(path)
|
||||||
|
|
||||||
@pyqtSlot(QObject, QUrl, str)
|
@pyqtSlot(QObject, QUrl, str)
|
||||||
|
|
|
@ -5,12 +5,12 @@ import os
|
||||||
import re
|
import re
|
||||||
import configparser
|
import configparser
|
||||||
|
|
||||||
from typing import cast, Optional
|
from typing import cast, Dict, Optional
|
||||||
|
|
||||||
from PyQt5.QtWidgets import QMessageBox
|
from PyQt5.QtWidgets import QMessageBox
|
||||||
|
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
from UM.Settings.ContainerFormatError import ContainerFormatError
|
from UM.Settings.ContainerFormatError import ContainerFormatError
|
||||||
|
from UM.Settings.Interfaces import ContainerInterface
|
||||||
from UM.Settings.ContainerRegistry import ContainerRegistry
|
from UM.Settings.ContainerRegistry import ContainerRegistry
|
||||||
from UM.Settings.ContainerStack import ContainerStack
|
from UM.Settings.ContainerStack import ContainerStack
|
||||||
from UM.Settings.InstanceContainer import InstanceContainer
|
from UM.Settings.InstanceContainer import InstanceContainer
|
||||||
|
@ -28,7 +28,7 @@ from . import GlobalStack
|
||||||
|
|
||||||
import cura.CuraApplication
|
import cura.CuraApplication
|
||||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||||
from cura.ReaderWriters.ProfileReader import NoProfileException
|
from cura.ReaderWriters.ProfileReader import NoProfileException, ProfileReader
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
catalog = i18nCatalog("cura")
|
catalog = i18nCatalog("cura")
|
||||||
|
@ -161,20 +161,20 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
|
|
||||||
## Imports a profile from a file
|
## Imports a profile from a file
|
||||||
#
|
#
|
||||||
# \param file_name \type{str} the full path and filename of the profile to import
|
# \param file_name The full path and filename of the profile to import.
|
||||||
# \return \type{Dict} dict with a 'status' key containing the string 'ok' or 'error', and a 'message' key
|
# \return Dict with a 'status' key containing the string 'ok' or 'error',
|
||||||
# containing a message for the user
|
# and a 'message' key containing a message for the user.
|
||||||
def importProfile(self, file_name):
|
def importProfile(self, file_name: str) -> Dict[str, str]:
|
||||||
Logger.log("d", "Attempting to import profile %s", file_name)
|
Logger.log("d", "Attempting to import profile %s", file_name)
|
||||||
if not file_name:
|
if not file_name:
|
||||||
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename> or <message>!", "Failed to import profile from <filename>{0}</filename>: <message>{1}</message>", file_name, "Invalid path")}
|
return { "status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Failed to import profile from <filename>{0}</filename>: {1}", file_name, "Invalid path")}
|
||||||
|
|
||||||
plugin_registry = PluginRegistry.getInstance()
|
plugin_registry = PluginRegistry.getInstance()
|
||||||
extension = file_name.split(".")[-1]
|
extension = file_name.split(".")[-1]
|
||||||
|
|
||||||
global_stack = Application.getInstance().getGlobalContainerStack()
|
global_stack = Application.getInstance().getGlobalContainerStack()
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
return
|
return {"status": "error", "message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "Can't import profile from <filename>{0}</filename> before a printer is added.", file_name)}
|
||||||
|
|
||||||
machine_extruders = []
|
machine_extruders = []
|
||||||
for position in sorted(global_stack.extruders):
|
for position in sorted(global_stack.extruders):
|
||||||
|
@ -183,7 +183,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
for plugin_id, meta_data in self._getIOPlugins("profile_reader"):
|
||||||
if meta_data["profile_reader"][0]["extension"] != extension:
|
if meta_data["profile_reader"][0]["extension"] != extension:
|
||||||
continue
|
continue
|
||||||
profile_reader = plugin_registry.getPluginObject(plugin_id)
|
profile_reader = cast(ProfileReader, plugin_registry.getPluginObject(plugin_id))
|
||||||
try:
|
try:
|
||||||
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
|
profile_or_list = profile_reader.read(file_name) # Try to open the file with the profile reader.
|
||||||
except NoProfileException:
|
except NoProfileException:
|
||||||
|
@ -221,13 +221,13 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
# Make sure we have a profile_definition in the file:
|
# Make sure we have a profile_definition in the file:
|
||||||
if profile_definition is None:
|
if profile_definition is None:
|
||||||
break
|
break
|
||||||
machine_definition = self.findDefinitionContainers(id = profile_definition)
|
machine_definitions = self.findDefinitionContainers(id = profile_definition)
|
||||||
if not machine_definition:
|
if not machine_definitions:
|
||||||
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
|
Logger.log("e", "Incorrect profile [%s]. Unknown machine type [%s]", file_name, profile_definition)
|
||||||
return {"status": "error",
|
return {"status": "error",
|
||||||
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)
|
"message": catalog.i18nc("@info:status Don't translate the XML tags <filename>!", "This profile <filename>{0}</filename> contains incorrect data, could not import it.", file_name)
|
||||||
}
|
}
|
||||||
machine_definition = machine_definition[0]
|
machine_definition = machine_definitions[0]
|
||||||
|
|
||||||
# Get the expected machine definition.
|
# Get the expected machine definition.
|
||||||
# i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode...
|
# i.e.: We expect gcode for a UM2 Extended to be defined as normal UM2 gcode...
|
||||||
|
@ -274,11 +274,12 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
setting_value = global_profile.getProperty(qc_setting_key, "value")
|
setting_value = global_profile.getProperty(qc_setting_key, "value")
|
||||||
|
|
||||||
setting_definition = global_stack.getSettingDefinition(qc_setting_key)
|
setting_definition = global_stack.getSettingDefinition(qc_setting_key)
|
||||||
new_instance = SettingInstance(setting_definition, profile)
|
if setting_definition is not None:
|
||||||
new_instance.setProperty("value", setting_value)
|
new_instance = SettingInstance(setting_definition, profile)
|
||||||
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
new_instance.setProperty("value", setting_value)
|
||||||
profile.addInstance(new_instance)
|
new_instance.resetState() # Ensure that the state is not seen as a user state.
|
||||||
profile.setDirty(True)
|
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)
|
extruder_profiles.append(profile)
|
||||||
|
@ -290,7 +291,7 @@ class CuraContainerRegistry(ContainerRegistry):
|
||||||
for profile_index, profile in enumerate(profile_or_list):
|
for profile_index, profile in enumerate(profile_or_list):
|
||||||
if profile_index == 0:
|
if profile_index == 0:
|
||||||
# This is assumed to be the global profile
|
# This is assumed to be the global profile
|
||||||
profile_id = (global_stack.getBottom().getId() + "_" + name_seed).lower().replace(" ", "_")
|
profile_id = (cast(ContainerInterface, global_stack.getBottom()).getId() + "_" + name_seed).lower().replace(" ", "_")
|
||||||
|
|
||||||
elif profile_index < len(machine_extruders) + 1:
|
elif profile_index < len(machine_extruders) + 1:
|
||||||
# This is assumed to be an extruder profile
|
# This is assumed to be an extruder profile
|
||||||
|
|
|
@ -5,6 +5,7 @@ from typing import Any, List, Optional, TYPE_CHECKING
|
||||||
|
|
||||||
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
|
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
|
||||||
from UM.Settings.SettingFunction import SettingFunction
|
from UM.Settings.SettingFunction import SettingFunction
|
||||||
|
from UM.Logger import Logger
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from cura.CuraApplication import CuraApplication
|
from cura.CuraApplication import CuraApplication
|
||||||
|
@ -38,7 +39,11 @@ class CuraFormulaFunctions:
|
||||||
extruder_position = int(machine_manager.defaultExtruderPosition)
|
extruder_position = int(machine_manager.defaultExtruderPosition)
|
||||||
|
|
||||||
global_stack = machine_manager.activeMachine
|
global_stack = machine_manager.activeMachine
|
||||||
extruder_stack = global_stack.extruders[str(extruder_position)]
|
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))
|
||||||
|
return None
|
||||||
|
|
||||||
value = extruder_stack.getRawProperty(property_key, "value", context = context)
|
value = extruder_stack.getRawProperty(property_key, "value", context = context)
|
||||||
if isinstance(value, SettingFunction):
|
if isinstance(value, SettingFunction):
|
||||||
|
|
|
@ -63,7 +63,7 @@ class ExtruderManager(QObject):
|
||||||
if not self._application.getGlobalContainerStack():
|
if not self._application.getGlobalContainerStack():
|
||||||
return None # No active machine, so no active extruder.
|
return None # No active machine, so no active extruder.
|
||||||
try:
|
try:
|
||||||
return self._extruder_trains[self._application.getGlobalContainerStack().getId()][str(self._active_extruder_index)].getId()
|
return self._extruder_trains[self._application.getGlobalContainerStack().getId()][str(self.activeExtruderIndex)].getId()
|
||||||
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
|
except KeyError: # Extruder index could be -1 if the global tab is selected, or the entry doesn't exist if the machine definition is wrong.
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
@ -83,8 +83,9 @@ class ExtruderManager(QObject):
|
||||||
# \param index The index of the new active extruder.
|
# \param index The index of the new active extruder.
|
||||||
@pyqtSlot(int)
|
@pyqtSlot(int)
|
||||||
def setActiveExtruderIndex(self, index: int) -> None:
|
def setActiveExtruderIndex(self, index: int) -> None:
|
||||||
self._active_extruder_index = index
|
if self._active_extruder_index != index:
|
||||||
self.activeExtruderChanged.emit()
|
self._active_extruder_index = index
|
||||||
|
self.activeExtruderChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(int, notify = activeExtruderChanged)
|
@pyqtProperty(int, notify = activeExtruderChanged)
|
||||||
def activeExtruderIndex(self) -> int:
|
def activeExtruderIndex(self) -> int:
|
||||||
|
@ -144,7 +145,7 @@ class ExtruderManager(QObject):
|
||||||
|
|
||||||
@pyqtSlot(result = QObject)
|
@pyqtSlot(result = QObject)
|
||||||
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
|
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
|
||||||
return self.getExtruderStack(self._active_extruder_index)
|
return self.getExtruderStack(self.activeExtruderIndex)
|
||||||
|
|
||||||
## Get an extruder stack by index
|
## Get an extruder stack by index
|
||||||
def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
|
def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
|
||||||
|
@ -300,12 +301,7 @@ class ExtruderManager(QObject):
|
||||||
global_stack = self._application.getGlobalContainerStack()
|
global_stack = self._application.getGlobalContainerStack()
|
||||||
if not global_stack:
|
if not global_stack:
|
||||||
return []
|
return []
|
||||||
|
return global_stack.extruderList
|
||||||
result_tuple_list = sorted(list(global_stack.extruders.items()), key = lambda x: int(x[0]))
|
|
||||||
result_list = [item[1] for item in result_tuple_list]
|
|
||||||
|
|
||||||
machine_extruder_count = global_stack.getProperty("machine_extruder_count", "value")
|
|
||||||
return result_list[:machine_extruder_count]
|
|
||||||
|
|
||||||
def _globalContainerStackChanged(self) -> None:
|
def _globalContainerStackChanged(self) -> None:
|
||||||
# If the global container changed, the machine changed and might have extruders that were not registered yet
|
# If the global container changed, the machine changed and might have extruders that were not registered yet
|
||||||
|
@ -344,6 +340,7 @@ class ExtruderManager(QObject):
|
||||||
if extruders_changed:
|
if extruders_changed:
|
||||||
self.extrudersChanged.emit(global_stack_id)
|
self.extrudersChanged.emit(global_stack_id)
|
||||||
self.setActiveExtruderIndex(0)
|
self.setActiveExtruderIndex(0)
|
||||||
|
self.activeExtruderChanged.emit()
|
||||||
|
|
||||||
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
|
# After 3.4, all single-extrusion machines have their own extruder definition files instead of reusing
|
||||||
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
|
# "fdmextruder". We need to check a machine here so its extruder definition is correct according to this.
|
||||||
|
|
|
@ -52,8 +52,8 @@ class ExtruderStack(CuraContainerStack):
|
||||||
return super().getNextStack()
|
return super().getNextStack()
|
||||||
|
|
||||||
def setEnabled(self, enabled: bool) -> None:
|
def setEnabled(self, enabled: bool) -> None:
|
||||||
if "enabled" not in self._metadata:
|
if self.getMetaDataEntry("enabled", True) == enabled: # No change.
|
||||||
self.setMetaDataEntry("enabled", "True")
|
return # Don't emit a signal then.
|
||||||
self.setMetaDataEntry("enabled", str(enabled))
|
self.setMetaDataEntry("enabled", str(enabled))
|
||||||
self.enabledChanged.emit()
|
self.enabledChanged.emit()
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, pyqtProperty, QTimer
|
from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer
|
||||||
from typing import Iterable
|
from typing import Iterable
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
@ -24,8 +24,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
|
|
||||||
## Human-readable name of the extruder.
|
## Human-readable name of the extruder.
|
||||||
NameRole = Qt.UserRole + 2
|
NameRole = Qt.UserRole + 2
|
||||||
## Is the extruder enabled?
|
|
||||||
EnabledRole = Qt.UserRole + 9
|
|
||||||
|
|
||||||
## Colour of the material loaded in the extruder.
|
## Colour of the material loaded in the extruder.
|
||||||
ColorRole = Qt.UserRole + 3
|
ColorRole = Qt.UserRole + 3
|
||||||
|
@ -47,6 +45,12 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
VariantRole = Qt.UserRole + 7
|
VariantRole = Qt.UserRole + 7
|
||||||
StackRole = Qt.UserRole + 8
|
StackRole = Qt.UserRole + 8
|
||||||
|
|
||||||
|
MaterialBrandRole = Qt.UserRole + 9
|
||||||
|
ColorNameRole = Qt.UserRole + 10
|
||||||
|
|
||||||
|
## Is the extruder enabled?
|
||||||
|
EnabledRole = Qt.UserRole + 11
|
||||||
|
|
||||||
## List of colours to display if there is no material or the material has no known
|
## List of colours to display if there is no material or the material has no known
|
||||||
# colour.
|
# colour.
|
||||||
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
|
defaultColors = ["#ffc924", "#86ec21", "#22eeee", "#245bff", "#9124ff", "#ff24c8"]
|
||||||
|
@ -67,14 +71,13 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
self.addRoleName(self.MaterialRole, "material")
|
self.addRoleName(self.MaterialRole, "material")
|
||||||
self.addRoleName(self.VariantRole, "variant")
|
self.addRoleName(self.VariantRole, "variant")
|
||||||
self.addRoleName(self.StackRole, "stack")
|
self.addRoleName(self.StackRole, "stack")
|
||||||
|
self.addRoleName(self.MaterialBrandRole, "material_brand")
|
||||||
|
self.addRoleName(self.ColorNameRole, "color_name")
|
||||||
self._update_extruder_timer = QTimer()
|
self._update_extruder_timer = QTimer()
|
||||||
self._update_extruder_timer.setInterval(100)
|
self._update_extruder_timer.setInterval(100)
|
||||||
self._update_extruder_timer.setSingleShot(True)
|
self._update_extruder_timer.setSingleShot(True)
|
||||||
self._update_extruder_timer.timeout.connect(self.__updateExtruders)
|
self._update_extruder_timer.timeout.connect(self.__updateExtruders)
|
||||||
|
|
||||||
self._simple_names = False
|
|
||||||
|
|
||||||
self._active_machine_extruders = [] # type: Iterable[ExtruderStack]
|
self._active_machine_extruders = [] # type: Iterable[ExtruderStack]
|
||||||
self._add_optional_extruder = False
|
self._add_optional_extruder = False
|
||||||
|
|
||||||
|
@ -96,21 +99,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
def addOptionalExtruder(self):
|
def addOptionalExtruder(self):
|
||||||
return self._add_optional_extruder
|
return self._add_optional_extruder
|
||||||
|
|
||||||
## Set the simpleNames property.
|
|
||||||
def setSimpleNames(self, simple_names):
|
|
||||||
if simple_names != self._simple_names:
|
|
||||||
self._simple_names = simple_names
|
|
||||||
self.simpleNamesChanged.emit()
|
|
||||||
self._updateExtruders()
|
|
||||||
|
|
||||||
## Emitted when the simpleNames property changes.
|
|
||||||
simpleNamesChanged = pyqtSignal()
|
|
||||||
|
|
||||||
## Whether or not the model should show all definitions regardless of visibility.
|
|
||||||
@pyqtProperty(bool, fset = setSimpleNames, notify = simpleNamesChanged)
|
|
||||||
def simpleNames(self):
|
|
||||||
return self._simple_names
|
|
||||||
|
|
||||||
## Links to the stack-changed signal of the new extruders when an extruder
|
## Links to the stack-changed signal of the new extruders when an extruder
|
||||||
# is swapped out or added in the current machine.
|
# is swapped out or added in the current machine.
|
||||||
#
|
#
|
||||||
|
@ -160,7 +148,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
def __updateExtruders(self):
|
def __updateExtruders(self):
|
||||||
extruders_changed = False
|
extruders_changed = False
|
||||||
|
|
||||||
if self.rowCount() != 0:
|
if self.count != 0:
|
||||||
extruders_changed = True
|
extruders_changed = True
|
||||||
|
|
||||||
items = []
|
items = []
|
||||||
|
@ -172,7 +160,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
|
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
|
||||||
|
|
||||||
for extruder in Application.getInstance().getExtruderManager().getActiveExtruderStacks():
|
for extruder in Application.getInstance().getExtruderManager().getActiveExtruderStacks():
|
||||||
position = extruder.getMetaDataEntry("position", default = "0") # Get the position
|
position = extruder.getMetaDataEntry("position", default = "0")
|
||||||
try:
|
try:
|
||||||
position = int(position)
|
position = int(position)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
|
@ -183,7 +171,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
|
|
||||||
default_color = self.defaultColors[position] if 0 <= position < len(self.defaultColors) else self.defaultColors[0]
|
default_color = self.defaultColors[position] if 0 <= position < len(self.defaultColors) else self.defaultColors[0]
|
||||||
color = extruder.material.getMetaDataEntry("color_code", default = default_color) if extruder.material else default_color
|
color = extruder.material.getMetaDataEntry("color_code", default = default_color) if extruder.material else default_color
|
||||||
|
material_brand = extruder.material.getMetaDataEntry("brand", default = "generic")
|
||||||
|
color_name = extruder.material.getMetaDataEntry("color_name")
|
||||||
# construct an item with only the relevant information
|
# construct an item with only the relevant information
|
||||||
item = {
|
item = {
|
||||||
"id": extruder.getId(),
|
"id": extruder.getId(),
|
||||||
|
@ -195,6 +184,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
"material": extruder.material.getName() if extruder.material else "",
|
"material": extruder.material.getName() if extruder.material else "",
|
||||||
"variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core
|
"variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core
|
||||||
"stack": extruder,
|
"stack": extruder,
|
||||||
|
"material_brand": material_brand,
|
||||||
|
"color_name": color_name
|
||||||
}
|
}
|
||||||
|
|
||||||
items.append(item)
|
items.append(item)
|
||||||
|
@ -213,9 +204,14 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
|
||||||
"enabled": True,
|
"enabled": True,
|
||||||
"color": "#ffffff",
|
"color": "#ffffff",
|
||||||
"index": -1,
|
"index": -1,
|
||||||
"definition": ""
|
"definition": "",
|
||||||
|
"material": "",
|
||||||
|
"variant": "",
|
||||||
|
"stack": None,
|
||||||
|
"material_brand": "",
|
||||||
|
"color_name": "",
|
||||||
}
|
}
|
||||||
items.append(item)
|
items.append(item)
|
||||||
|
if self._items != items:
|
||||||
self.setItems(items)
|
self.setItems(items)
|
||||||
self.modelChanged.emit()
|
self.modelChanged.emit()
|
||||||
|
|
|
@ -3,8 +3,8 @@
|
||||||
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
import threading
|
import threading
|
||||||
from typing import Any, Dict, Optional, Set, TYPE_CHECKING
|
from typing import Any, Dict, Optional, Set, TYPE_CHECKING, List
|
||||||
from PyQt5.QtCore import pyqtProperty, pyqtSlot
|
from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
|
||||||
|
|
||||||
from UM.Decorators import override
|
from UM.Decorators import override
|
||||||
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
from UM.MimeTypeDatabase import MimeType, MimeTypeDatabase
|
||||||
|
@ -42,13 +42,23 @@ class GlobalStack(CuraContainerStack):
|
||||||
# Per thread we have our own resolving_settings, or strange things sometimes occur.
|
# Per thread we have our own resolving_settings, or strange things sometimes occur.
|
||||||
self._resolving_settings = defaultdict(set) #type: Dict[str, Set[str]] # keys are thread names
|
self._resolving_settings = defaultdict(set) #type: Dict[str, Set[str]] # keys are thread names
|
||||||
|
|
||||||
|
extrudersChanged = pyqtSignal()
|
||||||
|
|
||||||
## Get the list of extruders of this stack.
|
## Get the list of extruders of this stack.
|
||||||
#
|
#
|
||||||
# \return The extruders registered with this stack.
|
# \return The extruders registered with this stack.
|
||||||
@pyqtProperty("QVariantMap")
|
@pyqtProperty("QVariantMap", notify = extrudersChanged)
|
||||||
def extruders(self) -> Dict[str, "ExtruderStack"]:
|
def extruders(self) -> Dict[str, "ExtruderStack"]:
|
||||||
return self._extruders
|
return self._extruders
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify = extrudersChanged)
|
||||||
|
def extruderList(self) -> List["ExtruderStack"]:
|
||||||
|
result_tuple_list = sorted(list(self.extruders.items()), key=lambda x: int(x[0]))
|
||||||
|
result_list = [item[1] for item in result_tuple_list]
|
||||||
|
|
||||||
|
machine_extruder_count = self.getProperty("machine_extruder_count", "value")
|
||||||
|
return result_list[:machine_extruder_count]
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def getLoadingPriority(cls) -> int:
|
def getLoadingPriority(cls) -> int:
|
||||||
return 2
|
return 2
|
||||||
|
@ -87,6 +97,7 @@ class GlobalStack(CuraContainerStack):
|
||||||
return
|
return
|
||||||
|
|
||||||
self._extruders[position] = extruder
|
self._extruders[position] = extruder
|
||||||
|
self.extrudersChanged.emit()
|
||||||
Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)
|
Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)
|
||||||
|
|
||||||
## Overridden from ContainerStack
|
## Overridden from ContainerStack
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
|
|
||||||
import collections
|
import collections
|
||||||
import time
|
import time
|
||||||
|
import re
|
||||||
|
import unicodedata
|
||||||
from typing import Any, Callable, List, Dict, TYPE_CHECKING, Optional, cast
|
from typing import Any, Callable, List, Dict, TYPE_CHECKING, Optional, cast
|
||||||
|
|
||||||
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
|
||||||
|
@ -21,7 +23,7 @@ from UM.Settings.SettingFunction import SettingFunction
|
||||||
from UM.Signal import postponeSignals, CompressTechnique
|
from UM.Signal import postponeSignals, CompressTechnique
|
||||||
|
|
||||||
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
from cura.Machines.QualityManager import getMachineDefinitionIDForQualitySearch
|
||||||
from cura.PrinterOutputDevice import PrinterOutputDevice
|
from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionType
|
||||||
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
|
||||||
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
|
||||||
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
|
||||||
|
@ -62,9 +64,7 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
self._default_extruder_position = "0" # to be updated when extruders are switched on and off
|
self._default_extruder_position = "0" # to be updated when extruders are switched on and off
|
||||||
|
|
||||||
self.machine_extruder_material_update_dict = collections.defaultdict(list) #type: Dict[str, List[Callable[[], None]]]
|
self._instance_container_timer = QTimer() # type: QTimer
|
||||||
|
|
||||||
self._instance_container_timer = QTimer() #type: QTimer
|
|
||||||
self._instance_container_timer.setInterval(250)
|
self._instance_container_timer.setInterval(250)
|
||||||
self._instance_container_timer.setSingleShot(True)
|
self._instance_container_timer.setSingleShot(True)
|
||||||
self._instance_container_timer.timeout.connect(self.__emitChangedSignals)
|
self._instance_container_timer.timeout.connect(self.__emitChangedSignals)
|
||||||
|
@ -74,7 +74,7 @@ class MachineManager(QObject):
|
||||||
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
|
||||||
self._container_registry.containerLoadComplete.connect(self._onContainersChanged)
|
self._container_registry.containerLoadComplete.connect(self._onContainersChanged)
|
||||||
|
|
||||||
## When the global container is changed, active material probably needs to be updated.
|
# When the global container is changed, active material probably needs to be updated.
|
||||||
self.globalContainerChanged.connect(self.activeMaterialChanged)
|
self.globalContainerChanged.connect(self.activeMaterialChanged)
|
||||||
self.globalContainerChanged.connect(self.activeVariantChanged)
|
self.globalContainerChanged.connect(self.activeVariantChanged)
|
||||||
self.globalContainerChanged.connect(self.activeQualityChanged)
|
self.globalContainerChanged.connect(self.activeQualityChanged)
|
||||||
|
@ -86,12 +86,14 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
self._onGlobalContainerChanged()
|
self._onGlobalContainerChanged()
|
||||||
|
|
||||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
|
extruder_manager = self._application.getExtruderManager()
|
||||||
|
|
||||||
|
extruder_manager.activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
|
||||||
self._onActiveExtruderStackChanged()
|
self._onActiveExtruderStackChanged()
|
||||||
|
|
||||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeMaterialChanged)
|
extruder_manager.activeExtruderChanged.connect(self.activeMaterialChanged)
|
||||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeVariantChanged)
|
extruder_manager.activeExtruderChanged.connect(self.activeVariantChanged)
|
||||||
ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeQualityChanged)
|
extruder_manager.activeExtruderChanged.connect(self.activeQualityChanged)
|
||||||
|
|
||||||
self.globalContainerChanged.connect(self.activeStackChanged)
|
self.globalContainerChanged.connect(self.activeStackChanged)
|
||||||
self.globalValueChanged.connect(self.activeStackValueChanged)
|
self.globalValueChanged.connect(self.activeStackValueChanged)
|
||||||
|
@ -115,15 +117,15 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
self._material_incompatible_message = Message(catalog.i18nc("@info:status",
|
self._material_incompatible_message = Message(catalog.i18nc("@info:status",
|
||||||
"The selected material is incompatible with the selected machine or configuration."),
|
"The selected material is incompatible with the selected machine or configuration."),
|
||||||
title = catalog.i18nc("@info:title", "Incompatible Material")) #type: Message
|
title = catalog.i18nc("@info:title", "Incompatible Material")) # type: Message
|
||||||
|
|
||||||
containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) #type: List[InstanceContainer]
|
containers = CuraContainerRegistry.getInstance().findInstanceContainers(id = self.activeMaterialId) # type: List[InstanceContainer]
|
||||||
if containers:
|
if containers:
|
||||||
containers[0].nameChanged.connect(self._onMaterialNameChanged)
|
containers[0].nameChanged.connect(self._onMaterialNameChanged)
|
||||||
|
|
||||||
self._material_manager = self._application.getMaterialManager() #type: MaterialManager
|
self._material_manager = self._application.getMaterialManager() # type: MaterialManager
|
||||||
self._variant_manager = self._application.getVariantManager() #type: VariantManager
|
self._variant_manager = self._application.getVariantManager() # type: VariantManager
|
||||||
self._quality_manager = self._application.getQualityManager() #type: QualityManager
|
self._quality_manager = self._application.getQualityManager() # type: QualityManager
|
||||||
|
|
||||||
# When the materials lookup table gets updated, it can mean that a material has its name changed, which should
|
# When the materials lookup table gets updated, it can mean that a material has its name changed, which should
|
||||||
# be reflected on the GUI. This signal emission makes sure that it happens.
|
# be reflected on the GUI. This signal emission makes sure that it happens.
|
||||||
|
@ -156,7 +158,7 @@ class MachineManager(QObject):
|
||||||
blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly
|
blurSettings = pyqtSignal() # Emitted to force fields in the advanced sidebar to un-focus, so they update properly
|
||||||
|
|
||||||
outputDevicesChanged = pyqtSignal()
|
outputDevicesChanged = pyqtSignal()
|
||||||
currentConfigurationChanged = pyqtSignal() # Emitted every time the current configurations of the machine changes
|
currentConfigurationChanged = pyqtSignal() # Emitted every time the current configurations of the machine changes
|
||||||
printerConnectedStatusChanged = pyqtSignal() # Emitted every time the active machine change or the outputdevices change
|
printerConnectedStatusChanged = pyqtSignal() # Emitted every time the active machine change or the outputdevices change
|
||||||
|
|
||||||
rootMaterialChanged = pyqtSignal()
|
rootMaterialChanged = pyqtSignal()
|
||||||
|
@ -174,6 +176,7 @@ class MachineManager(QObject):
|
||||||
self._printer_output_devices.append(printer_output_device)
|
self._printer_output_devices.append(printer_output_device)
|
||||||
|
|
||||||
self.outputDevicesChanged.emit()
|
self.outputDevicesChanged.emit()
|
||||||
|
self.printerConnectedStatusChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(QObject, notify = currentConfigurationChanged)
|
@pyqtProperty(QObject, notify = currentConfigurationChanged)
|
||||||
def currentConfiguration(self) -> ConfigurationModel:
|
def currentConfiguration(self) -> ConfigurationModel:
|
||||||
|
@ -201,7 +204,7 @@ class MachineManager(QObject):
|
||||||
extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != empty_variant_container else None
|
extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != empty_variant_container else None
|
||||||
self._current_printer_configuration.extruderConfigurations.append(extruder_configuration)
|
self._current_printer_configuration.extruderConfigurations.append(extruder_configuration)
|
||||||
|
|
||||||
# an empty build plate configuration from the network printer is presented as an empty string, so use "" for an
|
# An empty build plate configuration from the network printer is presented as an empty string, so use "" for an
|
||||||
# empty build plate.
|
# empty build plate.
|
||||||
self._current_printer_configuration.buildplateConfiguration = self._global_container_stack.getProperty("machine_buildplate_type", "value") if self._global_container_stack.variant != empty_variant_container else ""
|
self._current_printer_configuration.buildplateConfiguration = self._global_container_stack.getProperty("machine_buildplate_type", "value") if self._global_container_stack.variant != empty_variant_container else ""
|
||||||
self.currentConfigurationChanged.emit()
|
self.currentConfigurationChanged.emit()
|
||||||
|
@ -247,7 +250,7 @@ class MachineManager(QObject):
|
||||||
self.updateNumberExtrudersEnabled()
|
self.updateNumberExtrudersEnabled()
|
||||||
self.globalContainerChanged.emit()
|
self.globalContainerChanged.emit()
|
||||||
|
|
||||||
# after switching the global stack we reconnect all the signals and set the variant and material references
|
# After switching the global stack we reconnect all the signals and set the variant and material references
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
self._application.getPreferences().setValue("cura/active_machine", self._global_container_stack.getId())
|
self._application.getPreferences().setValue("cura/active_machine", self._global_container_stack.getId())
|
||||||
|
|
||||||
|
@ -261,7 +264,7 @@ class MachineManager(QObject):
|
||||||
if global_variant.getMetaDataEntry("hardware_type") != "buildplate":
|
if global_variant.getMetaDataEntry("hardware_type") != "buildplate":
|
||||||
self._global_container_stack.setVariant(empty_variant_container)
|
self._global_container_stack.setVariant(empty_variant_container)
|
||||||
|
|
||||||
# set the global material to empty as we now use the extruder stack at all times - CURA-4482
|
# Set the global material to empty as we now use the extruder stack at all times - CURA-4482
|
||||||
global_material = self._global_container_stack.material
|
global_material = self._global_container_stack.material
|
||||||
if global_material != empty_material_container:
|
if global_material != empty_material_container:
|
||||||
self._global_container_stack.setMaterial(empty_material_container)
|
self._global_container_stack.setMaterial(empty_material_container)
|
||||||
|
@ -271,11 +274,6 @@ class MachineManager(QObject):
|
||||||
extruder_stack.propertyChanged.connect(self._onPropertyChanged)
|
extruder_stack.propertyChanged.connect(self._onPropertyChanged)
|
||||||
extruder_stack.containersChanged.connect(self._onContainersChanged)
|
extruder_stack.containersChanged.connect(self._onContainersChanged)
|
||||||
|
|
||||||
if self._global_container_stack.getId() in self.machine_extruder_material_update_dict:
|
|
||||||
for func in self.machine_extruder_material_update_dict[self._global_container_stack.getId()]:
|
|
||||||
self._application.callLater(func)
|
|
||||||
del self.machine_extruder_material_update_dict[self._global_container_stack.getId()]
|
|
||||||
|
|
||||||
self.activeQualityGroupChanged.emit()
|
self.activeQualityGroupChanged.emit()
|
||||||
|
|
||||||
def _onActiveExtruderStackChanged(self) -> None:
|
def _onActiveExtruderStackChanged(self) -> None:
|
||||||
|
@ -295,6 +293,7 @@ class MachineManager(QObject):
|
||||||
self.activeMaterialChanged.emit()
|
self.activeMaterialChanged.emit()
|
||||||
|
|
||||||
self.rootMaterialChanged.emit()
|
self.rootMaterialChanged.emit()
|
||||||
|
self.numberExtrudersEnabledChanged.emit()
|
||||||
|
|
||||||
def _onContainersChanged(self, container: ContainerInterface) -> None:
|
def _onContainersChanged(self, container: ContainerInterface) -> None:
|
||||||
self._instance_container_timer.start()
|
self._instance_container_timer.start()
|
||||||
|
@ -419,7 +418,7 @@ class MachineManager(QObject):
|
||||||
# Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are
|
# Not a very pretty solution, but the extruder manager doesn't really know how many extruders there are
|
||||||
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
|
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
|
||||||
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||||
count = 1 # we start with the global stack
|
count = 1 # We start with the global stack
|
||||||
for stack in extruder_stacks:
|
for stack in extruder_stacks:
|
||||||
md = stack.getMetaData()
|
md = stack.getMetaData()
|
||||||
if "position" in md and int(md["position"]) >= machine_extruder_count:
|
if "position" in md and int(md["position"]) >= machine_extruder_count:
|
||||||
|
@ -438,12 +437,12 @@ class MachineManager(QObject):
|
||||||
if not self._global_container_stack:
|
if not self._global_container_stack:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if self._global_container_stack.getTop().findInstances():
|
if self._global_container_stack.getTop().getNumInstances() != 0:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
||||||
for stack in stacks:
|
for stack in stacks:
|
||||||
if stack.getTop().findInstances():
|
if stack.getTop().getNumInstances() != 0:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
@ -453,10 +452,10 @@ class MachineManager(QObject):
|
||||||
if not self._global_container_stack:
|
if not self._global_container_stack:
|
||||||
return 0
|
return 0
|
||||||
num_user_settings = 0
|
num_user_settings = 0
|
||||||
num_user_settings += len(self._global_container_stack.getTop().findInstances())
|
num_user_settings += self._global_container_stack.getTop().getNumInstances()
|
||||||
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
stacks = self._global_container_stack.extruderList
|
||||||
for stack in stacks:
|
for stack in stacks:
|
||||||
num_user_settings += len(stack.getTop().findInstances())
|
num_user_settings += stack.getTop().getNumInstances()
|
||||||
return num_user_settings
|
return num_user_settings
|
||||||
|
|
||||||
## Delete a user setting from the global stack and all extruder stacks.
|
## Delete a user setting from the global stack and all extruder stacks.
|
||||||
|
@ -516,10 +515,30 @@ class MachineManager(QObject):
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||||
def printerConnected(self):
|
def printerConnected(self) -> bool:
|
||||||
return bool(self._printer_output_devices)
|
return bool(self._printer_output_devices)
|
||||||
|
|
||||||
@pyqtProperty(str, notify = printerConnectedStatusChanged)
|
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||||
|
def activeMachineHasRemoteConnection(self) -> bool:
|
||||||
|
if self._global_container_stack:
|
||||||
|
connection_type = int(self._global_container_stack.getMetaDataEntry("connection_type", ConnectionType.NotConnected.value))
|
||||||
|
return connection_type in [ConnectionType.NetworkConnection.value, ConnectionType.CloudConnection.value]
|
||||||
|
return False
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||||
|
def activeMachineIsGroup(self) -> bool:
|
||||||
|
return bool(self._printer_output_devices) and len(self._printer_output_devices[0].printers) > 1
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||||
|
def activeMachineHasActiveNetworkConnection(self) -> bool:
|
||||||
|
# A network connection is only available if any output device is actually a network connected device.
|
||||||
|
return any(d.connectionType == ConnectionType.NetworkConnection for d in self._printer_output_devices)
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
|
||||||
|
def activeMachineHasActiveCloudConnection(self) -> bool:
|
||||||
|
# A cloud connection is only available if any output device actually is a cloud connected device.
|
||||||
|
return any(d.connectionType == ConnectionType.CloudConnection for d in self._printer_output_devices)
|
||||||
|
|
||||||
def activeMachineNetworkKey(self) -> str:
|
def activeMachineNetworkKey(self) -> str:
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
return self._global_container_stack.getMetaDataEntry("um_network_key", "")
|
return self._global_container_stack.getMetaDataEntry("um_network_key", "")
|
||||||
|
@ -616,6 +635,14 @@ class MachineManager(QObject):
|
||||||
is_supported = self._current_quality_group.is_available
|
is_supported = self._current_quality_group.is_available
|
||||||
return is_supported
|
return is_supported
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = activeQualityGroupChanged)
|
||||||
|
def isActiveQualityExperimental(self) -> bool:
|
||||||
|
is_experimental = False
|
||||||
|
if self._global_container_stack:
|
||||||
|
if self._current_quality_group:
|
||||||
|
is_experimental = self._current_quality_group.is_experimental
|
||||||
|
return is_experimental
|
||||||
|
|
||||||
## Returns whether there is anything unsupported in the current set-up.
|
## Returns whether there is anything unsupported in the current set-up.
|
||||||
#
|
#
|
||||||
# The current set-up signifies the global stack and all extruder stacks,
|
# The current set-up signifies the global stack and all extruder stacks,
|
||||||
|
@ -646,7 +673,7 @@ class MachineManager(QObject):
|
||||||
new_value = self._active_container_stack.getProperty(key, "value")
|
new_value = self._active_container_stack.getProperty(key, "value")
|
||||||
extruder_stacks = [stack for stack in ExtruderManager.getInstance().getActiveExtruderStacks()]
|
extruder_stacks = [stack for stack in ExtruderManager.getInstance().getActiveExtruderStacks()]
|
||||||
|
|
||||||
# check in which stack the value has to be replaced
|
# Check in which stack the value has to be replaced
|
||||||
for extruder_stack in extruder_stacks:
|
for extruder_stack in extruder_stacks:
|
||||||
if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
|
if extruder_stack != self._active_container_stack and extruder_stack.getProperty(key, "value") != new_value:
|
||||||
extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved
|
extruder_stack.userChanges.setProperty(key, "value", new_value) # TODO: nested property access, should be improved
|
||||||
|
@ -662,7 +689,7 @@ class MachineManager(QObject):
|
||||||
for key in self._active_container_stack.userChanges.getAllKeys():
|
for key in self._active_container_stack.userChanges.getAllKeys():
|
||||||
new_value = self._active_container_stack.getProperty(key, "value")
|
new_value = self._active_container_stack.getProperty(key, "value")
|
||||||
|
|
||||||
# check if the value has to be replaced
|
# Check if the value has to be replaced
|
||||||
extruder_stack.userChanges.setProperty(key, "value", new_value)
|
extruder_stack.userChanges.setProperty(key, "value", new_value)
|
||||||
|
|
||||||
@pyqtProperty(str, notify = activeVariantChanged)
|
@pyqtProperty(str, notify = activeVariantChanged)
|
||||||
|
@ -731,7 +758,7 @@ class MachineManager(QObject):
|
||||||
# If the machine that is being removed is the currently active machine, set another machine as the active machine.
|
# If the machine that is being removed is the currently active machine, set another machine as the active machine.
|
||||||
activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id)
|
activate_new_machine = (self._global_container_stack and self._global_container_stack.getId() == machine_id)
|
||||||
|
|
||||||
# activate a new machine before removing a machine because this is safer
|
# Activate a new machine before removing a machine because this is safer
|
||||||
if activate_new_machine:
|
if activate_new_machine:
|
||||||
machine_stacks = CuraContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine")
|
machine_stacks = CuraContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine")
|
||||||
other_machine_stacks = [s for s in machine_stacks if s["id"] != machine_id]
|
other_machine_stacks = [s for s in machine_stacks if s["id"] != machine_id]
|
||||||
|
@ -739,7 +766,7 @@ class MachineManager(QObject):
|
||||||
self.setActiveMachine(other_machine_stacks[0]["id"])
|
self.setActiveMachine(other_machine_stacks[0]["id"])
|
||||||
|
|
||||||
metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0]
|
metadata = CuraContainerRegistry.getInstance().findContainerStacksMetadata(id = machine_id)[0]
|
||||||
network_key = metadata["um_network_key"] if "um_network_key" in metadata else None
|
network_key = metadata.get("um_network_key", None)
|
||||||
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
ExtruderManager.getInstance().removeMachineExtruders(machine_id)
|
||||||
containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
|
containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
|
||||||
for container in containers:
|
for container in containers:
|
||||||
|
@ -864,7 +891,7 @@ class MachineManager(QObject):
|
||||||
caution_message = Message(catalog.i18nc(
|
caution_message = Message(catalog.i18nc(
|
||||||
"@info:generic",
|
"@info:generic",
|
||||||
"Settings have been changed to match the current availability of extruders: [%s]" % ", ".join(add_user_changes)),
|
"Settings have been changed to match the current availability of extruders: [%s]" % ", ".join(add_user_changes)),
|
||||||
lifetime=0,
|
lifetime = 0,
|
||||||
title = catalog.i18nc("@info:title", "Settings updated"))
|
title = catalog.i18nc("@info:title", "Settings updated"))
|
||||||
caution_message.show()
|
caution_message.show()
|
||||||
|
|
||||||
|
@ -909,21 +936,18 @@ class MachineManager(QObject):
|
||||||
# After CURA-4482 this should not be the case anymore, but we still want to support older project files.
|
# After CURA-4482 this should not be the case anymore, but we still want to support older project files.
|
||||||
global_user_container = self._global_container_stack.userChanges
|
global_user_container = self._global_container_stack.userChanges
|
||||||
|
|
||||||
# Make sure extruder_stacks exists
|
|
||||||
extruder_stacks = [] #type: List[ExtruderStack]
|
|
||||||
|
|
||||||
if previous_extruder_count == 1:
|
|
||||||
extruder_stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
|
|
||||||
global_user_container = self._global_container_stack.userChanges
|
|
||||||
|
|
||||||
for setting_instance in global_user_container.findInstances():
|
for setting_instance in global_user_container.findInstances():
|
||||||
setting_key = setting_instance.definition.key
|
setting_key = setting_instance.definition.key
|
||||||
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
|
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
|
||||||
|
|
||||||
if settable_per_extruder:
|
if settable_per_extruder:
|
||||||
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
|
limit_to_extruder = int(self._global_container_stack.getProperty(setting_key, "limit_to_extruder"))
|
||||||
extruder_stack = extruder_stacks[max(0, limit_to_extruder)]
|
extruder_position = max(0, limit_to_extruder)
|
||||||
extruder_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
|
extruder_stack = self.getExtruder(extruder_position)
|
||||||
|
if extruder_stack:
|
||||||
|
extruder_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
|
||||||
|
else:
|
||||||
|
Logger.log("e", "Unable to find extruder on position %s", extruder_position)
|
||||||
global_user_container.removeInstance(setting_key)
|
global_user_container.removeInstance(setting_key)
|
||||||
|
|
||||||
# Signal that the global stack has changed
|
# Signal that the global stack has changed
|
||||||
|
@ -932,10 +956,9 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
@pyqtSlot(int, result = QObject)
|
@pyqtSlot(int, result = QObject)
|
||||||
def getExtruder(self, position: int) -> Optional[ExtruderStack]:
|
def getExtruder(self, position: int) -> Optional[ExtruderStack]:
|
||||||
extruder = None
|
|
||||||
if self._global_container_stack:
|
if self._global_container_stack:
|
||||||
extruder = self._global_container_stack.extruders.get(str(position))
|
return self._global_container_stack.extruders.get(str(position))
|
||||||
return extruder
|
return None
|
||||||
|
|
||||||
def updateDefaultExtruder(self) -> None:
|
def updateDefaultExtruder(self) -> None:
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None:
|
||||||
|
@ -1001,12 +1024,12 @@ class MachineManager(QObject):
|
||||||
if not enabled and position == ExtruderManager.getInstance().activeExtruderIndex:
|
if not enabled and position == ExtruderManager.getInstance().activeExtruderIndex:
|
||||||
ExtruderManager.getInstance().setActiveExtruderIndex(int(self._default_extruder_position))
|
ExtruderManager.getInstance().setActiveExtruderIndex(int(self._default_extruder_position))
|
||||||
|
|
||||||
# ensure that the quality profile is compatible with current combination, or choose a compatible one if available
|
# Ensure that the quality profile is compatible with current combination, or choose a compatible one if available
|
||||||
self._updateQualityWithMaterial()
|
self._updateQualityWithMaterial()
|
||||||
self.extruderChanged.emit()
|
self.extruderChanged.emit()
|
||||||
# update material compatibility color
|
# Update material compatibility color
|
||||||
self.activeQualityGroupChanged.emit()
|
self.activeQualityGroupChanged.emit()
|
||||||
# update items in SettingExtruder
|
# Update items in SettingExtruder
|
||||||
ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId())
|
ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId())
|
||||||
# Make sure the front end reflects changes
|
# Make sure the front end reflects changes
|
||||||
self.forceUpdateAllSettings()
|
self.forceUpdateAllSettings()
|
||||||
|
@ -1080,7 +1103,6 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
#
|
|
||||||
# Sets all quality and quality_changes containers to empty_quality and empty_quality_changes containers
|
# Sets all quality and quality_changes containers to empty_quality and empty_quality_changes containers
|
||||||
# for all stacks in the currently active machine.
|
# for all stacks in the currently active machine.
|
||||||
#
|
#
|
||||||
|
@ -1139,7 +1161,7 @@ class MachineManager(QObject):
|
||||||
|
|
||||||
def _setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
|
def _setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> None:
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None:
|
||||||
return #Can't change that.
|
return # Can't change that.
|
||||||
quality_type = quality_changes_group.quality_type
|
quality_type = quality_changes_group.quality_type
|
||||||
# A custom quality can be created based on "not supported".
|
# A custom quality can be created based on "not supported".
|
||||||
# In that case, do not set quality containers to empty.
|
# In that case, do not set quality containers to empty.
|
||||||
|
@ -1209,7 +1231,7 @@ class MachineManager(QObject):
|
||||||
self.rootMaterialChanged.emit()
|
self.rootMaterialChanged.emit()
|
||||||
|
|
||||||
def activeMaterialsCompatible(self) -> bool:
|
def activeMaterialsCompatible(self) -> bool:
|
||||||
# check material - variant compatibility
|
# Check material - variant compatibility
|
||||||
if self._global_container_stack is not None:
|
if self._global_container_stack is not None:
|
||||||
if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)):
|
if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)):
|
||||||
for position, extruder in self._global_container_stack.extruders.items():
|
for position, extruder in self._global_container_stack.extruders.items():
|
||||||
|
@ -1310,17 +1332,18 @@ class MachineManager(QObject):
|
||||||
# Get the definition id corresponding to this machine name
|
# Get the definition id corresponding to this machine name
|
||||||
machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()
|
machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()
|
||||||
# Try to find a machine with the same network key
|
# Try to find a machine with the same network key
|
||||||
new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey})
|
new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey()})
|
||||||
# If there is no machine, then create a new one and set it to the non-hidden instance
|
# If there is no machine, then create a new one and set it to the non-hidden instance
|
||||||
if not new_machine:
|
if not new_machine:
|
||||||
new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id)
|
new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id)
|
||||||
if not new_machine:
|
if not new_machine:
|
||||||
return
|
return
|
||||||
new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey)
|
new_machine.setMetaDataEntry("um_network_key", self.activeMachineNetworkKey())
|
||||||
new_machine.setMetaDataEntry("connect_group_name", self.activeMachineNetworkGroupName)
|
new_machine.setMetaDataEntry("connect_group_name", self.activeMachineNetworkGroupName)
|
||||||
new_machine.setMetaDataEntry("hidden", False)
|
new_machine.setMetaDataEntry("hidden", False)
|
||||||
|
new_machine.setMetaDataEntry("connection_type", self._global_container_stack.getMetaDataEntry("connection_type"))
|
||||||
else:
|
else:
|
||||||
Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey)
|
Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey())
|
||||||
new_machine.setMetaDataEntry("hidden", False)
|
new_machine.setMetaDataEntry("hidden", False)
|
||||||
|
|
||||||
# Set the current printer instance to hidden (the metadata entry must exist)
|
# Set the current printer instance to hidden (the metadata entry must exist)
|
||||||
|
@ -1380,10 +1403,10 @@ class MachineManager(QObject):
|
||||||
# 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
|
# 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.
|
# then all the container stacks are updated, both the current and the hidden ones.
|
||||||
def checkCorrectGroupName(self, device_id: str, group_name: str) -> None:
|
def checkCorrectGroupName(self, device_id: str, group_name: str) -> None:
|
||||||
if self._global_container_stack and device_id == self.activeMachineNetworkKey:
|
if self._global_container_stack and device_id == self.activeMachineNetworkKey():
|
||||||
# Check if the connect_group_name is correct. If not, update all the containers connected to the same printer
|
# Check if the connect_group_name is correct. If not, update all the containers connected to the same printer
|
||||||
if self.activeMachineNetworkGroupName != group_name:
|
if self.activeMachineNetworkGroupName != group_name:
|
||||||
metadata_filter = {"um_network_key": self.activeMachineNetworkKey}
|
metadata_filter = {"um_network_key": self.activeMachineNetworkKey()}
|
||||||
containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
containers = CuraContainerRegistry.getInstance().findContainerStacks(type = "machine", **metadata_filter)
|
||||||
for container in containers:
|
for container in containers:
|
||||||
container.setMetaDataEntry("connect_group_name", group_name)
|
container.setMetaDataEntry("connect_group_name", group_name)
|
||||||
|
@ -1419,7 +1442,7 @@ class MachineManager(QObject):
|
||||||
material_diameter, root_material_id)
|
material_diameter, root_material_id)
|
||||||
self.setMaterial(position, material_node)
|
self.setMaterial(position, material_node)
|
||||||
|
|
||||||
## global_stack: if you want to provide your own global_stack instead of the current active one
|
## Global_stack: if you want to provide your own global_stack instead of the current active one
|
||||||
# if you update an active machine, special measures have to be taken.
|
# if you update an active machine, special measures have to be taken.
|
||||||
@pyqtSlot(str, "QVariant")
|
@pyqtSlot(str, "QVariant")
|
||||||
def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None:
|
def setMaterial(self, position: str, container_node, global_stack: Optional["GlobalStack"] = None) -> None:
|
||||||
|
@ -1522,6 +1545,10 @@ class MachineManager(QObject):
|
||||||
def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]:
|
def activeQualityChangesGroup(self) -> Optional["QualityChangesGroup"]:
|
||||||
return self._current_quality_changes_group
|
return self._current_quality_changes_group
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = activeQualityChangesGroupChanged)
|
||||||
|
def hasCustomQuality(self) -> bool:
|
||||||
|
return self._current_quality_changes_group is not None
|
||||||
|
|
||||||
@pyqtProperty(str, notify = activeQualityGroupChanged)
|
@pyqtProperty(str, notify = activeQualityGroupChanged)
|
||||||
def activeQualityOrQualityChangesName(self) -> str:
|
def activeQualityOrQualityChangesName(self) -> str:
|
||||||
name = empty_quality_container.getName()
|
name = empty_quality_container.getName()
|
||||||
|
@ -1531,9 +1558,32 @@ class MachineManager(QObject):
|
||||||
name = self._current_quality_group.name
|
name = self._current_quality_group.name
|
||||||
return name
|
return name
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = activeQualityGroupChanged)
|
||||||
|
def hasNotSupportedQuality(self) -> bool:
|
||||||
|
return self._current_quality_group is None and self._current_quality_changes_group is None
|
||||||
|
|
||||||
def _updateUponMaterialMetadataChange(self) -> None:
|
def _updateUponMaterialMetadataChange(self) -> None:
|
||||||
if self._global_container_stack is None:
|
if self._global_container_stack is None:
|
||||||
return
|
return
|
||||||
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
|
||||||
self.updateMaterialWithVariant(None)
|
self.updateMaterialWithVariant(None)
|
||||||
self._updateQualityWithMaterial()
|
self._updateQualityWithMaterial()
|
||||||
|
|
||||||
|
## This function will translate any printer type name to an abbreviated printer type name
|
||||||
|
@pyqtSlot(str, result = str)
|
||||||
|
def getAbbreviatedMachineName(self, machine_type_name: str) -> str:
|
||||||
|
abbr_machine = ""
|
||||||
|
for word in re.findall(r"[\w']+", machine_type_name):
|
||||||
|
if word.lower() == "ultimaker":
|
||||||
|
abbr_machine += "UM"
|
||||||
|
elif word.isdigit():
|
||||||
|
abbr_machine += word
|
||||||
|
else:
|
||||||
|
stripped_word = "".join(char for char in unicodedata.normalize("NFD", word.upper()) if unicodedata.category(char) != "Mn")
|
||||||
|
# - use only the first character if the word is too long (> 3 characters)
|
||||||
|
# - use the whole word if it's not too long (<= 3 characters)
|
||||||
|
if len(stripped_word) > 3:
|
||||||
|
stripped_word = stripped_word[0]
|
||||||
|
abbr_machine += stripped_word
|
||||||
|
|
||||||
|
return abbr_machine
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2017 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
from typing import Set
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty
|
from PyQt5.QtCore import QObject, pyqtSignal, pyqtProperty, pyqtSlot
|
||||||
|
|
||||||
from UM.Application import Application
|
from UM.Application import Application
|
||||||
|
|
||||||
|
@ -16,15 +17,11 @@ class SimpleModeSettingsManager(QObject):
|
||||||
self._is_profile_user_created = False # True when profile was custom created by user
|
self._is_profile_user_created = False # True when profile was custom created by user
|
||||||
|
|
||||||
self._machine_manager.activeStackValueChanged.connect(self._updateIsProfileCustomized)
|
self._machine_manager.activeStackValueChanged.connect(self._updateIsProfileCustomized)
|
||||||
self._machine_manager.activeQualityGroupChanged.connect(self._updateIsProfileUserCreated)
|
|
||||||
self._machine_manager.activeQualityChangesGroupChanged.connect(self._updateIsProfileUserCreated)
|
|
||||||
|
|
||||||
# update on create as the activeQualityChanged signal is emitted before this manager is created when Cura starts
|
# update on create as the activeQualityChanged signal is emitted before this manager is created when Cura starts
|
||||||
self._updateIsProfileCustomized()
|
self._updateIsProfileCustomized()
|
||||||
self._updateIsProfileUserCreated()
|
|
||||||
|
|
||||||
isProfileCustomizedChanged = pyqtSignal()
|
isProfileCustomizedChanged = pyqtSignal()
|
||||||
isProfileUserCreatedChanged = pyqtSignal()
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = isProfileCustomizedChanged)
|
@pyqtProperty(bool, notify = isProfileCustomizedChanged)
|
||||||
def isProfileCustomized(self):
|
def isProfileCustomized(self):
|
||||||
|
@ -57,33 +54,6 @@ class SimpleModeSettingsManager(QObject):
|
||||||
self._is_profile_customized = has_customized_user_settings
|
self._is_profile_customized = has_customized_user_settings
|
||||||
self.isProfileCustomizedChanged.emit()
|
self.isProfileCustomizedChanged.emit()
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = isProfileUserCreatedChanged)
|
|
||||||
def isProfileUserCreated(self):
|
|
||||||
return self._is_profile_user_created
|
|
||||||
|
|
||||||
def _updateIsProfileUserCreated(self):
|
|
||||||
quality_changes_keys = set()
|
|
||||||
|
|
||||||
if not self._machine_manager.activeMachine:
|
|
||||||
return False
|
|
||||||
|
|
||||||
global_stack = self._machine_manager.activeMachine
|
|
||||||
|
|
||||||
# check quality changes settings in the global stack
|
|
||||||
quality_changes_keys.update(global_stack.qualityChanges.getAllKeys())
|
|
||||||
|
|
||||||
# check quality changes settings in the extruder stacks
|
|
||||||
if global_stack.extruders:
|
|
||||||
for extruder_stack in global_stack.extruders.values():
|
|
||||||
quality_changes_keys.update(extruder_stack.qualityChanges.getAllKeys())
|
|
||||||
|
|
||||||
# check if the qualityChanges container is not empty (meaning it is a user created profile)
|
|
||||||
has_quality_changes = len(quality_changes_keys) > 0
|
|
||||||
|
|
||||||
if has_quality_changes != self._is_profile_user_created:
|
|
||||||
self._is_profile_user_created = has_quality_changes
|
|
||||||
self.isProfileUserCreatedChanged.emit()
|
|
||||||
|
|
||||||
# These are the settings included in the Simple ("Recommended") Mode, so only when the other settings have been
|
# These are the settings included in the Simple ("Recommended") Mode, so only when the other settings have been
|
||||||
# changed, we consider it as a user customized profile in the Simple ("Recommended") Mode.
|
# changed, we consider it as a user customized profile in the Simple ("Recommended") Mode.
|
||||||
__ignored_custom_setting_keys = ["support_enable",
|
__ignored_custom_setting_keys = ["support_enable",
|
||||||
|
|
|
@ -1,23 +1,29 @@
|
||||||
# Copyright (c) 2017 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from PyQt5.QtCore import pyqtProperty, QUrl
|
from PyQt5.QtCore import pyqtProperty, QUrl
|
||||||
|
|
||||||
from UM.Stage import Stage
|
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):
|
class CuraStage(Stage):
|
||||||
|
def __init__(self, parent = None) -> None:
|
||||||
def __init__(self, parent = None):
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
@pyqtProperty(str, constant = True)
|
@pyqtProperty(str, constant = True)
|
||||||
def stageId(self):
|
def stageId(self) -> str:
|
||||||
return self.getPluginId()
|
return self.getPluginId()
|
||||||
|
|
||||||
@pyqtProperty(QUrl, constant = True)
|
@pyqtProperty(QUrl, constant = True)
|
||||||
def mainComponent(self):
|
def mainComponent(self) -> QUrl:
|
||||||
return self.getDisplayComponent("main")
|
return self.getDisplayComponent("main")
|
||||||
|
|
||||||
@pyqtProperty(QUrl, constant = True)
|
@pyqtProperty(QUrl, constant = True)
|
||||||
def sidebarComponent(self):
|
def stageMenuComponent(self) -> QUrl:
|
||||||
return self.getDisplayComponent("sidebar")
|
return self.getDisplayComponent("menu")
|
30
cura/UltimakerCloudAuthentication.py
Normal file
30
cura/UltimakerCloudAuthentication.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
# ---------
|
||||||
|
# Constants used for the Cloud API
|
||||||
|
# ---------
|
||||||
|
DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" # type: str
|
||||||
|
DEFAULT_CLOUD_API_VERSION = "1" # type: str
|
||||||
|
DEFAULT_CLOUD_ACCOUNT_API_ROOT = "https://account.ultimaker.com" # type: str
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraCloudAPIRoot # type: ignore
|
||||||
|
if CuraCloudAPIRoot == "":
|
||||||
|
CuraCloudAPIRoot = DEFAULT_CLOUD_API_ROOT
|
||||||
|
except ImportError:
|
||||||
|
CuraCloudAPIRoot = DEFAULT_CLOUD_API_ROOT
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraCloudAPIVersion # type: ignore
|
||||||
|
if CuraCloudAPIVersion == "":
|
||||||
|
CuraCloudAPIVersion = DEFAULT_CLOUD_API_VERSION
|
||||||
|
except ImportError:
|
||||||
|
CuraCloudAPIVersion = DEFAULT_CLOUD_API_VERSION
|
||||||
|
|
||||||
|
try:
|
||||||
|
from cura.CuraVersion import CuraCloudAccountAPIRoot # type: ignore
|
||||||
|
if CuraCloudAccountAPIRoot == "":
|
||||||
|
CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT
|
||||||
|
except ImportError:
|
||||||
|
CuraCloudAccountAPIRoot = DEFAULT_CLOUD_ACCOUNT_API_ROOT
|
|
@ -17,12 +17,6 @@ parser.add_argument("--debug",
|
||||||
default = False,
|
default = False,
|
||||||
help = "Turn on the debug mode by setting this option."
|
help = "Turn on the debug mode by setting this option."
|
||||||
)
|
)
|
||||||
parser.add_argument("--trigger-early-crash",
|
|
||||||
dest = "trigger_early_crash",
|
|
||||||
action = "store_true",
|
|
||||||
default = False,
|
|
||||||
help = "FOR TESTING ONLY. Trigger an early crash to show the crash dialog."
|
|
||||||
)
|
|
||||||
known_args = vars(parser.parse_known_args()[0])
|
known_args = vars(parser.parse_known_args()[0])
|
||||||
|
|
||||||
if not known_args["debug"]:
|
if not known_args["debug"]:
|
||||||
|
|
|
@ -794,7 +794,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
|
||||||
# Clear all existing containers
|
# Clear all existing containers
|
||||||
quality_changes_info.global_info.container.clear()
|
quality_changes_info.global_info.container.clear()
|
||||||
for container_info in quality_changes_info.extruder_info_dict.values():
|
for container_info in quality_changes_info.extruder_info_dict.values():
|
||||||
container_info.container.clear()
|
if container_info.container:
|
||||||
|
container_info.container.clear()
|
||||||
|
|
||||||
# Loop over everything and override the existing containers
|
# Loop over everything and override the existing containers
|
||||||
global_info = quality_changes_info.global_info
|
global_info = quality_changes_info.global_info
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "3MF Reader",
|
"name": "3MF Reader",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for reading 3MF files.",
|
"description": "Provides support for reading 3MF files.",
|
||||||
"api": 5,
|
"api": "6.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "3MF Writer",
|
"name": "3MF Writer",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for writing 3MF files.",
|
"description": "Provides support for writing 3MF files.",
|
||||||
"api": 5,
|
"api": "6.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Copyright (c) 2015 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from UM.i18n import i18nCatalog
|
from UM.i18n import i18nCatalog
|
||||||
|
@ -29,6 +29,7 @@ class ChangeLog(Extension, QObject,):
|
||||||
self._change_logs = None
|
self._change_logs = None
|
||||||
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
Application.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
|
||||||
Application.getInstance().getPreferences().addPreference("general/latest_version_changelog_shown", "2.0.0") #First version of CURA with uranium
|
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)
|
self.addMenuItem(catalog.i18nc("@item:inmenu", "Show Changelog"), self.showChangelog)
|
||||||
|
|
||||||
def getChangeLogs(self):
|
def getChangeLogs(self):
|
||||||
|
|
|
@ -943,7 +943,7 @@ This release adds support for printers with elliptic buildplates. This feature h
|
||||||
*AppImage for Linux
|
*AppImage for Linux
|
||||||
The Linux distribution is now in AppImage format, which makes Cura easier to install.
|
The Linux distribution is now in AppImage format, which makes Cura easier to install.
|
||||||
|
|
||||||
*bugfixes
|
*Bugfixes
|
||||||
The user is now notified when a new version of Cura is available.
|
The user is now notified when a new version of Cura is available.
|
||||||
When searching in the setting visibility preferences, the category for each setting is always displayed.
|
When searching in the setting visibility preferences, the category for each setting is always displayed.
|
||||||
3MF files are now saved and loaded correctly.
|
3MF files are now saved and loaded correctly.
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "Changelog",
|
"name": "Changelog",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Shows changes since latest checked version.",
|
"description": "Shows changes since latest checked version.",
|
||||||
"api": 5,
|
"api": "6.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
12
plugins/CuraDrive/__init__.py
Normal file
12
plugins/CuraDrive/__init__.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from .src.DrivePluginExtension import DrivePluginExtension
|
||||||
|
|
||||||
|
|
||||||
|
def getMetaData():
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def register(app):
|
||||||
|
return {"extension": DrivePluginExtension()}
|
8
plugins/CuraDrive/plugin.json
Normal file
8
plugins/CuraDrive/plugin.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"name": "Cura Backups",
|
||||||
|
"author": "Ultimaker B.V.",
|
||||||
|
"description": "Backup and restore your configuration.",
|
||||||
|
"version": "1.2.0",
|
||||||
|
"api": 6,
|
||||||
|
"i18n-catalog": "cura"
|
||||||
|
}
|
168
plugins/CuraDrive/src/DriveApiService.py
Normal file
168
plugins/CuraDrive/src/DriveApiService.py
Normal file
|
@ -0,0 +1,168 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
from datetime import datetime
|
||||||
|
from tempfile import NamedTemporaryFile
|
||||||
|
from typing import Any, Optional, List, Dict
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
|
from UM.Signal import Signal, signalemitter
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
from .UploadBackupJob import UploadBackupJob
|
||||||
|
from .Settings import Settings
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
## The DriveApiService is responsible for interacting with the CuraDrive API and Cura's backup handling.
|
||||||
|
@signalemitter
|
||||||
|
class DriveApiService:
|
||||||
|
BACKUP_URL = "{}/backups".format(Settings.DRIVE_API_URL)
|
||||||
|
|
||||||
|
# Emit signal when restoring backup started or finished.
|
||||||
|
restoringStateChanged = Signal()
|
||||||
|
|
||||||
|
# Emit signal when creating backup started or finished.
|
||||||
|
creatingStateChanged = Signal()
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._cura_api = CuraApplication.getInstance().getCuraAPI()
|
||||||
|
|
||||||
|
def getBackups(self) -> List[Dict[str, Any]]:
|
||||||
|
access_token = self._cura_api.account.accessToken
|
||||||
|
if not access_token:
|
||||||
|
Logger.log("w", "Could not get access token.")
|
||||||
|
return []
|
||||||
|
|
||||||
|
backup_list_request = requests.get(self.BACKUP_URL, headers = {
|
||||||
|
"Authorization": "Bearer {}".format(access_token)
|
||||||
|
})
|
||||||
|
|
||||||
|
# HTTP status 300s mean redirection. 400s and 500s are errors.
|
||||||
|
# Technically 300s are not errors, but the use case here relies on "requests" to handle redirects automatically.
|
||||||
|
if backup_list_request.status_code >= 300:
|
||||||
|
Logger.log("w", "Could not get backups list from remote: %s", backup_list_request.text)
|
||||||
|
Message(catalog.i18nc("@info:backup_status", "There was an error listing your backups."), title = catalog.i18nc("@info:title", "Backup")).show()
|
||||||
|
return []
|
||||||
|
return backup_list_request.json()["data"]
|
||||||
|
|
||||||
|
def createBackup(self) -> None:
|
||||||
|
self.creatingStateChanged.emit(is_creating = True)
|
||||||
|
|
||||||
|
# Create the backup.
|
||||||
|
backup_zip_file, backup_meta_data = self._cura_api.backups.createBackup()
|
||||||
|
if not backup_zip_file or not backup_meta_data:
|
||||||
|
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not create backup.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Create an upload entry for the backup.
|
||||||
|
timestamp = datetime.now().isoformat()
|
||||||
|
backup_meta_data["description"] = "{}.backup.{}.cura.zip".format(timestamp, backup_meta_data["cura_release"])
|
||||||
|
backup_upload_url = self._requestBackupUpload(backup_meta_data, len(backup_zip_file))
|
||||||
|
if not backup_upload_url:
|
||||||
|
self.creatingStateChanged.emit(is_creating = False, error_message ="Could not upload backup.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Upload the backup to storage.
|
||||||
|
upload_backup_job = UploadBackupJob(backup_upload_url, backup_zip_file)
|
||||||
|
upload_backup_job.finished.connect(self._onUploadFinished)
|
||||||
|
upload_backup_job.start()
|
||||||
|
|
||||||
|
def _onUploadFinished(self, job: "UploadBackupJob") -> None:
|
||||||
|
if job.backup_upload_error_message != "":
|
||||||
|
# If the job contains an error message we pass it along so the UI can display it.
|
||||||
|
self.creatingStateChanged.emit(is_creating = False, error_message = job.backup_upload_error_message)
|
||||||
|
else:
|
||||||
|
self.creatingStateChanged.emit(is_creating = False)
|
||||||
|
|
||||||
|
def restoreBackup(self, backup: Dict[str, Any]) -> None:
|
||||||
|
self.restoringStateChanged.emit(is_restoring = True)
|
||||||
|
download_url = backup.get("download_url")
|
||||||
|
if not download_url:
|
||||||
|
# If there is no download URL, we can't restore the backup.
|
||||||
|
return self._emitRestoreError()
|
||||||
|
|
||||||
|
download_package = requests.get(download_url, stream = True)
|
||||||
|
if download_package.status_code >= 300:
|
||||||
|
# Something went wrong when attempting to download the backup.
|
||||||
|
Logger.log("w", "Could not download backup from url %s: %s", download_url, download_package.text)
|
||||||
|
return self._emitRestoreError()
|
||||||
|
|
||||||
|
# We store the file in a temporary path fist to ensure integrity.
|
||||||
|
temporary_backup_file = NamedTemporaryFile(delete = False)
|
||||||
|
with open(temporary_backup_file.name, "wb") as write_backup:
|
||||||
|
for chunk in download_package:
|
||||||
|
write_backup.write(chunk)
|
||||||
|
|
||||||
|
if not self._verifyMd5Hash(temporary_backup_file.name, backup.get("md5_hash", "")):
|
||||||
|
# Don't restore the backup if the MD5 hashes do not match.
|
||||||
|
# This can happen if the download was interrupted.
|
||||||
|
Logger.log("w", "Remote and local MD5 hashes do not match, not restoring backup.")
|
||||||
|
return self._emitRestoreError()
|
||||||
|
|
||||||
|
# Tell Cura to place the backup back in the user data folder.
|
||||||
|
with open(temporary_backup_file.name, "rb") as read_backup:
|
||||||
|
self._cura_api.backups.restoreBackup(read_backup.read(), backup.get("metadata", {}))
|
||||||
|
self.restoringStateChanged.emit(is_restoring = False)
|
||||||
|
|
||||||
|
def _emitRestoreError(self) -> None:
|
||||||
|
self.restoringStateChanged.emit(is_restoring = False,
|
||||||
|
error_message = catalog.i18nc("@info:backup_status",
|
||||||
|
"There was an error trying to restore your backup."))
|
||||||
|
|
||||||
|
# Verify the MD5 hash of a file.
|
||||||
|
# \param file_path Full path to the file.
|
||||||
|
# \param known_hash The known MD5 hash of the file.
|
||||||
|
# \return: Success or not.
|
||||||
|
@staticmethod
|
||||||
|
def _verifyMd5Hash(file_path: str, known_hash: str) -> bool:
|
||||||
|
with open(file_path, "rb") as read_backup:
|
||||||
|
local_md5_hash = base64.b64encode(hashlib.md5(read_backup.read()).digest(), altchars = b"_-").decode("utf-8")
|
||||||
|
return known_hash == local_md5_hash
|
||||||
|
|
||||||
|
def deleteBackup(self, backup_id: str) -> bool:
|
||||||
|
access_token = self._cura_api.account.accessToken
|
||||||
|
if not access_token:
|
||||||
|
Logger.log("w", "Could not get access token.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
delete_backup = requests.delete("{}/{}".format(self.BACKUP_URL, backup_id), headers = {
|
||||||
|
"Authorization": "Bearer {}".format(access_token)
|
||||||
|
})
|
||||||
|
if delete_backup.status_code >= 300:
|
||||||
|
Logger.log("w", "Could not delete backup: %s", delete_backup.text)
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Request a backup upload slot from the API.
|
||||||
|
# \param backup_metadata: A dict containing some meta data about the backup.
|
||||||
|
# \param backup_size The size of the backup file in bytes.
|
||||||
|
# \return: The upload URL for the actual backup file if successful, otherwise None.
|
||||||
|
def _requestBackupUpload(self, backup_metadata: Dict[str, Any], backup_size: int) -> Optional[str]:
|
||||||
|
access_token = self._cura_api.account.accessToken
|
||||||
|
if not access_token:
|
||||||
|
Logger.log("w", "Could not get access token.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
backup_upload_request = requests.put(self.BACKUP_URL, json = {
|
||||||
|
"data": {
|
||||||
|
"backup_size": backup_size,
|
||||||
|
"metadata": backup_metadata
|
||||||
|
}
|
||||||
|
}, headers = {
|
||||||
|
"Authorization": "Bearer {}".format(access_token)
|
||||||
|
})
|
||||||
|
|
||||||
|
# Any status code of 300 or above indicates an error.
|
||||||
|
if backup_upload_request.status_code >= 300:
|
||||||
|
Logger.log("w", "Could not request backup upload: %s", backup_upload_request.text)
|
||||||
|
return None
|
||||||
|
|
||||||
|
return backup_upload_request.json()["data"]["upload_url"]
|
162
plugins/CuraDrive/src/DrivePluginExtension.py
Normal file
162
plugins/CuraDrive/src/DrivePluginExtension.py
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import os
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import Optional, List, Dict, Any
|
||||||
|
|
||||||
|
from PyQt5.QtCore import QObject, pyqtSlot, pyqtProperty, pyqtSignal
|
||||||
|
|
||||||
|
from UM.Extension import Extension
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
|
from cura.CuraApplication import CuraApplication
|
||||||
|
|
||||||
|
from .Settings import Settings
|
||||||
|
from .DriveApiService import DriveApiService
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
# The DivePluginExtension provides functionality to backup and restore your Cura configuration to Ultimaker's cloud.
|
||||||
|
class DrivePluginExtension(QObject, Extension):
|
||||||
|
|
||||||
|
# Signal emitted when the list of backups changed.
|
||||||
|
backupsChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# Signal emitted when restoring has started. Needed to prevent parallel restoring.
|
||||||
|
restoringStateChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# Signal emitted when creating has started. Needed to prevent parallel creation of backups.
|
||||||
|
creatingStateChanged = pyqtSignal()
|
||||||
|
|
||||||
|
# Signal emitted when preferences changed (like auto-backup).
|
||||||
|
preferencesChanged = pyqtSignal()
|
||||||
|
|
||||||
|
DATE_FORMAT = "%d/%m/%Y %H:%M:%S"
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
QObject.__init__(self, None)
|
||||||
|
Extension.__init__(self)
|
||||||
|
|
||||||
|
# Local data caching for the UI.
|
||||||
|
self._drive_window = None # type: Optional[QObject]
|
||||||
|
self._backups = [] # type: List[Dict[str, Any]]
|
||||||
|
self._is_restoring_backup = False
|
||||||
|
self._is_creating_backup = False
|
||||||
|
|
||||||
|
# Initialize services.
|
||||||
|
preferences = CuraApplication.getInstance().getPreferences()
|
||||||
|
self._drive_api_service = DriveApiService()
|
||||||
|
|
||||||
|
# Attach signals.
|
||||||
|
CuraApplication.getInstance().getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged)
|
||||||
|
self._drive_api_service.restoringStateChanged.connect(self._onRestoringStateChanged)
|
||||||
|
self._drive_api_service.creatingStateChanged.connect(self._onCreatingStateChanged)
|
||||||
|
|
||||||
|
# Register preferences.
|
||||||
|
preferences.addPreference(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, False)
|
||||||
|
preferences.addPreference(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY,
|
||||||
|
datetime.now().strftime(self.DATE_FORMAT))
|
||||||
|
|
||||||
|
# Register the menu item
|
||||||
|
self.addMenuItem(catalog.i18nc("@item:inmenu", "Manage backups"), self.showDriveWindow)
|
||||||
|
|
||||||
|
# Make auto-backup on boot if required.
|
||||||
|
CuraApplication.getInstance().engineCreatedSignal.connect(self._autoBackup)
|
||||||
|
|
||||||
|
def showDriveWindow(self) -> None:
|
||||||
|
if not self._drive_window:
|
||||||
|
plugin_dir_path = CuraApplication.getInstance().getPluginRegistry().getPluginPath("CuraDrive")
|
||||||
|
path = os.path.join(plugin_dir_path, "src", "qml", "main.qml")
|
||||||
|
self._drive_window = CuraApplication.getInstance().createQmlComponent(path, {"CuraDrive": self})
|
||||||
|
self.refreshBackups()
|
||||||
|
if self._drive_window:
|
||||||
|
self._drive_window.show()
|
||||||
|
|
||||||
|
def _autoBackup(self) -> None:
|
||||||
|
preferences = CuraApplication.getInstance().getPreferences()
|
||||||
|
if preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY) and self._isLastBackupTooLongAgo():
|
||||||
|
self.createBackup()
|
||||||
|
|
||||||
|
def _isLastBackupTooLongAgo(self) -> bool:
|
||||||
|
current_date = datetime.now()
|
||||||
|
last_backup_date = self._getLastBackupDate()
|
||||||
|
date_diff = current_date - last_backup_date
|
||||||
|
return date_diff.days > 1
|
||||||
|
|
||||||
|
def _getLastBackupDate(self) -> "datetime":
|
||||||
|
preferences = CuraApplication.getInstance().getPreferences()
|
||||||
|
last_backup_date = preferences.getValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY)
|
||||||
|
return datetime.strptime(last_backup_date, self.DATE_FORMAT)
|
||||||
|
|
||||||
|
def _storeBackupDate(self) -> None:
|
||||||
|
backup_date = datetime.now().strftime(self.DATE_FORMAT)
|
||||||
|
preferences = CuraApplication.getInstance().getPreferences()
|
||||||
|
preferences.setValue(Settings.AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY, backup_date)
|
||||||
|
|
||||||
|
def _onLoginStateChanged(self, logged_in: bool = False) -> None:
|
||||||
|
if logged_in:
|
||||||
|
self.refreshBackups()
|
||||||
|
|
||||||
|
def _onRestoringStateChanged(self, is_restoring: bool = False, error_message: str = None) -> None:
|
||||||
|
self._is_restoring_backup = is_restoring
|
||||||
|
self.restoringStateChanged.emit()
|
||||||
|
if error_message:
|
||||||
|
Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show()
|
||||||
|
|
||||||
|
def _onCreatingStateChanged(self, is_creating: bool = False, error_message: str = None) -> None:
|
||||||
|
self._is_creating_backup = is_creating
|
||||||
|
self.creatingStateChanged.emit()
|
||||||
|
if error_message:
|
||||||
|
Message(error_message, title = catalog.i18nc("@info:title", "Backup")).show()
|
||||||
|
else:
|
||||||
|
self._storeBackupDate()
|
||||||
|
if not is_creating and not error_message:
|
||||||
|
# We've finished creating a new backup, to the list has to be updated.
|
||||||
|
self.refreshBackups()
|
||||||
|
|
||||||
|
@pyqtSlot(bool, name = "toggleAutoBackup")
|
||||||
|
def toggleAutoBackup(self, enabled: bool) -> None:
|
||||||
|
preferences = CuraApplication.getInstance().getPreferences()
|
||||||
|
preferences.setValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY, enabled)
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = preferencesChanged)
|
||||||
|
def autoBackupEnabled(self) -> bool:
|
||||||
|
preferences = CuraApplication.getInstance().getPreferences()
|
||||||
|
return bool(preferences.getValue(Settings.AUTO_BACKUP_ENABLED_PREFERENCE_KEY))
|
||||||
|
|
||||||
|
@pyqtProperty("QVariantList", notify = backupsChanged)
|
||||||
|
def backups(self) -> List[Dict[str, Any]]:
|
||||||
|
return self._backups
|
||||||
|
|
||||||
|
@pyqtSlot(name = "refreshBackups")
|
||||||
|
def refreshBackups(self) -> None:
|
||||||
|
self._backups = self._drive_api_service.getBackups()
|
||||||
|
self.backupsChanged.emit()
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = restoringStateChanged)
|
||||||
|
def isRestoringBackup(self) -> bool:
|
||||||
|
return self._is_restoring_backup
|
||||||
|
|
||||||
|
@pyqtProperty(bool, notify = creatingStateChanged)
|
||||||
|
def isCreatingBackup(self) -> bool:
|
||||||
|
return self._is_creating_backup
|
||||||
|
|
||||||
|
@pyqtSlot(str, name = "restoreBackup")
|
||||||
|
def restoreBackup(self, backup_id: str) -> None:
|
||||||
|
for backup in self._backups:
|
||||||
|
if backup.get("backup_id") == backup_id:
|
||||||
|
self._drive_api_service.restoreBackup(backup)
|
||||||
|
return
|
||||||
|
Logger.log("w", "Unable to find backup with the ID %s", backup_id)
|
||||||
|
|
||||||
|
@pyqtSlot(name = "createBackup")
|
||||||
|
def createBackup(self) -> None:
|
||||||
|
self._drive_api_service.createBackup()
|
||||||
|
|
||||||
|
@pyqtSlot(str, name = "deleteBackup")
|
||||||
|
def deleteBackup(self, backup_id: str) -> None:
|
||||||
|
self._drive_api_service.deleteBackup(backup_id)
|
||||||
|
self.refreshBackups()
|
13
plugins/CuraDrive/src/Settings.py
Normal file
13
plugins/CuraDrive/src/Settings.py
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
from cura import UltimakerCloudAuthentication
|
||||||
|
|
||||||
|
|
||||||
|
class Settings:
|
||||||
|
# Keeps the plugin settings.
|
||||||
|
DRIVE_API_VERSION = 1
|
||||||
|
DRIVE_API_URL = "{}/cura-drive/v{}".format(UltimakerCloudAuthentication.CuraCloudAPIRoot, str(DRIVE_API_VERSION))
|
||||||
|
|
||||||
|
AUTO_BACKUP_ENABLED_PREFERENCE_KEY = "cura_drive/auto_backup_enabled"
|
||||||
|
AUTO_BACKUP_LAST_DATE_PREFERENCE_KEY = "cura_drive/auto_backup_date"
|
41
plugins/CuraDrive/src/UploadBackupJob.py
Normal file
41
plugins/CuraDrive/src/UploadBackupJob.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from UM.Job import Job
|
||||||
|
from UM.Logger import Logger
|
||||||
|
from UM.Message import Message
|
||||||
|
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
|
|
||||||
|
class UploadBackupJob(Job):
|
||||||
|
MESSAGE_TITLE = catalog.i18nc("@info:title", "Backups")
|
||||||
|
|
||||||
|
# This job is responsible for uploading the backup file to cloud storage.
|
||||||
|
# As it can take longer than some other tasks, we schedule this using a Cura Job.
|
||||||
|
def __init__(self, signed_upload_url: str, backup_zip: bytes) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._signed_upload_url = signed_upload_url
|
||||||
|
self._backup_zip = backup_zip
|
||||||
|
self._upload_success = False
|
||||||
|
self.backup_upload_error_message = ""
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
upload_message = Message(catalog.i18nc("@info:backup_status", "Uploading your backup..."), title = self.MESSAGE_TITLE, progress = -1)
|
||||||
|
upload_message.show()
|
||||||
|
|
||||||
|
backup_upload = requests.put(self._signed_upload_url, data = self._backup_zip)
|
||||||
|
upload_message.hide()
|
||||||
|
|
||||||
|
if backup_upload.status_code >= 300:
|
||||||
|
self.backup_upload_error_message = backup_upload.text
|
||||||
|
Logger.log("w", "Could not upload backup file: %s", backup_upload.text)
|
||||||
|
Message(catalog.i18nc("@info:backup_status", "There was an error while uploading your backup."), title = self.MESSAGE_TITLE).show()
|
||||||
|
else:
|
||||||
|
self._upload_success = True
|
||||||
|
Message(catalog.i18nc("@info:backup_status", "Your backup has finished uploading."), title = self.MESSAGE_TITLE).show()
|
||||||
|
|
||||||
|
self.finished.emit(self)
|
0
plugins/CuraDrive/src/__init__.py
Normal file
0
plugins/CuraDrive/src/__init__.py
Normal file
39
plugins/CuraDrive/src/qml/components/BackupList.qml
Normal file
39
plugins/CuraDrive/src/qml/components/BackupList.qml
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.2
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
|
import UM 1.1 as UM
|
||||||
|
|
||||||
|
ScrollView
|
||||||
|
{
|
||||||
|
property alias model: backupList.model
|
||||||
|
width: parent.width
|
||||||
|
clip: true
|
||||||
|
ListView
|
||||||
|
{
|
||||||
|
id: backupList
|
||||||
|
width: parent.width
|
||||||
|
delegate: Item
|
||||||
|
{
|
||||||
|
// Add a margin, otherwise the scrollbar is on top of the right most component
|
||||||
|
width: parent.width - UM.Theme.getSize("default_margin").width
|
||||||
|
height: childrenRect.height
|
||||||
|
|
||||||
|
BackupListItem
|
||||||
|
{
|
||||||
|
id: backupListItem
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle
|
||||||
|
{
|
||||||
|
id: divider
|
||||||
|
color: UM.Theme.getColor("lining")
|
||||||
|
height: UM.Theme.getSize("default_lining").height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
46
plugins/CuraDrive/src/qml/components/BackupListFooter.qml
Normal file
46
plugins/CuraDrive/src/qml/components/BackupListFooter.qml
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
|
import UM 1.3 as UM
|
||||||
|
import Cura 1.0 as Cura
|
||||||
|
|
||||||
|
import "../components"
|
||||||
|
|
||||||
|
RowLayout
|
||||||
|
{
|
||||||
|
id: backupListFooter
|
||||||
|
width: parent.width
|
||||||
|
property bool showInfoButton: false
|
||||||
|
|
||||||
|
Cura.PrimaryButton
|
||||||
|
{
|
||||||
|
id: infoButton
|
||||||
|
text: catalog.i18nc("@button", "Want more?")
|
||||||
|
iconSource: UM.Theme.getIcon("info")
|
||||||
|
onClicked: Qt.openUrlExternally("https://goo.gl/forms/QACEP8pP3RV60QYG2")
|
||||||
|
visible: backupListFooter.showInfoButton
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.PrimaryButton
|
||||||
|
{
|
||||||
|
id: createBackupButton
|
||||||
|
text: catalog.i18nc("@button", "Backup Now")
|
||||||
|
iconSource: UM.Theme.getIcon("plus")
|
||||||
|
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
|
||||||
|
onClicked: CuraDrive.createBackup()
|
||||||
|
busy: CuraDrive.isCreatingBackup
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.CheckBoxWithTooltip
|
||||||
|
{
|
||||||
|
id: autoBackupEnabled
|
||||||
|
checked: CuraDrive.autoBackupEnabled
|
||||||
|
onClicked: CuraDrive.toggleAutoBackup(autoBackupEnabled.checked)
|
||||||
|
text: catalog.i18nc("@checkbox:description", "Auto Backup")
|
||||||
|
tooltip: catalog.i18nc("@checkbox:description", "Automatically create a backup each day that Cura is started.")
|
||||||
|
}
|
||||||
|
}
|
113
plugins/CuraDrive/src/qml/components/BackupListItem.qml
Normal file
113
plugins/CuraDrive/src/qml/components/BackupListItem.qml
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
import QtQuick.Dialogs 1.1
|
||||||
|
|
||||||
|
import UM 1.1 as UM
|
||||||
|
import Cura 1.0 as Cura
|
||||||
|
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
id: backupListItem
|
||||||
|
width: parent.width
|
||||||
|
height: showDetails ? dataRow.height + backupDetails.height : dataRow.height
|
||||||
|
property bool showDetails: false
|
||||||
|
|
||||||
|
// Backup details toggle animation.
|
||||||
|
Behavior on height
|
||||||
|
{
|
||||||
|
PropertyAnimation
|
||||||
|
{
|
||||||
|
duration: 70
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
RowLayout
|
||||||
|
{
|
||||||
|
id: dataRow
|
||||||
|
spacing: UM.Theme.getSize("wide_margin").width
|
||||||
|
width: parent.width
|
||||||
|
height: 50 * screenScaleFactor
|
||||||
|
|
||||||
|
UM.SimpleButton
|
||||||
|
{
|
||||||
|
width: UM.Theme.getSize("section_icon").width
|
||||||
|
height: UM.Theme.getSize("section_icon").height
|
||||||
|
color: UM.Theme.getColor("small_button_text")
|
||||||
|
hoverColor: UM.Theme.getColor("small_button_text_hover")
|
||||||
|
iconSource: UM.Theme.getIcon("info")
|
||||||
|
onClicked: backupListItem.showDetails = !backupListItem.showDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: new Date(modelData.generated_time).toLocaleString(UM.Preferences.getValue("general/language"))
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.minimumWidth: 100 * screenScaleFactor
|
||||||
|
Layout.maximumWidth: 500 * screenScaleFactor
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: modelData.metadata.description
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.minimumWidth: 100 * screenScaleFactor
|
||||||
|
Layout.maximumWidth: 500 * screenScaleFactor
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.SecondaryButton
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@button", "Restore")
|
||||||
|
enabled: !CuraDrive.isCreatingBackup && !CuraDrive.isRestoringBackup
|
||||||
|
onClicked: confirmRestoreDialog.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
UM.SimpleButton
|
||||||
|
{
|
||||||
|
width: UM.Theme.getSize("message_close").width
|
||||||
|
height: UM.Theme.getSize("message_close").height
|
||||||
|
color: UM.Theme.getColor("small_button_text")
|
||||||
|
hoverColor: UM.Theme.getColor("small_button_text_hover")
|
||||||
|
iconSource: UM.Theme.getIcon("cross1")
|
||||||
|
onClicked: confirmDeleteDialog.visible = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BackupListItemDetails
|
||||||
|
{
|
||||||
|
id: backupDetails
|
||||||
|
backupDetailsData: modelData
|
||||||
|
width: parent.width
|
||||||
|
visible: parent.showDetails
|
||||||
|
anchors.top: dataRow.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog
|
||||||
|
{
|
||||||
|
id: confirmDeleteDialog
|
||||||
|
title: catalog.i18nc("@dialog:title", "Delete Backup")
|
||||||
|
text: catalog.i18nc("@dialog:info", "Are you sure you want to delete this backup? This cannot be undone.")
|
||||||
|
standardButtons: StandardButton.Yes | StandardButton.No
|
||||||
|
onYes: CuraDrive.deleteBackup(modelData.backup_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
MessageDialog
|
||||||
|
{
|
||||||
|
id: confirmRestoreDialog
|
||||||
|
title: catalog.i18nc("@dialog:title", "Restore Backup")
|
||||||
|
text: catalog.i18nc("@dialog:info", "You will need to restart Cura before your backup is restored. Do you want to close Cura now?")
|
||||||
|
standardButtons: StandardButton.Yes | StandardButton.No
|
||||||
|
onYes: CuraDrive.restoreBackup(modelData.backup_id)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
|
import UM 1.1 as UM
|
||||||
|
|
||||||
|
ColumnLayout
|
||||||
|
{
|
||||||
|
id: backupDetails
|
||||||
|
width: parent.width
|
||||||
|
spacing: UM.Theme.getSize("default_margin").width
|
||||||
|
property var backupDetailsData
|
||||||
|
|
||||||
|
// Cura version
|
||||||
|
BackupListItemDetailsRow
|
||||||
|
{
|
||||||
|
iconSource: UM.Theme.getIcon("application")
|
||||||
|
label: catalog.i18nc("@backuplist:label", "Cura Version")
|
||||||
|
value: backupDetailsData.metadata.cura_release
|
||||||
|
}
|
||||||
|
|
||||||
|
// Machine count.
|
||||||
|
BackupListItemDetailsRow
|
||||||
|
{
|
||||||
|
iconSource: UM.Theme.getIcon("printer_single")
|
||||||
|
label: catalog.i18nc("@backuplist:label", "Machines")
|
||||||
|
value: backupDetailsData.metadata.machine_count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Material count
|
||||||
|
BackupListItemDetailsRow
|
||||||
|
{
|
||||||
|
iconSource: UM.Theme.getIcon("category_material")
|
||||||
|
label: catalog.i18nc("@backuplist:label", "Materials")
|
||||||
|
value: backupDetailsData.metadata.material_count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Profile count.
|
||||||
|
BackupListItemDetailsRow
|
||||||
|
{
|
||||||
|
iconSource: UM.Theme.getIcon("settings")
|
||||||
|
label: catalog.i18nc("@backuplist:label", "Profiles")
|
||||||
|
value: backupDetailsData.metadata.profile_count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plugin count.
|
||||||
|
BackupListItemDetailsRow
|
||||||
|
{
|
||||||
|
iconSource: UM.Theme.getIcon("plugin")
|
||||||
|
label: catalog.i18nc("@backuplist:label", "Plugins")
|
||||||
|
value: backupDetailsData.metadata.plugin_count
|
||||||
|
}
|
||||||
|
|
||||||
|
// Spacer.
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
width: parent.width
|
||||||
|
height: UM.Theme.getSize("default_margin").height
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,52 @@
|
||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
|
import UM 1.3 as UM
|
||||||
|
|
||||||
|
RowLayout
|
||||||
|
{
|
||||||
|
id: detailsRow
|
||||||
|
width: parent.width
|
||||||
|
height: 40 * screenScaleFactor
|
||||||
|
|
||||||
|
property alias iconSource: icon.source
|
||||||
|
property alias label: detailName.text
|
||||||
|
property alias value: detailValue.text
|
||||||
|
|
||||||
|
UM.RecolorImage
|
||||||
|
{
|
||||||
|
id: icon
|
||||||
|
width: 18 * screenScaleFactor
|
||||||
|
height: width
|
||||||
|
source: ""
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: detailName
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.minimumWidth: 50 * screenScaleFactor
|
||||||
|
Layout.maximumWidth: 100 * screenScaleFactor
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: detailValue
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
elide: Text.ElideRight
|
||||||
|
Layout.minimumWidth: 50 * screenScaleFactor
|
||||||
|
Layout.maximumWidth: 100 * screenScaleFactor
|
||||||
|
Layout.fillWidth: true
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
}
|
BIN
plugins/CuraDrive/src/qml/images/icon.png
Normal file
BIN
plugins/CuraDrive/src/qml/images/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
44
plugins/CuraDrive/src/qml/main.qml
Normal file
44
plugins/CuraDrive/src/qml/main.qml
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
|
import UM 1.3 as UM
|
||||||
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
|
import "components"
|
||||||
|
import "pages"
|
||||||
|
|
||||||
|
Window
|
||||||
|
{
|
||||||
|
id: curaDriveDialog
|
||||||
|
minimumWidth: Math.round(UM.Theme.getSize("modal_window_minimum").width)
|
||||||
|
minimumHeight: Math.round(UM.Theme.getSize("modal_window_minimum").height)
|
||||||
|
maximumWidth: Math.round(minimumWidth * 1.2)
|
||||||
|
maximumHeight: Math.round(minimumHeight * 1.2)
|
||||||
|
width: minimumWidth
|
||||||
|
height: minimumHeight
|
||||||
|
color: UM.Theme.getColor("main_background")
|
||||||
|
title: catalog.i18nc("@title:window", "Cura Backups")
|
||||||
|
|
||||||
|
// Globally available.
|
||||||
|
UM.I18nCatalog
|
||||||
|
{
|
||||||
|
id: catalog
|
||||||
|
name: "cura"
|
||||||
|
}
|
||||||
|
|
||||||
|
WelcomePage
|
||||||
|
{
|
||||||
|
id: welcomePage
|
||||||
|
visible: !Cura.API.account.isLoggedIn
|
||||||
|
}
|
||||||
|
|
||||||
|
BackupsPage
|
||||||
|
{
|
||||||
|
id: backupsPage
|
||||||
|
visible: Cura.API.account.isLoggedIn
|
||||||
|
}
|
||||||
|
}
|
75
plugins/CuraDrive/src/qml/pages/BackupsPage.qml
Normal file
75
plugins/CuraDrive/src/qml/pages/BackupsPage.qml
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
|
import UM 1.3 as UM
|
||||||
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
|
import "../components"
|
||||||
|
|
||||||
|
Item
|
||||||
|
{
|
||||||
|
id: backupsPage
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: UM.Theme.getSize("wide_margin").width
|
||||||
|
|
||||||
|
ColumnLayout
|
||||||
|
{
|
||||||
|
spacing: UM.Theme.getSize("wide_margin").height
|
||||||
|
width: parent.width
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: backupTitle
|
||||||
|
text: catalog.i18nc("@title", "My Backups")
|
||||||
|
font: UM.Theme.getFont("large")
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
Layout.fillWidth: true
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@empty_state",
|
||||||
|
"You don't have any backups currently. Use the 'Backup Now' button to create one.")
|
||||||
|
width: parent.width
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
wrapMode: Label.WordWrap
|
||||||
|
visible: backupList.model.length == 0
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
BackupList
|
||||||
|
{
|
||||||
|
id: backupList
|
||||||
|
model: CuraDrive.backups
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
text: catalog.i18nc("@backup_limit_info",
|
||||||
|
"During the preview phase, you'll be limited to 5 visible backups. Remove a backup to see older ones.")
|
||||||
|
width: parent.width
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
wrapMode: Label.WordWrap
|
||||||
|
visible: backupList.model.length > 4
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
BackupListFooter
|
||||||
|
{
|
||||||
|
id: backupListFooter
|
||||||
|
showInfoButton: backupList.model.length > 4
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
plugins/CuraDrive/src/qml/pages/WelcomePage.qml
Normal file
56
plugins/CuraDrive/src/qml/pages/WelcomePage.qml
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
// Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
// Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import QtQuick 2.7
|
||||||
|
import QtQuick.Controls 2.1
|
||||||
|
import QtQuick.Window 2.2
|
||||||
|
|
||||||
|
import UM 1.3 as UM
|
||||||
|
import Cura 1.1 as Cura
|
||||||
|
|
||||||
|
import "../components"
|
||||||
|
|
||||||
|
|
||||||
|
Column
|
||||||
|
{
|
||||||
|
id: welcomePage
|
||||||
|
spacing: UM.Theme.getSize("wide_margin").height
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
Image
|
||||||
|
{
|
||||||
|
id: profileImage
|
||||||
|
fillMode: Image.PreserveAspectFit
|
||||||
|
source: "../images/icon.png"
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
width: Math.round(parent.width / 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
Label
|
||||||
|
{
|
||||||
|
id: welcomeTextLabel
|
||||||
|
text: catalog.i18nc("@description", "Backup and synchronize your Cura settings.")
|
||||||
|
width: Math.round(parent.width / 2)
|
||||||
|
font: UM.Theme.getFont("default")
|
||||||
|
color: UM.Theme.getColor("text")
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
wrapMode: Label.WordWrap
|
||||||
|
renderType: Text.NativeRendering
|
||||||
|
}
|
||||||
|
|
||||||
|
Cura.PrimaryButton
|
||||||
|
{
|
||||||
|
id: loginButton
|
||||||
|
width: UM.Theme.getSize("account_button").width
|
||||||
|
height: UM.Theme.getSize("account_button").height
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: catalog.i18nc("@button", "Sign in")
|
||||||
|
onClicked: Cura.API.account.login()
|
||||||
|
fixedWidthMode: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@ message Object
|
||||||
bytes normals = 3; //An array of 3 floats.
|
bytes normals = 3; //An array of 3 floats.
|
||||||
bytes indices = 4; //An array of ints.
|
bytes indices = 4; //An array of ints.
|
||||||
repeated Setting settings = 5; // Setting override per object, overruling the global settings.
|
repeated Setting settings = 5; // Setting override per object, overruling the global settings.
|
||||||
string name = 6;
|
string name = 6; //Mesh name
|
||||||
}
|
}
|
||||||
|
|
||||||
message Progress
|
message Progress
|
||||||
|
@ -58,6 +58,7 @@ message Polygon {
|
||||||
MoveCombingType = 8;
|
MoveCombingType = 8;
|
||||||
MoveRetractionType = 9;
|
MoveRetractionType = 9;
|
||||||
SupportInterfaceType = 10;
|
SupportInterfaceType = 10;
|
||||||
|
PrimeTowerType = 11;
|
||||||
}
|
}
|
||||||
Type type = 1; // Type of move
|
Type type = 1; // Type of move
|
||||||
bytes points = 2; // The points of the polygon, or two points if only a line segment (Currently only line segments are used)
|
bytes points = 2; // The points of the polygon, or two points if only a line segment (Currently only line segments are used)
|
||||||
|
@ -108,8 +109,9 @@ message PrintTimeMaterialEstimates { // The print time for each feature and mate
|
||||||
float time_travel = 9;
|
float time_travel = 9;
|
||||||
float time_retract = 10;
|
float time_retract = 10;
|
||||||
float time_support_interface = 11;
|
float time_support_interface = 11;
|
||||||
|
float time_prime_tower = 12;
|
||||||
|
|
||||||
repeated MaterialEstimates materialEstimates = 12; // materialEstimates data
|
repeated MaterialEstimates materialEstimates = 13; // materialEstimates data
|
||||||
}
|
}
|
||||||
|
|
||||||
message MaterialEstimates {
|
message MaterialEstimates {
|
||||||
|
|
|
@ -86,8 +86,8 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self._layer_view_active = False #type: bool
|
self._layer_view_active = False #type: bool
|
||||||
self._onActiveViewChanged()
|
self._onActiveViewChanged()
|
||||||
|
|
||||||
self._stored_layer_data = [] #type: List[Arcus.PythonMessage]
|
self._stored_layer_data = [] # type: List[Arcus.PythonMessage]
|
||||||
self._stored_optimized_layer_data = {} #type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
|
self._stored_optimized_layer_data = {} # type: Dict[int, List[Arcus.PythonMessage]] # key is build plate number, then arrays are stored until they go to the ProcessSlicesLayersJob
|
||||||
|
|
||||||
self._scene = self._application.getController().getScene() #type: Scene
|
self._scene = self._application.getController().getScene() #type: Scene
|
||||||
self._scene.sceneChanged.connect(self._onSceneChanged)
|
self._scene.sceneChanged.connect(self._onSceneChanged)
|
||||||
|
@ -203,7 +203,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
|
|
||||||
@pyqtSlot()
|
@pyqtSlot()
|
||||||
def stopSlicing(self) -> None:
|
def stopSlicing(self) -> None:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.setState(BackendState.NotStarted)
|
||||||
if self._slicing: # We were already slicing. Stop the old job.
|
if self._slicing: # We were already slicing. Stop the old job.
|
||||||
self._terminate()
|
self._terminate()
|
||||||
self._createSocket()
|
self._createSocket()
|
||||||
|
@ -229,6 +229,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
if not self._build_plates_to_be_sliced:
|
if not self._build_plates_to_be_sliced:
|
||||||
self.processingProgress.emit(1.0)
|
self.processingProgress.emit(1.0)
|
||||||
Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
|
Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
|
||||||
|
self.setState(BackendState.Done)
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._process_layers_job:
|
if self._process_layers_job:
|
||||||
|
@ -245,7 +246,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
num_objects = self._numObjectsPerBuildPlate()
|
num_objects = self._numObjectsPerBuildPlate()
|
||||||
|
|
||||||
self._stored_layer_data = []
|
self._stored_layer_data = []
|
||||||
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
|
|
||||||
|
|
||||||
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
|
if build_plate_to_be_sliced not in num_objects or num_objects[build_plate_to_be_sliced] == 0:
|
||||||
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #Because we created this attribute above.
|
self._scene.gcode_dict[build_plate_to_be_sliced] = [] #type: ignore #Because we created this attribute above.
|
||||||
|
@ -253,7 +254,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
if self._build_plates_to_be_sliced:
|
if self._build_plates_to_be_sliced:
|
||||||
self.slice()
|
self.slice()
|
||||||
return
|
return
|
||||||
|
self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
|
||||||
if self._application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
|
if self._application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
|
||||||
self._application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
|
self._application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
|
||||||
|
|
||||||
|
@ -322,7 +323,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self._start_slice_job = None
|
self._start_slice_job = None
|
||||||
|
|
||||||
if job.isCancelled() or job.getError() or job.getResult() == StartJobResult.Error:
|
if job.isCancelled() or job.getError() or job.getResult() == StartJobResult.Error:
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.setState(BackendState.Error)
|
||||||
self.backendError.emit(job)
|
self.backendError.emit(job)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -331,10 +332,10 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self._error_message = Message(catalog.i18nc("@info:status",
|
self._error_message = Message(catalog.i18nc("@info:status",
|
||||||
"Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
|
"Unable to slice with the current material as it is incompatible with the selected machine or configuration."), title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.setState(BackendState.Error)
|
||||||
self.backendError.emit(job)
|
self.backendError.emit(job)
|
||||||
else:
|
else:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.setState(BackendState.NotStarted)
|
||||||
return
|
return
|
||||||
|
|
||||||
if job.getResult() == StartJobResult.SettingError:
|
if job.getResult() == StartJobResult.SettingError:
|
||||||
|
@ -362,10 +363,10 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}").format(", ".join(error_labels)),
|
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice with the current settings. The following settings have errors: {0}").format(", ".join(error_labels)),
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.setState(BackendState.Error)
|
||||||
self.backendError.emit(job)
|
self.backendError.emit(job)
|
||||||
else:
|
else:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.setState(BackendState.NotStarted)
|
||||||
return
|
return
|
||||||
|
|
||||||
elif job.getResult() == StartJobResult.ObjectSettingError:
|
elif job.getResult() == StartJobResult.ObjectSettingError:
|
||||||
|
@ -386,7 +387,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = ", ".join(errors.values())),
|
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice due to some per-model settings. The following settings have errors on one or more models: {error_labels}").format(error_labels = ", ".join(errors.values())),
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.setState(BackendState.Error)
|
||||||
self.backendError.emit(job)
|
self.backendError.emit(job)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -395,28 +396,28 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."),
|
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because the prime tower or prime position(s) are invalid."),
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.setState(BackendState.Error)
|
||||||
self.backendError.emit(job)
|
self.backendError.emit(job)
|
||||||
else:
|
else:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.setState(BackendState.NotStarted)
|
||||||
|
|
||||||
if job.getResult() == StartJobResult.ObjectsWithDisabledExtruder:
|
if job.getResult() == StartJobResult.ObjectsWithDisabledExtruder:
|
||||||
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because there are objects associated with disabled Extruder %s." % job.getMessage()),
|
self._error_message = Message(catalog.i18nc("@info:status", "Unable to slice because there are objects associated with disabled Extruder %s." % job.getMessage()),
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.setState(BackendState.Error)
|
||||||
self.backendError.emit(job)
|
self.backendError.emit(job)
|
||||||
return
|
return
|
||||||
|
|
||||||
if job.getResult() == StartJobResult.NothingToSlice:
|
if job.getResult() == StartJobResult.NothingToSlice:
|
||||||
if self._application.platformActivity:
|
if self._application.platformActivity:
|
||||||
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume. Please scale or rotate models to fit."),
|
self._error_message = Message(catalog.i18nc("@info:status", "Nothing to slice because none of the models fit the build volume or are assigned to a disabled extruder. Please scale or rotate models to fit, or enable an extruder."),
|
||||||
title = catalog.i18nc("@info:title", "Unable to slice"))
|
title = catalog.i18nc("@info:title", "Unable to slice"))
|
||||||
self._error_message.show()
|
self._error_message.show()
|
||||||
self.backendStateChange.emit(BackendState.Error)
|
self.setState(BackendState.Error)
|
||||||
self.backendError.emit(job)
|
self.backendError.emit(job)
|
||||||
else:
|
else:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.setState(BackendState.NotStarted)
|
||||||
self._invokeSlice()
|
self._invokeSlice()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
@ -424,7 +425,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self._socket.sendMessage(job.getSliceMessage())
|
self._socket.sendMessage(job.getSliceMessage())
|
||||||
|
|
||||||
# Notify the user that it's now up to the backend to do it's job
|
# Notify the user that it's now up to the backend to do it's job
|
||||||
self.backendStateChange.emit(BackendState.Processing)
|
self.setState(BackendState.Processing)
|
||||||
|
|
||||||
if self._slice_start_time:
|
if self._slice_start_time:
|
||||||
Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
|
Logger.log("d", "Sending slice message took %s seconds", time() - self._slice_start_time )
|
||||||
|
@ -442,7 +443,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
for node in DepthFirstIterator(self._scene.getRoot()): #type: ignore #Ignore type error because iter() should get called automatically by Python syntax.
|
||||||
if node.callDecoration("isBlockSlicing"):
|
if node.callDecoration("isBlockSlicing"):
|
||||||
enable_timer = False
|
enable_timer = False
|
||||||
self.backendStateChange.emit(BackendState.Disabled)
|
self.setState(BackendState.Disabled)
|
||||||
self._is_disabled = True
|
self._is_disabled = True
|
||||||
gcode_list = node.callDecoration("getGCodeList")
|
gcode_list = node.callDecoration("getGCodeList")
|
||||||
if gcode_list is not None:
|
if gcode_list is not None:
|
||||||
|
@ -451,7 +452,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
if self._use_timer == enable_timer:
|
if self._use_timer == enable_timer:
|
||||||
return self._use_timer
|
return self._use_timer
|
||||||
if enable_timer:
|
if enable_timer:
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.setState(BackendState.NotStarted)
|
||||||
self.enableTimer()
|
self.enableTimer()
|
||||||
return True
|
return True
|
||||||
else:
|
else:
|
||||||
|
@ -518,7 +519,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self._build_plates_to_be_sliced.append(build_plate_number)
|
self._build_plates_to_be_sliced.append(build_plate_number)
|
||||||
self.printDurationMessage.emit(source_build_plate_number, {}, [])
|
self.printDurationMessage.emit(source_build_plate_number, {}, [])
|
||||||
self.processingProgress.emit(0.0)
|
self.processingProgress.emit(0.0)
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.setState(BackendState.NotStarted)
|
||||||
# if not self._use_timer:
|
# if not self._use_timer:
|
||||||
# With manually having to slice, we want to clear the old invalid layer data.
|
# With manually having to slice, we want to clear the old invalid layer data.
|
||||||
self._clearLayerData(build_plate_changed)
|
self._clearLayerData(build_plate_changed)
|
||||||
|
@ -567,7 +568,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self.stopSlicing()
|
self.stopSlicing()
|
||||||
self.markSliceAll()
|
self.markSliceAll()
|
||||||
self.processingProgress.emit(0.0)
|
self.processingProgress.emit(0.0)
|
||||||
self.backendStateChange.emit(BackendState.NotStarted)
|
self.setState(BackendState.NotStarted)
|
||||||
if not self._use_timer:
|
if not self._use_timer:
|
||||||
# With manually having to slice, we want to clear the old invalid layer data.
|
# With manually having to slice, we want to clear the old invalid layer data.
|
||||||
self._clearLayerData()
|
self._clearLayerData()
|
||||||
|
@ -613,7 +614,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
# \param message The protobuf message containing the slicing progress.
|
# \param message The protobuf message containing the slicing progress.
|
||||||
def _onProgressMessage(self, message: Arcus.PythonMessage) -> None:
|
def _onProgressMessage(self, message: Arcus.PythonMessage) -> None:
|
||||||
self.processingProgress.emit(message.amount)
|
self.processingProgress.emit(message.amount)
|
||||||
self.backendStateChange.emit(BackendState.Processing)
|
self.setState(BackendState.Processing)
|
||||||
|
|
||||||
def _invokeSlice(self) -> None:
|
def _invokeSlice(self) -> None:
|
||||||
if self._use_timer:
|
if self._use_timer:
|
||||||
|
@ -632,7 +633,7 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
#
|
#
|
||||||
# \param message The protobuf message signalling that slicing is finished.
|
# \param message The protobuf message signalling that slicing is finished.
|
||||||
def _onSlicingFinishedMessage(self, message: Arcus.PythonMessage) -> None:
|
def _onSlicingFinishedMessage(self, message: Arcus.PythonMessage) -> None:
|
||||||
self.backendStateChange.emit(BackendState.Done)
|
self.setState(BackendState.Done)
|
||||||
self.processingProgress.emit(1.0)
|
self.processingProgress.emit(1.0)
|
||||||
|
|
||||||
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically.
|
gcode_list = self._scene.gcode_dict[self._start_slice_job_build_plate] #type: ignore #Because we generate this attribute dynamically.
|
||||||
|
@ -832,7 +833,10 @@ class CuraEngineBackend(QObject, Backend):
|
||||||
self._onChanged()
|
self._onChanged()
|
||||||
|
|
||||||
def _onProcessLayersFinished(self, job: ProcessSlicedLayersJob) -> None:
|
def _onProcessLayersFinished(self, job: ProcessSlicedLayersJob) -> None:
|
||||||
del self._stored_optimized_layer_data[job.getBuildPlate()]
|
if job.getBuildPlate() in self._stored_optimized_layer_data:
|
||||||
|
del self._stored_optimized_layer_data[job.getBuildPlate()]
|
||||||
|
else:
|
||||||
|
Logger.log("w", "The optimized layer data was already deleted for buildplate %s", job.getBuildPlate())
|
||||||
self._process_layers_job = None
|
self._process_layers_job = None
|
||||||
Logger.log("d", "See if there is more to slice(2)...")
|
Logger.log("d", "See if there is more to slice(2)...")
|
||||||
self._invokeSlice()
|
self._invokeSlice()
|
||||||
|
|
|
@ -137,6 +137,7 @@ class ProcessSlicedLayersJob(Job):
|
||||||
extruder = polygon.extruder
|
extruder = polygon.extruder
|
||||||
|
|
||||||
line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array
|
line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array
|
||||||
|
|
||||||
line_types = line_types.reshape((-1,1))
|
line_types = line_types.reshape((-1,1))
|
||||||
|
|
||||||
points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array
|
points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array
|
||||||
|
@ -195,7 +196,7 @@ class ProcessSlicedLayersJob(Job):
|
||||||
if extruders:
|
if extruders:
|
||||||
material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32)
|
material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32)
|
||||||
for extruder in extruders:
|
for extruder in extruders:
|
||||||
position = int(extruder.getMetaDataEntry("position", default="0")) # Get the position
|
position = int(extruder.getMetaDataEntry("position", default = "0"))
|
||||||
try:
|
try:
|
||||||
default_color = ExtrudersModel.defaultColors[position]
|
default_color = ExtrudersModel.defaultColors[position]
|
||||||
except IndexError:
|
except IndexError:
|
||||||
|
|
|
@ -66,11 +66,19 @@ class GcodeStartEndFormatter(Formatter):
|
||||||
return "{" + key + "}"
|
return "{" + key + "}"
|
||||||
|
|
||||||
key = key_fragments[0]
|
key = key_fragments[0]
|
||||||
try:
|
|
||||||
return kwargs[str(extruder_nr)][key]
|
default_value_str = "{" + key + "}"
|
||||||
except KeyError:
|
value = default_value_str
|
||||||
|
# "-1" is global stack, and if the setting value exists in the global stack, use it as the fallback value.
|
||||||
|
if key in kwargs["-1"]:
|
||||||
|
value = kwargs["-1"]
|
||||||
|
if str(extruder_nr) in kwargs and key in kwargs[str(extruder_nr)]:
|
||||||
|
value = kwargs[str(extruder_nr)][key]
|
||||||
|
|
||||||
|
if value == default_value_str:
|
||||||
Logger.log("w", "Unable to replace '%s' placeholder in start/end g-code", key)
|
Logger.log("w", "Unable to replace '%s' placeholder in start/end g-code", key)
|
||||||
return "{" + key + "}"
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
## Job class that builds up the message of scene data to send to CuraEngine.
|
## Job class that builds up the message of scene data to send to CuraEngine.
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"name": "CuraEngine Backend",
|
"name": "CuraEngine Backend",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"description": "Provides the link to the CuraEngine slicing backend.",
|
"description": "Provides the link to the CuraEngine slicing backend.",
|
||||||
"api": 5,
|
"api": "6.0",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "Cura Profile Reader",
|
"name": "Cura Profile Reader",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for importing Cura profiles.",
|
"description": "Provides support for importing Cura profiles.",
|
||||||
"api": 5,
|
"api": "6.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "Cura Profile Writer",
|
"name": "Cura Profile Writer",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for exporting Cura profiles.",
|
"description": "Provides support for exporting Cura profiles.",
|
||||||
"api": 5,
|
"api": "6.0",
|
||||||
"i18n-catalog":"cura"
|
"i18n-catalog":"cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -76,7 +76,7 @@ class FirmwareUpdateChecker(Extension):
|
||||||
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(container_name))
|
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(container_name))
|
||||||
return
|
return
|
||||||
|
|
||||||
self._check_job = FirmwareUpdateCheckerJob(container = container, silent = silent,
|
self._check_job = FirmwareUpdateCheckerJob(silent = silent,
|
||||||
machine_name = container_name, metadata = metadata,
|
machine_name = container_name, metadata = metadata,
|
||||||
callback = self._onActionTriggered)
|
callback = self._onActionTriggered)
|
||||||
self._check_job.start()
|
self._check_job.start()
|
||||||
|
|
|
@ -25,15 +25,14 @@ class FirmwareUpdateCheckerJob(Job):
|
||||||
ZERO_VERSION = Version(STRING_ZERO_VERSION)
|
ZERO_VERSION = Version(STRING_ZERO_VERSION)
|
||||||
EPSILON_VERSION = Version(STRING_EPSILON_VERSION)
|
EPSILON_VERSION = Version(STRING_EPSILON_VERSION)
|
||||||
|
|
||||||
def __init__(self, container, silent, machine_name, metadata, callback) -> None:
|
def __init__(self, silent, machine_name, metadata, callback) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._container = container
|
|
||||||
self.silent = silent
|
self.silent = silent
|
||||||
self._callback = callback
|
self._callback = callback
|
||||||
|
|
||||||
self._machine_name = machine_name
|
self._machine_name = machine_name
|
||||||
self._metadata = metadata
|
self._metadata = metadata
|
||||||
self._lookups = None # type:Optional[FirmwareUpdateCheckerLookup]
|
self._lookups = FirmwareUpdateCheckerLookup(self._machine_name, self._metadata)
|
||||||
self._headers = {} # type:Dict[str, str] # Don't set headers yet.
|
self._headers = {} # type:Dict[str, str] # Don't set headers yet.
|
||||||
|
|
||||||
def getUrlResponse(self, url: str) -> str:
|
def getUrlResponse(self, url: str) -> str:
|
||||||
|
@ -45,7 +44,6 @@ class FirmwareUpdateCheckerJob(Job):
|
||||||
result = response.read().decode("utf-8")
|
result = response.read().decode("utf-8")
|
||||||
except URLError:
|
except URLError:
|
||||||
Logger.log("w", "Could not reach '{0}', if this URL is old, consider removal.".format(url))
|
Logger.log("w", "Could not reach '{0}', if this URL is old, consider removal.".format(url))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def parseVersionResponse(self, response: str) -> Version:
|
def parseVersionResponse(self, response: str) -> Version:
|
||||||
|
@ -70,9 +68,6 @@ class FirmwareUpdateCheckerJob(Job):
|
||||||
return max_version
|
return max_version
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
if self._lookups is None:
|
|
||||||
self._lookups = FirmwareUpdateCheckerLookup(self._machine_name, self._metadata)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Initialize a Preference that stores the last version checked for this printer.
|
# Initialize a Preference that stores the last version checked for this printer.
|
||||||
Application.getInstance().getPreferences().addPreference(
|
Application.getInstance().getPreferences().addPreference(
|
||||||
|
@ -83,16 +78,18 @@ class FirmwareUpdateCheckerJob(Job):
|
||||||
application_version = Application.getInstance().getVersion()
|
application_version = Application.getInstance().getVersion()
|
||||||
self._headers = {"User-Agent": "%s - %s" % (application_name, application_version)}
|
self._headers = {"User-Agent": "%s - %s" % (application_name, application_version)}
|
||||||
|
|
||||||
# get machine name from the definition container
|
|
||||||
machine_name = self._container.definition.getName()
|
|
||||||
|
|
||||||
# If it is not None, then we compare between the checked_version and the current_version
|
# If it is not None, then we compare between the checked_version and the current_version
|
||||||
machine_id = self._lookups.getMachineId()
|
machine_id = self._lookups.getMachineId()
|
||||||
if machine_id is not None:
|
if machine_id is not None:
|
||||||
Logger.log("i", "You have a(n) {0} in the printer list. Let's check the firmware!".format(machine_name))
|
Logger.log("i", "You have a(n) {0} in the printer list. Do firmware-check.".format(self._machine_name))
|
||||||
|
|
||||||
current_version = self.getCurrentVersion()
|
current_version = self.getCurrentVersion()
|
||||||
|
|
||||||
|
# This case indicates that was an error checking the version.
|
||||||
|
# It happens for instance when not connected to internet.
|
||||||
|
if current_version == self.ZERO_VERSION:
|
||||||
|
return
|
||||||
|
|
||||||
# If it is the first time the version is checked, the checked_version is ""
|
# If it is the first time the version is checked, the checked_version is ""
|
||||||
setting_key_str = getSettingsKeyForMachine(machine_id)
|
setting_key_str = getSettingsKeyForMachine(machine_id)
|
||||||
checked_version = Version(Application.getInstance().getPreferences().getValue(setting_key_str))
|
checked_version = Version(Application.getInstance().getPreferences().getValue(setting_key_str))
|
||||||
|
@ -100,18 +97,20 @@ class FirmwareUpdateCheckerJob(Job):
|
||||||
# If the checked_version is "", it's because is the first time we check firmware and in this case
|
# If the checked_version is "", it's because is the first time we check firmware and in this case
|
||||||
# we will not show the notification, but we will store it for the next time
|
# we will not show the notification, but we will store it for the next time
|
||||||
Application.getInstance().getPreferences().setValue(setting_key_str, current_version)
|
Application.getInstance().getPreferences().setValue(setting_key_str, current_version)
|
||||||
Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s", machine_name, checked_version, current_version)
|
Logger.log("i", "Reading firmware version of %s: checked = %s - latest = %s",
|
||||||
|
self._machine_name, checked_version, current_version)
|
||||||
|
|
||||||
# The first time we want to store the current version, the notification will not be shown,
|
# The first time we want to store the current version, the notification will not be shown,
|
||||||
# because the new version of Cura will be release before the firmware and we don't want to
|
# because the new version of Cura will be release before the firmware and we don't want to
|
||||||
# notify the user when no new firmware version is available.
|
# notify the user when no new firmware version is available.
|
||||||
if (checked_version != "") and (checked_version != current_version):
|
if (checked_version != "") and (checked_version != current_version):
|
||||||
Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE")
|
Logger.log("i", "SHOWING FIRMWARE UPDATE MESSAGE")
|
||||||
message = FirmwareUpdateCheckerMessage(machine_id, machine_name, self._lookups.getRedirectUserUrl())
|
message = FirmwareUpdateCheckerMessage(machine_id, self._machine_name,
|
||||||
|
self._lookups.getRedirectUserUrl())
|
||||||
message.actionTriggered.connect(self._callback)
|
message.actionTriggered.connect(self._callback)
|
||||||
message.show()
|
message.show()
|
||||||
else:
|
else:
|
||||||
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(machine_name))
|
Logger.log("i", "No machine with name {0} in list of firmware to check.".format(self._machine_name))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
Logger.log("w", "Failed to check for new version: %s", e)
|
Logger.log("w", "Failed to check for new version: %s", e)
|
||||||
|
|
|
@ -18,7 +18,7 @@ class FirmwareUpdateCheckerLookup:
|
||||||
self._machine_id = machine_json.get("id")
|
self._machine_id = machine_json.get("id")
|
||||||
self._machine_name = machine_name.lower() # Lower in-case upper-case chars are added to the original json.
|
self._machine_name = machine_name.lower() # Lower in-case upper-case chars are added to the original json.
|
||||||
self._check_urls = [] # type:List[str]
|
self._check_urls = [] # type:List[str]
|
||||||
for check_url in machine_json.get("check_urls"):
|
for check_url in machine_json.get("check_urls", []):
|
||||||
self._check_urls.append(check_url)
|
self._check_urls.append(check_url)
|
||||||
self._redirect_user = machine_json.get("update_url")
|
self._redirect_user = machine_json.get("update_url")
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "Firmware Update Checker",
|
"name": "Firmware Update Checker",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Checks for firmware updates.",
|
"description": "Checks for firmware updates.",
|
||||||
"api": 5,
|
"api": "6.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from UM.Version import Version
|
||||||
|
|
||||||
|
from plugins.FirmwareUpdateChecker.FirmwareUpdateCheckerJob import FirmwareUpdateCheckerJob
|
||||||
|
from plugins.FirmwareUpdateChecker.FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup
|
||||||
|
|
||||||
|
json_data = \
|
||||||
|
{
|
||||||
|
"ned":
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"name": "ned",
|
||||||
|
"check_urls": [""],
|
||||||
|
"update_url": "https://ultimaker.com/en/resources/20500-upgrade-firmware",
|
||||||
|
"version_parser": "default"
|
||||||
|
},
|
||||||
|
"olivia":
|
||||||
|
{
|
||||||
|
"id": 3,
|
||||||
|
"name": "olivia",
|
||||||
|
"check_urls": [""],
|
||||||
|
"update_url": "https://ultimaker.com/en/resources/20500-upgrade-firmware",
|
||||||
|
"version_parser": "default"
|
||||||
|
},
|
||||||
|
"emmerson":
|
||||||
|
{
|
||||||
|
"id": 5,
|
||||||
|
"name": "emmerson",
|
||||||
|
"check_urls": [""],
|
||||||
|
"update_url": "https://ultimaker.com/en/resources/20500-upgrade-firmware",
|
||||||
|
"version_parser": "default"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name, id", [
|
||||||
|
("ned" , 1),
|
||||||
|
("olivia" , 3),
|
||||||
|
("emmerson", 5),
|
||||||
|
])
|
||||||
|
def test_FirmwareUpdateCheckerLookup(id, name):
|
||||||
|
lookup = FirmwareUpdateCheckerLookup(name, json_data.get(name))
|
||||||
|
|
||||||
|
assert lookup.getMachineName() == name
|
||||||
|
assert lookup.getMachineId() == id
|
||||||
|
assert len(lookup.getCheckUrls()) >= 1
|
||||||
|
assert lookup.getRedirectUserUrl() is not None
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name, version", [
|
||||||
|
("ned" , Version("5.1.2.3")),
|
||||||
|
("olivia" , Version("4.3.2.1")),
|
||||||
|
("emmerson", Version("6.7.8.1")),
|
||||||
|
])
|
||||||
|
def test_FirmwareUpdateCheckerJob_getCurrentVersion(name, version):
|
||||||
|
machine_data = json_data.get(name)
|
||||||
|
job = FirmwareUpdateCheckerJob(False, name, machine_data, MagicMock)
|
||||||
|
job.getUrlResponse = MagicMock(return_value = str(version)) # Pretend like we got a good response from the server
|
||||||
|
assert job.getCurrentVersion() == version
|
|
@ -57,7 +57,7 @@ class FirmwareUpdaterMachineAction(MachineAction):
|
||||||
outputDeviceCanUpdateFirmwareChanged = pyqtSignal()
|
outputDeviceCanUpdateFirmwareChanged = pyqtSignal()
|
||||||
@pyqtProperty(QObject, notify = outputDeviceCanUpdateFirmwareChanged)
|
@pyqtProperty(QObject, notify = outputDeviceCanUpdateFirmwareChanged)
|
||||||
def firmwareUpdater(self) -> Optional["FirmwareUpdater"]:
|
def firmwareUpdater(self) -> Optional["FirmwareUpdater"]:
|
||||||
if self._active_output_device and self._active_output_device.activePrinter.getController().can_update_firmware:
|
if self._active_output_device and self._active_output_device.activePrinter and self._active_output_device.activePrinter.getController().can_update_firmware:
|
||||||
self._active_firmware_updater = self._active_output_device.getFirmwareUpdater()
|
self._active_firmware_updater = self._active_output_device.getFirmwareUpdater()
|
||||||
return self._active_firmware_updater
|
return self._active_firmware_updater
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,7 @@ Cura.MachineAction
|
||||||
{
|
{
|
||||||
id: firmwareUpdaterMachineAction
|
id: firmwareUpdaterMachineAction
|
||||||
anchors.fill: parent;
|
anchors.fill: parent;
|
||||||
UM.I18nCatalog { id: catalog; name:"cura"}
|
UM.I18nCatalog { id: catalog; name: "cura"}
|
||||||
spacing: UM.Theme.getSize("default_margin").height
|
spacing: UM.Theme.getSize("default_margin").height
|
||||||
|
|
||||||
Label
|
Label
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "Firmware Updater",
|
"name": "Firmware Updater",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Provides a machine actions for updating firmware.",
|
"description": "Provides a machine actions for updating firmware.",
|
||||||
"api": 5,
|
"api": "6.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "Compressed G-code Reader",
|
"name": "Compressed G-code Reader",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Reads g-code from a compressed archive.",
|
"description": "Reads g-code from a compressed archive.",
|
||||||
"api": 5,
|
"api": "6.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "Compressed G-code Writer",
|
"name": "Compressed G-code Writer",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Writes g-code to a compressed archive.",
|
"description": "Writes g-code to a compressed archive.",
|
||||||
"api": 5,
|
"api": "6.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "G-code Profile Reader",
|
"name": "G-code Profile Reader",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Provides support for importing profiles from g-code files.",
|
"description": "Provides support for importing profiles from g-code files.",
|
||||||
"api": 5,
|
"api": "6.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -364,6 +364,8 @@ class FlavorParser:
|
||||||
self._layer_type = LayerPolygon.SupportType
|
self._layer_type = LayerPolygon.SupportType
|
||||||
elif type == "FILL":
|
elif type == "FILL":
|
||||||
self._layer_type = LayerPolygon.InfillType
|
self._layer_type = LayerPolygon.InfillType
|
||||||
|
elif type == "SUPPORT-INTERFACE":
|
||||||
|
self._layer_type = LayerPolygon.SupportInterfaceType
|
||||||
else:
|
else:
|
||||||
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
|
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "G-code Reader",
|
"name": "G-code Reader",
|
||||||
"author": "Victor Larchenko",
|
"author": "Victor Larchenko, Ultimaker",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Allows loading and displaying G-code files.",
|
"description": "Allows loading and displaying G-code files.",
|
||||||
"api": 5,
|
"api": "6.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "G-code Writer",
|
"name": "G-code Writer",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Writes g-code to a file.",
|
"description": "Writes g-code to a file.",
|
||||||
"api": 5,
|
"api": "6.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,7 +20,7 @@ UM.Dialog
|
||||||
|
|
||||||
GridLayout
|
GridLayout
|
||||||
{
|
{
|
||||||
UM.I18nCatalog{id: catalog; name:"cura"}
|
UM.I18nCatalog{id: catalog; name: "cura"}
|
||||||
anchors.fill: parent;
|
anchors.fill: parent;
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
columnSpacing: 16 * screenScaleFactor
|
columnSpacing: 16 * screenScaleFactor
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
{
|
{
|
||||||
"name": "Image Reader",
|
"name": "Image Reader",
|
||||||
"author": "Ultimaker B.V.",
|
"author": "Ultimaker B.V.",
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"description": "Enables ability to generate printable geometry from 2D image files.",
|
"description": "Enables ability to generate printable geometry from 2D image files.",
|
||||||
"api": 5,
|
"api": "6.0",
|
||||||
"i18n-catalog": "cura"
|
"i18n-catalog": "cura"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"source_version": "15.04",
|
"source_version": "15.04",
|
||||||
"target_version": 3,
|
"target_version": "4.5",
|
||||||
|
|
||||||
"translation": {
|
"translation": {
|
||||||
"machine_nozzle_size": "nozzle_size",
|
"machine_nozzle_size": "nozzle_size",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue