diff --git a/.gitignore b/.gitignore
index 0a66b6eb33..60b59e6829 100644
--- a/.gitignore
+++ b/.gitignore
@@ -42,7 +42,6 @@ plugins/cura-siemensnx-plugin
plugins/CuraBlenderPlugin
plugins/CuraCloudPlugin
plugins/CuraDrivePlugin
-plugins/CuraDrive
plugins/CuraLiveScriptingPlugin
plugins/CuraOpenSCADPlugin
plugins/CuraPrintProfileCreator
diff --git a/Jenkinsfile b/Jenkinsfile
index f9a3a9864a..a345ebbd05 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -38,20 +38,9 @@ parallel_nodes(['linux && cura', 'windows && cura'])
{
if (isUnix())
{
- // For Linux to show everything
- def branch = env.BRANCH_NAME
- if(!fileExists("${env.CURA_ENVIRONMENT_PATH}/${branch}"))
- {
- branch = "master"
- }
- def uranium_dir = get_workspace_dir("Ultimaker/Uranium/${branch}")
-
+ // For Linux
try {
- sh """
- cd ..
- export PYTHONPATH=.:"${uranium_dir}"
- ${env.CURA_ENVIRONMENT_PATH}/${branch}/bin/pytest -x --verbose --full-trace --capture=no ./tests
- """
+ sh 'make CTEST_OUTPUT_ON_FAILURE=TRUE test'
} catch(e)
{
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"
- }
- }
- }
}
}
diff --git a/README.md b/README.md
index 70466e9c22..93abcc0c61 100644
--- a/README.md
+++ b/README.md
@@ -20,8 +20,9 @@ Dependencies
------------
* [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.
+* [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.
-* [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
-------------
diff --git a/cmake/CuraTests.cmake b/cmake/CuraTests.cmake
index f2ee92d65b..b6d04de036 100644
--- a/cmake/CuraTests.cmake
+++ b/cmake/CuraTests.cmake
@@ -6,6 +6,8 @@ include(CMakeParseArguments)
find_package(PythonInterp 3.5.0 REQUIRED)
+add_custom_target(test-verbose COMMAND ${CMAKE_CTEST_COMMAND} --verbose)
+
function(cura_add_test)
set(_single_args NAME DIRECTORY PYTHONPATH)
cmake_parse_arguments("" "" "${_single_args}" "" ${ARGN})
diff --git a/cura.desktop.in b/cura.desktop.in
index fbe8b30fed..b0195015a5 100644
--- a/cura.desktop.in
+++ b/cura.desktop.in
@@ -13,6 +13,6 @@ TryExec=@CMAKE_INSTALL_FULL_BINDIR@/cura
Icon=cura-icon
Terminal=false
Type=Application
-MimeType=model/stl;application/vnd.ms-3mfdocument;application/prs.wavefront-obj;image/bmp;image/gif;image/jpeg;image/png;model/x3d+xml;
+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;
Keywords=3D;Printing;Slicer;
diff --git a/cura.sharedmimeinfo b/cura.sharedmimeinfo
index 23d38795eb..ed9099d425 100644
--- a/cura.sharedmimeinfo
+++ b/cura.sharedmimeinfo
@@ -19,4 +19,12 @@
+
+
+ Gcode file
+
+
+
+
+
\ No newline at end of file
diff --git a/cura/API/Account.py b/cura/API/Account.py
index 397e220478..a04f97ef1c 100644
--- a/cura/API/Account.py
+++ b/cura/API/Account.py
@@ -6,6 +6,7 @@ from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot, pyqtProperty
from UM.i18n import i18nCatalog
from UM.Message import Message
+from cura import UltimakerCloudAuthentication
from cura.OAuth2.AuthorizationService import AuthorizationService
from cura.OAuth2.Models import OAuth2Settings
@@ -37,15 +38,16 @@ class Account(QObject):
self._logged_in = False
self._callback_port = 32118
- self._oauth_root = "https://account.ultimaker.com"
- self._cloud_api_root = "https://api.ultimaker.com"
+ self._oauth_root = UltimakerCloudAuthentication.CuraCloudAccountAPIRoot
self._oauth_settings = OAuth2Settings(
OAUTH_SERVER_URL= self._oauth_root,
CALLBACK_PORT=self._callback_port,
CALLBACK_URL="http://localhost:{}/callback".format(self._callback_port),
- CLIENT_ID="um---------------ultimaker_cura_drive_plugin",
- CLIENT_SCOPES="account.user.read drive.backup.read drive.backup.write packages.download packages.rating.read packages.rating.write",
+ CLIENT_ID="um----------------------------ultimaker_cura",
+ 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_SUCCESS_REDIRECT="{}/app/auth-success".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.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)
def isLoggedIn(self) -> bool:
return self._logged_in
diff --git a/cura/API/Backups.py b/cura/API/Backups.py
index 8e5cd7b83a..ef74e74be0 100644
--- a/cura/API/Backups.py
+++ b/cura/API/Backups.py
@@ -1,6 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V.
# 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
@@ -24,12 +24,12 @@ class Backups:
## Create a new back-up using the BackupsManager.
# \return Tuple containing a ZIP file with the back-up data and a dict
# 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()
## Restore a back-up using the BackupsManager.
# \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
# 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)
diff --git a/cura/ApplicationMetadata.py b/cura/ApplicationMetadata.py
new file mode 100644
index 0000000000..4cb19edb72
--- /dev/null
+++ b/cura/ApplicationMetadata.py
@@ -0,0 +1,42 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+# ---------
+# Genearl constants used in 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 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
diff --git a/cura/Arranging/Arrange.py b/cura/Arranging/Arrange.py
index 5657ee991a..32796005c8 100644
--- a/cura/Arranging/Arrange.py
+++ b/cura/Arranging/Arrange.py
@@ -66,6 +66,11 @@ class Arrange:
continue
vertices = vertices.getMinkowskiHull(Polygon.approximatedCircle(min_offset))
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)
arranger.place(0, 0, shape_arr)
diff --git a/cura/Backups/Backup.py b/cura/Backups/Backup.py
index 897d5fa979..714d6527fe 100644
--- a/cura/Backups/Backup.py
+++ b/cura/Backups/Backup.py
@@ -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.
# 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 = Resources.getPath(Resources.Preferences, "{}.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)
- shutil.copyfile(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)):
+ 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.
buffer = io.BytesIO()
diff --git a/cura/BuildVolume.py b/cura/BuildVolume.py
index 547c3dae71..aa1f170707 100755
--- a/cura/BuildVolume.py
+++ b/cura/BuildVolume.py
@@ -83,7 +83,14 @@ class BuildVolume(SceneNode):
" with printed models."), title = catalog.i18nc("@info:title", "Build Volume"))
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._onStackChanged()
self._engine_ready = False
@@ -105,6 +112,8 @@ class BuildVolume(SceneNode):
self._setting_change_timer.setSingleShot(True)
self._setting_change_timer.timeout.connect(self._onSettingChangeTimerFinished)
+
+
# Must be after setting _build_volume_message, apparently that is used in getMachineManager.
# activeQualityChanged is always emitted after setActiveVariant, setActiveMaterial and setActiveQuality.
# 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)
)
+ self._application.getController().getScene()._maximum_bounds = scale_to_max_bounds
+
self.updateNodeBoundaryCheck()
def getBoundingBox(self) -> AxisAlignedBox:
@@ -489,7 +500,9 @@ class BuildVolume(SceneNode):
def _updateRaftThickness(self):
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
if self._adhesion_type == "raft":
self._raft_thickness = (
@@ -522,8 +535,11 @@ class BuildVolume(SceneNode):
if extra_z != self._extra_z_clearance:
self._extra_z_clearance = extra_z
- ## Update the build volume visualization
def _onStackChanged(self):
+ self._stack_change_timer.start()
+
+ ## Update the build volume visualization
+ def _onStackChangeTimerFinished(self):
if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onSettingPropertyChanged)
extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
diff --git a/cura/CrashHandler.py b/cura/CrashHandler.py
index 46544ca0ef..d43743bc37 100644
--- a/cura/CrashHandler.py
+++ b/cura/CrashHandler.py
@@ -36,18 +36,14 @@ else:
except ImportError:
CuraDebugMode = False # [CodeStyle: Reflecting imported value]
-# List of exceptions that should be considered "fatal" and abort the program.
-# These are primarily some exception types that we simply cannot really recover from
-# (MemoryError and SystemError) and exceptions that indicate grave errors in the
-# code that cause the Python interpreter to fail (SyntaxError, ImportError).
-fatal_exception_types = [
- MemoryError,
- SyntaxError,
- ImportError,
- SystemError,
+# List of exceptions that should not be considered "fatal" and abort the program.
+# These are primarily some exception types that we simply skip
+skip_exception_types = [
+ SystemExit,
+ KeyboardInterrupt,
+ GeneratorExit
]
-
class CrashHandler:
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 not fully started yet, we always show the early crash dialog. Otherwise, Cura will just crash
# 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
if not has_started:
@@ -387,7 +383,7 @@ class CrashHandler:
Application.getInstance().callLater(self._show)
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:
self.dialog.exec_()
os._exit(1)
diff --git a/cura/CuraActions.py b/cura/CuraActions.py
index 93a18318df..91e0966fed 100644
--- a/cura/CuraActions.py
+++ b/cura/CuraActions.py
@@ -3,7 +3,7 @@
from PyQt5.QtCore import QObject, QUrl
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.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.
# 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.
- 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)
@pyqtSlot()
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)
## Reset camera position and direction to default
@@ -61,8 +61,10 @@ class CuraActions(QObject):
operation = GroupedOperation()
for node in Selection.getAllSelectedObjects():
current_node = node
- while current_node.getParent() and current_node.getParent().callDecoration("isGroup"):
- current_node = current_node.getParent()
+ parent_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
# unpredictable matrix deconstruction it was possible that mirrors
@@ -150,13 +152,13 @@ class CuraActions(QObject):
root = cura.CuraApplication.CuraApplication.getInstance().getController().getScene().getRoot()
- nodes_to_change = []
+ nodes_to_change = [] # type: List[SceneNode]
for node in Selection.getAllSelectedObjects():
parent_node = node # Find the parent node to change instead
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)
if not nodes_to_change:
diff --git a/cura/CuraApplication.py b/cura/CuraApplication.py
index 47cc94f972..d3de15690d 100755
--- a/cura/CuraApplication.py
+++ b/cura/CuraApplication.py
@@ -4,7 +4,7 @@
import os
import sys
import time
-from typing import cast, TYPE_CHECKING, Optional, Callable
+from typing import cast, TYPE_CHECKING, Optional, Callable, List
import numpy
@@ -51,6 +51,7 @@ from cura.Arranging.ArrangeObjectsJob import ArrangeObjectsJob
from cura.Arranging.ArrangeObjectsAllBuildPlatesJob import ArrangeObjectsAllBuildPlatesJob
from cura.Arranging.ShapeArray import ShapeArray
from cura.MultiplyObjectsJob import MultiplyObjectsJob
+from cura.GlobalStacksModel import GlobalStacksModel
from cura.Scene.ConvexHullDecorator import ConvexHullDecorator
from cura.Operations.SetParentOperation import SetParentOperation
from cura.Scene.SliceableObjectDecorator import SliceableObjectDecorator
@@ -113,8 +114,11 @@ from cura.Settings.CuraFormulaFunctions import CuraFormulaFunctions
from cura.ObjectsModel import ObjectsModel
+from cura.PrinterOutputDevice import PrinterOutputDevice
from cura.PrinterOutput.NetworkMJPGImage import NetworkMJPGImage
+from cura import ApplicationMetadata
+
from UM.FlameProfiler import pyqtSlot
from UM.Decorators import override
@@ -127,6 +131,7 @@ if TYPE_CHECKING:
numpy.seterr(all = "ignore")
+
try:
from cura.CuraVersion import CuraAppDisplayName, CuraVersion, CuraBuildType, CuraDebugMode, CuraSDKVersion # type: ignore
except ImportError:
@@ -134,14 +139,14 @@ except ImportError:
CuraVersion = "master" # [CodeStyle: Reflecting imported value]
CuraBuildType = ""
CuraDebugMode = False
- CuraSDKVersion = "5.0.0"
+ CuraSDKVersion = "6.0.0"
class CuraApplication(QtApplication):
# SettingVersion represents the set of settings available in the machine/extruder definitions.
# You need to make sure that this version number needs to be increased if there is any non-backwards-compatible
# changes of the settings.
- SettingVersion = 5
+ SettingVersion = 6
Created = False
@@ -162,11 +167,11 @@ class CuraApplication(QtApplication):
def __init__(self, *args, **kwargs):
super().__init__(name = "cura",
- app_display_name = CuraAppDisplayName,
- version = CuraVersion,
- api_version = CuraSDKVersion,
- buildtype = CuraBuildType,
- is_debug_mode = CuraDebugMode,
+ app_display_name = ApplicationMetadata.CuraAppDisplayName,
+ version = ApplicationMetadata.CuraVersion,
+ api_version = ApplicationMetadata.CuraSDKVersion,
+ buildtype = ApplicationMetadata.CuraBuildType,
+ is_debug_mode = ApplicationMetadata.CuraDebugMode,
tray_icon_name = "cura-icon-32.png",
**kwargs)
@@ -181,7 +186,6 @@ class CuraApplication(QtApplication):
# Variables set from CLI
self._files_to_open = []
self._use_single_instance = False
- self._trigger_early_crash = False # For debug only
self._single_instance = None
@@ -206,6 +210,8 @@ class CuraApplication(QtApplication):
self._container_manager = None
self._object_manager = None
+ self._extruders_model = None
+ self._extruders_model_with_optional = None
self._build_plate_model = None
self._multi_build_plate_model = None
self._setting_visibility_presets_model = None
@@ -292,7 +298,10 @@ class CuraApplication(QtApplication):
sys.exit(0)
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:
self._files_to_open.append(os.path.abspath(filename))
@@ -428,7 +437,8 @@ class CuraApplication(QtApplication):
def startSplashWindowPhase(self) -> None:
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([
# Misc.:
@@ -439,6 +449,7 @@ class CuraApplication(QtApplication):
"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.
"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.
"LocalFileOutputDevice", #Major part of Cura's functionality.
"LocalContainerProvider", #Cura is useless without any profiles or setting definitions.
@@ -492,7 +503,8 @@ class CuraApplication(QtApplication):
preferences.addPreference("cura/choice_on_profile_override", "always_ask")
preferences.addPreference("cura/choice_on_open_project", "always_ask")
preferences.addPreference("cura/use_multi_build_plate", False)
-
+ preferences.addPreference("view/settings_list_height", 400)
+ preferences.addPreference("view/settings_visible", False)
preferences.addPreference("cura/currency", "€")
preferences.addPreference("cura/material_settings", "{}")
@@ -631,9 +643,7 @@ class CuraApplication(QtApplication):
self._message_box_callback(button, *self._message_box_callback_arguments)
self._message_box_callback = None
self._message_box_callback_arguments = []
-
- showPrintMonitor = pyqtSignal(bool, arguments = ["show"])
-
+
def setSaveDataEnabled(self, enabled: bool) -> None:
self._save_data_enabled = enabled
@@ -659,12 +669,12 @@ class CuraApplication(QtApplication):
## Handle loading of all plugin types (and the backend explicitly)
# \sa PluginRegistry
- def _loadPlugins(self):
+ def _loadPlugins(self) -> None:
self._plugin_registry.addType("profile_reader", self._addProfileReader)
self._plugin_registry.addType("profile_writer", self._addProfileWriter)
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:
lib_suffixes = {""}
for suffix in lib_suffixes:
@@ -861,6 +871,19 @@ class CuraApplication(QtApplication):
self._object_manager = ObjectsModel.createObjectsModel()
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)
def getMultiBuildPlateModel(self, *args) -> MultiBuildPlateModel:
if self._multi_build_plate_model is None:
@@ -935,7 +958,7 @@ class CuraApplication(QtApplication):
engine.rootContext().setContextProperty("CuraApplication", self)
engine.rootContext().setContextProperty("PrintInformation", self._print_information)
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")
@@ -953,6 +976,7 @@ class CuraApplication(QtApplication):
qmlRegisterType(MultiBuildPlateModel, "Cura", 1, 0, "MultiBuildPlateModel")
qmlRegisterType(InstanceContainer, "Cura", 1, 0, "InstanceContainer")
qmlRegisterType(ExtrudersModel, "Cura", 1, 0, "ExtrudersModel")
+ qmlRegisterType(GlobalStacksModel, "Cura", 1, 0, "GlobalStacksModel")
qmlRegisterType(FavoriteMaterialsModel, "Cura", 1, 0, "FavoriteMaterialsModel")
qmlRegisterType(GenericMaterialsModel, "Cura", 1, 0, "GenericMaterialsModel")
@@ -974,6 +998,8 @@ class CuraApplication(QtApplication):
qmlRegisterSingletonType(ContainerManager, "Cura", 1, 0, "ContainerManager", ContainerManager.getInstance)
qmlRegisterType(SidebarCustomMenuItemsModel, "Cura", 1, 0, "SidebarCustomMenuItemsModel")
+ qmlRegisterType(PrinterOutputDevice, "Cura", 1, 0, "PrinterOutputDevice")
+
from cura.API import CuraAPI
qmlRegisterSingletonType(CuraAPI, "Cura", 1, 1, "API", self.getCuraAPI)
@@ -1084,88 +1110,6 @@ class CuraApplication(QtApplication):
self._platform_activity = True if count > 0 else False
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.
@pyqtSlot()
def selectAll(self):
@@ -1245,62 +1189,75 @@ class CuraApplication(QtApplication):
## Arrange all objects.
@pyqtSlot()
- def arrangeObjectsToAllBuildPlates(self):
- nodes = []
- for node in DepthFirstIterator(self.getController().getScene().getRoot()):
+ def arrangeObjectsToAllBuildPlates(self) -> None:
+ nodes_to_arrange = []
+ for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
if not isinstance(node, SceneNode):
continue
+
if not node.getMeshData() and not node.callDecoration("isGroup"):
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"):
continue # i.e. node with layer data
+
+ bounding_box = node.getBoundingBox()
# Skip nodes that are too big
- if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
- nodes.append(node)
- job = ArrangeObjectsAllBuildPlatesJob(nodes)
+ if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
+ nodes_to_arrange.append(node)
+ job = ArrangeObjectsAllBuildPlatesJob(nodes_to_arrange)
job.start()
self.getCuraSceneController().setActiveBuildPlate(0) # Select first build plate
# Single build plate
@pyqtSlot()
- def arrangeAll(self):
- nodes = []
+ def arrangeAll(self) -> None:
+ nodes_to_arrange = []
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):
continue
+
if not node.getMeshData() and not node.callDecoration("isGroup"):
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)
+
if not node.isSelectable():
continue # i.e. node with layer data
+
if not node.callDecoration("isSliceable") and not node.callDecoration("isGroup"):
continue # i.e. node with layer data
+
if node.callDecoration("getBuildPlateNumber") == active_build_plate:
# Skip nodes that are too big
- if node.getBoundingBox().width < self._volume.getBoundingBox().width or node.getBoundingBox().depth < self._volume.getBoundingBox().depth:
- nodes.append(node)
- self.arrange(nodes, fixed_nodes = [])
+ bounding_box = node.getBoundingBox()
+ if bounding_box is None or bounding_box.width < self._volume.getBoundingBox().width or bounding_box.depth < self._volume.getBoundingBox().depth:
+ nodes_to_arrange.append(node)
+ self.arrange(nodes_to_arrange, fixed_nodes = [])
## Arrange a set of nodes given a set of fixed nodes
# \param nodes nodes that we have to place
# \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
job = ArrangeObjectsJob(nodes, fixed_nodes, min_offset = max(min_offset, 8))
job.start()
## Reload all mesh data on the screen from file.
@pyqtSlot()
- def reloadAll(self):
+ def reloadAll(self) -> None:
Logger.log("i", "Reloading all loaded mesh data.")
nodes = []
has_merged_nodes = False
- for node in DepthFirstIterator(self.getController().getScene().getRoot()):
- if not isinstance(node, CuraSceneNode) or not node.getMeshData() :
+ for node in DepthFirstIterator(self.getController().getScene().getRoot()): # type: ignore
+ if not isinstance(node, CuraSceneNode) or not node.getMeshData():
if node.getName() == "MergedMesh":
has_merged_nodes = True
continue
@@ -1314,7 +1271,7 @@ class CuraApplication(QtApplication):
file_name = node.getMeshData().getFileName()
if file_name:
job = ReadMeshJob(file_name)
- job._node = node
+ job._node = node # type: ignore
job.finished.connect(self._reloadMeshFinished)
if has_merged_nodes:
job.finished.connect(self.updateOriginOfMergedMeshes)
@@ -1323,20 +1280,8 @@ class CuraApplication(QtApplication):
else:
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")
- def setExpandedCategories(self, categories):
+ def setExpandedCategories(self, categories: List[str]) -> None:
categories = list(set(categories))
categories.sort()
joined = ";".join(categories)
@@ -1347,7 +1292,7 @@ class CuraApplication(QtApplication):
expandedCategoriesChanged = pyqtSignal()
@pyqtProperty("QStringList", notify = expandedCategoriesChanged)
- def expandedCategories(self):
+ def expandedCategories(self) -> List[str]:
return self.getPreferences().getValue("cura/categories_expanded").split(";")
@pyqtSlot()
@@ -1397,13 +1342,12 @@ class CuraApplication(QtApplication):
## Updates origin position of all merged meshes
- # \param jobNode \type{Job} empty object which passed which is required by JobQueue
- def updateOriginOfMergedMeshes(self, jobNode):
+ def updateOriginOfMergedMeshes(self, _):
group_nodes = []
for node in DepthFirstIterator(self.getController().getScene().getRoot()):
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():
if isinstance(decorator, GroupDecorator):
group_nodes.append(node)
@@ -1447,7 +1391,7 @@ class CuraApplication(QtApplication):
@pyqtSlot()
- def groupSelected(self):
+ def groupSelected(self) -> None:
# Create a group-node
group_node = CuraSceneNode()
group_decorator = GroupDecorator()
@@ -1463,7 +1407,8 @@ class CuraApplication(QtApplication):
# Remove nodes that are directly parented to another selected node from the selection so they remain parented
selected_nodes = Selection.getAllSelectedObjects().copy()
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)
# Move selected nodes into the group-node
@@ -1475,7 +1420,7 @@ class CuraApplication(QtApplication):
Selection.add(group_node)
@pyqtSlot()
- def ungroupSelected(self):
+ def ungroupSelected(self) -> None:
selected_objects = Selection.getAllSelectedObjects().copy()
for node in selected_objects:
if node.callDecoration("isGroup"):
@@ -1498,7 +1443,7 @@ class CuraApplication(QtApplication):
# Note: The group removes itself from the scene once all its children have left it,
# see GroupDecorator._onChildrenChanged
- def _createSplashScreen(self):
+ def _createSplashScreen(self) -> Optional[CuraSplashScreen.CuraSplashScreen]:
if self._is_headless:
return None
return CuraSplashScreen.CuraSplashScreen()
@@ -1664,7 +1609,9 @@ class CuraApplication(QtApplication):
is_non_sliceable = "." + file_extension in self._non_sliceable_extensions
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()
node.addDecorator(block_slicing_decorator)
diff --git a/cura/CuraVersion.py.in b/cura/CuraVersion.py.in
index 7c6304231d..770a0efd7b 100644
--- a/cura/CuraVersion.py.in
+++ b/cura/CuraVersion.py.in
@@ -8,3 +8,4 @@ CuraDebugMode = True if "@_cura_debugmode@" == "ON" else False
CuraSDKVersion = "@CURA_SDK_VERSION@"
CuraCloudAPIRoot = "@CURA_CLOUD_API_ROOT@"
CuraCloudAPIVersion = "@CURA_CLOUD_API_VERSION@"
+CuraCloudAccountAPIRoot = "@CURA_CLOUD_ACCOUNT_API_ROOT@"
diff --git a/cura/CuraView.py b/cura/CuraView.py
new file mode 100644
index 0000000000..978c651b43
--- /dev/null
+++ b/cura/CuraView.py
@@ -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")
\ No newline at end of file
diff --git a/cura/GlobalStacksModel.py b/cura/GlobalStacksModel.py
new file mode 100644
index 0000000000..289a03d1c4
--- /dev/null
+++ b/cura/GlobalStacksModel.py
@@ -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)
diff --git a/cura/LayerPolygon.py b/cura/LayerPolygon.py
index f33934de0c..1941a558ba 100644
--- a/cura/LayerPolygon.py
+++ b/cura/LayerPolygon.py
@@ -5,6 +5,8 @@ from UM.Application import Application
from typing import Any
import numpy
+from UM.Logger import Logger
+
class LayerPolygon:
NoneType = 0
@@ -18,7 +20,8 @@ class LayerPolygon:
MoveCombingType = 8
MoveRetractionType = 9
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)
@@ -33,7 +36,8 @@ class LayerPolygon:
self._extruder = extruder
self._types = line_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._data = data
self._line_widths = line_widths
@@ -236,7 +240,8 @@ class LayerPolygon:
theme.getColor("layerview_support_infill").getRgbF(), # SupportInfillType
theme.getColor("layerview_move_combing").getRgbF(), # MoveCombingType
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
diff --git a/cura/Machines/MachineErrorChecker.py b/cura/Machines/MachineErrorChecker.py
index 06f064315b..fb11123af6 100644
--- a/cura/Machines/MachineErrorChecker.py
+++ b/cura/Machines/MachineErrorChecker.py
@@ -64,21 +64,21 @@ class MachineErrorChecker(QObject):
def _onMachineChanged(self) -> None:
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)
for extruder in self._global_stack.extruders.values():
- extruder.propertyChanged.disconnect(self.startErrorCheck)
+ extruder.propertyChanged.disconnect(self.startErrorCheckPropertyChanged)
extruder.containersChanged.disconnect(self.startErrorCheck)
self._global_stack = self._machine_manager.activeMachine
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)
for extruder in self._global_stack.extruders.values():
- extruder.propertyChanged.connect(self.startErrorCheck)
+ extruder.propertyChanged.connect(self.startErrorCheckPropertyChanged)
extruder.containersChanged.connect(self.startErrorCheck)
hasErrorUpdated = pyqtSignal()
@@ -93,6 +93,13 @@ class MachineErrorChecker(QObject):
def needToWaitForResult(self) -> bool:
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.
def startErrorCheck(self, *args) -> None:
if not self._check_in_progress:
diff --git a/cura/Machines/MaterialManager.py b/cura/Machines/MaterialManager.py
index aee96f3153..160508e7a6 100644
--- a/cura/Machines/MaterialManager.py
+++ b/cura/Machines/MaterialManager.py
@@ -302,6 +302,10 @@ class MaterialManager(QObject):
def getMaterialGroupListByGUID(self, guid: str) -> Optional[List[MaterialGroup]]:
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.
#
@@ -679,7 +683,11 @@ class MaterialManager(QObject):
@pyqtSlot(str)
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()
# Ensure all settings are saved.
@@ -688,4 +696,4 @@ class MaterialManager(QObject):
@pyqtSlot()
def getFavorites(self):
- return self._favorites
\ No newline at end of file
+ return self._favorites
diff --git a/cura/Machines/Models/BaseMaterialsModel.py b/cura/Machines/Models/BaseMaterialsModel.py
index ef2e760330..212e4fcf1e 100644
--- a/cura/Machines/Models/BaseMaterialsModel.py
+++ b/cura/Machines/Models/BaseMaterialsModel.py
@@ -1,5 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V.
# 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 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.
# 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
+from cura.Machines.MaterialNode import MaterialNode
+
+
class BaseMaterialsModel(ListModel):
extruderPositionChanged = pyqtSignal()
@@ -54,8 +58,8 @@ class BaseMaterialsModel(ListModel):
self._extruder_position = 0
self._extruder_stack = None
- self._available_materials = None
- self._favorite_ids = None
+ self._available_materials = None # type: Optional[Dict[str, MaterialNode]]
+ self._favorite_ids = set() # type: Set[str]
def _updateExtruderStack(self):
global_stack = self._machine_manager.activeMachine
@@ -102,7 +106,6 @@ class BaseMaterialsModel(ListModel):
return False
extruder_stack = global_stack.extruders[extruder_position]
-
self._available_materials = self._material_manager.getAvailableMaterialsForMachineExtruder(global_stack, extruder_stack)
if self._available_materials is None:
return False
diff --git a/cura/Machines/Models/FavoriteMaterialsModel.py b/cura/Machines/Models/FavoriteMaterialsModel.py
index 18fe310c44..98a2a01597 100644
--- a/cura/Machines/Models/FavoriteMaterialsModel.py
+++ b/cura/Machines/Models/FavoriteMaterialsModel.py
@@ -1,20 +1,16 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
-from UM.Logger import Logger
from cura.Machines.Models.BaseMaterialsModel import BaseMaterialsModel
+## Model that shows the list of favorite materials.
class FavoriteMaterialsModel(BaseMaterialsModel):
-
def __init__(self, parent = None):
super().__init__(parent)
self._update()
def _update(self):
-
- # Perform standard check and reset if the check fails
if not self._canUpdate():
- self.setItems([])
return
# Get updated list of favorites
diff --git a/cura/Machines/Models/GenericMaterialsModel.py b/cura/Machines/Models/GenericMaterialsModel.py
index c276b865bf..8f41dd6a70 100644
--- a/cura/Machines/Models/GenericMaterialsModel.py
+++ b/cura/Machines/Models/GenericMaterialsModel.py
@@ -11,10 +11,7 @@ class GenericMaterialsModel(BaseMaterialsModel):
self._update()
def _update(self):
-
- # Perform standard check and reset if the check fails
if not self._canUpdate():
- self.setItems([])
return
# Get updated list of favorites
diff --git a/cura/Machines/Models/MaterialBrandsModel.py b/cura/Machines/Models/MaterialBrandsModel.py
index 458e4d9b47..ac82cf6670 100644
--- a/cura/Machines/Models/MaterialBrandsModel.py
+++ b/cura/Machines/Models/MaterialBrandsModel.py
@@ -28,12 +28,8 @@ class MaterialBrandsModel(BaseMaterialsModel):
self._update()
def _update(self):
-
- # Perform standard check and reset if the check fails
if not self._canUpdate():
- self.setItems([])
return
-
# Get updated list of favorites
self._favorite_ids = self._material_manager.getFavorites()
diff --git a/cura/Machines/Models/NozzleModel.py b/cura/Machines/Models/NozzleModel.py
index 9d97106d6b..785ff5b9b9 100644
--- a/cura/Machines/Models/NozzleModel.py
+++ b/cura/Machines/Models/NozzleModel.py
@@ -33,8 +33,6 @@ class NozzleModel(ListModel):
def _update(self):
Logger.log("d", "Updating {model_class_name}.".format(model_class_name = self.__class__.__name__))
- self.items.clear()
-
global_stack = self._machine_manager.activeMachine
if global_stack is None:
self.setItems([])
diff --git a/cura/Machines/Models/QualityProfilesDropDownMenuModel.py b/cura/Machines/Models/QualityProfilesDropDownMenuModel.py
index a01cc1194f..7ccc886bfe 100644
--- a/cura/Machines/Models/QualityProfilesDropDownMenuModel.py
+++ b/cura/Machines/Models/QualityProfilesDropDownMenuModel.py
@@ -1,7 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V.
# 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.Logger import Logger
@@ -21,6 +21,7 @@ class QualityProfilesDropDownMenuModel(ListModel):
AvailableRole = Qt.UserRole + 5
QualityGroupRole = Qt.UserRole + 6
QualityChangesGroupRole = Qt.UserRole + 7
+ IsExperimentalRole = Qt.UserRole + 8
def __init__(self, parent = None):
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.QualityGroupRole, "quality_group")
self.addRoleName(self.QualityChangesGroupRole, "quality_changes_group")
+ self.addRoleName(self.IsExperimentalRole, "is_experimental")
self._application = Application.getInstance()
self._machine_manager = self._application.getMachineManager()
self._quality_manager = Application.getInstance().getQualityManager()
- self._application.globalContainerStackChanged.connect(self._update)
- self._machine_manager.activeQualityGroupChanged.connect(self._update)
- self._machine_manager.extruderChanged.connect(self._update)
- self._quality_manager.qualitiesUpdated.connect(self._update)
+ self._application.globalContainerStackChanged.connect(self._onChange)
+ self._machine_manager.activeQualityGroupChanged.connect(self._onChange)
+ self._machine_manager.extruderChanged.connect(self._onChange)
+ self._quality_manager.qualitiesUpdated.connect(self._onChange)
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()
+ def _onChange(self) -> None:
+ self._update_timer.start()
+
def _update(self):
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_unit": self._layer_height_unit,
"available": quality_group.is_available,
- "quality_group": quality_group}
+ "quality_group": quality_group,
+ "is_experimental": quality_group.is_experimental}
item_list.append(item)
diff --git a/cura/Machines/Models/SettingVisibilityPresetsModel.py b/cura/Machines/Models/SettingVisibilityPresetsModel.py
index 79131521f2..baa8e3ed29 100644
--- a/cura/Machines/Models/SettingVisibilityPresetsModel.py
+++ b/cura/Machines/Models/SettingVisibilityPresetsModel.py
@@ -6,6 +6,7 @@ from typing import Optional, List
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from UM.Logger import Logger
+from UM.Preferences import Preferences
from UM.Resources import Resources
from UM.i18n import i18nCatalog
@@ -18,14 +19,20 @@ class SettingVisibilityPresetsModel(QObject):
onItemsChanged = pyqtSignal()
activePresetChanged = pyqtSignal()
- def __init__(self, preferences, parent = None):
+ def __init__(self, preferences: Preferences, parent = None) -> None:
super().__init__(parent)
self._items = [] # type: List[SettingVisibilityPreset]
+ self._custom_preset = SettingVisibilityPreset(preset_id = "custom", name = "Custom selection", weight = -100)
+
self._populate()
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
@@ -42,7 +49,8 @@ class SettingVisibilityPresetsModel(QObject):
visible_settings = self._preferences.getValue("general/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:
self._onPreferencesChanged("general/visible_settings")
@@ -59,9 +67,7 @@ class SettingVisibilityPresetsModel(QObject):
def _populate(self) -> None:
from cura.CuraApplication import CuraApplication
items = [] # type: List[SettingVisibilityPreset]
-
- custom_preset = SettingVisibilityPreset(preset_id="custom", name ="Custom selection", weight = -100)
- items.append(custom_preset)
+ items.append(self._custom_preset)
for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.SettingVisibilityPreset):
setting_visibility_preset = SettingVisibilityPreset()
try:
@@ -77,7 +83,7 @@ class SettingVisibilityPresetsModel(QObject):
self.setItems(items)
@pyqtProperty("QVariantList", notify = onItemsChanged)
- def items(self):
+ def items(self) -> List[SettingVisibilityPreset]:
return self._items
def setItems(self, items: List[SettingVisibilityPreset]) -> None:
@@ -87,7 +93,7 @@ class SettingVisibilityPresetsModel(QObject):
@pyqtSlot(str)
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)
return
@@ -96,7 +102,7 @@ class SettingVisibilityPresetsModel(QObject):
Logger.log("w", "Tried to set active preset to unknown id [%s]", preset_id)
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:
# Save the current visibility settings to custom
current_visibility_string = self._preferences.getValue("general/visible_settings")
@@ -117,7 +123,9 @@ class SettingVisibilityPresetsModel(QObject):
@pyqtProperty(str, notify = activePresetChanged)
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:
if name != "general/visible_settings":
@@ -149,7 +157,12 @@ class SettingVisibilityPresetsModel(QObject):
else:
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:
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()
diff --git a/cura/Machines/QualityGroup.py b/cura/Machines/QualityGroup.py
index 535ba453f8..f5bcbb0de8 100644
--- a/cura/Machines/QualityGroup.py
+++ b/cura/Machines/QualityGroup.py
@@ -4,6 +4,9 @@
from typing import Dict, Optional, List, Set
from PyQt5.QtCore import QObject, pyqtSlot
+
+from UM.Util import parseBool
+
from cura.Machines.ContainerNode import ContainerNode
@@ -29,6 +32,7 @@ class QualityGroup(QObject):
self.nodes_for_extruders = {} # type: Dict[int, ContainerNode]
self.quality_type = quality_type
self.is_available = False
+ self.is_experimental = False
@pyqtSlot(result = str)
def getName(self) -> str:
@@ -51,3 +55,17 @@ class QualityGroup(QObject):
for extruder_node in self.nodes_for_extruders.values():
result.append(extruder_node)
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
diff --git a/cura/Machines/QualityManager.py b/cura/Machines/QualityManager.py
index a784d17f0b..34cc9ce4b2 100644
--- a/cura/Machines/QualityManager.py
+++ b/cura/Machines/QualityManager.py
@@ -235,7 +235,7 @@ class QualityManager(QObject):
for quality_type, quality_node in node.quality_type_map.items():
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
break
@@ -337,7 +337,7 @@ class QualityManager(QObject):
quality_group = quality_group_dict[quality_type]
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
# and use the material/variant specific qualities.
@@ -367,7 +367,7 @@ class QualityManager(QObject):
if node and node.quality_type_map:
for quality_type, quality_node in node.quality_type_map.items():
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
break
diff --git a/cura/Machines/VariantManager.py b/cura/Machines/VariantManager.py
index f6feb70e09..eaaa9fc5f0 100644
--- a/cura/Machines/VariantManager.py
+++ b/cura/Machines/VariantManager.py
@@ -107,7 +107,7 @@ class VariantManager:
break
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]:
machine_definition_id = machine.definition.getId()
diff --git a/cura/MultiplyObjectsJob.py b/cura/MultiplyObjectsJob.py
index 3cbf795952..e71bbf6668 100644
--- a/cura/MultiplyObjectsJob.py
+++ b/cura/MultiplyObjectsJob.py
@@ -25,7 +25,7 @@ class MultiplyObjectsJob(Job):
def run(self):
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()
scene = Application.getInstance().getController().getScene()
diff --git a/cura/OAuth2/AuthorizationHelpers.py b/cura/OAuth2/AuthorizationHelpers.py
index f75ad9c9f9..762d0db069 100644
--- a/cura/OAuth2/AuthorizationHelpers.py
+++ b/cura/OAuth2/AuthorizationHelpers.py
@@ -81,9 +81,14 @@ class AuthorizationHelpers:
# \param access_token: The encoded JWT token.
# \return: Dict containing some profile data.
def parseJWT(self, access_token: str) -> Optional["UserProfile"]:
- token_request = requests.get("{}/check-token".format(self._settings.OAUTH_SERVER_URL), headers = {
- "Authorization": "Bearer {}".format(access_token)
- })
+ try:
+ 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):
Logger.log("w", "Could not retrieve token data from auth server: %s", token_request.text)
return None
diff --git a/cura/OAuth2/AuthorizationService.py b/cura/OAuth2/AuthorizationService.py
index 4355891139..1e98dc9cee 100644
--- a/cura/OAuth2/AuthorizationService.py
+++ b/cura/OAuth2/AuthorizationService.py
@@ -52,8 +52,11 @@ class AuthorizationService:
if not self._user_profile:
# If no user profile was stored locally, we try to get it from JWT.
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.
+ Logger.log("w", "The user profile could not be loaded. The user must log in again!")
+ self.deleteAuthData()
return None
return self._user_profile
@@ -83,9 +86,11 @@ class AuthorizationService:
if not self.getUserProfile():
# 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.
+ Logger.log("w", "Unable to get the user profile.")
return None
if self._auth_data is None:
+ Logger.log("d", "No auth data to retrieve the access_token from")
return None
return self._auth_data.access_token
diff --git a/cura/OAuth2/Models.py b/cura/OAuth2/Models.py
index 83fc22554f..0515e789e6 100644
--- a/cura/OAuth2/Models.py
+++ b/cura/OAuth2/Models.py
@@ -1,4 +1,6 @@
# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
from typing import Optional
diff --git a/cura/PrintInformation.py b/cura/PrintInformation.py
index e11f70a54c..e863689e21 100644
--- a/cura/PrintInformation.py
+++ b/cura/PrintInformation.py
@@ -14,8 +14,7 @@ from UM.Logger import Logger
from UM.Qt.Duration import Duration
from UM.Scene.SceneNode import SceneNode
from UM.i18n import i18nCatalog
-from UM.MimeTypeDatabase import MimeTypeDatabase
-
+from UM.MimeTypeDatabase import MimeTypeDatabase, MimeTypeNotFoundError
from typing import TYPE_CHECKING
@@ -361,7 +360,7 @@ class PrintInformation(QObject):
try:
mime_type = MimeTypeDatabase.getMimeTypeForFile(name)
data = mime_type.stripExtension(name)
- except:
+ except MimeTypeNotFoundError:
Logger.log("w", "Unsupported Mime Type Database file extension %s", name)
if data is not None and check_name is not None:
@@ -395,28 +394,14 @@ class PrintInformation(QObject):
return
active_machine_type_name = global_container_stack.definition.getName()
- abbr_machine = ""
- 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
+ self._abbr_machine = self._application.getMachineManager().getAbbreviatedMachineName(active_machine_type_name)
## Utility method that strips accents from characters (eg: â -> a)
def _stripAccents(self, to_strip: str) -> str:
return ''.join(char for char in unicodedata.normalize('NFD', to_strip) if unicodedata.category(char) != 'Mn')
@pyqtSlot(result = "QVariantMap")
- def getFeaturePrintTimes(self):
+ def getFeaturePrintTimes(self) -> Dict[str, Duration]:
result = {}
if self._active_build_plate not in self._print_times_per_feature:
self._initPrintTimesPerFeature(self._active_build_plate)
diff --git a/cura/PrinterOutput/ConfigurationModel.py b/cura/PrinterOutput/ConfigurationModel.py
index 89e609c913..f9d0c7e36b 100644
--- a/cura/PrinterOutput/ConfigurationModel.py
+++ b/cura/PrinterOutput/ConfigurationModel.py
@@ -54,7 +54,7 @@ class ConfigurationModel(QObject):
for configuration in self._extruder_configurations:
if configuration is None:
return False
- return self._printer_type is not None
+ return self._printer_type != ""
def __str__(self):
message_chunks = []
diff --git a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py
index 35d2ce014a..47a6caf3e5 100644
--- a/cura/PrinterOutput/NetworkedPrinterOutputDevice.py
+++ b/cura/PrinterOutput/NetworkedPrinterOutputDevice.py
@@ -4,19 +4,21 @@
from UM.FileHandler.FileHandler import FileHandler #For typing.
from UM.Logger import Logger
from UM.Scene.SceneNode import SceneNode #For typing.
+from cura.API import Account
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.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject, QUrl, QCoreApplication
from time import time
-from typing import Any, Callable, Dict, List, Optional
+from typing import Callable, Dict, List, Optional, Union
from enum import IntEnum
import os # To get the username
import gzip
+
class AuthState(IntEnum):
NotAuthenticated = 1
AuthenticationRequested = 2
@@ -28,8 +30,8 @@ class AuthState(IntEnum):
class NetworkedPrinterOutputDevice(PrinterOutputDevice):
authenticationStateChanged = pyqtSignal()
- def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], parent: QObject = None) -> None:
- super().__init__(device_id = device_id, parent = parent)
+ def __init__(self, device_id, address: str, properties: Dict[bytes, bytes], connection_type: ConnectionType = ConnectionType.NetworkConnection, parent: QObject = None) -> None:
+ super().__init__(device_id = device_id, connection_type = connection_type, parent = parent)
self._manager = None # type: Optional[QNetworkAccessManager]
self._last_manager_create_time = None # type: Optional[float]
self._recreate_network_manager_time = 30
@@ -41,7 +43,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._api_prefix = ""
self._address = address
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._authentication_state = AuthState.NotAuthenticated
@@ -55,7 +58,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._gcode = [] # type: List[str]
self._connection_state_before_timeout = None # type: Optional[ConnectionState]
- def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False, file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
+ def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
+ file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
raise NotImplementedError("requestWrite needs to be implemented")
def setAuthenticationState(self, authentication_state: AuthState) -> None:
@@ -125,7 +129,7 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
if self._connection_state_before_timeout is None:
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
# 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:
self._createNetworkManager()
assert(self._manager is not None)
- elif self._connection_state == ConnectionState.closed:
+ elif self._connection_state == ConnectionState.Closed:
# Go out of timeout.
if self._connection_state_before_timeout is not None: # sanity check, but it should never be None here
self.setConnectionState(self._connection_state_before_timeout)
@@ -143,10 +147,15 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
url = QUrl("http://" + self._address + self._api_prefix + target)
request = QNetworkRequest(url)
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)
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:
part = QHttpPart()
@@ -160,9 +169,15 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
part.setBody(data)
return part
- ## Convenience function to get the username from the OS.
- # The code was copied from the getpass module, as we try to use as little dependencies as possible.
+ ## Convenience function to get the username, either from the cloud or from the OS.
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"):
user = os.environ.get(name)
if user:
@@ -178,49 +193,89 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._createNetworkManager()
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()
- 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()
- 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()
- 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()
- 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()
request = self._createEmptyRequest(target, content_type=None)
multi_post_part = QHttpMultiPart(QHttpMultiPart.FormDataType)
@@ -282,8 +337,8 @@ class NetworkedPrinterOutputDevice(PrinterOutputDevice):
self._last_response_time = time()
- if self._connection_state == ConnectionState.connecting:
- self.setConnectionState(ConnectionState.connected)
+ if self._connection_state == ConnectionState.Connecting:
+ self.setConnectionState(ConnectionState.Connected)
callback_key = reply.url().toString() + str(reply.operation())
try:
diff --git a/cura/PrinterOutput/PrintJobOutputModel.py b/cura/PrinterOutput/PrintJobOutputModel.py
index 25b168e6fd..a77ac81909 100644
--- a/cura/PrinterOutput/PrintJobOutputModel.py
+++ b/cura/PrinterOutput/PrintJobOutputModel.py
@@ -118,17 +118,40 @@ class PrintJobOutputModel(QObject):
self.nameChanged.emit()
@pyqtProperty(int, notify = timeTotalChanged)
- def timeTotal(self):
+ def timeTotal(self) -> int:
return self._time_total
@pyqtProperty(int, notify = timeElapsedChanged)
- def timeElapsed(self):
+ def timeElapsed(self) -> int:
return self._time_elapsed
+ @pyqtProperty(int, notify = timeElapsedChanged)
+ def timeRemaining(self) -> int:
+ # Never get a negative time remaining
+ return max(self.timeTotal - self.timeElapsed, 0)
+
+ @pyqtProperty(float, notify = timeElapsedChanged)
+ def progress(self) -> float:
+ 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)
- def state(self):
+ def state(self) -> str:
return self._state
+ @pyqtProperty(bool, notify=stateChanged)
+ def isActive(self) -> bool:
+ inactiveStates = [
+ "pausing",
+ "paused",
+ "resuming",
+ "wait_cleanup"
+ ]
+ if self.state in inactiveStates and self.timeRemaining > 0:
+ return False
+ return True
+
def updateTimeTotal(self, new_time_total):
if self._time_total != new_time_total:
self._time_total = new_time_total
diff --git a/cura/PrinterOutputDevice.py b/cura/PrinterOutputDevice.py
index 969aa3c460..dbdf8c986c 100644
--- a/cura/PrinterOutputDevice.py
+++ b/cura/PrinterOutputDevice.py
@@ -1,5 +1,7 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+from enum import IntEnum
+from typing import Callable, List, Optional, Union
from UM.Decorators import deprecated
from UM.i18n import i18nCatalog
@@ -12,9 +14,6 @@ from UM.Signal import signalemitter
from UM.Qt.QtApplication import QtApplication
from UM.FlameProfiler import pyqtSlot
-from enum import IntEnum # For the connection state tracking.
-from typing import Callable, List, Optional, Union
-
MYPY = False
if MYPY:
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
@@ -28,11 +27,18 @@ i18n_catalog = i18nCatalog("cura")
## The current processing state of the backend.
class ConnectionState(IntEnum):
- closed = 0
- connecting = 1
- connected = 2
- busy = 3
- error = 4
+ Closed = 0
+ Connecting = 1
+ Connected = 2
+ Busy = 3
+ Error = 4
+
+
+class ConnectionType(IntEnum):
+ NotConnected = 0
+ UsbConnection = 1
+ NetworkConnection = 2
+ CloudConnection = 3
## Printer output device adds extra interface options on top of output device.
@@ -46,6 +52,7 @@ class ConnectionState(IntEnum):
# For all other uses it should be used in the same way as a "regular" OutputDevice.
@signalemitter
class PrinterOutputDevice(QObject, OutputDevice):
+
printersChanged = pyqtSignal()
connectionStateChanged = pyqtSignal(str)
acceptsCommandsChanged = pyqtSignal()
@@ -62,33 +69,34 @@ class PrinterOutputDevice(QObject, OutputDevice):
# Signal to indicate that the configuration of one of the printers has changed.
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
self._printers = [] # type: List[PrinterOutputModel]
self._unique_configurations = [] # type: List[ConfigurationModel]
- self._monitor_view_qml_path = "" #type: str
- self._monitor_component = None #type: Optional[QObject]
- self._monitor_item = None #type: Optional[QObject]
+ self._monitor_view_qml_path = "" # type: str
+ self._monitor_component = None # type: Optional[QObject]
+ self._monitor_item = None # type: Optional[QObject]
- self._control_view_qml_path = "" #type: str
- self._control_component = None #type: Optional[QObject]
- self._control_item = None #type: Optional[QObject]
+ self._control_view_qml_path = "" # type: str
+ self._control_component = None # type: Optional[QObject]
+ self._control_item = None # type: Optional[QObject]
- self._accepts_commands = False #type: bool
+ self._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.setSingleShot(False)
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_name = None #type: Optional[str]
- self._address = "" #type: str
- self._connection_text = "" #type: str
+ self._firmware_updater = None # type: Optional[FirmwareUpdater]
+ self._firmware_name = None # type: Optional[str]
+ self._address = "" # type: str
+ self._connection_text = "" # type: str
self.printersChanged.connect(self._onPrintersChanged)
QtApplication.getInstance().getOutputDeviceManager().outputDevicesChanged.connect(self._updateUniqueConfigurations)
@@ -110,15 +118,19 @@ class PrinterOutputDevice(QObject, OutputDevice):
callback(QMessageBox.Yes)
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:
self._connection_state = connection_state
self.connectionStateChanged.emit(self._id)
- @pyqtProperty(str, notify = connectionStateChanged)
- def connectionState(self) -> ConnectionState:
+ @pyqtProperty(int, constant = True)
+ def connectionType(self) -> "ConnectionType":
+ return self._connection_type
+
+ @pyqtProperty(int, notify = connectionStateChanged)
+ def connectionState(self) -> "ConnectionState":
return self._connection_state
def _update(self) -> None:
@@ -131,7 +143,8 @@ class PrinterOutputDevice(QObject, OutputDevice):
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")
@pyqtProperty(QObject, notify = printersChanged)
@@ -174,13 +187,13 @@ class PrinterOutputDevice(QObject, OutputDevice):
## Attempt to establish connection
def connect(self) -> None:
- self.setConnectionState(ConnectionState.connecting)
+ self.setConnectionState(ConnectionState.Connecting)
self._update_timer.start()
## Attempt to close the connection
def close(self) -> None:
self._update_timer.stop()
- self.setConnectionState(ConnectionState.closed)
+ self.setConnectionState(ConnectionState.Closed)
## Ensure that close gets called when object is destroyed
def __del__(self) -> None:
@@ -207,10 +220,17 @@ class PrinterOutputDevice(QObject, OutputDevice):
return self._unique_configurations
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.sort(key = lambda k: k.printerType)
+ self._unique_configurations = sorted(
+ {printer.printerConfiguration for printer in self._printers if printer.printerConfiguration is not None},
+ key=lambda config: config.printerType,
+ )
self.uniqueConfigurationsChanged.emit()
+ # Returns the unique configurations of the printers within this output device
+ @pyqtProperty("QStringList", notify = uniqueConfigurationsChanged)
+ def uniquePrinterTypes(self) -> List[str]:
+ return list(sorted(set([configuration.printerType for configuration in self._unique_configurations])))
+
def _onPrintersChanged(self) -> None:
for printer in self._printers:
printer.configurationChanged.connect(self._updateUniqueConfigurations)
@@ -238,4 +258,4 @@ class PrinterOutputDevice(QObject, OutputDevice):
if not self._firmware_updater:
return
- self._firmware_updater.updateFirmware(firmware_file)
\ No newline at end of file
+ self._firmware_updater.updateFirmware(firmware_file)
diff --git a/cura/Scene/ConvexHullDecorator.py b/cura/Scene/ConvexHullDecorator.py
index 39124c5ba3..9bdb6a2c0e 100644
--- a/cura/Scene/ConvexHullDecorator.py
+++ b/cura/Scene/ConvexHullDecorator.py
@@ -187,7 +187,10 @@ class ConvexHullDecorator(SceneNodeDecorator):
for child in self._node.getChildren():
child_hull = child.callDecoration("_compute2DConvexHull")
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:
return None
@@ -239,7 +242,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
# See http://stackoverflow.com/questions/16970982/find-unique-rows-in-numpy-array
vertex_byte_view = numpy.ascontiguousarray(vertex_data).view(
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.
hull = Polygon(vertex_data)
@@ -272,7 +275,7 @@ class ConvexHullDecorator(SceneNodeDecorator):
head_and_fans = self._getHeadAndFans().intersectionConvexHulls(mirrored)
# Min head hull is used for the push free
- convex_hull = self._compute2DConvexHeadFull()
+ convex_hull = self._compute2DConvexHull()
if convex_hull:
return convex_hull.getMinkowskiHull(head_and_fans)
return None
diff --git a/cura/Scene/ConvexHullNode.py b/cura/Scene/ConvexHullNode.py
index 4c79c7d5dc..90bf536308 100644
--- a/cura/Scene/ConvexHullNode.py
+++ b/cura/Scene/ConvexHullNode.py
@@ -1,7 +1,10 @@
# Copyright (c) 2015 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Optional
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.Resources import Resources
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
# then displayed as a transparent shadow. If the adhesion type is set to raft, the area is extruded
# 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)
self.setCalculateBoundingBox(False)
@@ -25,7 +28,11 @@ class ConvexHullNode(SceneNode):
# Color of the drawn convex hull
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:
self._color = Color(0, 0, 0)
@@ -47,7 +54,7 @@ class ConvexHullNode(SceneNode):
if hull_mesh_builder.addConvexPolygonExtrusion(
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()
self.setMeshData(hull_mesh)
@@ -75,7 +82,7 @@ class ConvexHullNode(SceneNode):
return True
- def _onNodeDecoratorsChanged(self, node):
+ def _onNodeDecoratorsChanged(self, node: SceneNode) -> None:
convex_hull_head = self._node.callDecoration("getConvexHullHead")
if convex_hull_head:
convex_hull_head_builder = MeshBuilder()
diff --git a/cura/Settings/CuraFormulaFunctions.py b/cura/Settings/CuraFormulaFunctions.py
index 1db01857f8..9ef80bd3d4 100644
--- a/cura/Settings/CuraFormulaFunctions.py
+++ b/cura/Settings/CuraFormulaFunctions.py
@@ -5,6 +5,7 @@ from typing import Any, List, Optional, TYPE_CHECKING
from UM.Settings.PropertyEvaluationContext import PropertyEvaluationContext
from UM.Settings.SettingFunction import SettingFunction
+from UM.Logger import Logger
if TYPE_CHECKING:
from cura.CuraApplication import CuraApplication
@@ -38,7 +39,11 @@ class CuraFormulaFunctions:
extruder_position = int(machine_manager.defaultExtruderPosition)
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)
if isinstance(value, SettingFunction):
diff --git a/cura/Settings/ExtruderManager.py b/cura/Settings/ExtruderManager.py
index 9089ba96e9..a459d65ba3 100755
--- a/cura/Settings/ExtruderManager.py
+++ b/cura/Settings/ExtruderManager.py
@@ -63,7 +63,7 @@ class ExtruderManager(QObject):
if not self._application.getGlobalContainerStack():
return None # No active machine, so no active extruder.
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.
return None
@@ -83,8 +83,9 @@ class ExtruderManager(QObject):
# \param index The index of the new active extruder.
@pyqtSlot(int)
def setActiveExtruderIndex(self, index: int) -> None:
- self._active_extruder_index = index
- self.activeExtruderChanged.emit()
+ if self._active_extruder_index != index:
+ self._active_extruder_index = index
+ self.activeExtruderChanged.emit()
@pyqtProperty(int, notify = activeExtruderChanged)
def activeExtruderIndex(self) -> int:
@@ -144,7 +145,7 @@ class ExtruderManager(QObject):
@pyqtSlot(result = QObject)
def getActiveExtruderStack(self) -> Optional["ExtruderStack"]:
- return self.getExtruderStack(self._active_extruder_index)
+ return self.getExtruderStack(self.activeExtruderIndex)
## Get an extruder stack by index
def getExtruderStack(self, index) -> Optional["ExtruderStack"]:
@@ -300,12 +301,7 @@ class ExtruderManager(QObject):
global_stack = self._application.getGlobalContainerStack()
if not global_stack:
return []
-
- 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]
+ return global_stack.extruderList
def _globalContainerStackChanged(self) -> None:
# 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:
self.extrudersChanged.emit(global_stack_id)
self.setActiveExtruderIndex(0)
+ self.activeExtruderChanged.emit()
# 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.
diff --git a/cura/Settings/ExtruderStack.py b/cura/Settings/ExtruderStack.py
index d7faedb71c..edb0e7d41f 100644
--- a/cura/Settings/ExtruderStack.py
+++ b/cura/Settings/ExtruderStack.py
@@ -52,8 +52,8 @@ class ExtruderStack(CuraContainerStack):
return super().getNextStack()
def setEnabled(self, enabled: bool) -> None:
- if "enabled" not in self._metadata:
- self.setMetaDataEntry("enabled", "True")
+ if self.getMetaDataEntry("enabled", True) == enabled: # No change.
+ return # Don't emit a signal then.
self.setMetaDataEntry("enabled", str(enabled))
self.enabledChanged.emit()
diff --git a/cura/Settings/ExtrudersModel.py b/cura/Settings/ExtrudersModel.py
index 52fc502bfc..076cebf60d 100644
--- a/cura/Settings/ExtrudersModel.py
+++ b/cura/Settings/ExtrudersModel.py
@@ -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.
-from PyQt5.QtCore import Qt, pyqtSignal, pyqtSlot, pyqtProperty, QTimer
+from PyQt5.QtCore import Qt, pyqtSignal, pyqtProperty, QTimer
from typing import Iterable
from UM.i18n import i18nCatalog
@@ -24,8 +24,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
## Human-readable name of the extruder.
NameRole = Qt.UserRole + 2
- ## Is the extruder enabled?
- EnabledRole = Qt.UserRole + 9
## Colour of the material loaded in the extruder.
ColorRole = Qt.UserRole + 3
@@ -47,6 +45,12 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
VariantRole = Qt.UserRole + 7
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
# colour.
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.VariantRole, "variant")
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.setInterval(100)
self._update_extruder_timer.setSingleShot(True)
self._update_extruder_timer.timeout.connect(self.__updateExtruders)
- self._simple_names = False
-
self._active_machine_extruders = [] # type: Iterable[ExtruderStack]
self._add_optional_extruder = False
@@ -96,21 +99,6 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
def addOptionalExtruder(self):
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
# is swapped out or added in the current machine.
#
@@ -160,7 +148,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
def __updateExtruders(self):
extruders_changed = False
- if self.rowCount() != 0:
+ if self.count != 0:
extruders_changed = True
items = []
@@ -172,7 +160,7 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
machine_extruder_count = global_container_stack.getProperty("machine_extruder_count", "value")
for extruder in Application.getInstance().getExtruderManager().getActiveExtruderStacks():
- position = extruder.getMetaDataEntry("position", default = "0") # Get the position
+ position = extruder.getMetaDataEntry("position", default = "0")
try:
position = int(position)
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]
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
item = {
"id": extruder.getId(),
@@ -195,6 +184,8 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
"material": extruder.material.getName() if extruder.material else "",
"variant": extruder.variant.getName() if extruder.variant else "", # e.g. print core
"stack": extruder,
+ "material_brand": material_brand,
+ "color_name": color_name
}
items.append(item)
@@ -213,9 +204,14 @@ class ExtrudersModel(UM.Qt.ListModel.ListModel):
"enabled": True,
"color": "#ffffff",
"index": -1,
- "definition": ""
+ "definition": "",
+ "material": "",
+ "variant": "",
+ "stack": None,
+ "material_brand": "",
+ "color_name": "",
}
items.append(item)
-
- self.setItems(items)
- self.modelChanged.emit()
+ if self._items != items:
+ self.setItems(items)
+ self.modelChanged.emit()
diff --git a/cura/Settings/GlobalStack.py b/cura/Settings/GlobalStack.py
index da1ec61254..8dba0f5204 100755
--- a/cura/Settings/GlobalStack.py
+++ b/cura/Settings/GlobalStack.py
@@ -3,8 +3,8 @@
from collections import defaultdict
import threading
-from typing import Any, Dict, Optional, Set, TYPE_CHECKING
-from PyQt5.QtCore import pyqtProperty, pyqtSlot
+from typing import Any, Dict, Optional, Set, TYPE_CHECKING, List
+from PyQt5.QtCore import pyqtProperty, pyqtSlot, pyqtSignal
from UM.Decorators import override
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.
self._resolving_settings = defaultdict(set) #type: Dict[str, Set[str]] # keys are thread names
+ extrudersChanged = pyqtSignal()
+
## Get the list of extruders of this stack.
#
# \return The extruders registered with this stack.
- @pyqtProperty("QVariantMap")
+ @pyqtProperty("QVariantMap", notify = extrudersChanged)
def extruders(self) -> Dict[str, "ExtruderStack"]:
return self._extruders
+ @pyqtProperty("QVariantList", notify = extrudersChanged)
+ def extruderList(self) -> List["ExtruderStack"]:
+ result_tuple_list = sorted(list(self.extruders.items()), key=lambda x: int(x[0]))
+ result_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
def getLoadingPriority(cls) -> int:
return 2
@@ -87,6 +97,7 @@ class GlobalStack(CuraContainerStack):
return
self._extruders[position] = extruder
+ self.extrudersChanged.emit()
Logger.log("i", "Extruder[%s] added to [%s] at position [%s]", extruder.id, self.id, position)
## Overridden from ContainerStack
diff --git a/cura/Settings/MachineManager.py b/cura/Settings/MachineManager.py
index f321ce94a6..5763d2bcab 100755
--- a/cura/Settings/MachineManager.py
+++ b/cura/Settings/MachineManager.py
@@ -3,6 +3,8 @@
import collections
import time
+import re
+import unicodedata
from typing import Any, Callable, List, Dict, TYPE_CHECKING, Optional, cast
from UM.ConfigurationErrorMessage import ConfigurationErrorMessage
@@ -21,7 +23,7 @@ from UM.Settings.SettingFunction import SettingFunction
from UM.Signal import postponeSignals, CompressTechnique
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.ExtruderConfigurationModel import ExtruderConfigurationModel
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.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.setSingleShot(True)
self._instance_container_timer.timeout.connect(self.__emitChangedSignals)
@@ -74,7 +74,7 @@ class MachineManager(QObject):
self._application.globalContainerStackChanged.connect(self._onGlobalContainerChanged)
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.activeVariantChanged)
self.globalContainerChanged.connect(self.activeQualityChanged)
@@ -86,12 +86,14 @@ class MachineManager(QObject):
self._onGlobalContainerChanged()
- ExtruderManager.getInstance().activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
+ extruder_manager = self._application.getExtruderManager()
+
+ extruder_manager.activeExtruderChanged.connect(self._onActiveExtruderStackChanged)
self._onActiveExtruderStackChanged()
- ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeMaterialChanged)
- ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeVariantChanged)
- ExtruderManager.getInstance().activeExtruderChanged.connect(self.activeQualityChanged)
+ extruder_manager.activeExtruderChanged.connect(self.activeMaterialChanged)
+ extruder_manager.activeExtruderChanged.connect(self.activeVariantChanged)
+ extruder_manager.activeExtruderChanged.connect(self.activeQualityChanged)
self.globalContainerChanged.connect(self.activeStackChanged)
self.globalValueChanged.connect(self.activeStackValueChanged)
@@ -115,15 +117,15 @@ class MachineManager(QObject):
self._material_incompatible_message = Message(catalog.i18nc("@info:status",
"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:
containers[0].nameChanged.connect(self._onMaterialNameChanged)
- self._material_manager = self._application.getMaterialManager() #type: MaterialManager
- self._variant_manager = self._application.getVariantManager() #type: VariantManager
- self._quality_manager = self._application.getQualityManager() #type: QualityManager
+ self._material_manager = self._application.getMaterialManager() # type: MaterialManager
+ self._variant_manager = self._application.getVariantManager() # type: VariantManager
+ 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
# 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
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
rootMaterialChanged = pyqtSignal()
@@ -174,6 +176,7 @@ class MachineManager(QObject):
self._printer_output_devices.append(printer_output_device)
self.outputDevicesChanged.emit()
+ self.printerConnectedStatusChanged.emit()
@pyqtProperty(QObject, notify = currentConfigurationChanged)
def currentConfiguration(self) -> ConfigurationModel:
@@ -201,7 +204,7 @@ class MachineManager(QObject):
extruder_configuration.hotendID = extruder.variant.getName() if extruder.variant != empty_variant_container else None
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.
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()
@@ -247,7 +250,7 @@ class MachineManager(QObject):
self.updateNumberExtrudersEnabled()
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:
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":
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
if global_material != 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.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()
def _onActiveExtruderStackChanged(self) -> None:
@@ -295,6 +293,7 @@ class MachineManager(QObject):
self.activeMaterialChanged.emit()
self.rootMaterialChanged.emit()
+ self.numberExtrudersEnabledChanged.emit()
def _onContainersChanged(self, container: ContainerInterface) -> None:
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
machine_extruder_count = self._global_container_stack.getProperty("machine_extruder_count", "value")
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:
md = stack.getMetaData()
if "position" in md and int(md["position"]) >= machine_extruder_count:
@@ -438,12 +437,12 @@ class MachineManager(QObject):
if not self._global_container_stack:
return False
- if self._global_container_stack.getTop().findInstances():
+ if self._global_container_stack.getTop().getNumInstances() != 0:
return True
stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
for stack in stacks:
- if stack.getTop().findInstances():
+ if stack.getTop().getNumInstances() != 0:
return True
return False
@@ -453,10 +452,10 @@ class MachineManager(QObject):
if not self._global_container_stack:
return 0
num_user_settings = 0
- num_user_settings += len(self._global_container_stack.getTop().findInstances())
- stacks = ExtruderManager.getInstance().getActiveExtruderStacks()
+ num_user_settings += self._global_container_stack.getTop().getNumInstances()
+ stacks = self._global_container_stack.extruderList
for stack in stacks:
- num_user_settings += len(stack.getTop().findInstances())
+ num_user_settings += stack.getTop().getNumInstances()
return num_user_settings
## Delete a user setting from the global stack and all extruder stacks.
@@ -516,10 +515,30 @@ class MachineManager(QObject):
return ""
@pyqtProperty(bool, notify = printerConnectedStatusChanged)
- def printerConnected(self):
+ def printerConnected(self) -> bool:
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:
if self._global_container_stack:
return self._global_container_stack.getMetaDataEntry("um_network_key", "")
@@ -616,6 +635,14 @@ class MachineManager(QObject):
is_supported = self._current_quality_group.is_available
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.
#
# 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")
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:
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
@@ -662,7 +689,7 @@ class MachineManager(QObject):
for key in self._active_container_stack.userChanges.getAllKeys():
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)
@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.
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:
machine_stacks = CuraContainerRegistry.getInstance().findContainerStacksMetadata(type = "machine")
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"])
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)
containers = CuraContainerRegistry.getInstance().findInstanceContainersMetadata(type = "user", machine = machine_id)
for container in containers:
@@ -864,7 +891,7 @@ class MachineManager(QObject):
caution_message = Message(catalog.i18nc(
"@info:generic",
"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"))
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.
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():
setting_key = setting_instance.definition.key
settable_per_extruder = self._global_container_stack.getProperty(setting_key, "settable_per_extruder")
if settable_per_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_stack.userChanges.setProperty(setting_key, "value", global_user_container.getProperty(setting_key, "value"))
+ extruder_position = max(0, limit_to_extruder)
+ 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)
# Signal that the global stack has changed
@@ -932,10 +956,9 @@ class MachineManager(QObject):
@pyqtSlot(int, result = QObject)
def getExtruder(self, position: int) -> Optional[ExtruderStack]:
- extruder = None
if self._global_container_stack:
- extruder = self._global_container_stack.extruders.get(str(position))
- return extruder
+ return self._global_container_stack.extruders.get(str(position))
+ return None
def updateDefaultExtruder(self) -> None:
if self._global_container_stack is None:
@@ -1001,12 +1024,12 @@ class MachineManager(QObject):
if not enabled and position == ExtruderManager.getInstance().activeExtruderIndex:
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.extruderChanged.emit()
- # update material compatibility color
+ # Update material compatibility color
self.activeQualityGroupChanged.emit()
- # update items in SettingExtruder
+ # Update items in SettingExtruder
ExtruderManager.getInstance().extrudersChanged.emit(self._global_container_stack.getId())
# Make sure the front end reflects changes
self.forceUpdateAllSettings()
@@ -1080,7 +1103,6 @@ class MachineManager(QObject):
return result
- #
# Sets all quality and quality_changes containers to empty_quality and empty_quality_changes containers
# for all stacks in the currently active machine.
#
@@ -1139,7 +1161,7 @@ class MachineManager(QObject):
def _setQualityChangesGroup(self, quality_changes_group: "QualityChangesGroup") -> 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
# A custom quality can be created based on "not supported".
# In that case, do not set quality containers to empty.
@@ -1209,7 +1231,7 @@ class MachineManager(QObject):
self.rootMaterialChanged.emit()
def activeMaterialsCompatible(self) -> bool:
- # check material - variant compatibility
+ # Check material - variant compatibility
if self._global_container_stack is not None:
if Util.parseBool(self._global_container_stack.getMetaDataEntry("has_materials", False)):
for position, extruder in self._global_container_stack.extruders.items():
@@ -1310,17 +1332,18 @@ class MachineManager(QObject):
# Get the definition id corresponding to this machine name
machine_definition_id = CuraContainerRegistry.getInstance().findDefinitionContainers(name = machine_name)[0].getId()
# Try to find a machine with the same network key
- new_machine = self.getMachine(machine_definition_id, metadata_filter = {"um_network_key": self.activeMachineNetworkKey})
+ 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 not new_machine:
new_machine = CuraStackBuilder.createMachine(machine_definition_id + "_sync", machine_definition_id)
if not new_machine:
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("hidden", False)
+ new_machine.setMetaDataEntry("connection_type", self._global_container_stack.getMetaDataEntry("connection_type"))
else:
- Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey)
+ Logger.log("i", "Found a %s with the key %s. Let's use it!", machine_name, self.activeMachineNetworkKey())
new_machine.setMetaDataEntry("hidden", False)
# Set the current printer instance to hidden (the metadata entry must exist)
@@ -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
# then all the container stacks are updated, both the current and the hidden ones.
def checkCorrectGroupName(self, device_id: str, group_name: str) -> None:
- if self._global_container_stack and device_id == self.activeMachineNetworkKey:
+ 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
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)
for container in containers:
container.setMetaDataEntry("connect_group_name", group_name)
@@ -1419,7 +1442,7 @@ class MachineManager(QObject):
material_diameter, root_material_id)
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.
@pyqtSlot(str, "QVariant")
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"]:
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)
def activeQualityOrQualityChangesName(self) -> str:
name = empty_quality_container.getName()
@@ -1531,9 +1558,32 @@ class MachineManager(QObject):
name = self._current_quality_group.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:
if self._global_container_stack is None:
return
with postponeSignals(*self._getContainerChangedSignals(), compress = CompressTechnique.CompressPerParameterValue):
self.updateMaterialWithVariant(None)
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
diff --git a/cura/Settings/SimpleModeSettingsManager.py b/cura/Settings/SimpleModeSettingsManager.py
index fce43243bd..b1896a9205 100644
--- a/cura/Settings/SimpleModeSettingsManager.py
+++ b/cura/Settings/SimpleModeSettingsManager.py
@@ -1,7 +1,8 @@
# Copyright (c) 2017 Ultimaker B.V.
# 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
@@ -16,15 +17,11 @@ class SimpleModeSettingsManager(QObject):
self._is_profile_user_created = False # True when profile was custom created by user
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
self._updateIsProfileCustomized()
- self._updateIsProfileUserCreated()
isProfileCustomizedChanged = pyqtSignal()
- isProfileUserCreatedChanged = pyqtSignal()
@pyqtProperty(bool, notify = isProfileCustomizedChanged)
def isProfileCustomized(self):
@@ -57,33 +54,6 @@ class SimpleModeSettingsManager(QObject):
self._is_profile_customized = has_customized_user_settings
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
# changed, we consider it as a user customized profile in the Simple ("Recommended") Mode.
__ignored_custom_setting_keys = ["support_enable",
diff --git a/cura/Stages/CuraStage.py b/cura/Stages/CuraStage.py
index b2f6d61799..844b0d0768 100644
--- a/cura/Stages/CuraStage.py
+++ b/cura/Stages/CuraStage.py
@@ -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.
+
from PyQt5.QtCore import pyqtProperty, QUrl
from UM.Stage import Stage
+# Since Cura has a few pre-defined "space claims" for the locations of certain components, we've provided some structure
+# to indicate this.
+# * The StageMenuComponent is the horizontal area below the stage bar. This should be used to show stage specific
+# buttons and elements. This component will be drawn over the bar & main component.
+# * The MainComponent is the component that will be drawn starting from the bottom of the stageBar and fills the rest
+# of the screen.
class CuraStage(Stage):
-
- def __init__(self, parent = None):
+ def __init__(self, parent = None) -> None:
super().__init__(parent)
@pyqtProperty(str, constant = True)
- def stageId(self):
+ def stageId(self) -> str:
return self.getPluginId()
@pyqtProperty(QUrl, constant = True)
- def mainComponent(self):
+ def mainComponent(self) -> QUrl:
return self.getDisplayComponent("main")
@pyqtProperty(QUrl, constant = True)
- def sidebarComponent(self):
- return self.getDisplayComponent("sidebar")
+ def stageMenuComponent(self) -> QUrl:
+ return self.getDisplayComponent("menu")
\ No newline at end of file
diff --git a/cura/UltimakerCloudAuthentication.py b/cura/UltimakerCloudAuthentication.py
new file mode 100644
index 0000000000..69bb577354
--- /dev/null
+++ b/cura/UltimakerCloudAuthentication.py
@@ -0,0 +1,28 @@
+# 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
+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
diff --git a/cura_app.py b/cura_app.py
index 164e32e738..8df12d771a 100755
--- a/cura_app.py
+++ b/cura_app.py
@@ -17,12 +17,6 @@ parser.add_argument("--debug",
default = False,
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])
if not known_args["debug"]:
diff --git a/plugins/3MFReader/ThreeMFWorkspaceReader.py b/plugins/3MFReader/ThreeMFWorkspaceReader.py
index 9ee2ef0dd4..55296979b5 100755
--- a/plugins/3MFReader/ThreeMFWorkspaceReader.py
+++ b/plugins/3MFReader/ThreeMFWorkspaceReader.py
@@ -794,7 +794,8 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
# Clear all existing containers
quality_changes_info.global_info.container.clear()
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
global_info = quality_changes_info.global_info
diff --git a/plugins/3MFReader/plugin.json b/plugins/3MFReader/plugin.json
index 5e41975752..5af21a7033 100644
--- a/plugins/3MFReader/plugin.json
+++ b/plugins/3MFReader/plugin.json
@@ -1,8 +1,8 @@
{
"name": "3MF Reader",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides support for reading 3MF files.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/3MFWriter/plugin.json b/plugins/3MFWriter/plugin.json
index 9ec4fb0c20..3820ebd2e7 100644
--- a/plugins/3MFWriter/plugin.json
+++ b/plugins/3MFWriter/plugin.json
@@ -1,8 +1,8 @@
{
"name": "3MF Writer",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides support for writing 3MF files.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/ChangeLogPlugin/ChangeLog.txt b/plugins/ChangeLogPlugin/ChangeLog.txt
index 7e5cf2dd3b..651abb0cac 100755
--- a/plugins/ChangeLogPlugin/ChangeLog.txt
+++ b/plugins/ChangeLogPlugin/ChangeLog.txt
@@ -943,7 +943,7 @@ This release adds support for printers with elliptic buildplates. This feature h
*AppImage for Linux
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.
When searching in the setting visibility preferences, the category for each setting is always displayed.
3MF files are now saved and loaded correctly.
diff --git a/plugins/ChangeLogPlugin/plugin.json b/plugins/ChangeLogPlugin/plugin.json
index e09a08564a..92041d1543 100644
--- a/plugins/ChangeLogPlugin/plugin.json
+++ b/plugins/ChangeLogPlugin/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Changelog",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Shows changes since latest checked version.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/CuraDrive/__init__.py b/plugins/CuraDrive/__init__.py
new file mode 100644
index 0000000000..eeb6b78689
--- /dev/null
+++ b/plugins/CuraDrive/__init__.py
@@ -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()}
diff --git a/plugins/CuraDrive/plugin.json b/plugins/CuraDrive/plugin.json
new file mode 100644
index 0000000000..d1cab39ca5
--- /dev/null
+++ b/plugins/CuraDrive/plugin.json
@@ -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"
+}
diff --git a/plugins/CuraDrive/src/DriveApiService.py b/plugins/CuraDrive/src/DriveApiService.py
new file mode 100644
index 0000000000..7c1f8faa83
--- /dev/null
+++ b/plugins/CuraDrive/src/DriveApiService.py
@@ -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"]
diff --git a/plugins/CuraDrive/src/DrivePluginExtension.py b/plugins/CuraDrive/src/DrivePluginExtension.py
new file mode 100644
index 0000000000..060f1496f1
--- /dev/null
+++ b/plugins/CuraDrive/src/DrivePluginExtension.py
@@ -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()
diff --git a/plugins/CuraDrive/src/Settings.py b/plugins/CuraDrive/src/Settings.py
new file mode 100644
index 0000000000..abe64e0acd
--- /dev/null
+++ b/plugins/CuraDrive/src/Settings.py
@@ -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"
diff --git a/plugins/CuraDrive/src/UploadBackupJob.py b/plugins/CuraDrive/src/UploadBackupJob.py
new file mode 100644
index 0000000000..2e76ed9b4b
--- /dev/null
+++ b/plugins/CuraDrive/src/UploadBackupJob.py
@@ -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)
diff --git a/plugins/CuraDrive/src/__init__.py b/plugins/CuraDrive/src/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/plugins/CuraDrive/src/qml/components/BackupList.qml b/plugins/CuraDrive/src/qml/components/BackupList.qml
new file mode 100644
index 0000000000..a4a460a885
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupList.qml
@@ -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
+ }
+ }
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/components/BackupListFooter.qml b/plugins/CuraDrive/src/qml/components/BackupListFooter.qml
new file mode 100644
index 0000000000..8decdc5c27
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupListFooter.qml
@@ -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.")
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/components/BackupListItem.qml b/plugins/CuraDrive/src/qml/components/BackupListItem.qml
new file mode 100644
index 0000000000..5cdb500b4e
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupListItem.qml
@@ -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)
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml b/plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml
new file mode 100644
index 0000000000..4da15c6f16
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupListItemDetails.qml
@@ -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
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml b/plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml
new file mode 100644
index 0000000000..9e4612fcf8
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/components/BackupListItemDetailsRow.qml
@@ -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
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/images/icon.png b/plugins/CuraDrive/src/qml/images/icon.png
new file mode 100644
index 0000000000..3f75491786
Binary files /dev/null and b/plugins/CuraDrive/src/qml/images/icon.png differ
diff --git a/plugins/CuraDrive/src/qml/images/loading.gif b/plugins/CuraDrive/src/qml/images/loading.gif
new file mode 100644
index 0000000000..791dcaa0c9
Binary files /dev/null and b/plugins/CuraDrive/src/qml/images/loading.gif differ
diff --git a/plugins/CuraDrive/src/qml/main.qml b/plugins/CuraDrive/src/qml/main.qml
new file mode 100644
index 0000000000..48bf3b6ea4
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/main.qml
@@ -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
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/pages/BackupsPage.qml b/plugins/CuraDrive/src/qml/pages/BackupsPage.qml
new file mode 100644
index 0000000000..c337294744
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/pages/BackupsPage.qml
@@ -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
+ }
+ }
+}
diff --git a/plugins/CuraDrive/src/qml/pages/WelcomePage.qml b/plugins/CuraDrive/src/qml/pages/WelcomePage.qml
new file mode 100644
index 0000000000..0b207bc170
--- /dev/null
+++ b/plugins/CuraDrive/src/qml/pages/WelcomePage.qml
@@ -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
+ }
+}
+
diff --git a/plugins/CuraEngineBackend/Cura.proto b/plugins/CuraEngineBackend/Cura.proto
index 292330576b..2eabe62366 100644
--- a/plugins/CuraEngineBackend/Cura.proto
+++ b/plugins/CuraEngineBackend/Cura.proto
@@ -29,7 +29,7 @@ message Object
bytes normals = 3; //An array of 3 floats.
bytes indices = 4; //An array of ints.
repeated Setting settings = 5; // Setting override per object, overruling the global settings.
- string name = 6;
+ string name = 6; //Mesh name
}
message Progress
@@ -58,6 +58,7 @@ message Polygon {
MoveCombingType = 8;
MoveRetractionType = 9;
SupportInterfaceType = 10;
+ PrimeTowerType = 11;
}
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)
@@ -108,8 +109,9 @@ message PrintTimeMaterialEstimates { // The print time for each feature and mate
float time_travel = 9;
float time_retract = 10;
float time_support_interface = 11;
+ float time_prime_tower = 12;
- repeated MaterialEstimates materialEstimates = 12; // materialEstimates data
+ repeated MaterialEstimates materialEstimates = 13; // materialEstimates data
}
message MaterialEstimates {
diff --git a/plugins/CuraEngineBackend/CuraEngineBackend.py b/plugins/CuraEngineBackend/CuraEngineBackend.py
index 58bc74f3f1..ef0898bb04 100755
--- a/plugins/CuraEngineBackend/CuraEngineBackend.py
+++ b/plugins/CuraEngineBackend/CuraEngineBackend.py
@@ -86,8 +86,8 @@ class CuraEngineBackend(QObject, Backend):
self._layer_view_active = False #type: bool
self._onActiveViewChanged()
- 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_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._scene = self._application.getController().getScene() #type: Scene
self._scene.sceneChanged.connect(self._onSceneChanged)
@@ -203,7 +203,7 @@ class CuraEngineBackend(QObject, Backend):
@pyqtSlot()
def stopSlicing(self) -> None:
- self.backendStateChange.emit(BackendState.NotStarted)
+ self.setState(BackendState.NotStarted)
if self._slicing: # We were already slicing. Stop the old job.
self._terminate()
self._createSocket()
@@ -229,6 +229,7 @@ class CuraEngineBackend(QObject, Backend):
if not self._build_plates_to_be_sliced:
self.processingProgress.emit(1.0)
Logger.log("w", "Slice unnecessary, nothing has changed that needs reslicing.")
+ self.setState(BackendState.Done)
return
if self._process_layers_job:
@@ -245,7 +246,7 @@ class CuraEngineBackend(QObject, Backend):
num_objects = self._numObjectsPerBuildPlate()
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:
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:
self.slice()
return
-
+ self._stored_optimized_layer_data[build_plate_to_be_sliced] = []
if self._application.getPrintInformation() and build_plate_to_be_sliced == active_build_plate:
self._application.getPrintInformation().setToZeroPrintInformation(build_plate_to_be_sliced)
@@ -322,7 +323,7 @@ class CuraEngineBackend(QObject, Backend):
self._start_slice_job = None
if job.isCancelled() or job.getError() or job.getResult() == StartJobResult.Error:
- self.backendStateChange.emit(BackendState.Error)
+ self.setState(BackendState.Error)
self.backendError.emit(job)
return
@@ -331,10 +332,10 @@ class CuraEngineBackend(QObject, Backend):
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"))
self._error_message.show()
- self.backendStateChange.emit(BackendState.Error)
+ self.setState(BackendState.Error)
self.backendError.emit(job)
else:
- self.backendStateChange.emit(BackendState.NotStarted)
+ self.setState(BackendState.NotStarted)
return
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)),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
- self.backendStateChange.emit(BackendState.Error)
+ self.setState(BackendState.Error)
self.backendError.emit(job)
else:
- self.backendStateChange.emit(BackendState.NotStarted)
+ self.setState(BackendState.NotStarted)
return
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())),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
- self.backendStateChange.emit(BackendState.Error)
+ self.setState(BackendState.Error)
self.backendError.emit(job)
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."),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
- self.backendStateChange.emit(BackendState.Error)
+ self.setState(BackendState.Error)
self.backendError.emit(job)
else:
- self.backendStateChange.emit(BackendState.NotStarted)
+ self.setState(BackendState.NotStarted)
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()),
title = catalog.i18nc("@info:title", "Unable to slice"))
self._error_message.show()
- self.backendStateChange.emit(BackendState.Error)
+ self.setState(BackendState.Error)
self.backendError.emit(job)
return
if job.getResult() == StartJobResult.NothingToSlice:
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"))
self._error_message.show()
- self.backendStateChange.emit(BackendState.Error)
+ self.setState(BackendState.Error)
self.backendError.emit(job)
else:
- self.backendStateChange.emit(BackendState.NotStarted)
+ self.setState(BackendState.NotStarted)
self._invokeSlice()
return
@@ -424,7 +425,7 @@ class CuraEngineBackend(QObject, Backend):
self._socket.sendMessage(job.getSliceMessage())
# 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:
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.
if node.callDecoration("isBlockSlicing"):
enable_timer = False
- self.backendStateChange.emit(BackendState.Disabled)
+ self.setState(BackendState.Disabled)
self._is_disabled = True
gcode_list = node.callDecoration("getGCodeList")
if gcode_list is not None:
@@ -451,7 +452,7 @@ class CuraEngineBackend(QObject, Backend):
if self._use_timer == enable_timer:
return self._use_timer
if enable_timer:
- self.backendStateChange.emit(BackendState.NotStarted)
+ self.setState(BackendState.NotStarted)
self.enableTimer()
return True
else:
@@ -518,7 +519,7 @@ class CuraEngineBackend(QObject, Backend):
self._build_plates_to_be_sliced.append(build_plate_number)
self.printDurationMessage.emit(source_build_plate_number, {}, [])
self.processingProgress.emit(0.0)
- self.backendStateChange.emit(BackendState.NotStarted)
+ self.setState(BackendState.NotStarted)
# if not self._use_timer:
# With manually having to slice, we want to clear the old invalid layer data.
self._clearLayerData(build_plate_changed)
@@ -567,7 +568,7 @@ class CuraEngineBackend(QObject, Backend):
self.stopSlicing()
self.markSliceAll()
self.processingProgress.emit(0.0)
- self.backendStateChange.emit(BackendState.NotStarted)
+ self.setState(BackendState.NotStarted)
if not self._use_timer:
# With manually having to slice, we want to clear the old invalid layer data.
self._clearLayerData()
@@ -613,7 +614,7 @@ class CuraEngineBackend(QObject, Backend):
# \param message The protobuf message containing the slicing progress.
def _onProgressMessage(self, message: Arcus.PythonMessage) -> None:
self.processingProgress.emit(message.amount)
- self.backendStateChange.emit(BackendState.Processing)
+ self.setState(BackendState.Processing)
def _invokeSlice(self) -> None:
if self._use_timer:
@@ -632,7 +633,7 @@ class CuraEngineBackend(QObject, Backend):
#
# \param message The protobuf message signalling that slicing is finished.
def _onSlicingFinishedMessage(self, message: Arcus.PythonMessage) -> None:
- self.backendStateChange.emit(BackendState.Done)
+ self.setState(BackendState.Done)
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.
@@ -832,7 +833,10 @@ class CuraEngineBackend(QObject, Backend):
self._onChanged()
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
Logger.log("d", "See if there is more to slice(2)...")
self._invokeSlice()
diff --git a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py
index 594bf3a43e..3cc23130ea 100644
--- a/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py
+++ b/plugins/CuraEngineBackend/ProcessSlicedLayersJob.py
@@ -137,6 +137,7 @@ class ProcessSlicedLayersJob(Job):
extruder = polygon.extruder
line_types = numpy.fromstring(polygon.line_type, dtype="u1") # Convert bytearray to numpy array
+
line_types = line_types.reshape((-1,1))
points = numpy.fromstring(polygon.points, dtype="f4") # Convert bytearray to numpy array
@@ -195,7 +196,7 @@ class ProcessSlicedLayersJob(Job):
if extruders:
material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32)
for extruder in extruders:
- position = int(extruder.getMetaDataEntry("position", default="0")) # Get the position
+ position = int(extruder.getMetaDataEntry("position", default = "0"))
try:
default_color = ExtrudersModel.defaultColors[position]
except IndexError:
diff --git a/plugins/CuraEngineBackend/StartSliceJob.py b/plugins/CuraEngineBackend/StartSliceJob.py
index 79b1e5249c..d3882a1209 100644
--- a/plugins/CuraEngineBackend/StartSliceJob.py
+++ b/plugins/CuraEngineBackend/StartSliceJob.py
@@ -66,11 +66,19 @@ class GcodeStartEndFormatter(Formatter):
return "{" + key + "}"
key = key_fragments[0]
- try:
- return kwargs[str(extruder_nr)][key]
- except KeyError:
+
+ default_value_str = "{" + key + "}"
+ 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)
- return "{" + key + "}"
+
+ return value
## Job class that builds up the message of scene data to send to CuraEngine.
@@ -315,7 +323,7 @@ class StartSliceJob(Job):
value = stack.getProperty(key, "value")
result[key] = value
Job.yieldThread()
-
+
result["print_bed_temperature"] = result["material_bed_temperature"] # Renamed settings.
result["print_temperature"] = result["material_print_temperature"]
result["time"] = time.strftime("%H:%M:%S") #Some extra settings.
diff --git a/plugins/CuraEngineBackend/plugin.json b/plugins/CuraEngineBackend/plugin.json
index 111698d8d1..28f0e294e7 100644
--- a/plugins/CuraEngineBackend/plugin.json
+++ b/plugins/CuraEngineBackend/plugin.json
@@ -2,7 +2,7 @@
"name": "CuraEngine Backend",
"author": "Ultimaker B.V.",
"description": "Provides the link to the CuraEngine slicing backend.",
- "api": 5,
- "version": "1.0.0",
+ "api": "6.0",
+ "version": "1.0.1",
"i18n-catalog": "cura"
}
diff --git a/plugins/CuraProfileReader/plugin.json b/plugins/CuraProfileReader/plugin.json
index 66a2a6a56b..169fb43360 100644
--- a/plugins/CuraProfileReader/plugin.json
+++ b/plugins/CuraProfileReader/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Cura Profile Reader",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides support for importing Cura profiles.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/CuraProfileWriter/plugin.json b/plugins/CuraProfileWriter/plugin.json
index 16c8c34152..9627c754d7 100644
--- a/plugins/CuraProfileWriter/plugin.json
+++ b/plugins/CuraProfileWriter/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Cura Profile Writer",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides support for exporting Cura profiles.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog":"cura"
}
diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py
index b37de81f34..906c20dd25 100644
--- a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py
+++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py
@@ -86,6 +86,11 @@ class FirmwareUpdateCheckerJob(Job):
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 ""
setting_key_str = getSettingsKeyForMachine(machine_id)
checked_version = Version(Application.getInstance().getPreferences().getValue(setting_key_str))
diff --git a/plugins/FirmwareUpdateChecker/plugin.json b/plugins/FirmwareUpdateChecker/plugin.json
index cbbd41e420..6c55d77fd8 100644
--- a/plugins/FirmwareUpdateChecker/plugin.json
+++ b/plugins/FirmwareUpdateChecker/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Firmware Update Checker",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Checks for firmware updates.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py b/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py
index 0a3e3a0ff0..59552775b6 100644
--- a/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py
+++ b/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.py
@@ -57,7 +57,7 @@ class FirmwareUpdaterMachineAction(MachineAction):
outputDeviceCanUpdateFirmwareChanged = pyqtSignal()
@pyqtProperty(QObject, notify = outputDeviceCanUpdateFirmwareChanged)
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()
return self._active_firmware_updater
diff --git a/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.qml b/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.qml
index 9a56dbb20a..b5b6c15f50 100644
--- a/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.qml
+++ b/plugins/FirmwareUpdater/FirmwareUpdaterMachineAction.qml
@@ -22,7 +22,7 @@ Cura.MachineAction
{
id: firmwareUpdaterMachineAction
anchors.fill: parent;
- UM.I18nCatalog { id: catalog; name:"cura"}
+ UM.I18nCatalog { id: catalog; name: "cura"}
spacing: UM.Theme.getSize("default_margin").height
Label
diff --git a/plugins/FirmwareUpdater/plugin.json b/plugins/FirmwareUpdater/plugin.json
index 3e09eab2b5..c1034e5e42 100644
--- a/plugins/FirmwareUpdater/plugin.json
+++ b/plugins/FirmwareUpdater/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Firmware Updater",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides a machine actions for updating firmware.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/GCodeGzReader/plugin.json b/plugins/GCodeGzReader/plugin.json
index 3bd6a4097d..d4f281682f 100644
--- a/plugins/GCodeGzReader/plugin.json
+++ b/plugins/GCodeGzReader/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Compressed G-code Reader",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Reads g-code from a compressed archive.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/GCodeGzWriter/plugin.json b/plugins/GCodeGzWriter/plugin.json
index 4c6497317b..b0e6f8d605 100644
--- a/plugins/GCodeGzWriter/plugin.json
+++ b/plugins/GCodeGzWriter/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Compressed G-code Writer",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Writes g-code to a compressed archive.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/GCodeProfileReader/plugin.json b/plugins/GCodeProfileReader/plugin.json
index 9677628c85..af1c2d1827 100644
--- a/plugins/GCodeProfileReader/plugin.json
+++ b/plugins/GCodeProfileReader/plugin.json
@@ -1,8 +1,8 @@
{
"name": "G-code Profile Reader",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides support for importing profiles from g-code files.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py
index 6fe2cb5260..baf21d47ce 100644
--- a/plugins/GCodeReader/FlavorParser.py
+++ b/plugins/GCodeReader/FlavorParser.py
@@ -364,6 +364,8 @@ class FlavorParser:
self._layer_type = LayerPolygon.SupportType
elif type == "FILL":
self._layer_type = LayerPolygon.InfillType
+ elif type == "SUPPORT-INTERFACE":
+ self._layer_type = LayerPolygon.SupportInterfaceType
else:
Logger.log("w", "Encountered a unknown type (%s) while parsing g-code.", type)
diff --git a/plugins/GCodeReader/plugin.json b/plugins/GCodeReader/plugin.json
index 75b4d0cd4f..bbc94fa917 100644
--- a/plugins/GCodeReader/plugin.json
+++ b/plugins/GCodeReader/plugin.json
@@ -1,8 +1,8 @@
{
"name": "G-code Reader",
- "author": "Victor Larchenko",
- "version": "1.0.0",
+ "author": "Victor Larchenko, Ultimaker",
+ "version": "1.0.1",
"description": "Allows loading and displaying G-code files.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/GCodeWriter/plugin.json b/plugins/GCodeWriter/plugin.json
index 3bbbab8b95..f3a95ddb78 100644
--- a/plugins/GCodeWriter/plugin.json
+++ b/plugins/GCodeWriter/plugin.json
@@ -1,8 +1,8 @@
{
"name": "G-code Writer",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Writes g-code to a file.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/ImageReader/ConfigUI.qml b/plugins/ImageReader/ConfigUI.qml
index 12c6aa8dde..b9ff2e4453 100644
--- a/plugins/ImageReader/ConfigUI.qml
+++ b/plugins/ImageReader/ConfigUI.qml
@@ -20,7 +20,7 @@ UM.Dialog
GridLayout
{
- UM.I18nCatalog{id: catalog; name:"cura"}
+ UM.I18nCatalog{id: catalog; name: "cura"}
anchors.fill: parent;
Layout.fillWidth: true
columnSpacing: 16 * screenScaleFactor
diff --git a/plugins/ImageReader/plugin.json b/plugins/ImageReader/plugin.json
index 08195863e8..d966537d99 100644
--- a/plugins/ImageReader/plugin.json
+++ b/plugins/ImageReader/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Image Reader",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Enables ability to generate printable geometry from 2D image files.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/LegacyProfileReader/plugin.json b/plugins/LegacyProfileReader/plugin.json
index 179f5444e0..2f5264ad37 100644
--- a/plugins/LegacyProfileReader/plugin.json
+++ b/plugins/LegacyProfileReader/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Legacy Cura Profile Reader",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides support for importing profiles from legacy Cura versions.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/MachineSettingsAction/MachineSettingsAction.qml b/plugins/MachineSettingsAction/MachineSettingsAction.qml
index 004b4e3cfc..ef8fda224a 100644
--- a/plugins/MachineSettingsAction/MachineSettingsAction.qml
+++ b/plugins/MachineSettingsAction/MachineSettingsAction.qml
@@ -1,4 +1,4 @@
-// Copyright (c) 2016 Ultimaker B.V.
+// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
@@ -13,7 +13,8 @@ import Cura 1.0 as Cura
Cura.MachineAction
{
id: base
- property var extrudersModel: Cura.ExtrudersModel{}
+ property var extrudersModel: Cura.ExtrudersModel{} // Do not retrieve the Model from a backend. Otherwise the tabs
+ // in tabView will not removed/updated. Probably QML bug
property int extruderTabsCount: 0
property var activeMachineId: Cura.MachineManager.activeMachine != null ? Cura.MachineManager.activeMachine.id : ""
@@ -23,7 +24,7 @@ Cura.MachineAction
target: base.extrudersModel
onModelChanged:
{
- var extruderCount = base.extrudersModel.rowCount();
+ var extruderCount = base.extrudersModel.count;
base.extruderTabsCount = extruderCount;
}
}
diff --git a/plugins/MachineSettingsAction/plugin.json b/plugins/MachineSettingsAction/plugin.json
index 571658e40a..d734c1adf5 100644
--- a/plugins/MachineSettingsAction/plugin.json
+++ b/plugins/MachineSettingsAction/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Machine Settings action",
"author": "fieldOfView",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/ModelChecker/ModelChecker.qml b/plugins/ModelChecker/ModelChecker.qml
index 98db233bf8..ddeed063b1 100644
--- a/plugins/ModelChecker/ModelChecker.qml
+++ b/plugins/ModelChecker/ModelChecker.qml
@@ -4,19 +4,19 @@
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
-import QtQuick.Layouts 1.1
-import QtQuick.Dialogs 1.1
-import QtQuick.Window 2.2
import UM 1.2 as UM
-import Cura 1.0 as Cura
Button
{
id: modelCheckerButton
- UM.I18nCatalog{id: catalog; name:"cura"}
+ UM.I18nCatalog
+ {
+ id: catalog
+ name: "cura"
+ }
visible: manager.hasWarnings
tooltip: catalog.i18nc("@info:tooltip", "Some things could be problematic in this print. Click to see tips for adjustment.")
@@ -25,6 +25,8 @@ Button
width: UM.Theme.getSize("save_button_specs_icons").width
height: UM.Theme.getSize("save_button_specs_icons").height
+ anchors.verticalCenter: parent ? parent.verticalCenter : undefined
+
style: ButtonStyle
{
background: Item
@@ -33,7 +35,6 @@ Button
{
width: UM.Theme.getSize("save_button_specs_icons").width;
height: UM.Theme.getSize("save_button_specs_icons").height;
- sourceSize.width: width;
sourceSize.height: width;
color: control.hovered ? UM.Theme.getColor("text_scene_hover") : UM.Theme.getColor("text_scene");
source: "model_checker.svg"
diff --git a/plugins/ModelChecker/plugin.json b/plugins/ModelChecker/plugin.json
index 3753c0cc88..59be5bbf0a 100644
--- a/plugins/ModelChecker/plugin.json
+++ b/plugins/ModelChecker/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Model Checker",
"author": "Ultimaker B.V.",
- "version": "0.1",
- "api": 5,
+ "version": "1.0.1",
+ "api": "6.0",
"description": "Checks models and print configuration for possible printing issues and give suggestions.",
"i18n-catalog": "cura"
}
diff --git a/plugins/MonitorStage/MonitorMain.qml b/plugins/MonitorStage/MonitorMain.qml
new file mode 100644
index 0000000000..5fda32db9e
--- /dev/null
+++ b/plugins/MonitorStage/MonitorMain.qml
@@ -0,0 +1,45 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.10
+import QtQuick.Controls 1.4
+
+import UM 1.3 as UM
+import Cura 1.0 as Cura
+
+
+// We show a nice overlay on the 3D viewer when the current output device has no monitor view
+Rectangle
+{
+ id: viewportOverlay
+
+ color: UM.Theme.getColor("viewport_overlay")
+ anchors.fill: parent
+
+ // This mouse area is to prevent mouse clicks to be passed onto the scene.
+ MouseArea
+ {
+ anchors.fill: parent
+ acceptedButtons: Qt.AllButtons
+ onWheel: wheel.accepted = true
+ }
+
+ // Disable dropping files into Cura when the monitor page is active
+ DropArea
+ {
+ anchors.fill: parent
+ }
+ Loader
+ {
+ id: monitorViewComponent
+
+ anchors.fill: parent
+
+ height: parent.height
+
+ property real maximumWidth: parent.width
+ property real maximumHeight: parent.height
+
+ sourceComponent: Cura.MachineManager.printerOutputDevices.length > 0 ? Cura.MachineManager.printerOutputDevices[0].monitorItem : null
+ }
+}
\ No newline at end of file
diff --git a/plugins/MonitorStage/MonitorMainView.qml b/plugins/MonitorStage/MonitorMainView.qml
deleted file mode 100644
index c48f6d0aab..0000000000
--- a/plugins/MonitorStage/MonitorMainView.qml
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright (c) 2017 Ultimaker B.V.
-
-import QtQuick 2.2
-import QtQuick.Controls 1.1
-
-import UM 1.3 as UM
-import Cura 1.0 as Cura
-
-Item
-{
- // parent could be undefined as this component is not visible at all times
- width: parent ? parent.width : 0
- height: parent ? parent.height : 0
-
- // We show a nice overlay on the 3D viewer when the current output device has no monitor view
- Rectangle
- {
- id: viewportOverlay
-
- color: UM.Theme.getColor("viewport_overlay")
- width: parent.width
- height: parent.height
-
- MouseArea
- {
- anchors.fill: parent
- acceptedButtons: Qt.AllButtons
- onWheel: wheel.accepted = true
- }
- }
-
- Loader
- {
- id: monitorViewComponent
-
- width: parent.width
- height: parent.height
-
- property real maximumWidth: parent.width
- property real maximumHeight: parent.height
-
- sourceComponent: Cura.MachineManager.printerOutputDevices.length > 0 ? Cura.MachineManager.printerOutputDevices[0].monitorItem: null
- visible: sourceComponent != null
- }
-}
diff --git a/plugins/MonitorStage/MonitorMenu.qml b/plugins/MonitorStage/MonitorMenu.qml
new file mode 100644
index 0000000000..bc95c276e8
--- /dev/null
+++ b/plugins/MonitorStage/MonitorMenu.qml
@@ -0,0 +1,23 @@
+// 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.3
+
+import UM 1.3 as UM
+import Cura 1.1 as Cura
+
+Item
+{
+ signal showTooltip(Item item, point location, string text)
+ signal hideTooltip()
+
+ Cura.MachineSelector
+ {
+ id: machineSelection
+ headerCornerSide: Cura.RoundedRectangle.Direction.All
+ width: UM.Theme.getSize("machine_selector_widget").width
+ height: parent.height
+ anchors.centerIn: parent
+ }
+}
\ No newline at end of file
diff --git a/plugins/MonitorStage/MonitorStage.py b/plugins/MonitorStage/MonitorStage.py
index ace201e994..69b7f20f4e 100644
--- a/plugins/MonitorStage/MonitorStage.py
+++ b/plugins/MonitorStage/MonitorStage.py
@@ -65,15 +65,10 @@ class MonitorStage(CuraStage):
# We can only connect now, as we need to be sure that everything is loaded (plugins get created quite early)
Application.getInstance().getMachineManager().outputDevicesChanged.connect(self._onOutputDevicesChanged)
self._onOutputDevicesChanged()
- self._updateMainOverlay()
- self._updateSidebar()
- def _updateMainOverlay(self):
- main_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("MonitorStage"),
- "MonitorMainView.qml")
- self.addDisplayComponent("main", main_component_path)
-
- def _updateSidebar(self):
- sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles),
- "MonitorSidebar.qml")
- self.addDisplayComponent("sidebar", sidebar_component_path)
+ plugin_path = Application.getInstance().getPluginRegistry().getPluginPath(self.getPluginId())
+ if plugin_path is not None:
+ menu_component_path = os.path.join(plugin_path, "MonitorMenu.qml")
+ main_component_path = os.path.join(plugin_path, "MonitorMain.qml")
+ self.addDisplayComponent("menu", menu_component_path)
+ self.addDisplayComponent("main", main_component_path)
diff --git a/plugins/MonitorStage/__init__.py b/plugins/MonitorStage/__init__.py
index bdaf53a36c..0468e6319b 100644
--- a/plugins/MonitorStage/__init__.py
+++ b/plugins/MonitorStage/__init__.py
@@ -7,14 +7,16 @@ from . import MonitorStage
from UM.i18n import i18nCatalog
i18n_catalog = i18nCatalog("cura")
+
def getMetaData():
return {
"stage": {
"name": i18n_catalog.i18nc("@item:inmenu", "Monitor"),
- "weight": 1
+ "weight": 2
}
}
+
def register(app):
return {
"stage": MonitorStage.MonitorStage()
diff --git a/plugins/MonitorStage/plugin.json b/plugins/MonitorStage/plugin.json
index 88b53840e0..95e4b86f36 100644
--- a/plugins/MonitorStage/plugin.json
+++ b/plugins/MonitorStage/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Monitor Stage",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides a monitor stage in Cura.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
\ No newline at end of file
diff --git a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml
index 5d4e17a102..0e2bd88619 100644
--- a/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml
+++ b/plugins/PerObjectSettingsTool/PerObjectSettingsPanel.qml
@@ -265,7 +265,6 @@ Item {
anchors.verticalCenter: parent.verticalCenter
width: parent.width
height: width
- sourceSize.width: width
sourceSize.height: width
color: control.hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button")
source: UM.Theme.getIcon("minus")
diff --git a/plugins/PerObjectSettingsTool/plugin.json b/plugins/PerObjectSettingsTool/plugin.json
index 15fde63387..f272abf06a 100644
--- a/plugins/PerObjectSettingsTool/plugin.json
+++ b/plugins/PerObjectSettingsTool/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Per Model Settings Tool",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides the Per Model Settings.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py
index 11ee610bec..78f9cc0516 100644
--- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py
+++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py
@@ -55,14 +55,14 @@ class PostProcessingPlugin(QObject, Extension):
def selectedScriptDefinitionId(self) -> Optional[str]:
try:
return self._script_list[self._selected_script_index].getDefinitionId()
- except:
+ except IndexError:
return ""
@pyqtProperty(str, notify=selectedIndexChanged)
def selectedScriptStackId(self) -> Optional[str]:
try:
return self._script_list[self._selected_script_index].getStackId()
- except:
+ except IndexError:
return ""
## Execute all post-processing scripts on the gcode.
diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.qml b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml
index d492e06462..cd8303d1d3 100644
--- a/plugins/PostProcessingPlugin/PostProcessingPlugin.qml
+++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml
@@ -25,13 +25,13 @@ UM.Dialog
{
if(!visible) //Whenever the window is closed (either via the "Close" button or the X on the window frame), we want to update it in the stack.
{
- manager.writeScriptsToStack();
+ manager.writeScriptsToStack()
}
}
Item
{
- UM.I18nCatalog{id: catalog; name:"cura"}
+ UM.I18nCatalog{id: catalog; name: "cura"}
id: base
property int columnWidth: Math.round((base.width / 2) - UM.Theme.getSize("default_margin").width)
property int textMargin: Math.round(UM.Theme.getSize("default_margin").width / 2)
@@ -61,18 +61,23 @@ UM.Dialog
anchors.leftMargin: base.textMargin
anchors.right: parent.right
anchors.rightMargin: base.textMargin
- font: UM.Theme.getFont("large")
+ font: UM.Theme.getFont("large_bold")
elide: Text.ElideRight
}
ListView
{
id: activeScriptsList
- anchors.top: activeScriptsHeader.bottom
- anchors.topMargin: base.textMargin
- anchors.left: parent.left
- anchors.leftMargin: UM.Theme.getSize("default_margin").width
- anchors.right: parent.right
- anchors.rightMargin: base.textMargin
+
+ anchors
+ {
+ top: activeScriptsHeader.bottom
+ left: parent.left
+ right: parent.right
+ rightMargin: base.textMargin
+ topMargin: base.textMargin
+ leftMargin: UM.Theme.getSize("default_margin").width
+ }
+
height: childrenRect.height
model: manager.scriptList
delegate: Item
@@ -84,8 +89,12 @@ UM.Dialog
id: activeScriptButton
text: manager.getScriptLabelByKey(modelData.toString())
exclusiveGroup: selectedScriptGroup
+ width: parent.width
+ height: UM.Theme.getSize("setting").height
checkable: true
- checked: {
+
+ checked:
+ {
if (manager.selectedScriptIndex == index)
{
base.activeScriptName = manager.getScriptLabelByKey(modelData.toString())
@@ -102,8 +111,7 @@ UM.Dialog
manager.setSelectedScriptIndex(index)
base.activeScriptName = manager.getScriptLabelByKey(modelData.toString())
}
- width: parent.width
- height: UM.Theme.getSize("setting").height
+
style: ButtonStyle
{
background: Rectangle
@@ -121,6 +129,7 @@ UM.Dialog
}
}
}
+
Button
{
id: removeButton
@@ -141,7 +150,6 @@ UM.Dialog
anchors.horizontalCenter: parent.horizontalCenter
width: Math.round(control.width / 2.7)
height: Math.round(control.height / 2.7)
- sourceSize.width: width
sourceSize.height: width
color: palette.text
source: UM.Theme.getIcon("cross1")
@@ -176,7 +184,6 @@ UM.Dialog
anchors.horizontalCenter: parent.horizontalCenter
width: Math.round(control.width / 2.5)
height: Math.round(control.height / 2.5)
- sourceSize.width: width
sourceSize.height: width
color: control.enabled ? palette.text : disabledPalette.text
source: UM.Theme.getIcon("arrow_bottom")
@@ -211,7 +218,6 @@ UM.Dialog
anchors.horizontalCenter: parent.horizontalCenter
width: Math.round(control.width / 2.5)
height: Math.round(control.height / 2.5)
- sourceSize.width: width
sourceSize.height: width
color: control.enabled ? palette.text : disabledPalette.text
source: UM.Theme.getIcon("arrow_top")
@@ -252,15 +258,15 @@ UM.Dialog
onTriggered: manager.addScriptToList(modelData.toString())
}
- onObjectAdded: scriptsMenu.insertItem(index, object);
- onObjectRemoved: scriptsMenu.removeItem(object);
+ onObjectAdded: scriptsMenu.insertItem(index, object)
+ onObjectRemoved: scriptsMenu.removeItem(object)
}
}
}
Rectangle
{
- color: UM.Theme.getColor("sidebar")
+ color: UM.Theme.getColor("main_background")
anchors.left: activeScripts.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: parent.right
@@ -271,26 +277,35 @@ UM.Dialog
{
id: scriptSpecsHeader
text: manager.selectedScriptIndex == -1 ? catalog.i18nc("@label", "Settings") : base.activeScriptName
- anchors.top: parent.top
- anchors.topMargin: base.textMargin
- anchors.left: parent.left
- anchors.leftMargin: base.textMargin
- anchors.right: parent.right
- anchors.rightMargin: base.textMargin
+ anchors
+ {
+ top: parent.top
+ topMargin: base.textMargin
+ left: parent.left
+ leftMargin: base.textMargin
+ right: parent.right
+ rightMargin: base.textMargin
+ }
+
elide: Text.ElideRight
height: 20 * screenScaleFactor
- font: UM.Theme.getFont("large")
+ font: UM.Theme.getFont("large_bold")
color: UM.Theme.getColor("text")
}
ScrollView
{
id: scrollView
- anchors.top: scriptSpecsHeader.bottom
- anchors.topMargin: settingsPanel.textMargin
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.bottom: parent.bottom
+ anchors
+ {
+ top: scriptSpecsHeader.bottom
+ topMargin: settingsPanel.textMargin
+ left: parent.left
+ leftMargin: UM.Theme.getSize("default_margin").width
+ right: parent.right
+ bottom: parent.bottom
+ }
+
visible: manager.selectedScriptDefinitionId != ""
style: UM.Theme.styles.scrollview;
@@ -300,11 +315,12 @@ UM.Dialog
spacing: UM.Theme.getSize("default_lining").height
model: UM.SettingDefinitionsModel
{
- id: definitionsModel;
+ id: definitionsModel
containerId: manager.selectedScriptDefinitionId
showAll: true
}
- delegate:Loader
+
+ delegate: Loader
{
id: settingLoader
@@ -315,23 +331,24 @@ UM.Dialog
{
if(model.type != undefined)
{
- return UM.Theme.getSize("section").height;
+ return UM.Theme.getSize("section").height
}
else
{
- return 0;
+ return 0
}
}
else
{
- return 0;
+ return 0
}
-
}
Behavior on height { NumberAnimation { duration: 100 } }
opacity: provider.properties.enabled == "True" ? 1 : 0
+
Behavior on opacity { NumberAnimation { duration: 100 } }
enabled: opacity > 0
+
property var definition: model
property var settingDefinitionsModel: definitionsModel
property var propertyProvider: provider
@@ -342,11 +359,12 @@ UM.Dialog
//causing nasty issues when selecting different options. So disable asynchronous loading of enum type completely.
asynchronous: model.type != "enum" && model.type != "extruder"
- onLoaded: {
+ onLoaded:
+ {
settingLoader.item.showRevertButton = false
settingLoader.item.showInheritButton = false
settingLoader.item.showLinkedSettingIcon = false
- settingLoader.item.doDepthIndentation = true
+ settingLoader.item.doDepthIndentation = false
settingLoader.item.doQualityUserSettingEmphasis = false
}
@@ -398,24 +416,20 @@ UM.Dialog
onShowTooltip:
{
- tooltip.text = text;
- var position = settingLoader.mapToItem(settingsPanel, settingsPanel.x, 0);
- tooltip.show(position);
+ tooltip.text = text
+ var position = settingLoader.mapToItem(settingsPanel, settingsPanel.x, 0)
+ tooltip.show(position)
tooltip.target.x = position.x + 1
}
- onHideTooltip:
- {
- tooltip.hide();
- }
+ onHideTooltip: tooltip.hide()
}
-
}
}
}
}
- Cura.SidebarTooltip
+ Cura.PrintSetupTooltip
{
id: tooltip
}
@@ -462,6 +476,7 @@ UM.Dialog
Cura.SettingUnknown { }
}
}
+
rightButtons: Button
{
text: catalog.i18nc("@action:button", "Close")
@@ -469,44 +484,15 @@ UM.Dialog
onClicked: dialog.accept()
}
- Button {
+ Cura.SecondaryButton
+ {
objectName: "postProcessingSaveAreaButton"
visible: activeScriptsList.count > 0
- height: UM.Theme.getSize("save_button_save_to_button").height
+ height: UM.Theme.getSize("action_button").height
width: height
tooltip: catalog.i18nc("@info:tooltip", "Change active post-processing scripts")
onClicked: dialog.show()
-
- style: ButtonStyle {
- background: Rectangle {
- id: deviceSelectionIcon
- border.width: UM.Theme.getSize("default_lining").width
- border.color: !control.enabled ? UM.Theme.getColor("action_button_disabled_border") :
- control.pressed ? UM.Theme.getColor("action_button_active_border") :
- control.hovered ? UM.Theme.getColor("action_button_hovered_border") : UM.Theme.getColor("action_button_border")
- color: !control.enabled ? UM.Theme.getColor("action_button_disabled") :
- control.pressed ? UM.Theme.getColor("action_button_active") :
- control.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button")
- Behavior on color { ColorAnimation { duration: 50; } }
- anchors.left: parent.left
- anchors.leftMargin: Math.round(UM.Theme.getSize("save_button_text_margin").width / 2);
- width: parent.height
- height: parent.height
-
- UM.RecolorImage {
- anchors.verticalCenter: parent.verticalCenter
- anchors.horizontalCenter: parent.horizontalCenter
- width: Math.round(parent.width / 2)
- height: Math.round(parent.height / 2)
- sourceSize.width: width
- sourceSize.height: height
- color: !control.enabled ? UM.Theme.getColor("action_button_disabled_text") :
- control.pressed ? UM.Theme.getColor("action_button_active_text") :
- control.hovered ? UM.Theme.getColor("action_button_hovered_text") : UM.Theme.getColor("action_button_text");
- source: "postprocessing.svg"
- }
- }
- label: Label{ }
- }
+ iconSource: "postprocessing.svg"
+ fixedWidthMode: true
}
}
\ No newline at end of file
diff --git a/plugins/PostProcessingPlugin/plugin.json b/plugins/PostProcessingPlugin/plugin.json
index fea061e93b..1e73133c53 100644
--- a/plugins/PostProcessingPlugin/plugin.json
+++ b/plugins/PostProcessingPlugin/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Post Processing",
"author": "Ultimaker",
- "version": "2.2",
- "api": 5,
+ "version": "2.2.1",
+ "api": "6.0",
"description": "Extension that allows for user created scripts for post processing",
"catalog": "cura"
}
\ No newline at end of file
diff --git a/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py b/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py
index 919b06d28e..be9f93c0f6 100644
--- a/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py
+++ b/plugins/PostProcessingPlugin/scripts/ChangeAtZ.py
@@ -112,7 +112,7 @@ class ChangeAtZ(Script):
"e1_Change_speed":
{
"label": "Change Speed",
- "description": "Select if total speed (print and travel) has to be cahnged",
+ "description": "Select if total speed (print and travel) has to be changed",
"type": "bool",
"default_value": false
},
diff --git a/plugins/PostProcessingPlugin/scripts/ExampleScript.md b/plugins/PostProcessingPlugin/scripts/ExampleScript.md
new file mode 100644
index 0000000000..08652132aa
--- /dev/null
+++ b/plugins/PostProcessingPlugin/scripts/ExampleScript.md
@@ -0,0 +1,3 @@
+A good example script is SearchAndReplace.py.
+If you have any questions please ask them at:
+https://github.com/Ultimaker/Cura/issues
\ No newline at end of file
diff --git a/plugins/PostProcessingPlugin/scripts/ExampleScript.py b/plugins/PostProcessingPlugin/scripts/ExampleScript.py
deleted file mode 100644
index 416a5f5404..0000000000
--- a/plugins/PostProcessingPlugin/scripts/ExampleScript.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
-# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
-from ..Script import Script
-
-class ExampleScript(Script):
- def __init__(self):
- super().__init__()
-
- def getSettingDataString(self):
- return """{
- "name":"Example script",
- "key": "ExampleScript",
- "metadata": {},
- "version": 2,
- "settings":
- {
- "test":
- {
- "label": "Test",
- "description": "None",
- "unit": "mm",
- "type": "float",
- "default_value": 0.5,
- "minimum_value": "0",
- "minimum_value_warning": "0.1",
- "maximum_value_warning": "1"
- },
- "derp":
- {
- "label": "zomg",
- "description": "afgasgfgasfgasf",
- "unit": "mm",
- "type": "float",
- "default_value": 0.5,
- "minimum_value": "0",
- "minimum_value_warning": "0.1",
- "maximum_value_warning": "1"
- }
- }
- }"""
-
- def execute(self, data):
- return data
\ No newline at end of file
diff --git a/plugins/PrepareStage/PrepareMenu.qml b/plugins/PrepareStage/PrepareMenu.qml
new file mode 100644
index 0000000000..b62d65254d
--- /dev/null
+++ b/plugins/PrepareStage/PrepareMenu.qml
@@ -0,0 +1,134 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Layouts 1.1
+import QtQuick.Controls 2.3
+
+import UM 1.3 as UM
+import Cura 1.1 as Cura
+
+import QtGraphicalEffects 1.0 // For the dropshadow
+
+Item
+{
+ id: prepareMenu
+
+ UM.I18nCatalog
+ {
+ id: catalog
+ name: "cura"
+ }
+
+ // Item to ensure that all of the buttons are nicely centered.
+ Item
+ {
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: openFileButton.width + itemRow.width + UM.Theme.getSize("default_margin").width
+ height: parent.height
+
+ RowLayout
+ {
+ id: itemRow
+
+ anchors.left: openFileButton.right
+ anchors.leftMargin: UM.Theme.getSize("default_margin").width
+
+ width: Math.round(0.9 * prepareMenu.width)
+ height: parent.height
+ spacing: 0
+
+ Cura.MachineSelector
+ {
+ id: machineSelection
+ headerCornerSide: Cura.RoundedRectangle.Direction.Left
+ Layout.minimumWidth: UM.Theme.getSize("machine_selector_widget").width
+ Layout.maximumWidth: UM.Theme.getSize("machine_selector_widget").width
+ Layout.fillWidth: true
+ Layout.fillHeight: true
+ }
+
+ // Separator line
+ Rectangle
+ {
+ height: parent.height
+ width: UM.Theme.getSize("default_lining").width
+ color: UM.Theme.getColor("lining")
+ }
+
+ Cura.ConfigurationMenu
+ {
+ Layout.fillHeight: true
+ Layout.fillWidth: true
+ Layout.preferredWidth: itemRow.width - machineSelection.width - printSetupSelectorItem.width - 2 * UM.Theme.getSize("default_lining").width
+ }
+
+ // Separator line
+ Rectangle
+ {
+ height: parent.height
+ width: UM.Theme.getSize("default_lining").width
+ color: UM.Theme.getColor("lining")
+ }
+
+ Item
+ {
+ id: printSetupSelectorItem
+ // This is a work around to prevent the printSetupSelector from having to be re-loaded every time
+ // a stage switch is done.
+ children: [printSetupSelector]
+ height: childrenRect.height
+ width: childrenRect.width
+ }
+ }
+
+ Button
+ {
+ id: openFileButton
+ height: UM.Theme.getSize("stage_menu").height
+ width: UM.Theme.getSize("stage_menu").height
+ onClicked: Cura.Actions.open.trigger()
+ hoverEnabled: true
+
+ contentItem: Item
+ {
+ anchors.fill: parent
+ UM.RecolorImage
+ {
+ id: buttonIcon
+ anchors.centerIn: parent
+ source: UM.Theme.getIcon("load")
+ width: UM.Theme.getSize("button_icon").width
+ height: UM.Theme.getSize("button_icon").height
+ color: UM.Theme.getColor("icon")
+
+ sourceSize.height: height
+ }
+ }
+
+ background: Rectangle
+ {
+ id: background
+ height: UM.Theme.getSize("stage_menu").height
+ width: UM.Theme.getSize("stage_menu").height
+
+ radius: UM.Theme.getSize("default_radius").width
+ color: openFileButton.hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("action_button")
+ }
+
+ DropShadow
+ {
+ id: shadow
+ // Don't blur the shadow
+ radius: 0
+ anchors.fill: background
+ source: background
+ verticalOffset: 2
+ visible: true
+ color: UM.Theme.getColor("action_button_shadow")
+ // Should always be drawn behind the background.
+ z: background.z - 1
+ }
+ }
+ }
+}
diff --git a/plugins/PrepareStage/PrepareStage.py b/plugins/PrepareStage/PrepareStage.py
index c3c9f0a1f8..b0f862dc48 100644
--- a/plugins/PrepareStage/PrepareStage.py
+++ b/plugins/PrepareStage/PrepareStage.py
@@ -1,19 +1,19 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
from UM.Application import Application
+from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from cura.Stages.CuraStage import CuraStage
+
## Stage for preparing model (slicing).
class PrepareStage(CuraStage):
-
def __init__(self, parent = None):
super().__init__(parent)
Application.getInstance().engineCreatedSignal.connect(self._engineCreated)
def _engineCreated(self):
- sidebar_component_path = os.path.join(Resources.getPath(Application.getInstance().ResourceTypes.QmlFiles),
- "PrepareSidebar.qml")
- self.addDisplayComponent("sidebar", sidebar_component_path)
+ menu_component_path = os.path.join(PluginRegistry.getInstance().getPluginPath("PrepareStage"), "PrepareMenu.qml")
+ self.addDisplayComponent("menu", menu_component_path)
diff --git a/plugins/PrepareStage/plugin.json b/plugins/PrepareStage/plugin.json
index f0464313c7..dc5c68ce16 100644
--- a/plugins/PrepareStage/plugin.json
+++ b/plugins/PrepareStage/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Prepare Stage",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides a prepare stage in Cura.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
\ No newline at end of file
diff --git a/plugins/PreviewStage/PreviewMain.qml b/plugins/PreviewStage/PreviewMain.qml
new file mode 100644
index 0000000000..04241783e9
--- /dev/null
+++ b/plugins/PreviewStage/PreviewMain.qml
@@ -0,0 +1,18 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.4
+import QtQuick.Controls 1.2
+import QtQuick.Layouts 1.1
+import QtQuick.Controls.Styles 1.1
+
+import UM 1.0 as UM
+import Cura 1.0 as Cura
+
+
+Loader
+{
+ id: previewMain
+
+ source: UM.Controller.activeView != null && UM.Controller.activeView.mainComponent != null ? UM.Controller.activeView.mainComponent : ""
+}
\ No newline at end of file
diff --git a/plugins/PreviewStage/PreviewMenu.qml b/plugins/PreviewStage/PreviewMenu.qml
new file mode 100644
index 0000000000..62f814aac9
--- /dev/null
+++ b/plugins/PreviewStage/PreviewMenu.qml
@@ -0,0 +1,79 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Layouts 1.1
+import QtQuick.Controls 2.3
+
+import UM 1.3 as UM
+import Cura 1.1 as Cura
+
+Item
+{
+ id: previewMenu
+
+ property real itemHeight: height - 2 * UM.Theme.getSize("default_lining").width
+
+ UM.I18nCatalog
+ {
+ id: catalog
+ name: "cura"
+ }
+
+ Row
+ {
+ id: stageMenuRow
+ anchors.centerIn: parent
+ height: parent.height
+ width: childrenRect.width
+
+ // We want this row to have a preferred with equals to the 85% of the parent
+ property int preferredWidth: Math.round(0.85 * previewMenu.width)
+
+ Cura.ViewsSelector
+ {
+ id: viewsSelector
+ height: parent.height
+ width: UM.Theme.getSize("views_selector").width
+ headerCornerSide: Cura.RoundedRectangle.Direction.Left
+ }
+
+ // Separator line
+ Rectangle
+ {
+ height: parent.height
+ // If there is no viewPanel, we only need a single spacer, so hide this one.
+ visible: viewPanel.source != ""
+ width: visible ? UM.Theme.getSize("default_lining").width : 0
+
+ color: UM.Theme.getColor("lining")
+ }
+
+ // This component will grow freely up to complete the preferredWidth of the row.
+ Loader
+ {
+ id: viewPanel
+ height: parent.height
+ width: source != "" ? (stageMenuRow.preferredWidth - viewsSelector.width - printSetupSelectorItem.width - 2 * UM.Theme.getSize("default_lining").width) : 0
+ source: UM.Controller.activeView != null && UM.Controller.activeView.stageMenuComponent != null ? UM.Controller.activeView.stageMenuComponent : ""
+ }
+
+ // Separator line
+ Rectangle
+ {
+ height: parent.height
+ width: UM.Theme.getSize("default_lining").width
+ color: UM.Theme.getColor("lining")
+ }
+
+ Item
+ {
+ id: printSetupSelectorItem
+ // This is a work around to prevent the printSetupSelector from having to be re-loaded every time
+ // a stage switch is done.
+ children: [printSetupSelector]
+ height: childrenRect.height
+ width: childrenRect.width
+ }
+ }
+}
diff --git a/plugins/PreviewStage/PreviewStage.py b/plugins/PreviewStage/PreviewStage.py
new file mode 100644
index 0000000000..1c487c8340
--- /dev/null
+++ b/plugins/PreviewStage/PreviewStage.py
@@ -0,0 +1,51 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+import os.path
+
+from UM.Qt.QtApplication import QtApplication
+from cura.Stages.CuraStage import CuraStage
+
+from typing import TYPE_CHECKING, Optional
+
+if TYPE_CHECKING:
+ from UM.View.View import View
+
+
+## Displays a preview of what you're about to print.
+#
+# The Python component of this stage just loads PreviewMain.qml for display
+# when the stage is selected, and makes sure that it reverts to the previous
+# view when the previous stage is activated.
+class PreviewStage(CuraStage):
+ def __init__(self, application: QtApplication, parent = None) -> None:
+ super().__init__(parent)
+ self._application = application
+ self._application.engineCreatedSignal.connect(self._engineCreated)
+ self._previously_active_view = None # type: Optional[View]
+
+ ## When selecting the stage, remember which was the previous view so that
+ # we can revert to that view when we go out of the stage later.
+ def onStageSelected(self) -> None:
+ self._previously_active_view = self._application.getController().getActiveView()
+
+ ## Called when going to a different stage (away from the Preview Stage).
+ #
+ # When going to a different stage, the view should be reverted to what it
+ # was before. Normally, that just reverts it to solid view.
+ def onStageDeselected(self) -> None:
+ if self._previously_active_view is not None:
+ self._application.getController().setActiveView(self._previously_active_view.getPluginId())
+ self._previously_active_view = None
+
+ ## Delayed load of the QML files.
+ #
+ # We need to make sure that the QML engine is running before we can load
+ # these.
+ def _engineCreated(self) -> None:
+ plugin_path = self._application.getPluginRegistry().getPluginPath(self.getPluginId())
+ if plugin_path is not None:
+ menu_component_path = os.path.join(plugin_path, "PreviewMenu.qml")
+ main_component_path = os.path.join(plugin_path, "PreviewMain.qml")
+ self.addDisplayComponent("menu", menu_component_path)
+ self.addDisplayComponent("main", main_component_path)
diff --git a/plugins/PreviewStage/__init__.py b/plugins/PreviewStage/__init__.py
new file mode 100644
index 0000000000..424f573e4a
--- /dev/null
+++ b/plugins/PreviewStage/__init__.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from . import PreviewStage
+
+from UM.i18n import i18nCatalog
+i18n_catalog = i18nCatalog("cura")
+
+
+def getMetaData():
+ return {
+ "stage": {
+ "name": i18n_catalog.i18nc("@item:inmenu", "Preview"),
+ "weight": 1
+ }
+ }
+
+
+def register(app):
+ return {
+ "stage": PreviewStage.PreviewStage(app)
+ }
diff --git a/plugins/PreviewStage/plugin.json b/plugins/PreviewStage/plugin.json
new file mode 100644
index 0000000000..e1e4288bae
--- /dev/null
+++ b/plugins/PreviewStage/plugin.json
@@ -0,0 +1,8 @@
+{
+ "name": "Preview Stage",
+ "author": "Ultimaker B.V.",
+ "version": "1.0.1",
+ "description": "Provides a preview stage in Cura.",
+ "api": "6.0",
+ "i18n-catalog": "cura"
+}
\ No newline at end of file
diff --git a/plugins/RemovableDriveOutputDevice/plugin.json b/plugins/RemovableDriveOutputDevice/plugin.json
index 36bb9ae186..5523d6b1c1 100644
--- a/plugins/RemovableDriveOutputDevice/plugin.json
+++ b/plugins/RemovableDriveOutputDevice/plugin.json
@@ -2,7 +2,7 @@
"name": "Removable Drive Output Device Plugin",
"author": "Ultimaker B.V.",
"description": "Provides removable drive hotplugging and writing support.",
- "version": "1.0.0",
- "api": 5,
+ "version": "1.0.1",
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/SimulationView/LayerSlider.qml b/plugins/SimulationView/LayerSlider.qml
index 1552506969..88f298d1f5 100644
--- a/plugins/SimulationView/LayerSlider.qml
+++ b/plugins/SimulationView/LayerSlider.qml
@@ -13,23 +13,20 @@ Item
{
id: sliderRoot
- // handle properties
- property real handleSize: 10
+ // Handle properties
+ property real handleSize: UM.Theme.getSize("slider_handle").width
property real handleRadius: handleSize / 2
property real minimumRangeHandleSize: handleSize / 2
- property color upperHandleColor: "black"
- property color lowerHandleColor: "black"
- property color rangeHandleColor: "black"
- property color handleActiveColor: "white"
- property real handleLabelWidth: width
+ property color upperHandleColor: UM.Theme.getColor("slider_handle")
+ property color lowerHandleColor: UM.Theme.getColor("slider_handle")
+ property color rangeHandleColor: UM.Theme.getColor("slider_groove_fill")
+ property color handleActiveColor: UM.Theme.getColor("slider_handle_active")
property var activeHandle: upperHandle
- // track properties
- property real trackThickness: 4 // width of the slider track
- property real trackRadius: trackThickness / 2
- property color trackColor: "white"
- property real trackBorderWidth: 1 // width of the slider track border
- property color trackBorderColor: "black"
+ // Track properties
+ property real trackThickness: UM.Theme.getSize("slider_groove").width // width of the slider track
+ property real trackRadius: UM.Theme.getSize("slider_groove_radius").width
+ property color trackColor: UM.Theme.getColor("slider_groove")
// value properties
property real maximumValue: 100
@@ -80,7 +77,7 @@ Item
return Math.min(Math.max(value, sliderRoot.minimumValue), sliderRoot.maximumValue)
}
- // slider track
+ // Slider track
Rectangle
{
id: track
@@ -90,8 +87,6 @@ Item
radius: sliderRoot.trackRadius
anchors.centerIn: sliderRoot
color: sliderRoot.trackColor
- border.width: sliderRoot.trackBorderWidth
- border.color: sliderRoot.trackBorderColor
visible: sliderRoot.layersVisible
}
@@ -106,7 +101,7 @@ Item
anchors.horizontalCenter: sliderRoot.horizontalCenter
visible: sliderRoot.layersVisible
- // set the new value when dragging
+ // Set the new value when dragging
function onHandleDragged()
{
sliderRoot.manuallyChanged = true
@@ -140,9 +135,10 @@ Item
Rectangle
{
- width: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
+ width: sliderRoot.trackThickness
height: parent.height + sliderRoot.handleSize
anchors.centerIn: parent
+ radius: sliderRoot.trackRadius
color: sliderRoot.rangeHandleColor
}
@@ -275,7 +271,7 @@ Item
id: upperHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
- x: parent.x - width - UM.Theme.getSize("default_margin").width
+ x: parent.x - parent.width - width
anchors.verticalCenter: parent.verticalCenter
target: Qt.point(sliderRoot.width, y + height / 2)
visible: sliderRoot.activeHandle == parent
@@ -385,9 +381,9 @@ Item
id: lowerHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
- x: parent.x - width - UM.Theme.getSize("default_margin").width
+ x: parent.x - parent.width - width
anchors.verticalCenter: parent.verticalCenter
- target: Qt.point(sliderRoot.width, y + height / 2)
+ target: Qt.point(sliderRoot.width + width, y + height / 2)
visible: sliderRoot.activeHandle == parent
// custom properties
diff --git a/plugins/SimulationView/PathSlider.qml b/plugins/SimulationView/PathSlider.qml
index f3c28fb5f7..c7a43c6407 100644
--- a/plugins/SimulationView/PathSlider.qml
+++ b/plugins/SimulationView/PathSlider.qml
@@ -14,19 +14,17 @@ Item
id: sliderRoot
// handle properties
- property real handleSize: 10
+ property real handleSize: UM.Theme.getSize("slider_handle").width
property real handleRadius: handleSize / 2
- property color handleColor: "black"
- property color handleActiveColor: "white"
- property color rangeColor: "black"
+ property color handleColor: UM.Theme.getColor("slider_handle")
+ property color handleActiveColor: UM.Theme.getColor("slider_handle_active")
+ property color rangeColor: UM.Theme.getColor("slider_groove_fill")
property real handleLabelWidth: width
// track properties
- property real trackThickness: 4 // width of the slider track
- property real trackRadius: trackThickness / 2
- property color trackColor: "white"
- property real trackBorderWidth: 1 // width of the slider track border
- property color trackBorderColor: "black"
+ property real trackThickness: UM.Theme.getSize("slider_groove").width
+ property real trackRadius: UM.Theme.getSize("slider_groove_radius").width
+ property color trackColor: UM.Theme.getColor("slider_groove")
// value properties
property real maximumValue: 100
@@ -68,8 +66,6 @@ Item
radius: sliderRoot.trackRadius
anchors.centerIn: sliderRoot
color: sliderRoot.trackColor
- border.width: sliderRoot.trackBorderWidth
- border.color: sliderRoot.trackBorderColor
visible: sliderRoot.pathsVisible
}
@@ -86,9 +82,10 @@ Item
Rectangle
{
- height: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
+ height: sliderRoot.trackThickness
width: parent.width + sliderRoot.handleSize
anchors.centerIn: parent
+ radius: sliderRoot.trackRadius
color: sliderRoot.rangeColor
}
}
diff --git a/plugins/SimulationView/SimulationView.py b/plugins/SimulationView/SimulationView.py
index 0ae8b4d9e4..3b2db2efac 100644
--- a/plugins/SimulationView/SimulationView.py
+++ b/plugins/SimulationView/SimulationView.py
@@ -16,6 +16,7 @@ from UM.Mesh.MeshBuilder import MeshBuilder
from UM.Message import Message
from UM.Platform import Platform
from UM.PluginRegistry import PluginRegistry
+from UM.Qt.QtApplication import QtApplication
from UM.Resources import Resources
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
@@ -26,8 +27,8 @@ from UM.View.GL.OpenGL import OpenGL
from UM.View.GL.OpenGLContext import OpenGLContext
from UM.View.GL.ShaderProgram import ShaderProgram
-from UM.View.View import View
from UM.i18n import i18nCatalog
+from cura.CuraView import CuraView
from cura.Scene.ConvexHullNode import ConvexHullNode
from cura.CuraApplication import CuraApplication
@@ -48,15 +49,15 @@ catalog = i18nCatalog("cura")
## View used to display g-code paths.
-class SimulationView(View):
- # Must match SimulationView.qml
+class SimulationView(CuraView):
+ # Must match SimulationViewMenuComponent.qml
LAYER_VIEW_TYPE_MATERIAL_TYPE = 0
LAYER_VIEW_TYPE_LINE_TYPE = 1
LAYER_VIEW_TYPE_FEEDRATE = 2
LAYER_VIEW_TYPE_THICKNESS = 3
- def __init__(self) -> None:
- super().__init__()
+ def __init__(self, parent = None) -> None:
+ super().__init__(parent)
self._max_layers = 0
self._current_layer_num = 0
@@ -113,6 +114,16 @@ class SimulationView(View):
self._wireprint_warning_message = Message(catalog.i18nc("@info:status", "Cura does not accurately display layers when Wire Printing is enabled"),
title = catalog.i18nc("@info:title", "Simulation View"))
+ QtApplication.getInstance().engineCreatedSignal.connect(self._onEngineCreated)
+
+ def _onEngineCreated(self) -> None:
+ plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
+ if plugin_path:
+ self.addDisplayComponent("main", os.path.join(plugin_path, "SimulationViewMainComponent.qml"))
+ self.addDisplayComponent("menu", os.path.join(plugin_path, "SimulationViewMenuComponent.qml"))
+ else:
+ Logger.log("e", "Unable to find the path for %s", self.getPluginId())
+
def _evaluateCompatibilityMode(self) -> bool:
return OpenGLContext.isLegacyOpenGL() or bool(Application.getInstance().getPreferences().getValue("view/force_layer_view_compatibility_mode"))
@@ -331,12 +342,16 @@ class SimulationView(View):
return self._extruder_count
def getMinFeedrate(self) -> float:
+ if abs(self._min_feedrate - sys.float_info.max) < 10: # Some lenience due to floating point rounding.
+ return 0.0 # If it's still max-float, there are no measurements. Use 0 then.
return self._min_feedrate
def getMaxFeedrate(self) -> float:
return self._max_feedrate
def getMinThickness(self) -> float:
+ if abs(self._min_thickness - sys.float_info.max) < 10: # Some lenience due to floating point rounding.
+ return 0.0 # If it's still max-float, there are no measurements. Use 0 then.
return self._min_thickness
def getMaxThickness(self) -> float:
diff --git a/plugins/SimulationView/SimulationView.qml b/plugins/SimulationView/SimulationView.qml
deleted file mode 100644
index be124157fb..0000000000
--- a/plugins/SimulationView/SimulationView.qml
+++ /dev/null
@@ -1,808 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.4
-import QtQuick.Controls 1.2
-import QtQuick.Layouts 1.1
-import QtQuick.Controls.Styles 1.1
-
-import UM 1.0 as UM
-import Cura 1.0 as Cura
-
-Item
-{
- id: base
- width:
- {
- if (UM.SimulationView.compatibilityMode)
- {
- return UM.Theme.getSize("layerview_menu_size_compatibility").width;
- }
- else
- {
- return UM.Theme.getSize("layerview_menu_size").width;
- }
- }
- height: {
- if (viewSettings.collapsed)
- {
- if (UM.SimulationView.compatibilityMode)
- {
- return UM.Theme.getSize("layerview_menu_size_compatibility_collapsed").height;
- }
- return UM.Theme.getSize("layerview_menu_size_collapsed").height;
- }
- else if (UM.SimulationView.compatibilityMode)
- {
- return UM.Theme.getSize("layerview_menu_size_compatibility").height;
- }
- else if (UM.Preferences.getValue("layerview/layer_view_type") == 0)
- {
- return UM.Theme.getSize("layerview_menu_size_material_color_mode").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
- }
- else
- {
- return UM.Theme.getSize("layerview_menu_size").height + UM.SimulationView.extruderCount * (UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("layerview_row_spacing").height)
- }
- }
- Behavior on height { NumberAnimation { duration: 100 } }
-
- property var buttonTarget:
- {
- if(parent != null)
- {
- var force_binding = parent.y; // ensure this gets reevaluated when the panel moves
- return base.mapFromItem(parent.parent, parent.buttonTarget.x, parent.buttonTarget.y)
- }
- return Qt.point(0,0)
- }
-
- Rectangle
- {
- id: layerViewMenu
- anchors.right: parent.right
- anchors.top: parent.top
- width: parent.width
- height: parent.height
- clip: true
- z: layerSlider.z - 1
- color: UM.Theme.getColor("tool_panel_background")
- border.width: UM.Theme.getSize("default_lining").width
- border.color: UM.Theme.getColor("lining")
-
- Button {
- id: collapseButton
- anchors.top: parent.top
- anchors.topMargin: Math.round(UM.Theme.getSize("default_margin").height + (UM.Theme.getSize("layerview_row").height - UM.Theme.getSize("default_margin").height) / 2)
- anchors.right: parent.right
- anchors.rightMargin: UM.Theme.getSize("default_margin").width
-
- width: UM.Theme.getSize("standard_arrow").width
- height: UM.Theme.getSize("standard_arrow").height
-
- onClicked: viewSettings.collapsed = !viewSettings.collapsed
-
- style: ButtonStyle
- {
- background: UM.RecolorImage
- {
- width: control.width
- height: control.height
- sourceSize.width: width
- sourceSize.height: width
- color: UM.Theme.getColor("setting_control_text")
- source: viewSettings.collapsed ? UM.Theme.getIcon("arrow_left") : UM.Theme.getIcon("arrow_bottom")
- }
- label: Label{ }
- }
- }
-
- ColumnLayout
- {
- id: viewSettings
-
- property bool collapsed: false
- property var extruder_opacities: UM.Preferences.getValue("layerview/extruder_opacities").split("|")
- property bool show_travel_moves: UM.Preferences.getValue("layerview/show_travel_moves")
- property bool show_helpers: UM.Preferences.getValue("layerview/show_helpers")
- property bool show_skin: UM.Preferences.getValue("layerview/show_skin")
- property bool show_infill: UM.Preferences.getValue("layerview/show_infill")
- // if we are in compatibility mode, we only show the "line type"
- property bool show_legend: UM.SimulationView.compatibilityMode ? true : UM.Preferences.getValue("layerview/layer_view_type") == 1
- property bool show_gradient: UM.SimulationView.compatibilityMode ? false : UM.Preferences.getValue("layerview/layer_view_type") == 2 || UM.Preferences.getValue("layerview/layer_view_type") == 3
- property bool show_feedrate_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 2
- property bool show_thickness_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 3
- property bool only_show_top_layers: UM.Preferences.getValue("view/only_show_top_layers")
- property int top_layer_count: UM.Preferences.getValue("view/top_layer_count")
-
- anchors.top: parent.top
- anchors.topMargin: UM.Theme.getSize("default_margin").height
- anchors.left: parent.left
- anchors.leftMargin: UM.Theme.getSize("default_margin").width
- anchors.right: parent.right
- anchors.rightMargin: UM.Theme.getSize("default_margin").width
- spacing: UM.Theme.getSize("layerview_row_spacing").height
-
- Label
- {
- id: layerViewTypesLabel
- anchors.left: parent.left
- text: catalog.i18nc("@label","Color scheme")
- font: UM.Theme.getFont("default");
- visible: !UM.SimulationView.compatibilityMode
- Layout.fillWidth: true
- color: UM.Theme.getColor("setting_control_text")
- }
-
- ListModel // matches SimulationView.py
- {
- id: layerViewTypes
- }
-
- Component.onCompleted:
- {
- layerViewTypes.append({
- text: catalog.i18nc("@label:listbox", "Material Color"),
- type_id: 0
- })
- layerViewTypes.append({
- text: catalog.i18nc("@label:listbox", "Line Type"),
- type_id: 1
- })
- layerViewTypes.append({
- text: catalog.i18nc("@label:listbox", "Feedrate"),
- type_id: 2
- })
- layerViewTypes.append({
- text: catalog.i18nc("@label:listbox", "Layer thickness"),
- type_id: 3 // these ids match the switching in the shader
- })
- }
-
- ComboBox
- {
- id: layerTypeCombobox
- anchors.left: parent.left
- Layout.fillWidth: true
- Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
- model: layerViewTypes
- visible: !UM.SimulationView.compatibilityMode
- style: UM.Theme.styles.combobox
- anchors.right: parent.right
-
- onActivated:
- {
- UM.Preferences.setValue("layerview/layer_view_type", index);
- }
-
- Component.onCompleted:
- {
- currentIndex = UM.SimulationView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type");
- updateLegends(currentIndex);
- }
-
- function updateLegends(type_id)
- {
- // update visibility of legends
- viewSettings.show_legend = UM.SimulationView.compatibilityMode || (type_id == 1);
- viewSettings.show_gradient = !UM.SimulationView.compatibilityMode && (type_id == 2 || type_id == 3);
- viewSettings.show_feedrate_gradient = viewSettings.show_gradient && (type_id == 2);
- viewSettings.show_thickness_gradient = viewSettings.show_gradient && (type_id == 3);
- }
- }
-
- Label
- {
- id: compatibilityModeLabel
- anchors.left: parent.left
- text: catalog.i18nc("@label","Compatibility Mode")
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text")
- visible: UM.SimulationView.compatibilityMode
- Layout.fillWidth: true
- Layout.preferredHeight: UM.Theme.getSize("layerview_row").height
- Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
- }
-
- Item
- {
- height: Math.round(UM.Theme.getSize("default_margin").width / 2)
- width: width
- }
-
- Connections
- {
- target: UM.Preferences
- onPreferenceChanged:
- {
- layerTypeCombobox.currentIndex = UM.SimulationView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type");
- layerTypeCombobox.updateLegends(layerTypeCombobox.currentIndex);
- playButton.pauseSimulation();
- viewSettings.extruder_opacities = UM.Preferences.getValue("layerview/extruder_opacities").split("|");
- viewSettings.show_travel_moves = UM.Preferences.getValue("layerview/show_travel_moves");
- viewSettings.show_helpers = UM.Preferences.getValue("layerview/show_helpers");
- viewSettings.show_skin = UM.Preferences.getValue("layerview/show_skin");
- viewSettings.show_infill = UM.Preferences.getValue("layerview/show_infill");
- viewSettings.only_show_top_layers = UM.Preferences.getValue("view/only_show_top_layers");
- viewSettings.top_layer_count = UM.Preferences.getValue("view/top_layer_count");
- }
- }
-
- Repeater
- {
- model: Cura.ExtrudersModel{}
- CheckBox
- {
- id: extrudersModelCheckBox
- checked: viewSettings.extruder_opacities[index] > 0.5 || viewSettings.extruder_opacities[index] == undefined || viewSettings.extruder_opacities[index] == ""
- onClicked:
- {
- viewSettings.extruder_opacities[index] = checked ? 1.0 : 0.0
- UM.Preferences.setValue("layerview/extruder_opacities", viewSettings.extruder_opacities.join("|"));
- }
- visible: !UM.SimulationView.compatibilityMode
- enabled: index + 1 <= 4
- Rectangle
- {
- anchors.verticalCenter: parent.verticalCenter
- anchors.right: extrudersModelCheckBox.right
- width: UM.Theme.getSize("layerview_legend_size").width
- height: UM.Theme.getSize("layerview_legend_size").height
- color: model.color
- radius: Math.round(width / 2)
- border.width: UM.Theme.getSize("default_lining").width
- border.color: UM.Theme.getColor("lining")
- visible: !viewSettings.show_legend & !viewSettings.show_gradient
- }
- Layout.fillWidth: true
- Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
- Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
- style: UM.Theme.styles.checkbox
- Label
- {
- text: model.name
- elide: Text.ElideRight
- color: UM.Theme.getColor("setting_control_text")
- font: UM.Theme.getFont("default")
- anchors.verticalCenter: parent.verticalCenter
- anchors.left: extrudersModelCheckBox.left;
- anchors.right: extrudersModelCheckBox.right;
- anchors.leftMargin: UM.Theme.getSize("checkbox").width + Math.round(UM.Theme.getSize("default_margin").width/2)
- anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
- }
- }
- }
-
- Repeater
- {
- model: ListModel
- {
- id: typesLegendModel
- Component.onCompleted:
- {
- typesLegendModel.append({
- label: catalog.i18nc("@label", "Show Travels"),
- initialValue: viewSettings.show_travel_moves,
- preference: "layerview/show_travel_moves",
- colorId: "layerview_move_combing"
- });
- typesLegendModel.append({
- label: catalog.i18nc("@label", "Show Helpers"),
- initialValue: viewSettings.show_helpers,
- preference: "layerview/show_helpers",
- colorId: "layerview_support"
- });
- typesLegendModel.append({
- label: catalog.i18nc("@label", "Show Shell"),
- initialValue: viewSettings.show_skin,
- preference: "layerview/show_skin",
- colorId: "layerview_inset_0"
- });
- typesLegendModel.append({
- label: catalog.i18nc("@label", "Show Infill"),
- initialValue: viewSettings.show_infill,
- preference: "layerview/show_infill",
- colorId: "layerview_infill"
- });
- }
- }
-
- CheckBox
- {
- id: legendModelCheckBox
- checked: model.initialValue
- onClicked:
- {
- UM.Preferences.setValue(model.preference, checked);
- }
- Rectangle
- {
- anchors.verticalCenter: parent.verticalCenter
- anchors.right: legendModelCheckBox.right
- width: UM.Theme.getSize("layerview_legend_size").width
- height: UM.Theme.getSize("layerview_legend_size").height
- color: UM.Theme.getColor(model.colorId)
- border.width: UM.Theme.getSize("default_lining").width
- border.color: UM.Theme.getColor("lining")
- visible: viewSettings.show_legend
- }
- Layout.fillWidth: true
- Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
- Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
- style: UM.Theme.styles.checkbox
- Label
- {
- text: label
- font: UM.Theme.getFont("default")
- elide: Text.ElideRight
- color: UM.Theme.getColor("setting_control_text")
- anchors.verticalCenter: parent.verticalCenter
- anchors.left: legendModelCheckBox.left;
- anchors.right: legendModelCheckBox.right;
- anchors.leftMargin: UM.Theme.getSize("checkbox").width + Math.round(UM.Theme.getSize("default_margin").width/2)
- anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
- }
- }
- }
-
- CheckBox
- {
- checked: viewSettings.only_show_top_layers
- onClicked:
- {
- UM.Preferences.setValue("view/only_show_top_layers", checked ? 1.0 : 0.0);
- }
- text: catalog.i18nc("@label", "Only Show Top Layers")
- visible: UM.SimulationView.compatibilityMode
- style: UM.Theme.styles.checkbox
- }
- CheckBox
- {
- checked: viewSettings.top_layer_count == 5
- onClicked:
- {
- UM.Preferences.setValue("view/top_layer_count", checked ? 5 : 1);
- }
- text: catalog.i18nc("@label", "Show 5 Detailed Layers On Top")
- visible: UM.SimulationView.compatibilityMode
- style: UM.Theme.styles.checkbox
- }
-
- Repeater
- {
- model: ListModel
- {
- id: typesLegendModelNoCheck
- Component.onCompleted:
- {
- typesLegendModelNoCheck.append({
- label: catalog.i18nc("@label", "Top / Bottom"),
- colorId: "layerview_skin",
- });
- typesLegendModelNoCheck.append({
- label: catalog.i18nc("@label", "Inner Wall"),
- colorId: "layerview_inset_x",
- });
- }
- }
-
- Label
- {
- text: label
- visible: viewSettings.show_legend
- id: typesLegendModelLabel
- Rectangle
- {
- anchors.verticalCenter: parent.verticalCenter
- anchors.right: typesLegendModelLabel.right
- width: UM.Theme.getSize("layerview_legend_size").width
- height: UM.Theme.getSize("layerview_legend_size").height
- color: UM.Theme.getColor(model.colorId)
- border.width: UM.Theme.getSize("default_lining").width
- border.color: UM.Theme.getColor("lining")
- visible: viewSettings.show_legend
- }
- Layout.fillWidth: true
- Layout.preferredHeight: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
- Layout.preferredWidth: UM.Theme.getSize("layerview_row").width
- color: UM.Theme.getColor("setting_control_text")
- font: UM.Theme.getFont("default")
- }
- }
-
- // Text for the minimum, maximum and units for the feedrates and layer thickness
- Item
- {
- id: gradientLegend
- visible: viewSettings.show_gradient
- width: parent.width
- height: UM.Theme.getSize("layerview_row").height
- anchors
- {
- topMargin: UM.Theme.getSize("slider_layerview_margin").height
- horizontalCenter: parent.horizontalCenter
- }
-
- Label
- {
- text: minText()
- anchors.left: parent.left
- color: UM.Theme.getColor("setting_control_text")
- font: UM.Theme.getFont("default")
-
- function minText()
- {
- if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
- {
- // Feedrate selected
- if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
- {
- return parseFloat(UM.SimulationView.getMinFeedrate()).toFixed(2)
- }
- // Layer thickness selected
- if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
- {
- return parseFloat(UM.SimulationView.getMinThickness()).toFixed(2)
- }
- }
- return catalog.i18nc("@label","min")
- }
- }
-
- Label
- {
- text: unitsText()
- anchors.horizontalCenter: parent.horizontalCenter
- color: UM.Theme.getColor("setting_control_text")
- font: UM.Theme.getFont("default")
-
- function unitsText()
- {
- if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
- {
- // Feedrate selected
- if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
- {
- return "mm/s"
- }
- // Layer thickness selected
- if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
- {
- return "mm"
- }
- }
- return ""
- }
- }
-
- Label
- {
- text: maxText()
- anchors.right: parent.right
- color: UM.Theme.getColor("setting_control_text")
- font: UM.Theme.getFont("default")
-
- function maxText()
- {
- if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
- {
- // Feedrate selected
- if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
- {
- return parseFloat(UM.SimulationView.getMaxFeedrate()).toFixed(2)
- }
- // Layer thickness selected
- if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
- {
- return parseFloat(UM.SimulationView.getMaxThickness()).toFixed(2)
- }
- }
- return catalog.i18nc("@label","max")
- }
- }
- }
-
- // Gradient colors for feedrate
- Rectangle
- { // In QML 5.9 can be changed by LinearGradient
- // Invert values because then the bar is rotated 90 degrees
- id: feedrateGradient
- visible: viewSettings.show_feedrate_gradient
- anchors.left: parent.right
- height: parent.width
- width: Math.round(UM.Theme.getSize("layerview_row").height * 1.5)
- border.width: UM.Theme.getSize("default_lining").width
- border.color: UM.Theme.getColor("lining")
- transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
- gradient: Gradient
- {
- GradientStop
- {
- position: 0.000
- color: Qt.rgba(1, 0.5, 0, 1)
- }
- GradientStop
- {
- position: 0.625
- color: Qt.rgba(0.375, 0.5, 0, 1)
- }
- GradientStop
- {
- position: 0.75
- color: Qt.rgba(0.25, 1, 0, 1)
- }
- GradientStop
- {
- position: 1.0
- color: Qt.rgba(0, 0, 1, 1)
- }
- }
- }
-
- // Gradient colors for layer thickness (similar to parula colormap)
- Rectangle // In QML 5.9 can be changed by LinearGradient
- {
- // Invert values because then the bar is rotated 90 degrees
- id: thicknessGradient
- visible: viewSettings.show_thickness_gradient
- anchors.left: parent.right
- height: parent.width
- width: Math.round(UM.Theme.getSize("layerview_row").height * 1.5)
- border.width: UM.Theme.getSize("default_lining").width
- border.color: UM.Theme.getColor("lining")
- transform: Rotation {origin.x: 0; origin.y: 0; angle: 90}
- gradient: Gradient
- {
- GradientStop
- {
- position: 0.000
- color: Qt.rgba(1, 1, 0, 1)
- }
- GradientStop
- {
- position: 0.25
- color: Qt.rgba(1, 0.75, 0.25, 1)
- }
- GradientStop
- {
- position: 0.5
- color: Qt.rgba(0, 0.75, 0.5, 1)
- }
- GradientStop
- {
- position: 0.75
- color: Qt.rgba(0, 0.375, 0.75, 1)
- }
- GradientStop
- {
- position: 1.0
- color: Qt.rgba(0, 0, 0.5, 1)
- }
- }
- }
- }
- }
-
- Item
- {
- id: slidersBox
-
- width: parent.width
- visible: UM.SimulationView.layerActivity && CuraApplication.platformActivity
-
- anchors
- {
- top: parent.bottom
- topMargin: UM.Theme.getSize("slider_layerview_margin").height
- left: parent.left
- }
-
- PathSlider
- {
- id: pathSlider
-
- height: UM.Theme.getSize("slider_handle").width
- anchors.left: playButton.right
- anchors.leftMargin: UM.Theme.getSize("default_margin").width
- anchors.right: parent.right
- visible: !UM.SimulationView.compatibilityMode
-
- // custom properties
- handleValue: UM.SimulationView.currentPath
- maximumValue: UM.SimulationView.numPaths
- handleSize: UM.Theme.getSize("slider_handle").width
- trackThickness: UM.Theme.getSize("slider_groove").width
- trackColor: UM.Theme.getColor("slider_groove")
- trackBorderColor: UM.Theme.getColor("slider_groove_border")
- handleColor: UM.Theme.getColor("slider_handle")
- handleActiveColor: UM.Theme.getColor("slider_handle_active")
- rangeColor: UM.Theme.getColor("slider_groove_fill")
-
- // update values when layer data changes
- Connections
- {
- target: UM.SimulationView
- onMaxPathsChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath)
- onCurrentPathChanged:
- {
- // Only pause the simulation when the layer was changed manually, not when the simulation is running
- if (pathSlider.manuallyChanged)
- {
- playButton.pauseSimulation()
- }
- pathSlider.setHandleValue(UM.SimulationView.currentPath)
- }
- }
-
- // make sure the slider handlers show the correct value after switching views
- Component.onCompleted:
- {
- pathSlider.setHandleValue(UM.SimulationView.currentPath)
- }
- }
-
- LayerSlider
- {
- id: layerSlider
-
- width: UM.Theme.getSize("slider_handle").width
- height: UM.Theme.getSize("layerview_menu_size").height
-
- anchors
- {
- top: !UM.SimulationView.compatibilityMode ? pathSlider.bottom : parent.top
- topMargin: !UM.SimulationView.compatibilityMode ? UM.Theme.getSize("default_margin").height : 0
- right: parent.right
- rightMargin: UM.Theme.getSize("slider_layerview_margin").width
- }
-
- // custom properties
- upperValue: UM.SimulationView.currentLayer
- lowerValue: UM.SimulationView.minimumLayer
- maximumValue: UM.SimulationView.numLayers
- handleSize: UM.Theme.getSize("slider_handle").width
- trackThickness: UM.Theme.getSize("slider_groove").width
- trackColor: UM.Theme.getColor("slider_groove")
- trackBorderColor: UM.Theme.getColor("slider_groove_border")
- upperHandleColor: UM.Theme.getColor("slider_handle")
- lowerHandleColor: UM.Theme.getColor("slider_handle")
- rangeHandleColor: UM.Theme.getColor("slider_groove_fill")
- handleActiveColor: UM.Theme.getColor("slider_handle_active")
- handleLabelWidth: UM.Theme.getSize("slider_layerview_background").width
-
- // update values when layer data changes
- Connections
- {
- target: UM.SimulationView
- onMaxLayersChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer)
- onMinimumLayerChanged: layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
- onCurrentLayerChanged:
- {
- // Only pause the simulation when the layer was changed manually, not when the simulation is running
- if (layerSlider.manuallyChanged)
- {
- playButton.pauseSimulation()
- }
- layerSlider.setUpperValue(UM.SimulationView.currentLayer)
- }
- }
-
- // make sure the slider handlers show the correct value after switching views
- Component.onCompleted:
- {
- layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
- layerSlider.setUpperValue(UM.SimulationView.currentLayer)
- }
- }
-
- // Play simulation button
- Button
- {
- id: playButton
- iconSource: "./resources/simulation_resume.svg"
- style: UM.Theme.styles.small_tool_button
- visible: !UM.SimulationView.compatibilityMode
- anchors
- {
- verticalCenter: pathSlider.verticalCenter
- }
-
- property var status: 0 // indicates if it's stopped (0) or playing (1)
-
- onClicked:
- {
- switch(status)
- {
- case 0:
- {
- resumeSimulation()
- break
- }
- case 1:
- {
- pauseSimulation()
- break
- }
- }
- }
-
- function pauseSimulation()
- {
- UM.SimulationView.setSimulationRunning(false)
- iconSource = "./resources/simulation_resume.svg"
- simulationTimer.stop()
- status = 0
- layerSlider.manuallyChanged = true
- pathSlider.manuallyChanged = true
- }
-
- function resumeSimulation()
- {
- UM.SimulationView.setSimulationRunning(true)
- iconSource = "./resources/simulation_pause.svg"
- simulationTimer.start()
- layerSlider.manuallyChanged = false
- pathSlider.manuallyChanged = false
- }
- }
-
- Timer
- {
- id: simulationTimer
- interval: 100
- running: false
- repeat: true
- onTriggered:
- {
- var currentPath = UM.SimulationView.currentPath
- var numPaths = UM.SimulationView.numPaths
- var currentLayer = UM.SimulationView.currentLayer
- var numLayers = UM.SimulationView.numLayers
- // When the user plays the simulation, if the path slider is at the end of this layer, we start
- // the simulation at the beginning of the current layer.
- if (playButton.status == 0)
- {
- if (currentPath >= numPaths)
- {
- UM.SimulationView.setCurrentPath(0)
- }
- else
- {
- UM.SimulationView.setCurrentPath(currentPath+1)
- }
- }
- // If the simulation is already playing and we reach the end of a layer, then it automatically
- // starts at the beginning of the next layer.
- else
- {
- if (currentPath >= numPaths)
- {
- // At the end of the model, the simulation stops
- if (currentLayer >= numLayers)
- {
- playButton.pauseSimulation()
- }
- else
- {
- UM.SimulationView.setCurrentLayer(currentLayer+1)
- UM.SimulationView.setCurrentPath(0)
- }
- }
- else
- {
- UM.SimulationView.setCurrentPath(currentPath+1)
- }
- }
- // The status must be set here instead of in the resumeSimulation function otherwise it won't work
- // correctly, because part of the logic is in this trigger function.
- playButton.status = 1
- }
- }
- }
-
- FontMetrics
- {
- id: fontMetrics
- font: UM.Theme.getFont("default")
- }
-}
diff --git a/plugins/SimulationView/SimulationViewMainComponent.qml b/plugins/SimulationView/SimulationViewMainComponent.qml
new file mode 100644
index 0000000000..16b049c921
--- /dev/null
+++ b/plugins/SimulationView/SimulationViewMainComponent.qml
@@ -0,0 +1,211 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.4
+import QtQuick.Controls 1.2
+import QtQuick.Layouts 1.1
+import QtQuick.Controls.Styles 1.1
+
+import UM 1.4 as UM
+import Cura 1.0 as Cura
+
+Item
+{
+ property bool is_simulation_playing: false
+ visible: UM.SimulationView.layerActivity && CuraApplication.platformActivity
+
+ PathSlider
+ {
+ id: pathSlider
+ height: UM.Theme.getSize("slider_handle").width
+ width: UM.Theme.getSize("slider_layerview_size").height
+
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: UM.Theme.getSize("default_margin").height
+
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ visible: !UM.SimulationView.compatibilityMode
+
+ // Custom properties
+ handleValue: UM.SimulationView.currentPath
+ maximumValue: UM.SimulationView.numPaths
+
+ // Update values when layer data changes.
+ Connections
+ {
+ target: UM.SimulationView
+ onMaxPathsChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath)
+ onCurrentPathChanged:
+ {
+ // Only pause the simulation when the layer was changed manually, not when the simulation is running
+ if (pathSlider.manuallyChanged)
+ {
+ playButton.pauseSimulation()
+ }
+ pathSlider.setHandleValue(UM.SimulationView.currentPath)
+ }
+ }
+
+ // Ensure that the slider handlers show the correct value after switching views.
+ Component.onCompleted:
+ {
+ pathSlider.setHandleValue(UM.SimulationView.currentPath)
+ }
+
+ }
+
+ UM.SimpleButton
+ {
+ id: playButton
+ iconSource: !is_simulation_playing ? "./resources/simulation_resume.svg": "./resources/simulation_pause.svg"
+ width: UM.Theme.getSize("small_button").width
+ height: UM.Theme.getSize("small_button").height
+ hoverColor: UM.Theme.getColor("slider_handle_active")
+ color: UM.Theme.getColor("slider_handle")
+ iconMargin: UM.Theme.getSize("thick_lining").width
+ visible: !UM.SimulationView.compatibilityMode
+
+ Connections
+ {
+ target: UM.Preferences
+ onPreferenceChanged:
+ {
+ playButton.pauseSimulation()
+ }
+ }
+
+ anchors
+ {
+ right: pathSlider.left
+ verticalCenter: pathSlider.verticalCenter
+ }
+
+ onClicked:
+ {
+ if(is_simulation_playing)
+ {
+ pauseSimulation()
+ }
+ else
+ {
+ resumeSimulation()
+ }
+ }
+
+ function pauseSimulation()
+ {
+ UM.SimulationView.setSimulationRunning(false)
+ simulationTimer.stop()
+ is_simulation_playing = false
+ layerSlider.manuallyChanged = true
+ pathSlider.manuallyChanged = true
+ }
+
+ function resumeSimulation()
+ {
+ UM.SimulationView.setSimulationRunning(true)
+ simulationTimer.start()
+ layerSlider.manuallyChanged = false
+ pathSlider.manuallyChanged = false
+ }
+ }
+
+ Timer
+ {
+ id: simulationTimer
+ interval: 100
+ running: false
+ repeat: true
+ onTriggered:
+ {
+ var currentPath = UM.SimulationView.currentPath
+ var numPaths = UM.SimulationView.numPaths
+ var currentLayer = UM.SimulationView.currentLayer
+ var numLayers = UM.SimulationView.numLayers
+
+ // When the user plays the simulation, if the path slider is at the end of this layer, we start
+ // the simulation at the beginning of the current layer.
+ if (!is_simulation_playing)
+ {
+ if (currentPath >= numPaths)
+ {
+ UM.SimulationView.setCurrentPath(0)
+ }
+ else
+ {
+ UM.SimulationView.setCurrentPath(currentPath + 1)
+ }
+ }
+ // If the simulation is already playing and we reach the end of a layer, then it automatically
+ // starts at the beginning of the next layer.
+ else
+ {
+ if (currentPath >= numPaths)
+ {
+ // At the end of the model, the simulation stops
+ if (currentLayer >= numLayers)
+ {
+ playButton.pauseSimulation()
+ }
+ else
+ {
+ UM.SimulationView.setCurrentLayer(currentLayer + 1)
+ UM.SimulationView.setCurrentPath(0)
+ }
+ }
+ else
+ {
+ UM.SimulationView.setCurrentPath(currentPath + 1)
+ }
+ }
+ // The status must be set here instead of in the resumeSimulation function otherwise it won't work
+ // correctly, because part of the logic is in this trigger function.
+ is_simulation_playing = true
+ }
+ }
+
+ LayerSlider
+ {
+ id: layerSlider
+
+ width: UM.Theme.getSize("slider_handle").width
+ height: UM.Theme.getSize("slider_layerview_size").height
+
+ anchors
+ {
+ right: parent.right
+ verticalCenter: parent.verticalCenter
+ rightMargin: UM.Theme.getSize("default_margin").width
+ }
+
+ // Custom properties
+ upperValue: UM.SimulationView.currentLayer
+ lowerValue: UM.SimulationView.minimumLayer
+ maximumValue: UM.SimulationView.numLayers
+
+ // Update values when layer data changes
+ Connections
+ {
+ target: UM.SimulationView
+ onMaxLayersChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer)
+ onMinimumLayerChanged: layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
+ onCurrentLayerChanged:
+ {
+ // Only pause the simulation when the layer was changed manually, not when the simulation is running
+ if (layerSlider.manuallyChanged)
+ {
+ playButton.pauseSimulation()
+ }
+ layerSlider.setUpperValue(UM.SimulationView.currentLayer)
+ }
+ }
+
+ // Make sure the slider handlers show the correct value after switching views
+ Component.onCompleted:
+ {
+ layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
+ layerSlider.setUpperValue(UM.SimulationView.currentLayer)
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/SimulationView/SimulationViewMenuComponent.qml b/plugins/SimulationView/SimulationViewMenuComponent.qml
new file mode 100644
index 0000000000..4c952d4c43
--- /dev/null
+++ b/plugins/SimulationView/SimulationViewMenuComponent.qml
@@ -0,0 +1,551 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.4
+import QtQuick.Controls 1.2
+import QtQuick.Layouts 1.1
+import QtQuick.Controls.Styles 1.1
+import QtGraphicalEffects 1.0
+
+import UM 1.0 as UM
+import Cura 1.0 as Cura
+
+
+Cura.ExpandableComponent
+{
+ id: base
+
+ contentHeaderTitle: catalog.i18nc("@label", "Color scheme")
+
+ Connections
+ {
+ target: UM.Preferences
+ onPreferenceChanged:
+ {
+ layerTypeCombobox.currentIndex = UM.SimulationView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type")
+ layerTypeCombobox.updateLegends(layerTypeCombobox.currentIndex)
+ viewSettings.extruder_opacities = UM.Preferences.getValue("layerview/extruder_opacities").split("|")
+ viewSettings.show_travel_moves = UM.Preferences.getValue("layerview/show_travel_moves")
+ viewSettings.show_helpers = UM.Preferences.getValue("layerview/show_helpers")
+ viewSettings.show_skin = UM.Preferences.getValue("layerview/show_skin")
+ viewSettings.show_infill = UM.Preferences.getValue("layerview/show_infill")
+ viewSettings.only_show_top_layers = UM.Preferences.getValue("view/only_show_top_layers")
+ viewSettings.top_layer_count = UM.Preferences.getValue("view/top_layer_count")
+ }
+ }
+
+ headerItem: Item
+ {
+ Label
+ {
+ id: colorSchemeLabel
+ text: catalog.i18nc("@label", "Color scheme")
+ verticalAlignment: Text.AlignVCenter
+ height: parent.height
+ elide: Text.ElideRight
+ font: UM.Theme.getFont("medium")
+ color: UM.Theme.getColor("text_medium")
+ renderType: Text.NativeRendering
+ }
+
+ Label
+ {
+ text: layerTypeCombobox.currentText
+ verticalAlignment: Text.AlignVCenter
+ anchors
+ {
+ left: colorSchemeLabel.right
+ leftMargin: UM.Theme.getSize("default_margin").width
+ right: parent.right
+ }
+ height: parent.height
+ elide: Text.ElideRight
+ font: UM.Theme.getFont("medium")
+ color: UM.Theme.getColor("text")
+ renderType: Text.NativeRendering
+ }
+ }
+
+ contentItem: Column
+ {
+ id: viewSettings
+
+ property var extruder_opacities: UM.Preferences.getValue("layerview/extruder_opacities").split("|")
+ property bool show_travel_moves: UM.Preferences.getValue("layerview/show_travel_moves")
+ property bool show_helpers: UM.Preferences.getValue("layerview/show_helpers")
+ property bool show_skin: UM.Preferences.getValue("layerview/show_skin")
+ property bool show_infill: UM.Preferences.getValue("layerview/show_infill")
+
+ // If we are in compatibility mode, we only show the "line type"
+ property bool show_legend: UM.SimulationView.compatibilityMode ? true : UM.Preferences.getValue("layerview/layer_view_type") == 1
+ property bool show_gradient: UM.SimulationView.compatibilityMode ? false : UM.Preferences.getValue("layerview/layer_view_type") == 2 || UM.Preferences.getValue("layerview/layer_view_type") == 3
+ property bool show_feedrate_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 2
+ property bool show_thickness_gradient: show_gradient && UM.Preferences.getValue("layerview/layer_view_type") == 3
+ property bool only_show_top_layers: UM.Preferences.getValue("view/only_show_top_layers")
+ property int top_layer_count: UM.Preferences.getValue("view/top_layer_count")
+
+ width: UM.Theme.getSize("layerview_menu_size").width - 2 * UM.Theme.getSize("default_margin").width
+ height: implicitHeight
+
+ spacing: UM.Theme.getSize("layerview_row_spacing").height
+
+ ListModel // matches SimulationView.py
+ {
+ id: layerViewTypes
+ }
+
+ Component.onCompleted:
+ {
+ layerViewTypes.append({
+ text: catalog.i18nc("@label:listbox", "Material Color"),
+ type_id: 0
+ })
+ layerViewTypes.append({
+ text: catalog.i18nc("@label:listbox", "Line Type"),
+ type_id: 1
+ })
+ layerViewTypes.append({
+ text: catalog.i18nc("@label:listbox", "Feedrate"),
+ type_id: 2
+ })
+ layerViewTypes.append({
+ text: catalog.i18nc("@label:listbox", "Layer thickness"),
+ type_id: 3 // these ids match the switching in the shader
+ })
+ }
+
+ ComboBox
+ {
+ id: layerTypeCombobox
+ width: parent.width
+ model: layerViewTypes
+ visible: !UM.SimulationView.compatibilityMode
+ style: UM.Theme.styles.combobox
+
+ onActivated:
+ {
+ UM.Preferences.setValue("layerview/layer_view_type", index);
+ }
+
+ Component.onCompleted:
+ {
+ currentIndex = UM.SimulationView.compatibilityMode ? 1 : UM.Preferences.getValue("layerview/layer_view_type");
+ updateLegends(currentIndex);
+ }
+
+ function updateLegends(type_id)
+ {
+ // Update the visibility of the legends.
+ viewSettings.show_legend = UM.SimulationView.compatibilityMode || (type_id == 1);
+ viewSettings.show_gradient = !UM.SimulationView.compatibilityMode && (type_id == 2 || type_id == 3);
+ viewSettings.show_feedrate_gradient = viewSettings.show_gradient && (type_id == 2);
+ viewSettings.show_thickness_gradient = viewSettings.show_gradient && (type_id == 3);
+ }
+ }
+
+ Label
+ {
+ id: compatibilityModeLabel
+ text: catalog.i18nc("@label", "Compatibility Mode")
+ font: UM.Theme.getFont("default")
+ color: UM.Theme.getColor("text")
+ visible: UM.SimulationView.compatibilityMode
+ height: UM.Theme.getSize("layerview_row").height
+ width: parent.width
+ renderType: Text.NativeRendering
+ }
+
+ Item // Spacer
+ {
+ height: UM.Theme.getSize("narrow_margin").width
+ width: width
+ }
+
+ Repeater
+ {
+ model: CuraApplication.getExtrudersModel()
+
+ CheckBox
+ {
+ id: extrudersModelCheckBox
+ checked: viewSettings.extruder_opacities[index] > 0.5 || viewSettings.extruder_opacities[index] == undefined || viewSettings.extruder_opacities[index] == ""
+ height: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
+ width: parent.width
+ visible: !UM.SimulationView.compatibilityMode
+ enabled: index < 4
+
+ onClicked:
+ {
+ viewSettings.extruder_opacities[index] = checked ? 1.0 : 0.0
+ UM.Preferences.setValue("layerview/extruder_opacities", viewSettings.extruder_opacities.join("|"));
+ }
+
+ style: UM.Theme.styles.checkbox
+
+
+ UM.RecolorImage
+ {
+ id: swatch
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: extrudersModelCheckBox.right
+ width: UM.Theme.getSize("layerview_legend_size").width
+ height: UM.Theme.getSize("layerview_legend_size").height
+ source: UM.Theme.getIcon("extruder_button")
+ color: model.color
+ }
+
+ Label
+ {
+ text: model.name
+ elide: Text.ElideRight
+ color: UM.Theme.getColor("setting_control_text")
+ font: UM.Theme.getFont("default")
+ anchors
+ {
+ verticalCenter: parent.verticalCenter
+ left: extrudersModelCheckBox.left
+ right: extrudersModelCheckBox.right
+ leftMargin: UM.Theme.getSize("checkbox").width + Math.round(UM.Theme.getSize("default_margin").width / 2)
+ rightMargin: UM.Theme.getSize("default_margin").width * 2
+ }
+ renderType: Text.NativeRendering
+ }
+ }
+ }
+
+ Repeater
+ {
+ model: ListModel
+ {
+ id: typesLegendModel
+ Component.onCompleted:
+ {
+ typesLegendModel.append({
+ label: catalog.i18nc("@label", "Travels"),
+ initialValue: viewSettings.show_travel_moves,
+ preference: "layerview/show_travel_moves",
+ colorId: "layerview_move_combing"
+ });
+ typesLegendModel.append({
+ label: catalog.i18nc("@label", "Helpers"),
+ initialValue: viewSettings.show_helpers,
+ preference: "layerview/show_helpers",
+ colorId: "layerview_support"
+ });
+ typesLegendModel.append({
+ label: catalog.i18nc("@label", "Shell"),
+ initialValue: viewSettings.show_skin,
+ preference: "layerview/show_skin",
+ colorId: "layerview_inset_0"
+ });
+ typesLegendModel.append({
+ label: catalog.i18nc("@label", "Infill"),
+ initialValue: viewSettings.show_infill,
+ preference: "layerview/show_infill",
+ colorId: "layerview_infill"
+ });
+ }
+ }
+
+ CheckBox
+ {
+ id: legendModelCheckBox
+ checked: model.initialValue
+ onClicked: UM.Preferences.setValue(model.preference, checked)
+ height: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
+ width: parent.width
+
+ style: UM.Theme.styles.checkbox
+
+ Rectangle
+ {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: legendModelCheckBox.right
+ width: UM.Theme.getSize("layerview_legend_size").width
+ height: UM.Theme.getSize("layerview_legend_size").height
+ color: UM.Theme.getColor(model.colorId)
+ border.width: UM.Theme.getSize("default_lining").width
+ border.color: UM.Theme.getColor("lining")
+ visible: viewSettings.show_legend
+ }
+
+ Label
+ {
+ text: label
+ font: UM.Theme.getFont("default")
+ elide: Text.ElideRight
+ renderType: Text.NativeRendering
+ color: UM.Theme.getColor("setting_control_text")
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.left: legendModelCheckBox.left
+ anchors.right: legendModelCheckBox.right
+ anchors.leftMargin: UM.Theme.getSize("checkbox").width + Math.round(UM.Theme.getSize("default_margin").width / 2)
+ anchors.rightMargin: UM.Theme.getSize("default_margin").width * 2
+ }
+ }
+ }
+
+ CheckBox
+ {
+ checked: viewSettings.only_show_top_layers
+ onClicked: UM.Preferences.setValue("view/only_show_top_layers", checked ? 1.0 : 0.0)
+ text: catalog.i18nc("@label", "Only Show Top Layers")
+ visible: UM.SimulationView.compatibilityMode
+ style: UM.Theme.styles.checkbox
+ width: parent.width
+ }
+
+ CheckBox
+ {
+ checked: viewSettings.top_layer_count == 5
+ onClicked: UM.Preferences.setValue("view/top_layer_count", checked ? 5 : 1)
+ text: catalog.i18nc("@label", "Show 5 Detailed Layers On Top")
+ width: parent.width
+ visible: UM.SimulationView.compatibilityMode
+ style: UM.Theme.styles.checkbox
+ }
+
+ Repeater
+ {
+ model: ListModel
+ {
+ id: typesLegendModelNoCheck
+ Component.onCompleted:
+ {
+ typesLegendModelNoCheck.append({
+ label: catalog.i18nc("@label", "Top / Bottom"),
+ colorId: "layerview_skin",
+ });
+ typesLegendModelNoCheck.append({
+ label: catalog.i18nc("@label", "Inner Wall"),
+ colorId: "layerview_inset_x",
+ });
+ }
+ }
+
+ Label
+ {
+ text: label
+ visible: viewSettings.show_legend
+ id: typesLegendModelLabel
+
+ height: UM.Theme.getSize("layerview_row").height + UM.Theme.getSize("default_lining").height
+ width: parent.width
+ color: UM.Theme.getColor("setting_control_text")
+ font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
+ Rectangle
+ {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: typesLegendModelLabel.right
+
+ width: UM.Theme.getSize("layerview_legend_size").width
+ height: UM.Theme.getSize("layerview_legend_size").height
+
+ color: UM.Theme.getColor(model.colorId)
+
+ border.width: UM.Theme.getSize("default_lining").width
+ border.color: UM.Theme.getColor("lining")
+ }
+ }
+ }
+
+ // Text for the minimum, maximum and units for the feedrates and layer thickness
+ Item
+ {
+ id: gradientLegend
+ visible: viewSettings.show_gradient
+ width: parent.width
+ height: UM.Theme.getSize("layerview_row").height
+
+ Label //Minimum value.
+ {
+ text:
+ {
+ if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
+ {
+ // Feedrate selected
+ if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
+ {
+ return parseFloat(UM.SimulationView.getMinFeedrate()).toFixed(2)
+ }
+ // Layer thickness selected
+ if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
+ {
+ return parseFloat(UM.SimulationView.getMinThickness()).toFixed(2)
+ }
+ }
+ return catalog.i18nc("@label","min")
+ }
+ anchors.left: parent.left
+ color: UM.Theme.getColor("setting_control_text")
+ font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
+ }
+
+ Label //Unit in the middle.
+ {
+ text:
+ {
+ if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
+ {
+ // Feedrate selected
+ if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
+ {
+ return "mm/s"
+ }
+ // Layer thickness selected
+ if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
+ {
+ return "mm"
+ }
+ }
+ return ""
+ }
+
+ anchors.horizontalCenter: parent.horizontalCenter
+ color: UM.Theme.getColor("setting_control_text")
+ font: UM.Theme.getFont("default")
+ }
+
+ Label //Maximum value.
+ {
+ text: {
+ if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
+ {
+ // Feedrate selected
+ if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
+ {
+ return parseFloat(UM.SimulationView.getMaxFeedrate()).toFixed(2)
+ }
+ // Layer thickness selected
+ if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
+ {
+ return parseFloat(UM.SimulationView.getMaxThickness()).toFixed(2)
+ }
+ }
+ return catalog.i18nc("@label","max")
+ }
+
+ anchors.right: parent.right
+ color: UM.Theme.getColor("setting_control_text")
+ font: UM.Theme.getFont("default")
+ }
+ }
+
+ // Gradient colors for feedrate
+ Rectangle
+ {
+ id: feedrateGradient
+ visible: viewSettings.show_feedrate_gradient
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: Math.round(UM.Theme.getSize("layerview_row").height * 1.5)
+ border.width: UM.Theme.getSize("default_lining").width
+ border.color: UM.Theme.getColor("lining")
+
+ LinearGradient
+ {
+ anchors
+ {
+ left: parent.left
+ leftMargin: UM.Theme.getSize("default_lining").width
+ right: parent.right
+ rightMargin: UM.Theme.getSize("default_lining").width
+ top: parent.top
+ topMargin: UM.Theme.getSize("default_lining").width
+ bottom: parent.bottom
+ bottomMargin: UM.Theme.getSize("default_lining").width
+ }
+ start: Qt.point(0, 0)
+ end: Qt.point(parent.width, 0)
+ gradient: Gradient
+ {
+ GradientStop
+ {
+ position: 0.000
+ color: Qt.rgba(0, 0, 1, 1)
+ }
+ GradientStop
+ {
+ position: 0.25
+ color: Qt.rgba(0.25, 1, 0, 1)
+ }
+ GradientStop
+ {
+ position: 0.375
+ color: Qt.rgba(0.375, 0.5, 0, 1)
+ }
+ GradientStop
+ {
+ position: 1.0
+ color: Qt.rgba(1, 0.5, 0, 1)
+ }
+ }
+ }
+ }
+
+ // Gradient colors for layer thickness (similar to parula colormap)
+ Rectangle
+ {
+ id: thicknessGradient
+ visible: viewSettings.show_thickness_gradient
+ anchors.left: parent.left
+ anchors.right: parent.right
+ height: Math.round(UM.Theme.getSize("layerview_row").height * 1.5)
+ border.width: UM.Theme.getSize("default_lining").width
+ border.color: UM.Theme.getColor("lining")
+
+ LinearGradient
+ {
+ anchors
+ {
+ left: parent.left
+ leftMargin: UM.Theme.getSize("default_lining").width
+ right: parent.right
+ rightMargin: UM.Theme.getSize("default_lining").width
+ top: parent.top
+ topMargin: UM.Theme.getSize("default_lining").width
+ bottom: parent.bottom
+ bottomMargin: UM.Theme.getSize("default_lining").width
+ }
+ start: Qt.point(0, 0)
+ end: Qt.point(parent.width, 0)
+ gradient: Gradient
+ {
+ GradientStop
+ {
+ position: 0.000
+ color: Qt.rgba(0, 0, 0.5, 1)
+ }
+ GradientStop
+ {
+ position: 0.25
+ color: Qt.rgba(0, 0.375, 0.75, 1)
+ }
+ GradientStop
+ {
+ position: 0.5
+ color: Qt.rgba(0, 0.75, 0.5, 1)
+ }
+ GradientStop
+ {
+ position: 0.75
+ color: Qt.rgba(1, 0.75, 0.25, 1)
+ }
+ GradientStop
+ {
+ position: 1.0
+ color: Qt.rgba(1, 1, 0, 1)
+ }
+ }
+ }
+ }
+ }
+
+ FontMetrics
+ {
+ id: fontMetrics
+ font: UM.Theme.getFont("default")
+ }
+}
diff --git a/plugins/SimulationView/__init__.py b/plugins/SimulationView/__init__.py
index 360fdc1de9..420ee60660 100644
--- a/plugins/SimulationView/__init__.py
+++ b/plugins/SimulationView/__init__.py
@@ -8,19 +8,21 @@ from . import SimulationViewProxy, SimulationView
catalog = i18nCatalog("cura")
+
def getMetaData():
return {
"view": {
"name": catalog.i18nc("@item:inlistbox", "Layer view"),
- "view_panel": "SimulationView.qml",
- "weight": 2
+ "weight": 0
}
}
+
def createSimulationViewProxy(engine, script_engine):
return SimulationViewProxy.SimulationViewProxy()
+
def register(app):
simulation_view = SimulationView.SimulationView()
qmlRegisterSingletonType(SimulationViewProxy.SimulationViewProxy, "UM", 1, 0, "SimulationView", simulation_view.getProxy)
- return { "view": SimulationView.SimulationView()}
+ return { "view": simulation_view}
diff --git a/plugins/SimulationView/layers.shader b/plugins/SimulationView/layers.shader
index 30f23a3189..69c7c61ee5 100644
--- a/plugins/SimulationView/layers.shader
+++ b/plugins/SimulationView/layers.shader
@@ -49,12 +49,13 @@ fragment =
// discard movements
discard;
}
- // support: 4, 5, 7, 10
+ // support: 4, 5, 7, 10, 11 (prime tower)
if ((u_show_helpers == 0) && (
((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
+ ((v_line_type >= 4.5) && (v_line_type <= 5.5)) ||
((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
- ((v_line_type >= 4.5) && (v_line_type <= 5.5))
+ ((v_line_type >= 10.5) && (v_line_type <= 11.5))
)) {
discard;
}
diff --git a/plugins/SimulationView/layers3d.shader b/plugins/SimulationView/layers3d.shader
index de2b9335d8..a277606509 100644
--- a/plugins/SimulationView/layers3d.shader
+++ b/plugins/SimulationView/layers3d.shader
@@ -154,7 +154,7 @@ geometry41core =
if ((u_show_travel_moves == 0) && ((v_line_type[0] == 8) || (v_line_type[0] == 9))) {
return;
}
- if ((u_show_helpers == 0) && ((v_line_type[0] == 4) || (v_line_type[0] == 5) || (v_line_type[0] == 7) || (v_line_type[0] == 10))) {
+ if ((u_show_helpers == 0) && ((v_line_type[0] == 4) || (v_line_type[0] == 5) || (v_line_type[0] == 7) || (v_line_type[0] == 10) || v_line_type[0] == 11)) {
return;
}
if ((u_show_skin == 0) && ((v_line_type[0] == 1) || (v_line_type[0] == 2) || (v_line_type[0] == 3))) {
diff --git a/plugins/SimulationView/layers_shadow.shader b/plugins/SimulationView/layers_shadow.shader
index 7ceccff21e..6149cc1703 100644
--- a/plugins/SimulationView/layers_shadow.shader
+++ b/plugins/SimulationView/layers_shadow.shader
@@ -45,19 +45,23 @@ fragment =
void main()
{
- if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5)) { // actually, 8 and 9
+ if ((u_show_travel_moves == 0) && (v_line_type >= 7.5) && (v_line_type <= 9.5))
+ { // actually, 8 and 9
// discard movements
discard;
}
- // support: 4, 5, 7, 10
+ // support: 4, 5, 7, 10, 11
if ((u_show_helpers == 0) && (
((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
- ((v_line_type >= 4.5) && (v_line_type <= 5.5))
- )) {
+ ((v_line_type >= 4.5) && (v_line_type <= 5.5)) ||
+ ((v_line_type >= 10.5) && (v_line_type <= 11.5))
+ ))
+ {
discard;
}
+
// skin: 1, 2, 3
if ((u_show_skin == 0) && (
(v_line_type >= 0.5) && (v_line_type <= 3.5)
@@ -65,7 +69,8 @@ fragment =
discard;
}
// infill:
- if ((u_show_infill == 0) && (v_line_type >= 5.5) && (v_line_type <= 6.5)) {
+ if ((u_show_infill == 0) && (v_line_type >= 5.5) && (v_line_type <= 6.5))
+ {
// discard movements
discard;
}
@@ -117,12 +122,13 @@ fragment41core =
// discard movements
discard;
}
- // helpers: 4, 5, 7, 10
+ // helpers: 4, 5, 7, 10, 11
if ((u_show_helpers == 0) && (
((v_line_type >= 3.5) && (v_line_type <= 4.5)) ||
((v_line_type >= 6.5) && (v_line_type <= 7.5)) ||
((v_line_type >= 9.5) && (v_line_type <= 10.5)) ||
- ((v_line_type >= 4.5) && (v_line_type <= 5.5))
+ ((v_line_type >= 4.5) && (v_line_type <= 5.5)) ||
+ ((v_line_type >= 10.5) && (v_line_type <= 11.5))
)) {
discard;
}
diff --git a/plugins/SimulationView/plugin.json b/plugins/SimulationView/plugin.json
index 93df98068f..3ccf91b9e6 100644
--- a/plugins/SimulationView/plugin.json
+++ b/plugins/SimulationView/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Simulation View",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides the Simulation view.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/SliceInfoPlugin/plugin.json b/plugins/SliceInfoPlugin/plugin.json
index 939e5ff235..8ff0e194fb 100644
--- a/plugins/SliceInfoPlugin/plugin.json
+++ b/plugins/SliceInfoPlugin/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Slice info",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Submits anonymous slice info. Can be disabled through preferences.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/SolidView/SolidView.py b/plugins/SolidView/SolidView.py
index b9ad5c8829..797d6dabec 100644
--- a/plugins/SolidView/SolidView.py
+++ b/plugins/SolidView/SolidView.py
@@ -12,7 +12,6 @@ from UM.Math.Color import Color
from UM.View.GL.OpenGL import OpenGL
from cura.Settings.ExtruderManager import ExtruderManager
-from cura.Settings.ExtrudersModel import ExtrudersModel
import math
@@ -29,13 +28,16 @@ class SolidView(View):
self._non_printing_shader = None
self._support_mesh_shader = None
- self._extruders_model = ExtrudersModel()
+ self._extruders_model = None
self._theme = None
def beginRendering(self):
scene = self.getController().getScene()
renderer = self.getRenderer()
+ if not self._extruders_model:
+ self._extruders_model = Application.getInstance().getExtrudersModel()
+
if not self._theme:
self._theme = Application.getInstance().getTheme()
diff --git a/plugins/SolidView/__init__.py b/plugins/SolidView/__init__.py
index db2e48f489..34148fcf05 100644
--- a/plugins/SolidView/__init__.py
+++ b/plugins/SolidView/__init__.py
@@ -10,7 +10,8 @@ def getMetaData():
return {
"view": {
"name": i18n_catalog.i18nc("@item:inmenu", "Solid view"),
- "weight": 0
+ "weight": 0,
+ "visible": False
}
}
diff --git a/plugins/SolidView/plugin.json b/plugins/SolidView/plugin.json
index e70ec224dd..b3f62221c5 100644
--- a/plugins/SolidView/plugin.json
+++ b/plugins/SolidView/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Solid View",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides a normal solid mesh view.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
\ No newline at end of file
diff --git a/plugins/SupportEraser/plugin.json b/plugins/SupportEraser/plugin.json
index 7af35e0fb5..fa6d6d230e 100644
--- a/plugins/SupportEraser/plugin.json
+++ b/plugins/SupportEraser/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Support Eraser",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Creates an eraser mesh to block the printing of support in certain places",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/Toolbox/plugin.json b/plugins/Toolbox/plugin.json
index 2557185524..61dc0429f5 100644
--- a/plugins/Toolbox/plugin.json
+++ b/plugins/Toolbox/plugin.json
@@ -1,7 +1,7 @@
{
"name": "Toolbox",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
- "api": 5,
+ "version": "1.0.1",
+ "api": "6.0",
"description": "Find, manage and install new Cura packages."
}
diff --git a/plugins/Toolbox/resources/qml/RatingWidget.qml b/plugins/Toolbox/resources/qml/RatingWidget.qml
new file mode 100644
index 0000000000..441cf238f7
--- /dev/null
+++ b/plugins/Toolbox/resources/qml/RatingWidget.qml
@@ -0,0 +1,106 @@
+import QtQuick 2.7
+import QtQuick.Controls 2.1
+import UM 1.0 as UM
+import Cura 1.1 as Cura
+Item
+{
+ id: ratingWidget
+
+ property real rating: 0
+ property int indexHovered: -1
+ property string packageId: ""
+
+ property int userRating: 0
+ property bool canRate: false
+
+ signal rated(int rating)
+
+ width: contentRow.width
+ height: contentRow.height
+ MouseArea
+ {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: ratingWidget.canRate
+ acceptedButtons: Qt.NoButton
+ onExited:
+ {
+ if(ratingWidget.canRate)
+ {
+ ratingWidget.indexHovered = -1
+ }
+ }
+
+ Row
+ {
+ id: contentRow
+ height: childrenRect.height
+ Repeater
+ {
+ model: 5 // We need to get 5 stars
+ Button
+ {
+ id: control
+ hoverEnabled: true
+ onHoveredChanged:
+ {
+ if(hovered && ratingWidget.canRate)
+ {
+ indexHovered = index
+ }
+ }
+
+ ToolTip.visible: control.hovered && !ratingWidget.canRate
+ ToolTip.text: !Cura.API.account.isLoggedIn ? catalog.i18nc("@label", "You need to login first before you can rate"): catalog.i18nc("@label", "You need to install the package before you can rate")
+
+ property bool isStarFilled:
+ {
+ // If the entire widget is hovered, override the actual rating.
+ if(ratingWidget.indexHovered >= 0)
+ {
+ return indexHovered >= index
+ }
+
+ if(ratingWidget.userRating > 0)
+ {
+ return userRating >= index +1
+ }
+
+ return rating >= index + 1
+ }
+
+ contentItem: Item {}
+ height: UM.Theme.getSize("rating_star").height
+ width: UM.Theme.getSize("rating_star").width
+ background: UM.RecolorImage
+ {
+ source: UM.Theme.getIcon(control.isStarFilled ? "star_filled" : "star_empty")
+ sourceSize.width: width
+ sourceSize.height: height
+
+ // Unfilled stars should always have the default color. Only filled stars should change on hover
+ color:
+ {
+ if(!ratingWidget.canRate)
+ {
+ return UM.Theme.getColor("rating_star")
+ }
+ if((ratingWidget.indexHovered >= 0 || ratingWidget.userRating > 0) && isStarFilled)
+ {
+ return UM.Theme.getColor("primary")
+ }
+ return UM.Theme.getColor("rating_star")
+ }
+ }
+ onClicked:
+ {
+ if(ratingWidget.canRate)
+ {
+ rated(index + 1) // Notify anyone who cares about this.
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/Toolbox/resources/qml/SmallRatingWidget.qml b/plugins/Toolbox/resources/qml/SmallRatingWidget.qml
new file mode 100644
index 0000000000..965b81dc0f
--- /dev/null
+++ b/plugins/Toolbox/resources/qml/SmallRatingWidget.qml
@@ -0,0 +1,36 @@
+import QtQuick 2.3
+import QtQuick.Controls 1.4
+import UM 1.1 as UM
+import Cura 1.1 as Cura
+
+Row
+{
+ id: rating
+ height: UM.Theme.getSize("rating_star").height
+ visible: model.average_rating > 0 //Has a rating at all.
+ spacing: UM.Theme.getSize("thick_lining").width
+ width: starIcon.width + spacing + numRatingsLabel.width
+ UM.RecolorImage
+ {
+ id: starIcon
+ source: UM.Theme.getIcon("star_filled")
+ color: model.user_rating == 0 ? UM.Theme.getColor("rating_star") : UM.Theme.getColor("primary")
+ height: UM.Theme.getSize("rating_star").height
+ width: UM.Theme.getSize("rating_star").width
+ sourceSize.height: height
+ sourceSize.width: width
+ }
+
+ Label
+ {
+ id: numRatingsLabel
+ text: model.average_rating != undefined ? model.average_rating.toFixed(1) + " (" + model.num_ratings + " " + catalog.i18nc("@label", "ratings") + ")": ""
+ verticalAlignment: Text.AlignVCenter
+ height: starIcon.height
+ width: contentWidth
+ anchors.verticalCenter: starIcon.verticalCenter
+ color: starIcon.color
+ font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
+ }
+}
\ No newline at end of file
diff --git a/plugins/Toolbox/resources/qml/Toolbox.qml b/plugins/Toolbox/resources/qml/Toolbox.qml
index 9a98c998b0..d15d98eed7 100644
--- a/plugins/Toolbox/resources/qml/Toolbox.qml
+++ b/plugins/Toolbox/resources/qml/Toolbox.qml
@@ -14,17 +14,17 @@ Window
modality: Qt.ApplicationModal
flags: Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint
- width: 720 * screenScaleFactor
- height: 640 * screenScaleFactor
+ width: Math.floor(720 * screenScaleFactor)
+ height: Math.floor(640 * screenScaleFactor)
minimumWidth: width
maximumWidth: minimumWidth
minimumHeight: height
maximumHeight: minimumHeight
- color: UM.Theme.getColor("sidebar")
+ color: UM.Theme.getColor("main_background")
UM.I18nCatalog
{
id: catalog
- name:"cura"
+ name: "cura"
}
Item
{
@@ -38,7 +38,7 @@ Window
{
id: mainView
width: parent.width
- z: -1
+ z: parent.z - 1
anchors
{
top: header.bottom
@@ -95,6 +95,7 @@ Window
licenseDialog.show();
}
}
+
ToolboxLicenseDialog
{
id: licenseDialog
diff --git a/plugins/Toolbox/resources/qml/ToolboxAuthorPage.qml b/plugins/Toolbox/resources/qml/ToolboxAuthorPage.qml
index 4aaea20813..b653f1a73b 100644
--- a/plugins/Toolbox/resources/qml/ToolboxAuthorPage.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxAuthorPage.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.3
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@@ -55,10 +55,11 @@ Item
bottomMargin: UM.Theme.getSize("default_margin").height
}
text: details.name || ""
- font: UM.Theme.getFont("large")
+ font: UM.Theme.getFont("large_bold")
wrapMode: Text.WordWrap
width: parent.width
height: UM.Theme.getSize("toolbox_property_label").height
+ renderType: Text.NativeRendering
}
Label
{
@@ -70,6 +71,7 @@ Item
left: title.left
topMargin: UM.Theme.getSize("default_margin").height
}
+ renderType: Text.NativeRendering
}
Column
{
@@ -86,14 +88,16 @@ Item
Label
{
text: catalog.i18nc("@label", "Website") + ":"
- font: UM.Theme.getFont("very_small")
+ font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
+ renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Email") + ":"
- font: UM.Theme.getFont("very_small")
+ font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
+ renderType: Text.NativeRendering
}
}
Column
@@ -118,10 +122,11 @@ Item
}
return ""
}
- font: UM.Theme.getFont("very_small")
+ font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
+ renderType: Text.NativeRendering
}
Label
@@ -134,10 +139,11 @@ Item
}
return ""
}
- font: UM.Theme.getFont("very_small")
+ font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
+ renderType: Text.NativeRendering
}
}
Rectangle
diff --git a/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml b/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml
index 8524b7d1e5..dba9f19ccd 100644
--- a/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxBackColumn.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.2
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@@ -61,9 +61,15 @@ Item
id: labelStyle
text: control.text
color: control.enabled ? (control.hovered ? UM.Theme.getColor("primary") : UM.Theme.getColor("text")) : UM.Theme.getColor("text_inactive")
- font: UM.Theme.getFont("default_bold")
- horizontalAlignment: Text.AlignRight
+ font: UM.Theme.getFont("medium_bold")
+ horizontalAlignment: Text.AlignLeft
+ anchors
+ {
+ left: parent.left
+ leftMargin: UM.Theme.getSize("default_margin").width
+ }
width: control.width
+ renderType: Text.NativeRendering
}
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml b/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml
index a48cb2ee3f..db4e8c628f 100644
--- a/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxCompatibilityChart.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@@ -36,12 +36,19 @@ Item
var pg_name = "printingGuidelines"
return (pg_name in packageData.links) ? packageData.links[pg_name] : undefined
}
+
+ property var materialWebsiteUrl:
+ {
+ var pg_name = "website"
+ return (pg_name in packageData.links) ? packageData.links[pg_name] : undefined
+ }
anchors.topMargin: UM.Theme.getSize("default_margin").height
height: visible ? childrenRect.height : 0
visible: packageData.type == "material" &&
(packageData.has_configs || technicalDataSheetUrl !== undefined ||
- safetyDataSheetUrl !== undefined || printingGuidelinesUrl !== undefined)
+ safetyDataSheetUrl !== undefined || printingGuidelinesUrl !== undefined ||
+ materialWebsiteUrl !== undefined)
Item
{
@@ -60,6 +67,7 @@ Item
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
+ renderType: Text.NativeRendering
}
TableView
@@ -83,7 +91,7 @@ Item
model: packageData.supported_configs
headerDelegate: Rectangle
{
- color: UM.Theme.getColor("sidebar")
+ color: UM.Theme.getColor("main_background")
height: UM.Theme.getSize("toolbox_chart_row").height
Label
{
@@ -92,6 +100,7 @@ Item
text: styleData.value || ""
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default_bold")
+ renderType: Text.NativeRendering
}
Rectangle
{
@@ -111,6 +120,7 @@ Item
text: styleData.value || ""
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
}
}
itemDelegate: Item
@@ -123,6 +133,7 @@ Item
text: styleData.value || ""
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
}
}
@@ -137,6 +148,7 @@ Item
elide: Text.ElideRight
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
}
}
@@ -180,7 +192,8 @@ Item
anchors.top: combatibilityItem.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height / 2
visible: base.technicalDataSheetUrl !== undefined ||
- base.safetyDataSheetUrl !== undefined || base.printingGuidelinesUrl !== undefined
+ base.safetyDataSheetUrl !== undefined || base.printingGuidelinesUrl !== undefined ||
+ base.materialWebsiteUrl !== undefined
height: visible ? contentHeight : 0
text:
{
@@ -208,11 +221,22 @@ Item
var pg_name = catalog.i18nc("@action:label", "Printing Guidelines")
result += "%2".arg(base.printingGuidelinesUrl).arg(pg_name)
}
+ if (base.materialWebsiteUrl !== undefined)
+ {
+ if (result.length > 0)
+ {
+ result += " "
+ }
+ var pg_name = catalog.i18nc("@action:label", "Website")
+ result += "%2".arg(base.materialWebsiteUrl).arg(pg_name)
+ }
+
return result
}
- font: UM.Theme.getFont("very_small")
+ font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
+ renderType: Text.NativeRendering
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxConfirmUninstallResetDialog.qml b/plugins/Toolbox/resources/qml/ToolboxConfirmUninstallResetDialog.qml
index 2c5d08aa72..e238132680 100644
--- a/plugins/Toolbox/resources/qml/ToolboxConfirmUninstallResetDialog.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxConfirmUninstallResetDialog.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.2
+import QtQuick 2.10
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
@@ -66,6 +66,7 @@ UM.Dialog
anchors.right: parent.right
font: UM.Theme.getFont("default")
wrapMode: Text.WordWrap
+ renderType: Text.NativeRendering
}
// Buttons
diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailList.qml b/plugins/Toolbox/resources/qml/ToolboxDetailList.qml
index 2e5eae098c..4e44ea7d0b 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDetailList.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDetailList.qml
@@ -26,10 +26,19 @@ Item
}
height: childrenRect.height + 2 * UM.Theme.getSize("wide_margin").height
spacing: UM.Theme.getSize("default_margin").height
+
Repeater
{
model: toolbox.packagesModel
- delegate: ToolboxDetailTile {}
+ delegate: Loader
+ {
+ // FIXME: When using asynchronous loading, on Mac and Windows, the tile may fail to load complete,
+ // leaving an empty space below the title part. We turn it off for now to make it work on Mac and
+ // Windows.
+ // Can be related to this QT bug: https://bugreports.qt.io/browse/QTBUG-50992
+ asynchronous: false
+ source: "ToolboxDetailTile.qml"
+ }
}
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml b/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml
index 437a2ef351..fef2732af9 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDetailPage.qml
@@ -1,11 +1,13 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.3
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
+import Cura 1.1 as Cura
+
Item
{
id: page
@@ -24,7 +26,7 @@ Item
right: parent.right
rightMargin: UM.Theme.getSize("wide_margin").width
}
- height: UM.Theme.getSize("toolbox_detail_header").height
+ height: childrenRect.height + 3 * UM.Theme.getSize("default_margin").width
Rectangle
{
id: thumbnail
@@ -37,7 +39,7 @@ Item
leftMargin: UM.Theme.getSize("wide_margin").width
topMargin: UM.Theme.getSize("wide_margin").height
}
- color: white //Always a white background for image (regardless of theme).
+ color: UM.Theme.getColor("main_background")
Image
{
anchors.fill: parent
@@ -55,16 +57,21 @@ Item
top: thumbnail.top
left: thumbnail.right
leftMargin: UM.Theme.getSize("default_margin").width
- right: parent.right
- rightMargin: UM.Theme.getSize("wide_margin").width
- bottomMargin: UM.Theme.getSize("default_margin").height
}
text: details === null ? "" : (details.name || "")
- font: UM.Theme.getFont("large")
+ font: UM.Theme.getFont("large_bold")
color: UM.Theme.getColor("text")
- wrapMode: Text.WordWrap
- width: parent.width
- height: UM.Theme.getSize("toolbox_property_label").height
+ width: contentWidth
+ height: contentHeight
+ renderType: Text.NativeRendering
+ }
+
+ SmallRatingWidget
+ {
+ anchors.left: title.right
+ anchors.leftMargin: UM.Theme.getSize("default_margin").width
+ anchors.verticalCenter: title.verticalCenter
+ property var model: details
}
Column
@@ -81,27 +88,38 @@ Item
height: childrenRect.height
Label
{
- text: catalog.i18nc("@label", "Version") + ":"
- font: UM.Theme.getFont("very_small")
+ text: catalog.i18nc("@label", "Your rating") + ":"
+ font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
+ renderType: Text.NativeRendering
+ }
+ Label
+ {
+ text: catalog.i18nc("@label", "Version") + ":"
+ font: UM.Theme.getFont("default")
+ color: UM.Theme.getColor("text_medium")
+ renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Last updated") + ":"
- font: UM.Theme.getFont("very_small")
+ font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
+ renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Author") + ":"
- font: UM.Theme.getFont("very_small")
+ font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
+ renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Downloads") + ":"
- font: UM.Theme.getFont("very_small")
+ font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
+ renderType: Text.NativeRendering
}
}
Column
@@ -116,11 +134,54 @@ Item
}
spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
height: childrenRect.height
+ RatingWidget
+ {
+ id: rating
+ visible: details.type == "plugin"
+ packageId: details.id != undefined ? details.id: ""
+ userRating: details.user_rating != undefined ? details.user_rating: 0
+ canRate: toolbox.isInstalled(details.id) && Cura.API.account.isLoggedIn
+
+ onRated:
+ {
+ toolbox.ratePackage(details.id, rating)
+ // HACK: This is a far from optimal solution, but without major refactoring, this is the best we can
+ // do. Since a rework of this is scheduled, it shouldn't live that long...
+ var index = toolbox.pluginsAvailableModel.find("id", details.id)
+ if(index != -1)
+ {
+ if(details.user_rating == 0) // User never rated before.
+ {
+ toolbox.pluginsAvailableModel.setProperty(index, "num_ratings", details.num_ratings + 1)
+ }
+
+ toolbox.pluginsAvailableModel.setProperty(index, "user_rating", rating)
+
+
+ // Hack; This is because the current selection is an outdated copy, so we need to re-copy it.
+ base.selection = toolbox.pluginsAvailableModel.getItem(index)
+ return
+ }
+ index = toolbox.pluginsShowcaseModel.find("id", details.id)
+ if(index != -1)
+ {
+ if(details.user_rating == 0) // User never rated before.
+ {
+ toolbox.pluginsShowcaseModel.setProperty(index, "user_rating", rating)
+ }
+ toolbox.pluginsShowcaseModel.setProperty(index, "num_ratings", details.num_ratings + 1)
+
+ // Hack; This is because the current selection is an outdated copy, so we need to re-copy it.
+ base.selection = toolbox.pluginsShowcaseModel.getItem(index)
+ }
+ }
+ }
Label
{
text: details === null ? "" : (details.version || catalog.i18nc("@label", "Unknown"))
- font: UM.Theme.getFont("very_small")
+ font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
+ renderType: Text.NativeRendering
}
Label
{
@@ -133,8 +194,9 @@ Item
var date = new Date(details.last_updated)
return date.toLocaleString(UM.Preferences.getValue("general/language"))
}
- font: UM.Theme.getFont("very_small")
+ font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
+ renderType: Text.NativeRendering
}
Label
{
@@ -144,34 +206,25 @@ Item
{
return ""
}
- if (details.author_email)
- {
- return "" + details.author_name + ""
- }
else
{
return "" + details.author_name + ""
}
}
- font: UM.Theme.getFont("very_small")
+ font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
+ renderType: Text.NativeRendering
}
Label
{
text: details === null ? "" : (details.download_count || catalog.i18nc("@label", "Unknown"))
- font: UM.Theme.getFont("very_small")
+ font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
+ renderType: Text.NativeRendering
}
}
- Rectangle
- {
- color: UM.Theme.getColor("lining")
- width: parent.width
- height: UM.Theme.getSize("default_lining").height
- anchors.bottom: parent.bottom
- }
}
ToolboxDetailList
{
diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml
index 9061a8e06b..43f97baf3f 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDetailTile.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@@ -31,17 +31,19 @@ Item
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("medium_bold")
+ renderType: Text.NativeRendering
}
Label
{
anchors.top: packageName.bottom
width: parent.width
text: model.description
- maximumLineCount: 6
+ maximumLineCount: 25
elide: Text.ElideRight
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
+ renderType: Text.NativeRendering
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml
index cd1e4cdbda..87fc5d6955 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDetailTileActions.qml
@@ -1,40 +1,69 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
+import Cura 1.1 as Cura
Column
{
property bool installed: toolbox.isInstalled(model.id)
property bool canUpdate: toolbox.canUpdate(model.id)
+ property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
+
width: UM.Theme.getSize("toolbox_action_button").width
spacing: UM.Theme.getSize("narrow_margin").height
- ToolboxProgressButton
+ Item
{
- id: installButton
- active: toolbox.isDownloading && toolbox.activePackage == model
- complete: installed
- readyAction: function()
+ width: installButton.width
+ height: installButton.height
+ ToolboxProgressButton
{
- toolbox.activePackage = model
- toolbox.startDownload(model.download_url)
+ id: installButton
+ active: toolbox.isDownloading && toolbox.activePackage == model
+ onReadyAction:
+ {
+ toolbox.activePackage = model
+ toolbox.startDownload(model.download_url)
+ }
+ onActiveAction: toolbox.cancelDownload()
+
+ // Don't allow installing while another download is running
+ enabled: installed || (!(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired)
+ opacity: enabled ? 1.0 : 0.5
+ visible: !updateButton.visible && !installed// Don't show when the update button is visible
}
- activeAction: function()
+
+ Cura.SecondaryButton
{
- toolbox.cancelDownload()
+ visible: installed
+ onClicked: toolbox.viewCategory = "installed"
+ text: catalog.i18nc("@action:button", "Installed")
+ fixedWidthMode: true
+ width: installButton.width
+ height: installButton.height
}
- completeAction: function()
+ }
+
+ Label
+ {
+ wrapMode: Text.WordWrap
+ text: catalog.i18nc("@label:The string between and is the highlighted link", "Log in is required to install or update")
+ font: UM.Theme.getFont("default")
+ color: UM.Theme.getColor("text")
+ linkColor: UM.Theme.getColor("text_link")
+ visible: loginRequired
+ width: installButton.width
+ renderType: Text.NativeRendering
+
+ MouseArea
{
- toolbox.viewCategory = "installed"
+ anchors.fill: parent
+ onClicked: Cura.API.account.login()
}
- // Don't allow installing while another download is running
- enabled: installed || !(toolbox.isDownloading && toolbox.activePackage != model)
- opacity: enabled ? 1.0 : 0.5
- visible: !updateButton.visible // Don't show when the update button is visible
}
ToolboxProgressButton
@@ -44,24 +73,28 @@ Column
readyLabel: catalog.i18nc("@action:button", "Update")
activeLabel: catalog.i18nc("@action:button", "Updating")
completeLabel: catalog.i18nc("@action:button", "Updated")
- readyAction: function()
+
+ onReadyAction:
{
toolbox.activePackage = model
toolbox.update(model.id)
}
- activeAction: function()
- {
- toolbox.cancelDownload()
- }
+ onActiveAction: toolbox.cancelDownload()
// Don't allow installing while another download is running
- enabled: !(toolbox.isDownloading && toolbox.activePackage != model)
+ enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired
opacity: enabled ? 1.0 : 0.5
visible: canUpdate
}
+
Connections
{
target: toolbox
onInstallChanged: installed = toolbox.isInstalled(model.id)
onMetadataChanged: canUpdate = toolbox.canUpdate(model.id)
+ onFilterChanged:
+ {
+ installed = toolbox.isInstalled(model.id)
+ canUpdate = toolbox.canUpdate(model.id)
+ }
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml
index c586828969..a9fcb39b28 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsGrid.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.3
@@ -22,9 +22,10 @@ Column
text: gridArea.heading
width: parent.width
color: UM.Theme.getColor("text_medium")
- font: UM.Theme.getFont("medium")
+ font: UM.Theme.getFont("large")
+ renderType: Text.NativeRendering
}
- GridLayout
+ Grid
{
id: grid
width: parent.width - 2 * parent.padding
@@ -34,10 +35,12 @@ Column
Repeater
{
model: gridArea.model
- delegate: ToolboxDownloadsGridTile
+ delegate: Loader
{
- Layout.preferredWidth: (grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns
- Layout.preferredHeight: UM.Theme.getSize("toolbox_thumbnail_small").height
+ asynchronous: true
+ width: Math.round((grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns)
+ height: UM.Theme.getSize("toolbox_thumbnail_small").height
+ source: "ToolboxDownloadsGridTile.qml"
}
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml
index 887140bbfa..a11c6ee963 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsGridTile.qml
@@ -1,11 +1,12 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.3
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.3
import UM 1.1 as UM
+import Cura 1.1 as Cura
Item
{
@@ -14,91 +15,13 @@ Item
property int installedPackages: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0)
height: childrenRect.height
Layout.alignment: Qt.AlignTop | Qt.AlignLeft
- Rectangle
- {
- id: highlight
- anchors.fill: parent
- opacity: 0.0
- color: UM.Theme.getColor("primary")
- }
- Row
- {
- width: parent.width
- height: childrenRect.height
- spacing: Math.floor(UM.Theme.getSize("narrow_margin").width)
- Rectangle
- {
- id: thumbnail
- width: UM.Theme.getSize("toolbox_thumbnail_small").width
- height: UM.Theme.getSize("toolbox_thumbnail_small").height
- color: "white"
- border.width: UM.Theme.getSize("default_lining").width
- border.color: UM.Theme.getColor("lining")
- Image
- {
- anchors.centerIn: parent
- width: UM.Theme.getSize("toolbox_thumbnail_small").width - UM.Theme.getSize("wide_margin").width
- height: UM.Theme.getSize("toolbox_thumbnail_small").height - UM.Theme.getSize("wide_margin").width
- fillMode: Image.PreserveAspectFit
- source: model.icon_url || "../images/logobot.svg"
- mipmap: true
- }
- UM.RecolorImage
- {
- width: (parent.width * 0.4) | 0
- height: (parent.height * 0.4) | 0
- anchors
- {
- bottom: parent.bottom
- right: parent.right
- }
- sourceSize.width: width
- sourceSize.height: height
- visible: installedPackages != 0
- color: (installedPackages == packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border")
- source: "../images/installed_check.svg"
- }
- }
- Column
- {
- width: parent.width - thumbnail.width - parent.spacing
- spacing: Math.floor(UM.Theme.getSize("narrow_margin").width)
- Label
- {
- id: name
- text: model.name
- width: parent.width
- wrapMode: Text.WordWrap
- color: UM.Theme.getColor("text")
- font: UM.Theme.getFont("default_bold")
- }
- Label
- {
- id: info
- text: model.description
- maximumLineCount: 2
- elide: Text.ElideRight
- width: parent.width
- wrapMode: Text.WordWrap
- color: UM.Theme.getColor("text_medium")
- font: UM.Theme.getFont("very_small")
- }
- }
- }
+
MouseArea
{
anchors.fill: parent
hoverEnabled: true
- onEntered:
- {
- thumbnail.border.color = UM.Theme.getColor("primary")
- highlight.opacity = 0.1
- }
- onExited:
- {
- thumbnail.border.color = UM.Theme.getColor("lining")
- highlight.opacity = 0.0
- }
+ onEntered: thumbnail.border.color = UM.Theme.getColor("primary")
+ onExited: thumbnail.border.color = UM.Theme.getColor("lining")
onClicked:
{
base.selection = model
@@ -128,4 +51,83 @@ Item
}
}
}
+
+ Rectangle
+ {
+ id: thumbnail
+ width: UM.Theme.getSize("toolbox_thumbnail_small").width
+ height: UM.Theme.getSize("toolbox_thumbnail_small").height
+ color: UM.Theme.getColor("main_background")
+ border.width: UM.Theme.getSize("default_lining").width
+ border.color: UM.Theme.getColor("lining")
+
+ Image
+ {
+ anchors.centerIn: parent
+ width: UM.Theme.getSize("toolbox_thumbnail_small").width - UM.Theme.getSize("wide_margin").width
+ height: UM.Theme.getSize("toolbox_thumbnail_small").height - UM.Theme.getSize("wide_margin").width
+ fillMode: Image.PreserveAspectFit
+ source: model.icon_url || "../images/logobot.svg"
+ mipmap: true
+ }
+ UM.RecolorImage
+ {
+ width: (parent.width * 0.4) | 0
+ height: (parent.height * 0.4) | 0
+ anchors
+ {
+ bottom: parent.bottom
+ right: parent.right
+ }
+ sourceSize.height: height
+ visible: installedPackages != 0
+ color: (installedPackages == packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border")
+ source: "../images/installed_check.svg"
+ }
+ }
+ Item
+ {
+ anchors
+ {
+ left: thumbnail.right
+ leftMargin: Math.floor(UM.Theme.getSize("narrow_margin").width)
+ right: parent.right
+ top: parent.top
+ bottom: parent.bottom
+ }
+
+ Label
+ {
+ id: name
+ text: model.name
+ width: parent.width
+ elide: Text.ElideRight
+ color: UM.Theme.getColor("text")
+ font: UM.Theme.getFont("default_bold")
+ }
+ Label
+ {
+ id: info
+ text: model.description
+ elide: Text.ElideRight
+ width: parent.width
+ wrapMode: Text.WordWrap
+ color: UM.Theme.getColor("text")
+ font: UM.Theme.getFont("default")
+ anchors.top: name.bottom
+ anchors.bottom: rating.top
+ verticalAlignment: Text.AlignVCenter
+ maximumLineCount: 2
+ }
+ SmallRatingWidget
+ {
+ id: rating
+ anchors
+ {
+ bottom: parent.bottom
+ left: parent.left
+ right: parent.right
+ }
+ }
+ }
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml
index 46f5debfdd..795622cf82 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcase.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@@ -23,30 +23,34 @@ Rectangle
text: catalog.i18nc("@label", "Featured")
width: parent.width
color: UM.Theme.getColor("text_medium")
- font: UM.Theme.getFont("medium")
+ font: UM.Theme.getFont("large")
+ renderType: Text.NativeRendering
}
Grid
{
height: childrenRect.height
spacing: UM.Theme.getSize("wide_margin").width
columns: 3
- anchors
- {
- horizontalCenter: parent.horizontalCenter
- }
+ anchors.horizontalCenter: parent.horizontalCenter
+
Repeater
{
- model: {
- if ( toolbox.viewCategory == "plugin" )
+ model:
+ {
+ if (toolbox.viewCategory == "plugin")
{
return toolbox.pluginsShowcaseModel
}
- if ( toolbox.viewCategory == "material" )
+ if (toolbox.viewCategory == "material")
{
return toolbox.materialsShowcaseModel
}
}
- delegate: ToolboxDownloadsShowcaseTile {}
+ delegate: Loader
+ {
+ asynchronous: true
+ source: "ToolboxDownloadsShowcaseTile.qml"
+ }
}
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml
index 4fb70541d2..c8c1e56c82 100644
--- a/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxDownloadsShowcaseTile.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtGraphicalEffects 1.0
@@ -13,92 +13,79 @@ Rectangle
property int installedPackages: toolbox.viewCategory == "material" ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0)
id: tileBase
width: UM.Theme.getSize("toolbox_thumbnail_large").width + (2 * UM.Theme.getSize("default_lining").width)
- height: thumbnail.height + packageNameBackground.height + (2 * UM.Theme.getSize("default_lining").width)
+ height: thumbnail.height + packageName.height + rating.height + UM.Theme.getSize("default_margin").width
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
- color: "transparent"
- Rectangle
+ color: UM.Theme.getColor("main_background")
+ Image
{
id: thumbnail
- color: "white"
- width: UM.Theme.getSize("toolbox_thumbnail_large").width
- height: UM.Theme.getSize("toolbox_thumbnail_large").height
+ height: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height
+ width: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height
+ fillMode: Image.PreserveAspectFit
+ source: model.icon_url || "../images/logobot.svg"
+ mipmap: true
anchors
{
top: parent.top
+ topMargin: UM.Theme.getSize("default_margin").height
horizontalCenter: parent.horizontalCenter
- topMargin: UM.Theme.getSize("default_lining").width
}
- Image
+ }
+ Label
+ {
+ id: packageName
+ text: model.name
+ anchors
{
- anchors.centerIn: parent
- width: UM.Theme.getSize("toolbox_thumbnail_large").width - 2 * UM.Theme.getSize("default_margin").width
- height: UM.Theme.getSize("toolbox_thumbnail_large").height - 2 * UM.Theme.getSize("default_margin").height
- fillMode: Image.PreserveAspectFit
- source: model.icon_url || "../images/logobot.svg"
- mipmap: true
+ horizontalCenter: parent.horizontalCenter
+ top: thumbnail.bottom
}
- UM.RecolorImage
+ verticalAlignment: Text.AlignVCenter
+ horizontalAlignment: Text.AlignHCenter
+ renderType: Text.NativeRendering
+ height: UM.Theme.getSize("toolbox_heading_label").height
+ width: parent.width - UM.Theme.getSize("default_margin").width
+ wrapMode: Text.WordWrap
+ elide: Text.ElideRight
+ font: UM.Theme.getFont("medium_bold")
+ }
+ UM.RecolorImage
+ {
+ width: (parent.width * 0.20) | 0
+ height: width
+ anchors
{
- width: (parent.width * 0.3) | 0
- height: (parent.height * 0.3) | 0
- anchors
- {
- bottom: parent.bottom
- right: parent.right
- bottomMargin: UM.Theme.getSize("default_lining").width
- }
- sourceSize.width: width
- sourceSize.height: height
- visible: installedPackages != 0
- color: (installedPackages == packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border")
- source: "../images/installed_check.svg"
+ bottom: bottomBorder.top
+ right: parent.right
}
+ visible: installedPackages != 0
+ color: (installedPackages == packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border")
+ source: "../images/installed_check.svg"
+ }
+
+ SmallRatingWidget
+ {
+ id: rating
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: UM.Theme.getSize("narrow_margin").height
+ anchors.horizontalCenter: parent.horizontalCenter
}
Rectangle
{
- id: packageNameBackground
+ id: bottomBorder
color: UM.Theme.getColor("primary")
- anchors
- {
- top: thumbnail.bottom
- horizontalCenter: parent.horizontalCenter
- }
- height: UM.Theme.getSize("toolbox_heading_label").height
+ anchors.bottom: parent.bottom
width: parent.width
- Label
- {
- id: packageName
- text: model.name
- anchors
- {
- horizontalCenter: parent.horizontalCenter
- }
- verticalAlignment: Text.AlignVCenter
- horizontalAlignment: Text.AlignHCenter
- height: UM.Theme.getSize("toolbox_heading_label").height
- width: parent.width
- wrapMode: Text.WordWrap
- color: UM.Theme.getColor("button_text")
- font: UM.Theme.getFont("medium_bold")
- }
+ height: UM.Theme.getSize("toolbox_header_highlight").height
}
+
MouseArea
{
anchors.fill: parent
hoverEnabled: true
- onEntered:
- {
- packageName.color = UM.Theme.getColor("button_text_hover")
- packageNameBackground.color = UM.Theme.getColor("primary_hover")
- tileBase.border.color = UM.Theme.getColor("primary_hover")
- }
- onExited:
- {
- packageName.color = UM.Theme.getColor("button_text")
- packageNameBackground.color = UM.Theme.getColor("primary")
- tileBase.border.color = UM.Theme.getColor("lining")
- }
+ onEntered: tileBase.border.color = UM.Theme.getColor("primary")
+ onExited: tileBase.border.color = UM.Theme.getColor("lining")
onClicked:
{
base.selection = model
diff --git a/plugins/Toolbox/resources/qml/ToolboxErrorPage.qml b/plugins/Toolbox/resources/qml/ToolboxErrorPage.qml
index 600ae2b39f..e57e63dbb9 100644
--- a/plugins/Toolbox/resources/qml/ToolboxErrorPage.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxErrorPage.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
@@ -18,5 +18,6 @@ Rectangle
{
centerIn: parent
}
+ renderType: Text.NativeRendering
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxFooter.qml b/plugins/Toolbox/resources/qml/ToolboxFooter.qml
index 5c2a6577ad..6f46e939ff 100644
--- a/plugins/Toolbox/resources/qml/ToolboxFooter.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxFooter.qml
@@ -1,22 +1,24 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.2
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
+import QtQuick 2.10
+import QtQuick.Controls 2.3
+
import UM 1.1 as UM
+import Cura 1.0 as Cura
Item
{
id: footer
width: parent.width
anchors.bottom: parent.bottom
- height: visible ? Math.floor(UM.Theme.getSize("toolbox_footer").height) : 0
+ height: visible ? UM.Theme.getSize("toolbox_footer").height : 0
+
Label
{
text: catalog.i18nc("@info", "You will need to restart Cura before changes in packages have effect.")
color: UM.Theme.getColor("text")
- height: Math.floor(UM.Theme.getSize("toolbox_footer_button").height)
+ height: UM.Theme.getSize("toolbox_footer_button").height
verticalAlignment: Text.AlignVCenter
anchors
{
@@ -26,12 +28,12 @@ Item
right: restartButton.right
rightMargin: UM.Theme.getSize("default_margin").width
}
-
+ renderType: Text.NativeRendering
}
- Button
+
+ Cura.PrimaryButton
{
id: restartButton
- text: catalog.i18nc("@info:button", "Quit Cura")
anchors
{
top: parent.top
@@ -39,26 +41,11 @@ Item
right: parent.right
rightMargin: UM.Theme.getSize("wide_margin").width
}
- iconName: "dialog-restart"
+ height: UM.Theme.getSize("toolbox_footer_button").height
+ text: catalog.i18nc("@info:button", "Quit Cura")
onClicked: toolbox.restart()
- style: ButtonStyle
- {
- background: Rectangle
- {
- implicitWidth: UM.Theme.getSize("toolbox_footer_button").width
- implicitHeight: Math.floor(UM.Theme.getSize("toolbox_footer_button").height)
- color: control.hovered ? UM.Theme.getColor("primary_hover") : UM.Theme.getColor("primary")
- }
- label: Label
- {
- color: UM.Theme.getColor("button_text")
- font: UM.Theme.getFont("default_bold")
- text: control.text
- verticalAlignment: Text.AlignVCenter
- horizontalAlignment: Text.AlignHCenter
- }
- }
}
+
ToolboxShadow
{
visible: footer.visible
diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml
index e683f89823..a85a69cbac 100644
--- a/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxInstalledPage.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
@@ -21,44 +21,40 @@ ScrollView
Column
{
spacing: UM.Theme.getSize("default_margin").height
+ visible: toolbox.pluginsInstalledModel.items.length > 0
+ height: childrenRect.height + 4 * UM.Theme.getSize("default_margin").height
+
anchors
{
right: parent.right
left: parent.left
- leftMargin: UM.Theme.getSize("wide_margin").width
- topMargin: UM.Theme.getSize("wide_margin").height
- bottomMargin: UM.Theme.getSize("wide_margin").height
+ margins: UM.Theme.getSize("default_margin").width
top: parent.top
}
- height: childrenRect.height + 4 * UM.Theme.getSize("default_margin").height
+
Label
{
- visible: toolbox.pluginsInstalledModel.items.length > 0
- width: parent.width
+ width: page.width
text: catalog.i18nc("@title:tab", "Plugins")
color: UM.Theme.getColor("text_medium")
- font: UM.Theme.getFont("medium")
+ font: UM.Theme.getFont("large")
+ renderType: Text.NativeRendering
}
Rectangle
{
- visible: toolbox.pluginsInstalledModel.items.length > 0
color: "transparent"
width: parent.width
- height: childrenRect.height + 1 * UM.Theme.getSize("default_lining").width
+ height: childrenRect.height + UM.Theme.getSize("default_margin").width
border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width
Column
{
- height: childrenRect.height
anchors
{
top: parent.top
right: parent.right
left: parent.left
- leftMargin: UM.Theme.getSize("default_margin").width
- rightMargin: UM.Theme.getSize("default_margin").width
- topMargin: UM.Theme.getSize("default_lining").width
- bottomMargin: UM.Theme.getSize("default_lining").width
+ margins: UM.Theme.getSize("default_margin").width
}
Repeater
{
@@ -70,32 +66,27 @@ ScrollView
}
Label
{
- visible: toolbox.materialsInstalledModel.items.length > 0
- width: page.width
text: catalog.i18nc("@title:tab", "Materials")
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
+ renderType: Text.NativeRendering
}
+
Rectangle
{
- visible: toolbox.materialsInstalledModel.items.length > 0
color: "transparent"
width: parent.width
- height: childrenRect.height + 1 * UM.Theme.getSize("default_lining").width
+ height: childrenRect.height + UM.Theme.getSize("default_margin").width
border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width
Column
{
- height: Math.max( UM.Theme.getSize("wide_margin").height, childrenRect.height)
anchors
{
top: parent.top
right: parent.right
left: parent.left
- leftMargin: UM.Theme.getSize("default_margin").width
- rightMargin: UM.Theme.getSize("default_margin").width
- topMargin: UM.Theme.getSize("default_lining").width
- bottomMargin: UM.Theme.getSize("default_lining").width
+ margins: UM.Theme.getSize("default_margin").width
}
Repeater
{
diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml
index b16564fdd2..f50c3f3ac6 100644
--- a/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTile.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
@@ -30,6 +30,7 @@ Item
CheckBox
{
id: disableButton
+ anchors.verticalCenter: pluginInfo.verticalCenter
checked: isEnabled
visible: model.type == "plugin"
width: visible ? UM.Theme.getSize("checkbox").width : 0
@@ -49,17 +50,20 @@ Item
width: parent.width
height: Math.floor(UM.Theme.getSize("toolbox_property_label").height)
wrapMode: Text.WordWrap
- font: UM.Theme.getFont("default_bold")
+ font: UM.Theme.getFont("large_bold")
color: pluginInfo.color
+ renderType: Text.NativeRendering
}
Label
{
text: model.description
+ font: UM.Theme.getFont("default")
maximumLineCount: 3
elide: Text.ElideRight
width: parent.width
wrapMode: Text.WordWrap
color: pluginInfo.color
+ renderType: Text.NativeRendering
}
}
Column
@@ -80,6 +84,7 @@ Item
return model.author_name
}
}
+ font: UM.Theme.getFont("medium")
width: parent.width
height: Math.floor(UM.Theme.getSize("toolbox_property_label").height)
wrapMode: Text.WordWrap
@@ -88,16 +93,19 @@ Item
onLinkActivated: Qt.openUrlExternally("mailto:" + model.author_email + "?Subject=Cura: " + model.name + " Plugin")
color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining")
linkColor: UM.Theme.getColor("text_link")
+ renderType: Text.NativeRendering
}
Label
{
text: model.version
+ font: UM.Theme.getFont("default")
width: parent.width
height: UM.Theme.getSize("toolbox_property_label").height
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
+ renderType: Text.NativeRendering
}
}
ToolboxInstalledTileActions
diff --git a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml
index 8fd88b1cfd..61af84fbe5 100644
--- a/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxInstalledTileActions.qml
@@ -1,15 +1,18 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
+import Cura 1.1 as Cura
+
Column
{
property bool canUpdate: false
property bool canDowngrade: false
+ property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
width: UM.Theme.getSize("toolbox_action_button").width
spacing: UM.Theme.getSize("narrow_margin").height
@@ -21,6 +24,7 @@ Column
font: UM.Theme.getFont("default")
wrapMode: Text.WordWrap
width: parent.width
+ renderType: Text.NativeRendering
}
ToolboxProgressButton
@@ -30,59 +34,49 @@ Column
readyLabel: catalog.i18nc("@action:button", "Update")
activeLabel: catalog.i18nc("@action:button", "Updating")
completeLabel: catalog.i18nc("@action:button", "Updated")
- readyAction: function()
+ onReadyAction:
{
toolbox.activePackage = model
toolbox.update(model.id)
}
- activeAction: function()
- {
- toolbox.cancelDownload()
- }
+ onActiveAction: toolbox.cancelDownload()
+
// Don't allow installing while another download is running
- enabled: !(toolbox.isDownloading && toolbox.activePackage != model)
+ enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired
opacity: enabled ? 1.0 : 0.5
visible: canUpdate
}
- Button
+ Label
+ {
+ wrapMode: Text.WordWrap
+ text: catalog.i18nc("@label:The string between and is the highlighted link", "Log in is required to update")
+ font: UM.Theme.getFont("default")
+ color: UM.Theme.getColor("text")
+ linkColor: UM.Theme.getColor("text_link")
+ visible: loginRequired
+ width: updateButton.width
+ renderType: Text.NativeRendering
+
+ MouseArea
+ {
+ anchors.fill: parent
+ onClicked: Cura.API.account.login()
+ }
+ }
+
+ Cura.SecondaryButton
{
id: removeButton
text: canDowngrade ? catalog.i18nc("@action:button", "Downgrade") : catalog.i18nc("@action:button", "Uninstall")
visible: !model.is_bundled && model.is_installed
enabled: !toolbox.isDownloading
- style: ButtonStyle
- {
- background: Rectangle
- {
- implicitWidth: UM.Theme.getSize("toolbox_action_button").width
- implicitHeight: UM.Theme.getSize("toolbox_action_button").height
- color: "transparent"
- border
- {
- width: UM.Theme.getSize("default_lining").width
- color:
- {
- if (control.hovered)
- {
- return UM.Theme.getColor("primary_hover")
- }
- else
- {
- return UM.Theme.getColor("lining")
- }
- }
- }
- }
- label: Label
- {
- text: control.text
- color: UM.Theme.getColor("text")
- verticalAlignment: Text.AlignVCenter
- horizontalAlignment: Text.AlignHCenter
- font: UM.Theme.getFont("default")
- }
- }
+
+ width: UM.Theme.getSize("toolbox_action_button").width
+ height: UM.Theme.getSize("toolbox_action_button").height
+
+ fixedWidthMode: true
+
onClicked: toolbox.checkPackageUsageAndUninstall(model.id)
Connections
{
diff --git a/plugins/Toolbox/resources/qml/ToolboxLicenseDialog.qml b/plugins/Toolbox/resources/qml/ToolboxLicenseDialog.qml
index b8baf7bc83..40b22c268d 100644
--- a/plugins/Toolbox/resources/qml/ToolboxLicenseDialog.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxLicenseDialog.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.2
+import QtQuick 2.10
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
@@ -32,6 +32,7 @@ UM.Dialog
anchors.right: parent.right
text: licenseDialog.pluginName + catalog.i18nc("@label", "This plugin contains a license.\nYou need to accept this license to install this plugin.\nDo you agree with the terms below?")
wrapMode: Text.Wrap
+ renderType: Text.NativeRendering
}
TextArea
{
diff --git a/plugins/Toolbox/resources/qml/ToolboxLoadingPage.qml b/plugins/Toolbox/resources/qml/ToolboxLoadingPage.qml
index 1ba271dcab..025239bd43 100644
--- a/plugins/Toolbox/resources/qml/ToolboxLoadingPage.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxLoadingPage.qml
@@ -1,7 +1,7 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.7
+import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
@@ -18,5 +18,6 @@ Rectangle
{
centerIn: parent
}
+ renderType: Text.NativeRendering
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml b/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml
index 2744e40ec9..933e3a5900 100644
--- a/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxProgressButton.qml
@@ -5,6 +5,7 @@ import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
+import Cura 1.0 as Cura
Item
@@ -18,16 +19,19 @@ Item
property var activeLabel: catalog.i18nc("@action:button", "Cancel")
property var completeLabel: catalog.i18nc("@action:button", "Installed")
- property var readyAction: null // Action when button is ready and clicked (likely install)
- property var activeAction: null // Action when button is active and clicked (likely cancel)
- property var completeAction: null // Action when button is complete and clicked (likely go to installed)
+ signal readyAction() // Action when button is ready and clicked (likely install)
+ signal activeAction() // Action when button is active and clicked (likely cancel)
+ signal completeAction() // Action when button is complete and clicked (likely go to installed)
width: UM.Theme.getSize("toolbox_action_button").width
height: UM.Theme.getSize("toolbox_action_button").height
- Button
+ Cura.PrimaryButton
{
id: button
+ width: UM.Theme.getSize("toolbox_action_button").width
+ height: UM.Theme.getSize("toolbox_action_button").height
+ fixedWidthMode: true
text:
{
if (complete)
@@ -47,101 +51,15 @@ Item
{
if (complete)
{
- return completeAction()
+ completeAction()
}
else if (active)
{
- return activeAction()
+ activeAction()
}
else
{
- return readyAction()
- }
- }
- style: ButtonStyle
- {
- background: Rectangle
- {
- implicitWidth: UM.Theme.getSize("toolbox_action_button").width
- implicitHeight: UM.Theme.getSize("toolbox_action_button").height
- color:
- {
- if (base.complete)
- {
- return "transparent"
- }
- else
- {
- if (control.hovered)
- {
- return UM.Theme.getColor("primary_hover")
- }
- else
- {
- return UM.Theme.getColor("primary")
- }
- }
- }
- border
- {
- width:
- {
- if (base.complete)
- {
- UM.Theme.getSize("default_lining").width
- }
- else
- {
- return 0
- }
- }
- color:
- {
- if (control.hovered)
- {
- return UM.Theme.getColor("primary_hover")
- }
- else
- {
- return UM.Theme.getColor("lining")
- }
- }
- }
- }
- label: Label
- {
- text: control.text
- color:
- {
- if (base.complete)
- {
- return UM.Theme.getColor("text")
- }
- else
- {
- if (control.hovered)
- {
- return UM.Theme.getColor("button_text_hover")
- }
- else
- {
- return UM.Theme.getColor("button_text")
- }
- }
- }
- verticalAlignment: Text.AlignVCenter
- horizontalAlignment: Text.AlignHCenter
- font:
- {
- if (base.complete)
- {
- return UM.Theme.getFont("default")
- }
- else
- {
- return UM.Theme.getFont("default_bold")
- }
- }
+ readyAction()
}
}
}
diff --git a/plugins/Toolbox/resources/qml/ToolboxTabButton.qml b/plugins/Toolbox/resources/qml/ToolboxTabButton.qml
index 22fb6d73ca..5e1aeaa636 100644
--- a/plugins/Toolbox/resources/qml/ToolboxTabButton.qml
+++ b/plugins/Toolbox/resources/qml/ToolboxTabButton.qml
@@ -1,51 +1,51 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.2
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
+import QtQuick 2.10
+import QtQuick.Controls 2.3
import UM 1.1 as UM
Button
{
+ id: control
property bool active: false
- style: ButtonStyle
+ hoverEnabled: true
+
+ background: Item
{
- background: Rectangle
+ implicitWidth: UM.Theme.getSize("toolbox_header_tab").width
+ implicitHeight: UM.Theme.getSize("toolbox_header_tab").height
+ Rectangle
{
- color: "transparent"
- implicitWidth: UM.Theme.getSize("toolbox_header_tab").width
- implicitHeight: UM.Theme.getSize("toolbox_header_tab").height
- Rectangle
- {
- visible: control.active
- color: UM.Theme.getColor("sidebar_header_highlight_hover")
- anchors.bottom: parent.bottom
- width: parent.width
- height: UM.Theme.getSize("sidebar_header_highlight").height
- }
- }
- label: Label
- {
- text: control.text
- color:
- {
- if(control.hovered)
- {
- return UM.Theme.getColor("topbar_button_text_hovered");
- }
- if(control.active)
- {
- return UM.Theme.getColor("topbar_button_text_active");
- }
- else
- {
- return UM.Theme.getColor("topbar_button_text_inactive");
- }
- }
- font: control.enabled ? (control.active ? UM.Theme.getFont("medium_bold") : UM.Theme.getFont("medium")) : UM.Theme.getFont("default_italic")
- verticalAlignment: Text.AlignVCenter
- horizontalAlignment: Text.AlignHCenter
+ visible: control.active
+ color: UM.Theme.getColor("primary")
+ anchors.bottom: parent.bottom
+ width: parent.width
+ height: UM.Theme.getSize("toolbox_header_highlight").height
}
}
-}
+ contentItem: Label
+ {
+ id: label
+ text: control.text
+ color:
+ {
+ if(control.hovered)
+ {
+ return UM.Theme.getColor("toolbox_header_button_text_hovered");
+ }
+ if(control.active)
+ {
+ return UM.Theme.getColor("toolbox_header_button_text_active");
+ }
+ else
+ {
+ return UM.Theme.getColor("toolbox_header_button_text_inactive");
+ }
+ }
+ font: control.enabled ? (control.active ? UM.Theme.getFont("medium_bold") : UM.Theme.getFont("medium")) : UM.Theme.getFont("default_italic")
+ verticalAlignment: Text.AlignVCenter
+ horizontalAlignment: Text.AlignHCenter
+ renderType: Text.NativeRendering
+ }
+}
\ No newline at end of file
diff --git a/plugins/Toolbox/src/AuthorsModel.py b/plugins/Toolbox/src/AuthorsModel.py
index bea3893504..877f8256ee 100644
--- a/plugins/Toolbox/src/AuthorsModel.py
+++ b/plugins/Toolbox/src/AuthorsModel.py
@@ -2,18 +2,19 @@
# Cura is released under the terms of the LGPLv3 or higher.
import re
-from typing import Dict
+from typing import Dict, List, Optional, Union
from PyQt5.QtCore import Qt, pyqtProperty, pyqtSignal
from UM.Qt.ListModel import ListModel
+
## Model that holds cura packages. By setting the filter property the instances held by this model can be changed.
class AuthorsModel(ListModel):
- def __init__(self, parent = None):
+ def __init__(self, parent = None) -> None:
super().__init__(parent)
- self._metadata = None
+ self._metadata = None # type: Optional[List[Dict[str, Union[str, List[str], int]]]]
self.addRoleName(Qt.UserRole + 1, "id")
self.addRoleName(Qt.UserRole + 2, "name")
@@ -25,39 +26,40 @@ class AuthorsModel(ListModel):
self.addRoleName(Qt.UserRole + 8, "description")
# List of filters for queries. The result is the union of the each list of results.
- self._filter = {} # type: Dict[str,str]
+ self._filter = {} # type: Dict[str, str]
- def setMetadata(self, data):
- self._metadata = data
- self._update()
+ def setMetadata(self, data: List[Dict[str, Union[str, List[str], int]]]):
+ if self._metadata != data:
+ self._metadata = data
+ self._update()
- def _update(self):
- items = []
+ def _update(self) -> None:
+ items = [] # type: List[Dict[str, Union[str, List[str], int, None]]]
if not self._metadata:
- self.setItems([])
+ self.setItems(items)
return
for author in self._metadata:
items.append({
- "id": author["author_id"],
- "name": author["display_name"],
- "email": author["email"] if "email" in author else None,
- "website": author["website"],
- "package_count": author["package_count"] if "package_count" in author else 0,
- "package_types": author["package_types"] if "package_types" in author else [],
- "icon_url": author["icon_url"] if "icon_url" in author else None,
- "description": "Material and quality profiles from {author_name}".format(author_name = author["display_name"])
+ "id": author.get("author_id"),
+ "name": author.get("display_name"),
+ "email": author.get("email"),
+ "website": author.get("website"),
+ "package_count": author.get("package_count", 0),
+ "package_types": author.get("package_types", []),
+ "icon_url": author.get("icon_url"),
+ "description": "Material and quality profiles from {author_name}".format(author_name = author.get("display_name", ""))
})
# Filter on all the key-word arguments.
for key, value in self._filter.items():
if key is "package_types":
- key_filter = lambda item, value = value: value in item["package_types"]
+ key_filter = lambda item, value = value: value in item["package_types"] # type: ignore
elif "*" in value:
- key_filter = lambda item, key = key, value = value: self._matchRegExp(item, key, value)
+ key_filter = lambda item, key = key, value = value: self._matchRegExp(item, key, value) # type: ignore
else:
- key_filter = lambda item, key = key, value = value: self._matchString(item, key, value)
- items = filter(key_filter, items)
+ key_filter = lambda item, key = key, value = value: self._matchString(item, key, value) # type: ignore
+ items = filter(key_filter, items) # type: ignore
# Execute all filters.
filtered_items = list(items)
diff --git a/plugins/Toolbox/src/PackagesModel.py b/plugins/Toolbox/src/PackagesModel.py
index a31facf75a..d94fdf6bb7 100644
--- a/plugins/Toolbox/src/PackagesModel.py
+++ b/plugins/Toolbox/src/PackagesModel.py
@@ -33,20 +33,25 @@ class PackagesModel(ListModel):
self.addRoleName(Qt.UserRole + 12, "last_updated")
self.addRoleName(Qt.UserRole + 13, "is_bundled")
self.addRoleName(Qt.UserRole + 14, "is_active")
- self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed
+ self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed
self.addRoleName(Qt.UserRole + 16, "has_configs")
self.addRoleName(Qt.UserRole + 17, "supported_configs")
self.addRoleName(Qt.UserRole + 18, "download_count")
self.addRoleName(Qt.UserRole + 19, "tags")
self.addRoleName(Qt.UserRole + 20, "links")
self.addRoleName(Qt.UserRole + 21, "website")
+ self.addRoleName(Qt.UserRole + 22, "login_required")
+ self.addRoleName(Qt.UserRole + 23, "average_rating")
+ self.addRoleName(Qt.UserRole + 24, "num_ratings")
+ self.addRoleName(Qt.UserRole + 25, "user_rating")
# List of filters for queries. The result is the union of the each list of results.
self._filter = {} # type: Dict[str, str]
def setMetadata(self, data):
- self._metadata = data
- self._update()
+ if self._metadata != data:
+ self._metadata = data
+ self._update()
def _update(self):
items = []
@@ -99,6 +104,10 @@ class PackagesModel(ListModel):
"tags": package["tags"] if "tags" in package else [],
"links": links_dict,
"website": package["website"] if "website" in package else None,
+ "login_required": "login-required" in package.get("tags", []),
+ "average_rating": float(package.get("rating", {}).get("average", 0)),
+ "num_ratings": package.get("rating", {}).get("count", 0),
+ "user_rating": package.get("rating", {}).get("user_rating", 0)
})
# Filter on all the key-word arguments.
diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py
index 562a964f01..ccd181cdbc 100644
--- a/plugins/Toolbox/src/Toolbox.py
+++ b/plugins/Toolbox/src/Toolbox.py
@@ -13,11 +13,11 @@ from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkRepl
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.Extension import Extension
-from UM.Qt.ListModel import ListModel
from UM.i18n import i18nCatalog
from UM.Version import Version
-import cura
+from cura import ApplicationMetadata
+from cura import UltimakerCloudAuthentication
from cura.CuraApplication import CuraApplication
from .AuthorsModel import AuthorsModel
@@ -31,17 +31,14 @@ i18n_catalog = i18nCatalog("cura")
## The Toolbox class is responsible of communicating with the server through the API
class Toolbox(QObject, Extension):
- DEFAULT_CLOUD_API_ROOT = "https://api.ultimaker.com" #type: str
- DEFAULT_CLOUD_API_VERSION = 1 #type: int
-
def __init__(self, application: CuraApplication) -> None:
super().__init__()
self._application = application # type: CuraApplication
- self._sdk_version = None # type: Optional[Union[str, int]]
- self._cloud_api_version = None # type: Optional[int]
- self._cloud_api_root = None # type: Optional[str]
+ self._sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
+ self._cloud_api_version = UltimakerCloudAuthentication.CuraCloudAPIVersion # type: int
+ self._cloud_api_root = UltimakerCloudAuthentication.CuraCloudAPIRoot # type: str
self._api_url = None # type: Optional[str]
# Network:
@@ -50,47 +47,35 @@ class Toolbox(QObject, Extension):
self._download_progress = 0 # type: float
self._is_downloading = False # type: bool
self._network_manager = None # type: Optional[QNetworkAccessManager]
- self._request_header = [
- b"User-Agent",
- str.encode(
- "%s/%s (%s %s)" % (
- self._application.getApplicationName(),
- self._application.getVersion(),
- platform.system(),
- platform.machine(),
- )
- )
- ]
+ self._request_headers = [] # type: List[Tuple[bytes, bytes]]
+ self._updateRequestHeader()
+
+
self._request_urls = {} # type: Dict[str, QUrl]
self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated
self._old_plugin_ids = set() # type: Set[str]
self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]]
- # Data:
- self._metadata = {
+ # The responses as given by the server parsed to a list.
+ self._server_response_data = {
"authors": [],
- "packages": [],
- "plugins_showcase": [],
- "plugins_available": [],
- "plugins_installed": [],
- "materials_showcase": [],
- "materials_available": [],
- "materials_installed": [],
- "materials_generic": []
+ "packages": []
} # type: Dict[str, List[Any]]
# Models:
self._models = {
"authors": AuthorsModel(self),
"packages": PackagesModel(self),
- "plugins_showcase": PackagesModel(self),
- "plugins_available": PackagesModel(self),
- "plugins_installed": PackagesModel(self),
- "materials_showcase": AuthorsModel(self),
- "materials_available": AuthorsModel(self),
- "materials_installed": PackagesModel(self),
- "materials_generic": PackagesModel(self)
- } # type: Dict[str, ListModel]
+ } # type: Dict[str, Union[AuthorsModel, PackagesModel]]
+
+ self._plugins_showcase_model = PackagesModel(self)
+ self._plugins_available_model = PackagesModel(self)
+ self._plugins_installed_model = PackagesModel(self)
+
+ self._materials_showcase_model = AuthorsModel(self)
+ self._materials_available_model = AuthorsModel(self)
+ self._materials_installed_model = PackagesModel(self)
+ self._materials_generic_model = PackagesModel(self)
# These properties are for keeping track of the UI state:
# ----------------------------------------------------------------------
@@ -120,6 +105,7 @@ class Toolbox(QObject, Extension):
self._restart_dialog_message = "" # type: str
self._application.initializationFinished.connect(self._onAppInitialized)
+ self._application.getCuraAPI().account.loginStateChanged.connect(self._updateRequestHeader)
# Signals:
# --------------------------------------------------------------------------
@@ -139,12 +125,38 @@ class Toolbox(QObject, Extension):
showLicenseDialog = pyqtSignal()
uninstallVariablesChanged = pyqtSignal()
+ def _updateRequestHeader(self):
+ self._request_headers = [
+ (b"User-Agent",
+ str.encode(
+ "%s/%s (%s %s)" % (
+ self._application.getApplicationName(),
+ self._application.getVersion(),
+ platform.system(),
+ platform.machine(),
+ )
+ ))
+ ]
+ access_token = self._application.getCuraAPI().account.accessToken
+ if access_token:
+ self._request_headers.append((b"Authorization", "Bearer {}".format(access_token).encode()))
+
def _resetUninstallVariables(self) -> None:
self._package_id_to_uninstall = None # type: Optional[str]
self._package_name_to_uninstall = ""
self._package_used_materials = [] # type: List[Tuple[GlobalStack, str, str]]
self._package_used_qualities = [] # type: List[Tuple[GlobalStack, str, str]]
+ @pyqtSlot(str, int)
+ def ratePackage(self, package_id: str, rating: int) -> None:
+ url = QUrl("{base_url}/packages/{package_id}/ratings".format(base_url=self._api_url, package_id = package_id))
+
+ self._rate_request = QNetworkRequest(url)
+ for header_name, header_value in self._request_headers:
+ cast(QNetworkRequest, self._rate_request).setRawHeader(header_name, header_value)
+ data = "{\"data\": {\"cura_version\": \"%s\", \"rating\": %i}}" % (Version(self._application.getVersion()), rating)
+ self._rate_reply = cast(QNetworkAccessManager, self._network_manager).put(self._rate_request, data.encode())
+
@pyqtSlot(result = str)
def getLicenseDialogPluginName(self) -> str:
return self._license_dialog_plugin_name
@@ -168,9 +180,6 @@ class Toolbox(QObject, Extension):
def _onAppInitialized(self) -> None:
self._plugin_registry = self._application.getPluginRegistry()
self._package_manager = self._application.getPackageManager()
- self._sdk_version = self._getSDKVersion()
- self._cloud_api_version = self._getCloudAPIVersion()
- self._cloud_api_root = self._getCloudAPIRoot()
self._api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
cloud_api_root = self._cloud_api_root,
cloud_api_version = self._cloud_api_version,
@@ -178,44 +187,9 @@ class Toolbox(QObject, Extension):
)
self._request_urls = {
"authors": QUrl("{base_url}/authors".format(base_url = self._api_url)),
- "packages": QUrl("{base_url}/packages".format(base_url = self._api_url)),
- "plugins_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)),
- "plugins_available": QUrl("{base_url}/packages?package_type=plugin".format(base_url = self._api_url)),
- "materials_showcase": QUrl("{base_url}/showcase".format(base_url = self._api_url)),
- "materials_available": QUrl("{base_url}/packages?package_type=material".format(base_url = self._api_url)),
- "materials_generic": QUrl("{base_url}/packages?package_type=material&tags=generic".format(base_url = self._api_url))
+ "packages": QUrl("{base_url}/packages".format(base_url = self._api_url))
}
- # Get the API root for the packages API depending on Cura version settings.
- def _getCloudAPIRoot(self) -> str:
- if not hasattr(cura, "CuraVersion"):
- return self.DEFAULT_CLOUD_API_ROOT
- if not hasattr(cura.CuraVersion, "CuraCloudAPIRoot"): # type: ignore
- return self.DEFAULT_CLOUD_API_ROOT
- if not cura.CuraVersion.CuraCloudAPIRoot: # type: ignore
- return self.DEFAULT_CLOUD_API_ROOT
- return cura.CuraVersion.CuraCloudAPIRoot # type: ignore
-
- # Get the cloud API version from CuraVersion
- def _getCloudAPIVersion(self) -> int:
- if not hasattr(cura, "CuraVersion"):
- return self.DEFAULT_CLOUD_API_VERSION
- if not hasattr(cura.CuraVersion, "CuraCloudAPIVersion"): # type: ignore
- return self.DEFAULT_CLOUD_API_VERSION
- if not cura.CuraVersion.CuraCloudAPIVersion: # type: ignore
- return self.DEFAULT_CLOUD_API_VERSION
- return cura.CuraVersion.CuraCloudAPIVersion # type: ignore
-
- # Get the packages version depending on Cura version settings.
- def _getSDKVersion(self) -> Union[int, str]:
- if not hasattr(cura, "CuraVersion"):
- return self._application.getAPIVersion().getMajor()
- if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
- return self._application.getAPIVersion().getMajor()
- if not cura.CuraVersion.CuraSDKVersion: # type: ignore
- return self._application.getAPIVersion().getMajor()
- return cura.CuraVersion.CuraSDKVersion # type: ignore
-
@pyqtSlot()
def browsePackages(self) -> None:
# Create the network manager:
@@ -231,12 +205,6 @@ class Toolbox(QObject, Extension):
# Make remote requests:
self._makeRequestByType("packages")
self._makeRequestByType("authors")
- # TODO: Uncomment in the future when the tag-filtered api calls work in the cloud server
- # self._makeRequestByType("plugins_showcase")
- # self._makeRequestByType("plugins_available")
- # self._makeRequestByType("materials_showcase")
- # self._makeRequestByType("materials_available")
- # self._makeRequestByType("materials_generic")
# Gather installed packages:
self._updateInstalledModels()
@@ -267,12 +235,17 @@ class Toolbox(QObject, Extension):
def _convertPluginMetadata(self, plugin_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
try:
+ highest_sdk_version_supported = Version(0)
+ for supported_version in plugin_data["plugin"]["supported_sdk_versions"]:
+ if supported_version > highest_sdk_version_supported:
+ highest_sdk_version_supported = supported_version
+
formatted = {
"package_id": plugin_data["id"],
"package_type": "plugin",
"display_name": plugin_data["plugin"]["name"],
"package_version": plugin_data["plugin"]["version"],
- "sdk_version": plugin_data["plugin"]["api"],
+ "sdk_version": highest_sdk_version_supported,
"author": {
"author_id": plugin_data["plugin"]["author"],
"display_name": plugin_data["plugin"]["author"]
@@ -281,7 +254,7 @@ class Toolbox(QObject, Extension):
"description": plugin_data["plugin"]["description"]
}
return formatted
- except:
+ except KeyError:
Logger.log("w", "Unable to convert plugin meta data %s", str(plugin_data))
return None
@@ -319,13 +292,10 @@ class Toolbox(QObject, Extension):
if plugin_id not in all_plugin_package_ids)
self._old_plugin_metadata = {k: v for k, v in self._old_plugin_metadata.items() if k in self._old_plugin_ids}
- self._metadata["plugins_installed"] = all_packages["plugin"] + list(self._old_plugin_metadata.values())
- self._models["plugins_installed"].setMetadata(self._metadata["plugins_installed"])
+ self._plugins_installed_model.setMetadata(all_packages["plugin"] + list(self._old_plugin_metadata.values()))
self.metadataChanged.emit()
if "material" in all_packages:
- self._metadata["materials_installed"] = all_packages["material"]
- # TODO: ADD MATERIALS HERE ONCE MATERIALS PORTION OF TOOLBOX IS LIVE
- self._models["materials_installed"].setMetadata(self._metadata["materials_installed"])
+ self._materials_installed_model.setMetadata(all_packages["material"])
self.metadataChanged.emit()
@pyqtSlot(str)
@@ -479,7 +449,7 @@ class Toolbox(QObject, Extension):
def getRemotePackage(self, package_id: str) -> Optional[Dict]:
# TODO: make the lookup in a dict, not a loop. canUpdate is called for every item.
remote_package = None
- for package in self._metadata["packages"]:
+ for package in self._server_response_data["packages"]:
if package["package_id"] == package_id:
remote_package = package
break
@@ -491,11 +461,8 @@ class Toolbox(QObject, Extension):
def canUpdate(self, package_id: str) -> bool:
local_package = self._package_manager.getInstalledPackageInfo(package_id)
if local_package is None:
- Logger.log("i", "Could not find package [%s] as installed in the package manager, fall back to check the old plugins",
- package_id)
local_package = self.getOldPluginPackageMetadata(package_id)
if local_package is None:
- Logger.log("i", "Could not find package [%s] in the old plugins", package_id)
return False
remote_package = self.getRemotePackage(package_id)
@@ -545,8 +512,8 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, result = int)
def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int:
count = 0
- for package in self._metadata["materials_installed"]:
- if package["author"]["author_id"] == author_id:
+ for package in self._materials_installed_model.items:
+ if package["author_id"] == author_id:
count += 1
return count
@@ -554,7 +521,7 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, result = int)
def getTotalNumberOfMaterialPackagesByAuthor(self, author_id: str) -> int:
count = 0
- for package in self._metadata["packages"]:
+ for package in self._server_response_data["packages"]:
if package["package_type"] == "material":
if package["author"]["author_id"] == author_id:
count += 1
@@ -568,34 +535,31 @@ class Toolbox(QObject, Extension):
# Check for plugins that were installed with the old plugin browser
def isOldPlugin(self, plugin_id: str) -> bool:
- if plugin_id in self._old_plugin_ids:
- return True
- return False
+ return plugin_id in self._old_plugin_ids
def getOldPluginPackageMetadata(self, plugin_id: str) -> Optional[Dict[str, Any]]:
return self._old_plugin_metadata.get(plugin_id)
- def loadingComplete(self) -> bool:
+ def isLoadingComplete(self) -> bool:
populated = 0
- for list in self._metadata.items():
- if len(list) > 0:
+ for metadata_list in self._server_response_data.items():
+ if metadata_list:
populated += 1
- if populated == len(self._metadata.items()):
- return True
- return False
+ return populated == len(self._server_response_data.items())
# Make API Calls
# --------------------------------------------------------------------------
- def _makeRequestByType(self, type: str) -> None:
- Logger.log("i", "Marketplace: Requesting %s metadata from server.", type)
- request = QNetworkRequest(self._request_urls[type])
- request.setRawHeader(*self._request_header)
+ def _makeRequestByType(self, request_type: str) -> None:
+ Logger.log("i", "Requesting %s metadata from server.", request_type)
+ request = QNetworkRequest(self._request_urls[request_type])
+ for header_name, header_value in self._request_headers:
+ request.setRawHeader(header_name, header_value)
if self._network_manager:
self._network_manager.get(request)
@pyqtSlot(str)
def startDownload(self, url: str) -> None:
- Logger.log("i", "Marketplace: Attempting to download & install package from %s.", url)
+ Logger.log("i", "Attempting to download & install package from %s.", url)
url = QUrl(url)
self._download_request = QNetworkRequest(url)
if hasattr(QNetworkRequest, "FollowRedirectsAttribute"):
@@ -604,7 +568,8 @@ class Toolbox(QObject, Extension):
if hasattr(QNetworkRequest, "RedirectPolicyAttribute"):
# Patch for Qt 5.9+
cast(QNetworkRequest, self._download_request).setAttribute(QNetworkRequest.RedirectPolicyAttribute, True)
- cast(QNetworkRequest, self._download_request).setRawHeader(*self._request_header)
+ for header_name, header_value in self._request_headers:
+ cast(QNetworkRequest, self._download_request).setRawHeader(header_name, header_value)
self._download_reply = cast(QNetworkAccessManager, self._network_manager).get(self._download_request)
self.setDownloadProgress(0)
self.setIsDownloading(True)
@@ -612,15 +577,15 @@ class Toolbox(QObject, Extension):
@pyqtSlot()
def cancelDownload(self) -> None:
- Logger.log("i", "Marketplace: User cancelled the download of a package.")
+ Logger.log("i", "User cancelled the download of a package.")
self.resetDownload()
def resetDownload(self) -> None:
if self._download_reply:
try:
self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
- except TypeError: #Raised when the method is not connected to the signal yet.
- pass #Don't need to disconnect.
+ except TypeError: # Raised when the method is not connected to the signal yet.
+ pass # Don't need to disconnect.
self._download_reply.abort()
self._download_reply = None
self._download_request = None
@@ -646,22 +611,8 @@ class Toolbox(QObject, Extension):
self.resetDownload()
return
- # HACK: These request are not handled independently at this moment, but together from the "packages" call
- do_not_handle = [
- "materials_available",
- "materials_showcase",
- "materials_generic",
- "plugins_available",
- "plugins_showcase",
- ]
-
if reply.operation() == QNetworkAccessManager.GetOperation:
- for type, url in self._request_urls.items():
-
- # HACK: Do nothing because we'll handle these from the "packages" call
- if type in do_not_handle:
- continue
-
+ for response_type, url in self._request_urls.items():
if reply.url() == url:
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200:
try:
@@ -674,39 +625,33 @@ class Toolbox(QObject, Extension):
return
# Create model and apply metadata:
- if not self._models[type]:
- Logger.log("e", "Could not find the %s model.", type)
+ if not self._models[response_type]:
+ Logger.log("e", "Could not find the %s model.", response_type)
break
- self._metadata[type] = json_data["data"]
- self._models[type].setMetadata(self._metadata[type])
+ self._server_response_data[response_type] = json_data["data"]
+ self._models[response_type].setMetadata(self._server_response_data[response_type])
- # Do some auto filtering
- # TODO: Make multiple API calls in the future to handle this
- if type is "packages":
- self._models[type].setFilter({"type": "plugin"})
- self.buildMaterialsModels()
- self.buildPluginsModels()
- if type is "authors":
- self._models[type].setFilter({"package_types": "material"})
- if type is "materials_generic":
- self._models[type].setFilter({"tags": "generic"})
+ if response_type is "packages":
+ self._models[response_type].setFilter({"type": "plugin"})
+ self.reBuildMaterialsModels()
+ self.reBuildPluginsModels()
+ elif response_type is "authors":
+ self._models[response_type].setFilter({"package_types": "material"})
+ self._models[response_type].setFilter({"tags": "generic"})
self.metadataChanged.emit()
- if self.loadingComplete() is True:
+ if self.isLoadingComplete():
self.setViewPage("overview")
- return
except json.decoder.JSONDecodeError:
- Logger.log("w", "Marketplace: Received invalid JSON for %s.", type)
+ Logger.log("w", "Received invalid JSON for %s.", response_type)
break
else:
self.setViewPage("errored")
self.resetDownload()
- return
-
- else:
+ elif reply.operation() == QNetworkAccessManager.PutOperation:
# Ignore any operation that is not a get operation
pass
@@ -716,7 +661,13 @@ class Toolbox(QObject, Extension):
self.setDownloadProgress(new_progress)
if bytes_sent == bytes_total:
self.setIsDownloading(False)
- cast(QNetworkReply, self._download_reply).downloadProgress.disconnect(self._onDownloadProgress)
+ self._download_reply = cast(QNetworkReply, self._download_reply)
+ self._download_reply.downloadProgress.disconnect(self._onDownloadProgress)
+
+ # Check if the download was sucessfull
+ if self._download_reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
+ Logger.log("w", "Failed to download package. The following error was returned: %s", json.loads(bytes(self._download_reply.readAll()).decode("utf-8")))
+ return
# Must not delete the temporary file on Windows
self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False)
file_path = self._temp_plugin_file.name
@@ -726,10 +677,10 @@ class Toolbox(QObject, Extension):
self._onDownloadComplete(file_path)
def _onDownloadComplete(self, file_path: str) -> None:
- Logger.log("i", "Marketplace: Download complete.")
+ Logger.log("i", "Download complete.")
package_info = self._package_manager.getPackageInfo(file_path)
if not package_info:
- Logger.log("w", "Marketplace: Package file [%s] was not a valid CuraPackage.", file_path)
+ Logger.log("w", "Package file [%s] was not a valid CuraPackage.", file_path)
return
license_content = self._package_manager.getPackageLicense(file_path)
@@ -738,7 +689,6 @@ class Toolbox(QObject, Extension):
return
self.install(file_path)
- return
# Getter & Setters for Properties:
# --------------------------------------------------------------------------
@@ -761,8 +711,9 @@ class Toolbox(QObject, Extension):
return self._is_downloading
def setActivePackage(self, package: Dict[str, Any]) -> None:
- self._active_package = package
- self.activePackageChanged.emit()
+ if self._active_package != package:
+ self._active_package = package
+ self.activePackageChanged.emit()
## The active package is the package that is currently being downloaded
@pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged)
@@ -770,16 +721,18 @@ class Toolbox(QObject, Extension):
return self._active_package
def setViewCategory(self, category: str = "plugin") -> None:
- self._view_category = category
- self.viewChanged.emit()
+ if self._view_category != category:
+ self._view_category = category
+ self.viewChanged.emit()
@pyqtProperty(str, fset = setViewCategory, notify = viewChanged)
def viewCategory(self) -> str:
return self._view_category
def setViewPage(self, page: str = "overview") -> None:
- self._view_page = page
- self.viewChanged.emit()
+ if self._view_page != page:
+ self._view_page = page
+ self.viewChanged.emit()
@pyqtProperty(str, fset = setViewPage, notify = viewChanged)
def viewPage(self) -> str:
@@ -787,48 +740,48 @@ class Toolbox(QObject, Extension):
# Exposed Models:
# --------------------------------------------------------------------------
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def authorsModel(self) -> AuthorsModel:
return cast(AuthorsModel, self._models["authors"])
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def packagesModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["packages"])
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def pluginsShowcaseModel(self) -> PackagesModel:
- return cast(PackagesModel, self._models["plugins_showcase"])
+ return self._plugins_showcase_model
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def pluginsAvailableModel(self) -> PackagesModel:
- return cast(PackagesModel, self._models["plugins_available"])
+ return self._plugins_available_model
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def pluginsInstalledModel(self) -> PackagesModel:
- return cast(PackagesModel, self._models["plugins_installed"])
+ return self._plugins_installed_model
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def materialsShowcaseModel(self) -> AuthorsModel:
- return cast(AuthorsModel, self._models["materials_showcase"])
+ return self._materials_showcase_model
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def materialsAvailableModel(self) -> AuthorsModel:
- return cast(AuthorsModel, self._models["materials_available"])
+ return self._materials_available_model
- @pyqtProperty(QObject, notify = metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def materialsInstalledModel(self) -> PackagesModel:
- return cast(PackagesModel, self._models["materials_installed"])
+ return self._materials_installed_model
- @pyqtProperty(QObject, notify=metadataChanged)
+ @pyqtProperty(QObject, constant=True)
def materialsGenericModel(self) -> PackagesModel:
- return cast(PackagesModel, self._models["materials_generic"])
+ return self._materials_generic_model
# Filter Models:
# --------------------------------------------------------------------------
@pyqtSlot(str, str, str)
def filterModelByProp(self, model_type: str, filter_type: str, parameter: str) -> None:
if not self._models[model_type]:
- Logger.log("w", "Marketplace: Couldn't filter %s model because it doesn't exist.", model_type)
+ Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type)
return
self._models[model_type].setFilter({filter_type: parameter})
self.filterChanged.emit()
@@ -836,7 +789,7 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, "QVariantMap")
def setFilters(self, model_type: str, filter_dict: dict) -> None:
if not self._models[model_type]:
- Logger.log("w", "Marketplace: Couldn't filter %s model because it doesn't exist.", model_type)
+ Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type)
return
self._models[model_type].setFilter(filter_dict)
self.filterChanged.emit()
@@ -844,21 +797,21 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str)
def removeFilters(self, model_type: str) -> None:
if not self._models[model_type]:
- Logger.log("w", "Marketplace: Couldn't remove filters on %s model because it doesn't exist.", model_type)
+ Logger.log("w", "Couldn't remove filters on %s model because it doesn't exist.", model_type)
return
self._models[model_type].setFilter({})
self.filterChanged.emit()
# HACK(S):
# --------------------------------------------------------------------------
- def buildMaterialsModels(self) -> None:
- self._metadata["materials_showcase"] = []
- self._metadata["materials_available"] = []
- self._metadata["materials_generic"] = []
+ def reBuildMaterialsModels(self) -> None:
+ materials_showcase_metadata = []
+ materials_available_metadata = []
+ materials_generic_metadata = []
- processed_authors = [] # type: List[str]
+ processed_authors = [] # type: List[str]
- for item in self._metadata["packages"]:
+ for item in self._server_response_data["packages"]:
if item["package_type"] == "material":
author = item["author"]
@@ -867,30 +820,29 @@ class Toolbox(QObject, Extension):
# Generic materials to be in the same section
if "generic" in item["tags"]:
- self._metadata["materials_generic"].append(item)
+ materials_generic_metadata.append(item)
else:
if "showcase" in item["tags"]:
- self._metadata["materials_showcase"].append(author)
+ materials_showcase_metadata.append(author)
else:
- self._metadata["materials_available"].append(author)
+ materials_available_metadata.append(author)
processed_authors.append(author["author_id"])
- self._models["materials_showcase"].setMetadata(self._metadata["materials_showcase"])
- self._models["materials_available"].setMetadata(self._metadata["materials_available"])
- self._models["materials_generic"].setMetadata(self._metadata["materials_generic"])
+ self._materials_showcase_model.setMetadata(materials_showcase_metadata)
+ self._materials_available_model.setMetadata(materials_available_metadata)
+ self._materials_generic_model.setMetadata(materials_generic_metadata)
- def buildPluginsModels(self) -> None:
- self._metadata["plugins_showcase"] = []
- self._metadata["plugins_available"] = []
+ def reBuildPluginsModels(self) -> None:
+ plugins_showcase_metadata = []
+ plugins_available_metadata = []
- for item in self._metadata["packages"]:
+ for item in self._server_response_data["packages"]:
if item["package_type"] == "plugin":
-
if "showcase" in item["tags"]:
- self._metadata["plugins_showcase"].append(item)
+ plugins_showcase_metadata.append(item)
else:
- self._metadata["plugins_available"].append(item)
+ plugins_available_metadata.append(item)
- self._models["plugins_showcase"].setMetadata(self._metadata["plugins_showcase"])
- self._models["plugins_available"].setMetadata(self._metadata["plugins_available"])
+ self._plugins_showcase_model.setMetadata(plugins_showcase_metadata)
+ self._plugins_available_model.setMetadata(plugins_available_metadata)
diff --git a/plugins/UFPWriter/plugin.json b/plugins/UFPWriter/plugin.json
index ab590353e0..288d6acf77 100644
--- a/plugins/UFPWriter/plugin.json
+++ b/plugins/UFPWriter/plugin.json
@@ -1,8 +1,8 @@
{
"name": "UFP Writer",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides support for writing Ultimaker Format Packages.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/__init__.py b/plugins/UM3NetworkPrinting/__init__.py
index e2ad5a2b12..3da7795589 100644
--- a/plugins/UM3NetworkPrinting/__init__.py
+++ b/plugins/UM3NetworkPrinting/__init__.py
@@ -1,11 +1,15 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
-
from .src import DiscoverUM3Action
from .src import UM3OutputDevicePlugin
+
def getMetaData():
return {}
+
def register(app):
- return { "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(), "machine_action": DiscoverUM3Action.DiscoverUM3Action()}
\ No newline at end of file
+ return {
+ "output_device": UM3OutputDevicePlugin.UM3OutputDevicePlugin(),
+ "machine_action": DiscoverUM3Action.DiscoverUM3Action()
+ }
diff --git a/plugins/UM3NetworkPrinting/plugin.json b/plugins/UM3NetworkPrinting/plugin.json
index d415338374..088b4dae6a 100644
--- a/plugins/UM3NetworkPrinting/plugin.json
+++ b/plugins/UM3NetworkPrinting/plugin.json
@@ -2,7 +2,7 @@
"name": "UM3 Network Connection",
"author": "Ultimaker B.V.",
"description": "Manages network connections to Ultimaker 3 printers.",
- "version": "1.0.0",
- "api": 5,
+ "version": "1.0.1",
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/UM3NetworkPrinting/resources/png/Ultimaker 3 Extended.png b/plugins/UM3NetworkPrinting/resources/png/Ultimaker 3 Extended.png
new file mode 100644
index 0000000000..1ce19c2933
Binary files /dev/null and b/plugins/UM3NetworkPrinting/resources/png/Ultimaker 3 Extended.png differ
diff --git a/plugins/UM3NetworkPrinting/resources/png/Ultimaker 3.png b/plugins/UM3NetworkPrinting/resources/png/Ultimaker 3.png
new file mode 100644
index 0000000000..4639cb3fde
Binary files /dev/null and b/plugins/UM3NetworkPrinting/resources/png/Ultimaker 3.png differ
diff --git a/plugins/UM3NetworkPrinting/resources/png/Ultimaker S5.png b/plugins/UM3NetworkPrinting/resources/png/Ultimaker S5.png
new file mode 100644
index 0000000000..29ba428e38
Binary files /dev/null and b/plugins/UM3NetworkPrinting/resources/png/Ultimaker S5.png differ
diff --git a/plugins/UM3NetworkPrinting/resources/qml/CameraButton.qml b/plugins/UM3NetworkPrinting/resources/qml/CameraButton.qml
index 7e5c254e5c..6f054f9c19 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/CameraButton.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/CameraButton.qml
@@ -8,11 +8,13 @@ import UM 1.3 as UM
import Cura 1.0 as Cura
Rectangle {
+ id: base
property var iconSource: null;
- color: clickArea.containsMouse ? UM.Theme.getColor("primary_hover") : UM.Theme.getColor("primary"); // "Cura Blue"
+ color: "#0a0850" // TODO: Theme!
height: width;
radius: Math.round(0.5 * width);
- width: 36 * screenScaleFactor;
+ width: 24 * screenScaleFactor;
+ property var enabled: true
UM.RecolorImage {
id: icon;
@@ -29,12 +31,18 @@ Rectangle {
MouseArea {
id: clickArea;
anchors.fill: parent;
- hoverEnabled: true;
+ hoverEnabled: base.enabled
onClicked: {
- if (OutputDevice.activeCameraUrl != "") {
- OutputDevice.setActiveCameraUrl("");
- } else {
- OutputDevice.setActiveCameraUrl(modelData.cameraUrl);
+ if (base.enabled)
+ {
+ if (OutputDevice.activeCameraUrl != "")
+ {
+ OutputDevice.setActiveCameraUrl("")
+ }
+ else
+ {
+ OutputDevice.setActiveCameraUrl(modelData.cameraUrl)
+ }
}
}
}
diff --git a/plugins/UM3NetworkPrinting/resources/qml/ClusterControlItem.qml b/plugins/UM3NetworkPrinting/resources/qml/ClusterControlItem.qml
deleted file mode 100644
index 068c369a3f..0000000000
--- a/plugins/UM3NetworkPrinting/resources/qml/ClusterControlItem.qml
+++ /dev/null
@@ -1,110 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.3
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.3
-import UM 1.3 as UM
-import Cura 1.0 as Cura
-
-Component {
- Rectangle {
- id: base;
- property var shadowRadius: UM.Theme.getSize("monitor_shadow_radius").width;
- property var cornerRadius: UM.Theme.getSize("monitor_corner_radius").width;
- anchors.fill: parent;
- color: UM.Theme.getColor("sidebar");
- visible: OutputDevice != null;
-
- UM.I18nCatalog {
- id: catalog;
- name: "cura";
- }
-
- Label {
- id: printingLabel;
- anchors {
- left: parent.left;
- leftMargin: 4 * UM.Theme.getSize("default_margin").width;
- margins: 2 * UM.Theme.getSize("default_margin").width;
- right: parent.right;
- top: parent.top;
- }
- color: UM.Theme.getColor("text");
- elide: Text.ElideRight;
- font: UM.Theme.getFont("large");
- text: catalog.i18nc("@label", "Printing");
- }
-
- Label {
- id: managePrintersLabel;
- anchors {
- bottom: printingLabel.bottom;
- right: printerScrollView.right;
- rightMargin: 4 * UM.Theme.getSize("default_margin").width;
- }
- color: UM.Theme.getColor("primary"); // "Cura Blue"
- font: UM.Theme.getFont("default");
- linkColor: UM.Theme.getColor("primary"); // "Cura Blue"
- text: catalog.i18nc("@label link to connect manager", "Manage printers");
- }
-
- MouseArea {
- anchors.fill: managePrintersLabel;
- hoverEnabled: true;
- onClicked: Cura.MachineManager.printerOutputDevices[0].openPrinterControlPanel();
- onEntered: managePrintersLabel.font.underline = true;
- onExited: managePrintersLabel.font.underline = false;
- }
-
- // Skeleton loading
- Column {
- id: skeletonLoader;
- anchors {
- left: parent.left;
- leftMargin: UM.Theme.getSize("wide_margin").width;
- right: parent.right;
- rightMargin: UM.Theme.getSize("wide_margin").width;
- top: printingLabel.bottom;
- topMargin: UM.Theme.getSize("default_margin").height;
- }
- spacing: UM.Theme.getSize("default_margin").height - 10;
- visible: printerList.count === 0;
-
- PrinterCard {
- printer: null;
- }
- PrinterCard {
- printer: null;
- }
- }
-
- // Actual content
- ScrollView {
- id: printerScrollView;
- anchors {
- bottom: parent.bottom;
- left: parent.left;
- right: parent.right;
- top: printingLabel.bottom;
- topMargin: UM.Theme.getSize("default_margin").height;
- }
- style: UM.Theme.styles.scrollview;
-
- ListView {
- id: printerList;
- property var currentIndex: -1;
- anchors {
- fill: parent;
- leftMargin: UM.Theme.getSize("wide_margin").width;
- rightMargin: UM.Theme.getSize("wide_margin").width;
- }
- delegate: PrinterCard {
- printer: modelData;
- }
- model: OutputDevice.printers;
- spacing: UM.Theme.getSize("default_margin").height - 10;
- }
- }
- }
-}
diff --git a/plugins/UM3NetworkPrinting/resources/qml/ClusterMonitorItem.qml b/plugins/UM3NetworkPrinting/resources/qml/ClusterMonitorItem.qml
deleted file mode 100644
index d210ab40f3..0000000000
--- a/plugins/UM3NetworkPrinting/resources/qml/ClusterMonitorItem.qml
+++ /dev/null
@@ -1,132 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.2
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
-import UM 1.3 as UM
-import Cura 1.0 as Cura
-
-Component {
- Rectangle {
- id: monitorFrame;
- property var emphasisColor: UM.Theme.getColor("setting_control_border_highlight");
- property var cornerRadius: UM.Theme.getSize("monitor_corner_radius").width;
- color: UM.Theme.getColor("viewport_background");
- height: maximumHeight;
- onVisibleChanged: {
- if (monitorFrame != null && !monitorFrame.visible) {
- OutputDevice.setActiveCameraUrl("");
- }
- }
- width: maximumWidth;
-
- UM.I18nCatalog {
- id: catalog;
- name: "cura";
- }
-
- Label {
- id: manageQueueLabel;
- anchors {
- bottom: queuedLabel.bottom;
- right: queuedPrintJobs.right;
- rightMargin: 3 * UM.Theme.getSize("default_margin").width;
- }
- color: UM.Theme.getColor("primary");
- font: UM.Theme.getFont("default");
- linkColor: UM.Theme.getColor("primary");
- text: catalog.i18nc("@label link to connect manager", "Manage queue");
- }
-
- MouseArea {
- anchors.fill: manageQueueLabel;
- hoverEnabled: true;
- onClicked: Cura.MachineManager.printerOutputDevices[0].openPrintJobControlPanel();
- onEntered: manageQueueLabel.font.underline = true;
- onExited: manageQueueLabel.font.underline = false;
- }
-
- Label {
- id: queuedLabel;
- anchors {
- left: queuedPrintJobs.left;
- leftMargin: 3 * UM.Theme.getSize("default_margin").width + 5 * screenScaleFactor;
- top: parent.top;
- topMargin: 2 * UM.Theme.getSize("default_margin").height;
- }
- color: UM.Theme.getColor("text");
- font: UM.Theme.getFont("large");
- text: catalog.i18nc("@label", "Queued");
- }
-
- Column {
- id: skeletonLoader;
- anchors {
- bottom: parent.bottom;
- bottomMargin: UM.Theme.getSize("default_margin").height;
- horizontalCenter: parent.horizontalCenter;
- top: queuedLabel.bottom;
- topMargin: UM.Theme.getSize("default_margin").height;
- }
- visible: !queuedPrintJobs.visible;
- width: Math.min(800 * screenScaleFactor, maximumWidth);
-
- PrintJobInfoBlock {
- anchors {
- left: parent.left;
- leftMargin: UM.Theme.getSize("default_margin").width;
- right: parent.right;
- rightMargin: UM.Theme.getSize("default_margin").width;
- }
- printJob: null; // Use as skeleton
- }
-
- PrintJobInfoBlock {
- anchors {
- left: parent.left;
- leftMargin: UM.Theme.getSize("default_margin").width;
- right: parent.right;
- rightMargin: UM.Theme.getSize("default_margin").width;
- }
- printJob: null; // Use as skeleton
- }
- }
-
- ScrollView {
- id: queuedPrintJobs;
- anchors {
- top: queuedLabel.bottom;
- topMargin: UM.Theme.getSize("default_margin").height;
- horizontalCenter: parent.horizontalCenter;
- bottomMargin: UM.Theme.getSize("default_margin").height;
- bottom: parent.bottom;
- }
- style: UM.Theme.styles.scrollview;
- visible: OutputDevice.receivedPrintJobs;
- width: Math.min(800 * screenScaleFactor, maximumWidth);
-
- ListView {
- id: printJobList;
- anchors.fill: parent;
- delegate: PrintJobInfoBlock {
- anchors {
- left: parent.left;
- leftMargin: UM.Theme.getSize("default_margin").width;
- right: parent.right;
- rightMargin: UM.Theme.getSize("default_margin").width;
- }
- printJob: modelData;
- }
- model: OutputDevice.queuedPrintJobs;
- spacing: UM.Theme.getSize("default_margin").height - 2 * UM.Theme.getSize("monitor_shadow_radius").width;
- }
- }
-
- PrinterVideoStream {
- anchors.fill: parent;
- cameraUrl: OutputDevice.activeCameraUrl;
- visible: OutputDevice.activeCameraUrl != "";
- }
- }
-}
diff --git a/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml b/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml
index 58443115a9..3883a7e285 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/DiscoverUM3Action.qml
@@ -28,7 +28,7 @@ Cura.MachineAction
// Check if there is another instance with the same key
if (!manager.existsKey(printerKey))
{
- manager.setKey(printerKey)
+ manager.associateActiveMachineWithPrinterDevice(base.selectedDevice)
manager.setGroupName(printerName) // TODO To change when the groups have a name
completed()
}
@@ -64,6 +64,7 @@ Cura.MachineAction
width: parent.width
text: catalog.i18nc("@title:window", "Connect to Networked Printer")
wrapMode: Text.WordWrap
+ renderType: Text.NativeRendering
font.pointSize: 18
}
@@ -72,6 +73,7 @@ Cura.MachineAction
id: pageDescription
width: parent.width
wrapMode: Text.WordWrap
+ renderType: Text.NativeRendering
text: catalog.i18nc("@label", "To print directly to your printer over the network, please make sure your printer is connected to the network using a network cable or by connecting your printer to your WIFI network. If you don't connect Cura with your printer, you can still use a USB drive to transfer g-code files to your printer.\n\nSelect your printer from the list below:")
}
@@ -182,6 +184,7 @@ Cura.MachineAction
text: listview.model[index].name
color: parent.ListView.isCurrentItem ? palette.highlightedText : palette.text
elide: Text.ElideRight
+ renderType: Text.NativeRendering
}
MouseArea
@@ -204,6 +207,7 @@ Cura.MachineAction
anchors.left: parent.left
anchors.right: parent.right
wrapMode: Text.WordWrap
+ renderType: Text.NativeRendering
text: catalog.i18nc("@label", "If your printer is not listed, read the network printing troubleshooting guide").arg("https://ultimaker.com/en/troubleshooting");
onLinkActivated: Qt.openUrlExternally(link)
}
@@ -219,8 +223,9 @@ Cura.MachineAction
width: parent.width
wrapMode: Text.WordWrap
text: base.selectedDevice ? base.selectedDevice.name : ""
- font: UM.Theme.getFont("large")
+ font: UM.Theme.getFont("large_bold")
elide: Text.ElideRight
+ renderType: Text.NativeRendering
}
Grid
{
@@ -231,12 +236,14 @@ Cura.MachineAction
{
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
+ renderType: Text.NativeRendering
text: catalog.i18nc("@label", "Type")
}
Label
{
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
+ renderType: Text.NativeRendering
text:
{
if(base.selectedDevice)
@@ -268,24 +275,28 @@ Cura.MachineAction
{
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
+ renderType: Text.NativeRendering
text: catalog.i18nc("@label", "Firmware version")
}
Label
{
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
+ renderType: Text.NativeRendering
text: base.selectedDevice ? base.selectedDevice.firmwareVersion : ""
}
Label
{
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
+ renderType: Text.NativeRendering
text: catalog.i18nc("@label", "Address")
}
Label
{
width: Math.round(parent.width * 0.5)
wrapMode: Text.WordWrap
+ renderType: Text.NativeRendering
text: base.selectedDevice ? base.selectedDevice.ipAddress : ""
}
}
@@ -294,6 +305,7 @@ Cura.MachineAction
{
width: parent.width
wrapMode: Text.WordWrap
+ renderType: Text.NativeRendering
text:{
// The property cluster size does not exist for older UM3 devices.
if(!base.selectedDevice || base.selectedDevice.clusterSize == null || base.selectedDevice.clusterSize == 1)
@@ -315,6 +327,7 @@ Cura.MachineAction
{
width: parent.width
wrapMode: Text.WordWrap
+ renderType: Text.NativeRendering
visible: base.selectedDevice != null && !base.completeProperties
text: catalog.i18nc("@label", "The printer at this address has not yet responded." )
}
@@ -358,9 +371,10 @@ Cura.MachineAction
Label
{
- text: catalog.i18nc("@alabel","Enter the IP address or hostname of your printer on the network.")
+ text: catalog.i18nc("@alabel", "Enter the IP address or hostname of your printer on the network.")
width: parent.width
wrapMode: Text.WordWrap
+ renderType: Text.NativeRendering
}
TextField
diff --git a/plugins/UM3NetworkPrinting/resources/qml/ExpandableCard.qml b/plugins/UM3NetworkPrinting/resources/qml/ExpandableCard.qml
new file mode 100644
index 0000000000..d4c123652d
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/resources/qml/ExpandableCard.qml
@@ -0,0 +1,87 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.2
+import QtQuick.Controls 2.0
+import UM 1.3 as UM
+import Cura 1.0 as Cura
+
+// TODO: Theme & documentation!
+// The expandable component has 3 major sub components:
+// * The headerItem Always visible and should hold some info about what happens if the component is expanded
+// * The popupItem The content that needs to be shown if the component is expanded.
+Item
+{
+ id: base
+
+ property bool expanded: false
+ property bool enabled: true
+ property var borderWidth: 1
+ property color borderColor: "#CCCCCC"
+ property color headerBackgroundColor: "white"
+ property color headerHoverColor: "#e8f2fc"
+ property color drawerBackgroundColor: "white"
+ property alias headerItem: header.children
+ property alias drawerItem: drawer.children
+
+ width: parent.width
+ height: childrenRect.height
+
+ Rectangle
+ {
+ id: header
+ border
+ {
+ color: borderColor
+ width: borderWidth
+ }
+ color: base.enabled && headerMouseArea.containsMouse ? headerHoverColor : headerBackgroundColor
+ height: childrenRect.height
+ width: parent.width
+ Behavior on color
+ {
+ ColorAnimation
+ {
+ duration: 100
+ }
+ }
+ }
+
+ MouseArea
+ {
+ id: headerMouseArea
+ anchors.fill: header
+ onClicked:
+ {
+ if (!base.enabled) return
+ base.expanded = !base.expanded
+ }
+ hoverEnabled: base.enabled
+ }
+
+ Rectangle
+ {
+ id: drawer
+ anchors
+ {
+ top: header.bottom
+ topMargin: -1
+ }
+ border
+ {
+ color: borderColor
+ width: borderWidth
+ }
+ clip: true
+ color: headerBackgroundColor
+ height: base.expanded ? childrenRect.height : 0
+ width: parent.width
+ Behavior on height
+ {
+ NumberAnimation
+ {
+ duration: 100
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/qml/HorizontalLine.qml b/plugins/UM3NetworkPrinting/resources/qml/HorizontalLine.qml
deleted file mode 100644
index aeb92697ad..0000000000
--- a/plugins/UM3NetworkPrinting/resources/qml/HorizontalLine.qml
+++ /dev/null
@@ -1,12 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.3
-import QtQuick.Controls 2.0
-import UM 1.3 as UM
-
-Rectangle {
- color: UM.Theme.getColor("monitor_lining_light"); // TODO: Maybe theme separately? Maybe not.
- height: UM.Theme.getSize("default_lining").height;
- width: parent.width;
-}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorBuildplateConfiguration.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorBuildplateConfiguration.qml
new file mode 100644
index 0000000000..192a5a7f76
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorBuildplateConfiguration.qml
@@ -0,0 +1,74 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.2
+import QtQuick.Controls 2.0
+import UM 1.3 as UM
+
+/**
+ * This component comprises a buildplate icon and the buildplate name. It is
+ * used by the MonitorPrinterConfiguration component along with two instances
+ * of MonitorExtruderConfiguration.
+ *
+ * NOTE: For most labels, a fixed height with vertical alignment is used to make
+ * layouts more deterministic (like the fixed-size textboxes used in original
+ * mock-ups). This is also a stand-in for CSS's 'line-height' property. Denoted
+ * with '// FIXED-LINE-HEIGHT:'.
+ */
+Item
+{
+ // The buildplate name
+ property var buildplate: null
+
+ // Height is one 18px label/icon
+ height: 18 * screenScaleFactor // TODO: Theme!
+ width: childrenRect.width
+
+ Row
+ {
+ height: parent.height
+ spacing: UM.Theme.getSize("print_setup_slider_handle").width // TODO: Theme! (Should be same as extruder spacing)
+
+ // This wrapper ensures that the buildplate icon is located centered
+ // below an extruder icon.
+ Item
+ {
+ height: parent.height
+ width: 32 * screenScaleFactor // Ensure the icon is centered under the extruder icon (same width)
+
+ Rectangle
+ {
+ anchors.centerIn: parent
+ height: parent.height
+ width: height
+ color: buildplateIcon.visible > 0 ? "transparent" : "#eeeeee" // TODO: Theme!
+ radius: Math.floor(height / 2)
+ }
+
+ UM.RecolorImage
+ {
+ id: buildplateIcon
+ anchors.centerIn: parent
+ color: "#0a0850" // TODO: Theme! (Standard purple)
+ height: parent.height
+ source: "../svg/icons/buildplate.svg"
+ width: height
+ visible: buildplate
+ }
+ }
+
+ Label
+ {
+ id: buildplateLabel
+ color: "#191919" // TODO: Theme!
+ elide: Text.ElideRight
+ font: UM.Theme.getFont("default") // 12pt, regular
+ text: buildplate ? buildplate : ""
+ visible: text !== ""
+
+ // FIXED-LINE-HEIGHT:
+ height: 18 * screenScaleFactor // TODO: Theme!
+ verticalAlignment: Text.AlignVCenter
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml
new file mode 100644
index 0000000000..de24ee5a8c
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorCarousel.qml
@@ -0,0 +1,258 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.3
+import QtQuick.Controls 2.0
+import QtGraphicalEffects 1.0
+import UM 1.3 as UM
+
+Item
+{
+ id: base
+
+ property var currentIndex: 0
+ property var tileWidth: 834 * screenScaleFactor // TODO: Theme!
+ property var tileHeight: 216 * screenScaleFactor // TODO: Theme!
+ property var tileSpacing: 60 * screenScaleFactor // TODO: Theme!
+
+ // Array/model of printers to populate the carousel with
+ property var printers: []
+
+ // Maximum distance the carousel can be shifted
+ property var maxOffset: (printers.length - 1) * (tileWidth + tileSpacing)
+
+ height: centerSection.height
+ width: maximumWidth
+
+ // Enable keyboard navigation
+ Keys.onLeftPressed: navigateTo(currentIndex - 1)
+ Keys.onRightPressed: navigateTo(currentIndex + 1)
+
+ Item
+ {
+ id: leftHint
+ anchors
+ {
+ right: leftButton.left
+ rightMargin: 12 * screenScaleFactor // TODO: Theme!
+ left: parent.left
+ }
+ height: parent.height
+ z: 10
+ LinearGradient
+ {
+ anchors.fill: parent
+ start: Qt.point(0, 0)
+ end: Qt.point(leftHint.width, 0)
+ gradient: Gradient
+ {
+ GradientStop
+ {
+ position: 0.0
+ color: "#fff6f6f6" // TODO: Theme!
+ }
+ GradientStop
+ {
+ position: 1.0
+ color: "#66f6f6f6" // TODO: Theme!
+ }
+ }
+ }
+ MouseArea
+ {
+ anchors.fill: parent
+ onClicked: navigateTo(currentIndex - 1)
+ }
+ }
+
+ Button
+ {
+ id: leftButton
+ anchors
+ {
+ verticalCenter: parent.verticalCenter
+ right: centerSection.left
+ rightMargin: 12 * screenScaleFactor // TODO: Theme!
+ }
+ width: 36 * screenScaleFactor // TODO: Theme!
+ height: 72 * screenScaleFactor // TODO: Theme!
+ visible: currentIndex > 0
+ hoverEnabled: true
+ z: 10
+ onClicked: navigateTo(currentIndex - 1)
+ background: Rectangle
+ {
+ color: leftButton.hovered ? "#e8f2fc" : "#ffffff" // TODO: Theme!
+ border.width: 1 * screenScaleFactor // TODO: Theme!
+ border.color: "#cccccc" // TODO: Theme!
+ radius: 2 * screenScaleFactor // TODO: Theme!
+ }
+ contentItem: Item
+ {
+ anchors.fill: parent
+ UM.RecolorImage
+ {
+ anchors.centerIn: parent
+ width: 18 // TODO: Theme!
+ height: width // TODO: Theme!
+ sourceSize.width: width // TODO: Theme!
+ sourceSize.height: width // TODO: Theme!
+ color: "#152950" // TODO: Theme!
+ source: UM.Theme.getIcon("arrow_left")
+ }
+ }
+ }
+
+ Item
+ {
+ id: centerSection
+ anchors
+ {
+ verticalCenter: parent.verticalCenter
+ horizontalCenter: parent.horizontalCenter
+ }
+ width: tileWidth
+ height: tiles.height
+ z: 1
+
+ Row
+ {
+ id: tiles
+ height: childrenRect.height
+ width: 5 * tileWidth + 4 * tileSpacing // TODO: Theme!
+ x: 0
+ z: 0
+ Behavior on x
+ {
+ NumberAnimation
+ {
+ duration: 200
+ easing.type: Easing.InOutCubic
+ }
+ }
+ spacing: 60 * screenScaleFactor // TODO: Theme!
+
+ Repeater
+ {
+ model: printers
+ MonitorPrinterCard
+ {
+ printer: modelData
+ enabled: model.index == currentIndex
+ }
+ }
+ }
+ }
+
+ Button
+ {
+ id: rightButton
+ anchors
+ {
+ verticalCenter: parent.verticalCenter
+ left: centerSection.right
+ leftMargin: 12 * screenScaleFactor // TODO: Theme!
+ }
+ width: 36 * screenScaleFactor // TODO: Theme!
+ height: 72 * screenScaleFactor // TODO: Theme!
+ z: 10
+ visible: currentIndex < printers.length - 1
+ onClicked: navigateTo(currentIndex + 1)
+ hoverEnabled: true
+ background: Rectangle
+ {
+ color: rightButton.hovered ? "#e8f2fc" : "#ffffff" // TODO: Theme!
+ border.width: 1 * screenScaleFactor // TODO: Theme!
+ border.color: "#cccccc" // TODO: Theme!
+ radius: 2 * screenScaleFactor // TODO: Theme!
+ }
+ contentItem: Item
+ {
+ anchors.fill: parent
+ UM.RecolorImage
+ {
+ anchors.centerIn: parent
+ width: 18 // TODO: Theme!
+ height: width // TODO: Theme!
+ sourceSize.width: width // TODO: Theme!
+ sourceSize.height: width // TODO: Theme!
+ color: "#152950" // TODO: Theme!
+ source: UM.Theme.getIcon("arrow_right")
+ }
+ }
+ }
+
+ Item
+ {
+ id: rightHint
+ anchors
+ {
+ left: rightButton.right
+ leftMargin: 12 * screenScaleFactor // TODO: Theme!
+ right: parent.right
+ }
+ height: centerSection.height
+ z: 10
+
+ LinearGradient
+ {
+ anchors.fill: parent
+ start: Qt.point(0, 0)
+ end: Qt.point(rightHint.width, 0)
+ gradient: Gradient
+ {
+ GradientStop
+ {
+ position: 0.0
+ color: "#66f6f6f6" // TODO: Theme!
+ }
+ GradientStop
+ {
+ position: 1.0
+ color: "#fff6f6f6" // TODO: Theme!
+ }
+ }
+ }
+ MouseArea
+ {
+ anchors.fill: parent
+ onClicked: navigateTo(currentIndex + 1)
+ }
+ }
+
+ Row
+ {
+ id: navigationDots
+ anchors
+ {
+ horizontalCenter: centerSection.horizontalCenter
+ top: centerSection.bottom
+ topMargin: 36 * screenScaleFactor // TODO: Theme!
+ }
+ spacing: 8 * screenScaleFactor // TODO: Theme!
+ visible: printers.length > 1
+ Repeater
+ {
+ model: printers
+ Button
+ {
+ background: Rectangle
+ {
+ color: model.index == currentIndex ? "#777777" : "#d8d8d8" // TODO: Theme!
+ radius: Math.floor(width / 2)
+ width: 12 * screenScaleFactor // TODO: Theme!
+ height: width // TODO: Theme!
+ }
+ onClicked: navigateTo(model.index)
+ }
+ }
+ }
+
+ function navigateTo( i ) {
+ if (i >= 0 && i < printers.length)
+ {
+ tiles.x = -1 * i * (tileWidth + tileSpacing)
+ currentIndex = i
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml b/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml
new file mode 100644
index 0000000000..1718994d83
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/resources/qml/MonitorConfigOverrideDialog.qml
@@ -0,0 +1,142 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.3
+import QtQuick.Controls 1.4
+import QtQuick.Layouts 1.3
+import QtQuick.Dialogs 1.2
+import UM 1.3 as UM
+
+UM.Dialog
+{
+ id: overrideConfirmationDialog
+
+ property var printer: null
+
+ minimumWidth: screenScaleFactor * 640;
+ minimumHeight: screenScaleFactor * 320;
+ width: minimumWidth
+ height: minimumHeight
+ title: catalog.i18nc("@title:window", "Configuration Changes")
+ rightButtons:
+ [
+ Button
+ {
+ id: overrideButton
+ anchors.margins: UM.Theme.getSize("default_margin").width
+ text: catalog.i18nc("@action:button", "Override")
+ onClicked:
+ {
+ OutputDevice.forceSendJob(printer.activePrintJob.key)
+ overrideConfirmationDialog.close()
+ }
+ },
+ Button
+ {
+ id: cancelButton
+ anchors.margins: UM.Theme.getSize("default_margin").width
+ text: catalog.i18nc("@action:button", "Cancel")
+ onClicked:
+ {
+ overrideConfirmationDialog.reject()
+ }
+ }
+ ]
+
+ Label
+ {
+ anchors
+ {
+ fill: parent
+ margins: 36 * screenScaleFactor // TODO: Theme!
+ bottomMargin: 56 * screenScaleFactor // TODO: Theme!
+ }
+ wrapMode: Text.WordWrap
+ text:
+ {
+ if (!printer || !printer.activePrintJob)
+ {
+ return ""
+ }
+ var topLine
+ if (materialsAreKnown(printer.activePrintJob))
+ {
+ topLine = catalog.i18ncp("@label", "The assigned printer, %1, requires the following configuration change:", "The assigned printer, %1, requires the following configuration changes:", printer.activePrintJob.configurationChanges.length).arg(printer.name)
+ }
+ else
+ {
+ topLine = catalog.i18nc("@label", "The printer %1 is assigned, but the job contains an unknown material configuration.").arg(printer.name)
+ }
+ var result = "
" + topLine +"
\n\n"
+ for (var i = 0; i < printer.activePrintJob.configurationChanges.length; i++)
+ {
+ var change = printer.activePrintJob.configurationChanges[i]
+ var text
+ switch (change.typeOfChange)
+ {
+ case "material_change":
+ text = catalog.i18nc("@label", "Change material %1 from %2 to %3.").arg(change.index + 1).arg(change.originName).arg(change.targetName)
+ break
+ case "material_insert":
+ text = catalog.i18nc("@label", "Load %3 as material %1 (This cannot be overridden).").arg(change.index + 1).arg(change.targetName)
+ break
+ case "print_core_change":
+ text = catalog.i18nc("@label", "Change print core %1 from %2 to %3.").arg(change.index + 1).arg(change.originName).arg(change.targetName)
+ break
+ case "buildplate_change":
+ text = catalog.i18nc("@label", "Change build plate to %1 (This cannot be overridden).").arg(formatBuildPlateType(change.target_name))
+ break
+ default:
+ text = "unknown"
+ }
+ result += "
" + text + "
\n\n"
+ }
+ var bottomLine = catalog.i18nc("@label", "Override will use the specified settings with the existing printer configuration. This may result in a failed print.")
+ result += "
";
- for (var i = 0; i < printJob.configurationChanges.length; i++) {
- var change = printJob.configurationChanges[i];
- var text;
- switch (change.typeOfChange) {
- case "material_change":
- text = catalog.i18nc("@label", "Change material %1 from %2 to %3.").arg(change.index + 1).arg(change.originName).arg(change.targetName);
- break;
- case "material_insert":
- text = catalog.i18nc("@label", "Load %3 as material %1 (This cannot be overridden).").arg(change.index + 1).arg(change.targetName);
- break;
- case "print_core_change":
- text = catalog.i18nc("@label", "Change print core %1 from %2 to %3.").arg(change.index + 1).arg(change.originName).arg(change.targetName);
- break;
- case "buildplate_change":
- text = catalog.i18nc("@label", "Change build plate to %1 (This cannot be overridden).").arg(formatBuildPlateType(change.target_name));
- break;
- default:
- text = "";
- }
- result += "
" + text + "
";
- }
- return result;
- }
- wrapMode: Text.WordWrap;
- }
-
- Button {
- anchors {
- bottom: parent.bottom;
- left: parent.left;
- }
- background: Rectangle {
- border {
- color: UM.Theme.getColor("monitor_lining_heavy");
- width: UM.Theme.getSize("default_lining").width;
- }
- color: parent.hovered ? UM.Theme.getColor("monitor_card_background_inactive") : UM.Theme.getColor("monitor_card_background");
- implicitHeight: UM.Theme.getSize("default_margin").height * 3;
- implicitWidth: UM.Theme.getSize("default_margin").height * 8;
- }
- contentItem: Label {
- color: UM.Theme.getColor("text");
- font: UM.Theme.getFont("medium");
- horizontalAlignment: Text.AlignHCenter;
- text: parent.text;
- verticalAlignment: Text.AlignVCenter;
- }
- onClicked: {
- overrideConfirmationDialog.visible = true;
- }
- text: catalog.i18nc("@label", "Override");
- visible: {
- if (printJob && printJob.configurationChanges) {
- var length = printJob.configurationChanges.length;
- for (var i = 0; i < length; i++) {
- var typeOfChange = printJob.configurationChanges[i].typeOfChange;
- if (typeOfChange === "material_insert" || typeOfChange === "buildplate_change") {
- return false;
- }
- }
- }
- return true;
- }
- }
- }
- }
-
- MessageDialog {
- id: overrideConfirmationDialog;
- Component.onCompleted: visible = false;
- icon: StandardIcon.Warning;
- onYes: OutputDevice.forceSendJob(printJob.key);
- standardButtons: StandardButton.Yes | StandardButton.No;
- text: {
- if (!printJob) {
- return "";
- }
- var printJobName = formatPrintJobName(printJob.name);
- var confirmText = catalog.i18nc("@label", "Starting a print job with an incompatible configuration could damage your 3D printer. Are you sure you want to override the configuration and print %1?").arg(printJobName);
- return confirmText;
- }
- title: catalog.i18nc("@window:title", "Override configuration configuration and start print");
- }
- }
- }
- }
- // Utils
- function formatPrintJobName(name) {
- var extensions = [ ".gz", ".gcode", ".ufp" ];
- for (var i = 0; i < extensions.length; i++) {
- var extension = extensions[i];
- if (name.slice(-extension.length) === extension) {
- name = name.substring(0, name.length - extension.length);
- }
- }
- return name;
- }
- function materialsAreKnown(job) {
- var conf0 = job.configuration[0];
- if (conf0 && !conf0.material.material) {
- return false;
- }
- var conf1 = job.configuration[1];
- if (conf1 && !conf1.material.material) {
- return false;
- }
- return true;
- }
- function formatBuildPlateType(buildPlateType) {
- var translationText = "";
- switch (buildPlateType) {
- case "glass":
- translationText = catalog.i18nc("@label", "Glass");
- break;
- case "aluminum":
- translationText = catalog.i18nc("@label", "Aluminum");
- break;
- default:
- translationText = null;
- }
- return translationText;
- }
-}
diff --git a/plugins/UM3NetworkPrinting/resources/qml/PrintJobPreview.qml b/plugins/UM3NetworkPrinting/resources/qml/PrintJobPreview.qml
deleted file mode 100644
index b1a73255f4..0000000000
--- a/plugins/UM3NetworkPrinting/resources/qml/PrintJobPreview.qml
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.3
-import QtQuick.Dialogs 1.1
-import QtQuick.Controls 2.0
-import QtQuick.Controls.Styles 1.3
-import QtGraphicalEffects 1.0
-import QtQuick.Controls 1.4 as LegacyControls
-import UM 1.3 as UM
-
-// Includes print job name, owner, and preview
-
-Item {
- property var job: null;
- property var useUltibot: false;
- height: 100 * screenScaleFactor;
- width: height;
-
- // Skeleton
- Rectangle {
- anchors.fill: parent;
- color: UM.Theme.getColor("monitor_skeleton_fill");
- radius: UM.Theme.getSize("default_margin").width;
- visible: !job;
- }
-
- // Actual content
- Image {
- id: previewImage;
- visible: job;
- source: job ? job.previewImageUrl : "";
- opacity: {
- if (job == null) {
- return 1.0;
- }
- var states = ["wait_cleanup", "wait_user_action", "error", "paused"];
- if (states.indexOf(job.state) !== -1) {
- return 0.5;
- }
- return 1.0;
- }
- anchors.fill: parent;
- }
-
- UM.RecolorImage {
- id: ultibotImage;
- anchors.centerIn: parent;
- color: UM.Theme.getColor("monitor_placeholder_image"); // TODO: Theme!
- height: parent.height;
- source: "../svg/ultibot.svg";
- sourceSize {
- height: height;
- width: width;
- }
- /* Since print jobs ALWAYS have an image url, we have to check if that image URL errors or
- not in order to determine if we show the placeholder (ultibot) image instead. */
- visible: job && previewImage.status == Image.Error;
- width: parent.width;
- }
-
- UM.RecolorImage {
- id: statusImage;
- anchors.centerIn: parent;
- color: "black"; // TODO: Theme!
- height: Math.round(0.5 * parent.height);
- source: job && job.state == "error" ? "../svg/aborted-icon.svg" : "";
- sourceSize {
- height: height;
- width: width;
- }
- visible: source != "";
- width: Math.round(0.5 * parent.width);
- }
-}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/qml/PrintJobTitle.qml b/plugins/UM3NetworkPrinting/resources/qml/PrintJobTitle.qml
deleted file mode 100644
index f9f7b5ae87..0000000000
--- a/plugins/UM3NetworkPrinting/resources/qml/PrintJobTitle.qml
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.3
-import QtQuick.Controls 2.0
-import UM 1.3 as UM
-
-Column {
- property var job: null;
- height: childrenRect.height;
- spacing: Math.floor( UM.Theme.getSize("default_margin").height / 2); // TODO: Use explicit theme size
- width: parent.width;
-
- Item {
- id: jobName;
- height: UM.Theme.getSize("monitor_text_line").height;
- width: parent.width;
-
- // Skeleton loading
- Rectangle {
- color: UM.Theme.getColor("monitor_skeleton_fill");
- height: parent.height;
- visible: !job;
- width: Math.round(parent.width / 3);
- }
-
- Label {
- anchors.fill: parent;
- color: UM.Theme.getColor("text");
- elide: Text.ElideRight;
- font: UM.Theme.getFont("default_bold");
- text: job && job.name ? job.name : "";
- visible: job;
- }
- }
-
- Item {
- id: ownerName;
- height: UM.Theme.getSize("monitor_text_line").height;
- width: parent.width;
-
- // Skeleton loading
- Rectangle {
- color: UM.Theme.getColor("monitor_skeleton_fill");
- height: parent.height;
- visible: !job;
- width: Math.round(parent.width / 2);
- }
-
- Label {
- anchors.fill: parent;
- color: UM.Theme.getColor("text")
- elide: Text.ElideRight;
- font: UM.Theme.getFont("default");
- text: job ? job.owner : "";
- visible: job;
- }
- }
-}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/qml/PrinterCard.qml b/plugins/UM3NetworkPrinting/resources/qml/PrinterCard.qml
deleted file mode 100644
index 24beaf70fe..0000000000
--- a/plugins/UM3NetworkPrinting/resources/qml/PrinterCard.qml
+++ /dev/null
@@ -1,241 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.3
-import QtQuick.Dialogs 1.1
-import QtQuick.Controls 2.0
-import QtQuick.Controls.Styles 1.3
-import QtGraphicalEffects 1.0
-import UM 1.3 as UM
-
-Item {
- id: root;
- property var shadowRadius: UM.Theme.getSize("monitor_shadow_radius").width;
- property var shadowOffset: UM.Theme.getSize("monitor_shadow_offset").width;
- property var printer: null;
- property var collapsed: true;
- height: childrenRect.height + shadowRadius * 2; // Bubbles upward
- width: parent.width; // Bubbles downward
-
- // The actual card (white block)
- Rectangle {
- // 5px margin, but shifted 2px vertically because of the shadow
- anchors {
- bottomMargin: root.shadowRadius + root.shadowOffset;
- leftMargin: root.shadowRadius;
- rightMargin: root.shadowRadius;
- topMargin: root.shadowRadius - root.shadowOffset;
- }
- color: {
- if (!printer) {
- return UM.Theme.getColor("monitor_card_background_inactive");
- }
- if (printer.state == "disabled") {
- return UM.Theme.getColor("monitor_card_background_inactive");
- } else {
- return UM.Theme.getColor("monitor_card_background");
- }
- }
- height: childrenRect.height;
- layer.effect: DropShadow {
- radius: root.shadowRadius;
- verticalOffset: root.shadowOffset;
- color: "#3F000000"; // 25% shadow
- }
- layer.enabled: true
- width: parent.width - 2 * shadowRadius;
-
- Column {
- id: cardContents;
- height: childrenRect.height;
- width: parent.width;
-
- // Main card
- Item {
- id: mainCard;
- anchors {
- left: parent.left;
- leftMargin: UM.Theme.getSize("default_margin").width;
- right: parent.right;
- rightMargin: UM.Theme.getSize("default_margin").width;
- }
- height: 60 * screenScaleFactor + 2 * UM.Theme.getSize("default_margin").height;
- width: parent.width;
-
- // Machine icon
- Item {
- id: machineIcon;
- anchors.verticalCenter: parent.verticalCenter;
- height: parent.height - 2 * UM.Theme.getSize("default_margin").width;
- width: height;
-
- // Skeleton
- Rectangle {
- anchors.fill: parent;
- color: UM.Theme.getColor("monitor_skeleton_fill_dark");
- radius: UM.Theme.getSize("default_margin").width;
- visible: !printer;
- }
-
- // Content
- UM.RecolorImage {
- anchors.centerIn: parent;
- color: {
- if (printer && printer.activePrintJob != undefined) {
- return UM.Theme.getColor("monitor_printer_icon");
- }
- return UM.Theme.getColor("monitor_printer_icon_inactive");
- }
- height: sourceSize.height;
- source: {
- if (!printer) {
- return "";
- }
- switch(printer.type) {
- case "Ultimaker 3":
- return "../svg/UM3-icon.svg";
- case "Ultimaker 3 Extended":
- return "../svg/UM3x-icon.svg";
- case "Ultimaker S5":
- return "../svg/UMs5-icon.svg";
- }
- }
- visible: printer;
- width: sourceSize.width;
- }
- }
-
- // Printer info
- Item {
- id: printerInfo;
- anchors {
- left: machineIcon.right;
- leftMargin: UM.Theme.getSize("wide_margin").width;
- right: collapseIcon.left;
- verticalCenter: machineIcon.verticalCenter;
- }
- height: childrenRect.height;
-
- // Machine name
- Item {
- id: machineNameLabel;
- height: UM.Theme.getSize("monitor_text_line").height;
- width: {
- var percent = printer ? 0.75 : 0.3;
- return Math.round(parent.width * percent);
- }
-
- // Skeleton
- Rectangle {
- anchors.fill: parent;
- color: UM.Theme.getColor("monitor_skeleton_fill_dark");
- visible: !printer;
- }
-
- // Actual content
- Label {
- anchors.fill: parent;
- color: UM.Theme.getColor("text");
- elide: Text.ElideRight;
- font: UM.Theme.getFont("default_bold");
- text: printer ? printer.name : "";
- visible: printer;
- width: parent.width;
- }
- }
-
- // Job name
- Item {
- id: activeJobLabel;
- anchors {
- top: machineNameLabel.bottom;
- topMargin: Math.round(UM.Theme.getSize("default_margin").height / 2);
- }
- height: UM.Theme.getSize("monitor_text_line").height;
- width: Math.round(parent.width * 0.75);
-
- // Skeleton
- Rectangle {
- anchors.fill: parent;
- color: UM.Theme.getColor("monitor_skeleton_fill_dark");
- visible: !printer;
- }
-
- // Actual content
- Label {
- anchors.fill: parent;
- color: UM.Theme.getColor("monitor_text_inactive");
- elide: Text.ElideRight;
- font: UM.Theme.getFont("default");
- text: {
- if (!printer) {
- return "";
- }
- if (printer.state == "disabled") {
- return catalog.i18nc("@label", "Not available");
- } else if (printer.state == "unreachable") {
- return catalog.i18nc("@label", "Unreachable");
- }
- if (printer.activePrintJob != null && printer.activePrintJob.name) {
- return printer.activePrintJob.name;
- }
- return catalog.i18nc("@label", "Available");
- }
- visible: printer;
- }
- }
- }
-
- // Collapse icon
- UM.RecolorImage {
- id: collapseIcon;
- anchors {
- right: parent.right;
- rightMargin: UM.Theme.getSize("default_margin").width;
- verticalCenter: parent.verticalCenter;
- }
- color: UM.Theme.getColor("text");
- height: 15 * screenScaleFactor; // TODO: Theme!
- source: root.collapsed ? UM.Theme.getIcon("arrow_left") : UM.Theme.getIcon("arrow_bottom");
- sourceSize {
- height: height;
- width: width;
- }
- visible: printer;
- width: 15 * screenScaleFactor; // TODO: Theme!
- }
-
- MouseArea {
- anchors.fill: parent;
- enabled: printer;
- onClicked: {
- if (model && root.collapsed) {
- printerList.currentIndex = model.index;
- } else {
- printerList.currentIndex = -1;
- }
- }
- }
-
- Connections {
- target: printerList;
- onCurrentIndexChanged: {
- root.collapsed = printerList.currentIndex != model.index;
- }
- }
- }
- // Detailed card
- PrinterCardDetails {
- collapsed: root.collapsed;
- printer: root.printer;
- visible: root.printer;
- }
-
- // Progress bar
- PrinterCardProgressBar {
- visible: printer && printer.activePrintJob != null;
- width: parent.width;
- }
- }
- }
-}
diff --git a/plugins/UM3NetworkPrinting/resources/qml/PrinterCardDetails.qml b/plugins/UM3NetworkPrinting/resources/qml/PrinterCardDetails.qml
deleted file mode 100644
index 31da388b00..0000000000
--- a/plugins/UM3NetworkPrinting/resources/qml/PrinterCardDetails.qml
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.3
-import QtQuick.Dialogs 1.1
-import QtQuick.Controls 2.0
-import QtQuick.Controls.Styles 1.3
-import QtGraphicalEffects 1.0
-import QtQuick.Controls 1.4 as LegacyControls
-import UM 1.3 as UM
-
-Item {
- id: root;
- property var printer: null;
- property var printJob: printer ? printer.activePrintJob : null;
- property var collapsed: true;
- Behavior on height { NumberAnimation { duration: 100 } }
- Behavior on opacity { NumberAnimation { duration: 100 } }
- height: collapsed ? 0 : childrenRect.height;
- opacity: collapsed ? 0 : 1;
- width: parent.width;
-
- Column {
- id: contentColumn;
- anchors {
- left: parent.left;
- leftMargin: UM.Theme.getSize("default_margin").width;
- right: parent.right;
- rightMargin: UM.Theme.getSize("default_margin").width;
- }
- height: childrenRect.height + UM.Theme.getSize("default_margin").height;
- spacing: UM.Theme.getSize("default_margin").height;
- width: parent.width;
-
- HorizontalLine {}
-
- PrinterInfoBlock {
- printer: root.printer;
- printJob: root.printer ? root.printer.activePrintJob : null;
- }
-
- HorizontalLine {}
-
- Row {
- height: childrenRect.height;
- visible: root.printJob;
- width: parent.width;
-
- PrintJobTitle {
- job: root.printer ? root.printer.activePrintJob : null;
- }
- PrintJobContextMenu {
- id: contextButton;
- anchors {
- right: parent.right;
- rightMargin: UM.Theme.getSize("wide_margin").width;
- }
- printJob: root.printer ? root.printer.activePrintJob : null;
- visible: printJob;
- }
- }
-
- PrintJobPreview {
- anchors.horizontalCenter: parent.horizontalCenter;
- job: root.printer && root.printer.activePrintJob ? root.printer.activePrintJob : null;
- visible: root.printJob;
- }
-
- CameraButton {
- id: showCameraButton;
- iconSource: "../svg/camera-icon.svg";
- visible: root.printer;
- }
- }
-}
diff --git a/plugins/UM3NetworkPrinting/resources/qml/PrinterCardProgressBar.qml b/plugins/UM3NetworkPrinting/resources/qml/PrinterCardProgressBar.qml
deleted file mode 100644
index e86c959b8c..0000000000
--- a/plugins/UM3NetworkPrinting/resources/qml/PrinterCardProgressBar.qml
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.3
-import QtQuick.Controls.Styles 1.3
-import QtQuick.Controls 1.4
-import UM 1.3 as UM
-
-ProgressBar {
- property var progress: {
- if (!printer || printer.activePrintJob == null) {
- return 0;
- }
- var result = printer.activePrintJob.timeElapsed / printer.activePrintJob.timeTotal;
- if (result > 1.0) {
- result = 1.0;
- }
- return result;
- }
- style: ProgressBarStyle {
- property var remainingTime: {
- if (!printer || printer.activePrintJob == null) {
- return 0;
- }
- /* Sometimes total minus elapsed is less than 0. Use Math.max() to prevent remaining
- time from ever being less than 0. Negative durations cause strange behavior such
- as displaying "-1h -1m". */
- return Math.max(printer.activePrintJob.timeTotal - printer.activePrintJob.timeElapsed, 0);
- }
- property var progressText: {
- if (printer === null ) {
- return "";
- }
- switch (printer.activePrintJob.state) {
- case "wait_cleanup":
- if (printer.activePrintJob.timeTotal > printer.activePrintJob.timeElapsed) {
- return catalog.i18nc("@label:status", "Aborted");
- }
- return catalog.i18nc("@label:status", "Finished");
- case "pre_print":
- case "sent_to_printer":
- return catalog.i18nc("@label:status", "Preparing");
- case "aborted":
- return catalog.i18nc("@label:status", "Aborted");
- case "wait_user_action":
- return catalog.i18nc("@label:status", "Aborted");
- case "pausing":
- return catalog.i18nc("@label:status", "Pausing");
- case "paused":
- return OutputDevice.formatDuration( remainingTime );
- case "resuming":
- return catalog.i18nc("@label:status", "Resuming");
- case "queued":
- return catalog.i18nc("@label:status", "Action required");
- default:
- return OutputDevice.formatDuration( remainingTime );
- }
- }
- background: Rectangle {
- color: UM.Theme.getColor("monitor_progress_background");
- implicitHeight: visible ? 24 : 0;
- implicitWidth: 100;
- }
- progress: Rectangle {
- id: progressItem;
- color: {
- if (! printer || !printer.activePrintJob) {
- return "black";
- }
- var state = printer.activePrintJob.state
- var inactiveStates = [
- "pausing",
- "paused",
- "resuming",
- "wait_cleanup"
- ];
- if (inactiveStates.indexOf(state) > -1 && remainingTime > 0) {
- return UM.Theme.getColor("monitor_progress_fill_inactive");
- } else {
- return UM.Theme.getColor("monitor_progress_fill");
- }
- }
-
- Label {
- id: progressLabel;
- anchors {
- left: parent.left;
- leftMargin: getTextOffset();
- }
- text: progressText;
- anchors.verticalCenter: parent.verticalCenter;
- color: progressItem.width + progressLabel.width < control.width ? UM.Theme.getColor("text") : UM.Theme.getColor("monitor_progress_fill_text");
- width: contentWidth;
- font: UM.Theme.getFont("default");
- }
-
- function getTextOffset() {
- if (progressItem.width + progressLabel.width + 16 < control.width) {
- return progressItem.width + UM.Theme.getSize("default_margin").width;
- } else {
- return progressItem.width - progressLabel.width - UM.Theme.getSize("default_margin").width;
- }
- }
- }
- }
- value: progress;
- width: parent.width;
-}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/qml/PrinterFamilyPill.qml b/plugins/UM3NetworkPrinting/resources/qml/PrinterFamilyPill.qml
deleted file mode 100644
index 0a88b053a8..0000000000
--- a/plugins/UM3NetworkPrinting/resources/qml/PrinterFamilyPill.qml
+++ /dev/null
@@ -1,32 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.2
-import QtQuick.Controls 1.4
-import UM 1.2 as UM
-
-Item {
- property alias text: familyNameLabel.text;
- property var padding: 3 * screenScaleFactor; // TODO: Theme!
- implicitHeight: familyNameLabel.contentHeight + 2 * padding; // Apply the padding to top and bottom.
- implicitWidth: Math.max(48 * screenScaleFactor, familyNameLabel.contentWidth + implicitHeight); // The extra height is added to ensure the radius doesn't cut something off.
-
- Rectangle {
- id: background;
- anchors {
- horizontalCenter: parent.horizontalCenter;
- right: parent.right;
- }
- color: familyNameLabel.text.length < 1 ? UM.Theme.getColor("monitor_skeleton_fill") : UM.Theme.getColor("monitor_pill_background");
- height: parent.height;
- radius: 0.5 * height;
- width: parent.width;
- }
-
- Label {
- id: familyNameLabel;
- anchors.centerIn: parent;
- color: UM.Theme.getColor("text");
- text: "";
- }
-}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/qml/PrinterInfoBlock.qml b/plugins/UM3NetworkPrinting/resources/qml/PrinterInfoBlock.qml
deleted file mode 100644
index 92a8f1dcb3..0000000000
--- a/plugins/UM3NetworkPrinting/resources/qml/PrinterInfoBlock.qml
+++ /dev/null
@@ -1,83 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.3
-import QtQuick.Dialogs 1.1
-import QtQuick.Controls 2.0
-import QtQuick.Controls.Styles 1.3
-import QtGraphicalEffects 1.0
-import QtQuick.Controls 1.4 as LegacyControls
-import UM 1.3 as UM
-
-// Includes printer type pill and extuder configurations
-
-Item {
- id: root;
- property var printer: null;
- property var printJob: null;
- width: parent.width;
- height: childrenRect.height;
-
- // Printer family pills
- Row {
- id: printerFamilyPills;
- anchors {
- left: parent.left;
- right: parent.right;
- }
- height: childrenRect.height;
- spacing: Math.round(0.5 * UM.Theme.getSize("default_margin").width);
- width: parent.width;
-
- Repeater {
- id: compatiblePills;
- delegate: PrinterFamilyPill {
- text: modelData;
- }
- model: printJob ? printJob.compatibleMachineFamilies : [];
- visible: printJob;
-
- }
-
- PrinterFamilyPill {
- text: printer ? printer.type : "";
- visible: !compatiblePills.visible && printer;
- }
- }
-
- // Extruder info
- Row {
- id: extrudersInfo;
- anchors {
- left: parent.left;
- right: parent.right;
- rightMargin: UM.Theme.getSize("default_margin").width;
- top: printerFamilyPills.bottom;
- topMargin: UM.Theme.getSize("default_margin").height;
- }
- height: childrenRect.height;
- spacing: UM.Theme.getSize("default_margin").width;
- width: parent.width;
-
- PrintCoreConfiguration {
- width: Math.round(parent.width / 2) * screenScaleFactor;
- printCoreConfiguration: getExtruderConfig(0);
- }
-
- PrintCoreConfiguration {
- width: Math.round(parent.width / 2) * screenScaleFactor;
- printCoreConfiguration: getExtruderConfig(1);
- }
- }
-
- function getExtruderConfig( i ) {
- if (root.printJob) {
- // Use more-specific print job if possible
- return root.printJob.configuration.extruderConfigurations[i];
- }
- if (root.printer) {
- return root.printer.printerConfiguration.extruderConfigurations[i];
- }
- return null;
- }
-}
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml b/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml
index 105143c851..c99ed1688e 100644
--- a/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml
+++ b/plugins/UM3NetworkPrinting/resources/qml/UM3InfoComponents.qml
@@ -13,8 +13,40 @@ Item {
property string activeQualityDefinitionId: Cura.MachineManager.activeQualityDefinitionId;
property bool isUM3: activeQualityDefinitionId == "ultimaker3" || activeQualityDefinitionId.match("ultimaker_") != null;
property bool printerConnected: Cura.MachineManager.printerConnected;
- property bool printerAcceptsCommands: printerConnected && Cura.MachineManager.printerOutputDevices[0].acceptsCommands;
- property bool authenticationRequested: printerConnected && (Cura.MachineManager.printerOutputDevices[0].authenticationState == 2 || Cura.MachineManager.printerOutputDevices[0].authenticationState == 5); // AuthState.AuthenticationRequested or AuthenticationReceived.
+ property bool printerAcceptsCommands:
+ {
+ if (printerConnected && Cura.MachineManager.printerOutputDevices[0])
+ {
+ return Cura.MachineManager.printerOutputDevices[0].acceptsCommands
+ }
+ return false
+ }
+ property bool authenticationRequested:
+ {
+ if (printerConnected && Cura.MachineManager.printerOutputDevices[0])
+ {
+ var device = Cura.MachineManager.printerOutputDevices[0]
+ // AuthState.AuthenticationRequested or AuthState.AuthenticationReceived
+ return device.authenticationState == 2 || device.authenticationState == 5
+ }
+ return false
+ }
+ property var materialNames:
+ {
+ if (printerConnected && Cura.MachineManager.printerOutputDevices[0])
+ {
+ return Cura.MachineManager.printerOutputDevices[0].materialNames
+ }
+ return null
+ }
+ property var hotendIds:
+ {
+ if (printerConnected && Cura.MachineManager.printerOutputDevices[0])
+ {
+ return Cura.MachineManager.printerOutputDevices[0].hotendIds
+ }
+ return null
+ }
UM.I18nCatalog {
id: catalog;
@@ -29,7 +61,7 @@ Item {
Button {
height: UM.Theme.getSize("save_button_save_to_button").height;
onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication();
- style: UM.Theme.styles.sidebar_action_button;
+ style: UM.Theme.styles.print_setup_action_button;
text: catalog.i18nc("@action:button", "Request Access");
tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer");
visible: printerConnected && !printerAcceptsCommands && !authenticationRequested;
@@ -38,7 +70,7 @@ Item {
Button {
height: UM.Theme.getSize("save_button_save_to_button").height;
onClicked: connectActionDialog.show();
- style: UM.Theme.styles.sidebar_action_button;
+ style: UM.Theme.styles.print_setup_action_button;
text: catalog.i18nc("@action:button", "Connect");
tooltip: catalog.i18nc("@info:tooltip", "Connect to a printer");
visible: !printerConnected;
@@ -58,69 +90,4 @@ Item {
source: "DiscoverUM3Action.qml";
}
}
-
- Column {
- anchors.fill: parent;
- objectName: "networkPrinterConnectionInfo";
- spacing: UM.Theme.getSize("default_margin").width;
- visible: isUM3;
-
- Button {
- onClicked: Cura.MachineManager.printerOutputDevices[0].requestAuthentication();
- text: catalog.i18nc("@action:button", "Request Access");
- tooltip: catalog.i18nc("@info:tooltip", "Send access request to the printer");
- visible: printerConnected && !printerAcceptsCommands && !authenticationRequested;
- }
-
- Row {
- anchors {
- left: parent.left;
- right: parent.right;
- }
- height: childrenRect.height;
- spacing: UM.Theme.getSize("default_margin").width;
- visible: printerConnected;
-
- Column {
- Repeater {
- model: Cura.ExtrudersModel {
- simpleNames: true;
- }
-
- Label {
- text: model.name;
- }
- }
- }
-
- Column {
- Repeater {
- id: nozzleColumn;
- model: printerConnected ? Cura.MachineManager.printerOutputDevices[0].hotendIds : null;
-
- Label {
- text: nozzleColumn.model[index];
- }
- }
- }
-
- Column {
- Repeater {
- id: materialColumn;
- model: printerConnected ? Cura.MachineManager.printerOutputDevices[0].materialNames : null;
-
- Label {
- text: materialColumn.model[index];
- }
- }
- }
- }
-
- Button {
- onClicked: manager.loadConfigurationFromPrinter();
- text: catalog.i18nc("@action:button", "Activate Configuration");
- tooltip: catalog.i18nc("@info:tooltip", "Load the configuration of the printer into Cura");
- visible: false; // printerConnected && !isClusterPrinter()
- }
- }
}
diff --git a/plugins/UM3NetworkPrinting/resources/svg/icons/buildplate.svg b/plugins/UM3NetworkPrinting/resources/svg/icons/buildplate.svg
new file mode 100644
index 0000000000..bcb278a8ca
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/resources/svg/icons/buildplate.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/svg/icons/camera.svg b/plugins/UM3NetworkPrinting/resources/svg/icons/camera.svg
new file mode 100644
index 0000000000..2eaebb812d
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/resources/svg/icons/camera.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/resources/svg/icons/extruder.svg b/plugins/UM3NetworkPrinting/resources/svg/icons/extruder.svg
new file mode 100644
index 0000000000..235cb432e9
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/resources/svg/icons/extruder.svg
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py
new file mode 100644
index 0000000000..9d6c29c0a4
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudApiClient.py
@@ -0,0 +1,166 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+import json
+from json import JSONDecodeError
+from time import time
+from typing import Callable, List, Type, TypeVar, Union, Optional, Tuple, Dict, Any, cast
+
+from PyQt5.QtCore import QUrl
+from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
+
+from UM.Logger import Logger
+from cura import UltimakerCloudAuthentication
+from cura.API import Account
+from .ToolPathUploader import ToolPathUploader
+from ..Models import BaseModel
+from .Models.CloudClusterResponse import CloudClusterResponse
+from .Models.CloudError import CloudError
+from .Models.CloudClusterStatus import CloudClusterStatus
+from .Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest
+from .Models.CloudPrintResponse import CloudPrintResponse
+from .Models.CloudPrintJobResponse import CloudPrintJobResponse
+
+
+## The generic type variable used to document the methods below.
+CloudApiClientModel = TypeVar("CloudApiClientModel", bound = BaseModel)
+
+
+## The cloud API client is responsible for handling the requests and responses from the cloud.
+# Each method should only handle models instead of exposing Any HTTP details.
+class CloudApiClient:
+
+ # The cloud URL to use for this remote cluster.
+ ROOT_PATH = UltimakerCloudAuthentication.CuraCloudAPIRoot
+ CLUSTER_API_ROOT = "{}/connect/v1".format(ROOT_PATH)
+ CURA_API_ROOT = "{}/cura/v1".format(ROOT_PATH)
+
+ ## Initializes a new cloud API client.
+ # \param account: The user's account object
+ # \param on_error: The callback to be called whenever we receive errors from the server.
+ def __init__(self, account: Account, on_error: Callable[[List[CloudError]], None]) -> None:
+ super().__init__()
+ self._manager = QNetworkAccessManager()
+ self._account = account
+ self._on_error = on_error
+ self._upload = None # type: Optional[ToolPathUploader]
+ # In order to avoid garbage collection we keep the callbacks in this list.
+ self._anti_gc_callbacks = [] # type: List[Callable[[], None]]
+
+ ## Gets the account used for the API.
+ @property
+ def account(self) -> Account:
+ return self._account
+
+ ## Retrieves all the clusters for the user that is currently logged in.
+ # \param on_finished: The function to be called after the result is parsed.
+ def getClusters(self, on_finished: Callable[[List[CloudClusterResponse]], Any]) -> None:
+ url = "{}/clusters".format(self.CLUSTER_API_ROOT)
+ reply = self._manager.get(self._createEmptyRequest(url))
+ self._addCallback(reply, on_finished, CloudClusterResponse)
+
+ ## Retrieves the status of the given cluster.
+ # \param cluster_id: The ID of the cluster.
+ # \param on_finished: The function to be called after the result is parsed.
+ def getClusterStatus(self, cluster_id: str, on_finished: Callable[[CloudClusterStatus], Any]) -> None:
+ url = "{}/clusters/{}/status".format(self.CLUSTER_API_ROOT, cluster_id)
+ reply = self._manager.get(self._createEmptyRequest(url))
+ self._addCallback(reply, on_finished, CloudClusterStatus)
+
+ ## Requests the cloud to register the upload of a print job mesh.
+ # \param request: The request object.
+ # \param on_finished: The function to be called after the result is parsed.
+ def requestUpload(self, request: CloudPrintJobUploadRequest, on_finished: Callable[[CloudPrintJobResponse], Any]
+ ) -> None:
+ url = "{}/jobs/upload".format(self.CURA_API_ROOT)
+ body = json.dumps({"data": request.toDict()})
+ reply = self._manager.put(self._createEmptyRequest(url), body.encode())
+ self._addCallback(reply, on_finished, CloudPrintJobResponse)
+
+ ## Uploads a print job tool path to the cloud.
+ # \param print_job: The object received after requesting an upload with `self.requestUpload`.
+ # \param mesh: The tool path data to be uploaded.
+ # \param on_finished: The function to be called after the upload is successful.
+ # \param on_progress: A function to be called during upload progress. It receives a percentage (0-100).
+ # \param on_error: A function to be called if the upload fails.
+ def uploadToolPath(self, print_job: CloudPrintJobResponse, mesh: bytes, on_finished: Callable[[], Any],
+ on_progress: Callable[[int], Any], on_error: Callable[[], Any]):
+ self._upload = ToolPathUploader(self._manager, print_job, mesh, on_finished, on_progress, on_error)
+ self._upload.start()
+
+ # Requests a cluster to print the given print job.
+ # \param cluster_id: The ID of the cluster.
+ # \param job_id: The ID of the print job.
+ # \param on_finished: The function to be called after the result is parsed.
+ def requestPrint(self, cluster_id: str, job_id: str, on_finished: Callable[[CloudPrintResponse], Any]) -> None:
+ url = "{}/clusters/{}/print/{}".format(self.CLUSTER_API_ROOT, cluster_id, job_id)
+ reply = self._manager.post(self._createEmptyRequest(url), b"")
+ self._addCallback(reply, on_finished, CloudPrintResponse)
+
+ ## We override _createEmptyRequest in order to add the user credentials.
+ # \param url: The URL to request
+ # \param content_type: The type of the body contents.
+ def _createEmptyRequest(self, path: str, content_type: Optional[str] = "application/json") -> QNetworkRequest:
+ request = QNetworkRequest(QUrl(path))
+ if content_type:
+ request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
+ if self._account.isLoggedIn:
+ request.setRawHeader(b"Authorization", "Bearer {}".format(self._account.accessToken).encode())
+ return request
+
+ ## Parses the given JSON network reply into a status code and a dictionary, handling unexpected errors as well.
+ # \param reply: The reply from the server.
+ # \return A tuple with a status code and a dictionary.
+ @staticmethod
+ def _parseReply(reply: QNetworkReply) -> Tuple[int, Dict[str, Any]]:
+ status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
+ try:
+ response = bytes(reply.readAll()).decode()
+ return status_code, json.loads(response)
+ except (UnicodeDecodeError, JSONDecodeError, ValueError) as err:
+ error = CloudError(code=type(err).__name__, title=str(err), http_code=str(status_code),
+ id=str(time()), http_status="500")
+ Logger.logException("e", "Could not parse the stardust response: %s", error)
+ return status_code, {"errors": [error.toDict()]}
+
+ ## Parses the given models and calls the correct callback depending on the result.
+ # \param response: The response from the server, after being converted to a dict.
+ # \param on_finished: The callback in case the response is successful.
+ # \param model_class: The type of the model to convert the response to. It may either be a single record or a list.
+ def _parseModels(self, response: Dict[str, Any],
+ on_finished: Union[Callable[[CloudApiClientModel], Any],
+ Callable[[List[CloudApiClientModel]], Any]],
+ model_class: Type[CloudApiClientModel]) -> None:
+ if "data" in response:
+ data = response["data"]
+ if isinstance(data, list):
+ results = [model_class(**c) for c in data] # type: List[CloudApiClientModel]
+ on_finished_list = cast(Callable[[List[CloudApiClientModel]], Any], on_finished)
+ on_finished_list(results)
+ else:
+ result = model_class(**data) # type: CloudApiClientModel
+ on_finished_item = cast(Callable[[CloudApiClientModel], Any], on_finished)
+ on_finished_item(result)
+ elif "errors" in response:
+ self._on_error([CloudError(**error) for error in response["errors"]])
+ else:
+ Logger.log("e", "Cannot find data or errors in the cloud response: %s", response)
+
+ ## Creates a callback function so that it includes the parsing of the response into the correct model.
+ # The callback is added to the 'finished' signal of the reply.
+ # \param reply: The reply that should be listened to.
+ # \param on_finished: The callback in case the response is successful. Depending on the endpoint it will be either
+ # a list or a single item.
+ # \param model: The type of the model to convert the response to.
+ def _addCallback(self,
+ reply: QNetworkReply,
+ on_finished: Union[Callable[[CloudApiClientModel], Any],
+ Callable[[List[CloudApiClientModel]], Any]],
+ model: Type[CloudApiClientModel],
+ ) -> None:
+ def parse() -> None:
+ status_code, response = self._parseReply(reply)
+ self._anti_gc_callbacks.remove(parse)
+ return self._parseModels(response, on_finished, model)
+
+ self._anti_gc_callbacks.append(parse)
+ reply.finished.connect(parse)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputController.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputController.py
new file mode 100644
index 0000000000..bd56ef3185
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputController.py
@@ -0,0 +1,22 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
+
+from typing import TYPE_CHECKING
+if TYPE_CHECKING:
+ from .CloudOutputDevice import CloudOutputDevice
+
+
+class CloudOutputController(PrinterOutputController):
+ def __init__(self, output_device: "CloudOutputDevice") -> None:
+ super().__init__(output_device)
+
+ # The cloud connection only supports fetching the printer and queue status and adding a job to the queue.
+ # To let the UI know this we mark all features below as False.
+ self.can_pause = False
+ self.can_abort = False
+ self.can_pre_heat_bed = False
+ self.can_pre_heat_hotends = False
+ self.can_send_raw_gcode = False
+ self.can_control_manually = False
+ self.can_update_firmware = False
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py
new file mode 100644
index 0000000000..33968beb6d
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDevice.py
@@ -0,0 +1,424 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+import os
+
+from time import time
+from typing import Dict, List, Optional, Set, cast
+
+from PyQt5.QtCore import QObject, QUrl, pyqtProperty, pyqtSignal, pyqtSlot
+
+from UM import i18nCatalog
+from UM.Backend.Backend import BackendState
+from UM.FileHandler.FileHandler import FileHandler
+from UM.Logger import Logger
+from UM.Message import Message
+from UM.Qt.Duration import Duration, DurationFormat
+from UM.Scene.SceneNode import SceneNode
+from cura.CuraApplication import CuraApplication
+from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice
+from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
+from cura.PrinterOutputDevice import ConnectionType
+
+from .CloudOutputController import CloudOutputController
+from ..MeshFormatHandler import MeshFormatHandler
+from ..UM3PrintJobOutputModel import UM3PrintJobOutputModel
+from .CloudProgressMessage import CloudProgressMessage
+from .CloudApiClient import CloudApiClient
+from .Models.CloudClusterResponse import CloudClusterResponse
+from .Models.CloudClusterStatus import CloudClusterStatus
+from .Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest
+from .Models.CloudPrintResponse import CloudPrintResponse
+from .Models.CloudPrintJobResponse import CloudPrintJobResponse
+from .Models.CloudClusterPrinterStatus import CloudClusterPrinterStatus
+from .Models.CloudClusterPrintJobStatus import CloudClusterPrintJobStatus
+from .Utils import findChanges, formatDateCompleted, formatTimeCompleted
+
+
+I18N_CATALOG = i18nCatalog("cura")
+
+
+## The cloud output device is a network output device that works remotely but has limited functionality.
+# Currently it only supports viewing the printer and print job status and adding a new job to the queue.
+# As such, those methods have been implemented here.
+# Note that this device represents a single remote cluster, not a list of multiple clusters.
+class CloudOutputDevice(NetworkedPrinterOutputDevice):
+
+ # The interval with which the remote clusters are checked
+ CHECK_CLUSTER_INTERVAL = 10.0 # seconds
+
+ # Signal triggered when the print jobs in the queue were changed.
+ printJobsChanged = pyqtSignal()
+
+ # Signal triggered when the selected printer in the UI should be changed.
+ activePrinterChanged = pyqtSignal()
+
+ # Notify can only use signals that are defined by the class that they are in, not inherited ones.
+ # Therefore we create a private signal used to trigger the printersChanged signal.
+ _clusterPrintersChanged = pyqtSignal()
+
+ ## Creates a new cloud output device
+ # \param api_client: The client that will run the API calls
+ # \param cluster: The device response received from the cloud API.
+ # \param parent: The optional parent of this output device.
+ def __init__(self, api_client: CloudApiClient, cluster: CloudClusterResponse, parent: QObject = None) -> None:
+ super().__init__(device_id = cluster.cluster_id, address = "",
+ connection_type = ConnectionType.CloudConnection, properties = {}, parent = parent)
+ self._api = api_client
+ self._cluster = cluster
+
+ self._setInterfaceElements()
+
+ self._account = api_client.account
+
+ # We use the Cura Connect monitor tab to get most functionality right away.
+ self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),
+ "../../resources/qml/MonitorStage.qml")
+
+ # Trigger the printersChanged signal when the private signal is triggered.
+ self.printersChanged.connect(self._clusterPrintersChanged)
+
+ # We keep track of which printer is visible in the monitor page.
+ self._active_printer = None # type: Optional[PrinterOutputModel]
+
+ # Properties to populate later on with received cloud data.
+ self._print_jobs = [] # type: List[UM3PrintJobOutputModel]
+ self._number_of_extruders = 2 # All networked printers are dual-extrusion Ultimaker machines.
+
+ # We only allow a single upload at a time.
+ self._progress = CloudProgressMessage()
+
+ # Keep server string of the last generated time to avoid updating models more than once for the same response
+ self._received_printers = None # type: Optional[List[CloudClusterPrinterStatus]]
+ self._received_print_jobs = None # type: Optional[List[CloudClusterPrintJobStatus]]
+
+ # A set of the user's job IDs that have finished
+ self._finished_jobs = set() # type: Set[str]
+
+ # Reference to the uploaded print job / mesh
+ self._tool_path = None # type: Optional[bytes]
+ self._uploaded_print_job = None # type: Optional[CloudPrintJobResponse]
+
+ ## Connects this device.
+ def connect(self) -> None:
+ if self.isConnected():
+ return
+ super().connect()
+ Logger.log("i", "Connected to cluster %s", self.key)
+ CuraApplication.getInstance().getBackend().backendStateChange.connect(self._onBackendStateChange)
+
+ ## Disconnects the device
+ def disconnect(self) -> None:
+ super().disconnect()
+ Logger.log("i", "Disconnected from cluster %s", self.key)
+ CuraApplication.getInstance().getBackend().backendStateChange.disconnect(self._onBackendStateChange)
+
+ ## Resets the print job that was uploaded to force a new upload, runs whenever the user re-slices.
+ def _onBackendStateChange(self, _: BackendState) -> None:
+ self._tool_path = None
+ self._uploaded_print_job = None
+
+ ## Gets the cluster response from which this device was created.
+ @property
+ def clusterData(self) -> CloudClusterResponse:
+ return self._cluster
+
+ ## Updates the cluster data from the cloud.
+ @clusterData.setter
+ def clusterData(self, value: CloudClusterResponse) -> None:
+ self._cluster = value
+
+ ## Checks whether the given network key is found in the cloud's host name
+ def matchesNetworkKey(self, network_key: str) -> bool:
+ # A network key looks like "ultimakersystem-aabbccdd0011._ultimaker._tcp.local."
+ # the host name should then be "ultimakersystem-aabbccdd0011"
+ return network_key.startswith(self.clusterData.host_name)
+
+ ## Set all the interface elements and texts for this output device.
+ def _setInterfaceElements(self) -> None:
+ self.setPriority(2) # Make sure we end up below the local networking and above 'save to file'
+ self.setName(self._id)
+ self.setShortDescription(I18N_CATALOG.i18nc("@action:button", "Print via Cloud"))
+ self.setDescription(I18N_CATALOG.i18nc("@properties:tooltip", "Print via Cloud"))
+ self.setConnectionText(I18N_CATALOG.i18nc("@info:status", "Connected via Cloud"))
+
+ ## Called when Cura requests an output device to receive a (G-code) file.
+ def requestWrite(self, nodes: List[SceneNode], file_name: Optional[str] = None, limit_mimetypes: bool = False,
+ file_handler: Optional[FileHandler] = None, **kwargs: str) -> None:
+
+ # Show an error message if we're already sending a job.
+ if self._progress.visible:
+ message = Message(
+ text = I18N_CATALOG.i18nc("@info:status", "Sending new jobs (temporarily) blocked, still sending the previous print job."),
+ title = I18N_CATALOG.i18nc("@info:title", "Cloud error"),
+ lifetime = 10
+ )
+ message.show()
+ return
+
+ if self._uploaded_print_job:
+ # The mesh didn't change, let's not upload it again
+ self._api.requestPrint(self.key, self._uploaded_print_job.job_id, self._onPrintUploadCompleted)
+ return
+
+ # Indicate we have started sending a job.
+ self.writeStarted.emit(self)
+
+ mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion)
+ if not mesh_format.is_valid:
+ Logger.log("e", "Missing file or mesh writer!")
+ return self._onUploadError(I18N_CATALOG.i18nc("@info:status", "Could not export print job."))
+
+ mesh = mesh_format.getBytes(nodes)
+
+ self._tool_path = mesh
+ request = CloudPrintJobUploadRequest(
+ job_name = file_name or mesh_format.file_extension,
+ file_size = len(mesh),
+ content_type = mesh_format.mime_type,
+ )
+ self._api.requestUpload(request, self._onPrintJobCreated)
+
+ ## Called when the network data should be updated.
+ def _update(self) -> None:
+ super()._update()
+ if self._last_request_time and time() - self._last_request_time < self.CHECK_CLUSTER_INTERVAL:
+ return # Avoid calling the cloud too often
+
+ Logger.log("d", "Updating: %s - %s >= %s", time(), self._last_request_time, self.CHECK_CLUSTER_INTERVAL)
+ if self._account.isLoggedIn:
+ self.setAuthenticationState(AuthState.Authenticated)
+ self._last_request_time = time()
+ self._api.getClusterStatus(self.key, self._onStatusCallFinished)
+ else:
+ self.setAuthenticationState(AuthState.NotAuthenticated)
+
+ ## Method called when HTTP request to status endpoint is finished.
+ # Contains both printers and print jobs statuses in a single response.
+ def _onStatusCallFinished(self, status: CloudClusterStatus) -> None:
+ # Update all data from the cluster.
+ self._last_response_time = time()
+ if self._received_printers != status.printers:
+ self._received_printers = status.printers
+ self._updatePrinters(status.printers)
+
+ if status.print_jobs != self._received_print_jobs:
+ self._received_print_jobs = status.print_jobs
+ self._updatePrintJobs(status.print_jobs)
+
+ ## Updates the local list of printers with the list received from the cloud.
+ # \param jobs: The printers received from the cloud.
+ def _updatePrinters(self, printers: List[CloudClusterPrinterStatus]) -> None:
+ previous = {p.key: p for p in self._printers} # type: Dict[str, PrinterOutputModel]
+ received = {p.uuid: p for p in printers} # type: Dict[str, CloudClusterPrinterStatus]
+
+ removed_printers, added_printers, updated_printers = findChanges(previous, received)
+
+ for removed_printer in removed_printers:
+ if self._active_printer == removed_printer:
+ self.setActivePrinter(None)
+ self._printers.remove(removed_printer)
+
+ for added_printer in added_printers:
+ self._printers.append(added_printer.createOutputModel(CloudOutputController(self)))
+
+ for model, printer in updated_printers:
+ printer.updateOutputModel(model)
+
+ # Always have an active printer
+ if self._printers and not self._active_printer:
+ self.setActivePrinter(self._printers[0])
+
+ if added_printers or removed_printers:
+ self.printersChanged.emit()
+
+ ## Updates the local list of print jobs with the list received from the cloud.
+ # \param jobs: The print jobs received from the cloud.
+ def _updatePrintJobs(self, jobs: List[CloudClusterPrintJobStatus]) -> None:
+ received = {j.uuid: j for j in jobs} # type: Dict[str, CloudClusterPrintJobStatus]
+ previous = {j.key: j for j in self._print_jobs} # type: Dict[str, UM3PrintJobOutputModel]
+
+ removed_jobs, added_jobs, updated_jobs = findChanges(previous, received)
+
+ for removed_job in removed_jobs:
+ if removed_job.assignedPrinter:
+ removed_job.assignedPrinter.updateActivePrintJob(None)
+ removed_job.stateChanged.disconnect(self._onPrintJobStateChanged)
+ self._print_jobs.remove(removed_job)
+
+ for added_job in added_jobs:
+ self._addPrintJob(added_job)
+
+ for model, job in updated_jobs:
+ job.updateOutputModel(model)
+ if job.printer_uuid:
+ self._updateAssignedPrinter(model, job.printer_uuid)
+
+ # We only have to update when jobs are added or removed
+ # updated jobs push their changes via their output model
+ if added_jobs or removed_jobs:
+ self.printJobsChanged.emit()
+
+ ## Registers a new print job received via the cloud API.
+ # \param job: The print job received.
+ def _addPrintJob(self, job: CloudClusterPrintJobStatus) -> None:
+ model = job.createOutputModel(CloudOutputController(self))
+ model.stateChanged.connect(self._onPrintJobStateChanged)
+ if job.printer_uuid:
+ self._updateAssignedPrinter(model, job.printer_uuid)
+ self._print_jobs.append(model)
+
+ ## Handles the event of a change in a print job state
+ def _onPrintJobStateChanged(self) -> None:
+ user_name = self._getUserName()
+ # TODO: confirm that notifications in Cura are still required
+ for job in self._print_jobs:
+ if job.state == "wait_cleanup" and job.key not in self._finished_jobs and job.owner == user_name:
+ self._finished_jobs.add(job.key)
+ Message(
+ title = I18N_CATALOG.i18nc("@info:status", "Print finished"),
+ text = (I18N_CATALOG.i18nc("@info:status", "Printer '{printer_name}' has finished printing '{job_name}'.").format(
+ printer_name = job.assignedPrinter.name,
+ job_name = job.name
+ ) if job.assignedPrinter else
+ I18N_CATALOG.i18nc("@info:status", "The print job '{job_name}' was finished.").format(
+ job_name = job.name
+ )),
+ ).show()
+
+ ## Updates the printer assignment for the given print job model.
+ def _updateAssignedPrinter(self, model: UM3PrintJobOutputModel, printer_uuid: str) -> None:
+ printer = next((p for p in self._printers if printer_uuid == p.key), None)
+ if not printer:
+ Logger.log("w", "Missing printer %s for job %s in %s", model.assignedPrinter, model.key,
+ [p.key for p in self._printers])
+ return
+
+ printer.updateActivePrintJob(model)
+ model.updateAssignedPrinter(printer)
+
+ ## Uploads the mesh when the print job was registered with the cloud API.
+ # \param job_response: The response received from the cloud API.
+ def _onPrintJobCreated(self, job_response: CloudPrintJobResponse) -> None:
+ self._progress.show()
+ self._uploaded_print_job = job_response
+ tool_path = cast(bytes, self._tool_path)
+ self._api.uploadToolPath(job_response, tool_path, self._onPrintJobUploaded, self._progress.update, self._onUploadError)
+
+ ## Requests the print to be sent to the printer when we finished uploading the mesh.
+ def _onPrintJobUploaded(self) -> None:
+ self._progress.update(100)
+ print_job = cast(CloudPrintJobResponse, self._uploaded_print_job)
+ self._api.requestPrint(self.key, print_job.job_id, self._onPrintUploadCompleted)
+
+ ## Displays the given message if uploading the mesh has failed
+ # \param message: The message to display.
+ def _onUploadError(self, message: str = None) -> None:
+ self._progress.hide()
+ self._uploaded_print_job = None
+ Message(
+ text = message or I18N_CATALOG.i18nc("@info:text", "Could not upload the data to the printer."),
+ title = I18N_CATALOG.i18nc("@info:title", "Cloud error"),
+ lifetime = 10
+ ).show()
+ self.writeError.emit()
+
+ ## Shows a message when the upload has succeeded
+ # \param response: The response from the cloud API.
+ def _onPrintUploadCompleted(self, response: CloudPrintResponse) -> None:
+ Logger.log("d", "The cluster will be printing this print job with the ID %s", response.cluster_job_id)
+ self._progress.hide()
+ Message(
+ text = I18N_CATALOG.i18nc("@info:status", "Print job was successfully sent to the printer."),
+ title = I18N_CATALOG.i18nc("@info:title", "Data Sent"),
+ lifetime = 5
+ ).show()
+ self.writeFinished.emit()
+
+ ## Gets the remote printers.
+ @pyqtProperty("QVariantList", notify=_clusterPrintersChanged)
+ def printers(self) -> List[PrinterOutputModel]:
+ return self._printers
+
+ ## Get the active printer in the UI (monitor page).
+ @pyqtProperty(QObject, notify = activePrinterChanged)
+ def activePrinter(self) -> Optional[PrinterOutputModel]:
+ return self._active_printer
+
+ ## Set the active printer in the UI (monitor page).
+ @pyqtSlot(QObject)
+ def setActivePrinter(self, printer: Optional[PrinterOutputModel] = None) -> None:
+ if printer != self._active_printer:
+ self._active_printer = printer
+ self.activePrinterChanged.emit()
+
+ @pyqtProperty(int, notify = _clusterPrintersChanged)
+ def clusterSize(self) -> int:
+ return len(self._printers)
+
+ ## Get remote print jobs.
+ @pyqtProperty("QVariantList", notify = printJobsChanged)
+ def printJobs(self) -> List[UM3PrintJobOutputModel]:
+ return self._print_jobs
+
+ ## Get remote print jobs that are still in the print queue.
+ @pyqtProperty("QVariantList", notify = printJobsChanged)
+ def queuedPrintJobs(self) -> List[UM3PrintJobOutputModel]:
+ return [print_job for print_job in self._print_jobs
+ if print_job.state == "queued" or print_job.state == "error"]
+
+ ## Get remote print jobs that are assigned to a printer.
+ @pyqtProperty("QVariantList", notify = printJobsChanged)
+ def activePrintJobs(self) -> List[UM3PrintJobOutputModel]:
+ return [print_job for print_job in self._print_jobs if
+ print_job.assignedPrinter is not None and print_job.state != "queued"]
+
+ @pyqtSlot(int, result = str)
+ def formatDuration(self, seconds: int) -> str:
+ return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
+
+ @pyqtSlot(int, result = str)
+ def getTimeCompleted(self, time_remaining: int) -> str:
+ return formatTimeCompleted(time_remaining)
+
+ @pyqtSlot(int, result = str)
+ def getDateCompleted(self, time_remaining: int) -> str:
+ return formatDateCompleted(time_remaining)
+
+ ## TODO: The following methods are required by the monitor page QML, but are not actually available using cloud.
+ # TODO: We fake the methods here to not break the monitor page.
+
+ @pyqtProperty(QUrl, notify = _clusterPrintersChanged)
+ def activeCameraUrl(self) -> "QUrl":
+ return QUrl()
+
+ @pyqtSlot(QUrl)
+ def setActiveCameraUrl(self, camera_url: "QUrl") -> None:
+ pass
+
+ @pyqtProperty(bool, notify = printJobsChanged)
+ def receivedPrintJobs(self) -> bool:
+ return bool(self._print_jobs)
+
+ @pyqtSlot()
+ def openPrintJobControlPanel(self) -> None:
+ pass
+
+ @pyqtSlot()
+ def openPrinterControlPanel(self) -> None:
+ pass
+
+ @pyqtSlot(str)
+ def sendJobToTop(self, print_job_uuid: str) -> None:
+ pass
+
+ @pyqtSlot(str)
+ def deleteJobFromQueue(self, print_job_uuid: str) -> None:
+ pass
+
+ @pyqtSlot(str)
+ def forceSendJob(self, print_job_uuid: str) -> None:
+ pass
+
+ @pyqtProperty("QVariantList", notify = _clusterPrintersChanged)
+ def connectedPrintersTypeCount(self) -> List[Dict[str, str]]:
+ return []
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py
new file mode 100644
index 0000000000..f9a0a59c81
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudOutputDeviceManager.py
@@ -0,0 +1,170 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Dict, List
+
+from PyQt5.QtCore import QTimer
+
+from UM import i18nCatalog
+from UM.Logger import Logger
+from UM.Message import Message
+from cura.API import Account
+from cura.CuraApplication import CuraApplication
+from cura.Settings.GlobalStack import GlobalStack
+from .CloudApiClient import CloudApiClient
+from .CloudOutputDevice import CloudOutputDevice
+from .Models.CloudClusterResponse import CloudClusterResponse
+from .Models.CloudError import CloudError
+from .Utils import findChanges
+
+
+## The cloud output device manager is responsible for using the Ultimaker Cloud APIs to manage remote clusters.
+# Keeping all cloud related logic in this class instead of the UM3OutputDevicePlugin results in more readable code.
+#
+# API spec is available on https://api.ultimaker.com/docs/connect/spec/.
+#
+class CloudOutputDeviceManager:
+ META_CLUSTER_ID = "um_cloud_cluster_id"
+
+ # The interval with which the remote clusters are checked
+ CHECK_CLUSTER_INTERVAL = 30.0 # seconds
+
+ # The translation catalog for this device.
+ I18N_CATALOG = i18nCatalog("cura")
+
+ def __init__(self) -> None:
+ # Persistent dict containing the remote clusters for the authenticated user.
+ self._remote_clusters = {} # type: Dict[str, CloudOutputDevice]
+
+ application = CuraApplication.getInstance()
+ self._output_device_manager = application.getOutputDeviceManager()
+
+ self._account = application.getCuraAPI().account # type: Account
+ self._api = CloudApiClient(self._account, self._onApiError)
+
+ # Create a timer to update the remote cluster list
+ self._update_timer = QTimer()
+ self._update_timer.setInterval(int(self.CHECK_CLUSTER_INTERVAL * 1000))
+ self._update_timer.setSingleShot(False)
+
+ self._running = False
+
+ # Called when the uses logs in or out
+ def _onLoginStateChanged(self, is_logged_in: bool) -> None:
+ Logger.log("d", "Log in state changed to %s", is_logged_in)
+ if is_logged_in:
+ if not self._update_timer.isActive():
+ self._update_timer.start()
+ self._getRemoteClusters()
+ else:
+ if self._update_timer.isActive():
+ self._update_timer.stop()
+
+ # Notify that all clusters have disappeared
+ self._onGetRemoteClustersFinished([])
+
+ ## Gets all remote clusters from the API.
+ def _getRemoteClusters(self) -> None:
+ Logger.log("d", "Retrieving remote clusters")
+ self._api.getClusters(self._onGetRemoteClustersFinished)
+
+ ## Callback for when the request for getting the clusters. is finished.
+ def _onGetRemoteClustersFinished(self, clusters: List[CloudClusterResponse]) -> None:
+ online_clusters = {c.cluster_id: c for c in clusters if c.is_online} # type: Dict[str, CloudClusterResponse]
+
+ removed_devices, added_clusters, updates = findChanges(self._remote_clusters, online_clusters)
+
+ Logger.log("d", "Parsed remote clusters to %s", [cluster.toDict() for cluster in online_clusters.values()])
+ Logger.log("d", "Removed: %s, added: %s, updates: %s", len(removed_devices), len(added_clusters), len(updates))
+
+ # Remove output devices that are gone
+ for removed_cluster in removed_devices:
+ if removed_cluster.isConnected():
+ removed_cluster.disconnect()
+ removed_cluster.close()
+ self._output_device_manager.removeOutputDevice(removed_cluster.key)
+ del self._remote_clusters[removed_cluster.key]
+
+ # Add an output device for each new remote cluster.
+ # We only add when is_online as we don't want the option in the drop down if the cluster is not online.
+ for added_cluster in added_clusters:
+ device = CloudOutputDevice(self._api, added_cluster)
+ self._remote_clusters[added_cluster.cluster_id] = device
+
+ for device, cluster in updates:
+ device.clusterData = cluster
+
+ self._connectToActiveMachine()
+
+ ## Callback for when the active machine was changed by the user or a new remote cluster was found.
+ def _connectToActiveMachine(self) -> None:
+ active_machine = CuraApplication.getInstance().getGlobalContainerStack()
+ if not active_machine:
+ return
+
+ # Remove all output devices that we have registered.
+ # This is needed because when we switch machines we can only leave
+ # output devices that are meant for that machine.
+ for stored_cluster_id in self._remote_clusters:
+ self._output_device_manager.removeOutputDevice(stored_cluster_id)
+
+ # Check if the stored cluster_id for the active machine is in our list of remote clusters.
+ stored_cluster_id = active_machine.getMetaDataEntry(self.META_CLUSTER_ID)
+ if stored_cluster_id in self._remote_clusters:
+ device = self._remote_clusters[stored_cluster_id]
+ self._connectToOutputDevice(device)
+ Logger.log("d", "Device connected by metadata cluster ID %s", stored_cluster_id)
+ else:
+ self._connectByNetworkKey(active_machine)
+
+ ## Tries to match the local network key to the cloud cluster host name.
+ def _connectByNetworkKey(self, active_machine: GlobalStack) -> None:
+ # Check if the active printer has a local network connection and match this key to the remote cluster.
+ local_network_key = active_machine.getMetaDataEntry("um_network_key")
+ if not local_network_key:
+ return
+
+ device = next((c for c in self._remote_clusters.values() if c.matchesNetworkKey(local_network_key)), None)
+ if not device:
+ return
+
+ Logger.log("i", "Found cluster %s with network key %s", device, local_network_key)
+ active_machine.setMetaDataEntry(self.META_CLUSTER_ID, device.key)
+ self._connectToOutputDevice(device)
+
+ ## Connects to an output device and makes sure it is registered in the output device manager.
+ def _connectToOutputDevice(self, device: CloudOutputDevice) -> None:
+ device.connect()
+ self._output_device_manager.addOutputDevice(device)
+
+ ## Handles an API error received from the cloud.
+ # \param errors: The errors received
+ def _onApiError(self, errors: List[CloudError]) -> None:
+ text = ". ".join(e.title for e in errors) # TODO: translate errors
+ message = Message(
+ text = text,
+ title = self.I18N_CATALOG.i18nc("@info:title", "Error"),
+ lifetime = 10
+ )
+ message.show()
+
+ ## Starts running the cloud output device manager, thus periodically requesting cloud data.
+ def start(self):
+ if self._running:
+ return
+ application = CuraApplication.getInstance()
+ self._account.loginStateChanged.connect(self._onLoginStateChanged)
+ # When switching machines we check if we have to activate a remote cluster.
+ application.globalContainerStackChanged.connect(self._connectToActiveMachine)
+ self._update_timer.timeout.connect(self._getRemoteClusters)
+ self._onLoginStateChanged(is_logged_in = self._account.isLoggedIn)
+
+ ## Stops running the cloud output device manager.
+ def stop(self):
+ if not self._running:
+ return
+ application = CuraApplication.getInstance()
+ self._account.loginStateChanged.disconnect(self._onLoginStateChanged)
+ # When switching machines we check if we have to activate a remote cluster.
+ application.globalContainerStackChanged.disconnect(self._connectToActiveMachine)
+ self._update_timer.timeout.disconnect(self._getRemoteClusters)
+ self._onLoginStateChanged(is_logged_in = False)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/CloudProgressMessage.py b/plugins/UM3NetworkPrinting/src/Cloud/CloudProgressMessage.py
new file mode 100644
index 0000000000..d85f49c1a0
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/CloudProgressMessage.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from UM import i18nCatalog
+from UM.Message import Message
+
+
+I18N_CATALOG = i18nCatalog("cura")
+
+
+## Class responsible for showing a progress message while a mesh is being uploaded to the cloud.
+class CloudProgressMessage(Message):
+ def __init__(self):
+ super().__init__(
+ text = I18N_CATALOG.i18nc("@info:status", "Sending data to remote cluster"),
+ title = I18N_CATALOG.i18nc("@info:status", "Sending data to remote cluster"),
+ progress = -1,
+ lifetime = 0,
+ dismissable = False,
+ use_inactivity_timer = False
+ )
+
+ ## Shows the progress message.
+ def show(self):
+ self.setProgress(0)
+ super().show()
+
+ ## Updates the percentage of the uploaded.
+ # \param percentage: The percentage amount (0-100).
+ def update(self, percentage: int) -> None:
+ if not self._visible:
+ super().show()
+ self.setProgress(percentage)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/BaseCloudModel.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/BaseCloudModel.py
new file mode 100644
index 0000000000..18a8cb5cba
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/BaseCloudModel.py
@@ -0,0 +1,55 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from datetime import datetime, timezone
+from typing import Dict, Union, TypeVar, Type, List, Any
+
+from ...Models import BaseModel
+
+
+## Base class for the models used in the interface with the Ultimaker cloud APIs.
+class BaseCloudModel(BaseModel):
+ ## Checks whether the two models are equal.
+ # \param other: The other model.
+ # \return True if they are equal, False if they are different.
+ def __eq__(self, other):
+ return type(self) == type(other) and self.toDict() == other.toDict()
+
+ ## Checks whether the two models are different.
+ # \param other: The other model.
+ # \return True if they are different, False if they are the same.
+ def __ne__(self, other) -> bool:
+ return type(self) != type(other) or self.toDict() != other.toDict()
+
+ ## Converts the model into a serializable dictionary
+ def toDict(self) -> Dict[str, Any]:
+ return self.__dict__
+
+ # Type variable used in the parse methods below, which should be a subclass of BaseModel.
+ T = TypeVar("T", bound=BaseModel)
+
+ ## Parses a single model.
+ # \param model_class: The model class.
+ # \param values: The value of the model, which is usually a dictionary, but may also be already parsed.
+ # \return An instance of the model_class given.
+ @staticmethod
+ def parseModel(model_class: Type[T], values: Union[T, Dict[str, Any]]) -> T:
+ if isinstance(values, dict):
+ return model_class(**values)
+ return values
+
+ ## Parses a list of models.
+ # \param model_class: The model class.
+ # \param values: The value of the list. Each value is usually a dictionary, but may also be already parsed.
+ # \return A list of instances of the model_class given.
+ @classmethod
+ def parseModels(cls, model_class: Type[T], values: List[Union[T, Dict[str, Any]]]) -> List[T]:
+ return [cls.parseModel(model_class, value) for value in values]
+
+ ## Parses the given date string.
+ # \param date: The date to parse.
+ # \return The parsed date.
+ @staticmethod
+ def parseDate(date: Union[str, datetime]) -> datetime:
+ if isinstance(date, datetime):
+ return date
+ return datetime.strptime(date, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterBuildPlate.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterBuildPlate.py
new file mode 100644
index 0000000000..4386bbb435
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterBuildPlate.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from .BaseCloudModel import BaseCloudModel
+
+
+## Class representing a cluster printer
+# Spec: https://api-staging.ultimaker.com/connect/v1/spec
+class CloudClusterBuildPlate(BaseCloudModel):
+ ## Create a new build plate
+ # \param type: The type of buildplate glass or aluminium
+ def __init__(self, type: str = "glass", **kwargs) -> None:
+ self.type = type
+ super().__init__(**kwargs)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintCoreConfiguration.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintCoreConfiguration.py
new file mode 100644
index 0000000000..7454401d09
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintCoreConfiguration.py
@@ -0,0 +1,52 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Union, Dict, Optional, Any
+
+from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
+from cura.PrinterOutput.ExtruderOutputModel import ExtruderOutputModel
+from .CloudClusterPrinterConfigurationMaterial import CloudClusterPrinterConfigurationMaterial
+from .BaseCloudModel import BaseCloudModel
+
+
+## Class representing a cloud cluster printer configuration
+# Spec: https://api-staging.ultimaker.com/connect/v1/spec
+class CloudClusterPrintCoreConfiguration(BaseCloudModel):
+ ## Creates a new cloud cluster printer configuration object
+ # \param extruder_index: The position of the extruder on the machine as list index. Numbered from left to right.
+ # \param material: The material of a configuration object in a cluster printer. May be in a dict or an object.
+ # \param nozzle_diameter: The diameter of the print core at this position in millimeters, e.g. '0.4'.
+ # \param print_core_id: The type of print core inserted at this position, e.g. 'AA 0.4'.
+ def __init__(self, extruder_index: int,
+ material: Union[None, Dict[str, Any], CloudClusterPrinterConfigurationMaterial],
+ print_core_id: Optional[str] = None, **kwargs) -> None:
+ self.extruder_index = extruder_index
+ self.material = self.parseModel(CloudClusterPrinterConfigurationMaterial, material) if material else None
+ self.print_core_id = print_core_id
+ super().__init__(**kwargs)
+
+ ## Updates the given output model.
+ # \param model - The output model to update.
+ def updateOutputModel(self, model: ExtruderOutputModel) -> None:
+ if self.print_core_id is not None:
+ model.updateHotendID(self.print_core_id)
+
+ if self.material:
+ active_material = model.activeMaterial
+ if active_material is None or active_material.guid != self.material.guid:
+ material = self.material.createOutputModel()
+ model.updateActiveMaterial(material)
+ else:
+ model.updateActiveMaterial(None)
+
+ ## Creates a configuration model
+ def createConfigurationModel(self) -> ExtruderConfigurationModel:
+ model = ExtruderConfigurationModel(position = self.extruder_index)
+ self.updateConfigurationModel(model)
+ return model
+
+ ## Creates a configuration model
+ def updateConfigurationModel(self, model: ExtruderConfigurationModel) -> ExtruderConfigurationModel:
+ model.setHotendID(self.print_core_id)
+ if self.material:
+ model.setMaterial(self.material.createOutputModel())
+ return model
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobConfigurationChange.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobConfigurationChange.py
new file mode 100644
index 0000000000..9ff4154666
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobConfigurationChange.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Optional
+
+from .BaseCloudModel import BaseCloudModel
+
+
+## Model for the types of changes that are needed before a print job can start
+# Spec: https://api-staging.ultimaker.com/connect/v1/spec
+class CloudClusterPrintJobConfigurationChange(BaseCloudModel):
+ ## Creates a new print job constraint.
+ # \param type_of_change: The type of configuration change, one of: "material", "print_core_change"
+ # \param index: The hotend slot or extruder index to change
+ # \param target_id: Target material guid or hotend id
+ # \param origin_id: Original/current material guid or hotend id
+ # \param target_name: Target material name or hotend id
+ # \param origin_name: Original/current material name or hotend id
+ def __init__(self, type_of_change: str, target_id: str, origin_id: str,
+ index: Optional[int] = None, target_name: Optional[str] = None, origin_name: Optional[str] = None,
+ **kwargs) -> None:
+ self.type_of_change = type_of_change
+ self.index = index
+ self.target_id = target_id
+ self.origin_id = origin_id
+ self.target_name = target_name
+ self.origin_name = origin_name
+ super().__init__(**kwargs)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobConstraint.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobConstraint.py
new file mode 100644
index 0000000000..8236ec06b9
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobConstraint.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Optional
+
+from .BaseCloudModel import BaseCloudModel
+
+
+## Class representing a cloud cluster print job constraint
+# Spec: https://api-staging.ultimaker.com/connect/v1/spec
+class CloudClusterPrintJobConstraints(BaseCloudModel):
+ ## Creates a new print job constraint.
+ # \param require_printer_name: Unique name of the printer that this job should be printed on.
+ # Should be one of the unique_name field values in the cluster, e.g. 'ultimakersystem-ccbdd30044ec'
+ def __init__(self, require_printer_name: Optional[str] = None, **kwargs) -> None:
+ self.require_printer_name = require_printer_name
+ super().__init__(**kwargs)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobImpediment.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobImpediment.py
new file mode 100644
index 0000000000..12b67996c1
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobImpediment.py
@@ -0,0 +1,15 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from .BaseCloudModel import BaseCloudModel
+
+
+## Class representing the reasons that prevent this job from being printed on the associated printer
+# Spec: https://api-staging.ultimaker.com/connect/v1/spec
+class CloudClusterPrintJobImpediment(BaseCloudModel):
+ ## Creates a new print job constraint.
+ # \param translation_key: A string indicating a reason the print cannot be printed, such as 'does_not_fit_in_build_volume'
+ # \param severity: A number indicating the severity of the problem, with higher being more severe
+ def __init__(self, translation_key: str, severity: int, **kwargs) -> None:
+ self.translation_key = translation_key
+ self.severity = severity
+ super().__init__(**kwargs)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobStatus.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobStatus.py
new file mode 100644
index 0000000000..45b7d838a5
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrintJobStatus.py
@@ -0,0 +1,134 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from typing import List, Optional, Union, Dict, Any
+
+from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
+from ...UM3PrintJobOutputModel import UM3PrintJobOutputModel
+from ...ConfigurationChangeModel import ConfigurationChangeModel
+from ..CloudOutputController import CloudOutputController
+from .BaseCloudModel import BaseCloudModel
+from .CloudClusterBuildPlate import CloudClusterBuildPlate
+from .CloudClusterPrintJobConfigurationChange import CloudClusterPrintJobConfigurationChange
+from .CloudClusterPrintJobImpediment import CloudClusterPrintJobImpediment
+from .CloudClusterPrintCoreConfiguration import CloudClusterPrintCoreConfiguration
+from .CloudClusterPrintJobConstraint import CloudClusterPrintJobConstraints
+
+
+## Model for the status of a single print job in a cluster.
+# Spec: https://api-staging.ultimaker.com/connect/v1/spec
+class CloudClusterPrintJobStatus(BaseCloudModel):
+ ## Creates a new cloud print job status model.
+ # \param assigned_to: The name of the printer this job is assigned to while being queued.
+ # \param configuration: The required print core configurations of this print job.
+ # \param constraints: Print job constraints object.
+ # \param created_at: The timestamp when the job was created in Cura Connect.
+ # \param force: Allow this job to be printed despite of mismatching configurations.
+ # \param last_seen: The number of seconds since this job was checked.
+ # \param machine_variant: The machine type that this job should be printed on.Coincides with the machine_type field
+ # of the printer object.
+ # \param name: The name of the print job. Usually the name of the .gcode file.
+ # \param network_error_count: The number of errors encountered when requesting data for this print job.
+ # \param owner: The name of the user who added the print job to Cura Connect.
+ # \param printer_uuid: UUID of the printer that the job is currently printing on or assigned to.
+ # \param started: Whether the job has started printing or not.
+ # \param status: The status of the print job.
+ # \param time_elapsed: The remaining printing time in seconds.
+ # \param time_total: The total printing time in seconds.
+ # \param uuid: UUID of this print job. Should be used for identification purposes.
+ # \param deleted_at: The time when this print job was deleted.
+ # \param printed_on_uuid: UUID of the printer used to print this job.
+ # \param configuration_changes_required: List of configuration changes the printer this job is associated with
+ # needs to make in order to be able to print this job
+ # \param build_plate: The build plate (type) this job needs to be printed on.
+ # \param compatible_machine_families: Family names of machines suitable for this print job
+ # \param impediments_to_printing: A list of reasons that prevent this job from being printed on the associated
+ # printer
+ def __init__(self, created_at: str, force: bool, machine_variant: str, name: str, started: bool, status: str,
+ time_total: int, uuid: str,
+ configuration: List[Union[Dict[str, Any], CloudClusterPrintCoreConfiguration]],
+ constraints: List[Union[Dict[str, Any], CloudClusterPrintJobConstraints]],
+ last_seen: Optional[float] = None, network_error_count: Optional[int] = None,
+ owner: Optional[str] = None, printer_uuid: Optional[str] = None, time_elapsed: Optional[int] = None,
+ assigned_to: Optional[str] = None, deleted_at: Optional[str] = None,
+ printed_on_uuid: Optional[str] = None,
+ configuration_changes_required: List[
+ Union[Dict[str, Any], CloudClusterPrintJobConfigurationChange]] = None,
+ build_plate: Union[Dict[str, Any], CloudClusterBuildPlate] = None,
+ compatible_machine_families: List[str] = None,
+ impediments_to_printing: List[Union[Dict[str, Any], CloudClusterPrintJobImpediment]] = None,
+ **kwargs) -> None:
+ self.assigned_to = assigned_to
+ self.configuration = self.parseModels(CloudClusterPrintCoreConfiguration, configuration)
+ self.constraints = self.parseModels(CloudClusterPrintJobConstraints, constraints)
+ self.created_at = created_at
+ self.force = force
+ self.last_seen = last_seen
+ self.machine_variant = machine_variant
+ self.name = name
+ self.network_error_count = network_error_count
+ self.owner = owner
+ self.printer_uuid = printer_uuid
+ self.started = started
+ self.status = status
+ self.time_elapsed = time_elapsed
+ self.time_total = time_total
+ self.uuid = uuid
+ self.deleted_at = deleted_at
+ self.printed_on_uuid = printed_on_uuid
+
+ self.configuration_changes_required = self.parseModels(CloudClusterPrintJobConfigurationChange,
+ configuration_changes_required) \
+ if configuration_changes_required else []
+ self.build_plate = self.parseModel(CloudClusterBuildPlate, build_plate) if build_plate else None
+ self.compatible_machine_families = compatible_machine_families if compatible_machine_families else []
+ self.impediments_to_printing = self.parseModels(CloudClusterPrintJobImpediment, impediments_to_printing) \
+ if impediments_to_printing else []
+
+ super().__init__(**kwargs)
+
+ ## Creates an UM3 print job output model based on this cloud cluster print job.
+ # \param printer: The output model of the printer
+ def createOutputModel(self, controller: CloudOutputController) -> UM3PrintJobOutputModel:
+ model = UM3PrintJobOutputModel(controller, self.uuid, self.name)
+ self.updateOutputModel(model)
+
+ return model
+
+ ## Creates a new configuration model
+ def _createConfigurationModel(self) -> ConfigurationModel:
+ extruders = [extruder.createConfigurationModel() for extruder in self.configuration or ()]
+ configuration = ConfigurationModel()
+ configuration.setExtruderConfigurations(extruders)
+ return configuration
+
+ ## Updates an UM3 print job output model based on this cloud cluster print job.
+ # \param model: The model to update.
+ def updateOutputModel(self, model: UM3PrintJobOutputModel) -> None:
+ model.updateConfiguration(self._createConfigurationModel())
+ model.updateTimeTotal(self.time_total)
+ model.updateTimeElapsed(self.time_elapsed)
+ model.updateOwner(self.owner)
+ model.updateState(self.status)
+ model.setCompatibleMachineFamilies(self.compatible_machine_families)
+ model.updateTimeTotal(self.time_total)
+ model.updateTimeElapsed(self.time_elapsed)
+ model.updateOwner(self.owner)
+
+ status_set_by_impediment = False
+ for impediment in self.impediments_to_printing:
+ # TODO: impediment.severity is defined as int, this will not work, is there a translation?
+ if impediment.severity == "UNFIXABLE":
+ status_set_by_impediment = True
+ model.updateState("error")
+ break
+
+ if not status_set_by_impediment:
+ model.updateState(self.status)
+
+ model.updateConfigurationChanges(
+ [ConfigurationChangeModel(
+ type_of_change = change.type_of_change,
+ index = change.index if change.index else 0,
+ target_name = change.target_name if change.target_name else "",
+ origin_name = change.origin_name if change.origin_name else "")
+ for change in self.configuration_changes_required])
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterConfigurationMaterial.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterConfigurationMaterial.py
new file mode 100644
index 0000000000..652cbdabda
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterConfigurationMaterial.py
@@ -0,0 +1,55 @@
+from typing import Optional
+
+from UM.Logger import Logger
+from cura.CuraApplication import CuraApplication
+from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
+from .BaseCloudModel import BaseCloudModel
+
+
+## Class representing a cloud cluster printer configuration
+# Spec: https://api-staging.ultimaker.com/connect/v1/spec
+class CloudClusterPrinterConfigurationMaterial(BaseCloudModel):
+ ## Creates a new material configuration model.
+ # \param brand: The brand of material in this print core, e.g. 'Ultimaker'.
+ # \param color: The color of material in this print core, e.g. 'Blue'.
+ # \param guid: he GUID of the material in this print core, e.g. '506c9f0d-e3aa-4bd4-b2d2-23e2425b1aa9'.
+ # \param material: The type of material in this print core, e.g. 'PLA'.
+ def __init__(self, brand: Optional[str] = None, color: Optional[str] = None, guid: Optional[str] = None,
+ material: Optional[str] = None, **kwargs) -> None:
+ self.guid = guid
+ self.brand = brand
+ self.color = color
+ self.material = material
+ super().__init__(**kwargs)
+
+ ## Creates a material output model based on this cloud printer material.
+ def createOutputModel(self) -> MaterialOutputModel:
+ material_manager = CuraApplication.getInstance().getMaterialManager()
+ material_group_list = material_manager.getMaterialGroupListByGUID(self.guid) or []
+
+ # Sort the material groups by "is_read_only = True" first, and then the name alphabetically.
+ read_only_material_group_list = list(filter(lambda x: x.is_read_only, material_group_list))
+ non_read_only_material_group_list = list(filter(lambda x: not x.is_read_only, material_group_list))
+ material_group = None
+ if read_only_material_group_list:
+ read_only_material_group_list = sorted(read_only_material_group_list, key = lambda x: x.name)
+ material_group = read_only_material_group_list[0]
+ elif non_read_only_material_group_list:
+ non_read_only_material_group_list = sorted(non_read_only_material_group_list, key = lambda x: x.name)
+ material_group = non_read_only_material_group_list[0]
+
+ if material_group:
+ container = material_group.root_material_node.getContainer()
+ color = container.getMetaDataEntry("color_code")
+ brand = container.getMetaDataEntry("brand")
+ material_type = container.getMetaDataEntry("material")
+ name = container.getName()
+ else:
+ Logger.log("w", "Unable to find material with guid {guid}. Using data as provided by cluster"
+ .format(guid = self.guid))
+ color = self.color
+ brand = self.brand
+ material_type = self.material
+ name = "Empty" if self.material == "empty" else "Unknown"
+
+ return MaterialOutputModel(guid = self.guid, type = material_type, brand = brand, color = color, name = name)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterStatus.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterStatus.py
new file mode 100644
index 0000000000..a8165ff69c
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterPrinterStatus.py
@@ -0,0 +1,72 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from typing import List, Union, Dict, Optional, Any
+
+from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
+from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
+from .CloudClusterBuildPlate import CloudClusterBuildPlate
+from .CloudClusterPrintCoreConfiguration import CloudClusterPrintCoreConfiguration
+from .BaseCloudModel import BaseCloudModel
+
+
+## Class representing a cluster printer
+# Spec: https://api-staging.ultimaker.com/connect/v1/spec
+class CloudClusterPrinterStatus(BaseCloudModel):
+ ## Creates a new cluster printer status
+ # \param enabled: A printer can be disabled if it should not receive new jobs. By default every printer is enabled.
+ # \param firmware_version: Firmware version installed on the printer. Can differ for each printer in a cluster.
+ # \param friendly_name: Human readable name of the printer. Can be used for identification purposes.
+ # \param ip_address: The IP address of the printer in the local network.
+ # \param machine_variant: The type of printer. Can be 'Ultimaker 3' or 'Ultimaker 3ext'.
+ # \param status: The status of the printer.
+ # \param unique_name: The unique name of the printer in the network.
+ # \param uuid: The unique ID of the printer, also known as GUID.
+ # \param configuration: The active print core configurations of this printer.
+ # \param reserved_by: A printer can be claimed by a specific print job.
+ # \param maintenance_required: Indicates if maintenance is necessary
+ # \param firmware_update_status: Whether the printer's firmware is up-to-date, value is one of: "up_to_date",
+ # "pending_update", "update_available", "update_in_progress", "update_failed", "update_impossible"
+ # \param latest_available_firmware: The version of the latest firmware that is available
+ # \param build_plate: The build plate that is on the printer
+ def __init__(self, enabled: bool, firmware_version: str, friendly_name: str, ip_address: str, machine_variant: str,
+ status: str, unique_name: str, uuid: str,
+ configuration: List[Union[Dict[str, Any], CloudClusterPrintCoreConfiguration]],
+ reserved_by: Optional[str] = None, maintenance_required: Optional[bool] = None,
+ firmware_update_status: Optional[str] = None, latest_available_firmware: Optional[str] = None,
+ build_plate: Union[Dict[str, Any], CloudClusterBuildPlate] = None, **kwargs) -> None:
+
+ self.configuration = self.parseModels(CloudClusterPrintCoreConfiguration, configuration)
+ self.enabled = enabled
+ self.firmware_version = firmware_version
+ self.friendly_name = friendly_name
+ self.ip_address = ip_address
+ self.machine_variant = machine_variant
+ self.status = status
+ self.unique_name = unique_name
+ self.uuid = uuid
+ self.reserved_by = reserved_by
+ self.maintenance_required = maintenance_required
+ self.firmware_update_status = firmware_update_status
+ self.latest_available_firmware = latest_available_firmware
+ self.build_plate = self.parseModel(CloudClusterBuildPlate, build_plate) if build_plate else None
+ super().__init__(**kwargs)
+
+ ## Creates a new output model.
+ # \param controller - The controller of the model.
+ def createOutputModel(self, controller: PrinterOutputController) -> PrinterOutputModel:
+ model = PrinterOutputModel(controller, len(self.configuration), firmware_version = self.firmware_version)
+ self.updateOutputModel(model)
+ return model
+
+ ## Updates the given output model.
+ # \param model - The output model to update.
+ def updateOutputModel(self, model: PrinterOutputModel) -> None:
+ model.updateKey(self.uuid)
+ model.updateName(self.friendly_name)
+ model.updateType(self.machine_variant)
+ model.updateState(self.status if self.enabled else "disabled")
+
+ for configuration, extruder_output, extruder_config in \
+ zip(self.configuration, model.extruders, model.printerConfiguration.extruderConfigurations):
+ configuration.updateOutputModel(extruder_output)
+ configuration.updateConfigurationModel(extruder_config)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterResponse.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterResponse.py
new file mode 100644
index 0000000000..9c0853e7c9
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterResponse.py
@@ -0,0 +1,32 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Optional
+
+from .BaseCloudModel import BaseCloudModel
+
+
+## Class representing a cloud connected cluster.
+# Spec: https://api-staging.ultimaker.com/connect/v1/spec
+class CloudClusterResponse(BaseCloudModel):
+ ## Creates a new cluster response object.
+ # \param cluster_id: The secret unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='.
+ # \param host_guid: The unique identifier of the print cluster host, e.g. 'e90ae0ac-1257-4403-91ee-a44c9b7e8050'.
+ # \param host_name: The name of the printer as configured during the Wi-Fi setup. Used as identifier for end users.
+ # \param is_online: Whether this cluster is currently connected to the cloud.
+ # \param status: The status of the cluster authentication (active or inactive).
+ # \param host_version: The firmware version of the cluster host. This is where the Stardust client is running on.
+ def __init__(self, cluster_id: str, host_guid: str, host_name: str, is_online: bool, status: str,
+ host_version: Optional[str] = None, **kwargs) -> None:
+ self.cluster_id = cluster_id
+ self.host_guid = host_guid
+ self.host_name = host_name
+ self.status = status
+ self.is_online = is_online
+ self.host_version = host_version
+ super().__init__(**kwargs)
+
+ # Validates the model, raising an exception if the model is invalid.
+ def validate(self) -> None:
+ super().validate()
+ if not self.cluster_id:
+ raise ValueError("cluster_id is required on CloudCluster")
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterStatus.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterStatus.py
new file mode 100644
index 0000000000..b0250c2ebb
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudClusterStatus.py
@@ -0,0 +1,26 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from datetime import datetime
+from typing import List, Dict, Union, Any
+
+from .CloudClusterPrinterStatus import CloudClusterPrinterStatus
+from .CloudClusterPrintJobStatus import CloudClusterPrintJobStatus
+from .BaseCloudModel import BaseCloudModel
+
+
+# Model that represents the status of the cluster for the cloud
+# Spec: https://api-staging.ultimaker.com/connect/v1/spec
+class CloudClusterStatus(BaseCloudModel):
+ ## Creates a new cluster status model object.
+ # \param printers: The latest status of each printer in the cluster.
+ # \param print_jobs: The latest status of each print job in the cluster.
+ # \param generated_time: The datetime when the object was generated on the server-side.
+ def __init__(self,
+ printers: List[Union[CloudClusterPrinterStatus, Dict[str, Any]]],
+ print_jobs: List[Union[CloudClusterPrintJobStatus, Dict[str, Any]]],
+ generated_time: Union[str, datetime],
+ **kwargs) -> None:
+ self.generated_time = self.parseDate(generated_time)
+ self.printers = self.parseModels(CloudClusterPrinterStatus, printers)
+ self.print_jobs = self.parseModels(CloudClusterPrintJobStatus, print_jobs)
+ super().__init__(**kwargs)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudError.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudError.py
new file mode 100644
index 0000000000..b53361022e
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudError.py
@@ -0,0 +1,28 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Dict, Optional, Any
+
+from .BaseCloudModel import BaseCloudModel
+
+
+## Class representing errors generated by the cloud servers, according to the JSON-API standard.
+# Spec: https://api-staging.ultimaker.com/connect/v1/spec
+class CloudError(BaseCloudModel):
+ ## Creates a new error object.
+ # \param id: Unique identifier for this particular occurrence of the problem.
+ # \param title: A short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence
+ # of the problem, except for purposes of localization.
+ # \param code: An application-specific error code, expressed as a string value.
+ # \param detail: A human-readable explanation specific to this occurrence of the problem. Like title, this field's
+ # value can be localized.
+ # \param http_status: The HTTP status code applicable to this problem, converted to string.
+ # \param meta: Non-standard meta-information about the error, depending on the error code.
+ def __init__(self, id: str, code: str, title: str, http_status: str, detail: Optional[str] = None,
+ meta: Optional[Dict[str, Any]] = None, **kwargs) -> None:
+ self.id = id
+ self.code = code
+ self.http_status = http_status
+ self.title = title
+ self.detail = detail
+ self.meta = meta
+ super().__init__(**kwargs)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintJobResponse.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintJobResponse.py
new file mode 100644
index 0000000000..79196ee38c
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintJobResponse.py
@@ -0,0 +1,33 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Optional
+
+from .BaseCloudModel import BaseCloudModel
+
+
+# Model that represents the response received from the cloud after requesting to upload a print job
+# Spec: https://api-staging.ultimaker.com/cura/v1/spec
+class CloudPrintJobResponse(BaseCloudModel):
+ ## Creates a new print job response model.
+ # \param job_id: The job unique ID, e.g. 'kBEeZWEifXbrXviO8mRYLx45P8k5lHVGs43XKvRniPg='.
+ # \param status: The status of the print job.
+ # \param status_description: Contains more details about the status, e.g. the cause of failures.
+ # \param download_url: A signed URL to download the resulting status. Only available when the job is finished.
+ # \param job_name: The name of the print job.
+ # \param slicing_details: Model for slice information.
+ # \param upload_url: The one-time use URL where the toolpath must be uploaded to (only if status is uploading).
+ # \param content_type: The content type of the print job (e.g. text/plain or application/gzip)
+ # \param generated_time: The datetime when the object was generated on the server-side.
+ def __init__(self, job_id: str, status: str, download_url: Optional[str] = None, job_name: Optional[str] = None,
+ upload_url: Optional[str] = None, content_type: Optional[str] = None,
+ status_description: Optional[str] = None, slicing_details: Optional[dict] = None, **kwargs) -> None:
+ self.job_id = job_id
+ self.status = status
+ self.download_url = download_url
+ self.job_name = job_name
+ self.upload_url = upload_url
+ self.content_type = content_type
+ self.status_description = status_description
+ # TODO: Implement slicing details
+ self.slicing_details = slicing_details
+ super().__init__(**kwargs)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintJobUploadRequest.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintJobUploadRequest.py
new file mode 100644
index 0000000000..e59c571558
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintJobUploadRequest.py
@@ -0,0 +1,17 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from .BaseCloudModel import BaseCloudModel
+
+
+# Model that represents the request to upload a print job to the cloud
+# Spec: https://api-staging.ultimaker.com/cura/v1/spec
+class CloudPrintJobUploadRequest(BaseCloudModel):
+ ## Creates a new print job upload request.
+ # \param job_name: The name of the print job.
+ # \param file_size: The size of the file in bytes.
+ # \param content_type: The content type of the print job (e.g. text/plain or application/gzip)
+ def __init__(self, job_name: str, file_size: int, content_type: str, **kwargs) -> None:
+ self.job_name = job_name
+ self.file_size = file_size
+ self.content_type = content_type
+ super().__init__(**kwargs)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintResponse.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintResponse.py
new file mode 100644
index 0000000000..919d1b3c3a
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/CloudPrintResponse.py
@@ -0,0 +1,23 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from datetime import datetime
+from typing import Optional, Union
+
+from .BaseCloudModel import BaseCloudModel
+
+
+# Model that represents the responses received from the cloud after requesting a job to be printed.
+# Spec: https://api-staging.ultimaker.com/connect/v1/spec
+class CloudPrintResponse(BaseCloudModel):
+ ## Creates a new print response object.
+ # \param job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect.
+ # \param status: The status of the print request (queued or failed).
+ # \param generated_time: The datetime when the object was generated on the server-side.
+ # \param cluster_job_id: The unique ID of a print job inside of the cluster. This ID is generated by Cura Connect.
+ def __init__(self, job_id: str, status: str, generated_time: Union[str, datetime],
+ cluster_job_id: Optional[str] = None, **kwargs) -> None:
+ self.job_id = job_id
+ self.status = status
+ self.cluster_job_id = cluster_job_id
+ self.generated_time = self.parseDate(generated_time)
+ super().__init__(**kwargs)
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Models/__init__.py b/plugins/UM3NetworkPrinting/src/Cloud/Models/__init__.py
new file mode 100644
index 0000000000..f3f6970c54
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Models/__init__.py
@@ -0,0 +1,2 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py
new file mode 100644
index 0000000000..176b7e6ab7
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/ToolPathUploader.py
@@ -0,0 +1,148 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# !/usr/bin/env python
+# -*- coding: utf-8 -*-
+from PyQt5.QtCore import QUrl
+from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply, QNetworkAccessManager
+from typing import Optional, Callable, Any, Tuple, cast
+
+from UM.Logger import Logger
+from .Models.CloudPrintJobResponse import CloudPrintJobResponse
+
+
+## Class responsible for uploading meshes to the cloud in separate requests.
+class ToolPathUploader:
+
+ # The maximum amount of times to retry if the server returns one of the RETRY_HTTP_CODES
+ MAX_RETRIES = 10
+
+ # The HTTP codes that should trigger a retry.
+ RETRY_HTTP_CODES = {500, 502, 503, 504}
+
+ # The amount of bytes to send per request
+ BYTES_PER_REQUEST = 256 * 1024
+
+ ## Creates a mesh upload object.
+ # \param manager: The network access manager that will handle the HTTP requests.
+ # \param print_job: The print job response that was returned by the cloud after registering the upload.
+ # \param data: The mesh bytes to be uploaded.
+ # \param on_finished: The method to be called when done.
+ # \param on_progress: The method to be called when the progress changes (receives a percentage 0-100).
+ # \param on_error: The method to be called when an error occurs.
+ def __init__(self, manager: QNetworkAccessManager, print_job: CloudPrintJobResponse, data: bytes,
+ on_finished: Callable[[], Any], on_progress: Callable[[int], Any], on_error: Callable[[], Any]
+ ) -> None:
+ self._manager = manager
+ self._print_job = print_job
+ self._data = data
+
+ self._on_finished = on_finished
+ self._on_progress = on_progress
+ self._on_error = on_error
+
+ self._sent_bytes = 0
+ self._retries = 0
+ self._finished = False
+ self._reply = None # type: Optional[QNetworkReply]
+
+ ## Returns the print job for which this object was created.
+ @property
+ def printJob(self):
+ return self._print_job
+
+ ## Creates a network request to the print job upload URL, adding the needed content range header.
+ def _createRequest(self) -> QNetworkRequest:
+ request = QNetworkRequest(QUrl(self._print_job.upload_url))
+ request.setHeader(QNetworkRequest.ContentTypeHeader, self._print_job.content_type)
+
+ first_byte, last_byte = self._chunkRange()
+ content_range = "bytes {}-{}/{}".format(first_byte, last_byte - 1, len(self._data))
+ request.setRawHeader(b"Content-Range", content_range.encode())
+ Logger.log("i", "Uploading %s to %s", content_range, self._print_job.upload_url)
+
+ return request
+
+ ## Determines the bytes that should be uploaded next.
+ # \return: A tuple with the first and the last byte to upload.
+ def _chunkRange(self) -> Tuple[int, int]:
+ last_byte = min(len(self._data), self._sent_bytes + self.BYTES_PER_REQUEST)
+ return self._sent_bytes, last_byte
+
+ ## Starts uploading the mesh.
+ def start(self) -> None:
+ if self._finished:
+ # reset state.
+ self._sent_bytes = 0
+ self._retries = 0
+ self._finished = False
+ self._uploadChunk()
+
+ ## Stops uploading the mesh, marking it as finished.
+ def stop(self):
+ Logger.log("i", "Stopped uploading")
+ self._finished = True
+
+ ## Uploads a chunk of the mesh to the cloud.
+ def _uploadChunk(self) -> None:
+ if self._finished:
+ raise ValueError("The upload is already finished")
+
+ first_byte, last_byte = self._chunkRange()
+ request = self._createRequest()
+
+ # now send the reply and subscribe to the results
+ self._reply = self._manager.put(request, self._data[first_byte:last_byte])
+ self._reply.finished.connect(self._finishedCallback)
+ self._reply.uploadProgress.connect(self._progressCallback)
+ self._reply.error.connect(self._errorCallback)
+
+ ## Handles an update to the upload progress
+ # \param bytes_sent: The amount of bytes sent in the current request.
+ # \param bytes_total: The amount of bytes to send in the current request.
+ def _progressCallback(self, bytes_sent: int, bytes_total: int) -> None:
+ Logger.log("i", "Progress callback %s / %s", bytes_sent, bytes_total)
+ if bytes_total:
+ total_sent = self._sent_bytes + bytes_sent
+ self._on_progress(int(total_sent / len(self._data) * 100))
+
+ ## Handles an error uploading.
+ def _errorCallback(self) -> None:
+ reply = cast(QNetworkReply, self._reply)
+ body = bytes(reply.readAll()).decode()
+ Logger.log("e", "Received error while uploading: %s", body)
+ self.stop()
+ self._on_error()
+
+ ## Checks whether a chunk of data was uploaded successfully, starting the next chunk if needed.
+ def _finishedCallback(self) -> None:
+ reply = cast(QNetworkReply, self._reply)
+ Logger.log("i", "Finished callback %s %s",
+ reply.attribute(QNetworkRequest.HttpStatusCodeAttribute), reply.url().toString())
+
+ status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) # type: int
+
+ # check if we should retry the last chunk
+ if self._retries < self.MAX_RETRIES and status_code in self.RETRY_HTTP_CODES:
+ self._retries += 1
+ Logger.log("i", "Retrying %s/%s request %s", self._retries, self.MAX_RETRIES, reply.url().toString())
+ self._uploadChunk()
+ return
+
+ # Http codes that are not to be retried are assumed to be errors.
+ if status_code > 308:
+ self._errorCallback()
+ return
+
+ Logger.log("d", "status_code: %s, Headers: %s, body: %s", status_code,
+ [bytes(header).decode() for header in reply.rawHeaderList()], bytes(reply.readAll()).decode())
+ self._chunkUploaded()
+
+ ## Handles a chunk of data being uploaded, starting the next chunk if needed.
+ def _chunkUploaded(self) -> None:
+ # We got a successful response. Let's start the next chunk or report the upload is finished.
+ first_byte, last_byte = self._chunkRange()
+ self._sent_bytes += last_byte - first_byte
+ if self._sent_bytes >= len(self._data):
+ self.stop()
+ self._on_finished()
+ else:
+ self._uploadChunk()
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/Utils.py b/plugins/UM3NetworkPrinting/src/Cloud/Utils.py
new file mode 100644
index 0000000000..5136e0e7db
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Cloud/Utils.py
@@ -0,0 +1,54 @@
+from datetime import datetime, timedelta
+from typing import TypeVar, Dict, Tuple, List
+
+from UM import i18nCatalog
+
+T = TypeVar("T")
+U = TypeVar("U")
+
+
+## Splits the given dictionaries into three lists (in a tuple):
+# - `removed`: Items that were in the first argument but removed in the second one.
+# - `added`: Items that were not in the first argument but were included in the second one.
+# - `updated`: Items that were in both dictionaries. Both values are given in a tuple.
+# \param previous: The previous items
+# \param received: The received items
+# \return: The tuple (removed, added, updated) as explained above.
+def findChanges(previous: Dict[str, T], received: Dict[str, U]) -> Tuple[List[T], List[U], List[Tuple[T, U]]]:
+ previous_ids = set(previous)
+ received_ids = set(received)
+
+ removed_ids = previous_ids.difference(received_ids)
+ new_ids = received_ids.difference(previous_ids)
+ updated_ids = received_ids.intersection(previous_ids)
+
+ removed = [previous[removed_id] for removed_id in removed_ids]
+ added = [received[new_id] for new_id in new_ids]
+ updated = [(previous[updated_id], received[updated_id]) for updated_id in updated_ids]
+
+ return removed, added, updated
+
+
+def formatTimeCompleted(seconds_remaining: int) -> str:
+ completed = datetime.now() + timedelta(seconds=seconds_remaining)
+ return "{hour:02d}:{minute:02d}".format(hour = completed.hour, minute = completed.minute)
+
+
+def formatDateCompleted(seconds_remaining: int) -> str:
+ now = datetime.now()
+ completed = now + timedelta(seconds=seconds_remaining)
+ days = (completed.date() - now.date()).days
+ i18n = i18nCatalog("cura")
+
+ # If finishing date is more than 7 days out, using "Mon Dec 3 at HH:MM" format
+ if days >= 7:
+ return completed.strftime("%a %b ") + "{day}".format(day = completed.day)
+ # If finishing date is within the next week, use "Monday at HH:MM" format
+ elif days >= 2:
+ return completed.strftime("%a")
+ # If finishing tomorrow, use "tomorrow at HH:MM" format
+ elif days >= 1:
+ return i18n.i18nc("@info:status", "tomorrow")
+ # If finishing today, use "today at HH:MM" format
+ else:
+ return i18n.i18nc("@info:status", "today")
diff --git a/plugins/UM3NetworkPrinting/src/Cloud/__init__.py b/plugins/UM3NetworkPrinting/src/Cloud/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py
index 8314b0f089..b48f9380e1 100644
--- a/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py
+++ b/plugins/UM3NetworkPrinting/src/ClusterUM3OutputDevice.py
@@ -1,45 +1,41 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
-from typing import Any, cast, Optional, Set, Tuple, Union
+from typing import Any, cast, Tuple, Union, Optional, Dict, List
+from time import time
+
+import io # To create the correct buffers for sending data to the printer.
+import json
+import os
from UM.FileHandler.FileHandler import FileHandler
-from UM.FileHandler.FileWriter import FileWriter # To choose based on the output file mode (text vs. binary).
from UM.FileHandler.WriteFileJob import WriteFileJob # To call the file writer asynchronously.
from UM.Logger import Logger
from UM.Settings.ContainerRegistry import ContainerRegistry
from UM.i18n import i18nCatalog
from UM.Message import Message
-from UM.Qt.Duration import Duration, DurationFormat
-from UM.OutputDevice import OutputDeviceError # To show that something went wrong when writing.
from UM.Scene.SceneNode import SceneNode # For typing.
-from UM.Version import Version # To check against firmware versions for support.
from cura.CuraApplication import CuraApplication
from cura.PrinterOutput.ConfigurationModel import ConfigurationModel
from cura.PrinterOutput.ExtruderConfigurationModel import ExtruderConfigurationModel
-from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutputDevice, AuthState
+from cura.PrinterOutput.NetworkedPrinterOutputDevice import AuthState, NetworkedPrinterOutputDevice
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
+from cura.PrinterOutputDevice import ConnectionType
+from .Cloud.Utils import formatTimeCompleted, formatDateCompleted
from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController
-from .SendMaterialJob import SendMaterialJob
from .ConfigurationChangeModel import ConfigurationChangeModel
+from .MeshFormatHandler import MeshFormatHandler
+from .SendMaterialJob import SendMaterialJob
from .UM3PrintJobOutputModel import UM3PrintJobOutputModel
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from PyQt5.QtGui import QDesktopServices, QImage
from PyQt5.QtCore import pyqtSlot, QUrl, pyqtSignal, pyqtProperty, QObject
-from time import time
-from datetime import datetime
-from typing import Optional, Dict, List
-
-import io # To create the correct buffers for sending data to the printer.
-import json
-import os
-
i18n_catalog = i18nCatalog("cura")
@@ -49,26 +45,27 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
activeCameraUrlChanged = pyqtSignal()
receivedPrintJobsChanged = pyqtSignal()
- # This is a bit of a hack, as the notify can only use signals that are defined by the class that they are in.
- # Inheritance doesn't seem to work. Tying them together does work, but i'm open for better suggestions.
- clusterPrintersChanged = pyqtSignal()
+ # Notify can only use signals that are defined by the class that they are in, not inherited ones.
+ # Therefore we create a private signal used to trigger the printersChanged signal.
+ _clusterPrintersChanged = pyqtSignal()
def __init__(self, device_id, address, properties, parent = None) -> None:
- super().__init__(device_id = device_id, address = address, properties=properties, parent = parent)
+ super().__init__(device_id = device_id, address = address, properties=properties, connection_type = ConnectionType.NetworkConnection, parent = parent)
self._api_prefix = "/cluster-api/v1/"
self._number_of_extruders = 2
- self._dummy_lambdas = ("", {}, io.BytesIO()) #type: Tuple[str, Dict, Union[io.StringIO, io.BytesIO]]
+ self._dummy_lambdas = (
+ "", {}, io.BytesIO()
+ ) # type: Tuple[Optional[str], Dict[str, Union[str, int, bool]], Union[io.StringIO, io.BytesIO]]
self._print_jobs = [] # type: List[UM3PrintJobOutputModel]
self._received_print_jobs = False # type: bool
- self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/ClusterMonitorItem.qml")
- self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/ClusterControlItem.qml")
+ self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/MonitorStage.qml")
- # See comments about this hack with the clusterPrintersChanged signal
- self.printersChanged.connect(self.clusterPrintersChanged)
+ # Trigger the printersChanged signal when the private signal is triggered
+ self.printersChanged.connect(self._clusterPrintersChanged)
self._accepts_commands = True # type: bool
@@ -101,53 +98,19 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._active_camera_url = QUrl() # type: QUrl
- 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:
self.writeStarted.emit(self)
self.sendMaterialProfiles()
- # Formats supported by this application (file types that we can actually write).
- if file_handler:
- file_formats = file_handler.getSupportedFileTypesWrite()
- else:
- file_formats = CuraApplication.getInstance().getMeshFileHandler().getSupportedFileTypesWrite()
-
- global_stack = CuraApplication.getInstance().getGlobalContainerStack()
- # Create a list from the supported file formats string.
- if not global_stack:
- Logger.log("e", "Missing global stack!")
- return
-
- machine_file_formats = global_stack.getMetaDataEntry("file_formats").split(";")
- machine_file_formats = [file_type.strip() for file_type in machine_file_formats]
- # Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
- if "application/x-ufp" not in machine_file_formats and Version(self.firmwareVersion) >= Version("4.4"):
- machine_file_formats = ["application/x-ufp"] + machine_file_formats
-
- # Take the intersection between file_formats and machine_file_formats.
- format_by_mimetype = {format["mime_type"]: format for format in file_formats}
- file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats] #Keep them ordered according to the preference in machine_file_formats.
-
- if len(file_formats) == 0:
- Logger.log("e", "There are no file formats available to write with!")
- raise OutputDeviceError.WriteRequestFailedError(i18n_catalog.i18nc("@info:status", "There are no file formats available to write with!"))
- preferred_format = file_formats[0]
-
- # Just take the first file format available.
- if file_handler is not None:
- writer = file_handler.getWriterByMimeType(cast(str, preferred_format["mime_type"]))
- else:
- writer = CuraApplication.getInstance().getMeshFileHandler().getWriterByMimeType(cast(str, preferred_format["mime_type"]))
-
- if not writer:
- Logger.log("e", "Unexpected error when trying to get the FileWriter")
- return
+ mesh_format = MeshFormatHandler(file_handler, self.firmwareVersion)
# This function pauses with the yield, waiting on instructions on which printer it needs to print with.
- if not writer:
+ if not mesh_format.is_valid:
Logger.log("e", "Missing file or mesh writer!")
return
- self._sending_job = self._sendPrintJob(writer, preferred_format, nodes)
+ self._sending_job = self._sendPrintJob(mesh_format, nodes)
if self._sending_job is not None:
self._sending_job.send(None) # Start the generator.
@@ -187,11 +150,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
# greenlet in order to optionally wait for selectPrinter() to select a
# printer.
# The greenlet yields exactly three times: First time None,
- # \param writer The file writer to use to create the data.
- # \param preferred_format A dictionary containing some information about
- # what format to write to. This is necessary to create the correct buffer
- # types and file extension and such.
- def _sendPrintJob(self, writer: FileWriter, preferred_format: Dict, nodes: List[SceneNode]):
+ # \param mesh_format Object responsible for choosing the right kind of format to write with.
+ def _sendPrintJob(self, mesh_format: MeshFormatHandler, nodes: List[SceneNode]):
Logger.log("i", "Sending print job to printer.")
if self._sending_gcode:
self._error_message = Message(
@@ -205,35 +165,37 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._sending_gcode = True
- target_printer = yield #Potentially wait on the user to select a target printer.
+ # Potentially wait on the user to select a target printer.
+ target_printer = yield # type: Optional[str]
# Using buffering greatly reduces the write time for many lines of gcode
- stream = io.BytesIO() # type: Union[io.BytesIO, io.StringIO]# Binary mode.
- if preferred_format["mode"] == FileWriter.OutputMode.TextMode:
- stream = io.StringIO()
+ stream = mesh_format.createStream()
- job = WriteFileJob(writer, stream, nodes, preferred_format["mode"])
+ job = WriteFileJob(mesh_format.writer, stream, nodes, mesh_format.file_mode)
- self._write_job_progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
- title = i18n_catalog.i18nc("@info:title", "Sending Data"), use_inactivity_timer = False)
+ self._write_job_progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"),
+ lifetime = 0, dismissable = False, progress = -1,
+ title = i18n_catalog.i18nc("@info:title", "Sending Data"),
+ use_inactivity_timer = False)
self._write_job_progress_message.show()
- self._dummy_lambdas = (target_printer, preferred_format, stream)
- job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished)
-
- job.start()
-
- yield True # Return that we had success!
- yield # To prevent having to catch the StopIteration exception.
+ if mesh_format.preferred_format is not None:
+ self._dummy_lambdas = (target_printer, mesh_format.preferred_format, stream)
+ job.finished.connect(self._sendPrintJobWaitOnWriteJobFinished)
+ job.start()
+ yield True # Return that we had success!
+ yield # To prevent having to catch the StopIteration exception.
def _sendPrintJobWaitOnWriteJobFinished(self, job: WriteFileJob) -> None:
if self._write_job_progress_message:
self._write_job_progress_message.hide()
- self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0, dismissable = False, progress = -1,
+ self._progress_message = Message(i18n_catalog.i18nc("@info:status", "Sending data to printer"), lifetime = 0,
+ dismissable = False, progress = -1,
title = i18n_catalog.i18nc("@info:title", "Sending Data"))
- self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = None, description = "")
+ self._progress_message.addAction("Abort", i18n_catalog.i18nc("@action:button", "Cancel"), icon = None,
+ description = "")
self._progress_message.actionTriggered.connect(self._progressMessageActionTriggered)
self._progress_message.show()
parts = []
@@ -257,7 +219,9 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
parts.append(self._createFormPart("name=\"file\"; filename=\"%s\"" % file_name, output))
- self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts, on_finished = self._onPostPrintJobFinished, on_progress = self._onUploadPrintJobProgress)
+ self._latest_reply_handler = self.postFormWithParts("print_jobs/", parts,
+ on_finished = self._onPostPrintJobFinished,
+ on_progress = self._onUploadPrintJobProgress)
@pyqtProperty(QObject, notify = activePrinterChanged)
def activePrinter(self) -> Optional[PrinterOutputModel]:
@@ -291,7 +255,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
# Treat upload progress as response. Uploading can take more than 10 seconds, so if we don't, we can get
# timeout responses if this happens.
self._last_response_time = time()
- if self._progress_message and new_progress > self._progress_message.getProgress():
+ if self._progress_message is not None and new_progress > self._progress_message.getProgress():
self._progress_message.show() # Ensure that the message is visible.
self._progress_message.setProgress(bytes_sent / bytes_total * 100)
@@ -357,7 +321,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
def activePrintJobs(self) -> List[UM3PrintJobOutputModel]:
return [print_job for print_job in self._print_jobs if print_job.assignedPrinter is not None and print_job.state != "queued"]
- @pyqtProperty("QVariantList", notify = clusterPrintersChanged)
+ @pyqtProperty("QVariantList", notify = _clusterPrintersChanged)
def connectedPrintersTypeCount(self) -> List[Dict[str, str]]:
printer_count = {} # type: Dict[str, int]
for printer in self._printers:
@@ -370,25 +334,17 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
result.append({"machine_type": machine_type, "count": str(printer_count[machine_type])})
return result
- @pyqtProperty("QVariantList", notify=clusterPrintersChanged)
+ @pyqtProperty("QVariantList", notify=_clusterPrintersChanged)
def printers(self):
return self._printers
- @pyqtSlot(int, result = str)
- def formatDuration(self, seconds: int) -> str:
- return Duration(seconds).getDisplayString(DurationFormat.Format.Short)
-
@pyqtSlot(int, result = str)
def getTimeCompleted(self, time_remaining: int) -> str:
- current_time = time()
- datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
- return "{hour:02d}:{minute:02d}".format(hour=datetime_completed.hour, minute=datetime_completed.minute)
+ return formatTimeCompleted(time_remaining)
@pyqtSlot(int, result = str)
def getDateCompleted(self, time_remaining: int) -> str:
- current_time = time()
- datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
- return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
+ return formatDateCompleted(time_remaining)
@pyqtSlot(str)
def sendJobToTop(self, print_job_uuid: str) -> None:
@@ -592,7 +548,25 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
def _createMaterialOutputModel(self, material_data: Dict[str, Any]) -> "MaterialOutputModel":
material_manager = CuraApplication.getInstance().getMaterialManager()
- material_group_list = material_manager.getMaterialGroupListByGUID(material_data["guid"])
+ material_group_list = None
+
+ # Avoid crashing if there is no "guid" field in the metadata
+ material_guid = material_data.get("guid")
+ if material_guid:
+ material_group_list = material_manager.getMaterialGroupListByGUID(material_guid)
+
+ # This can happen if the connected machine has no material in one or more extruders (if GUID is empty), or the
+ # material is unknown to Cura, so we should return an "empty" or "unknown" material model.
+ if material_group_list is None:
+ material_name = i18n_catalog.i18nc("@label:material", "Empty") if len(material_data.get("guid", "")) == 0 \
+ else i18n_catalog.i18nc("@label:material", "Unknown")
+
+ return MaterialOutputModel(guid = material_data.get("guid", ""),
+ type = material_data.get("material", ""),
+ color = material_data.get("color", ""),
+ brand = material_data.get("brand", ""),
+ name = material_data.get("name", material_name)
+ )
# Sort the material groups by "is_read_only = True" first, and then the name alphabetically.
read_only_material_group_list = list(filter(lambda x: x.is_read_only, material_group_list))
@@ -618,9 +592,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
color = material_data["color"]
brand = material_data["brand"]
material_type = material_data["material"]
- name = "Empty" if material_data["material"] == "empty" else "Unknown"
- return MaterialOutputModel(guid=material_data["guid"], type=material_type,
- brand=brand, color=color, name=name)
+ name = i18n_catalog.i18nc("@label:material", "Empty") if material_data["material"] == "empty" \
+ else i18n_catalog.i18nc("@label:material", "Unknown")
+ return MaterialOutputModel(guid = material_data["guid"], type = material_type,
+ brand = brand, color = color, name = name)
def _updatePrinter(self, printer: PrinterOutputModel, data: Dict[str, Any]) -> None:
# For some unknown reason the cluster wants UUID for everything, except for sending a job directly to a printer.
diff --git a/plugins/UM3NetworkPrinting/src/ClusterUM3PrinterOutputController.py b/plugins/UM3NetworkPrinting/src/ClusterUM3PrinterOutputController.py
index fcced0b883..fc6798386a 100644
--- a/plugins/UM3NetworkPrinting/src/ClusterUM3PrinterOutputController.py
+++ b/plugins/UM3NetworkPrinting/src/ClusterUM3PrinterOutputController.py
@@ -18,4 +18,3 @@ class ClusterUM3PrinterOutputController(PrinterOutputController):
def setJobState(self, job: "PrintJobOutputModel", state: str):
data = "{\"action\": \"%s\"}" % state
self._output_device.put("print_jobs/%s/action" % job.key, data, on_finished=None)
-
diff --git a/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py b/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py
index be83e04585..b688ee9d7d 100644
--- a/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py
+++ b/plugins/UM3NetworkPrinting/src/DiscoverUM3Action.py
@@ -3,7 +3,7 @@
import os.path
import time
-from typing import cast, Optional
+from typing import Optional, TYPE_CHECKING
from PyQt5.QtCore import pyqtSignal, pyqtProperty, pyqtSlot, QObject
@@ -16,6 +16,9 @@ from cura.MachineAction import MachineAction
from .UM3OutputDevicePlugin import UM3OutputDevicePlugin
+if TYPE_CHECKING:
+ from cura.PrinterOutputDevice import PrinterOutputDevice
+
catalog = i18nCatalog("cura")
@@ -116,22 +119,37 @@ class DiscoverUM3Action(MachineAction):
# Ensure that the connection states are refreshed.
self._network_plugin.reCheckConnections()
- @pyqtSlot(str)
- def setKey(self, key: str) -> None:
- Logger.log("d", "Attempting to set the network key of the active machine to %s", key)
+ # Associates the currently active machine with the given printer device. The network connection information will be
+ # stored into the metadata of the currently active machine.
+ @pyqtSlot(QObject)
+ def associateActiveMachineWithPrinterDevice(self, printer_device: Optional["PrinterOutputDevice"]) -> None:
+ if not printer_device:
+ return
+
+ Logger.log("d", "Attempting to set the network key of the active machine to %s", printer_device.key)
+
global_container_stack = CuraApplication.getInstance().getGlobalContainerStack()
- if global_container_stack:
- meta_data = global_container_stack.getMetaData()
- if "um_network_key" in meta_data:
- previous_network_key= meta_data["um_network_key"]
- global_container_stack.setMetaDataEntry("um_network_key", key)
- # Delete old authentication data.
- Logger.log("d", "Removing old authentication id %s for device %s", global_container_stack.getMetaDataEntry("network_authentication_id", None), key)
- global_container_stack.removeMetaDataEntry("network_authentication_id")
- global_container_stack.removeMetaDataEntry("network_authentication_key")
- CuraApplication.getInstance().getMachineManager().replaceContainersMetadata(key = "um_network_key", value = previous_network_key, new_value = key)
- else:
- global_container_stack.setMetaDataEntry("um_network_key", key)
+ if not global_container_stack:
+ return
+
+ meta_data = global_container_stack.getMetaData()
+ if "um_network_key" in meta_data:
+ previous_network_key = meta_data["um_network_key"]
+ global_container_stack.setMetaDataEntry("um_network_key", printer_device.key)
+ # Delete old authentication data.
+ Logger.log("d", "Removing old authentication id %s for device %s",
+ global_container_stack.getMetaDataEntry("network_authentication_id", None), printer_device.key)
+ global_container_stack.removeMetaDataEntry("network_authentication_id")
+ global_container_stack.removeMetaDataEntry("network_authentication_key")
+ CuraApplication.getInstance().getMachineManager().replaceContainersMetadata(key = "um_network_key", value = previous_network_key, new_value = printer_device.key)
+
+ if "connection_type" in meta_data:
+ previous_connection_type = meta_data["connection_type"]
+ global_container_stack.setMetaDataEntry("connection_type", printer_device.connectionType.value)
+ CuraApplication.getInstance().getMachineManager().replaceContainersMetadata(key = "connection_type", value = previous_connection_type, new_value = printer_device.connectionType.value)
+ else:
+ global_container_stack.setMetaDataEntry("um_network_key", printer_device.key)
+ global_container_stack.setMetaDataEntry("connection_type", printer_device.connectionType.value)
if self._network_plugin:
# Ensure that the connection states are refreshed.
@@ -182,4 +200,3 @@ class DiscoverUM3Action(MachineAction):
# Create extra components
CuraApplication.getInstance().addAdditionalComponent("monitorButtons", self.__additional_components_view.findChild(QObject, "networkPrinterConnectButton"))
- CuraApplication.getInstance().addAdditionalComponent("machinesDetailPane", self.__additional_components_view.findChild(QObject, "networkPrinterConnectionInfo"))
diff --git a/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py b/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py
index 18af22e72f..3ce0460d6b 100644
--- a/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py
+++ b/plugins/UM3NetworkPrinting/src/LegacyUM3OutputDevice.py
@@ -7,6 +7,7 @@ from cura.PrinterOutput.NetworkedPrinterOutputDevice import NetworkedPrinterOutp
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.MaterialOutputModel import MaterialOutputModel
+from cura.PrinterOutputDevice import ConnectionType
from cura.Settings.ContainerManager import ContainerManager
from cura.Settings.ExtruderManager import ExtruderManager
@@ -43,7 +44,7 @@ i18n_catalog = i18nCatalog("cura")
# 5. As a final step, we verify the authentication, as this forces the QT manager to setup the authenticator.
class LegacyUM3OutputDevice(NetworkedPrinterOutputDevice):
def __init__(self, device_id, address: str, properties, parent = None) -> None:
- super().__init__(device_id = device_id, address = address, properties = properties, parent = parent)
+ super().__init__(device_id = device_id, address = address, properties = properties, connection_type = ConnectionType.NetworkConnection, parent = parent)
self._api_prefix = "/api/v1/"
self._number_of_extruders = 2
diff --git a/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py
new file mode 100644
index 0000000000..c3cd82a86d
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/MeshFormatHandler.py
@@ -0,0 +1,115 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+import io
+from typing import Optional, Dict, Union, List, cast
+
+from UM.FileHandler.FileHandler import FileHandler
+from UM.FileHandler.FileWriter import FileWriter
+from UM.Logger import Logger
+from UM.OutputDevice import OutputDeviceError # To show that something went wrong when writing.
+from UM.Scene.SceneNode import SceneNode
+from UM.Version import Version # To check against firmware versions for support.
+from UM.i18n import i18nCatalog
+from cura.CuraApplication import CuraApplication
+
+
+I18N_CATALOG = i18nCatalog("cura")
+
+
+## This class is responsible for choosing the formats used by the connected clusters.
+class MeshFormatHandler:
+
+ def __init__(self, file_handler: Optional[FileHandler], firmware_version: str) -> None:
+ self._file_handler = file_handler or CuraApplication.getInstance().getMeshFileHandler()
+ self._preferred_format = self._getPreferredFormat(firmware_version)
+ self._writer = self._getWriter(self.mime_type) if self._preferred_format else None
+
+ @property
+ def is_valid(self) -> bool:
+ return bool(self._writer)
+
+ ## Chooses the preferred file format.
+ # \return A dict with the file format details, with the following keys:
+ # {id: str, extension: str, description: str, mime_type: str, mode: int, hide_in_file_dialog: bool}
+ @property
+ def preferred_format(self) -> Optional[Dict[str, Union[str, int, bool]]]:
+ return self._preferred_format
+
+ ## Gets the file writer for the given file handler and mime type.
+ # \return A file writer.
+ @property
+ def writer(self) -> Optional[FileWriter]:
+ return self._writer
+
+ @property
+ def mime_type(self) -> str:
+ return cast(str, self._preferred_format["mime_type"])
+
+ ## Gets the file mode (FileWriter.OutputMode.TextMode or FileWriter.OutputMode.BinaryMode)
+ @property
+ def file_mode(self) -> int:
+ return cast(int, self._preferred_format["mode"])
+
+ ## Gets the file extension
+ @property
+ def file_extension(self) -> str:
+ return cast(str, self._preferred_format["extension"])
+
+ ## Creates the right kind of stream based on the preferred format.
+ def createStream(self) -> Union[io.BytesIO, io.StringIO]:
+ if self.file_mode == FileWriter.OutputMode.TextMode:
+ return io.StringIO()
+ else:
+ return io.BytesIO()
+
+ ## Writes the mesh and returns its value.
+ def getBytes(self, nodes: List[SceneNode]) -> bytes:
+ if self.writer is None:
+ raise ValueError("There is no writer for the mesh format handler.")
+ stream = self.createStream()
+ self.writer.write(stream, nodes)
+ value = stream.getvalue()
+ if isinstance(value, str):
+ value = value.encode()
+ return value
+
+ ## Chooses the preferred file format for the given file handler.
+ # \param firmware_version: The version of the firmware.
+ # \return A dict with the file format details.
+ def _getPreferredFormat(self, firmware_version: str) -> Dict[str, Union[str, int, bool]]:
+ # Formats supported by this application (file types that we can actually write).
+ application = CuraApplication.getInstance()
+
+ file_formats = self._file_handler.getSupportedFileTypesWrite()
+
+ global_stack = application.getGlobalContainerStack()
+ # Create a list from the supported file formats string.
+ if not global_stack:
+ Logger.log("e", "Missing global stack!")
+ return {}
+
+ machine_file_formats = global_stack.getMetaDataEntry("file_formats").split(";")
+ machine_file_formats = [file_type.strip() for file_type in machine_file_formats]
+ # Exception for UM3 firmware version >=4.4: UFP is now supported and should be the preferred file format.
+ if "application/x-ufp" not in machine_file_formats and Version(firmware_version) >= Version("4.4"):
+ machine_file_formats = ["application/x-ufp"] + machine_file_formats
+
+ # Take the intersection between file_formats and machine_file_formats.
+ format_by_mimetype = {f["mime_type"]: f for f in file_formats}
+
+ # Keep them ordered according to the preference in machine_file_formats.
+ file_formats = [format_by_mimetype[mimetype] for mimetype in machine_file_formats]
+
+ if len(file_formats) == 0:
+ Logger.log("e", "There are no file formats available to write with!")
+ raise OutputDeviceError.WriteRequestFailedError(
+ I18N_CATALOG.i18nc("@info:status", "There are no file formats available to write with!")
+ )
+ return file_formats[0]
+
+ ## Gets the file writer for the given file handler and mime type.
+ # \param mime_type: The mine type.
+ # \return A file writer.
+ def _getWriter(self, mime_type: str) -> Optional[FileWriter]:
+ # Just take the first file format available.
+ return self._file_handler.getWriterByMimeType(mime_type)
diff --git a/plugins/UM3NetworkPrinting/src/Models.py b/plugins/UM3NetworkPrinting/src/Models.py
new file mode 100644
index 0000000000..c5b9b16665
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/src/Models.py
@@ -0,0 +1,46 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+
+## Base model that maps kwargs to instance attributes.
+class BaseModel:
+ def __init__(self, **kwargs) -> None:
+ self.__dict__.update(kwargs)
+ self.validate()
+
+ # Validates the model, raising an exception if the model is invalid.
+ def validate(self) -> None:
+ pass
+
+
+## Class representing a material that was fetched from the cluster API.
+class ClusterMaterial(BaseModel):
+ def __init__(self, guid: str, version: int, **kwargs) -> None:
+ self.guid = guid # type: str
+ self.version = version # type: int
+ super().__init__(**kwargs)
+
+ def validate(self) -> None:
+ if not self.guid:
+ raise ValueError("guid is required on ClusterMaterial")
+ if not self.version:
+ raise ValueError("version is required on ClusterMaterial")
+
+
+## Class representing a local material that was fetched from the container registry.
+class LocalMaterial(BaseModel):
+ def __init__(self, GUID: str, id: str, version: int, **kwargs) -> None:
+ self.GUID = GUID # type: str
+ self.id = id # type: str
+ self.version = version # type: int
+ super().__init__(**kwargs)
+
+ #
+ def validate(self) -> None:
+ super().validate()
+ if not self.GUID:
+ raise ValueError("guid is required on LocalMaterial")
+ if not self.version:
+ raise ValueError("version is required on LocalMaterial")
+ if not self.id:
+ raise ValueError("id is required on LocalMaterial")
diff --git a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py
index 8491e79c29..8cdd647a25 100644
--- a/plugins/UM3NetworkPrinting/src/SendMaterialJob.py
+++ b/plugins/UM3NetworkPrinting/src/SendMaterialJob.py
@@ -1,99 +1,197 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+import json
+import os
+from typing import Dict, TYPE_CHECKING, Set, Optional
-import json #To understand the list of materials from the printer reply.
-import os #To walk over material files.
-import os.path #To filter on material files.
-from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest #To listen to the reply from the printer.
-from typing import Any, Dict, Set, TYPE_CHECKING
-import urllib.parse #For getting material IDs from their file names.
+from PyQt5.QtNetwork import QNetworkReply, QNetworkRequest
-from UM.Job import Job #The interface we're implementing.
+from UM.Application import Application
+from UM.Job import Job
from UM.Logger import Logger
-from UM.MimeTypeDatabase import MimeTypeDatabase #To strip the extensions of the material profile files.
-from UM.Resources import Resources
-from UM.Settings.ContainerRegistry import ContainerRegistry #To find the GUIDs of materials.
-from cura.CuraApplication import CuraApplication #For the resource types.
+# Absolute imports don't work in plugins
+from .Models import ClusterMaterial, LocalMaterial
if TYPE_CHECKING:
from .ClusterUM3OutputDevice import ClusterUM3OutputDevice
+
## Asynchronous job to send material profiles to the printer.
#
# This way it won't freeze up the interface while sending those materials.
class SendMaterialJob(Job):
+
def __init__(self, device: "ClusterUM3OutputDevice") -> None:
super().__init__()
- self.device = device #type: ClusterUM3OutputDevice
+ self.device = device # type: ClusterUM3OutputDevice
+ ## Send the request to the printer and register a callback
def run(self) -> None:
- self.device.get("materials/", on_finished = self.sendMissingMaterials)
+ self.device.get("materials/", on_finished = self._onGetRemoteMaterials)
- def sendMissingMaterials(self, reply: QNetworkReply) -> None:
- if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200: #Got an error from the HTTP request.
- Logger.log("e", "Couldn't request current material storage on printer. Not syncing materials.")
+ ## Process the materials reply from the printer.
+ #
+ # \param reply The reply from the printer, a json file.
+ def _onGetRemoteMaterials(self, reply: QNetworkReply) -> None:
+ # Got an error from the HTTP request. If we did not receive a 200 something happened.
+ if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
+ Logger.log("e", "Error fetching materials from printer: %s", reply.errorString())
return
- remote_materials_list = reply.readAll().data().decode("utf-8")
+ # Collect materials from the printer's reply and send the missing ones if needed.
+ remote_materials_by_guid = self._parseReply(reply)
+ if remote_materials_by_guid:
+ self._sendMissingMaterials(remote_materials_by_guid)
+
+ ## Determine which materials should be updated and send them to the printer.
+ #
+ # \param remote_materials_by_guid The remote materials by GUID.
+ def _sendMissingMaterials(self, remote_materials_by_guid: Dict[str, ClusterMaterial]) -> None:
+ # Collect local materials
+ local_materials_by_guid = self._getLocalMaterials()
+ if len(local_materials_by_guid) == 0:
+ Logger.log("d", "There are no local materials to synchronize with the printer.")
+ return
+
+ # Find out what materials are new or updated and must be sent to the printer
+ material_ids_to_send = self._determineMaterialsToSend(local_materials_by_guid, remote_materials_by_guid)
+ if len(material_ids_to_send) == 0:
+ Logger.log("d", "There are no remote materials to update.")
+ return
+
+ # Send materials to the printer
+ self._sendMaterials(material_ids_to_send)
+
+ ## From the local and remote materials, determine which ones should be synchronized.
+ #
+ # Makes a Set of id's containing only the id's of the materials that are not on the printer yet or the ones that
+ # are newer in Cura.
+ #
+ # \param local_materials The local materials by GUID.
+ # \param remote_materials The remote materials by GUID.
+ @staticmethod
+ def _determineMaterialsToSend(local_materials: Dict[str, LocalMaterial],
+ remote_materials: Dict[str, ClusterMaterial]) -> Set[str]:
+ return {
+ material.id
+ for guid, material in local_materials.items()
+ if guid not in remote_materials or material.version > remote_materials[guid].version
+ }
+
+ ## Send the materials to the printer.
+ #
+ # The given materials will be loaded from disk en sent to to printer.
+ # The given id's will be matched with filenames of the locally stored materials.
+ #
+ # \param materials_to_send A set with id's of materials that must be sent.
+ def _sendMaterials(self, materials_to_send: Set[str]) -> None:
+ container_registry = Application.getInstance().getContainerRegistry()
+ material_manager = Application.getInstance().getMaterialManager()
+ material_group_dict = material_manager.getAllMaterialGroups()
+
+ for root_material_id in material_group_dict:
+ if root_material_id not in materials_to_send:
+ # If the material does not have to be sent we skip it.
+ continue
+
+ file_path = container_registry.getContainerFilePathById(root_material_id)
+ if not file_path:
+ Logger.log("w", "Cannot get file path for material container [%s]", root_material_id)
+ continue
+
+ file_name = os.path.basename(file_path)
+ self._sendMaterialFile(file_path, file_name, root_material_id)
+
+ ## Send a single material file to the printer.
+ #
+ # Also add the material signature file if that is available.
+ #
+ # \param file_path The path of the material file.
+ # \param file_name The name of the material file.
+ # \param material_id The ID of the material in the file.
+ def _sendMaterialFile(self, file_path: str, file_name: str, material_id: str) -> None:
+ parts = []
+
+ # Add the material file.
+ with open(file_path, "rb") as f:
+ parts.append(self.device.createFormPart("name=\"file\"; filename=\"{file_name}\""
+ .format(file_name = file_name), f.read()))
+
+ # Add the material signature file if needed.
+ signature_file_path = "{}.sig".format(file_path)
+ if os.path.exists(signature_file_path):
+ signature_file_name = os.path.basename(signature_file_path)
+ with open(signature_file_path, "rb") as f:
+ parts.append(self.device.createFormPart("name=\"signature_file\"; filename=\"{file_name}\""
+ .format(file_name = signature_file_name), f.read()))
+
+ Logger.log("d", "Syncing material {material_id} with cluster.".format(material_id = material_id))
+ self.device.postFormWithParts(target = "materials/", parts = parts, on_finished = self.sendingFinished)
+
+ ## Check a reply from an upload to the printer and log an error when the call failed
+ @staticmethod
+ def sendingFinished(reply: QNetworkReply) -> None:
+ if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
+ Logger.log("e", "Received error code from printer when syncing material: {code}, {text}".format(
+ code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute),
+ text = reply.errorString()
+ ))
+
+ ## Parse the reply from the printer
+ #
+ # Parses the reply to a "/materials" request to the printer
+ #
+ # \return a dictionary of ClusterMaterial objects by GUID
+ # \throw KeyError Raised when on of the materials does not include a valid guid
+ @classmethod
+ def _parseReply(cls, reply: QNetworkReply) -> Optional[Dict[str, ClusterMaterial]]:
try:
- remote_materials_list = json.loads(remote_materials_list)
+ remote_materials = json.loads(reply.readAll().data().decode("utf-8"))
+ return {material["guid"]: ClusterMaterial(**material) for material in remote_materials}
+ except UnicodeDecodeError:
+ Logger.log("e", "Request material storage on printer: I didn't understand the printer's answer.")
except json.JSONDecodeError:
Logger.log("e", "Request material storage on printer: I didn't understand the printer's answer.")
- return
- try:
- remote_materials_by_guid = {material["guid"]: material for material in remote_materials_list} #Index by GUID.
- except KeyError:
- Logger.log("e", "Request material storage on printer: Printer's answer was missing GUIDs.")
- return
+ except ValueError:
+ Logger.log("e", "Request material storage on printer: Printer's answer had an incorrect value.")
+ except TypeError:
+ Logger.log("e", "Request material storage on printer: Printer's answer was missing a required value.")
+ return None
- container_registry = ContainerRegistry.getInstance()
- local_materials_list = filter(lambda material: ("GUID" in material and "version" in material and "id" in material), container_registry.findContainersMetadata(type = "material"))
- local_materials_by_guid = {material["GUID"]: material for material in local_materials_list if material["id"] == material["base_file"]}
- for material in local_materials_list: #For each GUID get the material with the highest version number.
- try:
- if int(material["version"]) > local_materials_by_guid[material["GUID"]]["version"]:
- local_materials_by_guid[material["GUID"]] = material
- except ValueError:
- Logger.log("e", "Material {material_id} has invalid version number {number}.".format(material_id = material["id"], number = material["version"]))
- continue
+ ## Retrieves a list of local materials
+ #
+ # Only the new newest version of the local materials is returned
+ #
+ # \return a dictionary of LocalMaterial objects by GUID
+ def _getLocalMaterials(self) -> Dict[str, LocalMaterial]:
+ result = {} # type: Dict[str, LocalMaterial]
+ material_manager = Application.getInstance().getMaterialManager()
+
+ material_group_dict = material_manager.getAllMaterialGroups()
+
+ # Find the latest version of all material containers in the registry.
+ for root_material_id, material_group in material_group_dict.items():
+ material_metadata = material_group.root_material_node.getMetadata()
- materials_to_send = set() #type: Set[Dict[str, Any]]
- for guid, material in local_materials_by_guid.items():
- if guid not in remote_materials_by_guid:
- materials_to_send.add(material["id"])
- continue
try:
- if int(material["version"]) > remote_materials_by_guid[guid]["version"]:
- materials_to_send.add(material["id"])
- continue
+ # material version must be an int
+ material_metadata["version"] = int(material_metadata["version"])
+
+ # Create a new local material
+ local_material = LocalMaterial(**material_metadata)
+ local_material.id = root_material_id
+
+ if local_material.GUID not in result or \
+ local_material.GUID not in result or \
+ local_material.version > result[local_material.GUID].version:
+ result[local_material.GUID] = local_material
+
except KeyError:
- Logger.log("e", "Current material storage on printer was an invalid reply (missing version).")
- return
+ Logger.logException("w", "Local material {} has missing values.".format(material_metadata["id"]))
+ except ValueError:
+ Logger.logException("w", "Local material {} has invalid values.".format(material_metadata["id"]))
+ except TypeError:
+ Logger.logException("w", "Local material {} has invalid values.".format(material_metadata["id"]))
- for file_path in Resources.getAllResourcesOfType(CuraApplication.ResourceTypes.MaterialInstanceContainer):
- try:
- mime_type = MimeTypeDatabase.getMimeTypeForFile(file_path)
- except MimeTypeDatabase.MimeTypeNotFoundError:
- continue #Not the sort of file we'd like to send then.
- _, file_name = os.path.split(file_path)
- material_id = urllib.parse.unquote_plus(mime_type.stripExtension(file_name))
- if material_id not in materials_to_send:
- continue
-
- parts = []
- with open(file_path, "rb") as f:
- parts.append(self.device._createFormPart("name=\"file\"; filename=\"{file_name}\"".format(file_name = file_name), f.read()))
- signature_file_path = file_path + ".sig"
- if os.path.exists(signature_file_path):
- _, signature_file_name = os.path.split(signature_file_path)
- with open(signature_file_path, "rb") as f:
- parts.append(self.device._createFormPart("name=\"signature_file\"; filename=\"{file_name}\"".format(file_name = signature_file_name), f.read()))
-
- Logger.log("d", "Syncing material {material_id} with cluster.".format(material_id = material_id))
- self.device.postFormWithParts(target = "materials/", parts = parts, on_finished = self.sendingFinished)
-
- def sendingFinished(self, reply: QNetworkReply):
- if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
- Logger.log("e", "Received error code from printer when syncing material: {code}".format(code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)))
- Logger.log("e", reply.readAll().data().decode("utf-8"))
\ No newline at end of file
+ return result
diff --git a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py
index 9c070f2de2..4a510903dd 100644
--- a/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py
+++ b/plugins/UM3NetworkPrinting/src/UM3OutputDevicePlugin.py
@@ -1,23 +1,22 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
-
-from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
-from UM.Logger import Logger
-from UM.Application import Application
-from UM.Signal import Signal, signalemitter
-from UM.Version import Version
-
-from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice
-
-from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
-from PyQt5.QtCore import QUrl
-
-from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
+import json
from queue import Queue
from threading import Event, Thread
from time import time
-import json
+from zeroconf import Zeroconf, ServiceBrowser, ServiceStateChange, ServiceInfo
+from PyQt5.QtNetwork import QNetworkRequest, QNetworkAccessManager
+from PyQt5.QtCore import QUrl
+
+from UM.Application import Application
+from UM.OutputDevice.OutputDevicePlugin import OutputDevicePlugin
+from UM.Logger import Logger
+from UM.Signal import Signal, signalemitter
+from UM.Version import Version
+
+from . import ClusterUM3OutputDevice, LegacyUM3OutputDevice
+from .Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager
## This plugin handles the connection detection & creation of output device objects for the UM3 printer.
@@ -31,9 +30,13 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
def __init__(self):
super().__init__()
+
self._zero_conf = None
self._zero_conf_browser = None
+ # Create a cloud output device manager that abstracts all cloud connection logic away.
+ self._cloud_output_device_manager = CloudOutputDeviceManager()
+
# Because the model needs to be created in the same thread as the QMLEngine, we use a signal.
self.addDeviceSignal.connect(self._onAddDevice)
self.removeDeviceSignal.connect(self._onRemoveDevice)
@@ -83,6 +86,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
## Start looking for devices on network.
def start(self):
self.startDiscovery()
+ self._cloud_output_device_manager.start()
def startDiscovery(self):
self.stop()
@@ -114,6 +118,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
if key == um_network_key:
if not self._discovered_devices[key].isConnected():
Logger.log("d", "Attempting to connect with [%s]" % key)
+ active_machine.setMetaDataEntry("connection_type", self._discovered_devices[key].connectionType.value)
self._discovered_devices[key].connect()
self._discovered_devices[key].connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
else:
@@ -139,6 +144,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
if self._zero_conf is not None:
Logger.log("d", "zeroconf close...")
self._zero_conf.close()
+ self._cloud_output_device_manager.stop()
def removeManualDevice(self, key, address = None):
if key in self._discovered_devices:
@@ -283,6 +289,7 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
global_container_stack = Application.getInstance().getGlobalContainerStack()
if global_container_stack and device.getId() == global_container_stack.getMetaDataEntry("um_network_key"):
+ global_container_stack.setMetaDataEntry("connection_type", device.connectionType.value)
device.connect()
device.connectionStateChanged.connect(self._onDeviceConnectionStateChanged)
@@ -325,13 +332,12 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
## Handler for zeroConf detection.
# Return True or False indicating if the process succeeded.
- # Note that this function can take over 3 seconds to complete. Be carefull calling it from the main thread.
+ # Note that this function can take over 3 seconds to complete. Be careful
+ # calling it from the main thread.
def _onServiceChanged(self, zero_conf, service_type, name, state_change):
if state_change == ServiceStateChange.Added:
- Logger.log("d", "Bonjour service added: %s" % name)
-
# First try getting info from zero-conf cache
- info = ServiceInfo(service_type, name, properties={})
+ info = ServiceInfo(service_type, name, properties = {})
for record in zero_conf.cache.entries_with_name(name.lower()):
info.update_record(zero_conf, time(), record)
@@ -342,7 +348,6 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
# Request more data if info is not complete
if not info.address:
- Logger.log("d", "Trying to get address of %s", name)
info = zero_conf.get_service_info(service_type, name)
if info:
@@ -362,4 +367,4 @@ class UM3OutputDevicePlugin(OutputDevicePlugin):
Logger.log("d", "Bonjour service removed: %s" % name)
self.removeDeviceSignal.emit(str(name))
- return True
\ No newline at end of file
+ return True
diff --git a/plugins/UM3NetworkPrinting/src/UM3PrintJobOutputModel.py b/plugins/UM3NetworkPrinting/src/UM3PrintJobOutputModel.py
index 2ac3e6ba4f..4f44ca4af8 100644
--- a/plugins/UM3NetworkPrinting/src/UM3PrintJobOutputModel.py
+++ b/plugins/UM3NetworkPrinting/src/UM3PrintJobOutputModel.py
@@ -1,13 +1,12 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
-from PyQt5.QtCore import pyqtSignal, pyqtProperty, QObject, pyqtSlot
-from typing import Optional, TYPE_CHECKING, List
-from PyQt5.QtCore import QUrl
-from PyQt5.QtGui import QImage
+from typing import List
+
+from PyQt5.QtCore import pyqtProperty, pyqtSignal
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
-
+from cura.PrinterOutput.PrinterOutputController import PrinterOutputController
from .ConfigurationChangeModel import ConfigurationChangeModel
diff --git a/plugins/UM3NetworkPrinting/src/__init__.py b/plugins/UM3NetworkPrinting/src/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/__init__.py b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/__init__.py
new file mode 100644
index 0000000000..777afc92c2
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/__init__.py
@@ -0,0 +1,12 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+import json
+import os
+
+
+def readFixture(fixture_name: str) -> bytes:
+ with open("{}/{}.json".format(os.path.dirname(__file__), fixture_name), "rb") as f:
+ return f.read()
+
+def parseFixture(fixture_name: str) -> dict:
+ return json.loads(readFixture(fixture_name).decode())
diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/getClusterStatusResponse.json b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/getClusterStatusResponse.json
new file mode 100644
index 0000000000..4f9f47fc75
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/getClusterStatusResponse.json
@@ -0,0 +1,95 @@
+{
+ "data": {
+ "generated_time": "2018-12-10T08:23:55.110Z",
+ "printers": [
+ {
+ "configuration": [
+ {
+ "extruder_index": 0,
+ "material": {
+ "material": "empty"
+ },
+ "print_core_id": "AA 0.4"
+ },
+ {
+ "extruder_index": 1,
+ "material": {
+ "material": "empty"
+ },
+ "print_core_id": "AA 0.4"
+ }
+ ],
+ "enabled": true,
+ "firmware_version": "5.1.2.20180807",
+ "friendly_name": "Master-Luke",
+ "ip_address": "10.183.1.140",
+ "machine_variant": "Ultimaker 3",
+ "status": "maintenance",
+ "unique_name": "ultimakersystem-ccbdd30044ec",
+ "uuid": "b3a47ea3-1eeb-4323-9626-6f9c3c888f9e"
+ },
+ {
+ "configuration": [
+ {
+ "extruder_index": 0,
+ "material": {
+ "brand": "Generic",
+ "color": "Generic",
+ "guid": "506c9f0d-e3aa-4bd4-b2d2-23e2425b1aa9",
+ "material": "PLA"
+ },
+ "print_core_id": "AA 0.4"
+ },
+ {
+ "extruder_index": 1,
+ "material": {
+ "brand": "Ultimaker",
+ "color": "Red",
+ "guid": "9cfe5bf1-bdc5-4beb-871a-52c70777842d",
+ "material": "PLA"
+ },
+ "print_core_id": "AA 0.4"
+ }
+ ],
+ "enabled": true,
+ "firmware_version": "4.3.3.20180529",
+ "friendly_name": "UM-Marijn",
+ "ip_address": "10.183.1.166",
+ "machine_variant": "Ultimaker 3",
+ "status": "idle",
+ "unique_name": "ultimakersystem-ccbdd30058ab",
+ "uuid": "6e62c40a-4601-4b0e-9fec-c7c02c59c30a"
+ }
+ ],
+ "print_jobs": [
+ {
+ "assigned_to": "6e62c40a-4601-4b0e-9fec-c7c02c59c30a",
+ "configuration": [
+ {
+ "extruder_index": 0,
+ "material": {
+ "brand": "Ultimaker",
+ "color": "Black",
+ "guid": "3ee70a86-77d8-4b87-8005-e4a1bc57d2ce",
+ "material": "PLA"
+ },
+ "print_core_id": "AA 0.4"
+ }
+ ],
+ "constraints": {},
+ "created_at": "2018-12-10T08:28:04.108Z",
+ "force": false,
+ "last_seen": 500165.109491861,
+ "machine_variant": "Ultimaker 3",
+ "name": "UM3_dragon",
+ "network_error_count": 0,
+ "owner": "Daniel Testing",
+ "started": false,
+ "status": "queued",
+ "time_elapsed": 0,
+ "time_total": 14145,
+ "uuid": "d1c8bd52-5e9f-486a-8c25-a123cc8c7702"
+ }
+ ]
+ }
+}
diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/getClusters.json b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/getClusters.json
new file mode 100644
index 0000000000..5200e3b971
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/getClusters.json
@@ -0,0 +1,17 @@
+{
+ "data": [{
+ "cluster_id": "RIZ6cZbWA_Ua7RZVJhrdVfVpf0z-MqaSHQE4v8aRTtYq",
+ "host_guid": "e90ae0ac-1257-4403-91ee-a44c9b7e8050",
+ "host_name": "ultimakersystem-ccbdd30044ec",
+ "host_version": "5.0.0.20170101",
+ "is_online": true,
+ "status": "active"
+ }, {
+ "cluster_id": "NWKV6vJP_LdYsXgXqAcaNCR0YcLJwar1ugh0ikEZsZs8",
+ "host_guid": "e0ace90a-91ee-1257-4403-e8050a44c9b7",
+ "host_name": "ultimakersystem-30044ecccbdd",
+ "host_version": "5.1.2.20180807",
+ "is_online": true,
+ "status": "active"
+ }]
+}
diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/postJobPrintResponse.json b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/postJobPrintResponse.json
new file mode 100644
index 0000000000..caedcd8732
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/postJobPrintResponse.json
@@ -0,0 +1,8 @@
+{
+ "data": {
+ "cluster_job_id": "9a59d8e9-91d3-4ff6-b4cb-9db91c4094dd",
+ "job_id": "ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE=",
+ "status": "queued",
+ "generated_time": "2018-12-10T08:23:55.110Z"
+ }
+}
diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/putJobUploadResponse.json b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/putJobUploadResponse.json
new file mode 100644
index 0000000000..1304f3a9f6
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/tests/Cloud/Fixtures/putJobUploadResponse.json
@@ -0,0 +1,9 @@
+{
+ "data": {
+ "content_type": "text/plain",
+ "job_id": "ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE=",
+ "job_name": "Ultimaker Robot v3.0",
+ "status": "uploading",
+ "upload_url": "https://api.ultimaker.com/print-job-upload"
+ }
+}
diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/Models/__init__.py b/plugins/UM3NetworkPrinting/tests/Cloud/Models/__init__.py
new file mode 100644
index 0000000000..f3f6970c54
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/tests/Cloud/Models/__init__.py
@@ -0,0 +1,2 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/NetworkManagerMock.py b/plugins/UM3NetworkPrinting/tests/Cloud/NetworkManagerMock.py
new file mode 100644
index 0000000000..e504509d67
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/tests/Cloud/NetworkManagerMock.py
@@ -0,0 +1,105 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+import json
+from typing import Dict, Tuple, Union, Optional, Any
+from unittest.mock import MagicMock
+
+from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest
+
+from UM.Logger import Logger
+from UM.Signal import Signal
+
+
+class FakeSignal:
+ def __init__(self):
+ self._callbacks = []
+
+ def connect(self, callback):
+ self._callbacks.append(callback)
+
+ def disconnect(self, callback):
+ self._callbacks.remove(callback)
+
+ def emit(self, *args, **kwargs):
+ for callback in self._callbacks:
+ callback(*args, **kwargs)
+
+
+## This class can be used to mock the QNetworkManager class and test the code using it.
+# After patching the QNetworkManager class, requests are prepared before they can be executed.
+# Any requests not prepared beforehand will cause KeyErrors.
+class NetworkManagerMock:
+
+ # An enumeration of the supported operations and their code for the network access manager.
+ _OPERATIONS = {
+ "GET": QNetworkAccessManager.GetOperation,
+ "POST": QNetworkAccessManager.PostOperation,
+ "PUT": QNetworkAccessManager.PutOperation,
+ "DELETE": QNetworkAccessManager.DeleteOperation,
+ "HEAD": QNetworkAccessManager.HeadOperation,
+ } # type: Dict[str, int]
+
+ ## Initializes the network manager mock.
+ def __init__(self) -> None:
+ # A dict with the prepared replies, using the format {(http_method, url): reply}
+ self.replies = {} # type: Dict[Tuple[str, str], MagicMock]
+ self.request_bodies = {} # type: Dict[Tuple[str, str], bytes]
+
+ # Signals used in the network manager.
+ self.finished = Signal()
+ self.authenticationRequired = Signal()
+
+ ## Mock implementation of the get, post, put, delete and head methods from the network manager.
+ # Since the methods are very simple and the same it didn't make sense to repeat the code.
+ # \param method: The method being called.
+ # \return The mocked function, if the method name is known. Defaults to the standard getattr function.
+ def __getattr__(self, method: str) -> Any:
+ ## This mock implementation will simply return the reply from the prepared ones.
+ # it raises a KeyError if requests are done without being prepared.
+ def doRequest(request: QNetworkRequest, body: Optional[bytes] = None, *_):
+ key = method.upper(), request.url().toString()
+ if body:
+ self.request_bodies[key] = body
+ return self.replies[key]
+
+ operation = self._OPERATIONS.get(method.upper())
+ if operation:
+ return doRequest
+
+ # the attribute is not one of the implemented methods, default to the standard implementation.
+ return getattr(super(), method)
+
+ ## Prepares a server reply for the given parameters.
+ # \param method: The HTTP method.
+ # \param url: The URL being requested.
+ # \param status_code: The HTTP status code for the response.
+ # \param response: The response body from the server (generally json-encoded).
+ def prepareReply(self, method: str, url: str, status_code: int, response: Union[bytes, dict]) -> None:
+ reply_mock = MagicMock()
+ reply_mock.url().toString.return_value = url
+ reply_mock.operation.return_value = self._OPERATIONS[method]
+ reply_mock.attribute.return_value = status_code
+ reply_mock.finished = FakeSignal()
+ reply_mock.isFinished.return_value = False
+ reply_mock.readAll.return_value = response if isinstance(response, bytes) else json.dumps(response).encode()
+ self.replies[method, url] = reply_mock
+ Logger.log("i", "Prepared mock {}-response to {} {}", status_code, method, url)
+
+ ## Gets the request that was sent to the network manager for the given method and URL.
+ # \param method: The HTTP method.
+ # \param url: The URL.
+ def getRequestBody(self, method: str, url: str) -> Optional[bytes]:
+ return self.request_bodies.get((method.upper(), url))
+
+ ## Emits the signal that the reply is ready to all prepared replies.
+ def flushReplies(self) -> None:
+ for key, reply in self.replies.items():
+ Logger.log("i", "Flushing reply to {} {}", *key)
+ reply.isFinished.return_value = True
+ reply.finished.emit()
+ self.finished.emit(reply)
+ self.reset()
+
+ ## Deletes all prepared replies
+ def reset(self) -> None:
+ self.replies.clear()
diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py
new file mode 100644
index 0000000000..0be1d82141
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudApiClient.py
@@ -0,0 +1,117 @@
+# 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 List
+from unittest import TestCase
+from unittest.mock import patch, MagicMock
+
+from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot
+from ...src.Cloud.CloudApiClient import CloudApiClient
+from ...src.Cloud.Models.CloudClusterResponse import CloudClusterResponse
+from ...src.Cloud.Models.CloudClusterStatus import CloudClusterStatus
+from ...src.Cloud.Models.CloudPrintJobResponse import CloudPrintJobResponse
+from ...src.Cloud.Models.CloudPrintJobUploadRequest import CloudPrintJobUploadRequest
+from ...src.Cloud.Models.CloudError import CloudError
+from .Fixtures import readFixture, parseFixture
+from .NetworkManagerMock import NetworkManagerMock
+
+
+class TestCloudApiClient(TestCase):
+ maxDiff = None
+
+ def _errorHandler(self, errors: List[CloudError]):
+ raise Exception("Received unexpected error: {}".format(errors))
+
+ def setUp(self):
+ super().setUp()
+ self.account = MagicMock()
+ self.account.isLoggedIn.return_value = True
+
+ self.network = NetworkManagerMock()
+ with patch("plugins.UM3NetworkPrinting.src.Cloud.CloudApiClient.QNetworkAccessManager", return_value = self.network):
+ self.api = CloudApiClient(self.account, self._errorHandler)
+
+ def test_getClusters(self):
+ result = []
+
+ response = readFixture("getClusters")
+ data = parseFixture("getClusters")["data"]
+
+ self.network.prepareReply("GET", CuraCloudAPIRoot + "/connect/v1/clusters", 200, response)
+ # The callback is a function that adds the result of the call to getClusters to the result list
+ self.api.getClusters(lambda clusters: result.extend(clusters))
+
+ self.network.flushReplies()
+
+ self.assertEqual([CloudClusterResponse(**data[0]), CloudClusterResponse(**data[1])], result)
+
+ def test_getClusterStatus(self):
+ result = []
+
+ response = readFixture("getClusterStatusResponse")
+ data = parseFixture("getClusterStatusResponse")["data"]
+
+ url = CuraCloudAPIRoot + "/connect/v1/clusters/R0YcLJwar1ugh0ikEZsZs8NWKV6vJP_LdYsXgXqAcaNC/status"
+ self.network.prepareReply("GET", url, 200, response)
+ self.api.getClusterStatus("R0YcLJwar1ugh0ikEZsZs8NWKV6vJP_LdYsXgXqAcaNC", lambda s: result.append(s))
+
+ self.network.flushReplies()
+
+ self.assertEqual([CloudClusterStatus(**data)], result)
+
+ def test_requestUpload(self):
+
+ results = []
+
+ response = readFixture("putJobUploadResponse")
+
+ self.network.prepareReply("PUT", CuraCloudAPIRoot + "/cura/v1/jobs/upload", 200, response)
+ request = CloudPrintJobUploadRequest(job_name = "job name", file_size = 143234, content_type = "text/plain")
+ self.api.requestUpload(request, lambda r: results.append(r))
+ self.network.flushReplies()
+
+ self.assertEqual(["text/plain"], [r.content_type for r in results])
+ self.assertEqual(["uploading"], [r.status for r in results])
+
+ def test_uploadToolPath(self):
+
+ results = []
+ progress = MagicMock()
+
+ data = parseFixture("putJobUploadResponse")["data"]
+ upload_response = CloudPrintJobResponse(**data)
+
+ # Network client doesn't look into the reply
+ self.network.prepareReply("PUT", upload_response.upload_url, 200, b'{}')
+
+ mesh = ("1234" * 100000).encode()
+ self.api.uploadToolPath(upload_response, mesh, lambda: results.append("sent"), progress.advance, progress.error)
+
+ for _ in range(10):
+ self.network.flushReplies()
+ self.network.prepareReply("PUT", upload_response.upload_url, 200, b'{}')
+
+ self.assertEqual(["sent"], results)
+
+ def test_requestPrint(self):
+
+ results = []
+
+ response = readFixture("postJobPrintResponse")
+
+ cluster_id = "NWKV6vJP_LdYsXgXqAcaNCR0YcLJwar1ugh0ikEZsZs8"
+ cluster_job_id = "9a59d8e9-91d3-4ff6-b4cb-9db91c4094dd"
+ job_id = "ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE="
+
+ self.network.prepareReply("POST",
+ CuraCloudAPIRoot + "/connect/v1/clusters/{}/print/{}"
+ .format(cluster_id, job_id),
+ 200, response)
+
+ self.api.requestPrint(cluster_id, job_id, lambda r: results.append(r))
+
+ self.network.flushReplies()
+
+ self.assertEqual([job_id], [r.job_id for r in results])
+ self.assertEqual([cluster_job_id], [r.cluster_job_id for r in results])
+ self.assertEqual(["queued"], [r.status for r in results])
diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py
new file mode 100644
index 0000000000..191b92bdd5
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDevice.py
@@ -0,0 +1,145 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+import json
+from unittest import TestCase
+from unittest.mock import patch, MagicMock
+
+from UM.Scene.SceneNode import SceneNode
+from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot
+from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
+from ...src.Cloud.CloudApiClient import CloudApiClient
+from ...src.Cloud.CloudOutputDevice import CloudOutputDevice
+from ...src.Cloud.Models.CloudClusterResponse import CloudClusterResponse
+from .Fixtures import readFixture, parseFixture
+from .NetworkManagerMock import NetworkManagerMock
+
+
+class TestCloudOutputDevice(TestCase):
+ maxDiff = None
+
+ CLUSTER_ID = "RIZ6cZbWA_Ua7RZVJhrdVfVpf0z-MqaSHQE4v8aRTtYq"
+ JOB_ID = "ABCDefGHIjKlMNOpQrSTUvYxWZ0-1234567890abcDE="
+ HOST_NAME = "ultimakersystem-ccbdd30044ec"
+ HOST_GUID = "e90ae0ac-1257-4403-91ee-a44c9b7e8050"
+
+ STATUS_URL = "{}/connect/v1/clusters/{}/status".format(CuraCloudAPIRoot, CLUSTER_ID)
+ PRINT_URL = "{}/connect/v1/clusters/{}/print/{}".format(CuraCloudAPIRoot, CLUSTER_ID, JOB_ID)
+ REQUEST_UPLOAD_URL = "{}/cura/v1/jobs/upload".format(CuraCloudAPIRoot)
+
+ def setUp(self):
+ super().setUp()
+ self.app = MagicMock()
+
+ self.patches = [patch("UM.Qt.QtApplication.QtApplication.getInstance", return_value=self.app),
+ patch("UM.Application.Application.getInstance", return_value=self.app)]
+ for patched_method in self.patches:
+ patched_method.start()
+
+ self.cluster = CloudClusterResponse(self.CLUSTER_ID, self.HOST_GUID, self.HOST_NAME, is_online=True,
+ status="active")
+
+ self.network = NetworkManagerMock()
+ self.account = MagicMock(isLoggedIn=True, accessToken="TestAccessToken")
+ self.onError = MagicMock()
+ with patch("plugins.UM3NetworkPrinting.src.Cloud.CloudApiClient.QNetworkAccessManager",
+ return_value = self.network):
+ self._api = CloudApiClient(self.account, self.onError)
+
+ self.device = CloudOutputDevice(self._api, self.cluster)
+ self.cluster_status = parseFixture("getClusterStatusResponse")
+ self.network.prepareReply("GET", self.STATUS_URL, 200, readFixture("getClusterStatusResponse"))
+
+ def tearDown(self):
+ super().tearDown()
+ self.network.flushReplies()
+ for patched_method in self.patches:
+ patched_method.stop()
+
+ def test_status(self):
+ self.device._update()
+ self.network.flushReplies()
+
+ self.assertEqual([PrinterOutputModel, PrinterOutputModel], [type(printer) for printer in self.device.printers])
+
+ controller_fields = {
+ "_output_device": self.device,
+ "can_abort": False,
+ "can_control_manually": False,
+ "can_pause": False,
+ "can_pre_heat_bed": False,
+ "can_pre_heat_hotends": False,
+ "can_send_raw_gcode": False,
+ "can_update_firmware": False,
+ }
+
+ self.assertEqual({printer["uuid"] for printer in self.cluster_status["data"]["printers"]},
+ {printer.key for printer in self.device.printers})
+ self.assertEqual([controller_fields, controller_fields],
+ [printer.getController().__dict__ for printer in self.device.printers])
+
+ self.assertEqual(["UM3PrintJobOutputModel"], [type(printer).__name__ for printer in self.device.printJobs])
+ self.assertEqual({job["uuid"] for job in self.cluster_status["data"]["print_jobs"]},
+ {job.key for job in self.device.printJobs})
+ self.assertEqual({job["owner"] for job in self.cluster_status["data"]["print_jobs"]},
+ {job.owner for job in self.device.printJobs})
+ self.assertEqual({job["name"] for job in self.cluster_status["data"]["print_jobs"]},
+ {job.name for job in self.device.printJobs})
+
+ def test_remove_print_job(self):
+ self.device._update()
+ self.network.flushReplies()
+ self.assertEqual(1, len(self.device.printJobs))
+
+ self.cluster_status["data"]["print_jobs"].clear()
+ self.network.prepareReply("GET", self.STATUS_URL, 200, self.cluster_status)
+
+ self.device._last_request_time = None
+ self.device._update()
+ self.network.flushReplies()
+ self.assertEqual([], self.device.printJobs)
+
+ def test_remove_printers(self):
+ self.device._update()
+ self.network.flushReplies()
+ self.assertEqual(2, len(self.device.printers))
+
+ self.cluster_status["data"]["printers"].clear()
+ self.network.prepareReply("GET", self.STATUS_URL, 200, self.cluster_status)
+
+ self.device._last_request_time = None
+ self.device._update()
+ self.network.flushReplies()
+ self.assertEqual([], self.device.printers)
+
+ def test_print_to_cloud(self):
+ active_machine_mock = self.app.getGlobalContainerStack.return_value
+ active_machine_mock.getMetaDataEntry.side_effect = {"file_formats": "application/gzip"}.get
+
+ request_upload_response = parseFixture("putJobUploadResponse")
+ request_print_response = parseFixture("postJobPrintResponse")
+ self.network.prepareReply("PUT", self.REQUEST_UPLOAD_URL, 201, request_upload_response)
+ self.network.prepareReply("PUT", request_upload_response["data"]["upload_url"], 201, b"{}")
+ self.network.prepareReply("POST", self.PRINT_URL, 200, request_print_response)
+
+ file_handler = MagicMock()
+ file_handler.getSupportedFileTypesWrite.return_value = [{
+ "extension": "gcode.gz",
+ "mime_type": "application/gzip",
+ "mode": 2,
+ }]
+ file_handler.getWriterByMimeType.return_value.write.side_effect = \
+ lambda stream, nodes: stream.write(str(nodes).encode())
+
+ scene_nodes = [SceneNode()]
+ expected_mesh = str(scene_nodes).encode()
+ self.device.requestWrite(scene_nodes, file_handler=file_handler, file_name="FileName")
+
+ self.network.flushReplies()
+ self.assertEqual(
+ {"data": {"content_type": "application/gzip", "file_size": len(expected_mesh), "job_name": "FileName"}},
+ json.loads(self.network.getRequestBody("PUT", self.REQUEST_UPLOAD_URL).decode())
+ )
+ self.assertEqual(expected_mesh,
+ self.network.getRequestBody("PUT", request_upload_response["data"]["upload_url"]))
+
+ self.assertIsNone(self.network.getRequestBody("POST", self.PRINT_URL))
diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py
new file mode 100644
index 0000000000..c5006f35a1
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/tests/Cloud/TestCloudOutputDeviceManager.py
@@ -0,0 +1,124 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+from unittest import TestCase
+from unittest.mock import patch, MagicMock
+
+from UM.OutputDevice.OutputDeviceManager import OutputDeviceManager
+from cura.UltimakerCloudAuthentication import CuraCloudAPIRoot
+from ...src.Cloud.CloudOutputDeviceManager import CloudOutputDeviceManager
+from .Fixtures import parseFixture, readFixture
+from .NetworkManagerMock import NetworkManagerMock, FakeSignal
+
+
+class TestCloudOutputDeviceManager(TestCase):
+ maxDiff = None
+
+ URL = CuraCloudAPIRoot + "/connect/v1/clusters"
+
+ def setUp(self):
+ super().setUp()
+ self.app = MagicMock()
+ self.device_manager = OutputDeviceManager()
+ self.app.getOutputDeviceManager.return_value = self.device_manager
+
+ self.patches = [patch("UM.Qt.QtApplication.QtApplication.getInstance", return_value=self.app),
+ patch("UM.Application.Application.getInstance", return_value=self.app)]
+ for patched_method in self.patches:
+ patched_method.start()
+
+ self.network = NetworkManagerMock()
+ self.timer = MagicMock(timeout = FakeSignal())
+ with patch("plugins.UM3NetworkPrinting.src.Cloud.CloudApiClient.QNetworkAccessManager",
+ return_value = self.network), \
+ patch("plugins.UM3NetworkPrinting.src.Cloud.CloudOutputDeviceManager.QTimer",
+ return_value = self.timer):
+ self.manager = CloudOutputDeviceManager()
+ self.clusters_response = parseFixture("getClusters")
+ self.network.prepareReply("GET", self.URL, 200, readFixture("getClusters"))
+
+ def tearDown(self):
+ try:
+ self._beforeTearDown()
+
+ self.network.flushReplies()
+ self.manager.stop()
+ for patched_method in self.patches:
+ patched_method.stop()
+ finally:
+ super().tearDown()
+
+ ## Before tear down method we check whether the state of the output device manager is what we expect based on the
+ # mocked API response.
+ def _beforeTearDown(self):
+ # let the network send replies
+ self.network.flushReplies()
+ # get the created devices
+ devices = self.device_manager.getOutputDevices()
+ # TODO: Check active device
+
+ response_clusters = self.clusters_response.get("data", [])
+ manager_clusters = sorted([device.clusterData.toDict() for device in self.manager._remote_clusters.values()],
+ key=lambda cluster: cluster['cluster_id'], reverse=True)
+ self.assertEqual(response_clusters, manager_clusters)
+
+ ## Runs the initial request to retrieve the clusters.
+ def _loadData(self):
+ self.manager.start()
+ self.network.flushReplies()
+
+ def test_device_is_created(self):
+ # just create the cluster, it is checked at tearDown
+ self._loadData()
+
+ def test_device_is_updated(self):
+ self._loadData()
+
+ # update the cluster from member variable, which is checked at tearDown
+ self.clusters_response["data"][0]["host_name"] = "New host name"
+ self.network.prepareReply("GET", self.URL, 200, self.clusters_response)
+
+ self.manager._update_timer.timeout.emit()
+
+ def test_device_is_removed(self):
+ self._loadData()
+
+ # delete the cluster from member variable, which is checked at tearDown
+ del self.clusters_response["data"][1]
+ self.network.prepareReply("GET", self.URL, 200, self.clusters_response)
+
+ self.manager._update_timer.timeout.emit()
+
+ def test_device_connects_by_cluster_id(self):
+ active_machine_mock = self.app.getGlobalContainerStack.return_value
+ cluster1, cluster2 = self.clusters_response["data"]
+ cluster_id = cluster1["cluster_id"]
+ active_machine_mock.getMetaDataEntry.side_effect = {"um_cloud_cluster_id": cluster_id}.get
+
+ self._loadData()
+
+ self.assertTrue(self.device_manager.getOutputDevice(cluster1["cluster_id"]).isConnected())
+ self.assertIsNone(self.device_manager.getOutputDevice(cluster2["cluster_id"]))
+ self.assertEquals([], active_machine_mock.setMetaDataEntry.mock_calls)
+
+ def test_device_connects_by_network_key(self):
+ active_machine_mock = self.app.getGlobalContainerStack.return_value
+
+ cluster1, cluster2 = self.clusters_response["data"]
+ network_key = cluster2["host_name"] + ".ultimaker.local"
+ active_machine_mock.getMetaDataEntry.side_effect = {"um_network_key": network_key}.get
+
+ self._loadData()
+
+ self.assertIsNone(self.device_manager.getOutputDevice(cluster1["cluster_id"]))
+ self.assertTrue(self.device_manager.getOutputDevice(cluster2["cluster_id"]).isConnected())
+
+ active_machine_mock.setMetaDataEntry.assert_called_with("um_cloud_cluster_id", cluster2["cluster_id"])
+
+ @patch("plugins.UM3NetworkPrinting.src.Cloud.CloudOutputDeviceManager.Message")
+ def test_api_error(self, message_mock):
+ self.clusters_response = {
+ "errors": [{"id": "notFound", "title": "Not found!", "http_status": "404", "code": "notFound"}]
+ }
+ self.network.prepareReply("GET", self.URL, 200, self.clusters_response)
+ self._loadData()
+ message_mock.return_value.show.assert_called_once_with()
diff --git a/plugins/UM3NetworkPrinting/tests/Cloud/__init__.py b/plugins/UM3NetworkPrinting/tests/Cloud/__init__.py
new file mode 100644
index 0000000000..f3f6970c54
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/tests/Cloud/__init__.py
@@ -0,0 +1,2 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
diff --git a/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py
new file mode 100644
index 0000000000..6eac892af6
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/tests/TestSendMaterialJob.py
@@ -0,0 +1,245 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+import copy
+import io
+import json
+from unittest import TestCase, mock
+from unittest.mock import patch, call, MagicMock
+
+from PyQt5.QtCore import QByteArray
+
+from UM.Application import Application
+
+from cura.Machines.MaterialGroup import MaterialGroup
+from cura.Machines.MaterialNode import MaterialNode
+
+from plugins.UM3NetworkPrinting.src.SendMaterialJob import SendMaterialJob
+
+_FILES_MAP = {"generic_pla_white": "/materials/generic_pla_white.xml.fdm_material",
+ "generic_pla_black": "/materials/generic_pla_black.xml.fdm_material",
+ }
+
+
+@patch("builtins.open", lambda _, __: io.StringIO(""))
+class TestSendMaterialJob(TestCase):
+ # version 1
+ _LOCAL_MATERIAL_WHITE = {"type": "material", "status": "unknown", "id": "generic_pla_white",
+ "base_file": "generic_pla_white", "setting_version": "5", "name": "White PLA",
+ "brand": "Generic", "material": "PLA", "color_name": "White",
+ "GUID": "badb0ee7-87c8-4f3f-9398-938587b67dce", "version": "1", "color_code": "#ffffff",
+ "description": "Test PLA White", "adhesion_info": "Use glue.", "approximate_diameter": "3",
+ "properties": {"density": "1.00", "diameter": "2.85", "weight": "750"},
+ "definition": "fdmprinter", "compatible": True}
+
+ # version 2
+ _LOCAL_MATERIAL_WHITE_NEWER = {"type": "material", "status": "unknown", "id": "generic_pla_white",
+ "base_file": "generic_pla_white", "setting_version": "5", "name": "White PLA",
+ "brand": "Generic", "material": "PLA", "color_name": "White",
+ "GUID": "badb0ee7-87c8-4f3f-9398-938587b67dce", "version": "2",
+ "color_code": "#ffffff",
+ "description": "Test PLA White", "adhesion_info": "Use glue.",
+ "approximate_diameter": "3",
+ "properties": {"density": "1.00", "diameter": "2.85", "weight": "750"},
+ "definition": "fdmprinter", "compatible": True}
+
+ # invalid version: "one"
+ _LOCAL_MATERIAL_WHITE_INVALID_VERSION = {"type": "material", "status": "unknown", "id": "generic_pla_white",
+ "base_file": "generic_pla_white", "setting_version": "5", "name": "White PLA",
+ "brand": "Generic", "material": "PLA", "color_name": "White",
+ "GUID": "badb0ee7-87c8-4f3f-9398-938587b67dce", "version": "one",
+ "color_code": "#ffffff",
+ "description": "Test PLA White", "adhesion_info": "Use glue.",
+ "approximate_diameter": "3",
+ "properties": {"density": "1.00", "diameter": "2.85", "weight": "750"},
+ "definition": "fdmprinter", "compatible": True}
+
+ _LOCAL_MATERIAL_WHITE_ALL_RESULT = {"generic_pla_white": MaterialGroup("generic_pla_white",
+ MaterialNode(_LOCAL_MATERIAL_WHITE))}
+
+ _LOCAL_MATERIAL_WHITE_NEWER_ALL_RESULT = {"generic_pla_white": MaterialGroup("generic_pla_white",
+ MaterialNode(_LOCAL_MATERIAL_WHITE_NEWER))}
+
+ _LOCAL_MATERIAL_WHITE_INVALID_VERSION_ALL_RESULT = {"generic_pla_white": MaterialGroup("generic_pla_white",
+ MaterialNode(_LOCAL_MATERIAL_WHITE_INVALID_VERSION))}
+
+ _LOCAL_MATERIAL_BLACK = {"type": "material", "status": "unknown", "id": "generic_pla_black",
+ "base_file": "generic_pla_black", "setting_version": "5", "name": "Yellow CPE",
+ "brand": "Ultimaker", "material": "CPE", "color_name": "Black",
+ "GUID": "5fbb362a-41f9-4818-bb43-15ea6df34aa4", "version": "1", "color_code": "#000000",
+ "description": "Test PLA Black", "adhesion_info": "Use glue.", "approximate_diameter": "3",
+ "properties": {"density": "1.01", "diameter": "2.85", "weight": "750"},
+ "definition": "fdmprinter", "compatible": True}
+
+ _LOCAL_MATERIAL_BLACK_ALL_RESULT = {"generic_pla_black": MaterialGroup("generic_pla_black",
+ MaterialNode(_LOCAL_MATERIAL_BLACK))}
+
+ _REMOTE_MATERIAL_WHITE = {
+ "guid": "badb0ee7-87c8-4f3f-9398-938587b67dce",
+ "material": "PLA",
+ "brand": "Generic",
+ "version": 1,
+ "color": "White",
+ "density": 1.00
+ }
+
+ _REMOTE_MATERIAL_BLACK = {
+ "guid": "5fbb362a-41f9-4818-bb43-15ea6df34aa4",
+ "material": "PLA",
+ "brand": "Generic",
+ "version": 2,
+ "color": "Black",
+ "density": 1.00
+ }
+
+ def test_run(self):
+ device_mock = MagicMock()
+ job = SendMaterialJob(device_mock)
+ job.run()
+
+ # We expect the materials endpoint to be called when the job runs.
+ device_mock.get.assert_called_with("materials/", on_finished = job._onGetRemoteMaterials)
+
+ def test__onGetRemoteMaterials_withFailedRequest(self):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
+ reply_mock.attribute.return_value = 404
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ # We expect the device not to be called for any follow up.
+ self.assertEqual(0, device_mock.createFormPart.call_count)
+
+ def test__onGetRemoteMaterials_withWrongEncoding(self):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
+ reply_mock.attribute.return_value = 200
+ reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("cp500"))
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ # Given that the parsing fails we do no expect the device to be called for any follow up.
+ self.assertEqual(0, device_mock.createFormPart.call_count)
+
+ def test__onGetRemoteMaterials_withBadJsonAnswer(self):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
+ reply_mock.attribute.return_value = 200
+ reply_mock.readAll.return_value = QByteArray(b"Six sick hicks nick six slick bricks with picks and sticks.")
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ # Given that the parsing fails we do no expect the device to be called for any follow up.
+ self.assertEqual(0, device_mock.createFormPart.call_count)
+
+ def test__onGetRemoteMaterials_withMissingGuidInRemoteMaterial(self):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
+ reply_mock.attribute.return_value = 200
+ remote_material_without_guid = self._REMOTE_MATERIAL_WHITE.copy()
+ del remote_material_without_guid["guid"]
+ reply_mock.readAll.return_value = QByteArray(json.dumps([remote_material_without_guid]).encode("ascii"))
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ # Given that parsing fails we do not expect the device to be called for any follow up.
+ self.assertEqual(0, device_mock.createFormPart.call_count)
+
+ @patch("cura.Machines.MaterialManager.MaterialManager")
+ @patch("cura.Settings.CuraContainerRegistry")
+ @patch("UM.Application")
+ def test__onGetRemoteMaterials_withInvalidVersionInLocalMaterial(self, application_mock, container_registry_mock,
+ material_manager_mock):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
+ application_mock.getContainerRegistry.return_value = container_registry_mock
+ application_mock.getMaterialManager.return_value = material_manager_mock
+
+ reply_mock.attribute.return_value = 200
+ reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
+
+ material_manager_mock.getAllMaterialGroups.return_value = self._LOCAL_MATERIAL_WHITE_INVALID_VERSION_ALL_RESULT.copy()
+
+ with mock.patch.object(Application, "getInstance", new = lambda: application_mock):
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ self.assertEqual(0, device_mock.createFormPart.call_count)
+
+ @patch("UM.Application.Application.getInstance")
+ def test__onGetRemoteMaterials_withNoUpdate(self, application_mock):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
+ container_registry_mock = application_mock.getContainerRegistry.return_value
+ material_manager_mock = application_mock.getMaterialManager.return_value
+
+ device_mock.createFormPart.return_value = "_xXx_"
+
+ material_manager_mock.getAllMaterialGroups.return_value = self._LOCAL_MATERIAL_WHITE_ALL_RESULT.copy()
+
+ reply_mock.attribute.return_value = 200
+ reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
+
+ with mock.patch.object(Application, "getInstance", new = lambda: application_mock):
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ self.assertEqual(0, device_mock.createFormPart.call_count)
+ self.assertEqual(0, device_mock.postFormWithParts.call_count)
+
+ @patch("UM.Application.Application.getInstance")
+ def test__onGetRemoteMaterials_withUpdatedMaterial(self, get_instance_mock):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
+ application_mock = get_instance_mock.return_value
+ container_registry_mock = application_mock.getContainerRegistry.return_value
+ material_manager_mock = application_mock.getMaterialManager.return_value
+
+ container_registry_mock.getContainerFilePathById = lambda x: _FILES_MAP.get(x)
+
+ device_mock.createFormPart.return_value = "_xXx_"
+
+ material_manager_mock.getAllMaterialGroups.return_value = self._LOCAL_MATERIAL_WHITE_NEWER_ALL_RESULT.copy()
+
+ reply_mock.attribute.return_value = 200
+ reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_WHITE]).encode("ascii"))
+
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ self.assertEqual(1, device_mock.createFormPart.call_count)
+ self.assertEqual(1, device_mock.postFormWithParts.call_count)
+ self.assertEquals(
+ [call.createFormPart("name=\"file\"; filename=\"generic_pla_white.xml.fdm_material\"", ""),
+ call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)],
+ device_mock.method_calls)
+
+ @patch("UM.Application.Application.getInstance")
+ def test__onGetRemoteMaterials_withNewMaterial(self, application_mock):
+ reply_mock = MagicMock()
+ device_mock = MagicMock()
+ container_registry_mock = application_mock.getContainerRegistry.return_value
+ material_manager_mock = application_mock.getMaterialManager.return_value
+
+ container_registry_mock.getContainerFilePathById = lambda x: _FILES_MAP.get(x)
+
+ device_mock.createFormPart.return_value = "_xXx_"
+
+ all_results = self._LOCAL_MATERIAL_WHITE_ALL_RESULT.copy()
+ for key, value in self._LOCAL_MATERIAL_BLACK_ALL_RESULT.items():
+ all_results[key] = value
+ material_manager_mock.getAllMaterialGroups.return_value = all_results
+
+ reply_mock.attribute.return_value = 200
+ reply_mock.readAll.return_value = QByteArray(json.dumps([self._REMOTE_MATERIAL_BLACK]).encode("ascii"))
+
+ with mock.patch.object(Application, "getInstance", new = lambda: application_mock):
+ job = SendMaterialJob(device_mock)
+ job._onGetRemoteMaterials(reply_mock)
+
+ self.assertEqual(1, device_mock.createFormPart.call_count)
+ self.assertEqual(1, device_mock.postFormWithParts.call_count)
+ self.assertEquals(
+ [call.createFormPart("name=\"file\"; filename=\"generic_pla_white.xml.fdm_material\"", ""),
+ call.postFormWithParts(target = "materials/", parts = ["_xXx_"], on_finished = job.sendingFinished)],
+ device_mock.method_calls)
diff --git a/plugins/UM3NetworkPrinting/tests/__init__.py b/plugins/UM3NetworkPrinting/tests/__init__.py
new file mode 100644
index 0000000000..f3f6970c54
--- /dev/null
+++ b/plugins/UM3NetworkPrinting/tests/__init__.py
@@ -0,0 +1,2 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
diff --git a/plugins/USBPrinting/AutoDetectBaudJob.py b/plugins/USBPrinting/AutoDetectBaudJob.py
index 8b37c4b29d..2fa0af1795 100644
--- a/plugins/USBPrinting/AutoDetectBaudJob.py
+++ b/plugins/USBPrinting/AutoDetectBaudJob.py
@@ -4,6 +4,7 @@
from UM.Job import Job
from UM.Logger import Logger
+from .avr_isp import ispBase
from .avr_isp.stk500v2 import Stk500v2
from time import time, sleep
@@ -14,12 +15,12 @@ from serial import Serial, SerialException
# It tries a pre-set list of baud rates. All these baud rates are validated by requesting the temperature a few times
# and checking if the results make sense. If getResult() is not None, it was able to find a correct baud rate.
class AutoDetectBaudJob(Job):
- def __init__(self, serial_port):
+ def __init__(self, serial_port: int) -> None:
super().__init__()
self._serial_port = serial_port
- self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600]
+ self._all_baud_rates = [115200, 250000, 500000, 230400, 57600, 38400, 19200, 9600]
- def run(self):
+ def run(self) -> None:
Logger.log("d", "Auto detect baud rate started.")
wait_response_timeouts = [3, 15, 30]
wait_bootloader_times = [1.5, 5, 15]
@@ -32,7 +33,7 @@ class AutoDetectBaudJob(Job):
try:
programmer.connect(self._serial_port)
serial = programmer.leaveISP()
- except:
+ except ispBase.IspError:
programmer.close()
for retry in range(tries):
@@ -58,7 +59,7 @@ class AutoDetectBaudJob(Job):
# We already have a serial connection, just change the baud rate.
try:
serial.baudrate = baud_rate
- except:
+ except ValueError:
continue
sleep(wait_bootloader) # Ensure that we are not talking to the boot loader. 1.5 seconds seems to be the magic number
successful_responses = 0
@@ -81,5 +82,5 @@ class AutoDetectBaudJob(Job):
return
serial.write(b"M105\n")
- sleep(15) # Give the printer some time to init and try again.
+ sleep(15) # Give the printer some time to init and try again.
self.setResult(None) # Unable to detect the correct baudrate.
diff --git a/plugins/USBPrinting/MonitorItem.qml b/plugins/USBPrinting/MonitorItem.qml
new file mode 100644
index 0000000000..c86353f814
--- /dev/null
+++ b/plugins/USBPrinting/MonitorItem.qml
@@ -0,0 +1,48 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.10
+import QtQuick.Controls 2.0
+import QtQuick.Layouts 1.3
+
+import UM 1.2 as UM
+import Cura 1.0 as Cura
+Component
+{
+ Item
+ {
+ Rectangle
+ {
+ color: UM.Theme.getColor("main_background")
+
+ anchors.right: parent.right
+ width: parent.width * 0.3
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+
+ Cura.PrintMonitor
+ {
+ anchors.fill: parent
+ }
+
+ Rectangle
+ {
+ id: footerSeparator
+ width: parent.width
+ height: UM.Theme.getSize("wide_lining").height
+ color: UM.Theme.getColor("wide_lining")
+ anchors.bottom: monitorButton.top
+ anchors.bottomMargin: UM.Theme.getSize("thick_margin").height
+ }
+
+ // MonitorButton is actually the bottom footer panel.
+ Cura.MonitorButton
+ {
+ id: monitorButton
+ anchors.bottom: parent.bottom
+ anchors.left: parent.left
+ anchors.right: parent.right
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/plugins/USBPrinting/USBPrinterOutputDevice.py b/plugins/USBPrinting/USBPrinterOutputDevice.py
index e1c39ff8fa..89903b06f4 100644
--- a/plugins/USBPrinting/USBPrinterOutputDevice.py
+++ b/plugins/USBPrinting/USBPrinterOutputDevice.py
@@ -1,12 +1,13 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+import os
from UM.Logger import Logger
from UM.i18n import i18nCatalog
from UM.Qt.Duration import DurationFormat
from cura.CuraApplication import CuraApplication
-from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState
+from cura.PrinterOutputDevice import PrinterOutputDevice, ConnectionState, ConnectionType
from cura.PrinterOutput.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
from cura.PrinterOutput.GenericOutputController import GenericOutputController
@@ -28,7 +29,7 @@ catalog = i18nCatalog("cura")
class USBPrinterOutputDevice(PrinterOutputDevice):
def __init__(self, serial_port: str, baud_rate: Optional[int] = None) -> None:
- super().__init__(serial_port)
+ super().__init__(serial_port, connection_type = ConnectionType.UsbConnection)
self.setName(catalog.i18nc("@item:inmenu", "USB printing"))
self.setShortDescription(catalog.i18nc("@action:button Preceded by 'Ready to'.", "Print via USB"))
self.setDescription(catalog.i18nc("@info:tooltip", "Print via USB"))
@@ -48,7 +49,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._baud_rate = baud_rate
- self._all_baud_rates = [115200, 250000, 230400, 57600, 38400, 19200, 9600]
+ self._all_baud_rates = [115200, 250000, 500000, 230400, 57600, 38400, 19200, 9600]
# Instead of using a timer, we really need the update to be as a thread, as reading from serial can block.
self._update_thread = Thread(target = self._update, daemon = True)
@@ -64,7 +65,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._accepts_commands = True
self._paused = False
- self._printer_busy = False # when printer is preheating and waiting (M190/M109), or when waiting for action on the printer
+ self._printer_busy = False # When printer is preheating and waiting (M190/M109), or when waiting for action on the printer
self.setConnectionText(catalog.i18nc("@info:status", "Connected via USB"))
@@ -77,6 +78,8 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._firmware_name_requested = False
self._firmware_updater = AvrFirmwareUpdater(self)
+ self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "MonitorItem.qml")
+
CuraApplication.getInstance().getOnExitCallbackManager().addCallback(self._checkActivePrintingUponAppExit)
# This is a callback function that checks if there is any printing in progress via USB when the application tries
@@ -176,7 +179,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
return
CuraApplication.getInstance().globalContainerStackChanged.connect(self._onGlobalContainerStackChanged)
self._onGlobalContainerStackChanged()
- self.setConnectionState(ConnectionState.connected)
+ self.setConnectionState(ConnectionState.Connected)
self._update_thread.start()
def _onGlobalContainerStackChanged(self):
@@ -205,7 +208,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._sendCommand(command)
def _sendCommand(self, command: Union[str, bytes]):
- if self._serial is None or self._connection_state != ConnectionState.connected:
+ if self._serial is None or self._connection_state != ConnectionState.Connected:
return
new_command = cast(bytes, command) if type(command) is bytes else cast(str, command).encode() # type: bytes
@@ -219,7 +222,7 @@ class USBPrinterOutputDevice(PrinterOutputDevice):
self._command_received.set()
def _update(self):
- while self._connection_state == ConnectionState.connected and self._serial is not None:
+ while self._connection_state == ConnectionState.Connected and self._serial is not None:
try:
line = self._serial.readline()
except:
diff --git a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py
index bd207d9d96..d4c0d1828e 100644
--- a/plugins/USBPrinting/USBPrinterOutputDeviceManager.py
+++ b/plugins/USBPrinting/USBPrinterOutputDeviceManager.py
@@ -66,7 +66,7 @@ class USBPrinterOutputDeviceManager(QObject, OutputDevicePlugin):
return
changed_device = self._usb_output_devices[serial_port]
- if changed_device.connectionState == ConnectionState.connected:
+ if changed_device.connectionState == ConnectionState.Connected:
self.getOutputDeviceManager().addOutputDevice(changed_device)
else:
self.getOutputDeviceManager().removeOutputDevice(serial_port)
diff --git a/plugins/USBPrinting/plugin.json b/plugins/USBPrinting/plugin.json
index 3484c8a48a..45971d858b 100644
--- a/plugins/USBPrinting/plugin.json
+++ b/plugins/USBPrinting/plugin.json
@@ -1,8 +1,8 @@
{
"name": "USB printing",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
- "api": 5,
+ "version": "1.0.2",
+ "api": "6.0",
"description": "Accepts G-Code and sends them to a printer. Plugin can also update firmware.",
"i18n-catalog": "cura"
}
diff --git a/plugins/UltimakerMachineActions/UMOCheckupMachineAction.qml b/plugins/UltimakerMachineActions/UMOCheckupMachineAction.qml
index 4a1d42e248..2a01cfaa40 100644
--- a/plugins/UltimakerMachineActions/UMOCheckupMachineAction.qml
+++ b/plugins/UltimakerMachineActions/UMOCheckupMachineAction.qml
@@ -19,7 +19,7 @@ Cura.MachineAction
property bool heatupBedStarted: false
property bool printerConnected: Cura.MachineManager.printerConnected
- UM.I18nCatalog { id: catalog; name:"cura"}
+ UM.I18nCatalog { id: catalog; name: "cura"}
Label
{
id: pageTitle
diff --git a/plugins/UltimakerMachineActions/plugin.json b/plugins/UltimakerMachineActions/plugin.json
index b60c7df88e..3e3e0af9b0 100644
--- a/plugins/UltimakerMachineActions/plugin.json
+++ b/plugins/UltimakerMachineActions/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Ultimaker machine actions",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/UserAgreement/UserAgreement.qml b/plugins/UserAgreement/UserAgreement.qml
index 4ee03f4ad5..2e5893fc41 100644
--- a/plugins/UserAgreement/UserAgreement.qml
+++ b/plugins/UserAgreement/UserAgreement.qml
@@ -36,7 +36,7 @@ UM.Dialog
width: parent.width
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
- UM.I18nCatalog { id: catalog; name:"cura" }
+ UM.I18nCatalog { id: catalog; name: "cura" }
Button
{
diff --git a/plugins/UserAgreement/plugin.json b/plugins/UserAgreement/plugin.json
index 50a2aa0441..b172d1f9a2 100644
--- a/plugins/UserAgreement/plugin.json
+++ b/plugins/UserAgreement/plugin.json
@@ -1,8 +1,8 @@
{
"name": "UserAgreement",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Ask the user once if he/she agrees with our license.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/VersionUpgrade/VersionUpgrade21to22/MachineInstance.py b/plugins/VersionUpgrade/VersionUpgrade21to22/MachineInstance.py
index 37b6989add..ff5c33517d 100644
--- a/plugins/VersionUpgrade/VersionUpgrade21to22/MachineInstance.py
+++ b/plugins/VersionUpgrade/VersionUpgrade21to22/MachineInstance.py
@@ -1,14 +1,16 @@
-# Copyright (c) 2016 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
-import UM.VersionUpgrade #To indicate that a file is of incorrect format.
-import UM.VersionUpgradeManager #To schedule more files to be upgraded.
-from UM.Resources import Resources #To get the config storage path.
-
import configparser #To read config files.
import io #To write config files to strings as if they were files.
import os.path #To get the path to write new user profiles to.
+from typing import Dict, List, Optional, Set, Tuple
import urllib #To serialise the user container file name properly.
+import urllib.parse
+
+import UM.VersionUpgrade #To indicate that a file is of incorrect format.
+import UM.VersionUpgradeManager #To schedule more files to be upgraded.
+from UM.Resources import Resources #To get the config storage path.
## Creates a new machine instance instance by parsing a serialised machine
# instance in version 1 of the file format.
@@ -18,7 +20,7 @@ import urllib #To serialise the user container file name properly.
# extension.
# \return A machine instance instance, or None if the file format is
# incorrect.
-def importFrom(serialised, filename):
+def importFrom(serialised: str, filename: str) -> Optional["MachineInstance"]:
try:
return MachineInstance(serialised, filename)
except (configparser.Error, UM.VersionUpgrade.FormatException, UM.VersionUpgrade.InvalidVersionException):
@@ -32,7 +34,7 @@ class MachineInstance:
# \param serialised A string with the contents of a machine instance file,
# without extension.
# \param filename The supposed file name of this machine instance.
- def __init__(self, serialised, filename):
+ def __init__(self, serialised: str, filename: str) -> None:
self._filename = filename
config = configparser.ConfigParser(interpolation = None)
@@ -53,11 +55,11 @@ class MachineInstance:
self._type_name = config.get("general", "type")
self._variant_name = config.get("general", "variant", fallback = "empty_variant")
self._name = config.get("general", "name", fallback = "")
- self._key = config.get("general", "key", fallback = None)
+ self._key = config.get("general", "key", fallback = "")
self._active_profile_name = config.get("general", "active_profile", fallback = "empty_quality")
self._active_material_name = config.get("general", "material", fallback = "empty_material")
- self._machine_setting_overrides = {}
+ self._machine_setting_overrides = {} # type: Dict[str, str]
for key, value in config["machine_settings"].items():
self._machine_setting_overrides[key] = value
@@ -67,7 +69,7 @@ class MachineInstance:
#
# \return A tuple containing the new filename and a serialised form of
# this machine instance, serialised in version 2 of the file format.
- def export(self):
+ def export(self) -> Tuple[List[str], List[str]]:
config = configparser.ConfigParser(interpolation = None) # Build a config file in the form of version 2.
config.add_section("general")
@@ -108,7 +110,7 @@ class MachineInstance:
version_upgrade_manager = UM.VersionUpgradeManager.VersionUpgradeManager.getInstance()
user_version_to_paths_dict = version_upgrade_manager.getStoragePaths("user")
- paths_set = set()
+ paths_set = set() # type: Set[str]
for paths in user_version_to_paths_dict.values():
paths_set |= paths
diff --git a/plugins/VersionUpgrade/VersionUpgrade21to22/Preferences.py b/plugins/VersionUpgrade/VersionUpgrade21to22/Preferences.py
index 842499da86..953837b863 100644
--- a/plugins/VersionUpgrade/VersionUpgrade21to22/Preferences.py
+++ b/plugins/VersionUpgrade/VersionUpgrade21to22/Preferences.py
@@ -1,8 +1,9 @@
-# Copyright (c) 2016 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser #To read config files.
import io #To output config files to string.
+from typing import List, Optional, Tuple
import UM.VersionUpgrade #To indicate that a file is of the wrong format.
@@ -14,7 +15,7 @@ import UM.VersionUpgrade #To indicate that a file is of the wrong format.
# extension.
# \return A representation of those preferences, or None if the file format is
# incorrect.
-def importFrom(serialised, filename):
+def importFrom(serialised: str, filename: str) -> Optional["Preferences"]:
try:
return Preferences(serialised, filename)
except (configparser.Error, UM.VersionUpgrade.FormatException, UM.VersionUpgrade.InvalidVersionException):
@@ -28,7 +29,7 @@ class Preferences:
# \param serialised A serialised version 2 preferences file.
# \param filename The supposed filename of the preferences file, without
# extension.
- def __init__(self, serialised, filename):
+ def __init__(self, serialised: str, filename: str) -> None:
self._filename = filename
self._config = configparser.ConfigParser(interpolation = None)
@@ -50,7 +51,7 @@ class Preferences:
#
# \return A tuple containing the new filename and a serialised version of
# a preferences file in version 3.
- def export(self):
+ def export(self) -> Tuple[List[str], List[str]]:
#Reset the cura/categories_expanded property since it works differently now.
if self._config.has_section("cura") and self._config.has_option("cura", "categories_expanded"):
self._config.remove_option("cura", "categories_expanded")
@@ -58,11 +59,11 @@ class Preferences:
#Translate the setting names in the visible settings.
if self._config.has_section("machines") and self._config.has_option("machines", "setting_visibility"):
visible_settings = self._config.get("machines", "setting_visibility")
- visible_settings = visible_settings.split(",")
+ visible_settings_list = visible_settings.split(",")
import VersionUpgrade21to22 #Import here to prevent a circular dependency.
- visible_settings = [VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateSettingName(setting_name)
- for setting_name in visible_settings]
- visible_settings = ",".join(visible_settings)
+ visible_settings_list = [VersionUpgrade21to22.VersionUpgrade21to22.VersionUpgrade21to22.translateSettingName(setting_name)
+ for setting_name in visible_settings_list]
+ visible_settings = ",".join(visible_settings_list)
self._config.set("machines", "setting_visibility", value = visible_settings)
#Translate the active_instance key.
diff --git a/plugins/VersionUpgrade/VersionUpgrade21to22/Profile.py b/plugins/VersionUpgrade/VersionUpgrade21to22/Profile.py
index 161edcb67c..af9635d384 100644
--- a/plugins/VersionUpgrade/VersionUpgrade21to22/Profile.py
+++ b/plugins/VersionUpgrade/VersionUpgrade21to22/Profile.py
@@ -1,10 +1,9 @@
-# Copyright (c) 2016 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser #To read config files.
import io #To write config files to strings as if they were files.
-from typing import Dict
-from typing import List
+from typing import Dict, List, Optional, Tuple
import UM.VersionUpgrade
from UM.Logger import Logger
@@ -15,7 +14,7 @@ from UM.Logger import Logger
# \param serialised The serialised form of a profile in version 1.
# \param filename The supposed filename of the profile, without extension.
# \return A profile instance, or None if the file format is incorrect.
-def importFrom(serialised, filename):
+def importFrom(serialised: str, filename: str) -> Optional["Profile"]:
try:
return Profile(serialised, filename)
except (configparser.Error, UM.VersionUpgrade.FormatException, UM.VersionUpgrade.InvalidVersionException):
@@ -77,11 +76,11 @@ class Profile:
#
# \return A tuple containing the new filename and a serialised form of
# this profile, serialised in version 2 of the file format.
- def export(self):
+ def export(self) -> Optional[Tuple[List[str], List[str]]]:
import VersionUpgrade21to22 # Import here to prevent circular dependencies.
if self._name == "Current settings":
- return None, None #Can't upgrade these, because the new current profile needs to specify the definition ID and the old file only had the machine instance, not the definition.
+ return None #Can't upgrade these, because the new current profile needs to specify the definition ID and the old file only had the machine instance, not the definition.
config = configparser.ConfigParser(interpolation = None)
diff --git a/plugins/VersionUpgrade/VersionUpgrade21to22/VersionUpgrade21to22.py b/plugins/VersionUpgrade/VersionUpgrade21to22/VersionUpgrade21to22.py
index d8036491bf..536385b19d 100644
--- a/plugins/VersionUpgrade/VersionUpgrade21to22/VersionUpgrade21to22.py
+++ b/plugins/VersionUpgrade/VersionUpgrade21to22/VersionUpgrade21to22.py
@@ -1,7 +1,8 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser #To get version numbers from config files.
+from typing import Dict, Iterable, List, Optional, Set, Tuple
from UM.VersionUpgrade import VersionUpgrade # Superclass of the plugin.
@@ -30,7 +31,7 @@ _machines_with_machine_quality = {
"materials": { "generic_abs", "generic_cpe", "generic_pla", "generic_pva", "generic_cpe_plus", "generic_nylon", "generic_pc", "generic_tpu" },
"variants": { "0.25 mm", "0.4 mm", "0.6 mm", "0.8 mm" }
}
-}
+} # type: Dict[str, Dict[str, Set[str]]]
## How to translate material names from the old version to the new.
_material_translations = {
@@ -41,7 +42,7 @@ _material_translations = {
"Nylon": "generic_nylon",
"PC": "generic_pc",
"TPU": "generic_tpu",
-}
+} # type: Dict[str, str]
## How to translate material names for in the profile names.
_material_translations_profiles = {
@@ -52,17 +53,17 @@ _material_translations_profiles = {
"Nylon": "nylon",
"PC": "pc",
"TPU": "tpu",
-}
+} # type: Dict[str, str]
## How to translate printer names from the old version to the new.
_printer_translations = {
"ultimaker2plus": "ultimaker2_plus"
-}
+} # type: Dict[str, str]
_printer_translations_profiles = {
"ultimaker2plus": "um2p", #Does NOT get included in PLA profiles!
"ultimaker2_extended_plus": "um2ep" #Has no profiles for CPE+, Nylon, PC and TPU!
-}
+} # type: Dict[str, str]
## How to translate profile names from the old version to the new.
#
@@ -116,13 +117,13 @@ _profile_translations = {
"tpu_0.25_high": "um2p_tpu_0.25_high",
"tpu_0.4_normal": "um2p_tpu_0.4_normal",
"tpu_0.6_fast": "um2p_tpu_0.6_fast"
-}
+} # type: Dict[str, str]
## Settings that are no longer in the new version.
_removed_settings = {
"fill_perimeter_gaps",
"support_area_smoothing"
-}
+} # type: Set[str]
## How to translate setting names from the old version to the new.
_setting_name_translations = {
@@ -142,7 +143,7 @@ _setting_name_translations = {
"support_roof_line_distance": "support_interface_line_distance",
"support_roof_line_width": "support_interface_line_width",
"support_roof_pattern": "support_interface_pattern"
-}
+} # type: Dict[str, str]
## Custom profiles become quality_changes. This dictates which quality to base
# the quality_changes profile on.
@@ -190,7 +191,7 @@ _quality_fallbacks = {
#No TPU.
}
}
-}
+} # type: Dict[str, Dict[str, Dict[str, str]]]
## How to translate variants of specific machines from the old version to the
# new.
@@ -207,7 +208,7 @@ _variant_translations = {
"0.6 mm": "ultimaker2_extended_plus_0.6",
"0.8 mm": "ultimaker2_extended_plus_0.8"
}
-}
+} # type: Dict[str, Dict[str, str]]
## How to translate variant names for in the profile names.
_variant_translations_profiles = {
@@ -215,7 +216,7 @@ _variant_translations_profiles = {
"0.4 mm": "0.4",
"0.6 mm": "0.6",
"0.8 mm": "0.8"
-}
+} # type: Dict[str, str]
## Cura 2.2's material profiles use a different naming scheme for variants.
#
@@ -233,7 +234,7 @@ _variant_translations_materials = {
"0.6 mm": "ultimaker2_plus_0.6_mm",
"0.8 mm": "ultimaker2_plus_0.8_mm"
}
-}
+} # type: Dict[str, Dict[str, str]]
## Converts configuration from Cura 2.1's file formats to Cura 2.2's.
#
@@ -245,12 +246,12 @@ class VersionUpgrade21to22(VersionUpgrade):
# number is stored in general/version, so get the data from that key.
#
# \param serialised The contents of a config file.
- # \return \type{int} The version number of that config file.
- def getCfgVersion(self, serialised):
+ # \return The version number of that config file.
+ def getCfgVersion(self, serialised: str) -> int:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
- setting_version = int(parser.get("metadata", "setting_version", fallback = 0))
+ setting_version = int(parser.get("metadata", "setting_version", fallback = "0"))
return format_version * 1000000 + setting_version
## Gets the fallback quality to use for a specific machine-variant-material
@@ -263,7 +264,7 @@ class VersionUpgrade21to22(VersionUpgrade):
# \param variant The variant ID of the user's configuration in 2.2.
# \param material The material ID of the user's configuration in 2.2.
@staticmethod
- def getQualityFallback(machine, variant, material):
+ def getQualityFallback(machine: str, variant: str, material: str) -> str:
if machine not in _quality_fallbacks:
return "normal"
if variant not in _quality_fallbacks[machine]:
@@ -277,14 +278,14 @@ class VersionUpgrade21to22(VersionUpgrade):
# This is required to test if profiles should be converted to a quality
# profile or a quality-changes profile.
@staticmethod
- def builtInProfiles():
+ def builtInProfiles() -> Iterable[str]:
return _profile_translations.keys()
## Gets a set of the machines which now have per-material quality profiles.
#
# \return A set of machine identifiers.
@staticmethod
- def machinesWithMachineQuality():
+ def machinesWithMachineQuality() -> Dict[str, Dict[str, Set[str]]]:
return _machines_with_machine_quality
## Converts machine instances from format version 1 to version 2.
@@ -295,10 +296,10 @@ class VersionUpgrade21to22(VersionUpgrade):
# \return A tuple containing the new filename and the serialised machine
# instance in version 2, or None if the input was not of the correct
# format.
- def upgradeMachineInstance(self, serialised, filename):
+ def upgradeMachineInstance(self, serialised: str, filename: str) -> Optional[Tuple[List[str], List[str]]]:
machine_instance = MachineInstance.importFrom(serialised, filename)
if not machine_instance: #Invalid file format.
- return filename, None
+ return None
return machine_instance.export()
## Converts preferences from format version 2 to version 3.
@@ -309,10 +310,10 @@ class VersionUpgrade21to22(VersionUpgrade):
# \return A tuple containing the new filename and the serialised
# preferences in version 3, or None if the input was not of the correct
# format.
- def upgradePreferences(self, serialised, filename):
+ def upgradePreferences(self, serialised: str, filename: str) -> Optional[Tuple[List[str], List[str]]]:
preferences = Preferences.importFrom(serialised, filename)
if not preferences: #Invalid file format.
- return filename, None
+ return None
return preferences.export()
## Converts profiles from format version 1 to version 2.
@@ -322,10 +323,10 @@ class VersionUpgrade21to22(VersionUpgrade):
# extension.
# \return A tuple containing the new filename and the serialised profile
# in version 2, or None if the input was not of the correct format.
- def upgradeProfile(self, serialised, filename):
+ def upgradeProfile(self, serialised: str, filename: str) -> Optional[Tuple[List[str], List[str]]]:
profile = Profile.importFrom(serialised, filename)
if not profile: # Invalid file format.
- return filename, None
+ return None
return profile.export()
## Translates a material name for the change from Cura 2.1 to 2.2.
@@ -333,7 +334,7 @@ class VersionUpgrade21to22(VersionUpgrade):
# \param material A material name in Cura 2.1.
# \return The name of the corresponding material in Cura 2.2.
@staticmethod
- def translateMaterial(material):
+ def translateMaterial(material: str) -> str:
if material in _material_translations:
return _material_translations[material]
return material
@@ -345,7 +346,7 @@ class VersionUpgrade21to22(VersionUpgrade):
# \return The name of the corresponding material in the quality profiles
# in Cura 2.2.
@staticmethod
- def translateMaterialForProfiles(material):
+ def translateMaterialForProfiles(material: str) -> str:
if material in _material_translations_profiles:
return _material_translations_profiles[material]
return material
@@ -356,7 +357,7 @@ class VersionUpgrade21to22(VersionUpgrade):
# \param printer A printer name in Cura 2.1.
# \return The name of the corresponding printer in Cura 2.2.
@staticmethod
- def translatePrinter(printer):
+ def translatePrinter(printer: str) -> str:
if printer in _printer_translations:
return _printer_translations[printer]
return printer #Doesn't need to be translated.
@@ -367,7 +368,7 @@ class VersionUpgrade21to22(VersionUpgrade):
# \param printer A printer name in 2.1.
# \return The name of the corresponding printer in Cura 2.2.
@staticmethod
- def translatePrinterForProfile(printer):
+ def translatePrinterForProfile(printer: str) -> str:
if printer in _printer_translations_profiles:
return _printer_translations_profiles[printer]
return printer
@@ -378,7 +379,7 @@ class VersionUpgrade21to22(VersionUpgrade):
# \param profile A profile name in the old version.
# \return The corresponding profile name in the new version.
@staticmethod
- def translateProfile(profile):
+ def translateProfile(profile: str) -> str:
if profile in _profile_translations:
return _profile_translations[profile]
return profile #Doesn't need to be translated.
@@ -392,7 +393,7 @@ class VersionUpgrade21to22(VersionUpgrade):
# \param settings A dictionary of settings (as key-value pairs) to update.
# \return The same dictionary.
@staticmethod
- def translateSettings(settings):
+ def translateSettings(settings: Dict[str, str]) -> Dict[str, str]:
new_settings = {}
for key, value in settings.items():
if key in _removed_settings:
@@ -414,7 +415,7 @@ class VersionUpgrade21to22(VersionUpgrade):
# \param setting The name of a setting in Cura 2.1.
# \return The name of the corresponding setting in Cura 2.2.
@staticmethod
- def translateSettingName(setting):
+ def translateSettingName(setting: str) -> str:
if setting in _setting_name_translations:
return _setting_name_translations[setting]
return setting #Doesn't need to be translated.
@@ -426,7 +427,7 @@ class VersionUpgrade21to22(VersionUpgrade):
# 2.2's naming.
# \return The name of the corresponding variant in Cura 2.2.
@staticmethod
- def translateVariant(variant, machine):
+ def translateVariant(variant: str, machine: str) -> str:
if machine in _variant_translations and variant in _variant_translations[machine]:
return _variant_translations[machine][variant]
return variant
@@ -440,7 +441,7 @@ class VersionUpgrade21to22(VersionUpgrade):
# \return The name of the corresponding variant for in material profiles
# in Cura 2.2.
@staticmethod
- def translateVariantForMaterials(variant, machine):
+ def translateVariantForMaterials(variant: str, machine: str) -> str:
if machine in _variant_translations_materials and variant in _variant_translations_materials[machine]:
return _variant_translations_materials[machine][variant]
return variant
@@ -452,7 +453,7 @@ class VersionUpgrade21to22(VersionUpgrade):
# \return The name of the corresponding variant for in quality profiles in
# Cura 2.2.
@staticmethod
- def translateVariantForProfiles(variant):
+ def translateVariantForProfiles(variant: str) -> str:
if variant in _variant_translations_profiles:
return _variant_translations_profiles[variant]
return variant
\ No newline at end of file
diff --git a/plugins/VersionUpgrade/VersionUpgrade21to22/__init__.py b/plugins/VersionUpgrade/VersionUpgrade21to22/__init__.py
index 609781ebfe..67530b9d45 100644
--- a/plugins/VersionUpgrade/VersionUpgrade21to22/__init__.py
+++ b/plugins/VersionUpgrade/VersionUpgrade21to22/__init__.py
@@ -1,11 +1,16 @@
-# Copyright (c) 2016 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Any, Dict, TYPE_CHECKING
+
from . import VersionUpgrade21to22
+if TYPE_CHECKING:
+ from UM.Application import Application
+
upgrade = VersionUpgrade21to22.VersionUpgrade21to22()
-def getMetaData():
+def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
@@ -33,5 +38,5 @@ def getMetaData():
}
}
-def register(app):
+def register(app: "Application") -> Dict[str, Any]:
return { "version_upgrade": upgrade }
diff --git a/plugins/VersionUpgrade/VersionUpgrade21to22/plugin.json b/plugins/VersionUpgrade/VersionUpgrade21to22/plugin.json
index 463fcdc941..cad94c2eb5 100644
--- a/plugins/VersionUpgrade/VersionUpgrade21to22/plugin.json
+++ b/plugins/VersionUpgrade/VersionUpgrade21to22/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Version Upgrade 2.1 to 2.2",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.1 to Cura 2.2.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/VersionUpgrade/VersionUpgrade22to24/VersionUpgrade.py b/plugins/VersionUpgrade/VersionUpgrade22to24/VersionUpgrade.py
index a56f1f807b..ded892d137 100644
--- a/plugins/VersionUpgrade/VersionUpgrade22to24/VersionUpgrade.py
+++ b/plugins/VersionUpgrade/VersionUpgrade22to24/VersionUpgrade.py
@@ -1,18 +1,18 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser #To get version numbers from config files.
+import io
import os
import os.path
-import io
+from typing import Dict, List, Optional, Tuple
from UM.Resources import Resources
from UM.VersionUpgrade import VersionUpgrade # Superclass of the plugin.
import UM.VersionUpgrade
class VersionUpgrade22to24(VersionUpgrade):
-
- def upgradeMachineInstance(self, serialised, filename):
+ def upgradeMachineInstance(self, serialised: str, filename: str) -> Optional[Tuple[List[str], List[str]]]:
# All of this is needed to upgrade custom variant machines from old Cura to 2.4 where
# `definition_changes` instance container has been introduced. Variant files which
# look like the the handy work of the old machine settings plugin are converted directly
@@ -22,11 +22,11 @@ class VersionUpgrade22to24(VersionUpgrade):
config.read_string(serialised) # Read the input string as config file.
if config.get("metadata", "type") == "definition_changes":
# This is not a container stack, don't upgrade it here
- return
+ return None
config.set("general", "version", "3")
- container_list = []
+ container_list = [] # type: List[str]
if config.has_section("containers"):
for index, container_id in config.items("containers"):
container_list.append(container_id)
@@ -37,14 +37,14 @@ class VersionUpgrade22to24(VersionUpgrade):
user_variants = self.__getUserVariants()
name_path_dict = {}
for variant in user_variants:
- name_path_dict[variant.get("name")] = variant.get("path")
+ name_path_dict[variant["name"]] = variant["path"]
user_variant_names = set(container_list).intersection(name_path_dict.keys())
if len(user_variant_names):
# One of the user defined variants appears in the list of containers in the stack.
for variant_name in user_variant_names: # really there should just be one variant to convert.
- config_name = self.__convertVariant(name_path_dict.get(variant_name))
+ config_name = self.__convertVariant(name_path_dict[variant_name])
# Change the name of variant and insert empty_variant into the stack.
new_container_list = []
@@ -64,14 +64,14 @@ class VersionUpgrade22to24(VersionUpgrade):
config.remove_option("general", "containers")
- for index in range(len(container_list)):
- config.set("containers", str(index), container_list[index])
+ for idx in range(len(container_list)):
+ config.set("containers", str(idx), container_list[idx])
output = io.StringIO()
config.write(output)
return [filename], [output.getvalue()]
- def __convertVariant(self, variant_path):
+ def __convertVariant(self, variant_path: str) -> str:
# Copy the variant to the machine_instances/*_settings.inst.cfg
variant_config = configparser.ConfigParser(interpolation = None)
with open(variant_path, "r", encoding = "utf-8") as fhandle:
@@ -99,7 +99,7 @@ class VersionUpgrade22to24(VersionUpgrade):
return config_name
- def __getUserVariants(self):
+ def __getUserVariants(self) -> List[Dict[str, str]]:
resource_path = Resources.getDataStoragePath()
variants_dir = os.path.join(resource_path, "variants")
@@ -113,7 +113,7 @@ class VersionUpgrade22to24(VersionUpgrade):
result.append( { "path": entry.path, "name": config.get("general", "name") } )
return result
- def upgradeExtruderTrain(self, serialised, filename):
+ def upgradeExtruderTrain(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
config = configparser.ConfigParser(interpolation = None)
config.read_string(serialised) # Read the input string as config file.
config.set("general", "version", "3") # Just bump the version number. That is all we need for now.
@@ -122,7 +122,7 @@ class VersionUpgrade22to24(VersionUpgrade):
config.write(output)
return [filename], [output.getvalue()]
- def upgradePreferences(self, serialised, filename):
+ def upgradePreferences(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
config = configparser.ConfigParser(interpolation = None)
config.read_string(serialised)
@@ -142,7 +142,7 @@ class VersionUpgrade22to24(VersionUpgrade):
config.write(output)
return [filename], [output.getvalue()]
- def upgradeQuality(self, serialised, filename):
+ def upgradeQuality(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
config = configparser.ConfigParser(interpolation = None)
config.read_string(serialised) # Read the input string as config file.
config.set("metadata", "type", "quality_changes") # Update metadata/type to quality_changes
@@ -152,9 +152,9 @@ class VersionUpgrade22to24(VersionUpgrade):
config.write(output)
return [filename], [output.getvalue()]
- def getCfgVersion(self, serialised):
+ def getCfgVersion(self, serialised: str) -> int:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
- setting_version = int(parser.get("metadata", "setting_version", fallback = 0))
+ setting_version = int(parser.get("metadata", "setting_version", fallback = "0"))
return format_version * 1000000 + setting_version
diff --git a/plugins/VersionUpgrade/VersionUpgrade22to24/__init__.py b/plugins/VersionUpgrade/VersionUpgrade22to24/__init__.py
index 278b660ec1..fe79333544 100644
--- a/plugins/VersionUpgrade/VersionUpgrade22to24/__init__.py
+++ b/plugins/VersionUpgrade/VersionUpgrade22to24/__init__.py
@@ -1,11 +1,16 @@
-# Copyright (c) 2016 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Any, Dict, TYPE_CHECKING
+
from . import VersionUpgrade
+if TYPE_CHECKING:
+ from UM.Application import Application
+
upgrade = VersionUpgrade.VersionUpgrade22to24()
-def getMetaData():
+def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
@@ -26,5 +31,5 @@ def getMetaData():
}
}
-def register(app):
+def register(app: "Application"):
return { "version_upgrade": upgrade }
diff --git a/plugins/VersionUpgrade/VersionUpgrade22to24/plugin.json b/plugins/VersionUpgrade/VersionUpgrade22to24/plugin.json
index e7a0b1c559..7da1e7a56d 100644
--- a/plugins/VersionUpgrade/VersionUpgrade22to24/plugin.json
+++ b/plugins/VersionUpgrade/VersionUpgrade22to24/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Version Upgrade 2.2 to 2.4",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.2 to Cura 2.4.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/VersionUpgrade/VersionUpgrade25to26/VersionUpgrade25to26.py b/plugins/VersionUpgrade/VersionUpgrade25to26/VersionUpgrade25to26.py
index 6643edb765..6dbcfebc46 100644
--- a/plugins/VersionUpgrade/VersionUpgrade25to26/VersionUpgrade25to26.py
+++ b/plugins/VersionUpgrade/VersionUpgrade25to26/VersionUpgrade25to26.py
@@ -4,6 +4,7 @@
import configparser #To parse the files we need to upgrade and write the new files.
import io #To serialise configparser output to a string.
import os
+from typing import Dict, List, Set, Tuple
from urllib.parse import quote_plus
from UM.Resources import Resources
@@ -12,19 +13,18 @@ from UM.VersionUpgrade import VersionUpgrade
_removed_settings = { #Settings that were removed in 2.5.
"start_layers_at_same_position",
"sub_div_rad_mult"
-}
+} # type: Set[str]
_split_settings = { #These settings should be copied to all settings it was split into.
"support_interface_line_distance": {"support_roof_line_distance", "support_bottom_line_distance"}
-}
+} # type: Dict[str, Set[str]]
## A collection of functions that convert the configuration of the user in Cura
# 2.5 to a configuration for Cura 2.6.
#
# All of these methods are essentially stateless.
class VersionUpgrade25to26(VersionUpgrade):
-
- def __init__(self):
+ def __init__(self) -> None:
super().__init__()
self._current_fdm_printer_count = 2
@@ -39,18 +39,18 @@ class VersionUpgrade25to26(VersionUpgrade):
# \raises ValueError The format of the version number in the file is
# incorrect.
# \raises KeyError The format of the file is incorrect.
- def getCfgVersion(self, serialised):
+ def getCfgVersion(self, serialised: str) -> int:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
- setting_version = int(parser.get("metadata", "setting_version", fallback = 0))
+ setting_version = int(parser.get("metadata", "setting_version", fallback = "0"))
return format_version * 1000000 + setting_version
## Upgrades the preferences file from version 2.5 to 2.6.
#
# \param serialised The serialised form of a preferences file.
# \param filename The name of the file to upgrade.
- def upgradePreferences(self, serialised, filename):
+ def upgradePreferences(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
@@ -86,7 +86,7 @@ class VersionUpgrade25to26(VersionUpgrade):
#
# \param serialised The serialised form of a quality profile.
# \param filename The name of the file to upgrade.
- def upgradeInstanceContainer(self, serialised, filename):
+ def upgradeInstanceContainer(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
@@ -116,7 +116,7 @@ class VersionUpgrade25to26(VersionUpgrade):
#
# \param serialised The serialised form of a quality profile.
# \param filename The name of the file to upgrade.
- def upgradeMachineStack(self, serialised, filename):
+ def upgradeMachineStack(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
@@ -149,7 +149,7 @@ class VersionUpgrade25to26(VersionUpgrade):
return [filename], [output.getvalue()]
## Acquires the next unique extruder stack index number for the Custom FDM Printer.
- def _acquireNextUniqueCustomFdmPrinterExtruderStackIdIndex(self):
+ def _acquireNextUniqueCustomFdmPrinterExtruderStackIdIndex(self) -> int:
extruder_stack_dir = os.path.join(Resources.getDataStoragePath(), "extruders")
file_name_list = os.listdir(extruder_stack_dir)
file_name_list = [os.path.basename(file_name) for file_name in file_name_list]
@@ -169,7 +169,7 @@ class VersionUpgrade25to26(VersionUpgrade):
return self._current_fdm_printer_count
- def _checkCustomFdmPrinterHasExtruderStack(self, machine_id):
+ def _checkCustomFdmPrinterHasExtruderStack(self, machine_id: str) -> bool:
# go through all extruders and make sure that this custom FDM printer has extruder stacks.
extruder_stack_dir = os.path.join(Resources.getDataStoragePath(), "extruders")
has_extruders = False
@@ -197,7 +197,7 @@ class VersionUpgrade25to26(VersionUpgrade):
return has_extruders
- def _createCustomFdmPrinterExtruderStack(self, machine_id: str, position: int, quality_id: str, material_id: str):
+ def _createCustomFdmPrinterExtruderStack(self, machine_id: str, position: int, quality_id: str, material_id: str) -> None:
stack_id = "custom_extruder_%s" % (position + 1)
if self._current_fdm_printer_count > 1:
stack_id += " #%s" % self._current_fdm_printer_count
@@ -256,7 +256,7 @@ class VersionUpgrade25to26(VersionUpgrade):
## Creates a definition changes container which doesn't contain anything for the Custom FDM Printers.
# The container ID will be automatically generated according to the given stack name.
- def _getCustomFdmPrinterDefinitionChanges(self, stack_id: str):
+ def _getCustomFdmPrinterDefinitionChanges(self, stack_id: str) -> configparser.ConfigParser:
# In 2.5, there is no definition_changes container for the Custom FDM printer, so it should be safe to use the
# default name unless some one names the printer as something like "Custom FDM Printer_settings".
definition_changes_id = stack_id + "_settings"
@@ -277,7 +277,7 @@ class VersionUpgrade25to26(VersionUpgrade):
## Creates a user settings container which doesn't contain anything for the Custom FDM Printers.
# The container ID will be automatically generated according to the given stack name.
- def _getCustomFdmPrinterUserSettings(self, stack_id: str):
+ def _getCustomFdmPrinterUserSettings(self, stack_id: str) -> configparser.ConfigParser:
# For the extruder stacks created in the upgrade, also create user_settings containers so the user changes
# will be saved.
user_settings_id = stack_id + "_user"
diff --git a/plugins/VersionUpgrade/VersionUpgrade25to26/__init__.py b/plugins/VersionUpgrade/VersionUpgrade25to26/__init__.py
index 67aa73233f..c74b3218b6 100644
--- a/plugins/VersionUpgrade/VersionUpgrade25to26/__init__.py
+++ b/plugins/VersionUpgrade/VersionUpgrade25to26/__init__.py
@@ -1,11 +1,16 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Any, Dict, TYPE_CHECKING
+
from . import VersionUpgrade25to26
+if TYPE_CHECKING:
+ from UM.Application import Application
+
upgrade = VersionUpgrade25to26.VersionUpgrade25to26()
-def getMetaData():
+def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
@@ -41,5 +46,5 @@ def getMetaData():
}
}
-def register(app):
+def register(app: "Application") -> Dict[str, Any]:
return { "version_upgrade": upgrade }
diff --git a/plugins/VersionUpgrade/VersionUpgrade25to26/plugin.json b/plugins/VersionUpgrade/VersionUpgrade25to26/plugin.json
index 3029539887..e1f0a47685 100644
--- a/plugins/VersionUpgrade/VersionUpgrade25to26/plugin.json
+++ b/plugins/VersionUpgrade/VersionUpgrade25to26/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Version Upgrade 2.5 to 2.6",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.5 to Cura 2.6.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/VersionUpgrade/VersionUpgrade26to27/VersionUpgrade26to27.py b/plugins/VersionUpgrade/VersionUpgrade26to27/VersionUpgrade26to27.py
index dfa436e5bd..39e3dea4ed 100644
--- a/plugins/VersionUpgrade/VersionUpgrade26to27/VersionUpgrade26to27.py
+++ b/plugins/VersionUpgrade/VersionUpgrade26to27/VersionUpgrade26to27.py
@@ -3,6 +3,7 @@
import configparser #To parse the files we need to upgrade and write the new files.
import io #To serialise configparser output to a string.
+from typing import Dict, List, Tuple
from UM.VersionUpgrade import VersionUpgrade
@@ -61,7 +62,7 @@ _renamed_quality_profiles = {
"um3_bb0.8_TPU_Not_Supported_Quality": "um3_bb0.8_TPU_Fast_print",
"um3_bb0.8_TPU_Not_Supported_Superdraft_Quality": "um3_bb0.8_TPU_Superdraft_Print",
-}
+} # type: Dict[str, str]
## A collection of functions that convert the configuration of the user in Cura
# 2.6 to a configuration for Cura 2.7.
@@ -79,19 +80,19 @@ class VersionUpgrade26to27(VersionUpgrade):
# \raises ValueError The format of the version number in the file is
# incorrect.
# \raises KeyError The format of the file is incorrect.
- def getCfgVersion(self, serialised):
+ def getCfgVersion(self, serialised: str) -> int:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
- setting_version = int(parser.get("metadata", "setting_version", fallback = 0))
+ setting_version = int(parser.get("metadata", "setting_version", fallback = "0"))
return format_version * 1000000 + setting_version
## Upgrades a preferences file from version 2.6 to 2.7.
#
# \param serialised The serialised form of a preferences file.
# \param filename The name of the file to upgrade.
- def upgradePreferences(self, serialised, filename):
- parser = configparser.ConfigParser(interpolation=None)
+ def upgradePreferences(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
+ parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
# Update version numbers
@@ -117,8 +118,8 @@ class VersionUpgrade26to27(VersionUpgrade):
#
# \param serialised The serialised form of a container file.
# \param filename The name of the file to upgrade.
- def upgradeOtherContainer(self, serialised, filename):
- parser = configparser.ConfigParser(interpolation=None)
+ def upgradeOtherContainer(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
+ parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
# Update version numbers
@@ -139,7 +140,7 @@ class VersionUpgrade26to27(VersionUpgrade):
#
# \param serialised The serialised form of a container stack.
# \param filename The name of the file to upgrade.
- def upgradeStack(self, serialised, filename):
+ def upgradeStack(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
diff --git a/plugins/VersionUpgrade/VersionUpgrade26to27/__init__.py b/plugins/VersionUpgrade/VersionUpgrade26to27/__init__.py
index 0e26ca8bbf..1952c9ceff 100644
--- a/plugins/VersionUpgrade/VersionUpgrade26to27/__init__.py
+++ b/plugins/VersionUpgrade/VersionUpgrade26to27/__init__.py
@@ -1,11 +1,16 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Any, Dict, TYPE_CHECKING
+
from . import VersionUpgrade26to27
+if TYPE_CHECKING:
+ from UM.Application import Application
+
upgrade = VersionUpgrade26to27.VersionUpgrade26to27()
-def getMetaData():
+def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
@@ -59,5 +64,5 @@ def getMetaData():
}
}
-def register(app):
+def register(app: "Application") -> Dict[str, Any]:
return { "version_upgrade": upgrade }
diff --git a/plugins/VersionUpgrade/VersionUpgrade26to27/plugin.json b/plugins/VersionUpgrade/VersionUpgrade26to27/plugin.json
index 225da67235..6cdbd64cbb 100644
--- a/plugins/VersionUpgrade/VersionUpgrade26to27/plugin.json
+++ b/plugins/VersionUpgrade/VersionUpgrade26to27/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Version Upgrade 2.6 to 2.7",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.6 to Cura 2.7.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/VersionUpgrade/VersionUpgrade27to30/VersionUpgrade27to30.py b/plugins/VersionUpgrade/VersionUpgrade27to30/VersionUpgrade27to30.py
index 5a141f1558..b594c3c6c4 100644
--- a/plugins/VersionUpgrade/VersionUpgrade27to30/VersionUpgrade27to30.py
+++ b/plugins/VersionUpgrade/VersionUpgrade27to30/VersionUpgrade27to30.py
@@ -1,9 +1,10 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser #To parse preference files.
import io #To serialise the preference files afterwards.
import os
+from typing import Dict, List, Tuple
import urllib.parse
import re
@@ -11,7 +12,7 @@ from UM.VersionUpgrade import VersionUpgrade #We're inheriting from this.
_renamed_themes = {
"cura": "cura-light"
-}
+} # type: Dict[str, str]
_renamed_i18n = {
"7s": "en_7S",
"de": "de_DE",
@@ -28,7 +29,7 @@ _renamed_i18n = {
"ptbr": "pt_BR",
"ru": "ru_RU",
"tr": "tr_TR"
-}
+} # type: Dict[str, str]
class VersionUpgrade27to30(VersionUpgrade):
@@ -43,19 +44,19 @@ class VersionUpgrade27to30(VersionUpgrade):
# \raises ValueError The format of the version number in the file is
# incorrect.
# \raises KeyError The format of the file is incorrect.
- def getCfgVersion(self, serialised):
+ def getCfgVersion(self, serialised: str) -> int:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
- setting_version = int(parser.get("metadata", "setting_version", fallback = 0))
+ setting_version = int(parser.get("metadata", "setting_version", fallback = "0"))
return format_version * 1000000 + setting_version
## Upgrades a preferences file from version 2.7 to 3.0.
#
# \param serialised The serialised form of a preferences file.
# \param filename The name of the file to upgrade.
- def upgradePreferences(self, serialised, filename):
- parser = configparser.ConfigParser(interpolation=None)
+ def upgradePreferences(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
+ parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
# Update version numbers
@@ -100,8 +101,8 @@ class VersionUpgrade27to30(VersionUpgrade):
#
# \param serialised The serialised form of the container file.
# \param filename The name of the file to upgrade.
- def upgradeQualityChangesContainer(self, serialised, filename):
- parser = configparser.ConfigParser(interpolation=None)
+ def upgradeQualityChangesContainer(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
+ parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
# Update the skin pre-shrink settings:
@@ -156,8 +157,8 @@ class VersionUpgrade27to30(VersionUpgrade):
#
# \param serialised The serialised form of the container file.
# \param filename The name of the file to upgrade.
- def upgradeOtherContainer(self, serialised, filename):
- parser = configparser.ConfigParser(interpolation=None)
+ def upgradeOtherContainer(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
+ parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
# Update the skin pre-shrink settings:
@@ -185,7 +186,7 @@ class VersionUpgrade27to30(VersionUpgrade):
#
# \param serialised The serialised form of a container stack.
# \param filename The name of the file to upgrade.
- def upgradeStack(self, serialised, filename):
+ def upgradeStack(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation=None)
parser.read_string(serialised)
diff --git a/plugins/VersionUpgrade/VersionUpgrade27to30/__init__.py b/plugins/VersionUpgrade/VersionUpgrade27to30/__init__.py
index 4da7257b1c..bddc71a1e0 100644
--- a/plugins/VersionUpgrade/VersionUpgrade27to30/__init__.py
+++ b/plugins/VersionUpgrade/VersionUpgrade27to30/__init__.py
@@ -1,11 +1,16 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Any, Dict, TYPE_CHECKING
+
from . import VersionUpgrade27to30
+if TYPE_CHECKING:
+ from UM.Application import Application
+
upgrade = VersionUpgrade27to30.VersionUpgrade27to30()
-def getMetaData():
+def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
@@ -51,5 +56,5 @@ def getMetaData():
}
}
-def register(app):
+def register(app: "Application") -> Dict[str, Any]:
return { "version_upgrade": upgrade }
diff --git a/plugins/VersionUpgrade/VersionUpgrade27to30/plugin.json b/plugins/VersionUpgrade/VersionUpgrade27to30/plugin.json
index 9a139851ec..885d741a8c 100644
--- a/plugins/VersionUpgrade/VersionUpgrade27to30/plugin.json
+++ b/plugins/VersionUpgrade/VersionUpgrade27to30/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Version Upgrade 2.7 to 3.0",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Upgrades configurations from Cura 2.7 to Cura 3.0.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py b/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py
index 399eb18b5d..f0b2e939b9 100644
--- a/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py
+++ b/plugins/VersionUpgrade/VersionUpgrade30to31/VersionUpgrade30to31.py
@@ -1,14 +1,15 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import configparser #To parse preference files.
import io #To serialise the preference files afterwards.
+from typing import Dict, List, Set, Tuple
from UM.VersionUpgrade import VersionUpgrade #We're inheriting from this.
# a list of all legacy "Not Supported" quality profiles
-_OLD_NOT_SUPPORTED_PROFILES = [
+_OLD_NOT_SUPPORTED_PROFILES = {
"um2p_pp_0.25_normal",
"um2p_tpu_0.8_normal",
"um3_bb0.4_ABS_Fast_Print",
@@ -42,7 +43,7 @@ _OLD_NOT_SUPPORTED_PROFILES = [
"um3_bb0.8_PP_Superdraft_Print",
"um3_bb0.8_TPU_Fast_print",
"um3_bb0.8_TPU_Superdraft_Print",
-]
+} # type: Set[str]
# Some containers have their specific empty containers, those need to be set correctly.
@@ -51,13 +52,13 @@ _EMPTY_CONTAINER_DICT = {
"2": "empty_quality",
"3": "empty_material",
"4": "empty_variant",
-}
+} # type: Dict[str, str]
# Renamed definition files
_RENAMED_DEFINITION_DICT = {
"jellybox": "imade3d_jellybox",
-}
+} # type: Dict[str, str]
class VersionUpgrade30to31(VersionUpgrade):
@@ -72,18 +73,18 @@ class VersionUpgrade30to31(VersionUpgrade):
# \raises ValueError The format of the version number in the file is
# incorrect.
# \raises KeyError The format of the file is incorrect.
- def getCfgVersion(self, serialised):
+ def getCfgVersion(self, serialised: str) -> int:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
- setting_version = int(parser.get("metadata", "setting_version", fallback = 0))
+ setting_version = int(parser.get("metadata", "setting_version", fallback = "0"))
return format_version * 1000000 + setting_version
## Upgrades a preferences file from version 3.0 to 3.1.
#
# \param serialised The serialised form of a preferences file.
# \param filename The name of the file to upgrade.
- def upgradePreferences(self, serialised, filename):
+ def upgradePreferences(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
@@ -104,7 +105,7 @@ class VersionUpgrade30to31(VersionUpgrade):
#
# \param serialised The serialised form of the container file.
# \param filename The name of the file to upgrade.
- def upgradeInstanceContainer(self, serialised, filename):
+ def upgradeInstanceContainer(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
@@ -129,7 +130,7 @@ class VersionUpgrade30to31(VersionUpgrade):
#
# \param serialised The serialised form of a container stack.
# \param filename The name of the file to upgrade.
- def upgradeStack(self, serialised, filename):
+ def upgradeStack(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
diff --git a/plugins/VersionUpgrade/VersionUpgrade30to31/__init__.py b/plugins/VersionUpgrade/VersionUpgrade30to31/__init__.py
index 7b2c213a31..c5cc851d6a 100644
--- a/plugins/VersionUpgrade/VersionUpgrade30to31/__init__.py
+++ b/plugins/VersionUpgrade/VersionUpgrade30to31/__init__.py
@@ -1,11 +1,16 @@
-# Copyright (c) 2017 Ultimaker B.V.
+# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Any, Dict, TYPE_CHECKING
+
from . import VersionUpgrade30to31
+if TYPE_CHECKING:
+ from UM.Application import Application
+
upgrade = VersionUpgrade30to31.VersionUpgrade30to31()
-def getMetaData():
+def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
@@ -55,5 +60,5 @@ def getMetaData():
}
}
-def register(app):
+def register(app: "Application") -> Dict[str, Any]:
return { "version_upgrade": upgrade }
diff --git a/plugins/VersionUpgrade/VersionUpgrade30to31/plugin.json b/plugins/VersionUpgrade/VersionUpgrade30to31/plugin.json
index cf42b3f6cd..d5f22649c1 100644
--- a/plugins/VersionUpgrade/VersionUpgrade30to31/plugin.json
+++ b/plugins/VersionUpgrade/VersionUpgrade30to31/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Version Upgrade 3.0 to 3.1",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Upgrades configurations from Cura 3.0 to Cura 3.1.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/VersionUpgrade/VersionUpgrade32to33/VersionUpgrade32to33.py b/plugins/VersionUpgrade/VersionUpgrade32to33/VersionUpgrade32to33.py
index 18851b82c7..83cb15c864 100644
--- a/plugins/VersionUpgrade/VersionUpgrade32to33/VersionUpgrade32to33.py
+++ b/plugins/VersionUpgrade/VersionUpgrade32to33/VersionUpgrade32to33.py
@@ -3,6 +3,7 @@
import configparser #To parse preference files.
import io #To serialise the preference files afterwards.
+from typing import Dict, List, Tuple
from UM.VersionUpgrade import VersionUpgrade #We're inheriting from this.
@@ -51,22 +52,22 @@ _EXTRUDER_TO_POSITION = {
"ultimaker_original_dual_2nd": 1,
"vertex_k8400_dual_1st": 0,
"vertex_k8400_dual_2nd": 1
-}
+} # type: Dict[str, int]
_RENAMED_QUALITY_PROFILES = {
"low": "fast",
"um2_low": "um2_fast"
-}
+} # type: Dict[str, str]
_RENAMED_QUALITY_TYPES = {
"low": "fast"
-}
+} # type: Dict[str, str]
## Upgrades configurations from the state they were in at version 3.2 to the
# state they should be in at version 3.3.
class VersionUpgrade32to33(VersionUpgrade):
-
temporary_group_name_counter = 1
+
## Gets the version number from a CFG file in Uranium's 3.2 format.
#
# Since the format may change, this is implemented for the 3.2 format only
@@ -78,18 +79,18 @@ class VersionUpgrade32to33(VersionUpgrade):
# \raises ValueError The format of the version number in the file is
# incorrect.
# \raises KeyError The format of the file is incorrect.
- def getCfgVersion(self, serialised):
+ def getCfgVersion(self, serialised: str) -> int:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
- setting_version = int(parser.get("metadata", "setting_version", fallback = 0))
+ setting_version = int(parser.get("metadata", "setting_version", fallback = "0"))
return format_version * 1000000 + setting_version
## Upgrades a preferences file from version 3.2 to 3.3.
#
# \param serialised The serialised form of a preferences file.
# \param filename The name of the file to upgrade.
- def upgradePreferences(self, serialised, filename):
+ def upgradePreferences(self, serialised: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
@@ -117,7 +118,7 @@ class VersionUpgrade32to33(VersionUpgrade):
#
# \param serialised The serialised form of a container stack.
# \param filename The name of the file to upgrade.
- def upgradeStack(self, serialized, filename):
+ def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
@@ -141,7 +142,7 @@ class VersionUpgrade32to33(VersionUpgrade):
## Upgrades non-quality-changes instance containers to have the new version
# number.
- def upgradeInstanceContainer(self, serialized, filename):
+ def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
@@ -153,7 +154,7 @@ class VersionUpgrade32to33(VersionUpgrade):
return [filename], [result.getvalue()]
## Upgrades a quality changes container to the new format.
- def upgradeQualityChanges(self, serialized, filename):
+ def upgradeQualityChanges(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
@@ -182,7 +183,7 @@ class VersionUpgrade32to33(VersionUpgrade):
return [filename], [result.getvalue()]
## Upgrades a variant container to the new format.
- def upgradeVariants(self, serialized, filename):
+ def upgradeVariants(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
diff --git a/plugins/VersionUpgrade/VersionUpgrade32to33/__init__.py b/plugins/VersionUpgrade/VersionUpgrade32to33/__init__.py
index 5073be772d..b55ea5ebaf 100644
--- a/plugins/VersionUpgrade/VersionUpgrade32to33/__init__.py
+++ b/plugins/VersionUpgrade/VersionUpgrade32to33/__init__.py
@@ -1,11 +1,16 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Any, Dict, TYPE_CHECKING
+
from . import VersionUpgrade32to33
+if TYPE_CHECKING:
+ from UM.Application import Application
+
upgrade = VersionUpgrade32to33.VersionUpgrade32to33()
-def getMetaData():
+def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
@@ -42,7 +47,7 @@ def getMetaData():
},
"user": {
"get_version": upgrade.getCfgVersion,
- "location": {"./user"}
+ "location": {"./user", "./materials/*"}
},
"variant": {
"get_version": upgrade.getCfgVersion,
@@ -51,5 +56,5 @@ def getMetaData():
}
}
-def register(app):
+def register(app: "Application") -> Dict[str, Any]:
return { "version_upgrade": upgrade }
\ No newline at end of file
diff --git a/plugins/VersionUpgrade/VersionUpgrade32to33/plugin.json b/plugins/VersionUpgrade/VersionUpgrade32to33/plugin.json
index f9cc968dae..eb489169e0 100644
--- a/plugins/VersionUpgrade/VersionUpgrade32to33/plugin.json
+++ b/plugins/VersionUpgrade/VersionUpgrade32to33/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Version Upgrade 3.2 to 3.3",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Upgrades configurations from Cura 3.2 to Cura 3.3.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/VersionUpgrade/VersionUpgrade33to34/VersionUpgrade33to34.py b/plugins/VersionUpgrade/VersionUpgrade33to34/VersionUpgrade33to34.py
index e2241fd195..704ede02d6 100644
--- a/plugins/VersionUpgrade/VersionUpgrade33to34/VersionUpgrade33to34.py
+++ b/plugins/VersionUpgrade/VersionUpgrade33to34/VersionUpgrade33to34.py
@@ -3,17 +3,17 @@
import configparser #To parse preference files.
import io #To serialise the preference files afterwards.
+from typing import Dict, List, Tuple
from UM.VersionUpgrade import VersionUpgrade #We're inheriting from this.
_renamed_settings = {
"infill_hollow": "infill_support_enabled"
-}
+} # type: Dict[str, str]
## Upgrades configurations from the state they were in at version 3.3 to the
# state they should be in at version 3.4.
class VersionUpgrade33to34(VersionUpgrade):
-
## Gets the version number from a CFG file in Uranium's 3.3 format.
#
# Since the format may change, this is implemented for the 3.3 format only
@@ -25,16 +25,16 @@ class VersionUpgrade33to34(VersionUpgrade):
# \raises ValueError The format of the version number in the file is
# incorrect.
# \raises KeyError The format of the file is incorrect.
- def getCfgVersion(self, serialised):
+ def getCfgVersion(self, serialised: str) -> int:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
- setting_version = int(parser.get("metadata", "setting_version", fallback = 0))
+ setting_version = int(parser.get("metadata", "setting_version", fallback = "0"))
return format_version * 1000000 + setting_version
## Upgrades instance containers to have the new version
# number.
- def upgradeInstanceContainer(self, serialized, filename):
+ def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
diff --git a/plugins/VersionUpgrade/VersionUpgrade33to34/__init__.py b/plugins/VersionUpgrade/VersionUpgrade33to34/__init__.py
index 1130c1e9e2..5fd757f843 100644
--- a/plugins/VersionUpgrade/VersionUpgrade33to34/__init__.py
+++ b/plugins/VersionUpgrade/VersionUpgrade33to34/__init__.py
@@ -1,11 +1,16 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Any, Dict, TYPE_CHECKING
+
from . import VersionUpgrade33to34
+if TYPE_CHECKING:
+ from UM.Application import Application
+
upgrade = VersionUpgrade33to34.VersionUpgrade33to34()
-def getMetaData():
+def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
@@ -35,5 +40,5 @@ def getMetaData():
}
-def register(app):
+def register(app: "Application") -> Dict[str, Any]:
return { "version_upgrade": upgrade }
diff --git a/plugins/VersionUpgrade/VersionUpgrade33to34/plugin.json b/plugins/VersionUpgrade/VersionUpgrade33to34/plugin.json
index f5ba7235d1..9649010643 100644
--- a/plugins/VersionUpgrade/VersionUpgrade33to34/plugin.json
+++ b/plugins/VersionUpgrade/VersionUpgrade33to34/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Version Upgrade 3.3 to 3.4",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Upgrades configurations from Cura 3.3 to Cura 3.4.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/VersionUpgrade/VersionUpgrade34to35/VersionUpgrade34to35.py b/plugins/VersionUpgrade/VersionUpgrade34to35/VersionUpgrade34to35.py
index 9d59133036..8e45d7cf73 100644
--- a/plugins/VersionUpgrade/VersionUpgrade34to35/VersionUpgrade34to35.py
+++ b/plugins/VersionUpgrade/VersionUpgrade34to35/VersionUpgrade34to35.py
@@ -3,13 +3,14 @@
import configparser
import io
+from typing import Dict, List, Set, Tuple
from UM.VersionUpgrade import VersionUpgrade
-deleted_settings = {"prime_tower_wall_thickness", "dual_pre_wipe", "prime_tower_purge_volume"}
+deleted_settings = {"prime_tower_wall_thickness", "dual_pre_wipe", "prime_tower_purge_volume"} # type: Set[str]
-changed_settings = {'retraction_combing': 'noskin'}
-updated_settings = {'retraction_combing': 'infill'}
+changed_settings = {"retraction_combing": "noskin"} # type: Dict[str, str]
+updated_settings = {"retraction_combing": "infill"} # type: Dict[str, str]
_RENAMED_MATERIAL_PROFILES = {
"dsm_arnitel2045_175_cartesio_0.25_mm": "dsm_arnitel2045_175_cartesio_0.25mm_thermoplastic_extruder",
@@ -57,15 +58,14 @@ _RENAMED_MATERIAL_PROFILES = {
"ultimaker_pva_cartesio_0.25_mm": "ultimaker_pva_cartesio_0.25mm_thermoplastic_extruder",
"ultimaker_pva_cartesio_0.4_mm": "ultimaker_pva_cartesio_0.4mm_thermoplastic_extruder",
"ultimaker_pva_cartesio_0.8_mm": "ultimaker_pva_cartesio_0.8mm_thermoplastic_extruder"
-}
+} # type: Dict[str, str]
## Upgrades configurations from the state they were in at version 3.4 to the
# state they should be in at version 3.5.
class VersionUpgrade34to35(VersionUpgrade):
-
- ## Gets the version number from a CFG file in Uranium's 3.3 format.
+ ## Gets the version number from a CFG file in Uranium's 3.4 format.
#
- # Since the format may change, this is implemented for the 3.3 format only
+ # Since the format may change, this is implemented for the 3.4 format only
# and needs to be included in the version upgrade system rather than
# globally in Uranium.
#
@@ -74,15 +74,15 @@ class VersionUpgrade34to35(VersionUpgrade):
# \raises ValueError The format of the version number in the file is
# incorrect.
# \raises KeyError The format of the file is incorrect.
- def getCfgVersion(self, serialised):
+ def getCfgVersion(self, serialised: str) -> int:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialised)
format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
- setting_version = int(parser.get("metadata", "setting_version", fallback = 0))
+ setting_version = int(parser.get("metadata", "setting_version", fallback = "0"))
return format_version * 1000000 + setting_version
## Upgrades Preferences to have the new version number.
- def upgradePreferences(self, serialized, filename):
+ def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
@@ -103,7 +103,7 @@ class VersionUpgrade34to35(VersionUpgrade):
return [filename], [result.getvalue()]
## Upgrades stacks to have the new version number.
- def upgradeStack(self, serialized, filename):
+ def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
@@ -121,7 +121,7 @@ class VersionUpgrade34to35(VersionUpgrade):
## Upgrades instance containers to have the new version
# number.
- def upgradeInstanceContainer(self, serialized, filename):
+ def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
parser = configparser.ConfigParser(interpolation = None)
parser.read_string(serialized)
@@ -147,7 +147,7 @@ class VersionUpgrade34to35(VersionUpgrade):
parser.write(result)
return [filename], [result.getvalue()]
- def _resetConcentric3DInfillPattern(self, parser):
+ def _resetConcentric3DInfillPattern(self, parser: configparser.ConfigParser) -> None:
if "values" not in parser:
return
@@ -161,5 +161,4 @@ class VersionUpgrade34to35(VersionUpgrade):
if key not in parser["values"]:
continue
if parser["values"][key] == "concentric_3d":
- del parser["values"][key]
-
+ del parser["values"][key]
\ No newline at end of file
diff --git a/plugins/VersionUpgrade/VersionUpgrade34to35/__init__.py b/plugins/VersionUpgrade/VersionUpgrade34to35/__init__.py
index 2ea74f6194..332bc827b9 100644
--- a/plugins/VersionUpgrade/VersionUpgrade34to35/__init__.py
+++ b/plugins/VersionUpgrade/VersionUpgrade34to35/__init__.py
@@ -1,11 +1,16 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
+from typing import Any, Dict, TYPE_CHECKING
+
from . import VersionUpgrade34to35
+if TYPE_CHECKING:
+ from UM.Application import Application
+
upgrade = VersionUpgrade34to35.VersionUpgrade34to35()
-def getMetaData():
+def getMetaData() -> Dict[str, Any]:
return {
"version_upgrade": {
# From To Upgrade function
@@ -52,5 +57,5 @@ def getMetaData():
}
-def register(app):
+def register(app: "Application") -> Dict[str, Any]:
return { "version_upgrade": upgrade }
diff --git a/plugins/VersionUpgrade/VersionUpgrade34to35/plugin.json b/plugins/VersionUpgrade/VersionUpgrade34to35/plugin.json
index b73001b683..71b13ee5a9 100644
--- a/plugins/VersionUpgrade/VersionUpgrade34to35/plugin.json
+++ b/plugins/VersionUpgrade/VersionUpgrade34to35/plugin.json
@@ -1,8 +1,8 @@
- {
+{
"name": "Version Upgrade 3.4 to 3.5",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Upgrades configurations from Cura 3.4 to Cura 3.5.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/VersionUpgrade/VersionUpgrade35to40/VersionUpgrade35to40.py b/plugins/VersionUpgrade/VersionUpgrade35to40/VersionUpgrade35to40.py
new file mode 100644
index 0000000000..52cd7cf3cb
--- /dev/null
+++ b/plugins/VersionUpgrade/VersionUpgrade35to40/VersionUpgrade35to40.py
@@ -0,0 +1,68 @@
+import configparser
+from typing import Tuple, List, Set
+import io
+from UM.VersionUpgrade import VersionUpgrade
+from cura.PrinterOutputDevice import ConnectionType
+deleted_settings = {"bridge_wall_max_overhang"} # type: Set[str]
+
+
+class VersionUpgrade35to40(VersionUpgrade):
+ # Upgrades stacks to have the new version number.
+ def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
+ parser = configparser.ConfigParser(interpolation=None)
+ parser.read_string(serialized)
+
+ # Update version number.
+ parser["general"]["version"] = "4"
+ parser["metadata"]["setting_version"] = "6"
+
+ if parser["metadata"].get("um_network_key") is not None or parser["metadata"].get("octoprint_api_key") is not None:
+ # Set the connection type if um_network_key or the octoprint key is set.
+ parser["metadata"]["connection_type"] = str(ConnectionType.NetworkConnection.value)
+
+ result = io.StringIO()
+ parser.write(result)
+ return [filename], [result.getvalue()]
+ pass
+
+ def getCfgVersion(self, serialised: str) -> int:
+ parser = configparser.ConfigParser(interpolation = None)
+ parser.read_string(serialised)
+ format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
+ setting_version = int(parser.get("metadata", "setting_version", fallback = "0"))
+ return format_version * 1000000 + setting_version
+
+ ## Upgrades Preferences to have the new version number.
+ def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
+ parser = configparser.ConfigParser(interpolation=None)
+ parser.read_string(serialized)
+
+ if "metadata" not in parser:
+ parser["metadata"] = {}
+ parser["general"]["version"] = "6"
+ parser["metadata"]["setting_version"] = "6"
+
+ result = io.StringIO()
+ parser.write(result)
+ return [filename], [result.getvalue()]
+
+ ## Upgrades instance containers to have the new version
+ # number.
+ def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
+ parser = configparser.ConfigParser(interpolation=None)
+ parser.read_string(serialized)
+
+ # Update version number.
+ parser["general"]["version"] = "4"
+ parser["metadata"]["setting_version"] = "6"
+
+ #self._resetConcentric3DInfillPattern(parser)
+ if "values" in parser:
+ for deleted_setting in deleted_settings:
+ if deleted_setting not in parser["values"]:
+ continue
+ del parser["values"][deleted_setting]
+
+ result = io.StringIO()
+ parser.write(result)
+ return [filename], [result.getvalue()]
\ No newline at end of file
diff --git a/plugins/VersionUpgrade/VersionUpgrade35to40/__init__.py b/plugins/VersionUpgrade/VersionUpgrade35to40/__init__.py
new file mode 100644
index 0000000000..2ad1dddbf2
--- /dev/null
+++ b/plugins/VersionUpgrade/VersionUpgrade35to40/__init__.py
@@ -0,0 +1,56 @@
+from typing import Dict, Any
+
+from . import VersionUpgrade35to40
+
+upgrade = VersionUpgrade35to40.VersionUpgrade35to40()
+
+
+def getMetaData() -> Dict[str, Any]:
+ return {
+ "version_upgrade": {
+ # From To Upgrade function
+ ("preferences", 6000005): ("preferences", 6000006, upgrade.upgradePreferences),
+
+ ("definition_changes", 4000005): ("definition_changes", 4000006, upgrade.upgradeInstanceContainer),
+ ("quality_changes", 4000005): ("quality_changes", 4000006, upgrade.upgradeInstanceContainer),
+ ("quality", 4000005): ("quality", 4000006, upgrade.upgradeInstanceContainer),
+ ("user", 4000005): ("user", 4000006, upgrade.upgradeInstanceContainer),
+
+ ("machine_stack", 4000005): ("machine_stack", 4000006, upgrade.upgradeStack),
+ ("extruder_train", 4000005): ("extruder_train", 4000006, upgrade.upgradeStack),
+ },
+ "sources": {
+ "preferences": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"."}
+ },
+ "machine_stack": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"./machine_instances"}
+ },
+ "extruder_train": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"./extruders"}
+ },
+ "definition_changes": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"./definition_changes"}
+ },
+ "quality_changes": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"./quality_changes"}
+ },
+ "quality": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"./quality"}
+ },
+ "user": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"./user"}
+ }
+ }
+ }
+
+
+def register(app) -> Dict[str, Any]:
+ return {"version_upgrade": upgrade}
\ No newline at end of file
diff --git a/plugins/VersionUpgrade/VersionUpgrade35to40/plugin.json b/plugins/VersionUpgrade/VersionUpgrade35to40/plugin.json
new file mode 100644
index 0000000000..578594fb6d
--- /dev/null
+++ b/plugins/VersionUpgrade/VersionUpgrade35to40/plugin.json
@@ -0,0 +1,8 @@
+ {
+ "name": "Version Upgrade 3.5 to 4.0",
+ "author": "Ultimaker B.V.",
+ "version": "1.0.0",
+ "description": "Upgrades configurations from Cura 3.5 to Cura 4.0.",
+ "api": "6.0",
+ "i18n-catalog": "cura"
+}
diff --git a/plugins/VersionUpgrade/VersionUpgrade40to41/VersionUpgrade40to41.py b/plugins/VersionUpgrade/VersionUpgrade40to41/VersionUpgrade40to41.py
new file mode 100644
index 0000000000..ac54b7c8e0
--- /dev/null
+++ b/plugins/VersionUpgrade/VersionUpgrade40to41/VersionUpgrade40to41.py
@@ -0,0 +1,86 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+import configparser
+import io
+from typing import Dict, List, Tuple
+
+from UM.VersionUpgrade import VersionUpgrade
+
+_renamed_quality_profiles = {
+ "gmax15plus_pla_dual_normal": "gmax15plus_global_dual_normal",
+ "gmax15plus_pla_dual_thick": "gmax15plus_global_dual_thick",
+ "gmax15plus_pla_dual_thin": "gmax15plus_global_dual_thin",
+ "gmax15plus_pla_dual_very_thick": "gmax15plus_global_dual_very_thick",
+ "gmax15plus_pla_normal": "gmax15plus_global_normal",
+ "gmax15plus_pla_thick": "gmax15plus_global_thick",
+ "gmax15plus_pla_thin": "gmax15plus_global_thin",
+ "gmax15plus_pla_very_thick": "gmax15plus_global_very_thick"
+} # type: Dict[str, str]
+
+## Upgrades configurations from the state they were in at version 4.0 to the
+# state they should be in at version 4.1.
+class VersionUpgrade40to41(VersionUpgrade):
+ ## Gets the version number from a CFG file in Uranium's 4.0 format.
+ #
+ # Since the format may change, this is implemented for the 4.0 format only
+ # and needs to be included in the version upgrade system rather than
+ # globally in Uranium.
+ #
+ # \param serialised The serialised form of a CFG file.
+ # \return The version number stored in the CFG file.
+ # \raises ValueError The format of the version number in the file is
+ # incorrect.
+ # \raises KeyError The format of the file is incorrect.
+ def getCfgVersion(self, serialised: str) -> int:
+ parser = configparser.ConfigParser(interpolation = None)
+ parser.read_string(serialised)
+ format_version = int(parser.get("general", "version")) #Explicitly give an exception when this fails. That means that the file format is not recognised.
+ setting_version = int(parser.get("metadata", "setting_version", fallback = "0"))
+ return format_version * 1000000 + setting_version
+
+ ## Upgrades instance containers to have the new version
+ # number.
+ def upgradeInstanceContainer(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
+ parser = configparser.ConfigParser(interpolation = None)
+ parser.read_string(serialized)
+
+ # Update version number.
+ parser["general"]["version"] = "4"
+ parser["metadata"]["setting_version"] = "6"
+
+ result = io.StringIO()
+ parser.write(result)
+ return [filename], [result.getvalue()]
+
+ ## Upgrades Preferences to have the new version number.
+ def upgradePreferences(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
+ parser = configparser.ConfigParser(interpolation = None)
+ parser.read_string(serialized)
+
+ # Update version number.
+ parser["general"]["version"] = "6"
+ if "metadata" not in parser:
+ parser["metadata"] = {}
+ parser["metadata"]["setting_version"] = "6"
+
+ result = io.StringIO()
+ parser.write(result)
+ return [filename], [result.getvalue()]
+
+ ## Upgrades stacks to have the new version number.
+ def upgradeStack(self, serialized: str, filename: str) -> Tuple[List[str], List[str]]:
+ parser = configparser.ConfigParser(interpolation = None)
+ parser.read_string(serialized)
+
+ # Update version number.
+ parser["general"]["version"] = "4"
+ parser["metadata"]["setting_version"] = "6"
+
+ #Update the name of the quality profile.
+ if parser["containers"]["4"] in _renamed_quality_profiles:
+ parser["containers"]["4"] = _renamed_quality_profiles[parser["containers"]["4"]]
+
+ result = io.StringIO()
+ parser.write(result)
+ return [filename], [result.getvalue()]
\ No newline at end of file
diff --git a/plugins/VersionUpgrade/VersionUpgrade40to41/__init__.py b/plugins/VersionUpgrade/VersionUpgrade40to41/__init__.py
new file mode 100644
index 0000000000..757a7a51c0
--- /dev/null
+++ b/plugins/VersionUpgrade/VersionUpgrade40to41/__init__.py
@@ -0,0 +1,39 @@
+# Copyright (c) 2018 Ultimaker B.V.
+# Cura is released under the terms of the LGPLv3 or higher.
+
+from typing import Any, Dict, TYPE_CHECKING
+
+from . import VersionUpgrade40to41
+
+if TYPE_CHECKING:
+ from UM.Application import Application
+
+upgrade = VersionUpgrade40to41.VersionUpgrade40to41()
+
+def getMetaData() -> Dict[str, Any]:
+ return {
+ "version_upgrade": {
+ # From To Upgrade function
+ ("machine_stack", 4000005): ("machine_stack", 4000006, upgrade.upgradeStack),
+ ("extruder_train", 4000005): ("extruder_train", 4000006, upgrade.upgradeStack),
+ ("preferences", 6000005): ("preferences", 6000006, upgrade.upgradePreferences),
+ ("definition_changes", 4000005): ("definition_changes", 4000006, upgrade.upgradeInstanceContainer),
+ ("quality_changes", 4000005): ("quality_changes", 4000006, upgrade.upgradeInstanceContainer),
+ ("quality", 4000005): ("quality", 4000006, upgrade.upgradeInstanceContainer),
+ ("user", 4000005): ("user", 4000006, upgrade.upgradeInstanceContainer),
+ },
+ "sources": {
+ "machine_stack": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"./machine_instances"}
+ },
+ "extruder_train": {
+ "get_version": upgrade.getCfgVersion,
+ "location": {"./extruders"}
+ }
+ }
+ }
+
+
+def register(app: "Application") -> Dict[str, Any]:
+ return { "version_upgrade": upgrade }
diff --git a/plugins/VersionUpgrade/VersionUpgrade40to41/plugin.json b/plugins/VersionUpgrade/VersionUpgrade40to41/plugin.json
new file mode 100644
index 0000000000..b1c6d75669
--- /dev/null
+++ b/plugins/VersionUpgrade/VersionUpgrade40to41/plugin.json
@@ -0,0 +1,8 @@
+{
+ "name": "Version Upgrade 4.0 to 4.1",
+ "author": "Ultimaker B.V.",
+ "version": "1.0.1",
+ "description": "Upgrades configurations from Cura 4.0 to Cura 4.1.",
+ "api": "6.0",
+ "i18n-catalog": "cura"
+}
diff --git a/plugins/X3DReader/plugin.json b/plugins/X3DReader/plugin.json
index 9ee09e43df..1fc14104ed 100644
--- a/plugins/X3DReader/plugin.json
+++ b/plugins/X3DReader/plugin.json
@@ -1,8 +1,8 @@
{
"name": "X3D Reader",
"author": "Seva Alekseyev",
- "version": "0.5.0",
+ "version": "1.0.1",
"description": "Provides support for reading X3D files.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/XRayView/plugin.json b/plugins/XRayView/plugin.json
index 576dec4656..71cc165b6c 100644
--- a/plugins/XRayView/plugin.json
+++ b/plugins/XRayView/plugin.json
@@ -1,8 +1,8 @@
{
"name": "X-Ray View",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides the X-Ray view.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/plugins/XmlMaterialProfile/__init__.py b/plugins/XmlMaterialProfile/__init__.py
index e8bde78424..c50df69516 100644
--- a/plugins/XmlMaterialProfile/__init__.py
+++ b/plugins/XmlMaterialProfile/__init__.py
@@ -16,7 +16,7 @@ def getMetaData():
"mimetype": "application/x-ultimaker-material-profile"
},
"version_upgrade": {
- ("materials", 1000000): ("materials", 1000004, upgrader.upgradeMaterial),
+ ("materials", 1000000): ("materials", 1000006, upgrader.upgradeMaterial),
},
"sources": {
"materials": {
diff --git a/plugins/XmlMaterialProfile/plugin.json b/plugins/XmlMaterialProfile/plugin.json
index 4b2901c375..bb1db82fa4 100644
--- a/plugins/XmlMaterialProfile/plugin.json
+++ b/plugins/XmlMaterialProfile/plugin.json
@@ -1,8 +1,8 @@
{
"name": "Material Profiles",
"author": "Ultimaker B.V.",
- "version": "1.0.0",
+ "version": "1.0.1",
"description": "Provides capabilities to read and write XML-based material profiles.",
- "api": 5,
+ "api": "6.0",
"i18n-catalog": "cura"
}
diff --git a/resources/bundled_packages/cura.json b/resources/bundled_packages/cura.json
index ee82b17a75..21da1d9fdb 100644
--- a/resources/bundled_packages/cura.json
+++ b/resources/bundled_packages/cura.json
@@ -5,8 +5,8 @@
"package_type": "plugin",
"display_name": "3MF Reader",
"description": "Provides support for reading 3MF files.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -22,8 +22,8 @@
"package_type": "plugin",
"display_name": "3MF Writer",
"description": "Provides support for writing 3MF files.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -39,8 +39,25 @@
"package_type": "plugin",
"display_name": "Change Log",
"description": "Shows changes since latest checked version.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
+ "website": "https://ultimaker.com",
+ "author": {
+ "author_id": "UltimakerPackages",
+ "display_name": "Ultimaker B.V.",
+ "email": "plugins@ultimaker.com",
+ "website": "https://ultimaker.com"
+ }
+ }
+ },
+ "CuraDrive": {
+ "package_info": {
+ "package_id": "CuraDrive",
+ "package_type": "plugin",
+ "display_name": "Cura Backups",
+ "description": "Backup and restore your configuration.",
+ "package_version": "1.2.0",
+ "sdk_version": 6,
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -56,8 +73,8 @@
"package_type": "plugin",
"display_name": "CuraEngine Backend",
"description": "Provides the link to the CuraEngine slicing backend.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -73,8 +90,8 @@
"package_type": "plugin",
"display_name": "Cura Profile Reader",
"description": "Provides support for importing Cura profiles.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -90,8 +107,8 @@
"package_type": "plugin",
"display_name": "Cura Profile Writer",
"description": "Provides support for exporting Cura profiles.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -107,8 +124,8 @@
"package_type": "plugin",
"display_name": "Firmware Update Checker",
"description": "Checks for firmware updates.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -124,8 +141,8 @@
"package_type": "plugin",
"display_name": "Firmware Updater",
"description": "Provides a machine actions for updating firmware.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -141,8 +158,8 @@
"package_type": "plugin",
"display_name": "Compressed G-code Reader",
"description": "Reads g-code from a compressed archive.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -158,8 +175,8 @@
"package_type": "plugin",
"display_name": "Compressed G-code Writer",
"description": "Writes g-code to a compressed archive.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -175,8 +192,8 @@
"package_type": "plugin",
"display_name": "G-Code Profile Reader",
"description": "Provides support for importing profiles from g-code files.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -192,8 +209,8 @@
"package_type": "plugin",
"display_name": "G-Code Reader",
"description": "Allows loading and displaying G-code files.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "VictorLarchenko",
@@ -209,8 +226,8 @@
"package_type": "plugin",
"display_name": "G-Code Writer",
"description": "Writes g-code to a file.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -226,8 +243,8 @@
"package_type": "plugin",
"display_name": "Image Reader",
"description": "Enables ability to generate printable geometry from 2D image files.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -243,8 +260,8 @@
"package_type": "plugin",
"display_name": "Legacy Cura Profile Reader",
"description": "Provides support for importing profiles from legacy Cura versions.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -260,8 +277,8 @@
"package_type": "plugin",
"display_name": "Machine Settings Action",
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "fieldOfView",
@@ -277,8 +294,8 @@
"package_type": "plugin",
"display_name": "Model Checker",
"description": "Checks models and print configuration for possible printing issues and give suggestions.",
- "package_version": "0.1.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -294,8 +311,8 @@
"package_type": "plugin",
"display_name": "Monitor Stage",
"description": "Provides a monitor stage in Cura.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -311,8 +328,8 @@
"package_type": "plugin",
"display_name": "Per-Object Settings Tool",
"description": "Provides the per-model settings.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -328,8 +345,8 @@
"package_type": "plugin",
"display_name": "Post Processing",
"description": "Extension that allows for user created scripts for post processing.",
- "package_version": "2.2.0",
- "sdk_version": 5,
+ "package_version": "2.2.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -345,8 +362,25 @@
"package_type": "plugin",
"display_name": "Prepare Stage",
"description": "Provides a prepare stage in Cura.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
+ "website": "https://ultimaker.com",
+ "author": {
+ "author_id": "UltimakerPackages",
+ "display_name": "Ultimaker B.V.",
+ "email": "plugins@ultimaker.com",
+ "website": "https://ultimaker.com"
+ }
+ }
+ },
+ "PreviewStage": {
+ "package_info": {
+ "package_id": "PreviewStage",
+ "package_type": "plugin",
+ "display_name": "Preview Stage",
+ "description": "Provides a preview stage in Cura.",
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -362,8 +396,8 @@
"package_type": "plugin",
"display_name": "Removable Drive Output Device",
"description": "Provides removable drive hotplugging and writing support.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -379,8 +413,8 @@
"package_type": "plugin",
"display_name": "Simulation View",
"description": "Provides the Simulation view.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -396,8 +430,8 @@
"package_type": "plugin",
"display_name": "Slice Info",
"description": "Submits anonymous slice info. Can be disabled through preferences.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -413,8 +447,8 @@
"package_type": "plugin",
"display_name": "Solid View",
"description": "Provides a normal solid mesh view.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -430,8 +464,8 @@
"package_type": "plugin",
"display_name": "Support Eraser Tool",
"description": "Creates an eraser mesh to block the printing of support in certain places.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -447,8 +481,8 @@
"package_type": "plugin",
"display_name": "Toolbox",
"description": "Find, manage and install new Cura packages.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -464,8 +498,8 @@
"package_type": "plugin",
"display_name": "UFP Writer",
"description": "Provides support for writing Ultimaker Format Packages.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -481,8 +515,8 @@
"package_type": "plugin",
"display_name": "Ultimaker Machine Actions",
"description": "Provides machine actions for Ultimaker machines (such as bed leveling wizard, selecting upgrades, etc.).",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -498,8 +532,8 @@
"package_type": "plugin",
"display_name": "UM3 Network Printing",
"description": "Manages network connections to Ultimaker 3 printers.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -515,8 +549,8 @@
"package_type": "plugin",
"display_name": "USB Printing",
"description": "Accepts G-Code and sends them to a printer. Plugin can also update firmware.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.2",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -532,8 +566,8 @@
"package_type": "plugin",
"display_name": "User Agreement",
"description": "Ask the user once if he/she agrees with our license.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -549,8 +583,8 @@
"package_type": "plugin",
"display_name": "Version Upgrade 2.1 to 2.2",
"description": "Upgrades configurations from Cura 2.1 to Cura 2.2.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -566,8 +600,8 @@
"package_type": "plugin",
"display_name": "Version Upgrade 2.2 to 2.4",
"description": "Upgrades configurations from Cura 2.2 to Cura 2.4.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -583,8 +617,8 @@
"package_type": "plugin",
"display_name": "Version Upgrade 2.5 to 2.6",
"description": "Upgrades configurations from Cura 2.5 to Cura 2.6.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -600,8 +634,8 @@
"package_type": "plugin",
"display_name": "Version Upgrade 2.6 to 2.7",
"description": "Upgrades configurations from Cura 2.6 to Cura 2.7.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -617,8 +651,8 @@
"package_type": "plugin",
"display_name": "Version Upgrade 2.7 to 3.0",
"description": "Upgrades configurations from Cura 2.7 to Cura 3.0.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -634,8 +668,8 @@
"package_type": "plugin",
"display_name": "Version Upgrade 3.0 to 3.1",
"description": "Upgrades configurations from Cura 3.0 to Cura 3.1.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -651,8 +685,8 @@
"package_type": "plugin",
"display_name": "Version Upgrade 3.2 to 3.3",
"description": "Upgrades configurations from Cura 3.2 to Cura 3.3.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -668,8 +702,8 @@
"package_type": "plugin",
"display_name": "Version Upgrade 3.3 to 3.4",
"description": "Upgrades configurations from Cura 3.3 to Cura 3.4.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -685,8 +719,42 @@
"package_type": "plugin",
"display_name": "Version Upgrade 3.4 to 3.5",
"description": "Upgrades configurations from Cura 3.4 to Cura 3.5.",
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
+ "website": "https://ultimaker.com",
+ "author": {
+ "author_id": "UltimakerPackages",
+ "display_name": "Ultimaker B.V.",
+ "email": "plugins@ultimaker.com",
+ "website": "https://ultimaker.com"
+ }
+ }
+ },
+ "VersionUpgrade35to40": {
+ "package_info": {
+ "package_id": "VersionUpgrade35to40",
+ "package_type": "plugin",
+ "display_name": "Version Upgrade 3.5 to 4.0",
+ "description": "Upgrades configurations from Cura 3.5 to Cura 4.0.",
"package_version": "1.0.0",
- "sdk_version": 5,
+ "sdk_version": "6.0",
+ "website": "https://ultimaker.com",
+ "author": {
+ "author_id": "UltimakerPackages",
+ "display_name": "Ultimaker B.V.",
+ "email": "plugins@ultimaker.com",
+ "website": "https://ultimaker.com"
+ }
+ }
+ },
+ "VersionUpgrade40to41": {
+ "package_info": {
+ "package_id": "VersionUpgrade40to41",
+ "package_type": "plugin",
+ "display_name": "Version Upgrade 4.0 to 4.1",
+ "description": "Upgrades configurations from Cura 4.0 to Cura 4.1.",
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -702,8 +770,8 @@
"package_type": "plugin",
"display_name": "X3D Reader",
"description": "Provides support for reading X3D files.",
- "package_version": "0.5.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "SevaAlekseyev",
@@ -719,8 +787,8 @@
"package_type": "plugin",
"display_name": "XML Material Profiles",
"description": "Provides capabilities to read and write XML-based material profiles.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -736,8 +804,8 @@
"package_type": "plugin",
"display_name": "X-Ray View",
"description": "Provides the X-Ray view.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com",
"author": {
"author_id": "UltimakerPackages",
@@ -753,8 +821,8 @@
"package_type": "material",
"display_name": "Generic ABS",
"description": "The generic ABS profile which other profiles can be based upon.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -771,8 +839,8 @@
"package_type": "material",
"display_name": "Generic BAM",
"description": "The generic BAM profile which other profiles can be based upon.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -789,8 +857,8 @@
"package_type": "material",
"display_name": "Generic CFF CPE",
"description": "The generic CFF CPE profile which other profiles can be based upon.",
- "package_version": "1.1.0",
- "sdk_version": 5,
+ "package_version": "1.1.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -807,8 +875,8 @@
"package_type": "material",
"display_name": "Generic CFF PA",
"description": "The generic CFF PA profile which other profiles can be based upon.",
- "package_version": "1.1.0",
- "sdk_version": 5,
+ "package_version": "1.1.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -825,8 +893,8 @@
"package_type": "material",
"display_name": "Generic CPE",
"description": "The generic CPE profile which other profiles can be based upon.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -843,8 +911,8 @@
"package_type": "material",
"display_name": "Generic CPE+",
"description": "The generic CPE+ profile which other profiles can be based upon.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -861,8 +929,8 @@
"package_type": "material",
"display_name": "Generic GFF CPE",
"description": "The generic GFF CPE profile which other profiles can be based upon.",
- "package_version": "1.1.0",
- "sdk_version": 5,
+ "package_version": "1.1.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -879,8 +947,8 @@
"package_type": "material",
"display_name": "Generic GFF PA",
"description": "The generic GFF PA profile which other profiles can be based upon.",
- "package_version": "1.1.0",
- "sdk_version": 5,
+ "package_version": "1.1.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -897,8 +965,8 @@
"package_type": "material",
"display_name": "Generic HIPS",
"description": "The generic HIPS profile which other profiles can be based upon.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -915,8 +983,8 @@
"package_type": "material",
"display_name": "Generic Nylon",
"description": "The generic Nylon profile which other profiles can be based upon.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -933,8 +1001,8 @@
"package_type": "material",
"display_name": "Generic PC",
"description": "The generic PC profile which other profiles can be based upon.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -951,8 +1019,8 @@
"package_type": "material",
"display_name": "Generic PETG",
"description": "The generic PETG profile which other profiles can be based upon.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -969,8 +1037,8 @@
"package_type": "material",
"display_name": "Generic PLA",
"description": "The generic PLA profile which other profiles can be based upon.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -987,8 +1055,8 @@
"package_type": "material",
"display_name": "Generic PP",
"description": "The generic PP profile which other profiles can be based upon.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -1005,8 +1073,8 @@
"package_type": "material",
"display_name": "Generic PVA",
"description": "The generic PVA profile which other profiles can be based upon.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -1023,8 +1091,8 @@
"package_type": "material",
"display_name": "Generic Tough PLA",
"description": "The generic Tough PLA profile which other profiles can be based upon.",
- "package_version": "1.0.1",
- "sdk_version": 5,
+ "package_version": "1.0.2",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -1041,8 +1109,8 @@
"package_type": "material",
"display_name": "Generic TPU",
"description": "The generic TPU profile which other profiles can be based upon.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://github.com/Ultimaker/fdm_materials",
"author": {
"author_id": "Generic",
@@ -1059,8 +1127,8 @@
"package_type": "material",
"display_name": "Dagoma Chromatik PLA",
"description": "Filament testé et approuvé pour les imprimantes 3D Dagoma. Chromatik est l'idéal pour débuter et suivre les tutoriels premiers pas. Il vous offre qualité et résistance pour chacune de vos impressions.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://dagoma.fr/boutique/filaments.html",
"author": {
"author_id": "Dagoma",
@@ -1076,8 +1144,8 @@
"package_type": "material",
"display_name": "FABtotum ABS",
"description": "This material is easy to be extruded but it is not the simplest to use. It is one of the most used in 3D printing to get very well finished objects. It is not sustainable and its smoke can be dangerous if inhaled. The reason to prefer this filament to PLA is mainly because of its precision and mechanical specs. ABS (for plastic) stands for Acrylonitrile Butadiene Styrene and it is a thermoplastic which is widely used in everyday objects. It can be printed with any FFF 3D printer which can get to high temperatures as it must be extruded in a range between 220° and 245°, so it’s compatible with all versions of the FABtotum Personal fabricator.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=40",
"author": {
"author_id": "FABtotum",
@@ -1093,8 +1161,8 @@
"package_type": "material",
"display_name": "FABtotum Nylon",
"description": "When 3D printing started this material was not listed among the extrudable filaments. It is flexible as well as resistant to tractions. It is well known for its uses in textile but also in industries which require a strong and flexible material. There are different kinds of Nylon: 3D printing mostly uses Nylon 6 and Nylon 6.6, which are the most common. It requires higher temperatures to be printed, so a 3D printer must be able to reach them (around 240°C): the FABtotum, of course, can.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=53",
"author": {
"author_id": "FABtotum",
@@ -1110,8 +1178,8 @@
"package_type": "material",
"display_name": "FABtotum PLA",
"description": "It is the most common filament used for 3D printing. It is studied to be bio-degradable as it comes from corn starch’s sugar mainly. It is completely made of renewable sources and has no footprint on polluting. PLA stands for PolyLactic Acid and it is a thermoplastic that today is still considered the easiest material to be 3D printed. It can be extruded at lower temperatures: the standard range of FABtotum’s one is between 185° and 195°.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=39",
"author": {
"author_id": "FABtotum",
@@ -1127,8 +1195,8 @@
"package_type": "material",
"display_name": "FABtotum TPU Shore 98A",
"description": "",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://store.fabtotum.com/eu/products/filaments.html?filament_type=66",
"author": {
"author_id": "FABtotum",
@@ -1144,8 +1212,8 @@
"package_type": "material",
"display_name": "Fiberlogy HD PLA",
"description": "With our HD PLA you have many more options. You can use this material in two ways. Choose the one you like best. You can use it as a normal PLA and get prints characterized by a very good adhesion between the layers and high precision. You can also make your prints acquire similar properties to that of ABS – better impact resistance and high temperature resistance. All you need is an oven. Yes, an oven! By annealing our HD PLA in an oven, in accordance with the manual, you will avoid all the inconveniences of printing with ABS, such as unpleasant odour or hazardous fumes.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "http://fiberlogy.com/en/fiberlogy-filaments/filament-hd-pla/",
"author": {
"author_id": "Fiberlogy",
@@ -1161,8 +1229,8 @@
"package_type": "material",
"display_name": "Filo3D PLA",
"description": "Fast, safe and reliable printing. PLA is ideal for the fast and reliable printing of parts and prototypes with a great surface quality.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://dagoma.fr",
"author": {
"author_id": "Dagoma",
@@ -1178,8 +1246,8 @@
"package_type": "material",
"display_name": "IMADE3D JellyBOX PETG",
"description": "",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "http://shop.imade3d.com/filament.html",
"author": {
"author_id": "IMADE3D",
@@ -1195,8 +1263,8 @@
"package_type": "material",
"display_name": "IMADE3D JellyBOX PLA",
"description": "",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "http://shop.imade3d.com/filament.html",
"author": {
"author_id": "IMADE3D",
@@ -1212,8 +1280,8 @@
"package_type": "material",
"display_name": "Octofiber PLA",
"description": "PLA material from Octofiber.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://nl.octofiber.com/3d-printing-filament/pla.html",
"author": {
"author_id": "Octofiber",
@@ -1229,8 +1297,8 @@
"package_type": "material",
"display_name": "PolyFlex™ PLA",
"description": "PolyFlex™ is a highly flexible yet easy to print 3D printing material. Featuring good elasticity and a large strain-to- failure, PolyFlex™ opens up a completely new realm of applications.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "http://www.polymaker.com/shop/polyflex/",
"author": {
"author_id": "Polymaker",
@@ -1246,8 +1314,8 @@
"package_type": "material",
"display_name": "PolyMax™ PLA",
"description": "PolyMax™ PLA is a 3D printing material with excellent mechanical properties and printing quality. PolyMax™ PLA has an impact resistance of up to nine times that of regular PLA, and better overall mechanical properties than ABS.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "http://www.polymaker.com/shop/polymax/",
"author": {
"author_id": "Polymaker",
@@ -1263,8 +1331,8 @@
"package_type": "material",
"display_name": "PolyPlus™ PLA True Colour",
"description": "PolyPlus™ PLA is a premium PLA designed for all desktop FDM/FFF 3D printers. It is produced with our patented Jam-Free™ technology that ensures consistent extrusion and prevents jams.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "http://www.polymaker.com/shop/polyplus-true-colour/",
"author": {
"author_id": "Polymaker",
@@ -1280,8 +1348,8 @@
"package_type": "material",
"display_name": "PolyWood™ PLA",
"description": "PolyWood™ is a wood mimic printing material that contains no actual wood ensuring a clean Jam-Free™ printing experience.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "http://www.polymaker.com/shop/polywood/",
"author": {
"author_id": "Polymaker",
@@ -1297,8 +1365,8 @@
"package_type": "material",
"display_name": "Ultimaker ABS",
"description": "Example package for material and quality profiles for Ultimaker materials.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com/products/materials/abs",
"author": {
"author_id": "UltimakerPackages",
@@ -1316,8 +1384,8 @@
"package_type": "material",
"display_name": "Ultimaker Breakaway",
"description": "Example package for material and quality profiles for Ultimaker materials.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com/products/materials/breakaway",
"author": {
"author_id": "UltimakerPackages",
@@ -1335,8 +1403,8 @@
"package_type": "material",
"display_name": "Ultimaker CPE",
"description": "Example package for material and quality profiles for Ultimaker materials.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com/products/materials/abs",
"author": {
"author_id": "UltimakerPackages",
@@ -1354,8 +1422,8 @@
"package_type": "material",
"display_name": "Ultimaker CPE+",
"description": "Example package for material and quality profiles for Ultimaker materials.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com/products/materials/cpe",
"author": {
"author_id": "UltimakerPackages",
@@ -1373,8 +1441,8 @@
"package_type": "material",
"display_name": "Ultimaker Nylon",
"description": "Example package for material and quality profiles for Ultimaker materials.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com/products/materials/abs",
"author": {
"author_id": "UltimakerPackages",
@@ -1392,8 +1460,8 @@
"package_type": "material",
"display_name": "Ultimaker PC",
"description": "Example package for material and quality profiles for Ultimaker materials.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.2",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com/products/materials/pc",
"author": {
"author_id": "UltimakerPackages",
@@ -1411,8 +1479,8 @@
"package_type": "material",
"display_name": "Ultimaker PLA",
"description": "Example package for material and quality profiles for Ultimaker materials.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com/products/materials/abs",
"author": {
"author_id": "UltimakerPackages",
@@ -1430,8 +1498,8 @@
"package_type": "material",
"display_name": "Ultimaker PP",
"description": "Example package for material and quality profiles for Ultimaker materials.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com/products/materials/pp",
"author": {
"author_id": "UltimakerPackages",
@@ -1449,8 +1517,8 @@
"package_type": "material",
"display_name": "Ultimaker PVA",
"description": "Example package for material and quality profiles for Ultimaker materials.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com/products/materials/abs",
"author": {
"author_id": "UltimakerPackages",
@@ -1468,8 +1536,8 @@
"package_type": "material",
"display_name": "Ultimaker TPU 95A",
"description": "Example package for material and quality profiles for Ultimaker materials.",
- "package_version": "1.2.0",
- "sdk_version": 5,
+ "package_version": "1.2.1",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com/products/materials/tpu-95a",
"author": {
"author_id": "UltimakerPackages",
@@ -1487,8 +1555,8 @@
"package_type": "material",
"display_name": "Ultimaker Tough PLA",
"description": "Example package for material and quality profiles for Ultimaker materials.",
- "package_version": "1.0.2",
- "sdk_version": 5,
+ "package_version": "1.0.3",
+ "sdk_version": "6.0",
"website": "https://ultimaker.com/products/materials/tough-pla",
"author": {
"author_id": "UltimakerPackages",
@@ -1506,8 +1574,8 @@
"package_type": "material",
"display_name": "Vertex Delta ABS",
"description": "ABS material and quality files for the Delta Vertex K8800.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://vertex3dprinter.eu",
"author": {
"author_id": "Velleman",
@@ -1523,8 +1591,8 @@
"package_type": "material",
"display_name": "Vertex Delta PET",
"description": "ABS material and quality files for the Delta Vertex K8800.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://vertex3dprinter.eu",
"author": {
"author_id": "Velleman",
@@ -1540,8 +1608,8 @@
"package_type": "material",
"display_name": "Vertex Delta PLA",
"description": "ABS material and quality files for the Delta Vertex K8800.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://vertex3dprinter.eu",
"author": {
"author_id": "Velleman",
@@ -1557,8 +1625,8 @@
"package_type": "material",
"display_name": "Vertex Delta TPU",
"description": "ABS material and quality files for the Delta Vertex K8800.",
- "package_version": "1.0.0",
- "sdk_version": 5,
+ "package_version": "1.0.1",
+ "sdk_version": "6.0",
"website": "https://vertex3dprinter.eu",
"author": {
"author_id": "Velleman",
@@ -1568,4 +1636,4 @@
}
}
}
-}
+}
\ No newline at end of file
diff --git a/resources/definitions/alfawise_u20.def.json b/resources/definitions/alfawise_u20.def.json
new file mode 100644
index 0000000000..de8525fa4d
--- /dev/null
+++ b/resources/definitions/alfawise_u20.def.json
@@ -0,0 +1,93 @@
+{
+ "name": "Alfawise U20",
+ "version": 2,
+ "inherits": "fdmprinter",
+ "metadata": {
+ "visible": true,
+ "author": "Samuel Pinches",
+ "manufacturer": "Alfawise",
+ "file_formats": "text/x-gcode",
+ "preferred_quality_type": "fast",
+ "machine_extruder_trains":
+ {
+ "0": "alfawise_u20_extruder_0"
+ }
+ },
+ "overrides": {
+ "machine_name": {
+ "default_value": "Alfawise U20"
+ },
+ "machine_start_gcode": {
+ "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --"
+ },
+ "machine_end_gcode": {
+ "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --"
+ },
+ "machine_width": {
+ "default_value": 300
+ },
+ "machine_height": {
+ "default_value": 400
+ },
+ "machine_depth": {
+ "default_value": 300
+ },
+ "machine_heated_bed": {
+ "default_value": true
+ },
+ "machine_center_is_zero": {
+ "default_value": false
+ },
+ "gantry_height": {
+ "default_value": 10
+ },
+ "machine_gcode_flavor": {
+ "default_value": "RepRap (Marlin/Sprinter)"
+ },
+ "material_diameter": {
+ "default_value": 1.75
+ },
+ "material_print_temperature": {
+ "default_value": 210
+ },
+ "material_bed_temperature": {
+ "default_value": 50
+ },
+ "layer_height_0": {
+ "default_value": 0.2
+ },
+ "wall_thickness": {
+ "default_value": 1.2
+ },
+ "speed_print": {
+ "default_value": 40
+ },
+ "speed_infill": {
+ "default_value": 40
+ },
+ "speed_wall": {
+ "default_value": 35
+ },
+ "speed_topbottom": {
+ "default_value": 35
+ },
+ "speed_travel": {
+ "default_value": 120
+ },
+ "speed_layer_0": {
+ "default_value": 20
+ },
+ "support_enable": {
+ "default_value": true
+ },
+ "retraction_enable": {
+ "default_value": true
+ },
+ "retraction_amount": {
+ "default_value": 5
+ },
+ "retraction_speed": {
+ "default_value": 45
+ }
+ }
+}
diff --git a/resources/definitions/bibo2_dual.def.json b/resources/definitions/bibo2_dual.def.json
new file mode 100644
index 0000000000..2036290ebd
--- /dev/null
+++ b/resources/definitions/bibo2_dual.def.json
@@ -0,0 +1,92 @@
+{
+ "id": "BIBO2 dual",
+ "version": 2,
+ "name": "BIBO2 dual",
+ "inherits": "fdmprinter",
+ "metadata": {
+ "visible": true,
+ "author": "na",
+ "manufacturer": "BIBO",
+ "category": "Other",
+ "file_formats": "text/x-gcode",
+ "has_materials": true,
+ "machine_extruder_trains": {
+ "0": "bibo2_dual_extruder_0",
+ "1": "bibo2_dual_extruder_1"
+ },
+ "first_start_actions": [
+ "MachineSettingsAction"
+ ]
+ },
+ "overrides": {
+ "machine_name": {
+ "default_value": "BIBO2 dual"
+ },
+ "machine_width": {
+ "default_value": 214
+ },
+ "machine_height": {
+ "default_value": 160
+ },
+ "machine_depth": {
+ "default_value": 186
+ },
+ "machine_center_is_zero": {
+ "default_value": true
+ },
+ "machine_heated_bed": {
+ "default_value": true
+ },
+ "machine_nozzle_heat_up_speed": {
+ "default_value": 2
+ },
+ "machine_nozzle_cool_down_speed": {
+ "default_value": 2
+ },
+ "machine_head_with_fans_polygon": {
+ "default_value": [
+ [
+ -68.18,
+ 64.63
+ ],
+ [
+ -68.18,
+ -47.38
+ ],
+ [
+ 35.18,
+ 64.63
+ ],
+ [
+ 35.18,
+ -47.38
+ ]
+ ]
+ },
+ "gantry_height": {
+ "default_value": 12
+ },
+ "machine_use_extruder_offset_to_offset_coords": {
+ "default_value": true
+ },
+ "machine_gcode_flavor": {
+ "default_value": "RepRap (Marlin/Sprinter)"
+ },
+ "machine_start_gcode": {
+ "default_value": "G21 ;metric values\nG90 ;absolute positioning\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstops\nG1 Z2.0 F400 ;move the platform down 15mm\nT0\nG92 E0\nG28\nG1 Y0 F1200 E0\nG92 E0\nM117 BIBO Printing..."
+ },
+ "machine_end_gcode": {
+ "default_value": ";End GCode\nM104 T0 S0 ;extruder heater off\nM104 T1 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91\nG1 Z1 F100 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-2 X-20 Y-20 F300 ;move Z up a bit and retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning"
+ },
+ "machine_extruder_count": {
+ "default_value": 2
+ },
+ "prime_tower_position_x": {
+ "default_value": 50
+ },
+ "prime_tower_position_y": {
+ "default_value": 50
+ }
+ }
+}
+
diff --git a/resources/definitions/cocoon_create_modelmaker.def.json b/resources/definitions/cocoon_create_modelmaker.def.json
new file mode 100644
index 0000000000..22aa75d09e
--- /dev/null
+++ b/resources/definitions/cocoon_create_modelmaker.def.json
@@ -0,0 +1,96 @@
+{
+ "name": "Cocoon Create ModelMaker",
+ "version": 2,
+ "inherits": "fdmprinter",
+ "metadata": {
+ "visible": true,
+ "author": "Samuel Pinches",
+ "manufacturer": "Cocoon Create",
+ "file_formats": "text/x-gcode",
+ "preferred_quality_type": "fine",
+ "machine_extruder_trains":
+ {
+ "0": "cocoon_create_modelmaker_extruder_0"
+ }
+ },
+ "overrides": {
+ "machine_name": {
+ "default_value": "Cocoon Create ModelMaker"
+ },
+ "machine_start_gcode": {
+ "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --"
+ },
+ "machine_end_gcode": {
+ "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 Y0 ;move to the XY-axis origin (Home)\nM84 ;turn off stepper motors\n; -- end of END GCODE --"
+ },
+ "machine_width": {
+ "default_value": 120
+ },
+ "machine_height": {
+ "default_value": 100
+ },
+ "machine_depth": {
+ "default_value": 135
+ },
+ "machine_heated_bed": {
+ "default_value": false
+ },
+ "machine_center_is_zero": {
+ "default_value": false
+ },
+ "gantry_height": {
+ "default_value": 10
+ },
+ "machine_gcode_flavor": {
+ "default_value": "RepRap (Marlin/Sprinter)"
+ },
+ "material_diameter": {
+ "default_value": 1.75
+ },
+ "material_print_temperature": {
+ "default_value": 220
+ },
+ "layer_height": {
+ "default_value": 0.10
+ },
+ "layer_height_0": {
+ "default_value": 0.2
+ },
+ "wall_thickness": {
+ "default_value": 1.2
+ },
+ "top_bottom_thickness": {
+ "default_value": 0.6
+ },
+ "speed_print": {
+ "default_value": 40
+ },
+ "speed_infill": {
+ "default_value": 40
+ },
+ "speed_wall": {
+ "default_value": 35
+ },
+ "speed_topbottom": {
+ "default_value": 35
+ },
+ "speed_travel": {
+ "default_value": 70
+ },
+ "speed_layer_0": {
+ "default_value": 20
+ },
+ "support_enable": {
+ "default_value": true
+ },
+ "retraction_enable": {
+ "default_value": true
+ },
+ "retraction_amount": {
+ "default_value": 7
+ },
+ "retraction_speed": {
+ "default_value": 40
+ }
+ }
+}
diff --git a/resources/definitions/fdmextruder.def.json b/resources/definitions/fdmextruder.def.json
index 19c9e92d18..0af1e68075 100644
--- a/resources/definitions/fdmextruder.def.json
+++ b/resources/definitions/fdmextruder.def.json
@@ -78,7 +78,7 @@
"machine_extruder_start_code":
{
"label": "Extruder Start G-Code",
- "description": "Start g-code to execute whenever turning the extruder on.",
+ "description": "Start g-code to execute when switching to this extruder.",
"type": "str",
"default_value": "",
"settable_per_mesh": false,
@@ -124,7 +124,7 @@
"machine_extruder_end_code":
{
"label": "Extruder End G-Code",
- "description": "End g-code to execute whenever turning the extruder off.",
+ "description": "End g-code to execute when switching away from this extruder.",
"type": "str",
"default_value": "",
"settable_per_mesh": false,
@@ -189,7 +189,7 @@
"settable_per_mesh": false,
"settable_per_extruder": true,
"settable_per_meshgroup": false,
- "setttable_globally": false
+ "settable_globally": false
}
}
},
diff --git a/resources/definitions/fdmprinter.def.json b/resources/definitions/fdmprinter.def.json
index 7cb6720f27..f39e267354 100644
--- a/resources/definitions/fdmprinter.def.json
+++ b/resources/definitions/fdmprinter.def.json
@@ -2406,7 +2406,7 @@
"switch_extruder_retraction_amount":
{
"label": "Nozzle Switch Retraction Distance",
- "description": "The amount of retraction: Set at 0 for no retraction at all. This should generally be the same as the length of the heat zone.",
+ "description": "The amount of retraction when switching extruders. Set to 0 for no retraction at all. This should generally be the same as the length of the heat zone.",
"type": "float",
"unit": "mm",
"enabled": "retraction_enable",
@@ -3385,7 +3385,7 @@
"retraction_combing":
{
"label": "Combing Mode",
- "description": "Combing keeps the nozzle within already printed areas when traveling. This results in slightly longer travel moves but reduces the need for retractions. If combing is off, the material will retract and the nozzle moves in a straight line to the next point. It is also possible to avoid combing over top/bottom skin areas and also to only comb within the infill. Note that the 'Within Infill' option behaves exactly like the 'Not in Skin' option in earlier Cura releases.",
+ "description": "Combing keeps the nozzle within already printed areas when traveling. This results in slightly longer travel moves but reduces the need for retractions. If combing is off, the material will retract and the nozzle moves in a straight line to the next point. It is also possible to avoid combing over top/bottom skin areas or to only comb within the infill.",
"type": "enum",
"options":
{
diff --git a/resources/definitions/gmax15plus.def.json b/resources/definitions/gmax15plus.def.json
index 16695714f4..069b8be999 100644
--- a/resources/definitions/gmax15plus.def.json
+++ b/resources/definitions/gmax15plus.def.json
@@ -14,19 +14,24 @@
"has_variants": true,
"variants_name": "Hotend",
"preferred_variant_name": "0.5mm E3D (Default)",
+ "preferred_quality_type": "gmax15plus_global_normal",
"machine_extruder_trains": {
"0": "gmax15plus_extruder_0"
}
+
+
},
"overrides": {
- "machine_extruder_count": { "default_value": 1 },
+ "machine_extruder_count": { "default_value": 1 },
"machine_name": { "default_value": "gMax 1.5 Plus" },
"machine_heated_bed": { "default_value": false },
"machine_width": { "default_value": 406 },
"machine_depth": { "default_value": 406 },
"machine_height": { "default_value": 533 },
"machine_center_is_zero": { "default_value": false },
+ "material_diameter": { "default_value": 1.75 },
+ "machine_nozzle_size": { "default_value": 0.5 },
"layer_height": { "default_value": 0.2 },
"layer_height_0": { "default_value": 0.3 },
"retraction_amount": { "default_value": 1 },
@@ -43,10 +48,10 @@
"machine_max_jerk_z": { "default_value": 0.4 },
"machine_max_jerk_e": { "default_value": 5.0 },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
- "machine_start_gcode": { "default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 ;Home X/Y/Z\nG29 ; Bed level\nM104 S{material_print_temperature} ; Preheat\nM109 S{material_print_temperature} ; Preheat\nG91 ;relative positioning\nG90 ;absolute positioning\nG1 Z25.0 F9000 ;raise nozzle 25mm\nG92 E0 ;zero the extruded length again\nG1 F9000\n;Put printing message on LCD screen\nM117 Printing..." },
+ "machine_start_gcode": { "default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 ;Home X/Y/Z\nM104 S{material_print_temperature} ; Preheat\nM109 S{material_print_temperature} ; Preheat\nG91 ;relative positioning\nG90 ;absolute positioning\nG1 Z25.0 F9000 ;raise nozzle 25mm\nG92 E0 ;zero the extruded length again\nG1 F9000\n;Put printing message on LCD screen\nM117 Printing..." },
"machine_end_gcode": { "default_value": "M104 S0 ;extruder heater off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning" },
- "material_print_temperature": { "default_value": 202 },
- "wall_thickness": { "default_value": 1 },
+ "material_print_temperature": { "default_value": 202 },
+ "wall_thickness": { "default_value": 1 },
"top_bottom_thickness": { "default_value": 1 },
"bottom_thickness": { "default_value": 1 }
}
diff --git a/resources/definitions/gmax15plus_dual.def.json b/resources/definitions/gmax15plus_dual.def.json
index 5972061933..0264ef5977 100644
--- a/resources/definitions/gmax15plus_dual.def.json
+++ b/resources/definitions/gmax15plus_dual.def.json
@@ -10,24 +10,26 @@
"category": "Other",
"file_formats": "text/x-gcode",
"platform": "gmax_1-5_xt-plus_s3d_full model_150707.stl",
- "has_variants": true,
- "has_machine_quality": true,
- "variants_name": "Hotend",
- "preferred_variant_name": "0.5mm E3D (Default)",
- "machine_extruder_trains": {
- "0": "gmax15plus_dual_extruder_0",
- "1": "gmax15plus_dual_extruder_1"
- }
+ "has_variants": true,
+ "variants_name": "Hotend",
+ "preferred_variant_name": "0.5mm E3D (Default)",
+ "preferred_quality_type": "gmax15plus_global_dual_normal",
+ "machine_extruder_trains": {
+ "0": "gmax15plus_dual_extruder_0",
+ "1": "gmax15plus_dual_extruder_1"
+ }
},
"overrides": {
"machine_name": { "default_value": "gMax 1.5 Plus Dual Extruder" },
- "machine_extruder_count": { "default_value": 2 },
+ "machine_extruder_count": { "default_value": 2 },
"machine_heated_bed": { "default_value": false },
"machine_width": { "default_value": 406 },
"machine_depth": { "default_value": 406 },
"machine_height": { "default_value": 533 },
"machine_center_is_zero": { "default_value": false },
+ "material_diameter": { "default_value": 1.75 },
+ "machine_nozzle_size": { "default_value": 0.5 },
"layer_height": { "default_value": 0.2 },
"layer_height_0": { "default_value": 0.3 },
"retraction_amount": { "default_value": 1 },
@@ -44,10 +46,10 @@
"machine_max_jerk_z": { "default_value": 0.4 },
"machine_max_jerk_e": { "default_value": 5.0 },
"machine_gcode_flavor": { "default_value": "RepRap (Marlin/Sprinter)" },
- "machine_start_gcode": { "default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 ;Home X/Y/Z\nG29 ; Bed level\nM104 S{material_print_temperature} T0 ; Preheat Left Extruder\nM104 S{material_print_temperature} T1 ; Preheat Right Extruder\nM109 S{material_print_temperature} T0 ; Preheat Left Extruder\nM109 S{material_print_temperature} T1 ; Preheat Right Extruder\nG91 ;relative positioning\nG90 ;absolute positioning\nM218 T1 X34.3 Y0; Set 2nd extruder offset. This can be changed later if needed\nG1 Z25.0 F9000 ;raise nozzle 25mm\nG92 E0 ;zero the extruded length again\nG1 F9000\n;Put printing message on LCD screen\nM117 Printing..." },
+ "machine_start_gcode": { "default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 ;Home X/Y/Z\nM104 S{material_print_temperature} T0 ; Preheat Left Extruder\nM104 S{material_print_temperature} T1 ; Preheat Right Extruder\nM109 S{material_print_temperature} T0 ; Preheat Left Extruder\nM109 S{material_print_temperature} T1 ; Preheat Right Extruder\nG91 ;relative positioning\nG90 ;absolute positioning\nM218 T1 X34.3 Y0; Set 2nd extruder offset. This can be changed later if needed\nG1 Z25.0 F9000 ;raise nozzle 25mm\nG92 E0 ;zero the extruded length again\nG1 F9000\n;Put printing message on LCD screen\nM117 Printing..." },
"machine_end_gcode": { "default_value": "M104 S0 T0;Left extruder off\nM104 S0 T1; Right extruder off\nM140 S0 ;heated bed heater off (if you have it)\nG91 ;relative positioning\nG1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\nG1 Z+0.5 E-5 X-20 Y-20 F9000 ;move Z up a bit and retract filament even more\nG28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\nM84 ;steppers off\nG90 ;absolute positioning" },
- "material_print_temperature": { "default_value": 202 },
- "wall_thickness": { "default_value": 1 },
+ "material_print_temperature": { "default_value": 202 },
+ "wall_thickness": { "default_value": 1 },
"top_bottom_thickness": { "default_value": 1 },
"bottom_thickness": { "default_value": 1 }
}
diff --git a/resources/definitions/jgaurora_a1.def.json b/resources/definitions/jgaurora_a1.def.json
new file mode 100644
index 0000000000..b9a921c311
--- /dev/null
+++ b/resources/definitions/jgaurora_a1.def.json
@@ -0,0 +1,93 @@
+{
+ "name": "JGAurora A1",
+ "version": 2,
+ "inherits": "fdmprinter",
+ "metadata": {
+ "visible": true,
+ "author": "Samuel Pinches",
+ "manufacturer": "JGAurora",
+ "file_formats": "text/x-gcode",
+ "preferred_quality_type": "fast",
+ "machine_extruder_trains":
+ {
+ "0": "jgaurora_a1_extruder_0"
+ }
+ },
+ "overrides": {
+ "machine_name": {
+ "default_value": "JGAurora A1"
+ },
+ "machine_start_gcode": {
+ "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nM420 S1 ;turn on mesh bed levelling if enabled in firmware\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --"
+ },
+ "machine_end_gcode": {
+ "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --"
+ },
+ "machine_width": {
+ "default_value": 300
+ },
+ "machine_height": {
+ "default_value": 300
+ },
+ "machine_depth": {
+ "default_value": 300
+ },
+ "machine_heated_bed": {
+ "default_value": true
+ },
+ "machine_center_is_zero": {
+ "default_value": false
+ },
+ "gantry_height": {
+ "default_value": 10
+ },
+ "machine_gcode_flavor": {
+ "default_value": "RepRap (Marlin/Sprinter)"
+ },
+ "material_diameter": {
+ "default_value": 1.75
+ },
+ "material_print_temperature": {
+ "default_value": 215
+ },
+ "material_bed_temperature": {
+ "default_value": 67
+ },
+ "layer_height_0": {
+ "default_value": 0.12
+ },
+ "wall_thickness": {
+ "default_value": 1.2
+ },
+ "speed_print": {
+ "default_value": 40
+ },
+ "speed_infill": {
+ "default_value": 40
+ },
+ "speed_wall": {
+ "default_value": 35
+ },
+ "speed_topbottom": {
+ "default_value": 35
+ },
+ "speed_travel": {
+ "default_value": 120
+ },
+ "speed_layer_0": {
+ "default_value": 12
+ },
+ "support_enable": {
+ "default_value": true
+ },
+ "retraction_enable": {
+ "default_value": true
+ },
+ "retraction_amount": {
+ "default_value": 6
+ },
+ "retraction_speed": {
+ "default_value": 40
+ }
+ }
+}
diff --git a/resources/definitions/jgaurora_a5.def.json b/resources/definitions/jgaurora_a5.def.json
new file mode 100644
index 0000000000..d84a8440e6
--- /dev/null
+++ b/resources/definitions/jgaurora_a5.def.json
@@ -0,0 +1,95 @@
+{
+ "name": "JGAurora A5 & A5S",
+ "version": 2,
+ "inherits": "fdmprinter",
+ "metadata": {
+ "visible": true,
+ "author": "Samuel Pinches",
+ "manufacturer": "JGAurora",
+ "file_formats": "text/x-gcode",
+ "platform": "jgaurora_a5.stl",
+ "platform_offset": [-242, -101, 273],
+ "preferred_quality_type": "fast",
+ "machine_extruder_trains":
+ {
+ "0": "jgaurora_a5_extruder_0"
+ }
+ },
+ "overrides": {
+ "machine_name": {
+ "default_value": "JGAurora A5 & A5S"
+ },
+ "machine_start_gcode": {
+ "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nM420 S1 ;turn on mesh bed levelling if enabled in firmware\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --"
+ },
+ "machine_end_gcode": {
+ "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --"
+ },
+ "machine_width": {
+ "default_value": 300
+ },
+ "machine_height": {
+ "default_value": 320
+ },
+ "machine_depth": {
+ "default_value": 300
+ },
+ "machine_heated_bed": {
+ "default_value": true
+ },
+ "machine_center_is_zero": {
+ "default_value": false
+ },
+ "gantry_height": {
+ "default_value": 10
+ },
+ "machine_gcode_flavor": {
+ "default_value": "RepRap (Marlin/Sprinter)"
+ },
+ "material_diameter": {
+ "default_value": 1.75
+ },
+ "material_print_temperature": {
+ "default_value": 215
+ },
+ "material_bed_temperature": {
+ "default_value": 67
+ },
+ "layer_height_0": {
+ "default_value": 0.12
+ },
+ "wall_thickness": {
+ "default_value": 1.2
+ },
+ "speed_print": {
+ "default_value": 40
+ },
+ "speed_infill": {
+ "default_value": 40
+ },
+ "speed_wall": {
+ "default_value": 35
+ },
+ "speed_topbottom": {
+ "default_value": 35
+ },
+ "speed_travel": {
+ "default_value": 120
+ },
+ "speed_layer_0": {
+ "default_value": 12
+ },
+ "support_enable": {
+ "default_value": true
+ },
+ "retraction_enable": {
+ "default_value": true
+ },
+ "retraction_amount": {
+ "default_value": 8
+ },
+ "retraction_speed": {
+ "default_value": 45
+ }
+ }
+}
diff --git a/resources/definitions/jgaurora_z_603s.def.json b/resources/definitions/jgaurora_z_603s.def.json
new file mode 100644
index 0000000000..3a78585240
--- /dev/null
+++ b/resources/definitions/jgaurora_z_603s.def.json
@@ -0,0 +1,93 @@
+{
+ "name": "JGAurora Z-603S",
+ "version": 2,
+ "inherits": "fdmprinter",
+ "metadata": {
+ "visible": true,
+ "author": "Samuel Pinches",
+ "manufacturer": "JGAurora",
+ "file_formats": "text/x-gcode",
+ "preferred_quality_type": "fast",
+ "machine_extruder_trains":
+ {
+ "0": "jgaurora_z_603s_extruder_0"
+ }
+ },
+ "overrides": {
+ "machine_name": {
+ "default_value": "JGAurora Z-603S"
+ },
+ "machine_start_gcode": {
+ "default_value": "; -- START GCODE --\nG21 ;set units to millimetres\nG90 ;set to absolute positioning\nM106 S0 ;set fan speed to zero (turned off)\nG28 ;home all axis\nM420 S1 ;turn on mesh bed levelling if enabled in firmware\nG92 E0 ;zero the extruded length\nG1 Z1 F1000 ;move up slightly\nG1 X60.0 Z0 E9.0 F1000.0;intro line\nG1 X100.0 E21.5 F1000.0 ;continue line\nG92 E0 ;zero the extruded length again\n; -- end of START GCODE --"
+ },
+ "machine_end_gcode": {
+ "default_value": "; -- END GCODE --\nM104 S0 ;turn off nozzle heater\nM140 S0 ;turn off bed heater\nG91 ;set to relative positioning\nG1 E-10 F300 ;retract the filament slightly\nG90 ;set to absolute positioning\nG28 X0 ;move to the X-axis origin (Home)\nG0 Y280 F600 ;bring the bed to the front for easy print removal\nM84 ;turn off stepper motors\n; -- end of END GCODE --"
+ },
+ "machine_width": {
+ "default_value": 280
+ },
+ "machine_height": {
+ "default_value": 175
+ },
+ "machine_depth": {
+ "default_value": 180
+ },
+ "machine_heated_bed": {
+ "default_value": true
+ },
+ "machine_center_is_zero": {
+ "default_value": false
+ },
+ "gantry_height": {
+ "default_value": 10
+ },
+ "machine_gcode_flavor": {
+ "default_value": "RepRap (Marlin/Sprinter)"
+ },
+ "material_diameter": {
+ "default_value": 1.75
+ },
+ "material_print_temperature": {
+ "default_value": 210
+ },
+ "material_bed_temperature": {
+ "default_value": 55
+ },
+ "layer_height_0": {
+ "default_value": 0.2
+ },
+ "wall_thickness": {
+ "default_value": 1.2
+ },
+ "speed_print": {
+ "default_value": 60
+ },
+ "speed_infill": {
+ "default_value": 60
+ },
+ "speed_wall": {
+ "default_value": 30
+ },
+ "speed_topbottom": {
+ "default_value": 45
+ },
+ "speed_travel": {
+ "default_value": 125
+ },
+ "speed_layer_0": {
+ "default_value": 20
+ },
+ "support_enable": {
+ "default_value": true
+ },
+ "retraction_enable": {
+ "default_value": true
+ },
+ "retraction_amount": {
+ "default_value": 5
+ },
+ "retraction_speed": {
+ "default_value": 50
+ }
+ }
+}
diff --git a/resources/definitions/tevo_tarantula.def.json b/resources/definitions/tevo_tarantula.def.json
index 570ae24a3d..ec4ae667d5 100644
--- a/resources/definitions/tevo_tarantula.def.json
+++ b/resources/definitions/tevo_tarantula.def.json
@@ -42,7 +42,7 @@
"machine_max_feedrate_x": { "default_value": 255 },
"machine_max_feedrate_y": { "default_value": 225 },
"machine_max_feedrate_z": { "default_value": 3 },
- "machine_max_acceleration_x": { "default_value": 2620 },
+ "machine_max_acceleration_x": { "default_value": 2650 },
"machine_max_acceleration_y": { "default_value": 2650 },
"acceleration_print": { "default_value": 2650 },
"machine_start_gcode": { "default_value": "G21 ;metric values\nG90 ;absolute positioning\nM82 ;set extruder to absolute mode\nM107 ;start with the fan off\nG28 X0 Y0 ;move X/Y to min endstops\nG28 Z0 ;move Z to min endstops\nG1 Z15.0 F9000 ;move the platform down 15mm\nG92 E0 ;zero the extruded length\nG1 F200 E3 ;extrude 3mm of feed stock\nG92 E0 ;zero the extruded length again\nG1 F9000\n;Put printing message on LCD screen\nM117 Printing..." },
diff --git a/resources/definitions/ultimaker_original_plus.def.json b/resources/definitions/ultimaker_original_plus.def.json
index 5ad7ae66e8..bdb8a3d788 100644
--- a/resources/definitions/ultimaker_original_plus.def.json
+++ b/resources/definitions/ultimaker_original_plus.def.json
@@ -16,7 +16,8 @@
{
"0": "ultimaker_original_plus_extruder_0"
},
- "firmware_file": "MarlinUltimaker-UMOP-{baudrate}.hex"
+ "firmware_file": "MarlinUltimaker-UMOP-{baudrate}.hex",
+ "firmware_hbk_file": "MarlinUltimaker-UMOP-{baudrate}.hex"
},
"overrides": {
diff --git a/resources/definitions/wanhao_d4s.def.json b/resources/definitions/wanhao_d4s.def.json
index 8788353e92..c1807923c6 100644
--- a/resources/definitions/wanhao_d4s.def.json
+++ b/resources/definitions/wanhao_d4s.def.json
@@ -39,10 +39,10 @@
"default_value": "RepRap (Marlin/Sprinter)"
},
"machine_start_gcode": {
- "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..."
+ "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..."
},
"machine_end_gcode": {
- "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
+ "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
}
}
}
diff --git a/resources/definitions/wanhao_d6.def.json b/resources/definitions/wanhao_d6.def.json
index 7ca3031124..c8a690d02c 100644
--- a/resources/definitions/wanhao_d6.def.json
+++ b/resources/definitions/wanhao_d6.def.json
@@ -42,10 +42,10 @@
"default_value": "RepRap (Marlin/Sprinter)"
},
"machine_start_gcode": {
- "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..."
+ "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..."
},
"machine_end_gcode": {
- "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
+ "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
}
}
}
diff --git a/resources/definitions/wanhao_d6_plus.def.json b/resources/definitions/wanhao_d6_plus.def.json
index f17b58db85..b3b5ed9b0a 100644
--- a/resources/definitions/wanhao_d6_plus.def.json
+++ b/resources/definitions/wanhao_d6_plus.def.json
@@ -39,10 +39,10 @@
"default_value": "RepRap (Marlin/Sprinter)"
},
"machine_start_gcode": {
- "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..."
+ "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..."
},
"machine_end_gcode": {
- "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
+ "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
}
}
}
diff --git a/resources/definitions/wanhao_duplicator5S.def.json b/resources/definitions/wanhao_duplicator5S.def.json
index 1d29b90249..b27a13fda8 100644
--- a/resources/definitions/wanhao_duplicator5S.def.json
+++ b/resources/definitions/wanhao_duplicator5S.def.json
@@ -42,10 +42,10 @@
"default_value": "RepRap (Marlin/Sprinter)"
},
"machine_start_gcode": {
- "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..."
+ "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..."
},
"machine_end_gcode": {
- "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
+ "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
}
}
}
diff --git a/resources/definitions/wanhao_duplicator5Smini.def.json b/resources/definitions/wanhao_duplicator5Smini.def.json
index e7f9359cf1..e3ef0b92fe 100644
--- a/resources/definitions/wanhao_duplicator5Smini.def.json
+++ b/resources/definitions/wanhao_duplicator5Smini.def.json
@@ -39,10 +39,10 @@
"default_value": "RepRap (Marlin/Sprinter)"
},
"machine_start_gcode": {
- "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..."
+ "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..."
},
"machine_end_gcode": {
- "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
+ "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
}
}
}
diff --git a/resources/definitions/wanhao_i3.def.json b/resources/definitions/wanhao_i3.def.json
index 15121f8b8b..42b19c8748 100644
--- a/resources/definitions/wanhao_i3.def.json
+++ b/resources/definitions/wanhao_i3.def.json
@@ -39,10 +39,10 @@
"default_value": "RepRap (Marlin/Sprinter)"
},
"machine_start_gcode": {
- "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..."
+ "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..."
},
"machine_end_gcode": {
- "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
+ "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
}
}
}
diff --git a/resources/definitions/wanhao_i3mini.def.json b/resources/definitions/wanhao_i3mini.def.json
index 057fca81a6..0c70391c27 100644
--- a/resources/definitions/wanhao_i3mini.def.json
+++ b/resources/definitions/wanhao_i3mini.def.json
@@ -39,10 +39,10 @@
"default_value": "RepRap (Marlin/Sprinter)"
},
"machine_start_gcode": {
- "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..."
+ "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..."
},
"machine_end_gcode": {
- "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
+ "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
}
}
}
diff --git a/resources/definitions/wanhao_i3plus.def.json b/resources/definitions/wanhao_i3plus.def.json
index 2b705c6ff5..e454a40ae1 100644
--- a/resources/definitions/wanhao_i3plus.def.json
+++ b/resources/definitions/wanhao_i3plus.def.json
@@ -39,10 +39,10 @@
"default_value": "RepRap (Marlin/Sprinter)"
},
"machine_start_gcode": {
- "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{travel_speed} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{travel_speed} \n ;Put printing message on LCD screen\n M117 Printing..."
+ "default_value": "G21 ;metric values\n G90 ;absolute positioning\n M82 ;set extruder to absolute mode\n M107 ;start with the fan off\n G28 X0 Y0 ;move X/Y to min endstops\n G28 Z0 ;move Z to min endstops\n G1 Z15.0 F{speed_travel} ;move the platform down 15mm\n G92 E0 ;zero the extruded length\n G1 F200 E6 ;extrude 6 mm of feed stock\n G92 E0 ;zero the extruded length again\n G1 F{speed_travel} \n ;Put printing message on LCD screen\n M117 Printing..."
},
"machine_end_gcode": {
- "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{travel_speed} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
+ "default_value": "M104 S0 ;extruder heater off \n G91 ;relative positioning\n G1 E-1 F300 ;retract the filament a bit before lifting the nozzle, to release some of the pressure\n G1 Z+0.5 E-5 X-20 Y-20 F{speed_travel} ;move Z up a bit and retract filament even more\n G28 X0 Y0 ;move X/Y to min endstops, so the head is out of the way\n M84 ;steppers off\n G90 ;absolute positioning"
}
}
}
diff --git a/resources/extruders/alfawise_u20_extruder_0.def.json b/resources/extruders/alfawise_u20_extruder_0.def.json
new file mode 100644
index 0000000000..2fbe3f1772
--- /dev/null
+++ b/resources/extruders/alfawise_u20_extruder_0.def.json
@@ -0,0 +1,16 @@
+{
+ "id": "alfawise_u20_extruder_0",
+ "version": 2,
+ "name": "Extruder 1",
+ "inherits": "fdmextruder",
+ "metadata": {
+ "machine": "alfawise_u20",
+ "position": "0"
+ },
+
+ "overrides": {
+ "extruder_nr": { "default_value": 0 },
+ "machine_nozzle_size": { "default_value": 0.4 },
+ "material_diameter": { "default_value": 1.75 }
+ }
+}
\ No newline at end of file
diff --git a/resources/extruders/bibo2_dual_extruder_0.def.json b/resources/extruders/bibo2_dual_extruder_0.def.json
new file mode 100644
index 0000000000..f83801fa0c
--- /dev/null
+++ b/resources/extruders/bibo2_dual_extruder_0.def.json
@@ -0,0 +1,46 @@
+{
+ "id": "BIBO1",
+ "version": 2,
+ "name": "Extruder 1",
+ "inherits": "fdmextruder",
+ "metadata": {
+ "machine": "BIBO2 dual",
+ "position": "0"
+ },
+ "overrides": {
+ "extruder_nr": {
+ "default_value": 0,
+ "maximum_value": "1"
+ },
+ "material_diameter": {
+ "default_value": 1.75
+ },
+ "machine_nozzle_size": {
+ "default_value": 0.4
+ },
+ "machine_nozzle_offset_x": {
+ "default_value": 0.0
+ },
+ "machine_nozzle_offset_y": {
+ "default_value": 0.0
+ },
+ "machine_extruder_start_pos_abs": {
+ "default_value": true
+ },
+ "machine_extruder_start_pos_x": {
+ "value": "prime_tower_position_x"
+ },
+ "machine_extruder_start_pos_y": {
+ "value": "prime_tower_position_y"
+ },
+ "machine_extruder_end_pos_abs": {
+ "default_value": true
+ },
+ "machine_extruder_end_pos_x": {
+ "value": "prime_tower_position_x"
+ },
+ "machine_extruder_end_pos_y": {
+ "value": "prime_tower_position_y"
+ }
+ }
+}
diff --git a/resources/extruders/bibo2_dual_extruder_1.def.json b/resources/extruders/bibo2_dual_extruder_1.def.json
new file mode 100644
index 0000000000..5f479ba54b
--- /dev/null
+++ b/resources/extruders/bibo2_dual_extruder_1.def.json
@@ -0,0 +1,46 @@
+{
+ "id": "BIBO2",
+ "version": 2,
+ "name": "Extruder 2",
+ "inherits": "fdmextruder",
+ "metadata": {
+ "machine": "BIBO2 dual",
+ "position": "1"
+ },
+ "overrides": {
+ "extruder_nr": {
+ "default_value": 1,
+ "maximum_value": "1"
+ },
+ "material_diameter": {
+ "default_value": 1.75
+ },
+ "machine_nozzle_size": {
+ "default_value": 0.4
+ },
+ "machine_nozzle_offset_x": {
+ "default_value": 0.0
+ },
+ "machine_nozzle_offset_y": {
+ "default_value": 0.0
+ },
+ "machine_extruder_start_pos_abs": {
+ "default_value": true
+ },
+ "machine_extruder_start_pos_x": {
+ "value": "prime_tower_position_x"
+ },
+ "machine_extruder_start_pos_y": {
+ "value": "prime_tower_position_y"
+ },
+ "machine_extruder_end_pos_abs": {
+ "default_value": true
+ },
+ "machine_extruder_end_pos_x": {
+ "value": "prime_tower_position_x"
+ },
+ "machine_extruder_end_pos_y": {
+ "value": "prime_tower_position_y"
+ }
+ }
+}
diff --git a/resources/extruders/cocoon_create_modelmaker_extruder_0.def.json b/resources/extruders/cocoon_create_modelmaker_extruder_0.def.json
new file mode 100644
index 0000000000..26d847483d
--- /dev/null
+++ b/resources/extruders/cocoon_create_modelmaker_extruder_0.def.json
@@ -0,0 +1,16 @@
+{
+ "id": "cocoon_create_modelmaker_extruder_0",
+ "version": 2,
+ "name": "Extruder 1",
+ "inherits": "fdmextruder",
+ "metadata": {
+ "machine": "cocoon_create_modelmaker",
+ "position": "0"
+ },
+
+ "overrides": {
+ "extruder_nr": { "default_value": 0 },
+ "machine_nozzle_size": { "default_value": 0.4 },
+ "material_diameter": { "default_value": 1.75 }
+ }
+}
diff --git a/resources/extruders/gmax15plus_dual_extruder_0.def.json b/resources/extruders/gmax15plus_dual_extruder_0.def.json
index b490f4a40e..d3146a0576 100644
--- a/resources/extruders/gmax15plus_dual_extruder_0.def.json
+++ b/resources/extruders/gmax15plus_dual_extruder_0.def.json
@@ -15,10 +15,10 @@
},
"machine_nozzle_offset_x": { "default_value": 0.0 },
"machine_nozzle_offset_y": { "default_value": 0.0 },
- "machine_nozzle_size": { "default_value": 0.5 },
+ "machine_nozzle_size": { "default_value": 0.5 },
"material_diameter": { "default_value": 1.75 },
-
- "machine_extruder_start_pos_abs": { "default_value": true },
+
+ "machine_extruder_start_pos_abs": { "default_value": true },
"machine_extruder_start_pos_x": { "value": 40 },
"machine_extruder_start_pos_y": { "value": 210 },
"machine_extruder_end_pos_abs": { "default_value": true },
diff --git a/resources/extruders/gmax15plus_dual_extruder_1.def.json b/resources/extruders/gmax15plus_dual_extruder_1.def.json
index ad3c628d6f..7b7354d794 100644
--- a/resources/extruders/gmax15plus_dual_extruder_1.def.json
+++ b/resources/extruders/gmax15plus_dual_extruder_1.def.json
@@ -15,10 +15,10 @@
},
"machine_nozzle_offset_x": { "default_value": 0.0 },
"machine_nozzle_offset_y": { "default_value": 0.0 },
- "machine_nozzle_size": { "default_value": 0.5 },
+ "machine_nozzle_size": { "default_value": 0.5 },
"material_diameter": { "default_value": 1.75 },
-
- "machine_extruder_start_pos_abs": { "default_value": true },
+
+ "machine_extruder_start_pos_abs": { "default_value": true },
"machine_extruder_start_pos_x": { "value": 40 },
"machine_extruder_start_pos_y": { "value": 210 },
"machine_extruder_end_pos_abs": { "default_value": true },
diff --git a/resources/extruders/jgaurora_a1_extruder_0.def.json b/resources/extruders/jgaurora_a1_extruder_0.def.json
new file mode 100644
index 0000000000..71742b734a
--- /dev/null
+++ b/resources/extruders/jgaurora_a1_extruder_0.def.json
@@ -0,0 +1,16 @@
+{
+ "id": "jgaurora_a1_extruder_0",
+ "version": 2,
+ "name": "Extruder 1",
+ "inherits": "fdmextruder",
+ "metadata": {
+ "machine": "jgaurora_a1",
+ "position": "0"
+ },
+
+ "overrides": {
+ "extruder_nr": { "default_value": 0 },
+ "machine_nozzle_size": { "default_value": 0.4 },
+ "material_diameter": { "default_value": 1.75 }
+ }
+}
diff --git a/resources/extruders/jgaurora_a5_extruder_0.def.json b/resources/extruders/jgaurora_a5_extruder_0.def.json
new file mode 100644
index 0000000000..fbc6ba77e6
--- /dev/null
+++ b/resources/extruders/jgaurora_a5_extruder_0.def.json
@@ -0,0 +1,16 @@
+{
+ "id": "jgaurora_a5_extruder_0",
+ "version": 2,
+ "name": "Extruder 1",
+ "inherits": "fdmextruder",
+ "metadata": {
+ "machine": "jgaurora_a5",
+ "position": "0"
+ },
+
+ "overrides": {
+ "extruder_nr": { "default_value": 0 },
+ "machine_nozzle_size": { "default_value": 0.4 },
+ "material_diameter": { "default_value": 1.75 }
+ }
+}
diff --git a/resources/extruders/jgaurora_z_603s_extruder_0.def.json b/resources/extruders/jgaurora_z_603s_extruder_0.def.json
new file mode 100644
index 0000000000..987425b28a
--- /dev/null
+++ b/resources/extruders/jgaurora_z_603s_extruder_0.def.json
@@ -0,0 +1,16 @@
+{
+ "id": "jgaurora_z_603s_extruder_0",
+ "version": 2,
+ "name": "Extruder 1",
+ "inherits": "fdmextruder",
+ "metadata": {
+ "machine": "jgaurora_z_603s",
+ "position": "0"
+ },
+
+ "overrides": {
+ "extruder_nr": { "default_value": 0 },
+ "machine_nozzle_size": { "default_value": 0.4 },
+ "material_diameter": { "default_value": 1.75 }
+ }
+}
diff --git a/resources/meshes/jgaurora_a5.stl b/resources/meshes/jgaurora_a5.stl
new file mode 100644
index 0000000000..c525b03649
Binary files /dev/null and b/resources/meshes/jgaurora_a5.stl differ
diff --git a/resources/qml/AboutDialog.qml b/resources/qml/AboutDialog.qml
deleted file mode 100644
index 9a7e53260b..0000000000
--- a/resources/qml/AboutDialog.qml
+++ /dev/null
@@ -1,170 +0,0 @@
-// Copyright (c) 2015 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.2
-import QtQuick.Controls 1.1
-import QtQuick.Window 2.1
-
-import UM 1.1 as UM
-
-UM.Dialog
-{
- id: base
-
- //: About dialog title
- title: catalog.i18nc("@title:window","About Cura")
-
- minimumWidth: 500 * screenScaleFactor
- minimumHeight: 650 * screenScaleFactor
- width: minimumWidth
- height: minimumHeight
-
- Rectangle
- {
- width: parent.width + 2 * margin // margin from Dialog.qml
- height: version.y + version.height + margin
-
- anchors.top: parent.top
- anchors.topMargin: - margin
- anchors.horizontalCenter: parent.horizontalCenter
-
- color: UM.Theme.getColor("viewport_background")
- }
-
- Image
- {
- id: logo
- width: (base.minimumWidth * 0.85) | 0
- height: (width * (1/4.25)) | 0
-
- source: UM.Theme.getImage("logo")
-
- sourceSize.width: width
- sourceSize.height: height
- anchors.top: parent.top
- anchors.topMargin: ((base.minimumWidth - width) / 2) | 0
- anchors.horizontalCenter: parent.horizontalCenter
-
- UM.I18nCatalog{id: catalog; name:"cura"}
- }
-
- Label
- {
- id: version
-
- text: catalog.i18nc("@label","version: %1").arg(UM.Application.version)
- font: UM.Theme.getFont("large")
- color: UM.Theme.getColor("text")
- anchors.right : logo.right
- anchors.top: logo.bottom
- anchors.topMargin: (UM.Theme.getSize("default_margin").height / 2) | 0
- }
-
- Label
- {
- id: description
- width: parent.width
-
- //: About dialog application description
- text: catalog.i18nc("@label","End-to-end solution for fused filament 3D printing.")
- font: UM.Theme.getFont("system")
- wrapMode: Text.WordWrap
- anchors.top: version.bottom
- anchors.topMargin: UM.Theme.getSize("default_margin").height
- }
-
- Label
- {
- id: creditsNotes
- width: parent.width
-
- //: About dialog application author note
- text: catalog.i18nc("@info:credit","Cura is developed by Ultimaker B.V. in cooperation with the community.\nCura proudly uses the following open source projects:")
- font: UM.Theme.getFont("system")
- wrapMode: Text.WordWrap
- anchors.top: description.bottom
- anchors.topMargin: UM.Theme.getSize("default_margin").height
- }
-
- ScrollView
- {
- id: credits
- anchors.top: creditsNotes.bottom
- anchors.topMargin: UM.Theme.getSize("default_margin").height
-
- width: parent.width
- height: base.height - y - (2 * UM.Theme.getSize("default_margin").height + closeButton.height)
-
- ListView
- {
- id: projectsList
-
- width: parent.width
-
- delegate: Row
- {
- Label
- {
- text: "%2".arg(model.url).arg(model.name)
- width: (projectsList.width * 0.25) | 0
- elide: Text.ElideRight
- onLinkActivated: Qt.openUrlExternally(link)
- }
- Label
- {
- text: model.description
- elide: Text.ElideRight
- width: (projectsList.width * 0.6) | 0
- }
- Label
- {
- text: model.license
- elide: Text.ElideRight
- width: (projectsList.width * 0.15) | 0
- }
- }
- model: ListModel
- {
- id: projectsModel
- }
- Component.onCompleted:
- {
- projectsModel.append({ name:"Cura", description: catalog.i18nc("@label", "Graphical user interface"), license: "LGPLv3", url: "https://github.com/Ultimaker/Cura" });
- projectsModel.append({ name:"Uranium", description: catalog.i18nc("@label", "Application framework"), license: "LGPLv3", url: "https://github.com/Ultimaker/Uranium" });
- projectsModel.append({ name:"CuraEngine", description: catalog.i18nc("@label", "G-code generator"), license: "AGPLv3", url: "https://github.com/Ultimaker/CuraEngine" });
- projectsModel.append({ name:"libArcus", description: catalog.i18nc("@label", "Interprocess communication library"), license: "LGPLv3", url: "https://github.com/Ultimaker/libArcus" });
-
- projectsModel.append({ name:"Python", description: catalog.i18nc("@label", "Programming language"), license: "Python", url: "http://python.org/" });
- projectsModel.append({ name:"Qt5", description: catalog.i18nc("@label", "GUI framework"), license: "LGPLv3", url: "https://www.qt.io/" });
- projectsModel.append({ name:"PyQt", description: catalog.i18nc("@label", "GUI framework bindings"), license: "GPL", url: "https://riverbankcomputing.com/software/pyqt" });
- projectsModel.append({ name:"SIP", description: catalog.i18nc("@label", "C/C++ Binding library"), license: "GPL", url: "https://riverbankcomputing.com/software/sip" });
- projectsModel.append({ name:"Protobuf", description: catalog.i18nc("@label", "Data interchange format"), license: "BSD", url: "https://developers.google.com/protocol-buffers" });
- projectsModel.append({ name:"SciPy", description: catalog.i18nc("@label", "Support library for scientific computing"), license: "BSD-new", url: "https://www.scipy.org/" });
- projectsModel.append({ name:"NumPy", description: catalog.i18nc("@label", "Support library for faster math"), license: "BSD", url: "http://www.numpy.org/" });
- projectsModel.append({ name:"NumPy-STL", description: catalog.i18nc("@label", "Support library for handling STL files"), license: "BSD", url: "https://github.com/WoLpH/numpy-stl" });
- projectsModel.append({ name:"Shapely", description: catalog.i18nc("@label", "Support library for handling planar objects"), license: "BSD", url: "https://github.com/Toblerity/Shapely" });
- projectsModel.append({ name:"Trimesh", description: catalog.i18nc("@label", "Support library for handling triangular meshes"), license: "MIT", url: "https://trimsh.org" });
- projectsModel.append({ name:"NetworkX", description: catalog.i18nc("@label", "Support library for analysis of complex networks"), license: "3-clause BSD", url: "https://networkx.github.io/" });
- projectsModel.append({ name:"libSavitar", description: catalog.i18nc("@label", "Support library for handling 3MF files"), license: "LGPLv3", url: "https://github.com/ultimaker/libsavitar" });
- projectsModel.append({ name:"libCharon", description: catalog.i18nc("@label", "Support library for file metadata and streaming"), license: "LGPLv3", url: "https://github.com/ultimaker/libcharon" });
- projectsModel.append({ name:"PySerial", description: catalog.i18nc("@label", "Serial communication library"), license: "Python", url: "http://pyserial.sourceforge.net/" });
- projectsModel.append({ name:"python-zeroconf", description: catalog.i18nc("@label", "ZeroConf discovery library"), license: "LGPL", url: "https://github.com/jstasiak/python-zeroconf" });
- projectsModel.append({ name:"Clipper", description: catalog.i18nc("@label", "Polygon clipping library"), license: "Boost", url: "http://www.angusj.com/delphi/clipper.php" });
- projectsModel.append({ name:"Requests", description: catalog.i18nc("@Label", "Python HTTP library"), license: "GPL", url: "http://docs.python-requests.org" });
-
- projectsModel.append({ name:"Noto Sans", description: catalog.i18nc("@label", "Font"), license: "Apache 2.0", url: "https://www.google.com/get/noto/" });
- projectsModel.append({ name:"Font-Awesome-SVG-PNG", description: catalog.i18nc("@label", "SVG icons"), license: "SIL OFL 1.1", url: "https://github.com/encharm/Font-Awesome-SVG-PNG" });
- projectsModel.append({ name:"AppImageKit", description: catalog.i18nc("@label", "Linux cross-distribution application deployment"), license: "MIT", url: "https://github.com/AppImage/AppImageKit" });
- }
- }
- }
-
- rightButtons: Button
- {
- //: Close about dialog button
- id: closeButton
- text: catalog.i18nc("@action:button","Close");
-
- onClicked: base.visible = false;
- }
-}
diff --git a/resources/qml/Account/AccountDetails.qml b/resources/qml/Account/AccountDetails.qml
new file mode 100644
index 0000000000..45f822e41f
--- /dev/null
+++ b/resources/qml/Account/AccountDetails.qml
@@ -0,0 +1,69 @@
+// 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 UM 1.4 as UM
+import Cura 1.1 as Cura
+
+Column
+{
+ property var profile: null
+ property var loggedIn: false
+ property var profileImage: ""
+
+ padding: UM.Theme.getSize("wide_margin").height
+ spacing: UM.Theme.getSize("wide_margin").height
+
+ AvatarImage
+ {
+ id: avatar
+ width: UM.Theme.getSize("avatar_image").width
+ height: UM.Theme.getSize("avatar_image").height
+ anchors.horizontalCenter: parent.horizontalCenter
+ source:
+ {
+ if(loggedIn)
+ {
+ if(profileImage)
+ {
+ return profileImage
+ }
+ return UM.Theme.getImage("avatar_no_user")
+ }
+ return UM.Theme.getImage("avatar_no_user")
+ }
+ outlineColor: loggedIn ? UM.Theme.getColor("account_widget_outline_active") : UM.Theme.getColor("lining")
+ }
+
+ Label
+ {
+ id: information
+ anchors.horizontalCenter: parent.horizontalCenter
+ horizontalAlignment: Text.AlignHCenter
+ renderType: Text.NativeRendering
+ text: loggedIn ? profile["username"] : catalog.i18nc("@label", "Please log in or create an account to\nenjoy all features of Ultimaker Cura.")
+ font: loggedIn ? UM.Theme.getFont("large_bold") : UM.Theme.getFont("default")
+ color: UM.Theme.getColor("text")
+ }
+
+ Loader
+ {
+ id: accountOperations
+ anchors.horizontalCenter: parent.horizontalCenter
+ sourceComponent: loggedIn ? userOperations : generalOperations
+ }
+
+ Component
+ {
+ id: userOperations
+ UserOperations { }
+ }
+
+ Component
+ {
+ id: generalOperations
+ GeneralOperations { }
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/Account/AccountWidget.qml b/resources/qml/Account/AccountWidget.qml
new file mode 100644
index 0000000000..d3bd6fd130
--- /dev/null
+++ b/resources/qml/Account/AccountWidget.qml
@@ -0,0 +1,76 @@
+// 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 UM 1.4 as UM
+import Cura 1.1 as Cura
+
+Button
+{
+ id: accountWidget
+ property var profile: Cura.API.account.userProfile
+ property var loggedIn: Cura.API.account.isLoggedIn
+
+ implicitHeight: UM.Theme.getSize("main_window_header").height
+ implicitWidth: UM.Theme.getSize("main_window_header").height
+
+ background: AvatarImage
+ {
+ id: avatar
+
+ width: Math.round(0.8 * accountWidget.width)
+ height: Math.round(0.8 * accountWidget.height)
+ anchors.verticalCenter: accountWidget.verticalCenter
+ anchors.horizontalCenter: accountWidget.horizontalCenter
+
+ source:
+ {
+ if(loggedIn)
+ {
+ if(profile["profile_image_url"])
+ {
+ return profile["profile_image_url"]
+ }
+ return UM.Theme.getImage("avatar_no_user")
+ }
+ return UM.Theme.getImage("avatar_no_user")
+ }
+ outlineColor: loggedIn ? UM.Theme.getColor("account_widget_outline_active") : UM.Theme.getColor("lining")
+ }
+
+ onClicked: popup.opened ? popup.close() : popup.open()
+
+ Popup
+ {
+ id: popup
+
+ y: parent.height + UM.Theme.getSize("default_arrow").height
+ x: parent.width - width
+
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
+
+ opacity: opened ? 1 : 0
+ Behavior on opacity { NumberAnimation { duration: 100 } }
+
+ contentItem: AccountDetails
+ {
+ id: panel
+ profile: Cura.API.account.userProfile
+ loggedIn: Cura.API.account.isLoggedIn
+ profileImage: Cura.API.account.profileImageUrl
+ }
+
+ background: UM.PointingRectangle
+ {
+ color: UM.Theme.getColor("tool_panel_background")
+ borderColor: UM.Theme.getColor("lining")
+ borderWidth: UM.Theme.getSize("default_lining").width
+
+ target: Qt.point(width - (accountWidget.width / 2), -10)
+
+ arrowSize: UM.Theme.getSize("default_arrow").width
+ }
+ }
+}
diff --git a/resources/qml/Account/AvatarImage.qml b/resources/qml/Account/AvatarImage.qml
new file mode 100644
index 0000000000..b76aff6990
--- /dev/null
+++ b/resources/qml/Account/AvatarImage.qml
@@ -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 QtGraphicalEffects 1.0
+
+import UM 1.4 as UM
+
+Item
+{
+ // This item shows the provided image while applying a round mask to it.
+ // It also shows a round border around it. The color is defined by the outlineColor property.
+
+ id: avatar
+
+ property alias source: profileImage.source
+ property alias outlineColor: profileImageOutline.color
+
+ Image
+ {
+ id: profileImage
+ anchors.fill: parent
+ source: UM.Theme.getImage("avatar_default")
+ fillMode: Image.PreserveAspectCrop
+ visible: false
+ mipmap: true
+ }
+
+ Rectangle
+ {
+ id: profileImageMask
+ anchors.fill: parent
+ radius: width
+ }
+
+ OpacityMask
+ {
+ anchors.fill: parent
+ source: profileImage
+ maskSource: profileImageMask
+ cached: true
+ }
+
+ UM.RecolorImage
+ {
+ id: profileImageOutline
+ anchors.centerIn: parent
+ // Make it a bit bigger than it has to, otherwise it sometimes shows a white border.
+ width: parent.width + 2
+ height: parent.height + 2
+ source: UM.Theme.getIcon("circle_outline")
+ sourceSize: Qt.size(parent.width, parent.height)
+ color: UM.Theme.getColor("account_widget_ouline_active")
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/Account/GeneralOperations.qml b/resources/qml/Account/GeneralOperations.qml
new file mode 100644
index 0000000000..b9f1025d5e
--- /dev/null
+++ b/resources/qml/Account/GeneralOperations.qml
@@ -0,0 +1,31 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.2
+import QtQuick.Controls 1.1
+
+import UM 1.4 as UM
+import Cura 1.1 as Cura
+
+Row
+{
+ spacing: UM.Theme.getSize("default_margin").width
+
+ Cura.SecondaryButton
+ {
+ width: UM.Theme.getSize("account_button").width
+ height: UM.Theme.getSize("account_button").height
+ text: catalog.i18nc("@button", "Create account")
+ onClicked: Qt.openUrlExternally("https://account.ultimaker.com/app/create")
+ fixedWidthMode: true
+ }
+
+ Cura.PrimaryButton
+ {
+ width: UM.Theme.getSize("account_button").width
+ height: UM.Theme.getSize("account_button").height
+ text: catalog.i18nc("@button", "Login")
+ onClicked: Cura.API.account.login()
+ fixedWidthMode: true
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/Account/UserOperations.qml b/resources/qml/Account/UserOperations.qml
new file mode 100644
index 0000000000..b9ffa395d6
--- /dev/null
+++ b/resources/qml/Account/UserOperations.qml
@@ -0,0 +1,31 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.2
+import QtQuick.Controls 1.1
+
+import UM 1.4 as UM
+import Cura 1.1 as Cura
+
+Row
+{
+ spacing: UM.Theme.getSize("default_margin").width
+
+ Cura.SecondaryButton
+ {
+ width: UM.Theme.getSize("account_button").width
+ height: UM.Theme.getSize("account_button").height
+ text: catalog.i18nc("@button", "Manage account")
+ onClicked: Qt.openUrlExternally("https://account.ultimaker.com")
+ fixedWidthMode: true
+ }
+
+ Cura.PrimaryButton
+ {
+ width: UM.Theme.getSize("account_button").width
+ height: UM.Theme.getSize("account_button").height
+ text: catalog.i18nc("@button", "Logout")
+ onClicked: Cura.API.account.logout()
+ fixedWidthMode: true
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/ActionButton.qml b/resources/qml/ActionButton.qml
new file mode 100644
index 0000000000..fabdcebc64
--- /dev/null
+++ b/resources/qml/ActionButton.qml
@@ -0,0 +1,134 @@
+// 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 QtGraphicalEffects 1.0 // For the dropshadow
+
+import UM 1.1 as UM
+import Cura 1.0 as Cura
+
+
+Button
+{
+ id: button
+ property bool isIconOnRightSide: false
+
+ property alias iconSource: buttonIconLeft.source
+ property alias textFont: buttonText.font
+ property alias cornerRadius: backgroundRect.radius
+ property alias tooltip: tooltip.tooltipText
+ property alias cornerSide: backgroundRect.cornerSide
+
+ property color color: UM.Theme.getColor("primary")
+ property color hoverColor: UM.Theme.getColor("primary_hover")
+ property color disabledColor: color
+ property color textColor: UM.Theme.getColor("button_text")
+ property color textHoverColor: textColor
+ property color textDisabledColor: textColor
+ property color outlineColor: color
+ property color outlineHoverColor: hoverColor
+ property color outlineDisabledColor: outlineColor
+ property alias shadowColor: shadow.color
+ property alias shadowEnabled: shadow.visible
+ property alias busy: busyIndicator.visible
+
+ property alias toolTipContentAlignment: tooltip.contentAlignment
+
+ // This property is used to indicate whether the button has a fixed width or the width would depend on the contents
+ // Be careful when using fixedWidthMode, the translated texts can be too long that they won't fit. In any case,
+ // we elide the text to the right so the text will be cut off with the three dots at the end.
+ property var fixedWidthMode: false
+
+ leftPadding: UM.Theme.getSize("default_margin").width
+ rightPadding: UM.Theme.getSize("default_margin").width
+ height: UM.Theme.getSize("action_button").height
+ hoverEnabled: true
+
+ contentItem: Row
+ {
+ spacing: UM.Theme.getSize("narrow_margin").width
+ //Left side icon. Only displayed if !isIconOnRightSide.
+ UM.RecolorImage
+ {
+ id: buttonIconLeft
+ source: ""
+ height: UM.Theme.getSize("action_button_icon").height
+ width: visible ? height : 0
+ sourceSize.width: width
+ sourceSize.height: height
+ color: button.enabled ? (button.hovered ? button.textHoverColor : button.textColor) : button.textDisabledColor
+ visible: source != "" && !button.isIconOnRightSide
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Label
+ {
+ id: buttonText
+ text: button.text
+ color: button.enabled ? (button.hovered ? button.textHoverColor : button.textColor): button.textDisabledColor
+ font: UM.Theme.getFont("medium")
+ visible: text != ""
+ renderType: Text.NativeRendering
+ anchors.verticalCenter: parent.verticalCenter
+ width: fixedWidthMode ? button.width - button.leftPadding - button.rightPadding : undefined
+ horizontalAlignment: Text.AlignHCenter
+ elide: Text.ElideRight
+ }
+
+ //Right side icon. Only displayed if isIconOnRightSide.
+ UM.RecolorImage
+ {
+ id: buttonIconRight
+ source: buttonIconLeft.source
+ height: UM.Theme.getSize("action_button_icon").height
+ width: visible ? height : 0
+ sourceSize.width: width
+ sourceSize.height: height
+ color: buttonIconLeft.color
+ visible: source != "" && button.isIconOnRightSide
+ anchors.verticalCenter: buttonIconLeft.verticalCenter
+ }
+ }
+
+ background: Cura.RoundedRectangle
+ {
+ id: backgroundRect
+ cornerSide: Cura.RoundedRectangle.Direction.All
+ color: button.enabled ? (button.hovered ? button.hoverColor : button.color) : button.disabledColor
+ radius: UM.Theme.getSize("action_button_radius").width
+ border.width: UM.Theme.getSize("default_lining").width
+ border.color: button.enabled ? (button.hovered ? button.outlineHoverColor : button.outlineColor) : button.outlineDisabledColor
+ }
+
+ DropShadow
+ {
+ id: shadow
+ // Don't blur the shadow
+ radius: 0
+ anchors.fill: backgroundRect
+ source: backgroundRect
+ verticalOffset: 2
+ visible: false
+ // Should always be drawn behind the background.
+ z: backgroundRect.z - 1
+ }
+
+ Cura.ToolTip
+ {
+ id: tooltip
+ visible: button.hovered
+ }
+
+ BusyIndicator
+ {
+ id: busyIndicator
+
+ anchors.centerIn: parent
+
+ width: height
+ height: parent.height
+
+ visible: false
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/ActionPanel/ActionPanelWidget.qml b/resources/qml/ActionPanel/ActionPanelWidget.qml
new file mode 100644
index 0000000000..1d9ee95548
--- /dev/null
+++ b/resources/qml/ActionPanel/ActionPanelWidget.qml
@@ -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.Layouts 1.3
+
+import UM 1.2 as UM
+import Cura 1.0 as Cura
+
+
+// This element hold all the elements needed for the user to trigger the slicing process, and later
+// to get information about the printing times, material consumption and the output process (such as
+// saving to a file, printing over network, ...
+Rectangle
+{
+ id: actionPanelWidget
+
+ width: UM.Theme.getSize("action_panel_widget").width
+ height: childrenRect.height + 2 * UM.Theme.getSize("thick_margin").height
+
+ color: UM.Theme.getColor("main_background")
+ border.width: UM.Theme.getSize("default_lining").width
+ border.color: UM.Theme.getColor("lining")
+ radius: UM.Theme.getSize("default_radius").width
+ z: 10
+
+ property bool outputAvailable: UM.Backend.state == UM.Backend.Done || UM.Backend.state == UM.Backend.Disabled
+
+ Loader
+ {
+ id: loader
+ anchors
+ {
+ top: parent.top
+ topMargin: UM.Theme.getSize("thick_margin").height
+ left: parent.left
+ leftMargin: UM.Theme.getSize("thick_margin").width
+ right: parent.right
+ rightMargin: UM.Theme.getSize("thick_margin").width
+ }
+ sourceComponent: outputAvailable ? outputProcessWidget : sliceProcessWidget
+ }
+
+ Component
+ {
+ id: sliceProcessWidget
+ SliceProcessWidget { }
+ }
+
+ Component
+ {
+ id: outputProcessWidget
+ OutputProcessWidget { }
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/ActionPanel/OutputDevicesActionButton.qml b/resources/qml/ActionPanel/OutputDevicesActionButton.qml
new file mode 100644
index 0000000000..3bfaab0fc1
--- /dev/null
+++ b/resources/qml/ActionPanel/OutputDevicesActionButton.qml
@@ -0,0 +1,112 @@
+// 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
+import Cura 1.0 as Cura
+
+Item
+{
+ id: widget
+
+ function requestWriteToDevice()
+ {
+ UM.OutputDeviceManager.requestWriteToDevice(UM.OutputDeviceManager.activeDevice, PrintInformation.jobName,
+ { "filter_by_machine": true, "preferred_mimetypes": Cura.MachineManager.activeMachine.preferred_output_file_formats });
+ }
+
+ Cura.PrimaryButton
+ {
+ id: saveToButton
+ height: parent.height
+ fixedWidthMode: true
+ cornerSide: deviceSelectionMenu.visible ? Cura.RoundedRectangle.Direction.Left : Cura.RoundedRectangle.Direction.All
+
+ anchors
+ {
+ top: parent.top
+ left: parent.left
+ right: deviceSelectionMenu.visible ? deviceSelectionMenu.left : parent.right
+ }
+
+ tooltip: UM.OutputDeviceManager.activeDeviceDescription
+
+ text: UM.OutputDeviceManager.activeDeviceShortDescription
+
+ onClicked:
+ {
+ forceActiveFocus()
+ widget.requestWriteToDevice()
+ }
+ }
+
+ Cura.ActionButton
+ {
+ id: deviceSelectionMenu
+ height: parent.height
+
+ shadowEnabled: true
+ shadowColor: UM.Theme.getColor("primary_shadow")
+ cornerSide: Cura.RoundedRectangle.Direction.Right
+
+ anchors
+ {
+ top: parent.top
+ right: parent.right
+ }
+
+ leftPadding: UM.Theme.getSize("narrow_margin").width //Need more space than usual here for wide text.
+ rightPadding: UM.Theme.getSize("narrow_margin").width
+ iconSource: popup.opened ? UM.Theme.getIcon("arrow_top") : UM.Theme.getIcon("arrow_bottom")
+ color: UM.Theme.getColor("action_panel_secondary")
+ visible: (devicesModel.deviceCount > 1)
+
+ onClicked: popup.opened ? popup.close() : popup.open()
+
+ Popup
+ {
+ id: popup
+ padding: 0
+
+ y: -height
+ x: parent.width - width
+
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
+
+ contentItem: ColumnLayout
+ {
+ Repeater
+ {
+ model: devicesModel
+
+ delegate: Cura.ActionButton
+ {
+ text: model.description
+ visible: model.id != UM.OutputDeviceManager.activeDevice // Don't show the active device in the list
+ color: "transparent"
+ cornerRadius: 0
+ hoverColor: UM.Theme.getColor("primary")
+ Layout.fillWidth: true
+ onClicked:
+ {
+ UM.OutputDeviceManager.setActiveDevice(model.id)
+ popup.close()
+ }
+ }
+ }
+ }
+
+ background: Rectangle
+ {
+ opacity: visible ? 1 : 0
+ Behavior on opacity { NumberAnimation { duration: 100 } }
+ color: UM.Theme.getColor("action_panel_secondary")
+ }
+ }
+ }
+
+ UM.OutputDevicesModel { id: devicesModel }
+}
\ No newline at end of file
diff --git a/resources/qml/ActionPanel/OutputProcessWidget.qml b/resources/qml/ActionPanel/OutputProcessWidget.qml
new file mode 100644
index 0000000000..63974d7f34
--- /dev/null
+++ b/resources/qml/ActionPanel/OutputProcessWidget.qml
@@ -0,0 +1,135 @@
+// 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
+import Cura 1.0 as Cura
+
+
+// This element contains all the elements the user needs to visualize the data
+// that is gather after the slicing process, such as printint time, material usage, ...
+// There are also two buttons: one to previsualize the output layers, and the other to
+// select what to do with it (such as print over network, save to file, ...)
+Column
+{
+ id: widget
+
+ spacing: UM.Theme.getSize("thin_margin").height
+ property bool preSlicedData: PrintInformation.preSliced
+
+ UM.I18nCatalog
+ {
+ id: catalog
+ name: "cura"
+ }
+
+ Item
+ {
+ id: information
+ width: parent.width
+ height: childrenRect.height
+
+ PrintInformationWidget
+ {
+ id: printInformationPanel
+ visible: !preSlicedData
+ anchors.right: parent.right
+ }
+
+ Column
+ {
+ id: timeAndCostsInformation
+ spacing: UM.Theme.getSize("thin_margin").height
+
+ anchors
+ {
+ left: parent.left
+ right: parent.right
+ }
+
+ Cura.IconWithText
+ {
+ id: estimatedTime
+ width: parent.width
+
+ text: preSlicedData ? catalog.i18nc("@label", "No time estimation available") : PrintInformation.currentPrintTime.getDisplayString(UM.DurationFormat.Long)
+ source: UM.Theme.getIcon("clock")
+ font: UM.Theme.getFont("medium_bold")
+ }
+
+ Cura.IconWithText
+ {
+ id: estimatedCosts
+ width: parent.width
+
+ property var printMaterialLengths: PrintInformation.materialLengths
+ property var printMaterialWeights: PrintInformation.materialWeights
+
+ text:
+ {
+ if (preSlicedData)
+ {
+ return catalog.i18nc("@label", "No cost estimation available")
+ }
+ var totalLengths = 0
+ var totalWeights = 0
+ if (printMaterialLengths)
+ {
+ for(var index = 0; index < printMaterialLengths.length; index++)
+ {
+ if(printMaterialLengths[index] > 0)
+ {
+ totalLengths += printMaterialLengths[index]
+ totalWeights += Math.round(printMaterialWeights[index])
+ }
+ }
+ }
+ return totalWeights + "g · " + totalLengths.toFixed(2) + "m"
+ }
+ source: UM.Theme.getIcon("spool")
+ }
+ }
+ }
+
+ Item
+ {
+ id: buttonRow
+ anchors.right: parent.right
+ anchors.left: parent.left
+ height: UM.Theme.getSize("action_button").height
+
+ Cura.SecondaryButton
+ {
+ id: previewStageShortcut
+
+ anchors
+ {
+ left: parent.left
+ right: outputDevicesButton.left
+ rightMargin: UM.Theme.getSize("default_margin").width
+ }
+
+ height: UM.Theme.getSize("action_button").height
+ text: catalog.i18nc("@button", "Preview")
+ tooltip: text
+ fixedWidthMode: true
+
+ toolTipContentAlignment: Cura.ToolTip.ContentAlignment.AlignLeft
+
+ onClicked: UM.Controller.setActiveStage("PreviewStage")
+ visible: UM.Controller.activeStage != null && UM.Controller.activeStage.stageId != "PreviewStage"
+ }
+
+ Cura.OutputDevicesActionButton
+ {
+ id: outputDevicesButton
+
+ anchors.right: parent.right
+ width: previewStageShortcut.visible ? UM.Theme.getSize("action_button").width : parent.width
+ height: UM.Theme.getSize("action_button").height
+ }
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/ActionPanel/PrintInformationWidget.qml b/resources/qml/ActionPanel/PrintInformationWidget.qml
new file mode 100644
index 0000000000..2e108b05d7
--- /dev/null
+++ b/resources/qml/ActionPanel/PrintInformationWidget.qml
@@ -0,0 +1,58 @@
+// 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 UM 1.1 as UM
+import Cura 1.0 as Cura
+
+UM.RecolorImage
+{
+ id: widget
+
+ source: UM.Theme.getIcon("info")
+ width: visible ? UM.Theme.getSize("section_icon").width : 0
+ height: UM.Theme.getSize("section_icon").height
+
+ color: UM.Theme.getColor("icon")
+
+ MouseArea
+ {
+ anchors.fill: parent
+ hoverEnabled: true
+ onEntered: popup.open()
+ onExited: popup.close()
+ }
+
+ Popup
+ {
+ id: popup
+
+ y: -(height + UM.Theme.getSize("default_arrow").height + UM.Theme.getSize("thin_margin").height)
+ x: parent.width - width + UM.Theme.getSize("thin_margin").width
+
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
+
+ opacity: opened ? 1 : 0
+ Behavior on opacity { NumberAnimation { duration: 100 } }
+
+ contentItem: PrintJobInformation
+ {
+ id: printJobInformation
+ width: UM.Theme.getSize("action_panel_information_widget").width
+ }
+
+ background: UM.PointingRectangle
+ {
+ color: UM.Theme.getColor("tool_panel_background")
+ borderColor: UM.Theme.getColor("lining")
+ borderWidth: UM.Theme.getSize("default_lining").width
+
+ target: Qt.point(width - (widget.width / 2) - UM.Theme.getSize("thin_margin").width,
+ height + UM.Theme.getSize("default_arrow").height - UM.Theme.getSize("thin_margin").height)
+
+ arrowSize: UM.Theme.getSize("default_arrow").width
+ }
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/ActionPanel/PrintJobInformation.qml b/resources/qml/ActionPanel/PrintJobInformation.qml
new file mode 100644
index 0000000000..8bd5d5a0d3
--- /dev/null
+++ b/resources/qml/ActionPanel/PrintJobInformation.qml
@@ -0,0 +1,159 @@
+// 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 UM 1.1 as UM
+import Cura 1.0 as Cura
+
+Column
+{
+ id: base
+ spacing: UM.Theme.getSize("default_margin").width
+
+ UM.I18nCatalog
+ {
+ id: catalog
+ name: "cura"
+ }
+
+ Column
+ {
+ id: timeSpecification
+ width: parent.width
+ topPadding: UM.Theme.getSize("default_margin").height
+ leftPadding: UM.Theme.getSize("default_margin").width
+ rightPadding: UM.Theme.getSize("default_margin").width
+
+ Label
+ {
+ text: catalog.i18nc("@label", "Time specification").toUpperCase()
+ color: UM.Theme.getColor("primary")
+ font: UM.Theme.getFont("default_bold")
+ renderType: Text.NativeRendering
+ }
+
+ Label
+ {
+ property var printDuration: PrintInformation.currentPrintTime
+
+ text:
+ {
+ // All the time information for the different features is achieved
+ var printTime = PrintInformation.getFeaturePrintTimes()
+ var totalSeconds = parseInt(printDuration.getDisplayString(UM.DurationFormat.Seconds))
+
+ // A message is created and displayed when the user hover the time label
+ var text = "
"
+ for(var feature in printTime)
+ {
+ if(!printTime[feature].isTotalDurationZero)
+ {
+ text += "
Print with the recommended settings for the selected printer, material and quality."),
- item: sidebarSimple
- })
- modesListModel.append({
- text: catalog.i18nc("@title:tab", "Custom"),
- tooltipText: catalog.i18nc("@tooltip", "Custom Print Setup
Print with finegrained control over every last bit of the slicing process."),
- item: sidebarAdvanced
- })
-
- var index = Math.round(UM.Preferences.getValue("cura/active_mode"))
-
- if(index != null && !isNaN(index))
- {
- currentModeIndex = index;
- }
- else
- {
- currentModeIndex = 0;
- }
- }
-
- UM.SettingPropertyProvider
- {
- id: machineExtruderCount
-
- containerStack: Cura.MachineManager.activeMachine
- key: "machine_extruder_count"
- watchedProperties: [ "value" ]
- storeIndex: 0
- }
-
- UM.SettingPropertyProvider
- {
- id: machineHeatedBed
-
- containerStack: Cura.MachineManager.activeMachine
- key: "machine_heated_bed"
- watchedProperties: [ "value" ]
- storeIndex: 0
- }
-}
diff --git a/resources/qml/PrimaryButton.qml b/resources/qml/PrimaryButton.qml
new file mode 100644
index 0000000000..fca63d2cdb
--- /dev/null
+++ b/resources/qml/PrimaryButton.qml
@@ -0,0 +1,20 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.2
+
+import UM 1.4 as UM
+import Cura 1.1 as Cura
+
+
+Cura.ActionButton
+{
+ shadowEnabled: true
+ shadowColor: enabled ? UM.Theme.getColor("primary_button_shadow"): UM.Theme.getColor("action_button_disabled_shadow")
+ color: UM.Theme.getColor("primary_button")
+ textColor: UM.Theme.getColor("primary_button_text")
+ outlineColor: "transparent"
+ disabledColor: UM.Theme.getColor("action_button_disabled")
+ textDisabledColor: UM.Theme.getColor("action_button_disabled_text")
+ hoverColor: UM.Theme.getColor("primary_button_hover")
+}
\ No newline at end of file
diff --git a/resources/qml/PrintMonitor.qml b/resources/qml/PrintMonitor.qml
index 12e95d1e89..d44acf0adb 100644
--- a/resources/qml/PrintMonitor.qml
+++ b/resources/qml/PrintMonitor.qml
@@ -11,136 +11,176 @@ import Cura 1.0 as Cura
import "PrinterOutput"
-Column
+
+Item
{
- id: printMonitor
+ id: base
+ UM.I18nCatalog { id: catalog; name: "cura"}
+
+ function showTooltip(item, position, text)
+ {
+ tooltip.text = text;
+ position = item.mapToItem(base, position.x - UM.Theme.getSize("default_arrow").width, position.y);
+ tooltip.show(position);
+ }
+
+ function hideTooltip()
+ {
+ tooltip.hide();
+ }
+
+ function strPadLeft(string, pad, length) {
+ return (new Array(length + 1).join(pad) + string).slice(-length);
+ }
+
+ function getPrettyTime(time)
+ {
+ var hours = Math.floor(time / 3600)
+ time -= hours * 3600
+ var minutes = Math.floor(time / 60);
+ time -= minutes * 60
+ var seconds = Math.floor(time);
+
+ var finalTime = strPadLeft(hours, "0", 2) + ":" + strPadLeft(minutes, "0", 2) + ":" + strPadLeft(seconds, "0", 2);
+ return finalTime;
+ }
+
property var connectedDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null
property var activePrinter: connectedDevice != null ? connectedDevice.activePrinter : null
property var activePrintJob: activePrinter != null ? activePrinter.activePrintJob: null
- Cura.ExtrudersModel
+ PrintSetupTooltip
{
- id: extrudersModel
- simpleNames: true
+ id: tooltip
}
- OutputDeviceHeader
+ Column
{
- outputDevice: connectedDevice
- }
+ id: printMonitor
- Rectangle
- {
- color: UM.Theme.getColor("sidebar_lining")
- width: parent.width
- height: childrenRect.height
+ anchors.fill: parent
- Flow
+ property var extrudersModel: CuraApplication.getExtrudersModel()
+
+ OutputDeviceHeader
{
- id: extrudersGrid
- spacing: UM.Theme.getSize("sidebar_lining_thin").width
+ outputDevice: connectedDevice
+ }
+
+ Rectangle
+ {
+ color: UM.Theme.getColor("wide_lining")
width: parent.width
+ height: childrenRect.height
- Repeater
+ Flow
{
- id: extrudersRepeater
- model: activePrinter != null ? activePrinter.extruders : null
+ id: extrudersGrid
+ spacing: UM.Theme.getSize("thick_lining").width
+ width: parent.width
- ExtruderBox
+ Repeater
{
- color: UM.Theme.getColor("sidebar")
- width: index == machineExtruderCount.properties.value - 1 && index % 2 == 0 ? extrudersGrid.width : Math.round(extrudersGrid.width / 2 - UM.Theme.getSize("sidebar_lining_thin").width / 2)
- extruderModel: modelData
+ id: extrudersRepeater
+ model: activePrinter != null ? activePrinter.extruders : null
+
+ ExtruderBox
+ {
+ color: UM.Theme.getColor("main_background")
+ width: index == machineExtruderCount.properties.value - 1 && index % 2 == 0 ? extrudersGrid.width : Math.round(extrudersGrid.width / 2 - UM.Theme.getSize("thick_lining").width / 2)
+ extruderModel: modelData
+ }
}
}
}
- }
- Rectangle
- {
- color: UM.Theme.getColor("sidebar_lining")
- width: parent.width
- height: UM.Theme.getSize("sidebar_lining_thin").width
- }
-
- HeatedBedBox
- {
- visible: {
- if(activePrinter != null && activePrinter.bedTemperature != -1)
- {
- return true
- }
- return false
- }
- printerModel: activePrinter
- }
-
- UM.SettingPropertyProvider
- {
- id: bedTemperature
- containerStack: Cura.MachineManager.activeMachine
- key: "material_bed_temperature"
- watchedProperties: ["value", "minimum_value", "maximum_value", "resolve"]
- storeIndex: 0
-
- property var resolve: Cura.MachineManager.activeStack != Cura.MachineManager.activeMachine ? properties.resolve : "None"
- }
-
- UM.SettingPropertyProvider
- {
- id: machineExtruderCount
- containerStack: Cura.MachineManager.activeMachine
- key: "machine_extruder_count"
- watchedProperties: ["value"]
- }
-
- ManualPrinterControl
- {
- printerModel: activePrinter
- visible: activePrinter != null ? activePrinter.canControlManually : false
- }
-
-
- MonitorSection
- {
- label: catalog.i18nc("@label", "Active print")
- width: base.width
- visible: activePrinter != null
- }
-
-
- MonitorItem
- {
- label: catalog.i18nc("@label", "Job Name")
- value: activePrintJob != null ? activePrintJob.name : ""
- width: base.width
- visible: activePrinter != null
- }
-
- MonitorItem
- {
- label: catalog.i18nc("@label", "Printing Time")
- value: activePrintJob != null ? getPrettyTime(activePrintJob.timeTotal) : ""
- width: base.width
- visible: activePrinter != null
- }
-
- MonitorItem
- {
- label: catalog.i18nc("@label", "Estimated time left")
- value: activePrintJob != null ? getPrettyTime(activePrintJob.timeTotal - activePrintJob.timeElapsed) : ""
- visible:
+ Rectangle
{
- if(activePrintJob == null)
+ color: UM.Theme.getColor("wide_lining")
+ width: parent.width
+ height: UM.Theme.getSize("thick_lining").width
+ }
+
+ HeatedBedBox
+ {
+ visible:
{
+ if(activePrinter != null && activePrinter.bedTemperature != -1)
+ {
+ return true
+ }
return false
}
-
- return (activePrintJob.state == "printing" ||
- activePrintJob.state == "resuming" ||
- activePrintJob.state == "pausing" ||
- activePrintJob.state == "paused")
+ printerModel: activePrinter
+ }
+
+ UM.SettingPropertyProvider
+ {
+ id: bedTemperature
+ containerStack: Cura.MachineManager.activeMachine
+ key: "material_bed_temperature"
+ watchedProperties: ["value", "minimum_value", "maximum_value", "resolve"]
+ storeIndex: 0
+
+ property var resolve: Cura.MachineManager.activeStack != Cura.MachineManager.activeMachine ? properties.resolve : "None"
+ }
+
+ UM.SettingPropertyProvider
+ {
+ id: machineExtruderCount
+ containerStack: Cura.MachineManager.activeMachine
+ key: "machine_extruder_count"
+ watchedProperties: ["value"]
+ }
+
+ ManualPrinterControl
+ {
+ printerModel: activePrinter
+ visible: activePrinter != null ? activePrinter.canControlManually : false
+ }
+
+
+ MonitorSection
+ {
+ label: catalog.i18nc("@label", "Active print")
+ width: base.width
+ visible: activePrinter != null
+ }
+
+
+ MonitorItem
+ {
+ label: catalog.i18nc("@label", "Job Name")
+ value: activePrintJob != null ? activePrintJob.name : ""
+ width: base.width
+ visible: activePrinter != null
+ }
+
+ MonitorItem
+ {
+ label: catalog.i18nc("@label", "Printing Time")
+ value: activePrintJob != null ? getPrettyTime(activePrintJob.timeTotal) : ""
+ width: base.width
+ visible: activePrinter != null
+ }
+
+ MonitorItem
+ {
+ label: catalog.i18nc("@label", "Estimated time left")
+ value: activePrintJob != null ? getPrettyTime(activePrintJob.timeTotal - activePrintJob.timeElapsed) : ""
+ visible:
+ {
+ if(activePrintJob == null)
+ {
+ return false
+ }
+
+ return (activePrintJob.state == "printing" ||
+ activePrintJob.state == "resuming" ||
+ activePrintJob.state == "pausing" ||
+ activePrintJob.state == "paused")
+ }
+ width: base.width
}
- width: base.width
}
-}
+}
\ No newline at end of file
diff --git a/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml b/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml
new file mode 100644
index 0000000000..98bb5c0405
--- /dev/null
+++ b/resources/qml/PrintSetupSelector/Custom/CustomPrintSetup.qml
@@ -0,0 +1,131 @@
+// 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.0
+
+import UM 1.3 as UM
+import Cura 1.0 as Cura
+
+
+Item
+{
+ id: customPrintSetup
+
+ property real padding: UM.Theme.getSize("default_margin").width
+ property bool multipleExtruders: extrudersModel.count > 1
+
+ property var extrudersModel: CuraApplication.getExtrudersModel()
+
+ // Profile selector row
+ GlobalProfileSelector
+ {
+ id: globalProfileRow
+ anchors
+ {
+ top: parent.top
+ topMargin: parent.padding
+ left: parent.left
+ leftMargin: parent.padding
+ right: parent.right
+ rightMargin: parent.padding
+ }
+ }
+
+ UM.TabRow
+ {
+ id: tabBar
+
+ visible: multipleExtruders // The tab row is only visible when there are more than 1 extruder
+
+ anchors
+ {
+ top: globalProfileRow.bottom
+ topMargin: UM.Theme.getSize("default_margin").height
+ left: parent.left
+ leftMargin: parent.padding
+ right: parent.right
+ rightMargin: parent.padding
+ }
+
+ Repeater
+ {
+ id: repeater
+ model: extrudersModel
+ delegate: UM.TabRowButton
+ {
+ contentItem: Item
+ {
+ Cura.ExtruderIcon
+ {
+ anchors.horizontalCenter: parent.horizontalCenter
+ materialColor: model.color
+ extruderEnabled: model.enabled
+ }
+ }
+ onClicked:
+ {
+ Cura.ExtruderManager.setActiveExtruderIndex(tabBar.currentIndex)
+ }
+ }
+ }
+
+ //When active extruder changes for some other reason, switch tabs.
+ //Don't directly link currentIndex to Cura.ExtruderManager.activeExtruderIndex!
+ //This causes a segfault in Qt 5.11. Something with VisualItemModel removing index -1. We have to use setCurrentIndex instead.
+ Connections
+ {
+ target: Cura.ExtruderManager
+ onActiveExtruderChanged:
+ {
+ tabBar.setCurrentIndex(Cura.ExtruderManager.activeExtruderIndex);
+ }
+ }
+
+ //When the model of the extruders is rebuilt, the list of extruders is briefly emptied and rebuilt.
+ //This causes the currentIndex of the tab to be in an invalid position which resets it to 0.
+ //Therefore we need to change it back to what it was: The active extruder index.
+ Connections
+ {
+ target: repeater.model
+ onModelChanged:
+ {
+ tabBar.setCurrentIndex(Cura.ExtruderManager.activeExtruderIndex)
+ }
+ }
+ }
+
+ Rectangle
+ {
+ anchors
+ {
+ top: tabBar.visible ? tabBar.bottom : globalProfileRow.bottom
+ topMargin: -UM.Theme.getSize("default_lining").width
+ left: parent.left
+ leftMargin: parent.padding
+ right: parent.right
+ rightMargin: parent.padding
+ bottom: parent.bottom
+ }
+ z: tabBar.z - 1
+ // Don't show the border when only one extruder
+
+ border.color: tabBar.visible ? UM.Theme.getColor("lining") : "transparent"
+ border.width: UM.Theme.getSize("default_lining").width
+
+ color: UM.Theme.getColor("main_background")
+ Cura.SettingView
+ {
+ anchors
+ {
+ fill: parent
+ topMargin: UM.Theme.getSize("default_margin").height
+ leftMargin: UM.Theme.getSize("default_margin").width
+ // Small space for the scrollbar
+ rightMargin: UM.Theme.getSize("narrow_margin").width
+ // Compensate for the negative margin in the parent
+ bottomMargin: UM.Theme.getSize("default_lining").width
+ }
+ }
+ }
+}
diff --git a/resources/qml/PrintSetupSelector/Custom/GlobalProfileSelector.qml b/resources/qml/PrintSetupSelector/Custom/GlobalProfileSelector.qml
new file mode 100644
index 0000000000..32c07a52a6
--- /dev/null
+++ b/resources/qml/PrintSetupSelector/Custom/GlobalProfileSelector.qml
@@ -0,0 +1,100 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 1.1
+import QtQuick.Controls.Styles 1.1
+import QtQuick.Layouts 1.2
+
+import UM 1.2 as UM
+import Cura 1.0 as Cura
+
+Item
+{
+ id: globalProfileRow
+ height: childrenRect.height
+
+ Label
+ {
+ id: globalProfileLabel
+ anchors
+ {
+ top: parent.top
+ bottom: parent.bottom
+ left: parent.left
+ right: globalProfileSelection.left
+ }
+ text: catalog.i18nc("@label", "Profile")
+ font: UM.Theme.getFont("medium")
+ color: UM.Theme.getColor("text")
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ ToolButton
+ {
+ id: globalProfileSelection
+
+ text: generateActiveQualityText()
+ width: UM.Theme.getSize("print_setup_big_item").width
+ height: UM.Theme.getSize("print_setup_big_item").height
+ anchors
+ {
+ top: parent.top
+ right: parent.right
+ }
+ tooltip: Cura.MachineManager.activeQualityOrQualityChangesName
+ style: UM.Theme.styles.print_setup_header_button
+ activeFocusOnPress: true
+ menu: Cura.ProfileMenu { }
+
+ function generateActiveQualityText()
+ {
+ var result = Cura.MachineManager.activeQualityOrQualityChangesName
+ if (Cura.MachineManager.isActiveQualityExperimental)
+ {
+ result += " (Experimental)"
+ }
+
+ if (Cura.MachineManager.isActiveQualitySupported)
+ {
+ if (Cura.MachineManager.activeQualityLayerHeight > 0)
+ {
+ result += " "
+ result += " - "
+ result += Cura.MachineManager.activeQualityLayerHeight + "mm"
+ result += ""
+ }
+ }
+
+ return result
+ }
+
+ UM.SimpleButton
+ {
+ id: customisedSettings
+
+ visible: Cura.MachineManager.hasUserSettings
+ width: UM.Theme.getSize("print_setup_icon").width
+ height: UM.Theme.getSize("print_setup_icon").height
+
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.right: parent.right
+ anchors.rightMargin: Math.round(UM.Theme.getSize("setting_preferences_button_margin").width - UM.Theme.getSize("thick_margin").width)
+
+ color: hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button");
+ iconSource: UM.Theme.getIcon("star")
+
+ onClicked:
+ {
+ forceActiveFocus();
+ Cura.Actions.manageProfiles.trigger()
+ }
+ onEntered:
+ {
+ var content = catalog.i18nc("@tooltip","Some setting/override values are different from the values stored in the profile.\n\nClick to open the profile manager.")
+ base.showTooltip(globalProfileRow, Qt.point(-UM.Theme.getSize("default_margin").width, 0), content)
+ }
+ onExited: base.hideTooltip()
+ }
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/PrintSetupSelector/PrintSetupSelector.qml b/resources/qml/PrintSetupSelector/PrintSetupSelector.qml
new file mode 100644
index 0000000000..48ac07679d
--- /dev/null
+++ b/resources/qml/PrintSetupSelector/PrintSetupSelector.qml
@@ -0,0 +1,35 @@
+// 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.0
+
+import UM 1.3 as UM
+import Cura 1.0 as Cura
+
+Cura.ExpandableComponent
+{
+ id: printSetupSelector
+
+ property bool preSlicedData: PrintInformation.preSliced
+
+ contentPadding: UM.Theme.getSize("default_lining").width
+ contentHeaderTitle: catalog.i18nc("@label", "Print settings")
+ enabled: !preSlicedData
+ disabledText: catalog.i18nc("@label shown when we load a Gcode file", "Print setup disabled. G code file can not be modified.")
+
+ UM.I18nCatalog
+ {
+ id: catalog
+ name: "cura"
+ }
+
+ headerItem: PrintSetupSelectorHeader {}
+
+ property var extrudersModel: CuraApplication.getExtrudersModel()
+
+ contentItem: PrintSetupSelectorContents {}
+
+ onExpandedChanged: UM.Preferences.setValue("view/settings_visible", expanded)
+ Component.onCompleted: expanded = UM.Preferences.getValue("view/settings_visible")
+}
\ No newline at end of file
diff --git a/resources/qml/PrintSetupSelector/PrintSetupSelectorContents.qml b/resources/qml/PrintSetupSelector/PrintSetupSelectorContents.qml
new file mode 100644
index 0000000000..7c82a7324d
--- /dev/null
+++ b/resources/qml/PrintSetupSelector/PrintSetupSelectorContents.qml
@@ -0,0 +1,197 @@
+// 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.3
+
+import UM 1.3 as UM
+import Cura 1.0 as Cura
+
+import "Recommended"
+import "Custom"
+
+Item
+{
+ id: content
+
+ width: UM.Theme.getSize("print_setup_widget").width - 2 * UM.Theme.getSize("default_margin").width
+ height: contents.height + buttonRow.height
+
+ enum Mode
+ {
+ Recommended = 0,
+ Custom = 1
+ }
+
+ // Set the current mode index to the value that is stored in the preferences or Recommended mode otherwise.
+ property int currentModeIndex:
+ {
+ var index = Math.round(UM.Preferences.getValue("cura/active_mode"))
+
+ if (index != null && !isNaN(index))
+ {
+ return index
+ }
+ return PrintSetupSelectorContents.Mode.Recommended
+ }
+ onCurrentModeIndexChanged: UM.Preferences.setValue("cura/active_mode", currentModeIndex)
+
+ Item
+ {
+ id: contents
+ // Use the visible property instead of checking the currentModeIndex. That creates a binding that
+ // evaluates the new height every time the visible property changes.
+ height: recommendedPrintSetup.visible ? recommendedPrintSetup.height : customPrintSetup.height
+
+ anchors
+ {
+ top: parent.top
+ left: parent.left
+ right: parent.right
+ }
+
+ RecommendedPrintSetup
+ {
+ id: recommendedPrintSetup
+ anchors
+ {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ }
+ visible: currentModeIndex == PrintSetupSelectorContents.Mode.Recommended
+ }
+
+ CustomPrintSetup
+ {
+ id: customPrintSetup
+ anchors
+ {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ }
+ height: UM.Preferences.getValue("view/settings_list_height") - UM.Theme.getSize("default_margin").height
+ Connections
+ {
+ target: UM.Preferences
+ onPreferenceChanged:
+ {
+ customPrintSetup.height =
+ Math.min
+ (
+ UM.Preferences.getValue("view/settings_list_height"),
+ base.height - (customPrintSetup.mapToItem(null, 0, 0).y + buttonRow.height + UM.Theme.getSize("default_margin").height)
+ );
+ }
+ }
+ visible: currentModeIndex == PrintSetupSelectorContents.Mode.Custom
+ }
+ }
+
+ Rectangle
+ {
+ id: buttonsSeparator
+
+ // The buttonsSeparator is inside the contents. This is to avoid a double line in the bottom
+ anchors.bottom: contents.bottom
+ width: parent.width
+ height: UM.Theme.getSize("default_lining").height
+ color: UM.Theme.getColor("lining")
+ }
+
+ Item
+ {
+ id: buttonRow
+ property real padding: UM.Theme.getSize("default_margin").width
+ height: childrenRect.height + 2 * padding
+
+ anchors
+ {
+ bottom: parent.bottom
+ left: parent.left
+ right: parent.right
+ }
+
+ Cura.SecondaryButton
+ {
+ id: recommendedButton
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.margins: parent.padding
+ leftPadding: UM.Theme.getSize("default_margin").width
+ rightPadding: UM.Theme.getSize("default_margin").width
+ text: catalog.i18nc("@button", "Recommended")
+ iconSource: UM.Theme.getIcon("arrow_left")
+ visible: currentModeIndex == PrintSetupSelectorContents.Mode.Custom
+ onClicked: currentModeIndex = PrintSetupSelectorContents.Mode.Recommended
+ }
+
+ Cura.SecondaryButton
+ {
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.margins: UM.Theme.getSize("default_margin").width
+ leftPadding: UM.Theme.getSize("default_margin").width
+ rightPadding: UM.Theme.getSize("default_margin").width
+ text: catalog.i18nc("@button", "Custom")
+ iconSource: UM.Theme.getIcon("arrow_right")
+ isIconOnRightSide: true
+ visible: currentModeIndex == PrintSetupSelectorContents.Mode.Recommended
+ onClicked: currentModeIndex = PrintSetupSelectorContents.Mode.Custom
+ }
+
+ //Invisible area at the bottom with which you can resize the panel.
+ MouseArea
+ {
+ anchors
+ {
+ left: parent.left
+ right: parent.right
+ bottom: parent.bottom
+ top: recommendedButton.bottom
+ topMargin: UM.Theme.getSize("default_lining").height
+ }
+ cursorShape: Qt.SplitVCursor
+ visible: currentModeIndex == PrintSetupSelectorContents.Mode.Custom
+ drag
+ {
+ target: parent
+ axis: Drag.YAxis
+ }
+ onMouseYChanged:
+ {
+ if(drag.active)
+ {
+ // position of mouse relative to dropdown align vertical centre of mouse area to cursor
+ // v------------------------------v v------------v
+ var h = mouseY + buttonRow.y + content.y - height / 2 | 0;
+ if(h < 200 * screenScaleFactor) //Enforce a minimum size.
+ {
+ h = 200 * screenScaleFactor;
+ }
+
+ //Absolute mouse Y position in the window, to prevent it from going outside the window.
+ var mouse_absolute_y = mapToGlobal(mouseX, mouseY).y - UM.Preferences.getValue("general/window_top");
+ if(mouse_absolute_y > base.height)
+ {
+ h -= mouse_absolute_y - base.height;
+ }
+
+ UM.Preferences.setValue("view/settings_list_height", h);
+ }
+ }
+
+ UM.RecolorImage
+ {
+ width: parent.width * 0.05
+ height: parent.height * 0.3
+
+ anchors.centerIn: parent
+
+ source: UM.Theme.getIcon("grip_lines")
+ color: UM.Theme.getColor("lining")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml b/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml
new file mode 100644
index 0000000000..96b244d803
--- /dev/null
+++ b/resources/qml/PrintSetupSelector/PrintSetupSelectorHeader.qml
@@ -0,0 +1,88 @@
+// 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.3
+import QtQuick.Layouts 1.3
+
+import UM 1.3 as UM
+import Cura 1.0 as Cura
+
+RowLayout
+{
+ property string enabledText: catalog.i18nc("@label:Should be short", "On")
+ property string disabledText: catalog.i18nc("@label:Should be short", "Off")
+
+ Cura.IconWithText
+ {
+ source: UM.Theme.getIcon("category_layer_height")
+ text:
+ {
+ if (Cura.MachineManager.activeStack)
+ {
+ var text = Cura.MachineManager.activeQualityOrQualityChangesName
+ if (!Cura.MachineManager.hasNotSupportedQuality)
+ {
+ text += " " + layerHeight.properties.value + "mm"
+ text += Cura.MachineManager.isActiveQualityExperimental ? " - " + catalog.i18nc("@label", "Experimental") : ""
+ }
+ return text
+ }
+ return ""
+ }
+ font: UM.Theme.getFont("medium")
+
+ UM.SettingPropertyProvider
+ {
+ id: layerHeight
+ containerStack: Cura.MachineManager.activeStack
+ key: "layer_height"
+ watchedProperties: ["value"]
+ }
+ }
+
+ Cura.IconWithText
+ {
+ source: UM.Theme.getIcon("category_infill")
+ text: Cura.MachineManager.activeStack ? parseInt(infillDensity.properties.value) + "%" : "0%"
+ font: UM.Theme.getFont("medium")
+
+ UM.SettingPropertyProvider
+ {
+ id: infillDensity
+ containerStack: Cura.MachineManager.activeStack
+ key: "infill_sparse_density"
+ watchedProperties: ["value"]
+ }
+ }
+
+ Cura.IconWithText
+ {
+ source: UM.Theme.getIcon("category_support")
+ text: supportEnabled.properties.value == "True" ? enabledText : disabledText
+ font: UM.Theme.getFont("medium")
+
+ UM.SettingPropertyProvider
+ {
+ id: supportEnabled
+ containerStack: Cura.MachineManager.activeMachine
+ key: "support_enable"
+ watchedProperties: ["value"]
+ }
+ }
+
+ Cura.IconWithText
+ {
+ source: UM.Theme.getIcon("category_adhesion")
+ text: platformAdhesionType.properties.value != "skirt" && platformAdhesionType.properties.value != "none" ? enabledText : disabledText
+ font: UM.Theme.getFont("medium")
+
+ UM.SettingPropertyProvider
+ {
+ id: platformAdhesionType
+ containerStack: Cura.MachineManager.activeMachine
+ key: "adhesion_type"
+ watchedProperties: [ "value"]
+ }
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedAdhesionSelector.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedAdhesionSelector.qml
new file mode 100644
index 0000000000..941199707c
--- /dev/null
+++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedAdhesionSelector.qml
@@ -0,0 +1,101 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 1.4
+import QtQuick.Controls.Styles 1.4
+
+import UM 1.2 as UM
+import Cura 1.0 as Cura
+
+
+//
+// Adhesion
+//
+Item
+{
+ id: enableAdhesionRow
+ height: childrenRect.height
+
+ property real labelColumnWidth: Math.round(width / 3)
+
+ Cura.IconWithText
+ {
+ id: enableAdhesionRowTitle
+ anchors.top: parent.top
+ anchors.left: parent.left
+ source: UM.Theme.getIcon("category_adhesion")
+ text: catalog.i18nc("@label", "Adhesion")
+ font: UM.Theme.getFont("medium")
+ width: labelColumnWidth
+ }
+
+ Item
+ {
+ id: enableAdhesionContainer
+ height: enableAdhesionCheckBox.height
+
+ anchors
+ {
+ left: enableAdhesionRowTitle.right
+ right: parent.right
+ verticalCenter: enableAdhesionRowTitle.verticalCenter
+ }
+
+ CheckBox
+ {
+ id: enableAdhesionCheckBox
+ anchors.verticalCenter: parent.verticalCenter
+
+ property alias _hovered: adhesionMouseArea.containsMouse
+
+ //: Setting enable printing build-plate adhesion helper checkbox
+ style: UM.Theme.styles.checkbox
+ enabled: recommendedPrintSetup.settingsEnabled
+
+ visible: platformAdhesionType.properties.enabled == "True"
+ checked: platformAdhesionType.properties.value != "skirt" && platformAdhesionType.properties.value != "none"
+
+ MouseArea
+ {
+ id: adhesionMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+
+ onClicked:
+ {
+ var adhesionType = "skirt"
+ if (!parent.checked)
+ {
+ // Remove the "user" setting to see if the rest of the stack prescribes a brim or a raft
+ platformAdhesionType.removeFromContainer(0)
+ adhesionType = platformAdhesionType.properties.value
+ if(adhesionType == "skirt" || adhesionType == "none")
+ {
+ // If the rest of the stack doesn't prescribe an adhesion-type, default to a brim
+ adhesionType = "brim"
+ }
+ }
+ platformAdhesionType.setPropertyValue("value", adhesionType)
+ }
+
+ onEntered:
+ {
+ base.showTooltip(enableAdhesionCheckBox, Qt.point(-enableAdhesionContainer.x - UM.Theme.getSize("thick_margin").width, 0),
+ catalog.i18nc("@label", "Enable printing a brim or raft. This will add a flat area around or under your object which is easy to cut off afterwards."));
+ }
+ onExited: base.hideTooltip()
+ }
+ }
+ }
+
+ UM.SettingPropertyProvider
+ {
+ id: platformAdhesionType
+ containerStack: Cura.MachineManager.activeMachine
+ removeUnusedValue: false //Doesn't work with settings that are resolved.
+ key: "adhesion_type"
+ watchedProperties: [ "value", "enabled" ]
+ storeIndex: 0
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedInfillDensitySelector.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedInfillDensitySelector.qml
new file mode 100644
index 0000000000..19f199fea6
--- /dev/null
+++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedInfillDensitySelector.qml
@@ -0,0 +1,255 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 1.4
+import QtQuick.Controls.Styles 1.4
+
+import UM 1.2 as UM
+import Cura 1.0 as Cura
+
+
+//
+// Infill
+//
+Item
+{
+ id: infillRow
+ height: childrenRect.height
+
+ property real labelColumnWidth: Math.round(width / 3)
+
+ // Create a binding to update the icon when the infill density changes
+ Binding
+ {
+ target: infillRowTitle
+ property: "source"
+ value:
+ {
+ var density = parseInt(infillDensity.properties.value)
+ if (parseInt(infillSteps.properties.value) != 0)
+ {
+ return UM.Theme.getIcon("gradual")
+ }
+ if (density <= 0)
+ {
+ return UM.Theme.getIcon("hollow")
+ }
+ if (density < 40)
+ {
+ return UM.Theme.getIcon("sparse")
+ }
+ if (density < 90)
+ {
+ return UM.Theme.getIcon("dense")
+ }
+ return UM.Theme.getIcon("solid")
+ }
+ }
+
+ // We use a binding to make sure that after manually setting infillSlider.value it is still bound to the property provider
+ Binding
+ {
+ target: infillSlider
+ property: "value"
+ value: parseInt(infillDensity.properties.value)
+ }
+
+ // Here are the elements that are shown in the left column
+ Cura.IconWithText
+ {
+ id: infillRowTitle
+ anchors.top: parent.top
+ anchors.left: parent.left
+ source: UM.Theme.getIcon("category_infill")
+ text: catalog.i18nc("@label", "Infill") + " (%)"
+ font: UM.Theme.getFont("medium")
+ width: labelColumnWidth
+ }
+
+ Item
+ {
+ id: infillSliderContainer
+ height: childrenRect.height
+
+ anchors
+ {
+ left: infillRowTitle.right
+ right: parent.right
+ verticalCenter: infillRowTitle.verticalCenter
+ }
+
+ Slider
+ {
+ id: infillSlider
+
+ width: parent.width
+ height: UM.Theme.getSize("print_setup_slider_handle").height // The handle is the widest element of the slider
+
+ minimumValue: 0
+ maximumValue: 100
+ stepSize: 1
+ tickmarksEnabled: true
+
+ // disable slider when gradual support is enabled
+ enabled: parseInt(infillSteps.properties.value) == 0
+
+ // set initial value from stack
+ value: parseInt(infillDensity.properties.value)
+
+ style: SliderStyle
+ {
+ //Draw line
+ groove: Item
+ {
+ Rectangle
+ {
+ height: UM.Theme.getSize("print_setup_slider_groove").height
+ width: control.width - UM.Theme.getSize("print_setup_slider_handle").width
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ color: control.enabled ? UM.Theme.getColor("quality_slider_available") : UM.Theme.getColor("quality_slider_unavailable")
+ }
+ }
+
+ handle: Rectangle
+ {
+ id: handleButton
+ color: control.enabled ? UM.Theme.getColor("primary") : UM.Theme.getColor("quality_slider_unavailable")
+ implicitWidth: UM.Theme.getSize("print_setup_slider_handle").width
+ implicitHeight: implicitWidth
+ radius: Math.round(implicitWidth / 2)
+ }
+
+ tickmarks: Repeater
+ {
+ id: repeater
+ model: control.maximumValue / control.stepSize + 1
+
+ Rectangle
+ {
+ color: control.enabled ? UM.Theme.getColor("quality_slider_available") : UM.Theme.getColor("quality_slider_unavailable")
+ implicitWidth: UM.Theme.getSize("print_setup_slider_tickmarks").width
+ implicitHeight: UM.Theme.getSize("print_setup_slider_tickmarks").height
+ anchors.verticalCenter: parent.verticalCenter
+
+ // Do not use Math.round otherwise the tickmarks won't be aligned
+ x: ((styleData.handleWidth / 2) - (implicitWidth / 2) + (index * ((repeater.width - styleData.handleWidth) / (repeater.count-1))))
+ radius: Math.round(implicitWidth / 2)
+ visible: (index % 10) == 0 // Only show steps of 10%
+
+ Label
+ {
+ text: index
+ font: UM.Theme.getFont("default")
+ visible: (index % 20) == 0 // Only show steps of 20%
+ anchors.horizontalCenter: parent.horizontalCenter
+ y: UM.Theme.getSize("thin_margin").height
+ renderType: Text.NativeRendering
+ color: UM.Theme.getColor("quality_slider_available")
+ }
+ }
+ }
+ }
+
+ onValueChanged:
+ {
+ // Don't round the value if it's already the same
+ if (parseInt(infillDensity.properties.value) == infillSlider.value)
+ {
+ return
+ }
+
+ // Round the slider value to the nearest multiple of 10 (simulate step size of 10)
+ var roundedSliderValue = Math.round(infillSlider.value / 10) * 10
+
+ // Update the slider value to represent the rounded value
+ infillSlider.value = roundedSliderValue
+
+ // Update value only if the Recomended mode is Active,
+ // Otherwise if I change the value in the Custom mode the Recomended view will try to repeat
+ // same operation
+ var active_mode = UM.Preferences.getValue("cura/active_mode")
+
+ if (active_mode == 0 || active_mode == "simple")
+ {
+ Cura.MachineManager.setSettingForAllExtruders("infill_sparse_density", "value", roundedSliderValue)
+ Cura.MachineManager.resetSettingForAllExtruders("infill_line_distance")
+ }
+ }
+ }
+ }
+
+ // Gradual Support Infill Checkbox
+ CheckBox
+ {
+ id: enableGradualInfillCheckBox
+ property alias _hovered: enableGradualInfillMouseArea.containsMouse
+
+ anchors.top: infillSliderContainer.bottom
+ anchors.topMargin: UM.Theme.getSize("wide_margin").height
+ anchors.left: infillSliderContainer.left
+
+ text: catalog.i18nc("@label", "Gradual infill")
+ style: UM.Theme.styles.checkbox
+ enabled: recommendedPrintSetup.settingsEnabled
+ visible: infillSteps.properties.enabled == "True"
+ checked: parseInt(infillSteps.properties.value) > 0
+
+ MouseArea
+ {
+ id: enableGradualInfillMouseArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ enabled: true
+
+ property var previousInfillDensity: parseInt(infillDensity.properties.value)
+
+ onClicked:
+ {
+ // Set to 90% only when enabling gradual infill
+ var newInfillDensity;
+ if (parseInt(infillSteps.properties.value) == 0)
+ {
+ previousInfillDensity = parseInt(infillDensity.properties.value)
+ newInfillDensity = 90
+ } else {
+ newInfillDensity = previousInfillDensity
+ }
+ Cura.MachineManager.setSettingForAllExtruders("infill_sparse_density", "value", String(newInfillDensity))
+
+ var infill_steps_value = 0
+ if (parseInt(infillSteps.properties.value) == 0)
+ {
+ infill_steps_value = 5
+ }
+
+ Cura.MachineManager.setSettingForAllExtruders("gradual_infill_steps", "value", infill_steps_value)
+ }
+
+ onEntered: base.showTooltip(enableGradualInfillCheckBox, Qt.point(-infillSliderContainer.x - UM.Theme.getSize("thick_margin").width, 0),
+ catalog.i18nc("@label", "Gradual infill will gradually increase the amount of infill towards the top."))
+
+ onExited: base.hideTooltip()
+ }
+ }
+
+ UM.SettingPropertyProvider
+ {
+ id: infillDensity
+ containerStackId: Cura.MachineManager.activeStackId
+ key: "infill_sparse_density"
+ watchedProperties: [ "value" ]
+ storeIndex: 0
+ }
+
+ UM.SettingPropertyProvider
+ {
+ id: infillSteps
+ containerStackId: Cura.MachineManager.activeStackId
+ key: "gradual_infill_steps"
+ watchedProperties: ["value", "enabled"]
+ storeIndex: 0
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedPrintSetup.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedPrintSetup.qml
new file mode 100644
index 0000000000..44b3abf7cd
--- /dev/null
+++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedPrintSetup.qml
@@ -0,0 +1,86 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 1.4
+import QtQuick.Controls.Styles 1.4
+
+import UM 1.2 as UM
+import Cura 1.0 as Cura
+
+Item
+{
+ id: recommendedPrintSetup
+
+ height: childrenRect.height + 2 * padding
+
+ property Action configureSettings
+
+ property bool settingsEnabled: Cura.ExtruderManager.activeExtruderStackId || extrudersEnabledCount.properties.value == 1
+ property real padding: UM.Theme.getSize("thick_margin").width
+
+ UM.I18nCatalog
+ {
+ id: catalog
+ name: "cura"
+ }
+
+ Column
+ {
+ width: parent.width - 2 * parent.padding
+ spacing: UM.Theme.getSize("wide_margin").height
+
+ anchors
+ {
+ left: parent.left
+ right: parent.right
+ top: parent.top
+ margins: parent.padding
+ }
+
+ // TODO
+ property real firstColumnWidth: Math.round(width / 3)
+
+ RecommendedQualityProfileSelector
+ {
+ width: parent.width
+ // TODO Create a reusable component with these properties to not define them separately for each component
+ labelColumnWidth: parent.firstColumnWidth
+ }
+
+ RecommendedInfillDensitySelector
+ {
+ width: parent.width
+ // TODO Create a reusable component with these properties to not define them separately for each component
+ labelColumnWidth: parent.firstColumnWidth
+ }
+
+ RecommendedSupportSelector
+ {
+ width: parent.width
+ // TODO Create a reusable component with these properties to not define them separately for each component
+ labelColumnWidth: parent.firstColumnWidth
+ }
+
+ RecommendedAdhesionSelector
+ {
+ width: parent.width
+ // TODO Create a reusable component with these properties to not define them separately for each component
+ labelColumnWidth: parent.firstColumnWidth
+ }
+
+ RecommendedTroubleshootingGuides
+ {
+ width: parent.width
+ }
+ }
+
+ UM.SettingPropertyProvider
+ {
+ id: extrudersEnabledCount
+ containerStack: Cura.MachineManager.activeMachine
+ key: "extruders_enabled_count"
+ watchedProperties: [ "value" ]
+ storeIndex: 0
+ }
+}
diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml
new file mode 100644
index 0000000000..801e76382b
--- /dev/null
+++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedQualityProfileSelector.qml
@@ -0,0 +1,455 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 1.4
+import QtQuick.Controls.Styles 1.4
+
+import UM 1.2 as UM
+import Cura 1.0 as Cura
+
+
+//
+// Quality profile
+//
+Item
+{
+ id: qualityRow
+ height: childrenRect.height
+
+ property real labelColumnWidth: Math.round(width / 3)
+ property real settingsColumnWidth: width - labelColumnWidth
+
+ Timer
+ {
+ id: qualitySliderChangeTimer
+ interval: 50
+ running: false
+ repeat: false
+ onTriggered:
+ {
+ var item = Cura.QualityProfilesDropDownMenuModel.getItem(qualitySlider.value);
+ Cura.MachineManager.activeQualityGroup = item.quality_group;
+ }
+ }
+
+ Component.onCompleted: qualityModel.update()
+
+ Connections
+ {
+ target: Cura.QualityProfilesDropDownMenuModel
+ onItemsChanged: qualityModel.update()
+ }
+
+ Connections {
+ target: base
+ onVisibleChanged:
+ {
+ // update needs to be called when the widgets are visible, otherwise the step width calculation
+ // will fail because the width of an invisible item is 0.
+ if (visible)
+ {
+ qualityModel.update();
+ }
+ }
+ }
+
+ ListModel
+ {
+ id: qualityModel
+
+ property var totalTicks: 0
+ property var availableTotalTicks: 0
+ property var existingQualityProfile: 0
+
+ property var qualitySliderActiveIndex: 0
+ property var qualitySliderStepWidth: 0
+ property var qualitySliderAvailableMin: 0
+ property var qualitySliderAvailableMax: 0
+ property var qualitySliderMarginRight: 0
+
+ function update ()
+ {
+ reset()
+
+ var availableMin = -1
+ var availableMax = -1
+
+ for (var i = 0; i < Cura.QualityProfilesDropDownMenuModel.rowCount(); i++)
+ {
+ var qualityItem = Cura.QualityProfilesDropDownMenuModel.getItem(i)
+
+ // Add each quality item to the UI quality model
+ qualityModel.append(qualityItem)
+
+ // Set selected value
+ if (Cura.MachineManager.activeQualityType == qualityItem.quality_type)
+ {
+ // set to -1 when switching to user created profile so all ticks are clickable
+ if (Cura.MachineManager.hasCustomQuality)
+ {
+ qualityModel.qualitySliderActiveIndex = -1
+ }
+ else
+ {
+ qualityModel.qualitySliderActiveIndex = i
+ }
+
+ qualityModel.existingQualityProfile = 1
+ }
+
+ // Set min available
+ if (qualityItem.available && availableMin == -1)
+ {
+ availableMin = i
+ }
+
+ // Set max available
+ if (qualityItem.available)
+ {
+ availableMax = i
+ }
+ }
+
+ // Set total available ticks for active slider part
+ if (availableMin != -1)
+ {
+ qualityModel.availableTotalTicks = availableMax - availableMin + 1
+ }
+
+ // Calculate slider values
+ calculateSliderStepWidth(qualityModel.totalTicks)
+ calculateSliderMargins(availableMin, availableMax, qualityModel.totalTicks)
+
+ qualityModel.qualitySliderAvailableMin = availableMin
+ qualityModel.qualitySliderAvailableMax = availableMax
+ }
+
+ function calculateSliderStepWidth (totalTicks)
+ {
+ // Do not use Math.round otherwise the tickmarks won't be aligned
+ qualityModel.qualitySliderStepWidth = totalTicks != 0 ?
+ ((settingsColumnWidth - UM.Theme.getSize("print_setup_slider_handle").width) / (totalTicks)) : 0
+ }
+
+ function calculateSliderMargins (availableMin, availableMax, totalTicks)
+ {
+ if (availableMin == -1 || (availableMin == 0 && availableMax == 0))
+ {
+ // Do not use Math.round otherwise the tickmarks won't be aligned
+ qualityModel.qualitySliderMarginRight = settingsColumnWidth
+ }
+ else if (availableMin == availableMax)
+ {
+ // Do not use Math.round otherwise the tickmarks won't be aligned
+ qualityModel.qualitySliderMarginRight = (totalTicks - availableMin) * qualitySliderStepWidth
+ }
+ else
+ {
+ // Do not use Math.round otherwise the tickmarks won't be aligned
+ qualityModel.qualitySliderMarginRight = (totalTicks - availableMax) * qualitySliderStepWidth
+ }
+ }
+
+ function reset () {
+ qualityModel.clear()
+ qualityModel.availableTotalTicks = 0
+ qualityModel.existingQualityProfile = 0
+
+ // check, the ticks count cannot be less than zero
+ qualityModel.totalTicks = Math.max(0, Cura.QualityProfilesDropDownMenuModel.rowCount() - 1)
+ }
+ }
+
+ // Here are the elements that are shown in the left column
+ Item
+ {
+ id: titleRow
+ width: labelColumnWidth
+ height: childrenRect.height
+
+ Cura.IconWithText
+ {
+ id: qualityRowTitle
+ source: UM.Theme.getIcon("category_layer_height")
+ text: catalog.i18nc("@label", "Layer Height")
+ font: UM.Theme.getFont("medium")
+ anchors.left: parent.left
+ anchors.right: customisedSettings.left
+ }
+
+ UM.SimpleButton
+ {
+ id: customisedSettings
+
+ visible: Cura.SimpleModeSettingsManager.isProfileCustomized || Cura.MachineManager.hasCustomQuality
+ height: visible ? UM.Theme.getSize("print_setup_icon").height : 0
+ width: height
+ anchors
+ {
+ right: parent.right
+ rightMargin: UM.Theme.getSize("default_margin").width
+ leftMargin: UM.Theme.getSize("default_margin").width
+ verticalCenter: parent.verticalCenter
+ }
+
+ color: hovered ? UM.Theme.getColor("setting_control_button_hover") : UM.Theme.getColor("setting_control_button")
+ iconSource: UM.Theme.getIcon("reset")
+
+ onClicked:
+ {
+ // if the current profile is user-created, switch to a built-in quality
+ Cura.MachineManager.resetToUseDefaultQuality()
+ }
+ onEntered:
+ {
+ var tooltipContent = catalog.i18nc("@tooltip","You have modified some profile settings. If you want to change these go to custom mode.")
+ base.showTooltip(qualityRow, Qt.point(-UM.Theme.getSize("thick_margin").width, 0), tooltipContent)
+ }
+ onExited: base.hideTooltip()
+ }
+ }
+
+ // Show titles for the each quality slider ticks
+ Item
+ {
+ anchors.left: speedSlider.left
+ anchors.top: speedSlider.bottom
+ height: childrenRect.height
+
+ Repeater
+ {
+ model: qualityModel
+
+ Label
+ {
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.top: parent.top
+ // The height has to be set manually, otherwise it's not automatically calculated in the repeater
+ height: UM.Theme.getSize("default_margin").height
+ color: (Cura.MachineManager.activeMachine != null && Cura.QualityProfilesDropDownMenuModel.getItem(index).available) ? UM.Theme.getColor("quality_slider_available") : UM.Theme.getColor("quality_slider_unavailable")
+ text:
+ {
+ var result = ""
+ if(Cura.MachineManager.activeMachine != null)
+ {
+ result = Cura.QualityProfilesDropDownMenuModel.getItem(index).layer_height
+
+ if(result == undefined)
+ {
+ result = "";
+ }
+ else
+ {
+ result = Number(Math.round(result + "e+2") + "e-2"); //Round to 2 decimals. Javascript makes this difficult...
+ if (result == undefined || result != result) //Parse failure.
+ {
+ result = "";
+ }
+ }
+ }
+ return result
+ }
+
+ x:
+ {
+ // Make sure the text aligns correctly with each tick
+ if (qualityModel.totalTicks == 0)
+ {
+ // If there is only one tick, align it centrally
+ return Math.round(((settingsColumnWidth) - width) / 2)
+ }
+ else if (index == 0)
+ {
+ return Math.round(settingsColumnWidth / qualityModel.totalTicks) * index
+ }
+ else if (index == qualityModel.totalTicks)
+ {
+ return Math.round(settingsColumnWidth / qualityModel.totalTicks) * index - width
+ }
+ else
+ {
+ return Math.round((settingsColumnWidth / qualityModel.totalTicks) * index - (width / 2))
+ }
+ }
+ font: UM.Theme.getFont("default")
+ }
+ }
+ }
+
+ // Print speed slider
+ // Two sliders are created, one at the bottom with the unavailable qualities
+ // and the other at the top with the available quality profiles and so the handle to select them.
+ Item
+ {
+ id: speedSlider
+ height: childrenRect.height
+
+ anchors
+ {
+ left: titleRow.right
+ right: parent.right
+ verticalCenter: titleRow.verticalCenter
+ }
+
+ // Draw unavailable slider
+ Slider
+ {
+ id: unavailableSlider
+
+ width: parent.width
+ height: qualitySlider.height // Same height as the slider that is on top
+ updateValueWhileDragging : false
+ tickmarksEnabled: true
+
+ minimumValue: 0
+ // maximumValue must be greater than minimumValue to be able to see the handle. While the value is strictly
+ // speaking not always correct, it seems to have the correct behavior (switching from 0 available to 1 available)
+ maximumValue: qualityModel.totalTicks
+ stepSize: 1
+
+ style: SliderStyle
+ {
+ //Draw Unvailable line
+ groove: Item
+ {
+ Rectangle
+ {
+ height: UM.Theme.getSize("print_setup_slider_groove").height
+ width: control.width - UM.Theme.getSize("print_setup_slider_handle").width
+ anchors.horizontalCenter: parent.horizontalCenter
+ anchors.verticalCenter: parent.verticalCenter
+ color: UM.Theme.getColor("quality_slider_unavailable")
+ }
+ }
+
+ handle: Item {}
+
+ tickmarks: Repeater
+ {
+ id: qualityRepeater
+ model: qualityModel.totalTicks > 0 ? qualityModel : 0
+
+ Rectangle
+ {
+ color: Cura.QualityProfilesDropDownMenuModel.getItem(index).available ? UM.Theme.getColor("quality_slider_available") : UM.Theme.getColor("quality_slider_unavailable")
+ implicitWidth: UM.Theme.getSize("print_setup_slider_tickmarks").width
+ implicitHeight: UM.Theme.getSize("print_setup_slider_tickmarks").height
+ anchors.verticalCenter: parent.verticalCenter
+
+ // Do not use Math.round otherwise the tickmarks won't be aligned
+ x: ((UM.Theme.getSize("print_setup_slider_handle").width / 2) - (implicitWidth / 2) + (qualityModel.qualitySliderStepWidth * index))
+ radius: Math.round(implicitWidth / 2)
+ }
+ }
+ }
+
+ // Create a mouse area on top of the unavailable profiles to show a specific tooltip
+ MouseArea
+ {
+ anchors.fill: parent
+ hoverEnabled: true
+ enabled: !Cura.MachineManager.hasCustomQuality
+ onEntered:
+ {
+ var tooltipContent = catalog.i18nc("@tooltip", "This quality profile is not available for your current material and nozzle configuration. Please change these to enable this quality profile")
+ base.showTooltip(qualityRow, Qt.point(-UM.Theme.getSize("thick_margin").width, customisedSettings.height), tooltipContent)
+ }
+ onExited: base.hideTooltip()
+ }
+ }
+
+ // Draw available slider
+ Slider
+ {
+ id: qualitySlider
+
+ width: qualityModel.qualitySliderStepWidth * (qualityModel.availableTotalTicks - 1) + UM.Theme.getSize("print_setup_slider_handle").width
+ height: UM.Theme.getSize("print_setup_slider_handle").height // The handle is the widest element of the slider
+ enabled: qualityModel.totalTicks > 0 && !Cura.SimpleModeSettingsManager.isProfileCustomized
+ visible: qualityModel.availableTotalTicks > 0
+ updateValueWhileDragging : false
+
+ anchors
+ {
+ right: parent.right
+ rightMargin: qualityModel.qualitySliderMarginRight
+ }
+
+ minimumValue: qualityModel.qualitySliderAvailableMin >= 0 ? qualityModel.qualitySliderAvailableMin : 0
+ // maximumValue must be greater than minimumValue to be able to see the handle. While the value is strictly
+ // speaking not always correct, it seems to have the correct behavior (switching from 0 available to 1 available)
+ maximumValue: qualityModel.qualitySliderAvailableMax >= 1 ? qualityModel.qualitySliderAvailableMax : 1
+ stepSize: 1
+
+ value: qualityModel.qualitySliderActiveIndex
+
+ style: SliderStyle
+ {
+ // Draw Available line
+ groove: Item
+ {
+ Rectangle
+ {
+ height: UM.Theme.getSize("print_setup_slider_groove").height
+ width: control.width - UM.Theme.getSize("print_setup_slider_handle").width
+ anchors.verticalCenter: parent.verticalCenter
+
+ // Do not use Math.round otherwise the tickmarks won't be aligned
+ x: UM.Theme.getSize("print_setup_slider_handle").width / 2
+ color: UM.Theme.getColor("quality_slider_available")
+ }
+ }
+
+ handle: Rectangle
+ {
+ id: qualityhandleButton
+ color: UM.Theme.getColor("primary")
+ implicitWidth: UM.Theme.getSize("print_setup_slider_handle").width
+ implicitHeight: implicitWidth
+ radius: Math.round(implicitWidth / 2)
+ visible: !Cura.SimpleModeSettingsManager.isProfileCustomized && !Cura.MachineManager.hasCustomQuality && qualityModel.existingQualityProfile
+ }
+ }
+
+ onValueChanged:
+ {
+ // only change if an active machine is set and the slider is visible at all.
+ if (Cura.MachineManager.activeMachine != null && visible)
+ {
+ // prevent updating during view initializing. Trigger only if the value changed by user
+ if (qualitySlider.value != qualityModel.qualitySliderActiveIndex && qualityModel.qualitySliderActiveIndex != -1)
+ {
+ // start updating with short delay
+ qualitySliderChangeTimer.start()
+ }
+ }
+ }
+
+ // This mouse area is only used to capture the onHover state and don't propagate it to the unavailable mouse area
+ MouseArea
+ {
+ anchors.fill: parent
+ hoverEnabled: true
+ acceptedButtons: Qt.NoButton
+ enabled: !Cura.MachineManager.hasCustomQuality
+ }
+ }
+
+ // This mouse area will only take the mouse events and show a tooltip when the profile in use is
+ // a user created profile
+ MouseArea
+ {
+ anchors.fill: parent
+ hoverEnabled: true
+ visible: Cura.MachineManager.hasCustomQuality
+
+ onEntered:
+ {
+ var tooltipContent = catalog.i18nc("@tooltip", "A custom profile is currently active. To enable the quality slider, choose a default quality profile in Custom tab")
+ base.showTooltip(qualityRow, Qt.point(-UM.Theme.getSize("thick_margin").width, customisedSettings.height), tooltipContent)
+ }
+ onExited: base.hideTooltip()
+ }
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedSupportSelector.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedSupportSelector.qml
new file mode 100644
index 0000000000..0e834ac4df
--- /dev/null
+++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedSupportSelector.qml
@@ -0,0 +1,206 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.7
+import QtQuick.Controls 1.4
+import QtQuick.Controls.Styles 1.4
+
+import UM 1.2 as UM
+import Cura 1.0 as Cura
+
+
+//
+// Enable support
+//
+Item
+{
+ id: enableSupportRow
+ height: childrenRect.height
+
+ property real labelColumnWidth: Math.round(width / 3)
+
+ Cura.IconWithText
+ {
+ id: enableSupportRowTitle
+ anchors.top: parent.top
+ anchors.left: parent.left
+ visible: enableSupportCheckBox.visible
+ source: UM.Theme.getIcon("category_support")
+ text: catalog.i18nc("@label", "Support")
+ font: UM.Theme.getFont("medium")
+ width: labelColumnWidth
+ }
+
+ Item
+ {
+ id: enableSupportContainer
+ height: enableSupportCheckBox.height
+
+ anchors
+ {
+ left: enableSupportRowTitle.right
+ right: parent.right
+ verticalCenter: enableSupportRowTitle.verticalCenter
+ }
+
+ CheckBox
+ {
+ id: enableSupportCheckBox
+ anchors.verticalCenter: parent.verticalCenter
+
+ property alias _hovered: enableSupportMouseArea.containsMouse
+
+ style: UM.Theme.styles.checkbox
+ enabled: recommendedPrintSetup.settingsEnabled
+
+ visible: supportEnabled.properties.enabled == "True"
+ checked: supportEnabled.properties.value == "True"
+
+ MouseArea
+ {
+ id: enableSupportMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+
+ onClicked: supportEnabled.setPropertyValue("value", supportEnabled.properties.value != "True")
+
+ onEntered:
+ {
+ base.showTooltip(enableSupportCheckBox, Qt.point(-enableSupportContainer.x - UM.Theme.getSize("thick_margin").width, 0),
+ catalog.i18nc("@label", "Generate structures to support parts of the model which have overhangs. Without these structures, such parts would collapse during printing."))
+ }
+ onExited: base.hideTooltip()
+ }
+ }
+
+ ComboBox
+ {
+ id: supportExtruderCombobox
+
+ height: UM.Theme.getSize("print_setup_big_item").height
+ anchors
+ {
+ left: enableSupportCheckBox.right
+ right: parent.right
+ leftMargin: UM.Theme.getSize("thick_margin").width
+ rightMargin: UM.Theme.getSize("thick_margin").width
+ verticalCenter: parent.verticalCenter
+ }
+
+ style: UM.Theme.styles.combobox_color
+ enabled: recommendedPrintSetup.settingsEnabled
+ visible: enableSupportCheckBox.visible && (supportEnabled.properties.value == "True") && (extrudersEnabledCount.properties.value > 1)
+ textRole: "text" // this solves that the combobox isn't populated in the first time Cura is started
+
+ model: extruderModel
+
+ property alias _hovered: supportExtruderMouseArea.containsMouse
+ property string color_override: "" // for manually setting values
+ property string color: // is evaluated automatically, but the first time is before extruderModel being filled
+ {
+ var current_extruder = extruderModel.get(currentIndex)
+ color_override = ""
+ if (current_extruder === undefined) return ""
+ return (current_extruder.color) ? current_extruder.color : ""
+ }
+
+ currentIndex:
+ {
+ if (supportExtruderNr.properties == null)
+ {
+ return Cura.MachineManager.defaultExtruderPosition
+ }
+ else
+ {
+ var extruder = parseInt(supportExtruderNr.properties.value)
+ if ( extruder === -1)
+ {
+ return Cura.MachineManager.defaultExtruderPosition
+ }
+ return extruder;
+ }
+ }
+
+ onActivated: supportExtruderNr.setPropertyValue("value", String(index))
+
+ MouseArea
+ {
+ id: supportExtruderMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ enabled: recommendedPrintSetup.settingsEnabled
+ acceptedButtons: Qt.NoButton
+ onEntered:
+ {
+ base.showTooltip(supportExtruderCombobox, Qt.point(-enableSupportContainer.x - supportExtruderCombobox.x - UM.Theme.getSize("thick_margin").width, 0),
+ catalog.i18nc("@label", "Select which extruder to use for support. This will build up supporting structures below the model to prevent the model from sagging or printing in mid air."));
+ }
+ onExited: base.hideTooltip()
+
+ }
+
+ function updateCurrentColor()
+ {
+ var current_extruder = extruderModel.get(currentIndex)
+ if (current_extruder !== undefined)
+ {
+ supportExtruderCombobox.color_override = current_extruder.color
+ }
+ }
+ }
+ }
+
+ ListModel
+ {
+ id: extruderModel
+ Component.onCompleted: populateExtruderModel()
+ }
+
+ //: Model used to populate the extrudelModel
+ property var extruders: CuraApplication.getExtrudersModel()
+ Connections
+ {
+ target: extruders
+ onModelChanged: populateExtruderModel()
+ }
+
+ UM.SettingPropertyProvider
+ {
+ id: supportEnabled
+ containerStack: Cura.MachineManager.activeMachine
+ key: "support_enable"
+ watchedProperties: [ "value", "enabled", "description" ]
+ storeIndex: 0
+ }
+
+ UM.SettingPropertyProvider
+ {
+ id: supportExtruderNr
+ containerStack: Cura.MachineManager.activeMachine
+ key: "support_extruder_nr"
+ watchedProperties: [ "value" ]
+ storeIndex: 0
+ }
+
+ UM.SettingPropertyProvider
+ {
+ id: machineExtruderCount
+ containerStack: Cura.MachineManager.activeMachine
+ key: "machine_extruder_count"
+ watchedProperties: ["value"]
+ storeIndex: 0
+ }
+
+ function populateExtruderModel()
+ {
+ extruderModel.clear()
+ for (var extruderNumber = 0; extruderNumber < extruders.rowCount(); extruderNumber++)
+ {
+ extruderModel.append({
+ text: extruders.getItem(extruderNumber).name,
+ color: extruders.getItem(extruderNumber).color
+ })
+ }
+ supportExtruderCombobox.updateCurrentColor()
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/PrintSetupSelector/Recommended/RecommendedTroubleshootingGuides.qml b/resources/qml/PrintSetupSelector/Recommended/RecommendedTroubleshootingGuides.qml
new file mode 100644
index 0000000000..846e343028
--- /dev/null
+++ b/resources/qml/PrintSetupSelector/Recommended/RecommendedTroubleshootingGuides.qml
@@ -0,0 +1,36 @@
+// Copyright (c) 2019 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.10
+import QtQuick.Controls 1.4
+import QtQuick.Controls.Styles 1.4
+
+import UM 1.2 as UM
+import Cura 1.0 as Cura
+
+
+Item
+{
+ id: tipsCell
+ anchors.top: adhesionCheckBox.visible ? adhesionCheckBox.bottom : (enableSupportCheckBox.visible ? supportExtruderCombobox.bottom : infillCellRight.bottom)
+ anchors.topMargin: Math.round(UM.Theme.getSize("sidebar_margin").height * 2)
+ anchors.left: parent.left
+ width: parent.width
+ height: tipsText.contentHeight * tipsText.lineCount
+
+ Label
+ {
+ id: tipsText
+ anchors.left: parent.left
+ anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width
+ anchors.right: parent.right
+ anchors.rightMargin: UM.Theme.getSize("sidebar_margin").width
+ anchors.top: parent.top
+ wrapMode: Text.WordWrap
+ text: catalog.i18nc("@label", "Need help improving your prints? Read the Ultimaker Troubleshooting Guides").arg("https://ultimaker.com/en/troubleshooting")
+ font: UM.Theme.getFont("default")
+ color: UM.Theme.getColor("text")
+ linkColor: UM.Theme.getColor("text_link")
+ onLinkActivated: Qt.openUrlExternally(link)
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/SidebarTooltip.qml b/resources/qml/PrintSetupTooltip.qml
similarity index 87%
rename from resources/qml/SidebarTooltip.qml
rename to resources/qml/PrintSetupTooltip.qml
index 29199481f6..6b1538d849 100644
--- a/resources/qml/SidebarTooltip.qml
+++ b/resources/qml/PrintSetupTooltip.qml
@@ -2,9 +2,7 @@
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
-import QtQuick.Controls 1.1
-import QtQuick.Controls.Styles 1.1
-import QtQuick.Layouts 1.1
+import QtQuick.Controls 2.3
import UM 1.0 as UM
@@ -36,14 +34,15 @@ UM.PointingRectangle {
}
}
base.opacity = 1;
- target = Qt.point(40 , position.y + Math.round(UM.Theme.getSize("tooltip_arrow_margins").height / 2))
+ target = Qt.point(position.x + 1, position.y + Math.round(UM.Theme.getSize("tooltip_arrow_margins").height / 2))
}
function hide() {
base.opacity = 0;
}
- Label {
+ Label
+ {
id: label;
anchors {
top: parent.top;
@@ -57,5 +56,6 @@ UM.PointingRectangle {
textFormat: Text.RichText
font: UM.Theme.getFont("default");
color: UM.Theme.getColor("tooltip_text");
+ renderType: Text.NativeRendering
}
}
diff --git a/resources/qml/PrinterOutput/ExtruderBox.qml b/resources/qml/PrinterOutput/ExtruderBox.qml
index f0abd4cd6c..a19c02b0dd 100644
--- a/resources/qml/PrinterOutput/ExtruderBox.qml
+++ b/resources/qml/PrinterOutput/ExtruderBox.qml
@@ -12,8 +12,10 @@ Item
property alias color: background.color
property var extruderModel
property var position: index
+ property var connectedPrinter: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null
+
implicitWidth: parent.width
- implicitHeight: UM.Theme.getSize("sidebar_extruder_box").height
+ implicitHeight: UM.Theme.getSize("print_setup_extruder_box").height
UM.SettingPropertyProvider
{
@@ -45,7 +47,7 @@ Item
{
id: extruderTargetTemperature
text: Math.round(extruderModel.targetHotendTemperature) + "°C"
- font: UM.Theme.getFont("small")
+ font: UM.Theme.getFont("default_bold")
color: UM.Theme.getColor("text_inactive")
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
@@ -78,7 +80,7 @@ Item
id: extruderCurrentTemperature
text: Math.round(extruderModel.hotendTemperature) + "°C"
color: UM.Theme.getColor("text")
- font: UM.Theme.getFont("large")
+ font: UM.Theme.getFont("large_bold")
anchors.right: extruderTargetTemperature.left
anchors.top: parent.top
anchors.margins: UM.Theme.getSize("default_margin").width
@@ -324,7 +326,7 @@ Item
return UM.Theme.getColor("action_button_text");
}
}
- font: UM.Theme.getFont("action_button")
+ font: UM.Theme.getFont("medium")
text:
{
if(extruderModel == null)
diff --git a/resources/qml/PrinterOutput/HeatedBedBox.qml b/resources/qml/PrinterOutput/HeatedBedBox.qml
index 9de66ad0be..77421c8aad 100644
--- a/resources/qml/PrinterOutput/HeatedBedBox.qml
+++ b/resources/qml/PrinterOutput/HeatedBedBox.qml
@@ -1,10 +1,10 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.2
-import QtQuick.Controls 1.1
-import QtQuick.Controls.Styles 1.1
-import QtQuick.Layouts 1.1
+import QtQuick 2.10
+import QtQuick.Controls 1.4
+import QtQuick.Controls.Styles 1.4
+import QtQuick.Layouts 1.3
import UM 1.2 as UM
import Cura 1.0 as Cura
@@ -12,12 +12,13 @@ import Cura 1.0 as Cura
Item
{
implicitWidth: parent.width
- height: visible ? UM.Theme.getSize("sidebar_extruder_box").height : 0
+ height: visible ? UM.Theme.getSize("print_setup_extruder_box").height : 0
property var printerModel
+ property var connectedPrinter: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null
Rectangle
{
- color: UM.Theme.getColor("sidebar")
+ color: UM.Theme.getColor("main_background")
anchors.fill: parent
Label //Build plate label.
@@ -34,7 +35,7 @@ Item
{
id: bedTargetTemperature
text: printerModel != null ? printerModel.targetBedTemperature + "°C" : ""
- font: UM.Theme.getFont("small")
+ font: UM.Theme.getFont("default_bold")
color: UM.Theme.getColor("text_inactive")
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
@@ -66,7 +67,7 @@ Item
{
id: bedCurrentTemperature
text: printerModel != null ? printerModel.bedTemperature + "°C" : ""
- font: UM.Theme.getFont("large")
+ font: UM.Theme.getFont("large_bold")
color: UM.Theme.getColor("text")
anchors.right: bedTargetTemperature.left
anchors.top: parent.top
@@ -114,7 +115,7 @@ Item
{
return false; //Can't preheat if not connected.
}
- if (!connectedPrinter.acceptsCommands)
+ if (connectedPrinter == null || !connectedPrinter.acceptsCommands)
{
return false; //Not allowed to do anything.
}
@@ -319,7 +320,7 @@ Item
return UM.Theme.getColor("action_button_text");
}
}
- font: UM.Theme.getFont("action_button")
+ font: UM.Theme.getFont("medium")
text:
{
if(printerModel == null)
diff --git a/resources/qml/PrinterOutput/ManualPrinterControl.qml b/resources/qml/PrinterOutput/ManualPrinterControl.qml
index 70961a2eb2..106ae7db03 100644
--- a/resources/qml/PrinterOutput/ManualPrinterControl.qml
+++ b/resources/qml/PrinterOutput/ManualPrinterControl.qml
@@ -1,103 +1,26 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.2
-import QtQuick.Controls 1.1
-import QtQuick.Controls.Styles 1.1
-import QtQuick.Layouts 1.1
+import QtQuick 2.10
+import QtQuick.Controls 1.4
+import QtQuick.Controls.Styles 1.4
+import QtQuick.Layouts 1.3
-import UM 1.2 as UM
+import UM 1.3 as UM
import Cura 1.0 as Cura
+import "."
+
+
Item
{
- property var printerModel
+ property var printerModel: null
property var activePrintJob: printerModel != null ? printerModel.activePrintJob : null
+ property var connectedPrinter: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null
+
implicitWidth: parent.width
implicitHeight: childrenRect.height
- Component
- {
- id: monitorButtonStyle
-
- ButtonStyle
- {
- background: Rectangle
- {
- border.width: UM.Theme.getSize("default_lining").width
- border.color:
- {
- if(!control.enabled)
- {
- return UM.Theme.getColor("action_button_disabled_border");
- }
- else if(control.pressed)
- {
- return UM.Theme.getColor("action_button_active_border");
- }
- else if(control.hovered)
- {
- return UM.Theme.getColor("action_button_hovered_border");
- }
- return UM.Theme.getColor("action_button_border");
- }
- color:
- {
- if(!control.enabled)
- {
- return UM.Theme.getColor("action_button_disabled");
- }
- else if(control.pressed)
- {
- return UM.Theme.getColor("action_button_active");
- }
- else if(control.hovered)
- {
- return UM.Theme.getColor("action_button_hovered");
- }
- return UM.Theme.getColor("action_button");
- }
- Behavior on color
- {
- ColorAnimation
- {
- duration: 50
- }
- }
- }
-
- label: Item
- {
- UM.RecolorImage
- {
- anchors.verticalCenter: parent.verticalCenter
- anchors.horizontalCenter: parent.horizontalCenter
- width: Math.floor(control.width / 2)
- height: Math.floor(control.height / 2)
- sourceSize.width: width
- sourceSize.height: width
- color:
- {
- if(!control.enabled)
- {
- return UM.Theme.getColor("action_button_disabled_text");
- }
- else if(control.pressed)
- {
- return UM.Theme.getColor("action_button_active_text");
- }
- else if(control.hovered)
- {
- return UM.Theme.getColor("action_button_hovered_text");
- }
- return UM.Theme.getColor("action_button_text");
- }
- source: control.iconSource
- }
- }
- }
- }
-
Column
{
enabled:
@@ -180,7 +103,7 @@ Item
Layout.preferredWidth: width
Layout.preferredHeight: height
iconSource: UM.Theme.getIcon("arrow_top");
- style: monitorButtonStyle
+ style: UM.Theme.styles.monitor_button_style
width: height
height: UM.Theme.getSize("setting_control").height
@@ -197,7 +120,7 @@ Item
Layout.preferredWidth: width
Layout.preferredHeight: height
iconSource: UM.Theme.getIcon("arrow_left");
- style: monitorButtonStyle
+ style: UM.Theme.styles.monitor_button_style
width: height
height: UM.Theme.getSize("setting_control").height
@@ -214,7 +137,7 @@ Item
Layout.preferredWidth: width
Layout.preferredHeight: height
iconSource: UM.Theme.getIcon("arrow_right");
- style: monitorButtonStyle
+ style: UM.Theme.styles.monitor_button_style
width: height
height: UM.Theme.getSize("setting_control").height
@@ -231,7 +154,7 @@ Item
Layout.preferredWidth: width
Layout.preferredHeight: height
iconSource: UM.Theme.getIcon("arrow_bottom");
- style: monitorButtonStyle
+ style: UM.Theme.styles.monitor_button_style
width: height
height: UM.Theme.getSize("setting_control").height
@@ -248,7 +171,7 @@ Item
Layout.preferredWidth: width
Layout.preferredHeight: height
iconSource: UM.Theme.getIcon("home");
- style: monitorButtonStyle
+ style: UM.Theme.styles.monitor_button_style
width: height
height: UM.Theme.getSize("setting_control").height
@@ -278,7 +201,7 @@ Item
Button
{
iconSource: UM.Theme.getIcon("arrow_top");
- style: monitorButtonStyle
+ style: UM.Theme.styles.monitor_button_style
width: height
height: UM.Theme.getSize("setting_control").height
@@ -291,7 +214,7 @@ Item
Button
{
iconSource: UM.Theme.getIcon("home");
- style: monitorButtonStyle
+ style: UM.Theme.styles.monitor_button_style
width: height
height: UM.Theme.getSize("setting_control").height
@@ -304,7 +227,7 @@ Item
Button
{
iconSource: UM.Theme.getIcon("arrow_bottom");
- style: monitorButtonStyle
+ style: UM.Theme.styles.monitor_button_style
width: height
height: UM.Theme.getSize("setting_control").height
@@ -356,72 +279,7 @@ Item
checked: distancesRow.currentDistance == model.value
onClicked: distancesRow.currentDistance = model.value
- style: ButtonStyle {
- background: Rectangle {
- border.width: control.checked ? UM.Theme.getSize("default_lining").width * 2 : UM.Theme.getSize("default_lining").width
- border.color:
- {
- if(!control.enabled)
- {
- return UM.Theme.getColor("action_button_disabled_border");
- }
- else if (control.checked || control.pressed)
- {
- return UM.Theme.getColor("action_button_active_border");
- }
- else if(control.hovered)
- {
- return UM.Theme.getColor("action_button_hovered_border");
- }
- return UM.Theme.getColor("action_button_border");
- }
- color:
- {
- if(!control.enabled)
- {
- return UM.Theme.getColor("action_button_disabled");
- }
- else if (control.checked || control.pressed)
- {
- return UM.Theme.getColor("action_button_active");
- }
- else if (control.hovered)
- {
- return UM.Theme.getColor("action_button_hovered");
- }
- return UM.Theme.getColor("action_button");
- }
- Behavior on color { ColorAnimation { duration: 50; } }
- Label {
- anchors.left: parent.left
- anchors.right: parent.right
- anchors.verticalCenter: parent.verticalCenter
- anchors.leftMargin: UM.Theme.getSize("default_lining").width * 2
- anchors.rightMargin: UM.Theme.getSize("default_lining").width * 2
- color:
- {
- if(!control.enabled)
- {
- return UM.Theme.getColor("action_button_disabled_text");
- }
- else if (control.checked || control.pressed)
- {
- return UM.Theme.getColor("action_button_active_text");
- }
- else if (control.hovered)
- {
- return UM.Theme.getColor("action_button_hovered_text");
- }
- return UM.Theme.getColor("action_button_text");
- }
- font: UM.Theme.getFont("default")
- text: control.text
- horizontalAlignment: Text.AlignHCenter
- elide: Text.ElideMiddle
- }
- }
- label: Item { }
- }
+ style: UM.Theme.styles.monitor_checkable_button_style
}
}
}
@@ -462,7 +320,7 @@ Item
if (printerModel == null) {
return false // Can't send custom commands if not connected.
}
- if (!connectedPrinter.acceptsCommands) {
+ if (connectedPrinter == null || !connectedPrinter.acceptsCommands) {
return false // Not allowed to do anything
}
if (connectedPrinter.jobState == "printing" || connectedPrinter.jobState == "pre_print" || connectedPrinter.jobState == "resuming" || connectedPrinter.jobState == "pausing" || connectedPrinter.jobState == "paused" || connectedPrinter.jobState == "error" || connectedPrinter.jobState == "offline") {
@@ -551,4 +409,4 @@ Item
}
ExclusiveGroup { id: distanceGroup }
}
-}
\ No newline at end of file
+}
diff --git a/resources/qml/PrinterOutput/MonitorItem.qml b/resources/qml/PrinterOutput/MonitorItem.qml
index cad8d2f7f3..a26ec20f64 100644
--- a/resources/qml/PrinterOutput/MonitorItem.qml
+++ b/resources/qml/PrinterOutput/MonitorItem.qml
@@ -15,6 +15,8 @@ Item
property string value: ""
height: childrenRect.height;
+ property var connectedPrinter: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null
+
Row
{
height: UM.Theme.getSize("setting_control").height
diff --git a/resources/qml/PrinterOutput/MonitorSection.qml b/resources/qml/PrinterOutput/MonitorSection.qml
index 6ed762362d..1d9df777b6 100644
--- a/resources/qml/PrinterOutput/MonitorSection.qml
+++ b/resources/qml/PrinterOutput/MonitorSection.qml
@@ -1,10 +1,10 @@
// Copyright (c) 2017 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
-import QtQuick 2.2
-import QtQuick.Controls 1.1
-import QtQuick.Controls.Styles 1.1
-import QtQuick.Layouts 1.1
+import QtQuick 2.10
+import QtQuick.Controls 1.4
+import QtQuick.Controls.Styles 1.4
+import QtQuick.Layouts 1.3
import UM 1.2 as UM
import Cura 1.0 as Cura
@@ -13,7 +13,8 @@ Item
{
id: base
property string label
- height: childrenRect.height;
+ height: childrenRect.height
+
Rectangle
{
color: UM.Theme.getColor("setting_category")
@@ -26,8 +27,8 @@ Item
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
text: label
- font: UM.Theme.getFont("setting_category")
+ font: UM.Theme.getFont("default")
color: UM.Theme.getColor("setting_category_text")
}
}
-}
\ No newline at end of file
+}
diff --git a/resources/qml/PrinterOutput/OutputDeviceHeader.qml b/resources/qml/PrinterOutput/OutputDeviceHeader.qml
index b5ed1b7b4e..47f855266b 100644
--- a/resources/qml/PrinterOutput/OutputDeviceHeader.qml
+++ b/resources/qml/PrinterOutput/OutputDeviceHeader.qml
@@ -31,7 +31,7 @@ Item
Label
{
id: outputDeviceNameLabel
- font: UM.Theme.getFont("large")
+ font: UM.Theme.getFont("large_bold")
color: UM.Theme.getColor("text")
anchors.left: parent.left
anchors.top: parent.top
@@ -43,10 +43,10 @@ Item
{
id: outputDeviceAddressLabel
text: (outputDevice != null && outputDevice.address != null) ? outputDevice.address : ""
- font: UM.Theme.getFont("small")
+ font: UM.Theme.getFont("default_bold")
color: UM.Theme.getColor("text_inactive")
- anchors.top: parent.top
- anchors.right: parent.right
+ anchors.top: outputDeviceNameLabel.bottom
+ anchors.left: parent.left
anchors.margins: UM.Theme.getSize("default_margin").width
}
@@ -54,7 +54,7 @@ Item
{
text: outputDevice != null ? "" : catalog.i18nc("@info:status", "The printer is not connected.")
color: outputDevice != null && outputDevice.acceptsCommands ? UM.Theme.getColor("setting_control_text") : UM.Theme.getColor("setting_control_disabled_text")
- font: UM.Theme.getFont("very_small")
+ font: UM.Theme.getFont("default")
wrapMode: Text.WordWrap
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
diff --git a/resources/qml/PrinterSelector/MachineSelector.qml b/resources/qml/PrinterSelector/MachineSelector.qml
new file mode 100644
index 0000000000..cd5e041606
--- /dev/null
+++ b/resources/qml/PrinterSelector/MachineSelector.qml
@@ -0,0 +1,182 @@
+// 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.3
+
+import UM 1.2 as UM
+import Cura 1.0 as Cura
+
+Cura.ExpandablePopup
+{
+ id: machineSelector
+
+ property bool isNetworkPrinter: Cura.MachineManager.activeMachineHasActiveNetworkConnection
+ property bool isCloudPrinter: Cura.MachineManager.activeMachineHasActiveCloudConnection
+ property bool isGroup: Cura.MachineManager.activeMachineIsGroup
+
+ contentPadding: UM.Theme.getSize("default_lining").width
+ contentAlignment: Cura.ExpandablePopup.ContentAlignment.AlignLeft
+
+ UM.I18nCatalog
+ {
+ id: catalog
+ name: "cura"
+ }
+
+ headerItem: Cura.IconWithText
+ {
+ text:
+ {
+ if (isNetworkPrinter && Cura.MachineManager.activeMachineNetworkGroupName != "")
+ {
+ return Cura.MachineManager.activeMachineNetworkGroupName
+ }
+ return Cura.MachineManager.activeMachineName
+ }
+ source:
+ {
+ if (isGroup)
+ {
+ return UM.Theme.getIcon("printer_group")
+ }
+ else if (isNetworkPrinter || isCloudPrinter)
+ {
+ return UM.Theme.getIcon("printer_single")
+ }
+ else
+ {
+ return ""
+ }
+ }
+ font: UM.Theme.getFont("medium")
+ iconColor: UM.Theme.getColor("machine_selector_printer_icon")
+ iconSize: source != "" ? UM.Theme.getSize("machine_selector_icon").width: 0
+
+ UM.RecolorImage
+ {
+ anchors
+ {
+ bottom: parent.bottom
+ left: parent.left
+ leftMargin: UM.Theme.getSize("thick_margin").width
+ }
+
+ source:
+ {
+ if (isNetworkPrinter)
+ {
+ return UM.Theme.getIcon("printer_connected")
+ }
+ else if (isCloudPrinter)
+ {
+ return UM.Theme.getIcon("printer_cloud_connected")
+ }
+ else
+ {
+ return ""
+ }
+ }
+
+ width: UM.Theme.getSize("printer_status_icon").width
+ height: UM.Theme.getSize("printer_status_icon").height
+
+ color: UM.Theme.getColor("primary")
+ visible: isNetworkPrinter || isCloudPrinter
+
+ // Make a themable circle in the background so we can change it in other themes
+ Rectangle
+ {
+ id: iconBackground
+ anchors.centerIn: parent
+ // Make it a bit bigger so there is an outline
+ width: parent.width + 2 * UM.Theme.getSize("default_lining").width
+ height: parent.height + 2 * UM.Theme.getSize("default_lining").height
+ radius: Math.round(width / 2)
+ color: UM.Theme.getColor("main_background")
+ z: parent.z - 1
+ }
+ }
+ }
+
+ contentItem: Item
+ {
+ id: popup
+ width: UM.Theme.getSize("machine_selector_widget_content").width
+
+ ScrollView
+ {
+ id: scroll
+ width: parent.width
+ clip: true
+ leftPadding: UM.Theme.getSize("default_lining").width
+ rightPadding: UM.Theme.getSize("default_lining").width
+
+ MachineSelectorList
+ {
+ // Can't use parent.width since the parent is the flickable component and not the ScrollView
+ width: scroll.width - scroll.leftPadding - scroll.rightPadding
+ property real maximumHeight: UM.Theme.getSize("machine_selector_widget_content").height - buttonRow.height
+
+ // We use an extra property here, since we only want to to be informed about the content size changes.
+ onContentHeightChanged:
+ {
+ scroll.height = Math.min(contentHeight, maximumHeight)
+ popup.height = scroll.height + buttonRow.height
+ }
+
+ Component.onCompleted:
+ {
+ scroll.height = Math.min(contentHeight, maximumHeight)
+ popup.height = scroll.height + buttonRow.height
+ }
+
+ }
+ }
+
+ Rectangle
+ {
+ id: separator
+
+ anchors.top: scroll.bottom
+ width: parent.width
+ height: UM.Theme.getSize("default_lining").height
+ color: UM.Theme.getColor("lining")
+ }
+
+ Row
+ {
+ id: buttonRow
+
+ // The separator is inside the buttonRow. This is to avoid some weird behaviours with the scroll bar.
+ anchors.top: separator.top
+ anchors.horizontalCenter: parent.horizontalCenter
+ padding: UM.Theme.getSize("default_margin").width
+ spacing: UM.Theme.getSize("default_margin").width
+
+ Cura.SecondaryButton
+ {
+ leftPadding: UM.Theme.getSize("default_margin").width
+ rightPadding: UM.Theme.getSize("default_margin").width
+ text: catalog.i18nc("@button", "Add printer")
+ onClicked:
+ {
+ toggleContent()
+ Cura.Actions.addMachine.trigger()
+ }
+ }
+
+ Cura.SecondaryButton
+ {
+ leftPadding: UM.Theme.getSize("default_margin").width
+ rightPadding: UM.Theme.getSize("default_margin").width
+ text: catalog.i18nc("@button", "Manage printers")
+ onClicked:
+ {
+ toggleContent()
+ Cura.Actions.configureMachines.trigger()
+ }
+ }
+ }
+ }
+}
diff --git a/resources/qml/PrinterSelector/MachineSelectorButton.qml b/resources/qml/PrinterSelector/MachineSelectorButton.qml
new file mode 100644
index 0000000000..39e63d27c3
--- /dev/null
+++ b/resources/qml/PrinterSelector/MachineSelectorButton.qml
@@ -0,0 +1,103 @@
+// 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 UM 1.1 as UM
+import Cura 1.0 as Cura
+
+Button
+{
+ id: machineSelectorButton
+
+ width: parent.width
+ height: UM.Theme.getSize("action_button").height
+ leftPadding: UM.Theme.getSize("thick_margin").width
+ rightPadding: UM.Theme.getSize("thick_margin").width
+ checkable: true
+ hoverEnabled: true
+
+ property var outputDevice: null
+ property var printerTypesList: []
+
+ function updatePrinterTypesList()
+ {
+ printerTypesList = (checked && (outputDevice != null)) ? outputDevice.uniquePrinterTypes : []
+ }
+
+ contentItem: Item
+ {
+ width: machineSelectorButton.width - machineSelectorButton.leftPadding
+ height: UM.Theme.getSize("action_button").height
+
+ Label
+ {
+ id: buttonText
+ anchors
+ {
+ left: parent.left
+ right: printerTypes.left
+ verticalCenter: parent.verticalCenter
+ }
+ text: machineSelectorButton.text
+ color: UM.Theme.getColor("text")
+ font: UM.Theme.getFont("medium")
+ visible: text != ""
+ renderType: Text.NativeRendering
+ verticalAlignment: Text.AlignVCenter
+ elide: Text.ElideRight
+ }
+
+ Row
+ {
+ id: printerTypes
+ width: childrenRect.width
+
+ anchors
+ {
+ right: parent.right
+ verticalCenter: parent.verticalCenter
+ }
+ spacing: UM.Theme.getSize("narrow_margin").width
+
+ Repeater
+ {
+ model: printerTypesList
+ delegate: Cura.PrinterTypeLabel
+ {
+ text: Cura.MachineManager.getAbbreviatedMachineName(modelData)
+ }
+ }
+ }
+ }
+
+ background: Rectangle
+ {
+ id: backgroundRect
+ color: machineSelectorButton.hovered ? UM.Theme.getColor("action_button_hovered") : "transparent"
+ radius: UM.Theme.getSize("action_button_radius").width
+ border.width: UM.Theme.getSize("default_lining").width
+ border.color: machineSelectorButton.checked ? UM.Theme.getColor("primary") : "transparent"
+ }
+
+ onClicked:
+ {
+ toggleContent()
+ Cura.MachineManager.setActiveMachine(model.id)
+ }
+
+ Connections
+ {
+ target: outputDevice
+ onUniqueConfigurationsChanged: updatePrinterTypesList()
+ }
+
+ Connections
+ {
+ target: Cura.MachineManager
+ onOutputDevicesChanged: updatePrinterTypesList()
+ }
+
+ Component.onCompleted: updatePrinterTypesList()
+}
diff --git a/resources/qml/PrinterSelector/MachineSelectorList.qml b/resources/qml/PrinterSelector/MachineSelectorList.qml
new file mode 100644
index 0000000000..5fd3515cd3
--- /dev/null
+++ b/resources/qml/PrinterSelector/MachineSelectorList.qml
@@ -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.3
+
+import UM 1.2 as UM
+import Cura 1.0 as Cura
+
+ListView
+{
+ id: listView
+ model: Cura.GlobalStacksModel {}
+ section.property: "hasRemoteConnection"
+ property real contentHeight: childrenRect.height
+
+ section.delegate: Label
+ {
+ text: section == "true" ? catalog.i18nc("@label", "Connected printers") : catalog.i18nc("@label", "Preset printers")
+ width: parent.width
+ height: UM.Theme.getSize("action_button").height
+ leftPadding: UM.Theme.getSize("default_margin").width
+ renderType: Text.NativeRendering
+ font: UM.Theme.getFont("medium")
+ color: UM.Theme.getColor("text_medium")
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ delegate: MachineSelectorButton
+ {
+ text: model.name
+ width: listView.width
+ outputDevice: Cura.MachineManager.printerOutputDevices.length >= 1 ? Cura.MachineManager.printerOutputDevices[0] : null
+
+ checked:
+ {
+ // If the machine has a remote connection
+ var result = Cura.MachineManager.activeMachineId == model.id
+ if (Cura.MachineManager.activeMachineHasRemoteConnection)
+ {
+ result |= Cura.MachineManager.activeMachineNetworkGroupName == model.metadata["connect_group_name"]
+ }
+ return result
+ }
+ }
+}
diff --git a/resources/qml/PrinterTypeLabel.qml b/resources/qml/PrinterTypeLabel.qml
new file mode 100644
index 0000000000..cfc9e56513
--- /dev/null
+++ b/resources/qml/PrinterTypeLabel.qml
@@ -0,0 +1,35 @@
+// 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 UM 1.1 as UM
+
+// This component creates a label with the abbreviated name of a printer, with a rectangle surrounding the label.
+// It is created in a separated place in order to be reused whenever needed.
+Item
+{
+ property alias text: printerTypeLabel.text
+
+ width: UM.Theme.getSize("printer_type_label").width
+ height: UM.Theme.getSize("printer_type_label").height
+
+ Rectangle
+ {
+ anchors.fill: parent
+ color: UM.Theme.getColor("printer_type_label_background")
+ radius: UM.Theme.getSize("checkbox_radius").width
+ }
+
+ Label
+ {
+ id: printerTypeLabel
+ text: "CFFFP" // As an abbreviated name of the Custom FFF Printer
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.horizontalCenter: parent.horizontalCenter
+ renderType: Text.NativeRendering
+ font: UM.Theme.getFont("default")
+ color: UM.Theme.getColor("text")
+ }
+}
\ No newline at end of file
diff --git a/resources/qml/RoundedRectangle.qml b/resources/qml/RoundedRectangle.qml
new file mode 100644
index 0000000000..3ca05e2125
--- /dev/null
+++ b/resources/qml/RoundedRectangle.qml
@@ -0,0 +1,72 @@
+import QtQuick 2.7
+
+import UM 1.2 as UM
+
+// The rounded rectangle works mostly like a regular rectangle, but provides the option to have rounded corners on only one side of the rectangle.
+Item
+{
+ id: roundedRectangle
+ // As per the regular rectangle
+ property color color: "transparent"
+
+ // As per regular rectangle
+ property int radius: UM.Theme.getSize("default_radius").width
+
+ // On what side should the corners be shown 5 can be used if no radius is needed.
+ // 1 is down, 2 is left, 3 is up and 4 is right.
+ property int cornerSide: RoundedRectangle.Direction.None
+
+ // Simple object to ensure that border.width and border.color work
+ property BorderGroup border: BorderGroup {}
+
+ enum Direction
+ {
+ None = 0,
+ Down = 1,
+ Left = 2,
+ Up = 3,
+ Right = 4,
+ All = 5
+ }
+
+ Rectangle
+ {
+ id: background
+ anchors.fill: parent
+ radius: cornerSide != RoundedRectangle.Direction.None ? parent.radius : 0
+ color: parent.color
+ border.width: parent.border.width
+ border.color: parent.border.color
+ }
+
+ // The item that covers 2 of the corners to make them not rounded.
+ Rectangle
+ {
+ visible: cornerSide != RoundedRectangle.Direction.None && cornerSide != RoundedRectangle.Direction.All
+ height: cornerSide % 2 ? parent.radius: parent.height
+ width: cornerSide % 2 ? parent.width : parent.radius
+ color: parent.color
+ anchors
+ {
+ right: cornerSide == RoundedRectangle.Direction.Left ? parent.right: undefined
+ bottom: cornerSide == RoundedRectangle.Direction.Up ? parent.bottom: undefined
+ }
+
+ border.width: parent.border.width
+ border.color: parent.border.color
+
+ Rectangle
+ {
+ color: roundedRectangle.color
+ height: cornerSide % 2 ? roundedRectangle.border.width: roundedRectangle.height - 2 * roundedRectangle.border.width
+ width: cornerSide % 2 ? roundedRectangle.width - 2 * roundedRectangle.border.width: roundedRectangle.border.width
+ anchors
+ {
+ right: cornerSide == RoundedRectangle.Direction.Right ? parent.right : undefined
+ bottom: cornerSide == RoundedRectangle.Direction.Down ? parent.bottom: undefined
+ horizontalCenter: cornerSide % 2 ? parent.horizontalCenter: undefined
+ verticalCenter: cornerSide % 2 ? undefined: parent.verticalCenter
+ }
+ }
+ }
+}
diff --git a/resources/qml/SaveButton.qml b/resources/qml/SaveButton.qml
deleted file mode 100644
index 2a0a523026..0000000000
--- a/resources/qml/SaveButton.qml
+++ /dev/null
@@ -1,409 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.7
-import QtQuick.Controls 1.1
-import QtQuick.Controls.Styles 1.1
-import QtQuick.Layouts 1.1
-
-import UM 1.1 as UM
-import Cura 1.0 as Cura
-
-Item {
- id: base;
- UM.I18nCatalog { id: catalog; name:"cura"}
-
- property real progress: UM.Backend.progress
- property int backendState: UM.Backend.state
- property bool activity: CuraApplication.platformActivity
-
- property alias buttonRowWidth: saveRow.width
-
- property string fileBaseName
- property string statusText:
- {
- if(!activity)
- {
- return catalog.i18nc("@label:PrintjobStatus", "Please load a 3D model");
- }
-
- if (base.backendState == "undefined") {
- return ""
- }
-
- switch(base.backendState)
- {
- case 1:
- return catalog.i18nc("@label:PrintjobStatus", "Ready to slice");
- case 2:
- return catalog.i18nc("@label:PrintjobStatus", "Slicing...");
- case 3:
- return catalog.i18nc("@label:PrintjobStatus %1 is target operation","Ready to %1").arg(UM.OutputDeviceManager.activeDeviceShortDescription);
- case 4:
- return catalog.i18nc("@label:PrintjobStatus", "Unable to Slice");
- case 5:
- return catalog.i18nc("@label:PrintjobStatus", "Slicing unavailable");
- default:
- return "";
- }
- }
-
- function sliceOrStopSlicing() {
- try {
- if ([1, 5].indexOf(base.backendState) != -1) {
- CuraApplication.backend.forceSlice();
- } else {
- CuraApplication.backend.stopSlicing();
- }
- } catch (e) {
- console.log('Could not start or stop slicing', e)
- }
- }
-
- Label {
- id: statusLabel
- width: parent.width - 2 * UM.Theme.getSize("sidebar_margin").width
- anchors.top: parent.top
- anchors.left: parent.left
- anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width
-
- color: UM.Theme.getColor("text")
- font: UM.Theme.getFont("default_bold")
- text: statusText;
- }
-
- Rectangle {
- id: progressBar
- width: parent.width - 2 * UM.Theme.getSize("sidebar_margin").width
- height: UM.Theme.getSize("progressbar").height
- anchors.top: statusLabel.bottom
- anchors.topMargin: Math.round(UM.Theme.getSize("sidebar_margin").height / 4)
- anchors.left: parent.left
- anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width
- radius: UM.Theme.getSize("progressbar_radius").width
- color: UM.Theme.getColor("progressbar_background")
-
- Rectangle {
- width: Math.max(parent.width * base.progress)
- height: parent.height
- color: UM.Theme.getColor("progressbar_control")
- radius: UM.Theme.getSize("progressbar_radius").width
- visible: (base.backendState != "undefined" && base.backendState == 2) ? true : false
- }
- }
-
- // Shortcut for "save as/print/..."
- Action {
- shortcut: "Ctrl+P"
- onTriggered:
- {
- // only work when the button is enabled
- if (saveToButton.enabled) {
- saveToButton.clicked();
- }
- // prepare button
- if (prepareButton.enabled) {
- sliceOrStopSlicing();
- }
- }
- }
-
- Item {
- id: saveRow
- width: {
- // using childrenRect.width directly causes a binding loop, because setting the width affects the childrenRect
- var children_width = UM.Theme.getSize("default_margin").width;
- for (var index in children)
- {
- var child = children[index];
- if(child.visible)
- {
- children_width += child.width + child.anchors.rightMargin;
- }
- }
- return Math.min(children_width, base.width - UM.Theme.getSize("sidebar_margin").width);
- }
- height: saveToButton.height
- anchors.bottom: parent.bottom
- anchors.bottomMargin: UM.Theme.getSize("sidebar_margin").height
- anchors.right: parent.right
- clip: true
-
- Row {
- id: additionalComponentsRow
- anchors.top: parent.top
- anchors.right: saveToButton.visible ? saveToButton.left : (prepareButton.visible ? prepareButton.left : parent.right)
- anchors.rightMargin: UM.Theme.getSize("default_margin").width
-
- spacing: UM.Theme.getSize("default_margin").width
- }
-
- Component.onCompleted: {
- saveRow.addAdditionalComponents("saveButton")
- }
-
- Connections {
- target: CuraApplication
- onAdditionalComponentsChanged: saveRow.addAdditionalComponents("saveButton")
- }
-
- function addAdditionalComponents (areaId) {
- if(areaId == "saveButton") {
- for (var component in CuraApplication.additionalComponents["saveButton"]) {
- CuraApplication.additionalComponents["saveButton"][component].parent = additionalComponentsRow
- }
- }
- }
-
- Connections {
- target: UM.Preferences
- onPreferenceChanged:
- {
- var autoSlice = UM.Preferences.getValue("general/auto_slice");
- prepareButton.autoSlice = autoSlice;
- saveToButton.autoSlice = autoSlice;
- }
- }
-
- // Prepare button, only shows if auto_slice is off
- Button {
- id: prepareButton
-
- tooltip: [1, 5].indexOf(base.backendState) != -1 ? catalog.i18nc("@info:tooltip","Slice current printjob") : catalog.i18nc("@info:tooltip","Cancel slicing process")
- // 1 = not started, 2 = Processing
- enabled: base.backendState != "undefined" && ([1, 2].indexOf(base.backendState) != -1) && base.activity
- visible: base.backendState != "undefined" && !autoSlice && ([1, 2, 4].indexOf(base.backendState) != -1) && base.activity
- property bool autoSlice
- height: UM.Theme.getSize("save_button_save_to_button").height
-
- anchors.top: parent.top
- anchors.right: parent.right
- anchors.rightMargin: UM.Theme.getSize("sidebar_margin").width
-
- // 1 = not started, 4 = error, 5 = disabled
- text: [1, 4, 5].indexOf(base.backendState) != -1 ? catalog.i18nc("@label:Printjob", "Prepare") : catalog.i18nc("@label:Printjob", "Cancel")
- onClicked:
- {
- sliceOrStopSlicing();
- }
-
- style: ButtonStyle {
- background: Rectangle
- {
- border.width: UM.Theme.getSize("default_lining").width
- border.color:
- {
- if(!control.enabled)
- return UM.Theme.getColor("action_button_disabled_border");
- else if(control.pressed)
- return UM.Theme.getColor("action_button_active_border");
- else if(control.hovered)
- return UM.Theme.getColor("action_button_hovered_border");
- else
- return UM.Theme.getColor("action_button_border");
- }
- color:
- {
- if(!control.enabled)
- return UM.Theme.getColor("action_button_disabled");
- else if(control.pressed)
- return UM.Theme.getColor("action_button_active");
- else if(control.hovered)
- return UM.Theme.getColor("action_button_hovered");
- else
- return UM.Theme.getColor("action_button");
- }
-
- Behavior on color { ColorAnimation { duration: 50; } }
-
- implicitWidth: actualLabel.contentWidth + (UM.Theme.getSize("sidebar_margin").width * 2)
-
- Label {
- id: actualLabel
- anchors.centerIn: parent
- color:
- {
- if(!control.enabled)
- return UM.Theme.getColor("action_button_disabled_text");
- else if(control.pressed)
- return UM.Theme.getColor("action_button_active_text");
- else if(control.hovered)
- return UM.Theme.getColor("action_button_hovered_text");
- else
- return UM.Theme.getColor("action_button_text");
- }
- font: UM.Theme.getFont("action_button")
- text: control.text;
- }
- }
- label: Item { }
- }
- }
-
- Button {
- id: saveToButton
-
- tooltip: UM.OutputDeviceManager.activeDeviceDescription;
- // 3 = done, 5 = disabled
- enabled: base.backendState != "undefined" && (base.backendState == 3 || base.backendState == 5) && base.activity == true
- visible: base.backendState != "undefined" && autoSlice || ((base.backendState == 3 || base.backendState == 5) && base.activity == true)
- property bool autoSlice
- height: UM.Theme.getSize("save_button_save_to_button").height
-
- anchors.top: parent.top
- anchors.right: deviceSelectionMenu.visible ? deviceSelectionMenu.left : parent.right
- anchors.rightMargin: deviceSelectionMenu.visible ? -3 * UM.Theme.getSize("default_lining").width : UM.Theme.getSize("sidebar_margin").width
-
- text: UM.OutputDeviceManager.activeDeviceShortDescription
- onClicked:
- {
- forceActiveFocus();
- UM.OutputDeviceManager.requestWriteToDevice(UM.OutputDeviceManager.activeDevice, PrintInformation.jobName,
- { "filter_by_machine": true, "preferred_mimetypes": Cura.MachineManager.activeMachine.preferred_output_file_formats });
- }
-
- style: ButtonStyle {
- background: Rectangle
- {
- border.width: UM.Theme.getSize("default_lining").width
- border.color:
- {
- if(!control.enabled)
- return UM.Theme.getColor("action_button_disabled_border");
- else if(control.pressed)
- return UM.Theme.getColor("print_button_ready_pressed_border");
- else if(control.hovered)
- return UM.Theme.getColor("print_button_ready_hovered_border");
- else
- return UM.Theme.getColor("print_button_ready_border");
- }
- color:
- {
- if(!control.enabled)
- return UM.Theme.getColor("action_button_disabled");
- else if(control.pressed)
- return UM.Theme.getColor("print_button_ready_pressed");
- else if(control.hovered)
- return UM.Theme.getColor("print_button_ready_hovered");
- else
- return UM.Theme.getColor("print_button_ready");
- }
-
- Behavior on color { ColorAnimation { duration: 50; } }
-
- implicitWidth: actualLabel.contentWidth + (UM.Theme.getSize("sidebar_margin").width * 2)
-
- Label {
- id: actualLabel
- anchors.centerIn: parent
- color:
- {
- if(!control.enabled)
- return UM.Theme.getColor("action_button_disabled_text");
- else if(control.pressed)
- return UM.Theme.getColor("print_button_ready_text");
- else if(control.hovered)
- return UM.Theme.getColor("print_button_ready_text");
- else
- return UM.Theme.getColor("print_button_ready_text");
- }
- font: UM.Theme.getFont("action_button")
- text: control.text;
- }
- }
- label: Item { }
- }
- }
-
- Button {
- id: deviceSelectionMenu
- tooltip: catalog.i18nc("@info:tooltip","Select the active output device");
- anchors.top: parent.top
- anchors.right: parent.right
-
- anchors.rightMargin: UM.Theme.getSize("sidebar_margin").width
- width: UM.Theme.getSize("save_button_save_to_button").height
- height: UM.Theme.getSize("save_button_save_to_button").height
- // 3 = Done, 5 = Disabled
- enabled: base.backendState != "undefined" && (base.backendState == 3 || base.backendState == 5) && base.activity == true
- visible: base.backendState != "undefined" && (devicesModel.deviceCount > 1) && (base.backendState == 3 || base.backendState == 5) && base.activity == true
-
-
- style: ButtonStyle {
- background: Rectangle {
- id: deviceSelectionIcon
- border.width: UM.Theme.getSize("default_lining").width
- border.color:
- {
- if(!control.enabled)
- return UM.Theme.getColor("action_button_disabled_border");
- else if(control.pressed)
- return UM.Theme.getColor("print_button_ready_pressed_border");
- else if(control.hovered)
- return UM.Theme.getColor("print_button_ready_hovered_border");
- else
- return UM.Theme.getColor("print_button_ready_border");
- }
- color:
- {
- if(!control.enabled)
- return UM.Theme.getColor("action_button_disabled");
- else if(control.pressed)
- return UM.Theme.getColor("print_button_ready_pressed");
- else if(control.hovered)
- return UM.Theme.getColor("print_button_ready_hovered");
- else
- return UM.Theme.getColor("print_button_ready");
- }
- Behavior on color { ColorAnimation { duration: 50; } }
- anchors.left: parent.left
- anchors.leftMargin: Math.round(UM.Theme.getSize("save_button_text_margin").width / 2);
- width: parent.height
- height: parent.height
-
- UM.RecolorImage {
- anchors.verticalCenter: parent.verticalCenter
- anchors.horizontalCenter: parent.horizontalCenter
- width: UM.Theme.getSize("standard_arrow").width
- height: UM.Theme.getSize("standard_arrow").height
- sourceSize.width: width
- sourceSize.height: height
- color:
- {
- if(!control.enabled)
- return UM.Theme.getColor("action_button_disabled_text");
- else if(control.pressed)
- return UM.Theme.getColor("print_button_ready_text");
- else if(control.hovered)
- return UM.Theme.getColor("print_button_ready_text");
- else
- return UM.Theme.getColor("print_button_ready_text");
- }
- source: UM.Theme.getIcon("arrow_bottom");
- }
- }
- label: Label{ }
- }
-
- menu: Menu {
- id: devicesMenu;
- Instantiator {
- model: devicesModel;
- MenuItem {
- text: model.description
- checkable: true;
- checked: model.id == UM.OutputDeviceManager.activeDevice;
- exclusiveGroup: devicesMenuGroup;
- onTriggered: {
- UM.OutputDeviceManager.setActiveDevice(model.id);
- }
- }
- onObjectAdded: devicesMenu.insertItem(index, object)
- onObjectRemoved: devicesMenu.removeItem(object)
- }
- ExclusiveGroup { id: devicesMenuGroup; }
- }
- }
- UM.OutputDevicesModel { id: devicesModel; }
- }
-}
diff --git a/resources/qml/SecondaryButton.qml b/resources/qml/SecondaryButton.qml
new file mode 100644
index 0000000000..f03d8acdfa
--- /dev/null
+++ b/resources/qml/SecondaryButton.qml
@@ -0,0 +1,20 @@
+// Copyright (c) 2018 Ultimaker B.V.
+// Cura is released under the terms of the LGPLv3 or higher.
+
+import QtQuick 2.2
+
+import UM 1.4 as UM
+import Cura 1.1 as Cura
+
+
+Cura.ActionButton
+{
+ shadowEnabled: true
+ shadowColor: enabled ? UM.Theme.getColor("secondary_button_shadow"): UM.Theme.getColor("action_button_disabled_shadow")
+ color: UM.Theme.getColor("secondary_button")
+ textColor: UM.Theme.getColor("secondary_button_text")
+ outlineColor: "transparent"
+ disabledColor: UM.Theme.getColor("action_button_disabled")
+ textDisabledColor: UM.Theme.getColor("action_button_disabled_text")
+ hoverColor: UM.Theme.getColor("secondary_button_hover")
+}
\ No newline at end of file
diff --git a/resources/qml/Settings/SettingCategory.qml b/resources/qml/Settings/SettingCategory.qml
index e3202323eb..1e88867889 100644
--- a/resources/qml/Settings/SettingCategory.qml
+++ b/resources/qml/Settings/SettingCategory.qml
@@ -12,48 +12,39 @@ Button
id: base
anchors.left: parent.left
anchors.right: parent.right
- anchors.leftMargin: UM.Theme.getSize("sidebar_margin").width
- anchors.rightMargin: UM.Theme.getSize("sidebar_margin").width
+ // To avoid overlaping with the scrollBars
+ anchors.rightMargin: 2 * UM.Theme.getSize("thin_margin").width
+ hoverEnabled: true
+
background: Rectangle
{
id: backgroundRectangle
implicitHeight: UM.Theme.getSize("section").height
- color: {
- if (base.color) {
- return base.color;
- } else if (!base.enabled) {
- return UM.Theme.getColor("setting_category_disabled");
- } else if (base.hovered && base.checkable && base.checked) {
- return UM.Theme.getColor("setting_category_active_hover");
- } else if (base.pressed || (base.checkable && base.checked)) {
- return UM.Theme.getColor("setting_category_active");
- } else if (base.hovered) {
- return UM.Theme.getColor("setting_category_hover");
- } else {
- return UM.Theme.getColor("setting_category");
+ color:
+ {
+ if (base.color)
+ {
+ return base.color
}
+ else if (!base.enabled)
+ {
+ return UM.Theme.getColor("setting_category_disabled")
+ }
+ else if (base.hovered && base.checkable && base.checked)
+ {
+ return UM.Theme.getColor("setting_category_active_hover")
+ }
+ else if (base.pressed || (base.checkable && base.checked))
+ {
+ return UM.Theme.getColor("setting_category_active")
+ }
+ else if (base.hovered)
+ {
+ return UM.Theme.getColor("setting_category_hover")
+ }
+ return UM.Theme.getColor("setting_category")
}
Behavior on color { ColorAnimation { duration: 50; } }
- Rectangle
- {
- id: backgroundLiningRectangle
- height: UM.Theme.getSize("default_lining").height
- width: parent.width
- anchors.bottom: parent.bottom
- color: {
- if (!base.enabled) {
- return UM.Theme.getColor("setting_category_disabled_border");
- } else if ((base.hovered || base.activeFocus) && base.checkable && base.checked) {
- return UM.Theme.getColor("setting_category_active_hover_border");
- } else if (base.pressed || (base.checkable && base.checked)) {
- return UM.Theme.getColor("setting_category_active_border");
- } else if (base.hovered || base.activeFocus) {
- return UM.Theme.getColor("setting_category_hover_border");
- } else {
- return UM.Theme.getColor("setting_category_border");
- }
- }
- }
}
signal showTooltip(string text)
@@ -65,40 +56,47 @@ Button
property var focusItem: base
- contentItem: Item {
+ contentItem: Item
+ {
anchors.fill: parent
- anchors.left: parent.left
- Label {
+ Label
+ {
id: settingNameLabel
anchors
{
left: parent.left
leftMargin: 2 * UM.Theme.getSize("default_margin").width + UM.Theme.getSize("section_icon").width
- right: parent.right;
- verticalCenter: parent.verticalCenter;
+ right: parent.right
+ verticalCenter: parent.verticalCenter
}
text: definition.label
textFormat: Text.PlainText
renderType: Text.NativeRendering
- font: UM.Theme.getFont("setting_category")
+ font: UM.Theme.getFont("medium_bold")
color:
{
- if (!base.enabled) {
- return UM.Theme.getColor("setting_category_disabled_text");
- } else if ((base.hovered || base.activeFocus) && base.checkable && base.checked) {
- return UM.Theme.getColor("setting_category_active_hover_text");
- } else if (base.pressed || (base.checkable && base.checked)) {
- return UM.Theme.getColor("setting_category_active_text");
- } else if (base.hovered || base.activeFocus) {
- return UM.Theme.getColor("setting_category_hover_text");
- } else {
- return UM.Theme.getColor("setting_category_text");
+ if (!base.enabled)
+ {
+ return UM.Theme.getColor("setting_category_disabled_text")
+ } else if ((base.hovered || base.activeFocus) && base.checkable && base.checked)
+ {
+ return UM.Theme.getColor("setting_category_active_hover_text")
+ } else if (base.pressed || (base.checkable && base.checked))
+ {
+ return UM.Theme.getColor("setting_category_active_text")
+ } else if (base.hovered || base.activeFocus)
+ {
+ return UM.Theme.getColor("setting_category_hover_text")
+ } else
+ {
+ return UM.Theme.getColor("setting_category_text")
}
}
fontSizeMode: Text.HorizontalFit
minimumPointSize: 8
}
+
UM.RecolorImage
{
id: category_arrow
@@ -107,22 +105,8 @@ Button
anchors.rightMargin: UM.Theme.getSize("default_margin").width
width: UM.Theme.getSize("standard_arrow").width
height: UM.Theme.getSize("standard_arrow").height
- sourceSize.width: width
sourceSize.height: width
- color:
- {
- if (!base.enabled) {
- return UM.Theme.getColor("setting_category_disabled_text");
- } else if ((base.hovered || base.activeFocus) && base.checkable && base.checked) {
- return UM.Theme.getColor("setting_category_active_hover_text");
- } else if (base.pressed || (base.checkable && base.checked)) {
- return UM.Theme.getColor("setting_category_active_text");
- } else if (base.hovered || base.activeFocus) {
- return UM.Theme.getColor("setting_category_hover_text");
- } else {
- return UM.Theme.getColor("setting_category_text");
- }
- }
+ color: UM.Theme.getColor("setting_control_button")
source: base.checked ? UM.Theme.getIcon("arrow_bottom") : UM.Theme.getIcon("arrow_left")
}
}
@@ -132,24 +116,30 @@ Button
id: icon
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
- anchors.leftMargin: UM.Theme.getSize("default_margin").width
+ anchors.leftMargin: UM.Theme.getSize("thin_margin").width
color:
{
- if (!base.enabled) {
- return UM.Theme.getColor("setting_category_disabled_text");
- } else if((base.hovered || base.activeFocus) && base.checkable && base.checked) {
- return UM.Theme.getColor("setting_category_active_hover_text");
- } else if(base.pressed || (base.checkable && base.checked)) {
- return UM.Theme.getColor("setting_category_active_text");
- } else if(base.hovered || base.activeFocus) {
- return UM.Theme.getColor("setting_category_hover_text");
- } else {
- return UM.Theme.getColor("setting_category_text");
+ if (!base.enabled)
+ {
+ return UM.Theme.getColor("setting_category_disabled_text")
}
+ else if((base.hovered || base.activeFocus) && base.checkable && base.checked)
+ {
+ return UM.Theme.getColor("setting_category_active_hover_text")
+ }
+ else if(base.pressed || (base.checkable && base.checked))
+ {
+ return UM.Theme.getColor("setting_category_active_text")
+ }
+ else if(base.hovered || base.activeFocus)
+ {
+ return UM.Theme.getColor("setting_category_hover_text")
+ }
+ return UM.Theme.getColor("setting_category_text")
}
source: UM.Theme.getIcon(definition.icon)
- width: UM.Theme.getSize("section_icon").width;
- height: UM.Theme.getSize("section_icon").height;
+ width: UM.Theme.getSize("section_icon").width
+ height: UM.Theme.getSize("section_icon").height
sourceSize.width: width + 15 * screenScaleFactor
sourceSize.height: width + 15 * screenScaleFactor
}
@@ -159,31 +149,28 @@ Button
onClicked:
{
- if (definition.expanded) {
- settingDefinitionsModel.collapse(definition.key);
- } else {
- settingDefinitionsModel.expandRecursive(definition.key);
+ if (definition.expanded)
+ {
+ settingDefinitionsModel.collapse(definition.key)
+ }
+ else
+ {
+ settingDefinitionsModel.expandRecursive(definition.key)
}
//Set focus so that tab navigation continues from this point on.
//NB: This must be set AFTER collapsing/expanding the category so that the scroll position is correct.
- forceActiveFocus();
+ forceActiveFocus()
}
onActiveFocusChanged:
{
- if(activeFocus)
+ if (activeFocus)
{
- base.focusReceived();
+ base.focusReceived()
}
}
- Keys.onTabPressed:
- {
- base.setActiveFocusToNextSetting(true)
- }
- Keys.onBacktabPressed:
- {
- base.setActiveFocusToNextSetting(false)
- }
+ Keys.onTabPressed: base.setActiveFocusToNextSetting(true)
+ Keys.onBacktabPressed: base.setActiveFocusToNextSetting(false)
UM.SimpleButton
{
@@ -193,9 +180,10 @@ Button
height: Math.round(base.height * 0.6)
width: Math.round(base.height * 0.6)
- anchors {
+ anchors
+ {
right: inheritButton.visible ? inheritButton.left : parent.right
- // use 1.9 as the factor because there is a 0.1 difference between the settings and inheritance warning icons
+ // Use 1.9 as the factor because there is a 0.1 difference between the settings and inheritance warning icons
rightMargin: inheritButton.visible ? Math.round(UM.Theme.getSize("default_margin").width / 2) : category_arrow.width + Math.round(UM.Theme.getSize("default_margin").width * 1.9)
verticalCenter: parent.verticalCenter
}
@@ -204,9 +192,7 @@ Button
hoverColor: UM.Theme.getColor("setting_control_button_hover")
iconSource: UM.Theme.getIcon("settings")
- onClicked: {
- Cura.Actions.configureSettingVisibility.trigger(definition)
- }
+ onClicked: Cura.Actions.configureSettingVisibility.trigger(definition)
}
UM.SimpleButton
@@ -239,24 +225,18 @@ Button
onClicked:
{
- settingDefinitionsModel.expandRecursive(definition.key);
- base.checked = true;
- base.showAllHiddenInheritedSettings(definition.key);
+ settingDefinitionsModel.expandRecursive(definition.key)
+ base.checked = true
+ base.showAllHiddenInheritedSettings(definition.key)
}
color: UM.Theme.getColor("setting_control_button")
hoverColor: UM.Theme.getColor("setting_control_button_hover")
iconSource: UM.Theme.getIcon("notice")
- onEntered:
- {
- base.showTooltip(catalog.i18nc("@label","Some hidden settings use values different from their normal calculated value.\n\nClick to make these settings visible."))
- }
+ onEntered: base.showTooltip(catalog.i18nc("@label","Some hidden settings use values different from their normal calculated value.\n\nClick to make these settings visible."))
- onExited:
- {
- base.hideTooltip();
- }
+ onExited: base.hideTooltip()
UM.I18nCatalog { id: catalog; name: "cura" }
}
diff --git a/resources/qml/Settings/SettingCheckBox.qml b/resources/qml/Settings/SettingCheckBox.qml
index d37754d27c..0c7321d08a 100644
--- a/resources/qml/Settings/SettingCheckBox.qml
+++ b/resources/qml/Settings/SettingCheckBox.qml
@@ -28,37 +28,40 @@ SettingItem
// 3: material -> user changed material in materials page
// 4: variant
// 5: machine
- var value;
- if ((base.resolve != "None") && (stackLevel != 0) && (stackLevel != 1)) {
+ var value
+ if ((base.resolve != "None") && (stackLevel != 0) && (stackLevel != 1))
+ {
// We have a resolve function. Indicates that the setting is not settable per extruder and that
// we have to choose between the resolved value (default) and the global value
// (if user has explicitly set this).
- value = base.resolve;
- } else {
- value = propertyProvider.properties.value;
+ value = base.resolve
+ }
+ else
+ {
+ value = propertyProvider.properties.value
}
switch(value)
{
case "True":
- return true;
+ return true
case "False":
- return false;
+ return false
default:
- return value;
+ return value
}
}
Keys.onSpacePressed:
{
- forceActiveFocus();
- propertyProvider.setPropertyValue("value", !checked);
+ forceActiveFocus()
+ propertyProvider.setPropertyValue("value", !checked)
}
onClicked:
{
- forceActiveFocus();
- propertyProvider.setPropertyValue("value", !checked);
+ forceActiveFocus()
+ propertyProvider.setPropertyValue("value", !checked)
}
Keys.onTabPressed:
@@ -72,9 +75,9 @@ SettingItem
onActiveFocusChanged:
{
- if(activeFocus)
+ if (activeFocus)
{
- base.focusReceived();
+ base.focusReceived()
}
}
@@ -90,37 +93,38 @@ SettingItem
color:
{
- if(!enabled)
+ if (!enabled)
{
return UM.Theme.getColor("setting_control_disabled")
}
- if(control.containsMouse || control.activeFocus)
+ if (control.containsMouse || control.activeFocus)
{
return UM.Theme.getColor("setting_control_highlight")
}
return UM.Theme.getColor("setting_control")
}
+ radius: UM.Theme.getSize("setting_control_radius").width
border.width: UM.Theme.getSize("default_lining").width
border.color:
{
- if(!enabled)
+ if (!enabled)
{
return UM.Theme.getColor("setting_control_disabled_border")
}
- if(control.containsMouse || control.activeFocus)
+ if (control.containsMouse || control.activeFocus)
{
return UM.Theme.getColor("setting_control_border_highlight")
}
return UM.Theme.getColor("setting_control_border")
}
- UM.RecolorImage {
+ UM.RecolorImage
+ {
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenter: parent.horizontalCenter
width: Math.round(parent.width / 2.5)
height: Math.round(parent.height / 2.5)
- sourceSize.width: width
sourceSize.height: width
color: !enabled ? UM.Theme.getColor("setting_control_disabled_text") : UM.Theme.getColor("setting_control_text");
source: UM.Theme.getIcon("check")
diff --git a/resources/qml/Settings/SettingComboBox.qml b/resources/qml/Settings/SettingComboBox.qml
index 76d458e427..a287e0c3ce 100644
--- a/resources/qml/Settings/SettingComboBox.qml
+++ b/resources/qml/Settings/SettingComboBox.qml
@@ -35,6 +35,7 @@ SettingItem
return UM.Theme.getColor("setting_control")
}
+ radius: UM.Theme.getSize("setting_control_radius").width
border.width: UM.Theme.getSize("default_lining").width
border.color:
{
@@ -62,7 +63,7 @@ SettingItem
sourceSize.width: width + 5 * screenScaleFactor
sourceSize.height: width + 5 * screenScaleFactor
- color: UM.Theme.getColor("setting_control_text")
+ color: UM.Theme.getColor("setting_control_button")
}
contentItem: Label
diff --git a/resources/qml/Settings/SettingExtruder.qml b/resources/qml/Settings/SettingExtruder.qml
index a9427f863a..6d39192de7 100644
--- a/resources/qml/Settings/SettingExtruder.qml
+++ b/resources/qml/Settings/SettingExtruder.qml
@@ -17,10 +17,16 @@ SettingItem
id: control
anchors.fill: parent
- model: Cura.ExtrudersModel
+ property var extrudersModel: CuraApplication.getExtrudersModel()
+
+ model: extrudersModel
+
+ Connections
{
- onModelChanged: {
- control.color = getItem(control.currentIndex).color;
+ target: extrudersModel
+ onModelChanged:
+ {
+ control.color = extrudersModel.getItem(control.currentIndex).color
}
}
@@ -104,7 +110,7 @@ SettingItem
sourceSize.width: width + 5 * screenScaleFactor
sourceSize.height: width + 5 * screenScaleFactor
- color: UM.Theme.getColor("setting_control_text");
+ color: UM.Theme.getColor("setting_control_button");
}
background: Rectangle
@@ -113,14 +119,15 @@ SettingItem
{
if (!enabled)
{
- return UM.Theme.getColor("setting_control_disabled");
+ return UM.Theme.getColor("setting_control_disabled")
}
if (control.hovered || base.activeFocus)
{
- return UM.Theme.getColor("setting_control_highlight");
+ return UM.Theme.getColor("setting_control_highlight")
}
- return UM.Theme.getColor("setting_control");
+ return UM.Theme.getColor("setting_control")
}
+ radius: UM.Theme.getSize("setting_control_radius").width
border.width: UM.Theme.getSize("default_lining").width
border.color:
{
@@ -153,20 +160,18 @@ SettingItem
elide: Text.ElideLeft
verticalAlignment: Text.AlignVCenter
- background: Rectangle
+ background: UM.RecolorImage
{
id: swatch
- height: Math.round(UM.Theme.getSize("setting_control").height / 2)
+ height: Math.round(parent.height / 2)
width: height
-
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
- anchors.margins: Math.round(UM.Theme.getSize("default_margin").width / 4)
-
- border.width: UM.Theme.getSize("default_lining").width
- border.color: enabled ? UM.Theme.getColor("setting_control_border") : UM.Theme.getColor("setting_control_disabled_border")
- radius: Math.round(width / 2)
+ anchors.rightMargin: UM.Theme.getSize("thin_margin").width
+ sourceSize.width: width
+ sourceSize.height: height
+ source: UM.Theme.getIcon("extruder_button")
color: control.color
}
}
@@ -219,20 +224,18 @@ SettingItem
verticalAlignment: Text.AlignVCenter
rightPadding: swatch.width + UM.Theme.getSize("setting_unit_margin").width
- background: Rectangle
+ background: UM.RecolorImage
{
id: swatch
- height: Math.round(UM.Theme.getSize("setting_control").height / 2)
+ height: Math.round(parent.height / 2)
width: height
-
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
- anchors.margins: Math.round(UM.Theme.getSize("default_margin").width / 4)
-
- border.width: UM.Theme.getSize("default_lining").width
- border.color: enabled ? UM.Theme.getColor("setting_control_border") : UM.Theme.getColor("setting_control_disabled_border")
- radius: Math.round(width / 2)
+ anchors.rightMargin: UM.Theme.getSize("thin_margin").width
+ sourceSize.width: width
+ sourceSize.height: height
+ source: UM.Theme.getIcon("extruder_button")
color: control.model.getItem(index).color
}
}
diff --git a/resources/qml/Settings/SettingItem.qml b/resources/qml/Settings/SettingItem.qml
index 785562cff5..4dd53f8663 100644
--- a/resources/qml/Settings/SettingItem.qml
+++ b/resources/qml/Settings/SettingItem.qml
@@ -10,10 +10,15 @@ import Cura 1.0 as Cura
import "."
-Item {
- id: base;
+Item
+{
+ id: base
height: UM.Theme.getSize("section").height
+ anchors.left: parent.left
+ anchors.right: parent.right
+ // To avoid overlaping with the scrollBars
+ anchors.rightMargin: 2 * UM.Theme.getSize("thin_margin").width
property alias contents: controlContainer.children
property alias hovered: mouse.containsMouse
@@ -43,25 +48,25 @@ Item {
var affected_by = settingDefinitionsModel.getRequires(definition.key, "value")
var affected_by_list = ""
- for(var i in affected_by)
+ for (var i in affected_by)
{
affected_by_list += "
%1
\n".arg(affected_by[i].label)
}
var affects_list = ""
- for(var i in affects)
+ for (var i in affects)
{
affects_list += "