Merge branch 'master' into feature_firmware_updater

This commit is contained in:
fieldOfView 2018-09-28 11:32:28 +02:00
commit 7c23a4e187
337 changed files with 88130 additions and 18157 deletions

View file

@ -225,7 +225,7 @@ class ThreeMFReader(MeshReader):
except Exception:
Logger.logException("e", "An exception occurred in 3mf reader.")
return []
return None
return result

View file

@ -85,14 +85,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
def __init__(self) -> None:
super().__init__()
MimeTypeDatabase.addMimeType(
MimeType(
name="application/x-curaproject+xml",
comment="Cura Project File",
suffixes=["curaproject.3mf"]
)
)
self._supported_extensions = [".3mf"]
self._dialog = WorkspaceDialog()
self._3mf_mesh_reader = None
@ -726,8 +718,6 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
nodes = []
base_file_name = os.path.basename(file_name)
if base_file_name.endswith(".curaproject.3mf"):
base_file_name = base_file_name[:base_file_name.rfind(".curaproject.3mf")]
self.setWorkspaceName(base_file_name)
return nodes
@ -944,7 +934,7 @@ class ThreeMFWorkspaceReader(WorkspaceReader):
root_material_id)
if material_node is not None and material_node.getContainer() is not None:
extruder_stack.material = material_node.getContainer()
extruder_stack.material = material_node.getContainer() # type: InstanceContainer
def _applyChangesToMachine(self, global_stack, extruder_stack_dict):
# Clear all first

View file

@ -18,11 +18,7 @@ catalog = i18nCatalog("cura")
def getMetaData() -> Dict:
# Workaround for osx not supporting double file extensions correctly.
if Platform.isOSX():
workspace_extension = "3mf"
else:
workspace_extension = "curaproject.3mf"
workspace_extension = "3mf"
metaData = {}
if "3MFReader.ThreeMFReader" in sys.modules:

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides support for reading 3MF files.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -15,11 +15,7 @@ from UM.Platform import Platform
i18n_catalog = i18nCatalog("uranium")
def getMetaData():
# Workarround for osx not supporting double file extensions correctly.
if Platform.isOSX():
workspace_extension = "3mf"
else:
workspace_extension = "curaproject.3mf"
workspace_extension = "3mf"
metaData = {}
@ -36,7 +32,7 @@ def getMetaData():
"output": [{
"extension": workspace_extension,
"description": i18n_catalog.i18nc("@item:inlistbox", "Cura Project 3MF file"),
"mime_type": "application/x-curaproject+xml",
"mime_type": "application/vnd.ms-package.3dmanufacturing-3dmodel+xml",
"mode": ThreeMFWorkspaceWriter.ThreeMFWorkspaceWriter.OutputMode.BinaryMode
}]
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides support for writing 3MF files.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -1,3 +1,103 @@
[3.5.0]
*Monitor page
The monitor page of Ultimaker Cura has been remodeled for better consistency with the Cura Connect Print jobs interface. This means less switching between interfaces, and more control from within Ultimaker Cura.
*Open recent projects
Project files can now be found in the Open Recent menu.
*New tool hotkeys
New hotkeys have been assigned for quick toggling between the translate (T), scale (S), rotate (R) and mirror (M) tools.
*Project files use 3MF only
A 3MF extension is now used for project files. The .curaproject extension is no longer used.
*Camera maximum zoom
The maximum zoom has been adjusted to scale with the size of the selected printer. This fixes third-party printers with huge build volumes to be correctly visible.
*Corrected width of layer number box
The layer number indicator in the layer view now displays numbers above 999 correctly.
*Materials preferences
This screen has been redesigned to improve user experience. Materials can now be set as a favorites, so they can be easily accessed in the material selection panel at the top-right of the screen.
*Installed packages checkmark
Packages that are already installed in the Toolbox are now have a checkmark for easy reference.
*Mac OSX save dialog
The save dialog has been restored to its native behavior and bugs have been fixed.
*Removed .gz extension
Saving compressed g-code files from the save dialog has been removed because of incompatibility with MacOS. If sending jobs over Wi-Fi, g-code is still compressed.
*Updates to Chinese translations
Improved and updated Chinese translations. Contributed by MarmaladeForMeat.
*Save project
Saving the project no longer triggers the project to reslice.
*File menu
The Save option in the file menu now saves project files. The export option now saves other types of files, such as STL.
*Improved processing of overhang walls
Overhang walls are detected and printed with different speeds. It will not start a perimeter on an overhanging wall. The quality of overhanging walls may be improved by printing those at a different speed. Contributed by smartavionics.
*Prime tower reliability
The prime tower has been improved for better reliability. This is especially useful when printing with two materials that do not adhere well.
*Support infill line direction
The support infill lines can now be rotated to increase the supporting capabilities and reduce artifacts on the model. This setting rotates existing patterns, like triangle support infill. Contributed by fieldOfView.
*Minimum polygon circumference
Polygons in sliced layers that have a circumference smaller than the setting value will be filtered out. Lower values lead to higher resolution meshes at the cost of increased slicing time. This setting is ideal for very tiny prints with a lot of detail, or for SLA printers. Contributed by cubiq.
*Initial layer support line distance
This setting enables the user to reduce or increase the density of the support initial layer in order to increase or reduce adhesion to the build plate and the overall strength.
*Extra infill wall line count
Adds extra walls around infill. Contributed by BagelOrb.
*Multiply infill
Creates multiple infill lines on the same pattern for sturdier infill. Contributed by BagelOrb.
*Connected infill polygons
Connecting infill lines now also works with concentric and cross infill patterns. The benefit would be stronger infill and more consistent material flow/saving retractions. Contributed by BagelOrb.
*Fan speed override
New setting to modify the fan speed of supported areas. This setting can be found in Support settings > Fan Speed Override when support is enabled. Contributed by smartavionics.
*Minimum wall flow
New setting to define a minimum flow for thin printed walls. Contributed by smartavionics.
*Custom support plugin
A tool downloadable from the toolbox, similar to the support blocker, that adds cubes of support to the model manually by clicking parts of it. Contributed by Lokster.
*Quickly toggle autoslicing
Adds a pause/play button to the progress bar to quickly toggle autoslicing. Contributed by fieldOfview.
*Cura-DuetRRFPlugin
Adds output devices for a Duet RepRapFirmware printer: "Print", "Simulate", and "Upload". Contributed by Kriechi.
*Dremel 3D20
This plugin adds the Dremel printer to Ultimaker Cura. Contributed by Kriechi.
*Bug fixes
- Removed extra M109 commands. Older versions would generate superfluous M109 commands. This has been fixed for better temperature stability when printing.
- Fixed minor mesh handling bugs. A few combinations of modifier meshes now lead to expected behavior.
- Removed unnecessary travels. Connected infill lines are now always printed completely connected, without unnecessary travel moves.
- Removed concentric 3D infill. This infill type has been removed due to lack of reliability.
- Extra skin wall count. Fixed an issue that caused extra print moves with this setting enabled.
- Concentric skin. Small gaps in concentric skin are now filled correctly.
- Order of printed models. The order of a large batch of printed models is now more consistent, instead of random.
*Third party printers
- TiZYX
- Winbo
- Tevo Tornado
- Creality CR-10S
- Wanhao Duplicator
- Deltacomb (update)
- Dacoma (update)
[3.4.1]
*Bug fixes
- Fixed an issue that would occasionally cause an unnecessary extra skin wall to be printed, which increased print time.

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Shows changes since latest checked version.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -179,8 +179,7 @@ class CuraEngineBackend(QObject, Backend):
# This is useful for debugging and used to actually start the engine.
# \return list of commands and args / parameters.
def getEngineCommand(self) -> List[str]:
json_path = Resources.getPath(Resources.DefinitionContainers, "fdmprinter.def.json")
command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), "-j", json_path, ""]
command = [self._application.getPreferences().getValue("backend/location"), "connect", "127.0.0.1:{0}".format(self._port), ""]
parser = argparse.ArgumentParser(prog = "cura", add_help = False)
parser.add_argument("--debug", action = "store_true", default = False, help = "Turn on the debug mode by setting this option.")
@ -343,7 +342,7 @@ class CuraEngineBackend(QObject, Backend):
if not self._global_container_stack:
Logger.log("w", "Global container stack not assigned to CuraEngineBackend!")
return
extruders = list(ExtruderManager.getInstance().getMachineExtruders(self._global_container_stack.getId()))
extruders = ExtruderManager.getInstance().getActiveExtruderStacks()
error_keys = [] #type: List[str]
for extruder in extruders:
error_keys.extend(extruder.getErrorKeys())

View file

@ -178,7 +178,7 @@ class ProcessSlicedLayersJob(Job):
# Find out colors per extruder
global_container_stack = Application.getInstance().getGlobalContainerStack()
manager = ExtruderManager.getInstance()
extruders = list(manager.getMachineExtruders(global_container_stack.getId()))
extruders = manager.getActiveExtruderStacks()
if extruders:
material_color_map = numpy.zeros((len(extruders), 4), dtype=numpy.float32)
for extruder in extruders:

View file

@ -220,8 +220,10 @@ class StartSliceJob(Job):
stack = global_stack
skip_group = False
for node in group:
# Only check if the printing extruder is enabled for printing meshes
is_non_printing_mesh = node.callDecoration("evaluateIsNonPrintingMesh")
extruder_position = node.callDecoration("getActiveExtruderPosition")
if not extruders_enabled[extruder_position]:
if not is_non_printing_mesh and not extruders_enabled[extruder_position]:
skip_group = True
has_model_with_disabled_extruders = True
associated_disabled_extruders.add(extruder_position)
@ -331,7 +333,7 @@ class StartSliceJob(Job):
"-1": self._buildReplacementTokens(global_stack)
}
for extruder_stack in ExtruderManager.getInstance().getMachineExtruders(global_stack.getId()):
for extruder_stack in ExtruderManager.getInstance().getActiveExtruderStacks():
extruder_nr = extruder_stack.getProperty("extruder_nr", "value")
self._all_extruders_settings[str(extruder_nr)] = self._buildReplacementTokens(extruder_stack)
@ -438,8 +440,7 @@ class StartSliceJob(Job):
Job.yieldThread()
# Ensure that the engine is aware what the build extruder is.
if stack.getProperty("machine_extruder_count", "value") > 1:
changed_setting_keys.add("extruder_nr")
changed_setting_keys.add("extruder_nr")
# Get values for all changed settings
for key in changed_setting_keys:

View file

@ -2,7 +2,7 @@
"name": "CuraEngine Backend",
"author": "Ultimaker B.V.",
"description": "Provides the link to the CuraEngine slicing backend.",
"api": 4,
"api": 5,
"version": "1.0.0",
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides support for importing Cura profiles.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides support for exporting Cura profiles.",
"api": 4,
"api": 5,
"i18n-catalog":"cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Checks for firmware updates.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -4,15 +4,22 @@
import gzip
from UM.Mesh.MeshReader import MeshReader #The class we're extending/implementing.
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType #To add the .gcode.gz files to the MIME type database.
from UM.PluginRegistry import PluginRegistry
## A file reader that reads gzipped g-code.
#
# If you're zipping g-code, you might as well use gzip!
class GCodeGzReader(MeshReader):
def __init__(self) -> None:
super().__init__()
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-cura-compressed-gcode-file",
comment = "Cura Compressed GCode File",
suffixes = ["gcode.gz"]
)
)
self._supported_extensions = [".gcode.gz"]
def _read(self, file_name):

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Reads g-code from a compressed archive.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -17,6 +17,10 @@ catalog = i18nCatalog("cura")
#
# If you're zipping g-code, you might as well use gzip!
class GCodeGzWriter(MeshWriter):
def __init__(self) -> None:
super().__init__(add_to_recent_files = False)
## Writes the gzipped g-code to a stream.
#
# Note that even though the function accepts a collection of nodes, the

View file

@ -16,7 +16,8 @@ def getMetaData():
"extension": file_extension,
"description": catalog.i18nc("@item:inlistbox", "Compressed G-code File"),
"mime_type": "application/gzip",
"mode": GCodeGzWriter.GCodeGzWriter.OutputMode.BinaryMode
"mode": GCodeGzWriter.GCodeGzWriter.OutputMode.BinaryMode,
"hide_in_file_dialog": True,
}]
}
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Writes g-code to a compressed archive.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides support for importing profiles from g-code files.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -275,7 +275,7 @@ class FlavorParser:
## For showing correct x, y offsets for each extruder
def _extruderOffsets(self) -> Dict[int, List[float]]:
result = {}
for extruder in ExtruderManager.getInstance().getExtruderStacks():
for extruder in ExtruderManager.getInstance().getActiveExtruderStacks():
result[int(extruder.getMetaData().get("position", "0"))] = [
extruder.getProperty("machine_nozzle_offset_x", "value"),
extruder.getProperty("machine_nozzle_offset_y", "value")]

View file

@ -1,4 +1,5 @@
# Copyright (c) 2017 Aleph Objects, Inc.
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from UM.FileHandler.FileReader import FileReader
@ -11,13 +12,7 @@ catalog = i18nCatalog("cura")
from . import MarlinFlavorParser, RepRapFlavorParser
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-cura-gcode-file",
comment = "Cura GCode File",
suffixes = ["gcode", "gcode.gz"]
)
)
# Class for loading and parsing G-code files
@ -29,7 +24,15 @@ class GCodeReader(MeshReader):
def __init__(self) -> None:
super().__init__()
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-cura-gcode-file",
comment = "Cura GCode File",
suffixes = ["gcode"]
)
)
self._supported_extensions = [".gcode", ".g"]
self._flavor_reader = None
Application.getInstance().getPreferences().addPreference("gcodereader/show_caution", True)

View file

@ -3,6 +3,6 @@
"author": "Victor Larchenko",
"version": "1.0.0",
"description": "Allows loading and displaying G-code files.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -47,7 +47,7 @@ class GCodeWriter(MeshWriter):
_setting_keyword = ";SETTING_"
def __init__(self):
super().__init__()
super().__init__(add_to_recent_files = False)
self._application = Application.getInstance()

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Writes g-code to a file.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Enables ability to generate printable geometry from 2D image files.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides support for importing profiles from legacy Cura versions.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -16,6 +16,8 @@ Cura.MachineAction
property var extrudersModel: Cura.ExtrudersModel{}
property int extruderTabsCount: 0
property var activeMachineId: Cura.MachineManager.activeMachine != null ? Cura.MachineManager.activeMachine.id : ""
Connections
{
target: base.extrudersModel
@ -511,7 +513,7 @@ Cura.MachineAction
}
return "";
}
return Cura.MachineManager.activeMachineId;
return base.activeMachineId
}
key: settingKey
watchedProperties: [ "value", "description" ]
@ -564,7 +566,7 @@ Cura.MachineAction
}
return "";
}
return Cura.MachineManager.activeMachineId;
return base.activeMachineId
}
key: settingKey
watchedProperties: [ "value", "description" ]
@ -655,7 +657,7 @@ Cura.MachineAction
}
return "";
}
return Cura.MachineManager.activeMachineId;
return base.activeMachineId
}
key: settingKey
watchedProperties: [ "value", "options", "description" ]
@ -754,7 +756,7 @@ Cura.MachineAction
}
return "";
}
return Cura.MachineManager.activeMachineId;
return base.activeMachineId
}
key: settingKey
watchedProperties: [ "value", "description" ]
@ -879,7 +881,7 @@ Cura.MachineAction
{
id: machineExtruderCountProvider
containerStackId: Cura.MachineManager.activeMachineId
containerStackId: base.activeMachineId
key: "machine_extruder_count"
watchedProperties: [ "value", "description" ]
storeIndex: manager.containerIndex
@ -889,7 +891,7 @@ Cura.MachineAction
{
id: machineHeadPolygonProvider
containerStackId: Cura.MachineManager.activeMachineId
containerStackId: base.activeMachineId
key: "machine_head_with_fans_polygon"
watchedProperties: [ "value" ]
storeIndex: manager.containerIndex

View file

@ -3,6 +3,6 @@
"author": "fieldOfView",
"version": "1.0.0",
"description": "Provides a way to change machine settings (such as build volume, nozzle size, etc.).",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -2,7 +2,7 @@
"name": "Model Checker",
"author": "Ultimaker B.V.",
"version": "0.1",
"api": 4,
"api": 5,
"description": "Checks models and print configuration for possible printing issues and give suggestions.",
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides a monitor stage in Cura.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -17,7 +17,6 @@ Item {
width: childrenRect.width;
height: childrenRect.height;
property var all_categories_except_support: [ "machine_settings", "resolution", "shell", "infill", "material", "speed",
"travel", "cooling", "platform_adhesion", "dual", "meshfix", "blackmagic", "experimental"]
@ -45,7 +44,7 @@ Item {
UM.SettingPropertyProvider
{
id: meshTypePropertyProvider
containerStackId: Cura.MachineManager.activeMachineId
containerStack: Cura.MachineManager.activeMachine
watchedProperties: [ "enabled" ]
}
@ -518,7 +517,7 @@ Item {
{
id: machineExtruderCount
containerStackId: Cura.MachineManager.activeMachineId
containerStack: Cura.MachineManager.activeMachine
key: "machine_extruder_count"
watchedProperties: [ "value" ]
storeIndex: 0
@ -528,7 +527,7 @@ Item {
{
id: printSequencePropertyProvider
containerStackId: Cura.MachineManager.activeMachineId
containerStack: Cura.MachineManager.activeMachine
key: "print_sequence"
watchedProperties: [ "value" ]
storeIndex: 0

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides the Per Model Settings.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -1,5 +1,6 @@
# Copyright (c) 2015 Jaime van Kessel, Ultimaker B.V.
# Copyright (c) 2018 Jaime van Kessel, Ultimaker B.V.
# The PostProcessingPlugin is released under the terms of the AGPLv3 or higher.
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
from UM.PluginRegistry import PluginRegistry
@ -260,6 +261,9 @@ class PostProcessingPlugin(QObject, Extension):
# Create the plugin dialog component
path = os.path.join(PluginRegistry.getInstance().getPluginPath("PostProcessingPlugin"), "PostProcessingPlugin.qml")
self._view = Application.getInstance().createQmlComponent(path, {"manager": self})
if self._view is None:
Logger.log("e", "Not creating PostProcessing button near save button because the QML component failed to be created.")
return
Logger.log("d", "Post processing view created.")
# Create the save button component
@ -269,6 +273,9 @@ class PostProcessingPlugin(QObject, Extension):
def showPopup(self):
if self._view is None:
self._createView()
if self._view is None:
Logger.log("e", "Not creating PostProcessing window since the QML component failed to be created.")
return
self._view.show()
## Property changed: trigger re-slice

View file

@ -384,7 +384,7 @@ UM.Dialog
UM.SettingPropertyProvider
{
id: inheritStackProvider
containerStackId: Cura.MachineManager.activeMachineId
containerStack: Cura.MachineManager.activeMachine
key: model.key ? model.key : "None"
watchedProperties: [ "limit_to_extruder" ]
}

View file

@ -2,7 +2,7 @@
"name": "Post Processing",
"author": "Ultimaker",
"version": "2.2",
"api": 4,
"api": 5,
"description": "Extension that allows for user created scripts for post processing",
"catalog": "cura"
}

View file

@ -0,0 +1,51 @@
from ..Script import Script
class PauseAtHeightRepRapFirmwareDuet(Script):
def getSettingDataString(self):
return """{
"name": "Pause at height for RepRapFirmware DuetWifi / Duet Ethernet / Duet Maestro",
"key": "PauseAtHeightRepRapFirmwareDuet",
"metadata": {},
"version": 2,
"settings":
{
"pause_height":
{
"label": "Pause height",
"description": "At what height should the pause occur",
"unit": "mm",
"type": "float",
"default_value": 5.0
}
}
}"""
def execute(self, data):
current_z = 0.
pause_z = self.getSettingValueByKey("pause_height")
layers_started = False
for layer_number, layer in enumerate(data):
lines = layer.split("\n")
for line in lines:
if ";LAYER:0" in line:
layers_started = True
continue
if not layers_started:
continue
if self.getValue(line, 'G') == 1 or self.getValue(line, 'G') == 0:
current_z = self.getValue(line, 'Z')
if current_z != None:
if current_z >= pause_z:
prepend_gcode = ";TYPE:CUSTOM\n"
prepend_gcode += "; -- Pause at height (%.2f mm) --\n" % pause_z
prepend_gcode += self.putValue(M = 226) + "\n"
layer = prepend_gcode + layer
data[layer_number] = layer # Override the data of this layer with the modified data
return data
break
return data

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides a prepare stage in Cura.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"description": "Provides removable drive hotplugging and writing support.",
"version": "1.0.0",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -9,7 +9,8 @@ import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM
import Cura 1.0 as Cura
Item {
Item
{
id: sliderRoot
// handle properties
@ -39,40 +40,49 @@ Item {
property real lowerValue: minimumValue
property bool layersVisible: true
property bool manuallyChanged: true // Indicates whether the value was changed manually or during simulation
function getUpperValueFromSliderHandle() {
function getUpperValueFromSliderHandle()
{
return upperHandle.getValue()
}
function setUpperValue(value) {
function setUpperValue(value)
{
upperHandle.setValue(value)
updateRangeHandle()
}
function getLowerValueFromSliderHandle() {
function getLowerValueFromSliderHandle()
{
return lowerHandle.getValue()
}
function setLowerValue(value) {
function setLowerValue(value)
{
lowerHandle.setValue(value)
updateRangeHandle()
}
function updateRangeHandle() {
function updateRangeHandle()
{
rangeHandle.height = lowerHandle.y - (upperHandle.y + upperHandle.height)
}
// set the active handle to show only one label at a time
function setActiveHandle(handle) {
function setActiveHandle(handle)
{
activeHandle = handle
}
function normalizeValue(value) {
function normalizeValue(value)
{
return Math.min(Math.max(value, sliderRoot.minimumValue), sliderRoot.maximumValue)
}
// slider track
Rectangle {
Rectangle
{
id: track
width: sliderRoot.trackThickness
@ -86,7 +96,8 @@ Item {
}
// Range handle
Item {
Item
{
id: rangeHandle
y: upperHandle.y + upperHandle.height
@ -96,7 +107,9 @@ Item {
visible: sliderRoot.layersVisible
// set the new value when dragging
function onHandleDragged () {
function onHandleDragged()
{
sliderRoot.manuallyChanged = true
upperHandle.y = y - upperHandle.height
lowerHandle.y = y + height
@ -109,7 +122,14 @@ Item {
UM.SimulationView.setMinimumLayer(lowerValue)
}
function setValue (value) {
function setValueManually(value)
{
sliderRoot.manuallyChanged = true
upperHandle.setValue(value)
}
function setValue(value)
{
var range = sliderRoot.upperValue - sliderRoot.lowerValue
value = Math.min(value, sliderRoot.maximumValue)
value = Math.max(value, sliderRoot.minimumValue + range)
@ -118,17 +138,20 @@ Item {
UM.SimulationView.setMinimumLayer(value - range)
}
Rectangle {
Rectangle
{
width: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
height: parent.height + sliderRoot.handleSize
anchors.centerIn: parent
color: sliderRoot.rangeHandleColor
}
MouseArea {
MouseArea
{
anchors.fill: parent
drag {
drag
{
target: parent
axis: Drag.YAxis
minimumY: upperHandle.height
@ -139,7 +162,8 @@ Item {
onPressed: sliderRoot.setActiveHandle(rangeHandle)
}
SimulationSliderLabel {
SimulationSliderLabel
{
id: rangleHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
@ -152,12 +176,13 @@ Item {
maximumValue: sliderRoot.maximumValue
value: sliderRoot.upperValue
busy: UM.SimulationView.busy
setValue: rangeHandle.setValue // connect callback functions
setValue: rangeHandle.setValueManually // connect callback functions
}
}
// Upper handle
Rectangle {
Rectangle
{
id: upperHandle
y: sliderRoot.height - (sliderRoot.minimumRangeHandleSize + 2 * sliderRoot.handleSize)
@ -168,10 +193,13 @@ Item {
color: upperHandleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.upperHandleColor
visible: sliderRoot.layersVisible
function onHandleDragged () {
function onHandleDragged()
{
sliderRoot.manuallyChanged = true
// don't allow the lower handle to be heigher than the upper handle
if (lowerHandle.y - (y + height) < sliderRoot.minimumRangeHandleSize) {
if (lowerHandle.y - (y + height) < sliderRoot.minimumRangeHandleSize)
{
lowerHandle.y = y + height + sliderRoot.minimumRangeHandleSize
}
@ -183,15 +211,23 @@ Item {
}
// get the upper value based on the slider position
function getValue () {
function getValue()
{
var result = y / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize))
result = sliderRoot.maximumValue + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumValue))
result = sliderRoot.roundValues ? Math.round(result) : result
return result
}
function setValueManually(value)
{
sliderRoot.manuallyChanged = true
upperHandle.setValue(value)
}
// set the slider position based on the upper value
function setValue (value) {
function setValue(value)
{
// Normalize values between range, since using arrow keys will create out-of-the-range values
value = sliderRoot.normalizeValue(value)
@ -209,10 +245,12 @@ Item {
Keys.onDownPressed: upperHandleLabel.setValue(upperHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
// dragging
MouseArea {
MouseArea
{
anchors.fill: parent
drag {
drag
{
target: parent
axis: Drag.YAxis
minimumY: 0
@ -220,13 +258,15 @@ Item {
}
onPositionChanged: parent.onHandleDragged()
onPressed: {
onPressed:
{
sliderRoot.setActiveHandle(upperHandle)
upperHandleLabel.forceActiveFocus()
}
}
SimulationSliderLabel {
SimulationSliderLabel
{
id: upperHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
@ -239,12 +279,13 @@ Item {
maximumValue: sliderRoot.maximumValue
value: sliderRoot.upperValue
busy: UM.SimulationView.busy
setValue: upperHandle.setValue // connect callback functions
setValue: upperHandle.setValueManually // connect callback functions
}
}
// Lower handle
Rectangle {
Rectangle
{
id: lowerHandle
y: sliderRoot.height - sliderRoot.handleSize
@ -256,10 +297,13 @@ Item {
visible: sliderRoot.layersVisible
function onHandleDragged () {
function onHandleDragged()
{
sliderRoot.manuallyChanged = true
// don't allow the upper handle to be lower than the lower handle
if (y - (upperHandle.y + upperHandle.height) < sliderRoot.minimumRangeHandleSize) {
if (y - (upperHandle.y + upperHandle.height) < sliderRoot.minimumRangeHandleSize)
{
upperHandle.y = y - (upperHandle.heigth + sliderRoot.minimumRangeHandleSize)
}
@ -271,15 +315,24 @@ Item {
}
// get the lower value from the current slider position
function getValue () {
function getValue()
{
var result = (y - (sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize)) / (sliderRoot.height - (2 * sliderRoot.handleSize + sliderRoot.minimumRangeHandleSize));
result = sliderRoot.maximumValue - sliderRoot.minimumRange + result * (sliderRoot.minimumValue - (sliderRoot.maximumValue - sliderRoot.minimumRange))
result = sliderRoot.roundValues ? Math.round(result) : result
return result
}
function setValueManually(value)
{
sliderRoot.manuallyChanged = true
lowerHandle.setValue(value)
}
// set the slider position based on the lower value
function setValue (value) {
function setValue(value)
{
// Normalize values between range, since using arrow keys will create out-of-the-range values
value = sliderRoot.normalizeValue(value)
@ -297,10 +350,12 @@ Item {
Keys.onDownPressed: lowerHandleLabel.setValue(lowerHandleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
// dragging
MouseArea {
MouseArea
{
anchors.fill: parent
drag {
drag
{
target: parent
axis: Drag.YAxis
minimumY: upperHandle.height + sliderRoot.minimumRangeHandleSize
@ -308,13 +363,15 @@ Item {
}
onPositionChanged: parent.onHandleDragged()
onPressed: {
onPressed:
{
sliderRoot.setActiveHandle(lowerHandle)
lowerHandleLabel.forceActiveFocus()
}
}
SimulationSliderLabel {
SimulationSliderLabel
{
id: lowerHandleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
@ -327,7 +384,7 @@ Item {
maximumValue: sliderRoot.maximumValue
value: sliderRoot.lowerValue
busy: UM.SimulationView.busy
setValue: lowerHandle.setValue // connect callback functions
setValue: lowerHandle.setValueManually // connect callback functions
}
}
}
}

View file

@ -9,7 +9,8 @@ import QtQuick.Controls.Styles 1.1
import UM 1.0 as UM
import Cura 1.0 as Cura
Item {
Item
{
id: sliderRoot
// handle properties
@ -34,26 +35,32 @@ Item {
property real handleValue: maximumValue
property bool pathsVisible: true
property bool manuallyChanged: true // Indicates whether the value was changed manually or during simulation
function getHandleValueFromSliderHandle () {
function getHandleValueFromSliderHandle()
{
return handle.getValue()
}
function setHandleValue (value) {
function setHandleValue(value)
{
handle.setValue(value)
updateRangeHandle()
}
function updateRangeHandle () {
function updateRangeHandle()
{
rangeHandle.width = handle.x - sliderRoot.handleSize
}
function normalizeValue(value) {
function normalizeValue(value)
{
return Math.min(Math.max(value, sliderRoot.minimumValue), sliderRoot.maximumValue)
}
// slider track
Rectangle {
Rectangle
{
id: track
width: sliderRoot.width - sliderRoot.handleSize
@ -67,7 +74,8 @@ Item {
}
// Progress indicator
Item {
Item
{
id: rangeHandle
x: handle.width
@ -76,7 +84,8 @@ Item {
anchors.verticalCenter: sliderRoot.verticalCenter
visible: sliderRoot.pathsVisible
Rectangle {
Rectangle
{
height: sliderRoot.trackThickness - 2 * sliderRoot.trackBorderWidth
width: parent.width + sliderRoot.handleSize
anchors.centerIn: parent
@ -85,7 +94,8 @@ Item {
}
// Handle
Rectangle {
Rectangle
{
id: handle
x: sliderRoot.handleSize
@ -96,7 +106,9 @@ Item {
color: handleLabel.activeFocus ? sliderRoot.handleActiveColor : sliderRoot.handleColor
visible: sliderRoot.pathsVisible
function onHandleDragged () {
function onHandleDragged()
{
sliderRoot.manuallyChanged = true
// update the range handle
sliderRoot.updateRangeHandle()
@ -106,15 +118,23 @@ Item {
}
// get the value based on the slider position
function getValue () {
function getValue()
{
var result = x / (sliderRoot.width - sliderRoot.handleSize)
result = result * sliderRoot.maximumValue
result = sliderRoot.roundValues ? Math.round(result) : result
return result
}
function setValueManually(value)
{
sliderRoot.manuallyChanged = true
handle.setValue(value)
}
// set the slider position based on the value
function setValue (value) {
function setValue(value)
{
// Normalize values between range, since using arrow keys will create out-of-the-range values
value = sliderRoot.normalizeValue(value)
@ -132,23 +152,23 @@ Item {
Keys.onLeftPressed: handleLabel.setValue(handleLabel.value - ((event.modifiers & Qt.ShiftModifier) ? 10 : 1))
// dragging
MouseArea {
MouseArea
{
anchors.fill: parent
drag {
drag
{
target: parent
axis: Drag.XAxis
minimumX: 0
maximumX: sliderRoot.width - sliderRoot.handleSize
}
onPressed: {
handleLabel.forceActiveFocus()
}
onPressed: handleLabel.forceActiveFocus()
onPositionChanged: parent.onHandleDragged()
}
SimulationSliderLabel {
SimulationSliderLabel
{
id: handleLabel
height: sliderRoot.handleSize + UM.Theme.getSize("default_margin").height
@ -162,7 +182,7 @@ Item {
maximumValue: sliderRoot.maximumValue
value: sliderRoot.handleValue
busy: UM.SimulationView.busy
setValue: handle.setValue // connect callback functions
setValue: handle.setValueManually // connect callback functions
}
}
}

View file

@ -44,12 +44,11 @@ UM.PointingRectangle {
id: valueLabel
anchors {
left: parent.left
leftMargin: Math.round(UM.Theme.getSize("default_margin").width / 2)
verticalCenter: parent.verticalCenter
horizontalCenter: parent.horizontalCenter
}
width: maximumValue.toString().length * 12 * screenScaleFactor
width: (maximumValue.toString().length + 1) * 10 * screenScaleFactor
text: sliderLabelRoot.value + startFrom // the current handle value, add 1 because layers is an array
horizontalAlignment: TextInput.AlignRight

View file

@ -18,10 +18,13 @@ from UM.Platform import Platform
from UM.PluginRegistry import PluginRegistry
from UM.Resources import Resources
from UM.Scene.Iterator.DepthFirstIterator import DepthFirstIterator
from UM.Scene.Selection import Selection
from UM.Signal import Signal
from UM.View.GL.OpenGL import OpenGL
from UM.View.GL.OpenGLContext import OpenGLContext
from UM.View.View import View
from UM.i18n import i18nCatalog
from cura.Scene.ConvexHullNode import ConvexHullNode
@ -30,11 +33,20 @@ from cura.CuraApplication import CuraApplication
from .NozzleNode import NozzleNode
from .SimulationPass import SimulationPass
from .SimulationViewProxy import SimulationViewProxy
import numpy
import os.path
from typing import Optional, TYPE_CHECKING, List
if TYPE_CHECKING:
from UM.Scene.SceneNode import SceneNode
from UM.Scene.Scene import Scene
from UM.View.GL.ShaderProgram import ShaderProgram
from UM.View.RenderPass import RenderPass
from UM.Settings.ContainerStack import ContainerStack
catalog = i18nCatalog("cura")
import numpy
import os.path
## View used to display g-code paths.
class SimulationView(View):
@ -44,7 +56,7 @@ class SimulationView(View):
LAYER_VIEW_TYPE_FEEDRATE = 2
LAYER_VIEW_TYPE_THICKNESS = 3
def __init__(self):
def __init__(self) -> None:
super().__init__()
self._max_layers = 0
@ -64,21 +76,21 @@ class SimulationView(View):
self._busy = False
self._simulation_running = False
self._ghost_shader = None
self._layer_pass = None
self._composite_pass = None
self._ghost_shader = None # type: Optional["ShaderProgram"]
self._layer_pass = None # type: Optional[SimulationPass]
self._composite_pass = None # type: Optional[RenderPass]
self._old_layer_bindings = None
self._simulationview_composite_shader = None
self._simulationview_composite_shader = None # type: Optional["ShaderProgram"]
self._old_composite_shader = None
self._global_container_stack = None
self._global_container_stack = None # type: Optional[ContainerStack]
self._proxy = SimulationViewProxy()
self._controller.getScene().getRoot().childrenChanged.connect(self._onSceneChanged)
self._resetSettings()
self._legend_items = None
self._show_travel_moves = False
self._nozzle_node = None
self._nozzle_node = None # type: Optional[NozzleNode]
Application.getInstance().getPreferences().addPreference("view/top_layer_count", 5)
Application.getInstance().getPreferences().addPreference("view/only_show_top_layers", False)
@ -102,29 +114,29 @@ 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"))
def _evaluateCompatibilityMode(self):
def _evaluateCompatibilityMode(self) -> bool:
return OpenGLContext.isLegacyOpenGL() or bool(Application.getInstance().getPreferences().getValue("view/force_layer_view_compatibility_mode"))
def _resetSettings(self):
self._layer_view_type = 0 # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness
def _resetSettings(self) -> None:
self._layer_view_type = 0 # type: int # 0 is material color, 1 is color by linetype, 2 is speed, 3 is layer thickness
self._extruder_count = 0
self._extruder_opacity = [1.0, 1.0, 1.0, 1.0]
self._show_travel_moves = 0
self._show_helpers = 1
self._show_skin = 1
self._show_infill = 1
self._show_travel_moves = False
self._show_helpers = True
self._show_skin = True
self._show_infill = True
self.resetLayerData()
def getActivity(self):
def getActivity(self) -> bool:
return self._activity
def setActivity(self, activity):
def setActivity(self, activity: bool) -> None:
if self._activity == activity:
return
self._activity = activity
self.activityChanged.emit()
def getSimulationPass(self):
def getSimulationPass(self) -> SimulationPass:
if not self._layer_pass:
# Currently the RenderPass constructor requires a size > 0
# This should be fixed in RenderPass's constructor.
@ -133,30 +145,30 @@ class SimulationView(View):
self._layer_pass.setSimulationView(self)
return self._layer_pass
def getCurrentLayer(self):
def getCurrentLayer(self) -> int:
return self._current_layer_num
def getMinimumLayer(self):
def getMinimumLayer(self) -> int:
return self._minimum_layer_num
def getMaxLayers(self):
def getMaxLayers(self) -> int:
return self._max_layers
def getCurrentPath(self):
def getCurrentPath(self) -> int:
return self._current_path_num
def getMinimumPath(self):
def getMinimumPath(self) -> int:
return self._minimum_path_num
def getMaxPaths(self):
def getMaxPaths(self) -> int:
return self._max_paths
def getNozzleNode(self):
def getNozzleNode(self) -> NozzleNode:
if not self._nozzle_node:
self._nozzle_node = NozzleNode()
return self._nozzle_node
def _onSceneChanged(self, node):
def _onSceneChanged(self, node: "SceneNode") -> None:
if node.getMeshData() is None:
self.resetLayerData()
@ -164,21 +176,21 @@ class SimulationView(View):
self.calculateMaxLayers()
self.calculateMaxPathsOnLayer(self._current_layer_num)
def isBusy(self):
def isBusy(self) -> bool:
return self._busy
def setBusy(self, busy):
def setBusy(self, busy: bool) -> None:
if busy != self._busy:
self._busy = busy
self.busyChanged.emit()
def isSimulationRunning(self):
def isSimulationRunning(self) -> bool:
return self._simulation_running
def setSimulationRunning(self, running):
def setSimulationRunning(self, running: bool) -> None:
self._simulation_running = running
def resetLayerData(self):
def resetLayerData(self) -> None:
self._current_layer_mesh = None
self._current_layer_jumps = None
self._max_feedrate = sys.float_info.min
@ -186,7 +198,7 @@ class SimulationView(View):
self._max_thickness = sys.float_info.min
self._min_thickness = sys.float_info.max
def beginRendering(self):
def beginRendering(self) -> None:
scene = self.getController().getScene()
renderer = self.getRenderer()
@ -204,7 +216,7 @@ class SimulationView(View):
if (node.getMeshData()) and node.isVisible():
renderer.queueNode(node, transparent = True, shader = self._ghost_shader)
def setLayer(self, value):
def setLayer(self, value: int) -> None:
if self._current_layer_num != value:
self._current_layer_num = value
if self._current_layer_num < 0:
@ -218,7 +230,7 @@ class SimulationView(View):
self.currentLayerNumChanged.emit()
def setMinimumLayer(self, value):
def setMinimumLayer(self, value: int) -> None:
if self._minimum_layer_num != value:
self._minimum_layer_num = value
if self._minimum_layer_num < 0:
@ -232,7 +244,7 @@ class SimulationView(View):
self.currentLayerNumChanged.emit()
def setPath(self, value):
def setPath(self, value: int) -> None:
if self._current_path_num != value:
self._current_path_num = value
if self._current_path_num < 0:
@ -246,7 +258,7 @@ class SimulationView(View):
self.currentPathNumChanged.emit()
def setMinimumPath(self, value):
def setMinimumPath(self, value: int) -> None:
if self._minimum_path_num != value:
self._minimum_path_num = value
if self._minimum_path_num < 0:
@ -263,24 +275,24 @@ class SimulationView(View):
## Set the layer view type
#
# \param layer_view_type integer as in SimulationView.qml and this class
def setSimulationViewType(self, layer_view_type):
def setSimulationViewType(self, layer_view_type: int) -> None:
self._layer_view_type = layer_view_type
self.currentLayerNumChanged.emit()
## Return the layer view type, integer as in SimulationView.qml and this class
def getSimulationViewType(self):
def getSimulationViewType(self) -> int:
return self._layer_view_type
## Set the extruder opacity
#
# \param extruder_nr 0..3
# \param opacity 0.0 .. 1.0
def setExtruderOpacity(self, extruder_nr, opacity):
def setExtruderOpacity(self, extruder_nr: int, opacity: float) -> None:
if 0 <= extruder_nr <= 3:
self._extruder_opacity[extruder_nr] = opacity
self.currentLayerNumChanged.emit()
def getExtruderOpacities(self):
def getExtruderOpacities(self)-> List[float]:
return self._extruder_opacity
def setShowTravelMoves(self, show):
@ -290,46 +302,46 @@ class SimulationView(View):
def getShowTravelMoves(self):
return self._show_travel_moves
def setShowHelpers(self, show):
def setShowHelpers(self, show: bool) -> None:
self._show_helpers = show
self.currentLayerNumChanged.emit()
def getShowHelpers(self):
def getShowHelpers(self) -> bool:
return self._show_helpers
def setShowSkin(self, show):
def setShowSkin(self, show: bool) -> None:
self._show_skin = show
self.currentLayerNumChanged.emit()
def getShowSkin(self):
def getShowSkin(self) -> bool:
return self._show_skin
def setShowInfill(self, show):
def setShowInfill(self, show: bool) -> None:
self._show_infill = show
self.currentLayerNumChanged.emit()
def getShowInfill(self):
def getShowInfill(self) -> bool:
return self._show_infill
def getCompatibilityMode(self):
def getCompatibilityMode(self) -> bool:
return self._compatibility_mode
def getExtruderCount(self):
def getExtruderCount(self) -> int:
return self._extruder_count
def getMinFeedrate(self):
def getMinFeedrate(self) -> float:
return self._min_feedrate
def getMaxFeedrate(self):
def getMaxFeedrate(self) -> float:
return self._max_feedrate
def getMinThickness(self):
def getMinThickness(self) -> float:
return self._min_thickness
def getMaxThickness(self):
def getMaxThickness(self) -> float:
return self._max_thickness
def calculateMaxLayers(self):
def calculateMaxLayers(self) -> None:
scene = self.getController().getScene()
self._old_max_layers = self._max_layers
@ -383,7 +395,7 @@ class SimulationView(View):
self.maxLayersChanged.emit()
self._startUpdateTopLayers()
def calculateMaxPathsOnLayer(self, layer_num):
def calculateMaxPathsOnLayer(self, layer_num: int) -> None:
# Update the currentPath
scene = self.getController().getScene()
for node in DepthFirstIterator(scene.getRoot()):
@ -415,10 +427,10 @@ class SimulationView(View):
def getProxy(self, engine, script_engine):
return self._proxy
def endRendering(self):
def endRendering(self) -> None:
pass
def event(self, event):
def event(self, event) -> bool:
modifiers = QApplication.keyboardModifiers()
ctrl_is_active = modifiers & Qt.ControlModifier
shift_is_active = modifiers & Qt.ShiftModifier
@ -447,7 +459,7 @@ class SimulationView(View):
if QOpenGLContext.currentContext() is None:
Logger.log("d", "current context of OpenGL is empty on Mac OS X, will try to create shaders later")
CuraApplication.getInstance().callLater(lambda e=event: self.event(e))
return
return False
# Make sure the SimulationPass is created
layer_pass = self.getSimulationPass()
@ -480,11 +492,14 @@ class SimulationView(View):
Application.getInstance().globalContainerStackChanged.disconnect(self._onGlobalStackChanged)
if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
self._nozzle_node.setParent(None)
if self._nozzle_node:
self._nozzle_node.setParent(None)
self.getRenderer().removeRenderPass(self._layer_pass)
self._composite_pass.setLayerBindings(self._old_layer_bindings)
self._composite_pass.setCompositeShader(self._old_composite_shader)
if self._composite_pass:
self._composite_pass.setLayerBindings(self._old_layer_bindings)
self._composite_pass.setCompositeShader(self._old_composite_shader)
return False
def getCurrentLayerMesh(self):
return self._current_layer_mesh
@ -492,7 +507,7 @@ class SimulationView(View):
def getCurrentLayerJumps(self):
return self._current_layer_jumps
def _onGlobalStackChanged(self):
def _onGlobalStackChanged(self) -> None:
if self._global_container_stack:
self._global_container_stack.propertyChanged.disconnect(self._onPropertyChanged)
self._global_container_stack = Application.getInstance().getGlobalContainerStack()
@ -504,17 +519,17 @@ class SimulationView(View):
else:
self._wireprint_warning_message.hide()
def _onPropertyChanged(self, key, property_name):
def _onPropertyChanged(self, key: str, property_name: str) -> None:
if key == "wireframe_enabled" and property_name == "value":
if self._global_container_stack.getProperty("wireframe_enabled", "value"):
if self._global_container_stack and self._global_container_stack.getProperty("wireframe_enabled", "value"):
self._wireprint_warning_message.show()
else:
self._wireprint_warning_message.hide()
def _onCurrentLayerNumChanged(self):
def _onCurrentLayerNumChanged(self) -> None:
self.calculateMaxPathsOnLayer(self._current_layer_num)
def _startUpdateTopLayers(self):
def _startUpdateTopLayers(self) -> None:
if not self._compatibility_mode:
return
@ -525,10 +540,10 @@ class SimulationView(View):
self.setBusy(True)
self._top_layers_job = _CreateTopLayersJob(self._controller.getScene(), self._current_layer_num, self._solid_layers)
self._top_layers_job.finished.connect(self._updateCurrentLayerMesh)
self._top_layers_job.start()
self._top_layers_job.finished.connect(self._updateCurrentLayerMesh) # type: ignore # mypy doesn't understand the whole private class thing that's going on here.
self._top_layers_job.start() # type: ignore
def _updateCurrentLayerMesh(self, job):
def _updateCurrentLayerMesh(self, job: "_CreateTopLayersJob") -> None:
self.setBusy(False)
if not job.getResult():
@ -539,9 +554,9 @@ class SimulationView(View):
self._current_layer_jumps = job.getResult().get("jumps")
self._controller.getScene().sceneChanged.emit(self._controller.getScene().getRoot())
self._top_layers_job = None
self._top_layers_job = None # type: Optional["_CreateTopLayersJob"]
def _updateWithPreferences(self):
def _updateWithPreferences(self) -> None:
self._solid_layers = int(Application.getInstance().getPreferences().getValue("view/top_layer_count"))
self._only_show_top_layers = bool(Application.getInstance().getPreferences().getValue("view/only_show_top_layers"))
self._compatibility_mode = self._evaluateCompatibilityMode()
@ -563,7 +578,7 @@ class SimulationView(View):
self._startUpdateTopLayers()
self.preferencesChanged.emit()
def _onPreferencesChanged(self, preference):
def _onPreferencesChanged(self, preference: str) -> None:
if preference not in {
"view/top_layer_count",
"view/only_show_top_layers",
@ -581,7 +596,7 @@ class SimulationView(View):
class _CreateTopLayersJob(Job):
def __init__(self, scene, layer_number, solid_layers):
def __init__(self, scene: "Scene", layer_number: int, solid_layers: int) -> None:
super().__init__()
self._scene = scene
@ -589,7 +604,7 @@ class _CreateTopLayersJob(Job):
self._solid_layers = solid_layers
self._cancel = False
def run(self):
def run(self) -> None:
layer_data = None
for node in DepthFirstIterator(self._scene.getRoot()):
layer_data = node.callDecoration("getLayerData")
@ -638,6 +653,6 @@ class _CreateTopLayersJob(Job):
self.setResult({"layers": layer_mesh.build(), "jumps": jump_mesh})
def cancel(self):
def cancel(self) -> None:
self._cancel = True
super().cancel()

View file

@ -1,4 +1,4 @@
// Copyright (c) 2017 Ultimaker B.V.
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.4
@ -12,30 +12,43 @@ import Cura 1.0 as Cura
Item
{
id: base
width: {
if (UM.SimulationView.compatibilityMode) {
width:
{
if (UM.SimulationView.compatibilityMode)
{
return UM.Theme.getSize("layerview_menu_size_compatibility").width;
} else {
}
else
{
return UM.Theme.getSize("layerview_menu_size").width;
}
}
height: {
if (viewSettings.collapsed) {
if (UM.SimulationView.compatibilityMode) {
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) {
}
else if (UM.SimulationView.compatibilityMode)
{
return UM.Theme.getSize("layerview_menu_size_compatibility").height;
} else if (UM.Preferences.getValue("layerview/layer_view_type") == 0) {
}
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 {
}
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: {
property var buttonTarget:
{
if(parent != null)
{
var force_binding = parent.y; // ensure this gets reevaluated when the panel moves
@ -44,7 +57,8 @@ Item
return Qt.point(0,0)
}
Rectangle {
Rectangle
{
id: layerViewMenu
anchors.right: parent.right
anchors.top: parent.top
@ -83,7 +97,8 @@ Item
}
}
ColumnLayout {
ColumnLayout
{
id: viewSettings
property bool collapsed: false
@ -195,7 +210,8 @@ Item
width: width
}
Connections {
Connections
{
target: UM.Preferences
onPreferenceChanged:
{
@ -212,18 +228,22 @@ Item
}
}
Repeater {
Repeater
{
model: Cura.ExtrudersModel{}
CheckBox {
CheckBox
{
id: extrudersModelCheckBox
checked: viewSettings.extruder_opacities[index] > 0.5 || viewSettings.extruder_opacities[index] == undefined || viewSettings.extruder_opacities[index] == ""
onClicked: {
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 {
Rectangle
{
anchors.verticalCenter: parent.verticalCenter
anchors.right: extrudersModelCheckBox.right
width: UM.Theme.getSize("layerview_legend_size").width
@ -253,8 +273,10 @@ Item
}
}
Repeater {
model: ListModel {
Repeater
{
model: ListModel
{
id: typesLegendModel
Component.onCompleted:
{
@ -285,13 +307,16 @@ Item
}
}
CheckBox {
CheckBox
{
id: legendModelCheckBox
checked: model.initialValue
onClicked: {
onClicked:
{
UM.Preferences.setValue(model.preference, checked);
}
Rectangle {
Rectangle
{
anchors.verticalCenter: parent.verticalCenter
anchors.right: legendModelCheckBox.right
width: UM.Theme.getSize("layerview_legend_size").width
@ -320,18 +345,22 @@ Item
}
}
CheckBox {
CheckBox
{
checked: viewSettings.only_show_top_layers
onClicked: {
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 {
CheckBox
{
checked: viewSettings.top_layer_count == 5
onClicked: {
onClicked:
{
UM.Preferences.setValue("view/top_layer_count", checked ? 5 : 1);
}
text: catalog.i18nc("@label", "Show 5 Detailed Layers On Top")
@ -339,8 +368,10 @@ Item
style: UM.Theme.styles.checkbox
}
Repeater {
model: ListModel {
Repeater
{
model: ListModel
{
id: typesLegendModelNoCheck
Component.onCompleted:
{
@ -355,11 +386,13 @@ Item
}
}
Label {
Label
{
text: label
visible: viewSettings.show_legend
id: typesLegendModelLabel
Rectangle {
Rectangle
{
anchors.verticalCenter: parent.verticalCenter
anchors.right: typesLegendModelLabel.right
width: UM.Theme.getSize("layerview_legend_size").width
@ -378,30 +411,37 @@ Item
}
// Text for the minimum, maximum and units for the feedrates and layer thickness
Item {
Item
{
id: gradientLegend
visible: viewSettings.show_gradient
width: parent.width
height: UM.Theme.getSize("layerview_row").height
anchors {
anchors
{
topMargin: UM.Theme.getSize("slider_layerview_margin").height
horizontalCenter: parent.horizontalCenter
}
Label {
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) {
function minText()
{
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
{
// Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) {
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) {
if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
{
return parseFloat(UM.SimulationView.getMinThickness()).toFixed(2)
}
}
@ -409,20 +449,25 @@ Item
}
}
Label {
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) {
function unitsText()
{
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
{
// Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) {
if (UM.Preferences.getValue("layerview/layer_view_type") == 2)
{
return "mm/s"
}
// Layer thickness selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 3) {
if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
{
return "mm"
}
}
@ -430,20 +475,25 @@ Item
}
}
Label {
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) {
function maxText()
{
if (UM.SimulationView.layerActivity && CuraApplication.platformActivity)
{
// Feedrate selected
if (UM.Preferences.getValue("layerview/layer_view_type") == 2) {
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) {
if (UM.Preferences.getValue("layerview/layer_view_type") == 3)
{
return parseFloat(UM.SimulationView.getMaxThickness()).toFixed(2)
}
}
@ -453,7 +503,8 @@ Item
}
// Gradient colors for feedrate
Rectangle { // In QML 5.9 can be changed by LinearGradient
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
@ -463,20 +514,25 @@ Item
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 {
gradient: Gradient
{
GradientStop
{
position: 0.000
color: Qt.rgba(1, 0.5, 0, 1)
}
GradientStop {
GradientStop
{
position: 0.625
color: Qt.rgba(0.375, 0.5, 0, 1)
}
GradientStop {
GradientStop
{
position: 0.75
color: Qt.rgba(0.25, 1, 0, 1)
}
GradientStop {
GradientStop
{
position: 1.0
color: Qt.rgba(0, 0, 1, 1)
}
@ -484,7 +540,8 @@ Item
}
// Gradient colors for layer thickness (similar to parula colormap)
Rectangle { // In QML 5.9 can be changed by LinearGradient
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
@ -494,24 +551,30 @@ Item
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 {
gradient: Gradient
{
GradientStop
{
position: 0.000
color: Qt.rgba(1, 1, 0, 1)
}
GradientStop {
GradientStop
{
position: 0.25
color: Qt.rgba(1, 0.75, 0.25, 1)
}
GradientStop {
GradientStop
{
position: 0.5
color: Qt.rgba(0, 0.75, 0.5, 1)
}
GradientStop {
GradientStop
{
position: 0.75
color: Qt.rgba(0, 0.375, 0.75, 1)
}
GradientStop {
GradientStop
{
position: 1.0
color: Qt.rgba(0, 0, 0.5, 1)
}
@ -520,19 +583,22 @@ Item
}
}
Item {
Item
{
id: slidersBox
width: parent.width
visible: UM.SimulationView.layerActivity && CuraApplication.platformActivity
anchors {
anchors
{
top: parent.bottom
topMargin: UM.Theme.getSize("slider_layerview_margin").height
left: parent.left
}
PathSlider {
PathSlider
{
id: pathSlider
height: UM.Theme.getSize("slider_handle").width
@ -553,25 +619,37 @@ Item
rangeColor: UM.Theme.getColor("slider_groove_fill")
// update values when layer data changes
Connections {
Connections
{
target: UM.SimulationView
onMaxPathsChanged: pathSlider.setHandleValue(UM.SimulationView.currentPath)
onCurrentPathChanged: 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: {
Component.onCompleted:
{
pathSlider.setHandleValue(UM.SimulationView.currentPath)
}
}
LayerSlider {
LayerSlider
{
id: layerSlider
width: UM.Theme.getSize("slider_handle").width
height: UM.Theme.getSize("layerview_menu_size").height
anchors {
anchors
{
top: !UM.SimulationView.compatibilityMode ? pathSlider.bottom : parent.top
topMargin: !UM.SimulationView.compatibilityMode ? UM.Theme.getSize("default_margin").height : 0
right: parent.right
@ -593,56 +671,78 @@ Item
handleLabelWidth: UM.Theme.getSize("slider_layerview_background").width
// update values when layer data changes
Connections {
Connections
{
target: UM.SimulationView
onMaxLayersChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer)
onMinimumLayerChanged: layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
onCurrentLayerChanged: layerSlider.setUpperValue(UM.SimulationView.currentLayer)
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: {
Component.onCompleted:
{
layerSlider.setLowerValue(UM.SimulationView.minimumLayer)
layerSlider.setUpperValue(UM.SimulationView.currentLayer)
}
}
// Play simulation button
Button {
Button
{
id: playButton
iconSource: "./resources/simulation_resume.svg"
style: UM.Theme.styles.small_tool_button
visible: !UM.SimulationView.compatibilityMode
anchors {
anchors
{
verticalCenter: pathSlider.verticalCenter
}
property var status: 0 // indicates if it's stopped (0) or playing (1)
onClicked: {
switch(status) {
case 0: {
onClicked:
{
switch(status)
{
case 0:
{
resumeSimulation()
break
}
case 1: {
case 1:
{
pauseSimulation()
break
}
}
}
function pauseSimulation() {
function pauseSimulation()
{
UM.SimulationView.setSimulationRunning(false)
iconSource = "./resources/simulation_resume.svg"
simulationTimer.stop()
status = 0
layerSlider.manuallyChanged = true
pathSlider.manuallyChanged = true
}
function resumeSimulation() {
function resumeSimulation()
{
UM.SimulationView.setSimulationRunning(true)
iconSource = "./resources/simulation_pause.svg"
simulationTimer.start()
layerSlider.manuallyChanged = false
pathSlider.manuallyChanged = false
}
}
@ -652,7 +752,8 @@ Item
interval: 100
running: false
repeat: true
onTriggered: {
onTriggered:
{
var currentPath = UM.SimulationView.currentPath
var numPaths = UM.SimulationView.numPaths
var currentLayer = UM.SimulationView.currentLayer
@ -692,12 +793,15 @@ Item
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 {
FontMetrics
{
id: fontMetrics
font: UM.Theme.getFont("default")
}

View file

@ -256,6 +256,7 @@ fragment41core =
out vec4 frag_color;
uniform mediump vec4 u_ambientColor;
uniform mediump vec4 u_minimumAlbedo;
uniform highp vec3 u_lightPosition;
void main()
@ -263,7 +264,7 @@ fragment41core =
mediump vec4 finalColor = vec4(0.0);
float alpha = f_color.a;
finalColor.rgb += f_color.rgb * 0.3;
finalColor.rgb += f_color.rgb * 0.2 + u_minimumAlbedo.rgb;
highp vec3 normal = normalize(f_normal);
highp vec3 light_dir = normalize(u_lightPosition - f_vertex);
@ -285,6 +286,7 @@ u_extruder_opacity = [1.0, 1.0, 1.0, 1.0]
u_specularColor = [0.4, 0.4, 0.4, 1.0]
u_ambientColor = [0.3, 0.3, 0.3, 0.0]
u_diffuseColor = [1.0, 0.79, 0.14, 1.0]
u_minimumAlbedo = [0.1, 0.1, 0.1, 1.0]
u_shininess = 20.0
u_show_travel_moves = 0

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides the Simulation view.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -5,6 +5,7 @@ import json
import os
import platform
import time
from typing import cast, Optional, Set
from PyQt5.QtCore import pyqtSlot, QObject
@ -16,7 +17,7 @@ from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
from UM.Qt.Duration import DurationFormat
from typing import cast, Optional
from .SliceInfoJob import SliceInfoJob
@ -95,13 +96,29 @@ class SliceInfo(QObject, Extension):
def setSendSliceInfo(self, enabled: bool):
Application.getInstance().getPreferences().setValue("info/send_slice_info", enabled)
def _getUserModifiedSettingKeys(self) -> list:
from cura.CuraApplication import CuraApplication
application = cast(CuraApplication, Application.getInstance())
machine_manager = application.getMachineManager()
global_stack = machine_manager.activeMachine
user_modified_setting_keys = set() # type: Set[str]
for stack in [global_stack] + list(global_stack.extruders.values()):
# Get all settings in user_changes and quality_changes
all_keys = stack.userChanges.getAllKeys() | stack.qualityChanges.getAllKeys()
user_modified_setting_keys |= all_keys
return list(sorted(user_modified_setting_keys))
def _onWriteStarted(self, output_device):
try:
if not Application.getInstance().getPreferences().getValue("info/send_slice_info"):
Logger.log("d", "'info/send_slice_info' is turned off.")
return # Do nothing, user does not want to send data
application = Application.getInstance()
from cura.CuraApplication import CuraApplication
application = cast(CuraApplication, Application.getInstance())
machine_manager = application.getMachineManager()
print_information = application.getPrintInformation()
@ -164,6 +181,8 @@ class SliceInfo(QObject, Extension):
data["quality_profile"] = global_stack.quality.getMetaData().get("quality_type")
data["user_modified_setting_keys"] = self._getUserModifiedSettingKeys()
data["models"] = []
# Listing all files placed on the build plate
for node in DepthFirstIterator(application.getController().getScene().getRoot()):

View file

@ -56,6 +56,7 @@
}
],
"quality_profile": "fast",
"user_modified_setting_keys": ["layer_height", "wall_line_width", "infill_sparse_density"],
"models": [
{
"hash": "b72789b9beb5366dff20b1cf501020c3d4d4df7dc2295ecd0fddd0a6436df070",

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Submits anonymous slice info. Can be disabled through preferences.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides a normal solid mesh view.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Creates an eraser mesh to block the printing of support in certain places",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -2,6 +2,6 @@
"name": "Toolbox",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"api": 4,
"api": 5,
"description": "Find, manage and install new Cura packages."
}

View file

@ -33,6 +33,7 @@ Window
{
id: header
}
Item
{
id: mainView
@ -75,6 +76,7 @@ Window
visible: toolbox.viewCategory == "installed"
}
}
ToolboxFooter
{
id: footer

View file

@ -82,9 +82,16 @@ Item
}
spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
width: childrenRect.width
Label
{
text: catalog.i18nc("@label", "Contact") + ":"
text: catalog.i18nc("@label", "Website") + ":"
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text_medium")
}
Label
{
text: catalog.i18nc("@label", "Email") + ":"
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text_medium")
}
@ -100,18 +107,32 @@ Item
topMargin: UM.Theme.getSize("default_margin").height
}
spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
Label
{
text:
{
if (details.website)
{
return "<a href=\"" + details.website + "\">" + details.website + "</a>"
}
return ""
}
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
}
Label
{
text:
{
if (details.email)
{
return "<a href=\"mailto:"+details.email+"\">"+details.name+"</a>"
}
else
{
return "<a href=\""+details.website+"\">"+details.name+"</a>"
return "<a href=\"mailto:" + details.email + "\">" + details.email + "</a>"
}
return ""
}
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text")

View file

@ -1,14 +1,25 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick 2.7
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
Item
{
id: base
property var packageData
property var technicalDataSheetUrl: {
var link = undefined
if ("Technical Data Sheet" in packageData.links)
{
link = packageData.links["Technical Data Sheet"]
}
return link
}
anchors.topMargin: UM.Theme.getSize("default_margin").height
height: visible ? childrenRect.height : 0
visible: packageData.type == "material" && packageData.has_configs
@ -132,4 +143,25 @@ Item
width: Math.floor(table.width * 0.1)
}
}
Label
{
id: technical_data_sheet
anchors.top: table.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height / 2
visible: base.technicalDataSheetUrl !== undefined
text:
{
if (base.technicalDataSheetUrl !== undefined)
{
return "<a href='%1'>%2</a>".arg(base.technicalDataSheetUrl).arg("Technical Data Sheet")
}
return ""
}
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
}
}

View file

@ -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.7
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM

View file

@ -32,7 +32,7 @@ Item
width: UM.Theme.getSize("toolbox_thumbnail_medium").width
height: UM.Theme.getSize("toolbox_thumbnail_medium").height
fillMode: Image.PreserveAspectFit
source: details.icon_url || "../images/logobot.svg"
source: details === null ? "" : (details.icon_url || "../images/logobot.svg")
mipmap: true
anchors
{
@ -55,7 +55,7 @@ Item
rightMargin: UM.Theme.getSize("wide_margin").width
bottomMargin: UM.Theme.getSize("default_margin").height
}
text: details.name || ""
text: details === null ? "" : (details.name || "")
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
wrapMode: Text.WordWrap
@ -114,7 +114,7 @@ Item
height: childrenRect.height
Label
{
text: details.version || catalog.i18nc("@label", "Unknown")
text: details === null ? "" : (details.version || catalog.i18nc("@label", "Unknown"))
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text")
}
@ -122,6 +122,10 @@ Item
{
text:
{
if (details === null)
{
return ""
}
var date = new Date(details.last_updated)
return date.toLocaleString(UM.Preferences.getValue("general/language"))
}
@ -132,6 +136,10 @@ Item
{
text:
{
if (details === null)
{
return ""
}
if (details.author_email)
{
return "<a href=\"mailto:" + details.author_email+"?Subject=Cura: " + details.name + "\">" + details.author_name + "</a>"
@ -148,7 +156,7 @@ Item
}
Label
{
text: details.download_count || catalog.i18nc("@label", "Unknown")
text: details === null ? "" : (details.download_count || catalog.i18nc("@label", "Unknown"))
font: UM.Theme.getFont("very_small")
color: UM.Theme.getColor("text")
}

View file

@ -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.7
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM

View file

@ -9,7 +9,8 @@ import UM 1.1 as UM
Item
{
property int packageCount: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getTotalNumberOfPackagesByAuthor(model.id) : 1
id: toolboxDownloadsGridTile
property int packageCount: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getTotalNumberOfMaterialPackagesByAuthor(model.id) : 1
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

View file

@ -12,7 +12,9 @@ ScrollView
width: parent.width
height: parent.height
style: UM.Theme.styles.scrollview
flickableItem.flickableDirection: Flickable.VerticalFlick
Column
{
width: base.width
@ -30,7 +32,7 @@ ScrollView
id: allPlugins
width: parent.width
heading: toolbox.viewCategory == "material" ? catalog.i18nc("@label", "Community Contributions") : catalog.i18nc("@label", "Community Plugins")
model: toolbox.viewCategory == "material" ? toolbox.authorsModel : toolbox.packagesModel
model: toolbox.viewCategory == "material" ? toolbox.materialsAvailableModel : toolbox.pluginsAvailableModel
}
ToolboxDownloadsGrid

View file

@ -9,7 +9,7 @@ import UM 1.1 as UM
Rectangle
{
property int packageCount: toolbox.viewCategory == "material" ? toolbox.getTotalNumberOfPackagesByAuthor(model.id) : 1
property int packageCount: toolbox.viewCategory == "material" ? toolbox.getTotalNumberOfMaterialPackagesByAuthor(model.id) : 1
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)

View file

@ -21,8 +21,10 @@ Item
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
}
ToolboxTabButton
{
id: pluginsTabButton
text: catalog.i18nc("@title:tab", "Plugins")
active: toolbox.viewCategory == "plugin" && enabled
enabled: toolbox.viewPage != "loading" && toolbox.viewPage != "errored"
@ -36,6 +38,7 @@ Item
ToolboxTabButton
{
id: materialsTabButton
text: catalog.i18nc("@title:tab", "Materials")
active: toolbox.viewCategory == "material" && enabled
enabled: toolbox.viewPage != "loading" && toolbox.viewPage != "errored"
@ -49,6 +52,7 @@ Item
}
ToolboxTabButton
{
id: installedTabButton
text: catalog.i18nc("@title:tab", "Installed")
active: toolbox.viewCategory == "installed"
anchors

View file

@ -6,6 +6,7 @@ import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
ScrollView
@ -16,6 +17,7 @@ ScrollView
height: parent.height
style: UM.Theme.styles.scrollview
flickableItem.flickableDirection: Flickable.VerticalFlick
Column
{
spacing: UM.Theme.getSize("default_margin").height

View file

@ -33,6 +33,9 @@ class AuthorsModel(ListModel):
def _update(self):
items = []
if not self._metadata:
self.setItems([])
return
for author in self._metadata:
items.append({

View file

@ -5,9 +5,13 @@ import re
from typing import Dict
from PyQt5.QtCore import Qt, pyqtProperty
from UM.Logger import Logger
from UM.Qt.ListModel import ListModel
from .ConfigsModel import ConfigsModel
## Model that holds cura packages. By setting the filter property the instances held by this model can be changed.
class PackagesModel(ListModel):
def __init__(self, parent = None):
@ -34,6 +38,8 @@ class PackagesModel(ListModel):
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")
# List of filters for queries. The result is the union of the each list of results.
self._filter = {} # type: Dict[str, str]
@ -45,10 +51,16 @@ class PackagesModel(ListModel):
def _update(self):
items = []
for package in self._metadata:
if self._metadata is None:
Logger.logException("w", "Failed to load packages for Toolbox")
self.setItems(items)
return
for package in self._metadata:
has_configs = False
configs_model = None
links_dict = {}
if "data" in package:
if "supported_configs" in package["data"]:
if len(package["data"]["supported_configs"]) > 0:
@ -56,41 +68,47 @@ class PackagesModel(ListModel):
configs_model = ConfigsModel()
configs_model.setConfigs(package["data"]["supported_configs"])
# Links is a list of dictionaries with "title" and "url". Convert this list into a dict so it's easier
# to process.
link_list = package['data']['links'] if 'links' in package['data'] else []
links_dict = {d["title"]: d["url"] for d in link_list}
if "author_id" not in package["author"] or "display_name" not in package["author"]:
package["author"]["author_id"] = ""
package["author"]["display_name"] = ""
# raise Exception("Detected a package with malformed author data.")
items.append({
"id": package["package_id"],
"type": package["package_type"],
"name": package["display_name"],
"version": package["package_version"],
"author_id": package["author"]["author_id"],
"author_name": package["author"]["display_name"],
"author_email": package["author"]["email"] if "email" in package["author"] else None,
"description": package["description"] if "description" in package else None,
"icon_url": package["icon_url"] if "icon_url" in package else None,
"image_urls": package["image_urls"] if "image_urls" in package else None,
"download_url": package["download_url"] if "download_url" in package else None,
"last_updated": package["last_updated"] if "last_updated" in package else None,
"is_bundled": package["is_bundled"] if "is_bundled" in package else False,
"is_active": package["is_active"] if "is_active" in package else False,
"is_installed": package["is_installed"] if "is_installed" in package else False,
"has_configs": has_configs,
"supported_configs": configs_model,
"download_count": package["download_count"] if "download_count" in package else 0,
"tags": package["tags"] if "tags" in package else []
"id": package["package_id"],
"type": package["package_type"],
"name": package["display_name"],
"version": package["package_version"],
"author_id": package["author"]["author_id"],
"author_name": package["author"]["display_name"],
"author_email": package["author"]["email"] if "email" in package["author"] else None,
"description": package["description"] if "description" in package else None,
"icon_url": package["icon_url"] if "icon_url" in package else None,
"image_urls": package["image_urls"] if "image_urls" in package else None,
"download_url": package["download_url"] if "download_url" in package else None,
"last_updated": package["last_updated"] if "last_updated" in package else None,
"is_bundled": package["is_bundled"] if "is_bundled" in package else False,
"is_active": package["is_active"] if "is_active" in package else False,
"is_installed": package["is_installed"] if "is_installed" in package else False,
"has_configs": has_configs,
"supported_configs": configs_model,
"download_count": package["download_count"] if "download_count" in package else 0,
"tags": package["tags"] if "tags" in package else [],
"links": links_dict,
"website": package["website"] if "website" in package else None,
})
# Filter on all the key-word arguments.
for key, value in self._filter.items():
if key is "tags":
key_filter = lambda item, value = value: value in item["tags"]
key_filter = lambda item, v = value: v in item["tags"]
elif "*" in value:
key_filter = lambda candidate, key = key, value = value: self._matchRegExp(candidate, key, value)
key_filter = lambda candidate, k = key, v = value: self._matchRegExp(candidate, k, v)
else:
key_filter = lambda candidate, key = key, value = value: self._matchString(candidate, key, value)
key_filter = lambda candidate, k = key, v = value: self._matchString(candidate, k, v)
items = filter(key_filter, items)
# Execute all filters.

View file

@ -1,12 +1,11 @@
# Copyright (c) 2018 Ultimaker B.V.
# Toolbox is released under the terms of the LGPLv3 or higher.
from typing import Dict, Optional, Union, Any, cast
import json
import os
import tempfile
import platform
from typing import cast, List
from typing import cast, Any, Dict, List, Set, TYPE_CHECKING, Tuple, Optional, Union
from PyQt5.QtCore import QUrl, QObject, pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
@ -20,9 +19,13 @@ from UM.Version import Version
import cura
from cura.CuraApplication import CuraApplication
from .AuthorsModel import AuthorsModel
from .PackagesModel import PackagesModel
if TYPE_CHECKING:
from cura.Settings.GlobalStack import GlobalStack
i18n_catalog = i18nCatalog("cura")
@ -34,19 +37,19 @@ class Toolbox(QObject, Extension):
def __init__(self, application: CuraApplication) -> None:
super().__init__()
self._application = application #type: CuraApplication
self._application = application # type: CuraApplication
self._sdk_version = None # type: Optional[int]
self._cloud_api_version = None # type: Optional[int]
self._cloud_api_root = None # type: Optional[str]
self._api_url = None # type: Optional[str]
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._api_url = None # type: Optional[str]
# Network:
self._download_request = None #type: Optional[QNetworkRequest]
self._download_reply = None #type: Optional[QNetworkReply]
self._download_progress = 0 #type: float
self._is_downloading = False #type: bool
self._network_manager = None #type: Optional[QNetworkAccessManager]
self._download_request = None # type: Optional[QNetworkRequest]
self._download_reply = None # type: Optional[QNetworkReply]
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(
@ -58,9 +61,10 @@ class Toolbox(QObject, Extension):
)
)
]
self._request_urls = {} # type: Dict[str, QUrl]
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 = [] # type: List[str]
self._old_plugin_ids = set() # type: Set[str]
self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]]
# Data:
self._metadata = {
@ -73,7 +77,7 @@ class Toolbox(QObject, Extension):
"materials_available": [],
"materials_installed": [],
"materials_generic": []
} # type: Dict[str, List[Any]]
} # type: Dict[str, List[Any]]
# Models:
self._models = {
@ -83,42 +87,40 @@ class Toolbox(QObject, Extension):
"plugins_available": PackagesModel(self),
"plugins_installed": PackagesModel(self),
"materials_showcase": AuthorsModel(self),
"materials_available": PackagesModel(self),
"materials_available": AuthorsModel(self),
"materials_installed": PackagesModel(self),
"materials_generic": PackagesModel(self)
} # type: Dict[str, ListModel]
} # type: Dict[str, ListModel]
# These properties are for keeping track of the UI state:
# ----------------------------------------------------------------------
# View category defines which filter to use, and therefore effectively
# which category is currently being displayed. For example, possible
# values include "plugin" or "material", but also "installed".
self._view_category = "plugin" #type: str
self._view_category = "plugin" # type: str
# View page defines which type of page layout to use. For example,
# possible values include "overview", "detail" or "author".
self._view_page = "loading" #type: str
self._view_page = "loading" # type: str
# Active package refers to which package is currently being downloaded,
# installed, or otherwise modified.
self._active_package = None # type: Optional[Dict[str, Any]]
self._active_package = None # type: Optional[Dict[str, Any]]
self._dialog = None #type: Optional[QObject]
self._confirm_reset_dialog = None #type: Optional[QObject]
self._dialog = None # type: Optional[QObject]
self._confirm_reset_dialog = None # type: Optional[QObject]
self._resetUninstallVariables()
self._restart_required = False #type: bool
self._restart_required = False # type: bool
# variables for the license agreement dialog
self._license_dialog_plugin_name = "" #type: str
self._license_dialog_license_content = "" #type: str
self._license_dialog_plugin_file_location = "" #type: str
self._restart_dialog_message = "" #type: str
self._license_dialog_plugin_name = "" # type: str
self._license_dialog_license_content = "" # type: str
self._license_dialog_plugin_file_location = "" # type: str
self._restart_dialog_message = "" # type: str
self._application.initializationFinished.connect(self._onAppInitialized)
# Signals:
# --------------------------------------------------------------------------
# Downloading changes
@ -137,11 +139,11 @@ class Toolbox(QObject, Extension):
showLicenseDialog = pyqtSignal()
uninstallVariablesChanged = pyqtSignal()
def _resetUninstallVariables(self):
self._package_id_to_uninstall = None
def _resetUninstallVariables(self) -> None:
self._package_id_to_uninstall = None # type: Optional[str]
self._package_name_to_uninstall = ""
self._package_used_materials = []
self._package_used_qualities = []
self._package_used_materials = [] # type: List[Tuple[GlobalStack, str, str]]
self._package_used_qualities = [] # type: List[Tuple[GlobalStack, str, str]]
@pyqtSlot(result = str)
def getLicenseDialogPluginName(self) -> str:
@ -205,14 +207,14 @@ class Toolbox(QObject, Extension):
return cura.CuraVersion.CuraCloudAPIVersion # type: ignore
# Get the packages version depending on Cura version settings.
def _getSDKVersion(self) -> int:
def _getSDKVersion(self) -> Union[int, str]:
if not hasattr(cura, "CuraVersion"):
return self._plugin_registry.APIVersion
if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
if not hasattr(cura.CuraVersion, "CuraSDKVersion"): # type: ignore
return self._plugin_registry.APIVersion
if not cura.CuraVersion.CuraSDKVersion: # type: ignore
if not cura.CuraVersion.CuraSDKVersion: # type: ignore
return self._plugin_registry.APIVersion
return cura.CuraVersion.CuraSDKVersion # type: ignore
return cura.CuraVersion.CuraSDKVersion # type: ignore
@pyqtSlot()
def browsePackages(self) -> None:
@ -229,10 +231,12 @@ class Toolbox(QObject, Extension):
# Make remote requests:
self._makeRequestByType("packages")
self._makeRequestByType("authors")
self._makeRequestByType("plugins_showcase")
self._makeRequestByType("materials_showcase")
self._makeRequestByType("materials_available")
self._makeRequestByType("materials_generic")
# 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()
@ -285,8 +289,8 @@ class Toolbox(QObject, Extension):
installed_package_ids = self._package_manager.getAllInstalledPackageIDs()
scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs()
self._old_plugin_ids = []
self._old_plugin_metadata = [] # type: List[Dict[str, Any]]
self._old_plugin_ids = set()
self._old_plugin_metadata = dict()
for plugin_id in old_plugin_ids:
# Neither the installed packages nor the packages that are scheduled to remove are old plugins
@ -296,12 +300,20 @@ class Toolbox(QObject, Extension):
old_metadata = self._plugin_registry.getMetaData(plugin_id)
new_metadata = self._convertPluginMetadata(old_metadata)
self._old_plugin_ids.append(plugin_id)
self._old_plugin_metadata.append(new_metadata)
self._old_plugin_ids.add(plugin_id)
self._old_plugin_metadata[new_metadata["package_id"]] = new_metadata
all_packages = self._package_manager.getAllInstalledPackagesInfo()
if "plugin" in all_packages:
self._metadata["plugins_installed"] = all_packages["plugin"] + self._old_plugin_metadata
# For old plugins, we only want to include the old custom plugin that were installed via the old toolbox.
# The bundled plugins will be included in JSON files in the "bundled_packages" folder, so the bundled
# plugins should be excluded from the old plugins list/dict.
all_plugin_package_ids = set(package["package_id"] for package in all_packages["plugin"])
self._old_plugin_ids = set(plugin_id for plugin_id in self._old_plugin_ids
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.metadataChanged.emit()
if "material" in all_packages:
@ -344,26 +356,26 @@ class Toolbox(QObject, Extension):
self.uninstall(package_id)
@pyqtProperty(str, notify = uninstallVariablesChanged)
def pluginToUninstall(self):
def pluginToUninstall(self) -> str:
return self._package_name_to_uninstall
@pyqtProperty(str, notify = uninstallVariablesChanged)
def uninstallUsedMaterials(self):
def uninstallUsedMaterials(self) -> str:
return "\n".join(["%s (%s)" % (str(global_stack.getName()), material) for global_stack, extruder_nr, material in self._package_used_materials])
@pyqtProperty(str, notify = uninstallVariablesChanged)
def uninstallUsedQualities(self):
def uninstallUsedQualities(self) -> str:
return "\n".join(["%s (%s)" % (str(global_stack.getName()), quality) for global_stack, extruder_nr, quality in self._package_used_qualities])
@pyqtSlot()
def closeConfirmResetDialog(self):
def closeConfirmResetDialog(self) -> None:
if self._confirm_reset_dialog is not None:
self._confirm_reset_dialog.close()
## Uses "uninstall variables" to reset qualities and materials, then uninstall
# It's used as an action on Confirm reset on Uninstall
@pyqtSlot()
def resetMaterialsQualitiesAndUninstall(self):
def resetMaterialsQualitiesAndUninstall(self) -> None:
application = CuraApplication.getInstance()
material_manager = application.getMaterialManager()
quality_manager = application.getQualityManager()
@ -376,9 +388,9 @@ class Toolbox(QObject, Extension):
default_quality_group = quality_manager.getDefaultQualityType(global_stack)
machine_manager.setQualityGroup(default_quality_group, global_stack = global_stack)
self._markPackageMaterialsAsToBeUninstalled(self._package_id_to_uninstall)
self.uninstall(self._package_id_to_uninstall)
if self._package_id_to_uninstall is not None:
self._markPackageMaterialsAsToBeUninstalled(self._package_id_to_uninstall)
self.uninstall(self._package_id_to_uninstall)
self._resetUninstallVariables()
self.closeConfirmResetDialog()
@ -471,12 +483,14 @@ class Toolbox(QObject, Extension):
# --------------------------------------------------------------------------
@pyqtSlot(str, result = bool)
def canUpdate(self, package_id: str) -> bool:
if self.isOldPlugin(package_id):
return True
local_package = self._package_manager.getInstalledPackageInfo(package_id)
if local_package is None:
return False
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)
if remote_package is None:
@ -484,7 +498,16 @@ class Toolbox(QObject, Extension):
local_version = Version(local_package["package_version"])
remote_version = Version(remote_package["package_version"])
return remote_version > local_version
can_upgrade = False
if remote_version > local_version:
can_upgrade = True
# A package with the same version can be built to have different SDK versions. So, for a package with the same
# version, we also need to check if the current one has a lower SDK version. If so, this package should also
# be upgradable.
elif remote_version == local_version:
can_upgrade = local_package.get("sdk_version", 0) < remote_package.get("sdk_version", 0)
return can_upgrade
@pyqtSlot(str, result = bool)
def canDowngrade(self, package_id: str) -> bool:
@ -504,7 +527,11 @@ class Toolbox(QObject, Extension):
@pyqtSlot(str, result = bool)
def isInstalled(self, package_id: str) -> bool:
return self._package_manager.isPackageInstalled(package_id)
result = self._package_manager.isPackageInstalled(package_id)
# Also check the old plugins list if it's not found in the package manager.
if not result:
result = self.isOldPlugin(package_id)
return result
@pyqtSlot(str, result = int)
def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int:
@ -514,12 +541,14 @@ class Toolbox(QObject, Extension):
count += 1
return count
# This slot is only used to get the number of material packages by author, not any other type of packages.
@pyqtSlot(str, result = int)
def getTotalNumberOfPackagesByAuthor(self, author_id: str) -> int:
def getTotalNumberOfMaterialPackagesByAuthor(self, author_id: str) -> int:
count = 0
for package in self._metadata["materials_available"]:
if package["author"]["author_id"] == author_id:
count += 1
for package in self._metadata["packages"]:
if package["package_type"] == "material":
if package["author"]["author_id"] == author_id:
count += 1
return count
@pyqtSlot(str, result = bool)
@ -529,12 +558,14 @@ class Toolbox(QObject, Extension):
return False
# Check for plugins that were installed with the old plugin browser
@pyqtSlot(str, result = bool)
def isOldPlugin(self, plugin_id: str) -> bool:
if plugin_id in self._old_plugin_ids:
return True
return False
def getOldPluginPackageMetadata(self, plugin_id: str) -> Optional[Dict[str, Any]]:
return self._old_plugin_metadata.get(plugin_id)
def loadingComplete(self) -> bool:
populated = 0
for list in self._metadata.items():
@ -606,8 +637,22 @@ 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
if reply.url() == url:
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) == 200:
try:
@ -623,25 +668,16 @@ class Toolbox(QObject, Extension):
if not self._models[type]:
Logger.log("e", "Could not find the %s model.", type)
break
# HACK: Eventually get rid of the code from here...
if type is "plugins_showcase" or type is "materials_showcase":
self._metadata["plugins_showcase"] = json_data["data"]["plugin"]["packages"]
self._models["plugins_showcase"].setMetadata(self._metadata["plugins_showcase"])
self._metadata["materials_showcase"] = json_data["data"]["material"]["authors"]
self._models["materials_showcase"].setMetadata(self._metadata["materials_showcase"])
else:
# ...until here.
# This hack arises for multiple reasons but the main
# one is because there are not separate API calls
# for different kinds of showcases.
self._metadata[type] = json_data["data"]
self._models[type].setMetadata(self._metadata[type])
self._metadata[type] = json_data["data"]
self._models[type].setMetadata(self._metadata[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":
@ -680,7 +716,7 @@ class Toolbox(QObject, Extension):
self._temp_plugin_file.close()
self._onDownloadComplete(file_path)
def _onDownloadComplete(self, file_path: str):
def _onDownloadComplete(self, file_path: str) -> None:
Logger.log("i", "Toolbox: Download complete.")
package_info = self._package_manager.getPackageInfo(file_path)
if not package_info:
@ -739,9 +775,7 @@ class Toolbox(QObject, Extension):
def viewPage(self) -> str:
return self._view_page
# Expose Models:
# Exposed Models:
# --------------------------------------------------------------------------
@pyqtProperty(QObject, notify = metadataChanged)
def authorsModel(self) -> AuthorsModel:
@ -755,6 +789,10 @@ class Toolbox(QObject, Extension):
def pluginsShowcaseModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["plugins_showcase"])
@pyqtProperty(QObject, notify = metadataChanged)
def pluginsAvailableModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["plugins_available"])
@pyqtProperty(QObject, notify = metadataChanged)
def pluginsInstalledModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["plugins_installed"])
@ -763,6 +801,10 @@ class Toolbox(QObject, Extension):
def materialsShowcaseModel(self) -> AuthorsModel:
return cast(AuthorsModel, self._models["materials_showcase"])
@pyqtProperty(QObject, notify = metadataChanged)
def materialsAvailableModel(self) -> AuthorsModel:
return cast(AuthorsModel, self._models["materials_available"])
@pyqtProperty(QObject, notify = metadataChanged)
def materialsInstalledModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["materials_installed"])
@ -771,8 +813,6 @@ class Toolbox(QObject, Extension):
def materialsGenericModel(self) -> PackagesModel:
return cast(PackagesModel, self._models["materials_generic"])
# Filter Models:
# --------------------------------------------------------------------------
@pyqtSlot(str, str, str)
@ -798,3 +838,48 @@ class Toolbox(QObject, Extension):
return
self._models[model_type].setFilter({})
self.filterChanged.emit()
# HACK(S):
# --------------------------------------------------------------------------
def buildMaterialsModels(self) -> None:
self._metadata["materials_showcase"] = []
self._metadata["materials_available"] = []
processed_authors = [] # type: List[str]
for item in self._metadata["packages"]:
if item["package_type"] == "material":
author = item["author"]
if author["author_id"] in processed_authors:
continue
# Generic materials to be in the same section
if "generic" in item["tags"]:
self._metadata["materials_generic"].append(item)
else:
if "showcase" in item["tags"]:
self._metadata["materials_showcase"].append(author)
else:
self._metadata["materials_available"].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"])
def buildPluginsModels(self) -> None:
self._metadata["plugins_showcase"] = []
self._metadata["plugins_available"] = []
for item in self._metadata["packages"]:
if item["package_type"] == "plugin":
if "showcase" in item["tags"]:
self._metadata["plugins_showcase"].append(item)
else:
self._metadata["plugins_available"].append(item)
self._models["plugins_showcase"].setMetadata(self._metadata["plugins_showcase"])
self._models["plugins_available"].setMetadata(self._metadata["plugins_available"])

View file

@ -1,5 +1,6 @@
#Copyright (c) 2018 Ultimaker B.V.
#Cura is released under the terms of the LGPLv3 or higher.
from typing import cast
from Charon.VirtualFile import VirtualFile #To open UFP files.
@ -9,10 +10,12 @@ from io import StringIO #For converting g-code to bytes.
from UM.Application import Application
from UM.Logger import Logger
from UM.Mesh.MeshWriter import MeshWriter #The writer we need to implement.
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
from UM.PluginRegistry import PluginRegistry #To get the g-code writer.
from PyQt5.QtCore import QBuffer
from cura.Snapshot import Snapshot
from cura.Utils.Threading import call_on_qt_thread
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
@ -20,7 +23,16 @@ catalog = i18nCatalog("cura")
class UFPWriter(MeshWriter):
def __init__(self):
super().__init__()
super().__init__(add_to_recent_files = False)
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-cura-stl-file",
comment = "Cura UFP File",
suffixes = ["ufp"]
)
)
self._snapshot = None
Application.getInstance().getOutputDeviceManager().writeStarted.connect(self._createSnapshot)
@ -29,6 +41,11 @@ class UFPWriter(MeshWriter):
Logger.log("d", "Creating thumbnail image...")
self._snapshot = Snapshot.snapshot(width = 300, height = 300)
# This needs to be called on the main thread (Qt thread) because the serialization of material containers can
# trigger loading other containers. Because those loaded containers are QtObjects, they must be created on the
# Qt thread. The File read/write operations right now are executed on separated threads because they are scheduled
# by the Job class.
@call_on_qt_thread
def write(self, stream, nodes, mode = MeshWriter.OutputMode.BinaryMode):
archive = VirtualFile()
archive.openStream(stream, "application/x-ufp", OpenMode.WriteOnly)
@ -60,5 +77,50 @@ class UFPWriter(MeshWriter):
else:
Logger.log("d", "Thumbnail not created, cannot save it")
# Store the material.
application = Application.getInstance()
machine_manager = application.getMachineManager()
material_manager = application.getMaterialManager()
global_stack = machine_manager.activeMachine
material_extension = "xml.fdm_material"
material_mime_type = "application/x-ultimaker-material-profile"
try:
archive.addContentType(extension = material_extension, mime_type = material_mime_type)
except:
Logger.log("w", "The material extension: %s was already added", material_extension)
added_materials = []
for extruder_stack in global_stack.extruders.values():
material = extruder_stack.material
material_file_name = material.getMetaData()["base_file"] + ".xml.fdm_material"
material_file_name = "/Materials/" + material_file_name
#Same material cannot be added
if material_file_name in added_materials:
continue
material_root_id = material.getMetaDataEntry("base_file")
material_group = material_manager.getMaterialGroup(material_root_id)
if material_group is None:
Logger.log("e", "Cannot find material container with root id [%s]", material_root_id)
return False
material_container = material_group.root_material_node.getContainer()
try:
serialized_material = material_container.serialize()
except NotImplementedError:
Logger.log("e", "Unable serialize material container with root id: %s", material_root_id)
return False
material_file = archive.getStream(material_file_name)
material_file.write(serialized_material.encode("UTF-8"))
archive.addRelation(virtual_path = material_file_name,
relation_type = "http://schemas.ultimaker.org/package/2018/relationships/material",
origin = "/3D/model.gcode")
added_materials.append(material_file_name)
archive.close()
return True

View file

@ -11,16 +11,6 @@ except ImportError:
from UM.i18n import i18nCatalog #To translate the file format description.
from UM.Mesh.MeshWriter import MeshWriter #For the binary mode flag.
from UM.MimeTypeDatabase import MimeTypeDatabase, MimeType
MimeTypeDatabase.addMimeType(
MimeType(
name = "application/x-cura-stl-file",
comment = "Cura UFP File",
suffixes = ["ufp"]
)
)
i18n_catalog = i18nCatalog("cura")

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"version": "1.0.0",
"description": "Provides support for writing Ultimaker Format Packages.",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -1,241 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import UM 1.3 as UM
import Cura 1.0 as Cura
Component
{
Rectangle
{
id: base
property var manager: Cura.MachineManager.printerOutputDevices[0]
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
visible: manager != null
anchors.fill: parent
color: UM.Theme.getColor("viewport_background")
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Label
{
id: activePrintersLabel
font: UM.Theme.getFont("large")
anchors.horizontalCenter: parent.horizontalCenter
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.top: parent.top
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right:parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: Cura.MachineManager.printerOutputDevices[0].name
elide: Text.ElideRight
}
Rectangle
{
id: printJobArea
border.width: UM.Theme.getSize("default_lining").width
border.color: lineColor
anchors.top: activePrintersLabel.bottom
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
radius: cornerRadius
height: childrenRect.height
Item
{
id: printJobTitleBar
width: parent.width
height: printJobTitleLabel.height + 2 * UM.Theme.getSize("default_margin").height
Label
{
id: printJobTitleLabel
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
text: catalog.i18nc("@title", "Print jobs")
font: UM.Theme.getFont("default")
opacity: 0.75
}
Rectangle
{
anchors.bottom: parent.bottom
height: UM.Theme.getSize("default_lining").width
color: lineColor
width: parent.width
}
}
Column
{
id: printJobColumn
anchors.top: printJobTitleBar.bottom
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
Item
{
width: parent.width
height: childrenRect.height
opacity: 0.65
Label
{
text: catalog.i18nc("@label", "Printing")
font: UM.Theme.getFont("very_small")
}
Label
{
text: manager.activePrintJobs.length
font: UM.Theme.getFont("small")
anchors.right: parent.right
}
}
Item
{
width: parent.width
height: childrenRect.height
opacity: 0.65
Label
{
text: catalog.i18nc("@label", "Queued")
font: UM.Theme.getFont("very_small")
}
Label
{
text: manager.queuedPrintJobs.length
font: UM.Theme.getFont("small")
anchors.right: parent.right
}
}
}
OpenPanelButton
{
anchors.top: printJobColumn.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").height
id: configButton
onClicked: base.manager.openPrintJobControlPanel()
text: catalog.i18nc("@action:button", "View print jobs")
}
Item
{
// spacer
anchors.top: configButton.bottom
width: UM.Theme.getSize("default_margin").width
height: UM.Theme.getSize("default_margin").height
}
}
Rectangle
{
id: printersArea
border.width: UM.Theme.getSize("default_lining").width
border.color: lineColor
anchors.top: printJobArea.bottom
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
radius: cornerRadius
height: childrenRect.height
Item
{
id: printersTitleBar
width: parent.width
height: printJobTitleLabel.height + 2 * UM.Theme.getSize("default_margin").height
Label
{
id: printersTitleLabel
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
text: catalog.i18nc("@label:title", "Printers")
font: UM.Theme.getFont("default")
opacity: 0.75
}
Rectangle
{
anchors.bottom: parent.bottom
height: UM.Theme.getSize("default_lining").width
color: lineColor
width: parent.width
}
}
Column
{
id: printersColumn
anchors.top: printersTitleBar.bottom
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
Repeater
{
model: manager.connectedPrintersTypeCount
Item
{
width: parent.width
height: childrenRect.height
opacity: 0.65
Label
{
text: modelData.machine_type
font: UM.Theme.getFont("very_small")
}
Label
{
text: modelData.count
font: UM.Theme.getFont("small")
anchors.right: parent.right
}
}
}
}
OpenPanelButton
{
anchors.top: printersColumn.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").height
id: printerConfigButton
onClicked: base.manager.openPrinterControlPanel()
text: catalog.i18nc("@action:button", "View printers")
}
Item
{
// spacer
anchors.top: printerConfigButton.bottom
width: UM.Theme.getSize("default_margin").width
height: UM.Theme.getSize("default_margin").height
}
}
}
}

View file

@ -1,118 +0,0 @@
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
width: maximumWidth
height: maximumHeight
color: UM.Theme.getColor("viewport_background")
property var emphasisColor: UM.Theme.getColor("setting_control_border_highlight")
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Label
{
id: activePrintersLabel
font: UM.Theme.getFont("large")
anchors {
top: parent.top
topMargin: UM.Theme.getSize("default_margin").height * 2 // a bit more spacing to give it some breathing room
horizontalCenter: parent.horizontalCenter
}
text: OutputDevice.printers.length == 0 ? catalog.i18nc("@label: arg 1 is group name", "%1 is not set up to host a group of connected Ultimaker 3 printers").arg(Cura.MachineManager.printerOutputDevices[0].name) : ""
visible: OutputDevice.printers.length == 0
}
Item
{
anchors.topMargin: UM.Theme.getSize("default_margin").height
anchors.top: parent.top
anchors.horizontalCenter: parent.horizontalCenter
width: Math.min(800 * screenScaleFactor, maximumWidth)
height: children.height
visible: OutputDevice.printers.length != 0
Label
{
id: addRemovePrintersLabel
anchors.right: parent.right
text: catalog.i18nc("@label link to connect manager", "Add/Remove printers")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
}
MouseArea
{
anchors.fill: addRemovePrintersLabel
hoverEnabled: true
onClicked: Cura.MachineManager.printerOutputDevices[0].openPrinterControlPanel()
onEntered: addRemovePrintersLabel.font.underline = true
onExited: addRemovePrintersLabel.font.underline = false
}
}
ScrollView
{
id: printerScrollView
anchors.margins: UM.Theme.getSize("default_margin").width
anchors.top: activePrintersLabel.bottom
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_lining").width // To ensure border can be drawn.
anchors.rightMargin: UM.Theme.getSize("default_lining").width
anchors.right: parent.right
ListView
{
anchors.fill: parent
spacing: -UM.Theme.getSize("default_lining").height
model: OutputDevice.printers
delegate: PrinterInfoBlock
{
printer: modelData
width: Math.min(800 * screenScaleFactor, maximumWidth)
height: 125 * screenScaleFactor
// Add a 1 pix margin, as the border is sometimes cut off otherwise.
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
PrinterVideoStream
{
visible: OutputDevice.activePrinter != null
anchors.fill:parent
}
onVisibleChanged:
{
if (!monitorFrame.visible)
{
// After switching the Tab ensure that active printer is Null, the video stream image
// might be active
OutputDevice.setActivePrinter(null)
}
}
}
}

View file

@ -1,71 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.1 as UM
Button {
objectName: "openPanelSaveAreaButton"
id: openPanelSaveAreaButton
UM.I18nCatalog { id: catalog; name: "cura"; }
height: UM.Theme.getSize("save_button_save_to_button").height
tooltip: catalog.i18nc("@info:tooltip", "Opens the print jobs page with your default web browser.")
text: catalog.i18nc("@action:button", "View print jobs")
// FIXME: This button style is copied and duplicated from SaveButton.qml
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 { }
}
}

View file

@ -1,33 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.2 as UM
Item
{
id: extruderInfo
property var printCoreConfiguration
width: Math.round(parent.width / 2)
height: childrenRect.height
Label
{
id: materialLabel
text: printCoreConfiguration.activeMaterial != null ? printCoreConfiguration.activeMaterial.name : ""
elide: Text.ElideRight
width: parent.width
font: UM.Theme.getFont("very_small")
}
Label
{
id: printCoreLabel
text: printCoreConfiguration.hotendID
anchors.top: materialLabel.bottom
elide: Text.ElideRight
width: parent.width
font: UM.Theme.getFont("very_small")
opacity: 0.5
}
}

View file

@ -1,431 +0,0 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.3 as UM
Rectangle
{
function strPadLeft(string, pad, length)
{
return (new Array(length + 1).join(pad) + string).slice(-length);
}
function getPrettyTime(time)
{
return OutputDevice.formatDuration(time)
}
function formatPrintJobPercent(printJob)
{
if (printJob == null)
{
return "";
}
if (printJob.timeTotal === 0)
{
return "";
}
return Math.min(100, Math.round(printJob.timeElapsed / printJob.timeTotal * 100)) + "%";
}
function printerStatusText(printer)
{
switch (printer.state)
{
case "pre_print":
return catalog.i18nc("@label:status", "Preparing to print")
case "printing":
return catalog.i18nc("@label:status", "Printing");
case "idle":
return catalog.i18nc("@label:status", "Available");
case "unreachable":
return catalog.i18nc("@label:status", "Lost connection with the printer");
case "maintenance":
return catalog.i18nc("@label:status", "Unavailable");
default:
return catalog.i18nc("@label:status", "Unknown");
}
}
id: printerDelegate
property var printer: null
property var printJob: printer != null ? printer.activePrintJob: null
border.width: UM.Theme.getSize("default_lining").width
border.color: mouse.containsMouse ? emphasisColor : lineColor
z: mouse.containsMouse ? 1 : 0 // Push this item up a bit on mouse over to ensure that the highlighted bottom border is visible.
MouseArea
{
id: mouse
anchors.fill:parent
onClicked: OutputDevice.setActivePrinter(printer)
hoverEnabled: true;
// Only clickable if no printer is selected
enabled: OutputDevice.activePrinter == null && printer.state !== "unreachable"
}
Row
{
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: UM.Theme.getSize("default_margin").width
Rectangle
{
width: Math.round(parent.width / 3)
height: parent.height
Label // Print job name
{
id: jobNameLabel
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: printJob != null ? printJob.name : ""
font: UM.Theme.getFont("default_bold")
elide: Text.ElideRight
}
Label
{
id: jobOwnerLabel
anchors.top: jobNameLabel.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: printJob != null ? printJob.owner : ""
opacity: 0.50
elide: Text.ElideRight
}
Label
{
id: totalTimeLabel
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: printJob != null ? getPrettyTime(printJob.timeTotal) : ""
opacity: 0.65
font: UM.Theme.getFont("default")
elide: Text.ElideRight
}
}
Rectangle
{
width: Math.round(parent.width / 3 * 2)
height: parent.height
Label // Friendly machine name
{
id: printerNameLabel
anchors.top: parent.top
anchors.left: parent.left
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width - showCameraIcon.width)
text: printer.name
font: UM.Theme.getFont("default_bold")
elide: Text.ElideRight
}
Label // Machine variant
{
id: printerTypeLabel
anchors.top: printerNameLabel.bottom
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
text: printer.type
anchors.left: parent.left
elide: Text.ElideRight
font: UM.Theme.getFont("very_small")
opacity: 0.50
}
Rectangle // Camera icon
{
id: showCameraIcon
width: 40 * screenScaleFactor
height: width
radius: width
anchors.right: printProgressArea.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
color: emphasisColor
opacity: printer != null && printer.state === "unreachable" ? 0.3 : 1
Image
{
width: parent.width
height: width
anchors.right: parent.right
anchors.rightMargin: parent.rightMargin
source: "camera-icon.svg"
}
}
Row // PrintCore config
{
id: extruderInfo
anchors.bottom: parent.bottom
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
PrintCoreConfiguration
{
id: leftExtruderInfo
width: Math.round((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.extruders[0]
}
Rectangle
{
id: extruderSeperator
width: UM.Theme.getSize("default_lining").width
height: parent.height
color: lineColor
}
PrintCoreConfiguration
{
id: rightExtruderInfo
width: Math.round((parent.width - extruderSeperator.width) / 2)
printCoreConfiguration: printer.extruders[1]
}
}
Rectangle // Print progress
{
id: printProgressArea
anchors.right: parent.right
anchors.top: parent.top
height: showExtended ? parent.height: printProgressTitleBar.height
width: Math.round(parent.width / 2 - UM.Theme.getSize("default_margin").width)
border.width: UM.Theme.getSize("default_lining").width
border.color: lineColor
radius: cornerRadius
property var showExtended: {
if(printJob != null)
{
var extendStates = ["sent_to_printer", "wait_for_configuration", "printing", "pre_print", "post_print", "wait_cleanup", "queued"];
return extendStates.indexOf(printJob.state) !== -1;
}
return printer.state == "disabled"
}
Item // Status and Percent
{
id: printProgressTitleBar
property var showPercent: {
return printJob != null && (["printing", "post_print", "pre_print", "sent_to_printer"].indexOf(printJob.state) !== -1);
}
width: parent.width
//TODO: hardcoded value
height: 40 * screenScaleFactor
anchors.left: parent.left
Label
{
id: statusText
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.right: progressText.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: parent.verticalCenter
text: {
if (printer.state == "disabled")
{
return catalog.i18nc("@label:status", "Disabled");
}
if (printer.state === "unreachable")
{
return printerStatusText(printer);
}
if (printJob != null)
{
switch (printJob.state)
{
case "printing":
case "post_print":
return catalog.i18nc("@label:status", "Printing")
case "wait_for_configuration":
return catalog.i18nc("@label:status", "Reserved")
case "wait_cleanup":
case "wait_user_action":
return catalog.i18nc("@label:status", "Finished")
case "pre_print":
case "sent_to_printer":
return catalog.i18nc("@label", "Preparing to print")
case "queued":
return catalog.i18nc("@label:status", "Action required");
case "pausing":
case "paused":
return catalog.i18nc("@label:status", "Paused");
case "resuming":
return catalog.i18nc("@label:status", "Resuming");
case "aborted":
return catalog.i18nc("@label:status", "Print aborted");
default:
// If print job has unknown status show printer.status
return printerStatusText(printer);
}
}
return printerStatusText(printer);
}
elide: Text.ElideRight
font: UM.Theme.getFont("small")
}
Label
{
id: progressText
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.top: statusText.top
text: formatPrintJobPercent(printJob)
visible: printProgressTitleBar.showPercent
//TODO: Hardcoded value
opacity: 0.65
font: UM.Theme.getFont("very_small")
}
Image
{
width: statusText.height
height: width
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
anchors.top: statusText.top
visible: !printProgressTitleBar.showPercent
source: {
if (printer.state == "disabled")
{
return "blocked-icon.svg";
}
if (printer.state === "unreachable")
{
return "";
}
if (printJob != null)
{
if(printJob.state === "queued")
{
return "action-required-icon.svg";
}
else if (printJob.state === "wait_cleanup")
{
return "checkmark-icon.svg";
}
}
return ""; // We're not going to show it, so it will not be resolved as a url.
}
}
Rectangle
{
//TODO: This will become a progress bar in the future
width: parent.width
height: UM.Theme.getSize("default_lining").height
anchors.bottom: parent.bottom
anchors.left: parent.left
visible: printProgressArea.showExtended
color: lineColor
}
}
Column
{
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.top: printProgressTitleBar.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
width: parent.width - 2 * UM.Theme.getSize("default_margin").width
visible: printProgressArea.showExtended
Label // Status detail
{
text:
{
if (printer.state == "disabled")
{
return catalog.i18nc("@label", "Not accepting print jobs");
}
if (printer.state === "unreachable")
{
return "";
}
if(printJob != null)
{
switch (printJob.state)
{
case "printing":
case "post_print":
return catalog.i18nc("@label", "Finishes at: ") + OutputDevice.getTimeCompleted(printJob.timeTotal - printJob.timeElapsed)
case "wait_cleanup":
return catalog.i18nc("@label", "Clear build plate")
case "sent_to_printer":
case "pre_print":
return catalog.i18nc("@label", "Preparing to print")
case "wait_for_configuration":
return catalog.i18nc("@label", "Not accepting print jobs")
case "queued":
return catalog.i18nc("@label", "Waiting for configuration change");
default:
return "";
}
}
return "";
}
anchors.left: parent.left
anchors.right: parent.right
elide: Text.ElideRight
wrapMode: Text.Wrap
font: UM.Theme.getFont("default")
}
Label // Status 2nd row
{
text: {
if(printJob != null)
{
if(printJob.state == "printing" || printJob.state == "post_print")
{
return OutputDevice.getDateCompleted(printJob.timeTotal - printJob.timeElapsed)
}
}
return "";
}
elide: Text.ElideRight
font: UM.Theme.getFont("default")
}
}
}
}
}
}

View file

@ -1,54 +0,0 @@
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
Rectangle
{
id: base
width: 250 * screenScaleFactor
height: 250 * screenScaleFactor
signal clicked()
MouseArea
{
anchors.fill:parent
onClicked: base.clicked()
}
Rectangle
{
// TODO: Actually add UM icon / picture
width: 100 * screenScaleFactor
height: 100 * screenScaleFactor
border.width: UM.Theme.getSize("default_lining").width
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
anchors.topMargin: UM.Theme.getSize("default_margin").height
}
Label
{
id: nameLabel
anchors.bottom: ipLabel.top
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.rightMargin: UM.Theme.getSize("default_margin").width
text: modelData.friendly_name.toString()
font: UM.Theme.getFont("large")
elide: Text.ElideMiddle;
height: UM.Theme.getSize("section").height;
}
Label
{
id: ipLabel
text: modelData.ip_address.toString()
anchors.bottom: parent.bottom
anchors.bottomMargin: UM.Theme.getSize("default_margin").height
font: UM.Theme.getFont("default")
height:10 * screenScaleFactor
anchors.horizontalCenter: parent.horizontalCenter
}
}

View file

@ -1,11 +1,11 @@
# Copyright (c) 2017 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from . import DiscoverUM3Action
from .src import DiscoverUM3Action
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
from . import UM3OutputDevicePlugin
from .src import UM3OutputDevicePlugin
def getMetaData():
return {}

View file

@ -3,6 +3,6 @@
"author": "Ultimaker B.V.",
"description": "Manages network connections to Ultimaker 3 printers.",
"version": "1.0.0",
"api": 4,
"api": 5,
"i18n-catalog": "cura"
}

View file

@ -0,0 +1,729 @@
import QtQuick 2.3
import QtQuick.Dialogs 1.1
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.3
import QtGraphicalEffects 1.0
import QtQuick.Controls 2.0 as Controls2
import UM 1.3 as UM
import Cura 1.0 as Cura
Component
{
Rectangle
{
id: base
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
visible: OutputDevice != null
anchors.fill: parent
color: "white"
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Label
{
id: printingLabel
font: UM.Theme.getFont("large")
anchors
{
margins: 2 * UM.Theme.getSize("default_margin").width
leftMargin: 4 * UM.Theme.getSize("default_margin").width
top: parent.top
left: parent.left
right: parent.right
}
text: catalog.i18nc("@label", "Printing")
elide: Text.ElideRight
}
Label
{
id: managePrintersLabel
anchors.rightMargin: 4 * UM.Theme.getSize("default_margin").width
anchors.right: printerScrollView.right
anchors.bottom: printingLabel.bottom
text: catalog.i18nc("@label link to connect manager", "Manage printers")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("primary")
linkColor: UM.Theme.getColor("primary")
}
MouseArea
{
anchors.fill: managePrintersLabel
hoverEnabled: true
onClicked: Cura.MachineManager.printerOutputDevices[0].openPrinterControlPanel()
onEntered: managePrintersLabel.font.underline = true
onExited: managePrintersLabel.font.underline = false
}
ScrollView
{
id: printerScrollView
anchors
{
top: printingLabel.bottom
left: parent.left
right: parent.right
topMargin: UM.Theme.getSize("default_margin").height
bottom: parent.bottom
bottomMargin: UM.Theme.getSize("default_margin").height
}
style: UM.Theme.styles.scrollview
ListView
{
anchors
{
top: parent.top
bottom: parent.bottom
left: parent.left
right: parent.right
leftMargin: 2 * UM.Theme.getSize("default_margin").width
rightMargin: 2 * UM.Theme.getSize("default_margin").width
}
spacing: UM.Theme.getSize("default_margin").height -10
model: OutputDevice.printers
delegate: Item
{
width: parent.width
height: base.height + 2 * base.shadowRadius // To ensure that the shadow doesn't get cut off.
Rectangle
{
width: parent.width - 2 * shadowRadius
height: childrenRect.height + UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
id: base
property var shadowRadius: 5
property var collapsed: true
layer.enabled: true
layer.effect: DropShadow
{
radius: base.shadowRadius
verticalOffset: 2
color: "#3F000000" // 25% shadow
}
Item
{
id: printerInfo
height: machineIcon.height
anchors
{
top: parent.top
left: parent.left
right: parent.right
margins: UM.Theme.getSize("default_margin").width
}
MouseArea
{
anchors.fill: parent
onClicked: base.collapsed = !base.collapsed
}
Item
{
id: machineIcon
// Yeah, this is hardcoded now, but I can't think of a good way to fix this.
// The UI is going to get another update soon, so it's probably not worth the effort...
width: 58
height: 58
anchors.top: parent.top
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.left: parent.left
UM.RecolorImage
{
anchors.centerIn: parent
source:
{
switch(modelData.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"
}
}
width: sourceSize.width
height: sourceSize.height
color:
{
if(modelData.state == "disabled")
{
return UM.Theme.getColor("setting_control_disabled")
}
if(modelData.activePrintJob != undefined)
{
return UM.Theme.getColor("primary")
}
return UM.Theme.getColor("setting_control_disabled")
}
}
}
Item
{
height: childrenRect.height
anchors
{
right: collapseIcon.left
rightMargin: UM.Theme.getSize("default_margin").width
left: machineIcon.right
leftMargin: UM.Theme.getSize("default_margin").width
verticalCenter: machineIcon.verticalCenter
}
Label
{
id: machineNameLabel
text: modelData.name
width: parent.width
elide: Text.ElideRight
font: UM.Theme.getFont("default_bold")
}
Label
{
id: activeJobLabel
text:
{
if (modelData.state == "disabled")
{
return catalog.i18nc("@label", "Not available")
} else if (modelData.state == "unreachable")
{
return catalog.i18nc("@label", "Unreachable")
}
if (modelData.activePrintJob != null)
{
return modelData.activePrintJob.name
}
return catalog.i18nc("@label", "Available")
}
anchors.top: machineNameLabel.bottom
width: parent.width
elide: Text.ElideRight
font: UM.Theme.getFont("default")
opacity: 0.6
}
}
UM.RecolorImage
{
id: collapseIcon
width: 15
height: 15
sourceSize.width: width
sourceSize.height: height
source: base.collapsed ? UM.Theme.getIcon("arrow_left") : UM.Theme.getIcon("arrow_bottom")
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").width
color: "black"
}
}
Item
{
id: detailedInfo
property var printJob: modelData.activePrintJob
visible: height == childrenRect.height
anchors.top: printerInfo.bottom
width: parent.width
height: !base.collapsed ? childrenRect.height : 0
opacity: visible ? 1 : 0
Behavior on height { NumberAnimation { duration: 100 } }
Behavior on opacity { NumberAnimation { duration: 100 } }
Rectangle
{
id: topSpacer
color: UM.Theme.getColor("viewport_background")
height: 2
anchors
{
left: parent.left
right: parent.right
margins: UM.Theme.getSize("default_margin").width
top: parent.top
topMargin: UM.Theme.getSize("default_margin").width
}
}
PrinterFamilyPill
{
id: printerFamilyPill
color: UM.Theme.getColor("viewport_background")
anchors.top: topSpacer.bottom
anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height
text: modelData.type
anchors.left: parent.left
anchors.leftMargin: UM.Theme.getSize("default_margin").width
padding: 3
}
Row
{
id: extrudersInfo
anchors.top: printerFamilyPill.bottom
anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.leftMargin: 2 * UM.Theme.getSize("default_margin").width
anchors.right: parent.right
anchors.rightMargin: 2 * UM.Theme.getSize("default_margin").width
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
PrintCoreConfiguration
{
id: leftExtruderInfo
width: Math.round(parent.width / 2)
printCoreConfiguration: modelData.printerConfiguration.extruderConfigurations[0]
}
PrintCoreConfiguration
{
id: rightExtruderInfo
width: Math.round(parent.width / 2)
printCoreConfiguration: modelData.printerConfiguration.extruderConfigurations[1]
}
}
Rectangle
{
id: jobSpacer
color: UM.Theme.getColor("viewport_background")
height: 2
anchors
{
left: parent.left
right: parent.right
margins: UM.Theme.getSize("default_margin").width
top: extrudersInfo.bottom
topMargin: 2 * UM.Theme.getSize("default_margin").height
}
}
Item
{
id: jobInfo
property var showJobInfo: modelData.activePrintJob != null && modelData.activePrintJob.state != "queued"
anchors.top: jobSpacer.bottom
anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").width
anchors.leftMargin: 2 * UM.Theme.getSize("default_margin").width
height: showJobInfo ? childrenRect.height + 2 * UM.Theme.getSize("default_margin").height: 0
visible: showJobInfo
Label
{
id: printJobName
text: modelData.activePrintJob != null ? modelData.activePrintJob.name : ""
font: UM.Theme.getFont("default_bold")
anchors.left: parent.left
anchors.right: contextButton.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
elide: Text.ElideRight
}
Label
{
id: ownerName
anchors.top: printJobName.bottom
text: modelData.activePrintJob != null ? modelData.activePrintJob.owner : ""
font: UM.Theme.getFont("default")
opacity: 0.6
width: parent.width
elide: Text.ElideRight
}
function switchPopupState()
{
if (popup.visible)
{
popup.close()
}
else
{
popup.open()
}
}
Controls2.Button
{
id: contextButton
text: "\u22EE" //Unicode; Three stacked points.
font.pixelSize: 25
width: 35
height: width
anchors
{
right: parent.right
top: parent.top
}
hoverEnabled: true
background: Rectangle
{
opacity: contextButton.down || contextButton.hovered ? 1 : 0
width: contextButton.width
height: contextButton.height
radius: 0.5 * width
color: UM.Theme.getColor("viewport_background")
}
onClicked: parent.switchPopupState()
}
Controls2.Popup
{
// TODO Change once updating to Qt5.10 - The 'opened' property is in 5.10 but the behavior is now implemented with the visible property
id: popup
clip: true
closePolicy: Controls2.Popup.CloseOnPressOutsideParent
x: parent.width - width
y: contextButton.height
width: 160
height: contentItem.height + 2 * padding
visible: false
transformOrigin: Controls2.Popup.Top
contentItem: Item
{
width: popup.width - 2 * popup.padding
height: childrenRect.height + 15
Controls2.Button
{
id: pauseButton
text: modelData.activePrintJob != null && modelData.activePrintJob.state == "paused" ? catalog.i18nc("@label", "Resume") : catalog.i18nc("@label", "Pause")
onClicked:
{
if(modelData.activePrintJob.state == "paused")
{
modelData.activePrintJob.setState("print")
}
else if(modelData.activePrintJob.state == "printing")
{
modelData.activePrintJob.setState("pause")
}
popup.close()
}
width: parent.width
enabled: modelData.activePrintJob != null && ["paused", "printing"].indexOf(modelData.activePrintJob.state) >= 0
anchors.top: parent.top
anchors.topMargin: 10
hoverEnabled: true
background: Rectangle
{
opacity: pauseButton.down || pauseButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background")
}
}
Controls2.Button
{
id: abortButton
text: catalog.i18nc("@label", "Abort")
onClicked:
{
abortConfirmationDialog.visible = true;
popup.close();
}
width: parent.width
anchors.top: pauseButton.bottom
hoverEnabled: true
enabled: modelData.activePrintJob != null && ["paused", "printing", "pre_print"].indexOf(modelData.activePrintJob.state) >= 0
background: Rectangle
{
opacity: abortButton.down || abortButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background")
}
}
MessageDialog
{
id: abortConfirmationDialog
title: catalog.i18nc("@window:title", "Abort print")
icon: StandardIcon.Warning
text: catalog.i18nc("@label %1 is the name of a print job.", "Are you sure you want to abort %1?").arg(modelData.activePrintJob.name)
standardButtons: StandardButton.Yes | StandardButton.No
Component.onCompleted: visible = false
onYes: modelData.activePrintJob.setState("abort")
}
}
background: Item
{
width: popup.width
height: popup.height
DropShadow
{
anchors.fill: pointedRectangle
radius: 5
color: "#3F000000" // 25% shadow
source: pointedRectangle
transparentBorder: true
verticalOffset: 2
}
Item
{
id: pointedRectangle
width: parent.width -10
height: parent.height -10
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
Rectangle
{
id: point
height: 13
width: 13
color: UM.Theme.getColor("setting_control")
transform: Rotation { angle: 45}
anchors.right: bloop.right
y: 1
}
Rectangle
{
id: bloop
color: UM.Theme.getColor("setting_control")
width: parent.width
anchors.top: parent.top
anchors.topMargin: 10
anchors.bottom: parent.bottom
anchors.bottomMargin: 5
}
}
}
exit: Transition
{
// This applies a default NumberAnimation to any changes a state change makes to x or y properties
NumberAnimation { property: "visible"; duration: 75; }
}
enter: Transition
{
// This applies a default NumberAnimation to any changes a state change makes to x or y properties
NumberAnimation { property: "visible"; duration: 75; }
}
onClosed: visible = false
onOpened: visible = true
}
Image
{
id: printJobPreview
source: modelData.activePrintJob != null ? modelData.activePrintJob.previewImageUrl : ""
anchors.top: ownerName.bottom
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width / 2
height: width
opacity:
{
if(modelData.activePrintJob == null)
{
return 1.0
}
switch(modelData.activePrintJob.state)
{
case "wait_cleanup":
case "wait_user_action":
case "paused":
return 0.5
default:
return 1.0
}
}
}
UM.RecolorImage
{
id: statusImage
anchors.centerIn: printJobPreview
source:
{
if(modelData.activePrintJob == null)
{
return ""
}
switch(modelData.activePrintJob.state)
{
case "paused":
return "../svg/paused-icon.svg"
case "wait_cleanup":
if(modelData.activePrintJob.timeElapsed < modelData.activePrintJob.timeTotal)
{
return "../svg/aborted-icon.svg"
}
return "../svg/approved-icon.svg"
case "wait_user_action":
return "../svg/aborted-icon.svg"
default:
return ""
}
}
visible: source != ""
width: 0.5 * printJobPreview.width
height: 0.5 * printJobPreview.height
sourceSize.width: width
sourceSize.height: height
color: "black"
}
Rectangle
{
id: showCameraIcon
width: 35 * screenScaleFactor
height: width
radius: 0.5 * width
anchors.left: parent.left
anchors.bottom: printJobPreview.bottom
color: UM.Theme.getColor("setting_control_border_highlight")
Image
{
width: parent.width
height: width
anchors.right: parent.right
anchors.rightMargin: parent.rightMargin
source: "../svg/camera-icon.svg"
}
MouseArea
{
anchors.fill:parent
onClicked:
{
OutputDevice.setActiveCamera(modelData.camera)
}
}
}
}
}
ProgressBar
{
property var progress:
{
if(modelData.activePrintJob == null)
{
return 0
}
var result = modelData.activePrintJob.timeElapsed / modelData.activePrintJob.timeTotal
if(result > 1.0)
{
result = 1.0
}
return result
}
id: jobProgressBar
width: parent.width
value: progress
anchors.top: detailedInfo.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height
visible: modelData.activePrintJob != null && modelData.activePrintJob != undefined
style: ProgressBarStyle
{
property var progressText:
{
if(modelData.activePrintJob == null)
{
return ""
}
switch(modelData.activePrintJob.state)
{
case "wait_cleanup":
if(modelData.activePrintJob.timeTotal > modelData.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":
case "wait_user_action":
return catalog.i18nc("@label:status", "Aborted")
case "pausing":
return catalog.i18nc("@label:status", "Pausing")
case "paused":
return catalog.i18nc("@label:status", "Paused")
case "resuming":
return catalog.i18nc("@label:status", "Resuming")
case "queued":
return catalog.i18nc("@label:status", "Action required")
default:
OutputDevice.formatDuration(modelData.activePrintJob.timeTotal - modelData.activePrintJob.timeElapsed)
}
}
background: Rectangle
{
implicitWidth: 100
implicitHeight: visible ? 24 : 0
color: UM.Theme.getColor("viewport_background")
}
progress: Rectangle
{
color: UM.Theme.getColor("primary")
id: progressItem
function getTextOffset()
{
if(progressItem.width + progressLabel.width < control.width)
{
return progressItem.width + UM.Theme.getSize("default_margin").width
}
else
{
return progressItem.width - progressLabel.width - UM.Theme.getSize("default_margin").width
}
}
Label
{
id: progressLabel
anchors.left: parent.left
anchors.leftMargin: getTextOffset()
text: progressText
anchors.verticalCenter: parent.verticalCenter
color: progressItem.width + progressLabel.width < control.width ? "black" : "white"
width: contentWidth
font: UM.Theme.getFont("default")
}
}
}
}
}
}
}
}
}
}

View file

@ -0,0 +1,108 @@
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
width: maximumWidth
height: maximumHeight
color: UM.Theme.getColor("viewport_background")
property var emphasisColor: UM.Theme.getColor("setting_control_border_highlight")
property var lineColor: "#DCDCDC" // TODO: Should be linked to theme.
property var cornerRadius: 4 * screenScaleFactor // TODO: Should be linked to theme.
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Label
{
id: manageQueueLabel
anchors.rightMargin: 4 * UM.Theme.getSize("default_margin").width
anchors.right: queuedPrintJobs.right
anchors.bottom: queuedLabel.bottom
text: catalog.i18nc("@label link to connect manager", "Manage queue")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("primary")
linkColor: UM.Theme.getColor("primary")
}
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
anchors.top: parent.top
anchors.topMargin: 2 * UM.Theme.getSize("default_margin").height
anchors.leftMargin: 3 * UM.Theme.getSize("default_margin").width
text: catalog.i18nc("@label", "Queued")
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
}
ScrollView
{
id: queuedPrintJobs
anchors
{
top: queuedLabel.bottom
topMargin: UM.Theme.getSize("default_margin").height
horizontalCenter: parent.horizontalCenter
bottomMargin: 0
bottom: parent.bottom
}
style: UM.Theme.styles.scrollview
width: Math.min(800 * screenScaleFactor, maximumWidth)
ListView
{
anchors.fill: parent
//anchors.margins: UM.Theme.getSize("default_margin").height
spacing: UM.Theme.getSize("default_margin").height - 10 // 2x the shadow radius
model: OutputDevice.queuedPrintJobs
delegate: PrintJobInfoBlock
{
printJob: modelData
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: UM.Theme.getSize("default_margin").height
anchors.leftMargin: UM.Theme.getSize("default_margin").height
height: 175 * screenScaleFactor
}
}
}
PrinterVideoStream
{
visible: OutputDevice.activeCamera != null
anchors.fill: parent
camera: OutputDevice.activeCamera
}
onVisibleChanged:
{
if (monitorFrame != null && !monitorFrame.visible)
{
OutputDevice.setActiveCamera(null)
}
}
}
}

View file

@ -364,7 +364,6 @@ Cura.MachineAction
{
id: addressField
width: parent.width
maximumLength: 40
validator: RegExpValidator
{
regExp: /[a-zA-Z0-9\.\-\_]*/

View file

@ -0,0 +1,93 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.2 as UM
Item
{
id: extruderInfo
property var printCoreConfiguration
width: Math.round(parent.width / 2)
height: childrenRect.height
Item
{
id: extruderCircle
width: 30
height: 30
anchors.verticalCenter: printAndMaterialLabel.verticalCenter
opacity:
{
if(printCoreConfiguration == null || printCoreConfiguration.activeMaterial == null || printCoreConfiguration.hotendID == null)
{
return 0.5
}
return 1
}
Rectangle
{
anchors.fill: parent
radius: Math.round(width / 2)
border.width: 2
border.color: "black"
}
Label
{
anchors.centerIn: parent
font: UM.Theme.getFont("default_bold")
text: printCoreConfiguration.position + 1
}
}
Item
{
id: printAndMaterialLabel
anchors
{
right: parent.right
left: extruderCircle.right
margins: UM.Theme.getSize("default_margin").width
}
height: childrenRect.height
Label
{
id: materialLabel
text:
{
if(printCoreConfiguration != undefined && printCoreConfiguration.activeMaterial != undefined)
{
return printCoreConfiguration.activeMaterial.name
}
return ""
}
font: UM.Theme.getFont("default_bold")
elide: Text.ElideRight
width: parent.width
}
Label
{
id: printCoreLabel
text:
{
if(printCoreConfiguration != undefined && printCoreConfiguration.hotendID != undefined)
{
return printCoreConfiguration.hotendID
}
return ""
}
anchors.top: materialLabel.bottom
elide: Text.ElideRight
width: parent.width
opacity: 0.6
font: UM.Theme.getFont("default")
}
}
}

View file

@ -0,0 +1,401 @@
import QtQuick 2.2
import QtQuick.Dialogs 1.1
import QtQuick.Controls 2.0
import QtQuick.Controls.Styles 1.4
import QtGraphicalEffects 1.0
import UM 1.3 as UM
Item
{
id: base
property var printJob: null
property var shadowRadius: 5
function getPrettyTime(time)
{
return OutputDevice.formatDuration(time)
}
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Rectangle
{
id: background
anchors
{
top: parent.top
topMargin: 3
left: parent.left
leftMargin: base.shadowRadius
rightMargin: base.shadowRadius
right: parent.right
bottom: parent.bottom
bottomMargin: base.shadowRadius
}
layer.enabled: true
layer.effect: DropShadow
{
radius: base.shadowRadius
verticalOffset: 2
color: "#3F000000" // 25% shadow
}
Item
{
// Content on the left of the infobox
anchors
{
top: parent.top
bottom: parent.bottom
left: parent.left
right: parent.horizontalCenter
margins: 2 * UM.Theme.getSize("default_margin").width
rightMargin: UM.Theme.getSize("default_margin").width
}
Label
{
id: printJobName
text: printJob.name
font: UM.Theme.getFont("default_bold")
width: parent.width
elide: Text.ElideRight
}
Label
{
id: ownerName
anchors.top: printJobName.bottom
text: printJob.owner
font: UM.Theme.getFont("default")
opacity: 0.6
width: parent.width
elide: Text.ElideRight
}
Image
{
id: printJobPreview
source: printJob.previewImageUrl
anchors.top: ownerName.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: totalTimeLabel.bottom
width: height
opacity: printJob.state == "error" ? 0.5 : 1.0
}
UM.RecolorImage
{
id: statusImage
anchors.centerIn: printJobPreview
source: printJob.state == "error" ? "../svg/aborted-icon.svg" : ""
visible: source != ""
width: 0.5 * printJobPreview.width
height: 0.5 * printJobPreview.height
sourceSize.width: width
sourceSize.height: height
color: "black"
}
Label
{
id: totalTimeLabel
opacity: 0.6
anchors.bottom: parent.bottom
anchors.right: parent.right
font: UM.Theme.getFont("default")
text: printJob != null ? getPrettyTime(printJob.timeTotal) : ""
elide: Text.ElideRight
}
}
Item
{
// Content on the right side of the infobox.
anchors
{
top: parent.top
bottom: parent.bottom
left: parent.horizontalCenter
right: parent.right
margins: 2 * UM.Theme.getSize("default_margin").width
leftMargin: UM.Theme.getSize("default_margin").width
}
Label
{
id: targetPrinterLabel
elide: Text.ElideRight
font: UM.Theme.getFont("default_bold")
text:
{
if(printJob.assignedPrinter == null)
{
if(printJob.state == "error")
{
return catalog.i18nc("@label", "Waiting for: Unavailable printer")
}
return catalog.i18nc("@label", "Waiting for: First available")
}
else
{
return catalog.i18nc("@label", "Waiting for: ") + printJob.assignedPrinter.name
}
}
anchors
{
left: parent.left
right: contextButton.left
rightMargin: UM.Theme.getSize("default_margin").width
}
}
function switchPopupState()
{
popup.visible ? popup.close() : popup.open()
}
Button
{
id: contextButton
text: "\u22EE" //Unicode; Three stacked points.
font.pixelSize: 25
width: 35
height: width
anchors
{
right: parent.right
top: parent.top
}
hoverEnabled: true
background: Rectangle
{
opacity: contextButton.down || contextButton.hovered ? 1 : 0
width: contextButton.width
height: contextButton.height
radius: 0.5 * width
color: UM.Theme.getColor("viewport_background")
}
onClicked: parent.switchPopupState()
}
Popup
{
// TODO Change once updating to Qt5.10 - The 'opened' property is in 5.10 but the behavior is now implemented with the visible property
id: popup
clip: true
closePolicy: Popup.CloseOnPressOutsideParent
x: parent.width - width
y: contextButton.height
width: 160
height: contentItem.height + 2 * padding
visible: false
transformOrigin: Popup.Top
contentItem: Item
{
width: popup.width - 2 * popup.padding
height: childrenRect.height + 15
Button
{
id: sendToTopButton
text: catalog.i18nc("@label", "Move to top")
onClicked:
{
sendToTopConfirmationDialog.visible = true;
popup.close();
}
width: parent.width
enabled: OutputDevice.queuedPrintJobs[0].key != printJob.key
anchors.top: parent.top
anchors.topMargin: 10
hoverEnabled: true
background: Rectangle
{
opacity: sendToTopButton.down || sendToTopButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background")
}
}
MessageDialog
{
id: sendToTopConfirmationDialog
title: catalog.i18nc("@window:title", "Move print job to top")
icon: StandardIcon.Warning
text: catalog.i18nc("@label %1 is the name of a print job.", "Are you sure you want to move %1 to the top of the queue?").arg(printJob.name)
standardButtons: StandardButton.Yes | StandardButton.No
Component.onCompleted: visible = false
onYes: OutputDevice.sendJobToTop(printJob.key)
}
Button
{
id: deleteButton
text: catalog.i18nc("@label", "Delete")
onClicked:
{
deleteConfirmationDialog.visible = true;
popup.close();
}
width: parent.width
anchors.top: sendToTopButton.bottom
hoverEnabled: true
background: Rectangle
{
opacity: deleteButton.down || deleteButton.hovered ? 1 : 0
color: UM.Theme.getColor("viewport_background")
}
}
MessageDialog
{
id: deleteConfirmationDialog
title: catalog.i18nc("@window:title", "Delete print job")
icon: StandardIcon.Warning
text: catalog.i18nc("@label %1 is the name of a print job.", "Are you sure you want to delete %1?").arg(printJob.name)
standardButtons: StandardButton.Yes | StandardButton.No
Component.onCompleted: visible = false
onYes: OutputDevice.deleteJobFromQueue(printJob.key)
}
}
background: Item
{
width: popup.width
height: popup.height
DropShadow
{
anchors.fill: pointedRectangle
radius: 5
color: "#3F000000" // 25% shadow
source: pointedRectangle
transparentBorder: true
verticalOffset: 2
}
Item
{
id: pointedRectangle
width: parent.width -10
height: parent.height -10
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
Rectangle
{
id: point
height: 13
width: 13
color: UM.Theme.getColor("setting_control")
transform: Rotation { angle: 45}
anchors.right: bloop.right
y: 1
}
Rectangle
{
id: bloop
color: UM.Theme.getColor("setting_control")
width: parent.width
anchors.top: parent.top
anchors.topMargin: 10
anchors.bottom: parent.bottom
anchors.bottomMargin: 5
}
}
}
exit: Transition
{
// This applies a default NumberAnimation to any changes a state change makes to x or y properties
NumberAnimation { property: "visible"; duration: 75; }
}
enter: Transition
{
// This applies a default NumberAnimation to any changes a state change makes to x or y properties
NumberAnimation { property: "visible"; duration: 75; }
}
onClosed: visible = false
onOpened: visible = true
}
Row
{
id: printerFamilyPills
spacing: 0.5 * UM.Theme.getSize("default_margin").width
anchors
{
left: parent.left
right: parent.right
bottom: extrudersInfo.top
bottomMargin: UM.Theme.getSize("default_margin").height
}
height: childrenRect.height
Repeater
{
model: printJob.compatibleMachineFamilies
delegate: PrinterFamilyPill
{
text: modelData
color: UM.Theme.getColor("viewport_background")
padding: 3
}
}
}
// PrintCore && Material config
Row
{
id: extrudersInfo
anchors.bottom: parent.bottom
anchors
{
left: parent.left
right: parent.right
}
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
PrintCoreConfiguration
{
id: leftExtruderInfo
width: Math.round(parent.width / 2)
printCoreConfiguration: printJob.configuration.extruderConfigurations[0]
}
PrintCoreConfiguration
{
id: rightExtruderInfo
width: Math.round(parent.width / 2)
printCoreConfiguration: printJob.configuration.extruderConfigurations[1]
}
}
}
Rectangle
{
color: UM.Theme.getColor("viewport_background")
width: 2
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: UM.Theme.getSize("default_margin").height
anchors.horizontalCenter: parent.horizontalCenter
}
}
}

View file

@ -0,0 +1,28 @@
import QtQuick 2.2
import QtQuick.Controls 1.4
import UM 1.2 as UM
Item
{
property alias color: background.color
property alias text: familyNameLabel.text
property var padding: 0
implicitHeight: familyNameLabel.contentHeight + 2 * padding // Apply the padding to top and bottom.
implicitWidth: familyNameLabel.contentWidth + implicitHeight // The extra height is added to ensure the radius doesn't cut something off.
Rectangle
{
id: background
height: parent.height
width: parent.width
color: parent.color
anchors.right: parent.right
anchors.horizontalCenter: parent.horizontalCenter
radius: 0.5 * height
}
Label
{
id: familyNameLabel
anchors.centerIn: parent
text: ""
}
}

View file

@ -7,6 +7,8 @@ import UM 1.3 as UM
Item
{
property var camera: null
Rectangle
{
anchors.fill:parent
@ -17,7 +19,7 @@ Item
MouseArea
{
anchors.fill: parent
onClicked: OutputDevice.setActivePrinter(null)
onClicked: OutputDevice.setActiveCamera(null)
z: 0
}
@ -32,7 +34,7 @@ Item
width: 20 * screenScaleFactor
height: 20 * screenScaleFactor
onClicked: OutputDevice.setActivePrinter(null)
onClicked: OutputDevice.setActiveCamera(null)
style: ButtonStyle
{
@ -65,23 +67,24 @@ Item
{
if(visible)
{
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
if(camera != null)
{
OutputDevice.activePrinter.camera.start()
camera.start()
}
} else
{
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null)
if(camera != null)
{
OutputDevice.activePrinter.camera.stop()
camera.stop()
}
}
}
source:
{
if(OutputDevice.activePrinter != null && OutputDevice.activePrinter.camera != null && OutputDevice.activePrinter.camera.latestImage)
if(camera != null && camera.latestImage != null)
{
return OutputDevice.activePrinter.camera.latestImage;
return camera.latestImage;
}
return "";
}
@ -92,7 +95,7 @@ Item
anchors.fill: cameraImage
onClicked:
{
OutputDevice.setActivePrinter(null)
OutputDevice.setActiveCamera(null)
}
z: 1
}

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 46.16 48"><defs><style>.cls-1{fill:#9a9a9a;}</style></defs><title>UM3-icon</title><g id="Symbols"><g id="system_overview_inactive" data-name="system overview inactive"><path id="Shape" class="cls-1" d="M18.4,12.2h9.26c.1,0,.1,0,.2-.2l1.73-4.27a.22.22,0,0,0-.2-.2H16.67a.22.22,0,0,0-.2.2L18.2,12C18.3,12.2,18.3,12.2,18.4,12.2Z"/><path id="Shape-2" data-name="Shape" class="cls-1" d="M38.33,35.08H7.72a.48.48,0,0,0-.5.51V37a.48.48,0,0,0,.5.51H38.44a.48.48,0,0,0,.5-.51V35.59A.64.64,0,0,0,38.33,35.08Z"/><path id="Shape-3" data-name="Shape" class="cls-1" d="M0,0V48H3.76a2.86,2.86,0,0,1,2.13-1H40.27a2.86,2.86,0,0,1,2.13,1h3.76V0ZM41.28,37a2.83,2.83,0,0,1-2.84,2.84H7.72A2.84,2.84,0,0,1,4.88,37V5.49a.65.65,0,0,1,.61-.61H40.67a.65.65,0,0,1,.61.61Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 847 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 46.16 58"><defs><style>.cls-1{fill:#9a9a9a;}</style></defs><title>UM3x-icon</title><g id="Symbols"><g id="system_overview_inactive" data-name="system overview inactive"><path id="Shape" class="cls-1" d="M18.4,12.2h9.26c.1,0,.1,0,.2-.2l1.73-4.27a.22.22,0,0,0-.2-.2H16.67a.22.22,0,0,0-.2.2L18.2,12C18.3,12.2,18.3,12.2,18.4,12.2Z"/><path id="Shape-2" data-name="Shape" class="cls-1" d="M38.33,45.08H7.72a.48.48,0,0,0-.5.51V47a.48.48,0,0,0,.5.51H38.44a.48.48,0,0,0,.5-.51V45.59A.64.64,0,0,0,38.33,45.08Z"/><path id="Shape-3" data-name="Shape" class="cls-1" d="M0,0V58H3.76a2.86,2.86,0,0,1,2.13-1H40.27a2.86,2.86,0,0,1,2.13,1h3.76V0ZM41.28,35.32V47a2.83,2.83,0,0,1-2.84,2.84H7.72A2.84,2.84,0,0,1,4.88,47V5.49a.65.65,0,0,1,.61-.61H40.67a.65.65,0,0,1,.61.61Z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 854 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58"><defs><style>.cls-1{fill:none;}.cls-2{fill:#9a9a9a;}</style></defs><title>UMs5-icon</title><path class="cls-1" d="M33.83,12.33c-.1.2-.1.2-.2.2H24.37c-.1,0-.1,0-.2-.2L22.44,8.06a.22.22,0,0,1,.2-.2H35.36a.22.22,0,0,1,.2.2Z"/><path class="cls-2" d="M35.36,7.86H22.64a.22.22,0,0,0-.2.2l1.73,4.27c.1.2.1.2.2.2h9.26c.1,0,.1,0,.2-.2l1.73-4.27A.22.22,0,0,0,35.36,7.86Z"/><path class="cls-2" d="M0,0V58H3.75a2.85,2.85,0,0,1,2.12-1H52.13a2.85,2.85,0,0,1,2.12,1H58V0ZM37.5,53.82a1.5,1.5,0,0,1-1.5,1.5H22a1.5,1.5,0,0,1-1.5-1.5v-4a1.5,1.5,0,0,1,1.5-1.5H36a1.5,1.5,0,0,1,1.5,1.5Zm15.63-18.5V47a2.83,2.83,0,0,1-2.83,2.84H38.5v0a2.5,2.5,0,0,0-2.5-2.5H22a2.5,2.5,0,0,0-2.5,2.5v0H7.7A2.83,2.83,0,0,1,4.87,47V5.49a.65.65,0,0,1,.6-.61H52.53a.65.65,0,0,1,.6.61Z"/></svg>

After

Width:  |  Height:  |  Size: 842 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><title>aborted-icon</title><path d="M16,0A16,16,0,1,0,32,16,16,16,0,0,0,16,0Zm1.69,28.89a13,13,0,1,1,11.2-11.2A13,13,0,0,1,17.69,28.89Z"/><polygon points="20.6 9.28 16 13.88 11.4 9.28 9.28 11.4 13.88 16 9.28 20.6 11.4 22.72 16 18.12 20.6 22.72 22.72 20.6 18.12 16 22.72 11.4 20.6 9.28"/></svg>

After

Width:  |  Height:  |  Size: 386 B

View file

Before

Width:  |  Height:  |  Size: 844 B

After

Width:  |  Height:  |  Size: 844 B

Before After
Before After

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><title>approved-icon</title><path d="M16,29A13,13,0,1,0,3,16,13,13,0,0,0,16,29ZM8,14.59l6,5.3L23.89,9l2.22,2L14.18,24.11,6,16.83Z" fill="none"/><path d="M16,32A16,16,0,1,0,0,16,16,16,0,0,0,16,32ZM16,3A13,13,0,1,1,3,16,13,13,0,0,1,16,3Z"/><polygon points="26.11 11.01 23.89 8.99 13.96 19.89 8 14.59 6 16.83 14.18 24.11 26.11 11.01"/></svg>

After

Width:  |  Height:  |  Size: 431 B

View file

Before

Width:  |  Height:  |  Size: 311 B

After

Width:  |  Height:  |  Size: 311 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 438 B

After

Width:  |  Height:  |  Size: 438 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 194 B

After

Width:  |  Height:  |  Size: 194 B

Before After
Before After

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><title>paused-icon</title><path d="M16,0A16,16,0,1,0,32,16,16,16,0,0,0,16,0Zm0,29A13,13,0,1,1,29,16,13,13,0,0,1,16,29Z"/><rect x="11.5" y="9" width="3" height="14"/><rect x="17.5" y="9" width="3" height="14"/></svg>

After

Width:  |  Height:  |  Size: 308 B

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><title>warning-icon</title><path d="M18.09,1.31A2.35,2.35,0,0,0,16,0a2.31,2.31,0,0,0-2.09,1.31L.27,28.44A2.49,2.49,0,0,0,.11,30.3a2.38,2.38,0,0,0,1.16,1.42A2.33,2.33,0,0,0,2.36,32H29.64A2.4,2.4,0,0,0,32,29.57a2.55,2.55,0,0,0-.27-1.14ZM3.34,29,16,3.83,28.66,29Z"/><polygon points="13.94 25.19 13.94 25.19 13.94 25.19 13.94 25.19"/><polygon points="14.39 21.68 17.61 21.68 18.11 11.85 13.89 11.85 14.39 21.68"/><path d="M16.06,23.08a2.19,2.19,0,0,0-1.56,3.66,2.14,2.14,0,0,0,1.56.55,2.06,2.06,0,0,0,1.54-.55,2.1,2.1,0,0,0,.55-1.55,2.17,2.17,0,0,0-.53-1.55A2.05,2.05,0,0,0,16.06,23.08Z"/></svg>

After

Width:  |  Height:  |  Size: 684 B

View file

@ -4,19 +4,21 @@
from typing import Any, cast, Optional, Set, Tuple, Union
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.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.Mesh.MeshWriter import MeshWriter # For typing
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 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.PrinterOutputModel import PrinterOutputModel
from cura.PrinterOutput.PrintJobOutputModel import PrintJobOutputModel
@ -27,14 +29,14 @@ from .ClusterUM3PrinterOutputController import ClusterUM3PrinterOutputController
from .SendMaterialJob import SendMaterialJob
from PyQt5.QtNetwork import QNetworkRequest, QNetworkReply
from PyQt5.QtGui import QDesktopServices
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, Set
from typing import Optional, Dict, List
import io #To create the correct buffers for sending data to the printer.
import io # To create the correct buffers for sending data to the printer.
import json
import os
@ -44,6 +46,7 @@ i18n_catalog = i18nCatalog("cura")
class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
printJobsChanged = pyqtSignal()
activePrinterChanged = pyqtSignal()
activeCameraChanged = 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.
@ -59,24 +62,24 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._print_jobs = [] # type: List[PrintJobOutputModel]
self._monitor_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterMonitorItem.qml")
self._control_view_qml_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ClusterControlItem.qml")
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")
# See comments about this hack with the clusterPrintersChanged signal
self.printersChanged.connect(self.clusterPrintersChanged)
self._accepts_commands = True #type: bool
self._accepts_commands = True # type: bool
# Cluster does not have authentication, so default to authenticated
self._authentication_state = AuthState.Authenticated
self._error_message = None #type: Optional[Message]
self._write_job_progress_message = None #type: Optional[Message]
self._progress_message = None #type: Optional[Message]
self._error_message = None # type: Optional[Message]
self._write_job_progress_message = None # type: Optional[Message]
self._progress_message = None # type: Optional[Message]
self._active_printer = None # type: Optional[PrinterOutputModel]
self._printer_selection_dialog = None #type: QObject
self._printer_selection_dialog = None # type: QObject
self.setPriority(3) # Make sure the output device gets selected above local file output
self.setName(self._id)
@ -87,32 +90,35 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._printer_uuid_to_unique_name_mapping = {} # type: Dict[str, str]
self._finished_jobs = [] # type: List[PrintJobOutputModel]
self._finished_jobs = [] # type: List[PrintJobOutputModel]
self._cluster_size = int(properties.get(b"cluster_size", 0))
self._cluster_size = int(properties.get(b"cluster_size", 0)) # type: int
self._latest_reply_handler = None #type: Optional[QNetworkReply]
self._latest_reply_handler = None # type: Optional[QNetworkReply]
self._sending_job = None
self._active_camera = None # type: Optional[NetworkCamera]
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).
# 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.
# 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.
# 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
@ -125,7 +131,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
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.
# Just take the first file format available.
if file_handler is not None:
writer = file_handler.getWriterByMimeType(cast(str, preferred_format["mime_type"]))
else:
@ -135,29 +141,30 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
Logger.log("e", "Unexpected error when trying to get the FileWriter")
return
#This function pauses with the yield, waiting on instructions on which printer it needs to print with.
# This function pauses with the yield, waiting on instructions on which printer it needs to print with.
if not writer:
Logger.log("e", "Missing file or mesh writer!")
return
self._sending_job = self._sendPrintJob(writer, preferred_format, nodes)
self._sending_job.send(None) #Start the generator.
if self._sending_job is not None:
self._sending_job.send(None) # Start the generator.
if len(self._printers) > 1: #We need to ask the user.
self._spawnPrinterSelectionDialog()
is_job_sent = True
else: #Just immediately continue.
self._sending_job.send("") #No specifically selected printer.
is_job_sent = self._sending_job.send(None)
if len(self._printers) > 1: # We need to ask the user.
self._spawnPrinterSelectionDialog()
is_job_sent = True
else: # Just immediately continue.
self._sending_job.send("") # No specifically selected printer.
is_job_sent = self._sending_job.send(None)
def _spawnPrinterSelectionDialog(self):
if self._printer_selection_dialog is None:
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "PrintWindow.qml")
path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../resources/qml/PrintWindow.qml")
self._printer_selection_dialog = CuraApplication.getInstance().createQmlComponent(path, {"OutputDevice": self})
if self._printer_selection_dialog is not None:
self._printer_selection_dialog.show()
@pyqtProperty(int, constant=True)
def clusterSize(self):
def clusterSize(self) -> int:
return self._cluster_size
## Allows the user to choose a printer to print with from the printer
@ -165,7 +172,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
# \param target_printer The name of the printer to target.
@pyqtSlot(str)
def selectPrinter(self, target_printer: str = "") -> None:
self._sending_job.send(target_printer)
if self._sending_job is not None:
self._sending_job.send(target_printer)
@pyqtSlot()
def cancelPrintSelection(self) -> None:
@ -214,8 +222,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
job.start()
yield True #Return that we had success!
yield #To prevent having to catch the StopIteration exception.
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:
@ -240,7 +248,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
file_name = CuraApplication.getInstance().getPrintInformation().jobName + "." + preferred_format["extension"]
output = stream.getvalue() #Either str or bytes depending on the output mode.
output = stream.getvalue() # Either str or bytes depending on the output mode.
if isinstance(stream, io.StringIO):
output = cast(str, output).encode("utf-8")
output = cast(bytes, output)
@ -253,6 +261,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
def activePrinter(self) -> Optional[PrinterOutputModel]:
return self._active_printer
@pyqtProperty(QObject, notify=activeCameraChanged)
def activeCamera(self) -> Optional[NetworkCamera]:
return self._active_camera
@pyqtSlot(QObject)
def setActivePrinter(self, printer: Optional[PrinterOutputModel]) -> None:
if self._active_printer != printer:
@ -261,6 +273,19 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
self._active_printer = printer
self.activePrinterChanged.emit()
@pyqtSlot(QObject)
def setActiveCamera(self, camera: Optional[NetworkCamera]) -> None:
if self._active_camera != camera:
if self._active_camera:
self._active_camera.stop()
self._active_camera = camera
if self._active_camera:
self._active_camera.start()
self.activeCameraChanged.emit()
def _onPostPrintJobFinished(self, reply: QNetworkReply) -> None:
if self._progress_message:
self._progress_message.hide()
@ -279,8 +304,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
# If successfully sent:
if bytes_sent == bytes_total:
# Show a confirmation to the user so they know the job was sucessful and provide the option to switch to the
# monitor tab.
# Show a confirmation to the user so they know the job was sucessful and provide the option to switch to
# the monitor tab.
self._success_message = Message(
i18n_catalog.i18nc("@info:status", "Print job was successfully sent to the printer."),
lifetime=5, dismissable=True,
@ -329,7 +354,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
@pyqtProperty("QVariantList", notify = printJobsChanged)
def queuedPrintJobs(self) -> List[PrintJobOutputModel]:
return [print_job for print_job in self._print_jobs if print_job.state == "queued"]
return [print_job for print_job in self._print_jobs if print_job.state == "queued" or print_job.state == "error"]
@pyqtProperty("QVariantList", notify = printJobsChanged)
def activePrintJobs(self) -> List[PrintJobOutputModel]:
@ -348,6 +373,10 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
result.append({"machine_type": machine_type, "count": str(printer_count[machine_type])})
return result
@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)
@ -364,6 +393,19 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
datetime_completed = datetime.fromtimestamp(current_time + time_remaining)
return (datetime_completed.strftime("%a %b ") + "{day}".format(day=datetime_completed.day)).upper()
@pyqtSlot(str)
def sendJobToTop(self, print_job_uuid: str) -> None:
# This function is part of the output device (and not of the printjob output model) as this type of operation
# is a modification of the cluster queue and not of the actual job.
data = "{\"to_position\": 0}"
self.put("print_jobs/{uuid}/move_to_position".format(uuid = print_job_uuid), data, on_finished=None)
@pyqtSlot(str)
def deleteJobFromQueue(self, print_job_uuid: str) -> None:
# This function is part of the output device (and not of the printjob output model) as this type of operation
# is a modification of the cluster queue and not of the actual job.
self.delete("print_jobs/{uuid}".format(uuid = print_job_uuid), on_finished=None)
def _printJobStateChanged(self) -> None:
username = self._getUserName()
@ -392,11 +434,26 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
super().connect()
self.sendMaterialProfiles()
def _onGetPreviewImageFinished(self, reply: QNetworkReply) -> None:
reply_url = reply.url().toString()
uuid = reply_url[reply_url.find("print_jobs/")+len("print_jobs/"):reply_url.rfind("/preview_image")]
print_job = findByKey(self._print_jobs, uuid)
if print_job:
image = QImage()
image.loadFromData(reply.readAll())
print_job.updatePreviewImage(image)
def _update(self) -> None:
super()._update()
self.get("printers/", on_finished = self._onGetPrintersDataFinished)
self.get("print_jobs/", on_finished = self._onGetPrintJobsFinished)
for print_job in self._print_jobs:
if print_job.getPreviewImage() is None:
self.get("print_jobs/{uuid}/preview_image".format(uuid=print_job.key), on_finished=self._onGetPreviewImageFinished)
def _onGetPrintJobsFinished(self, reply: QNetworkReply) -> None:
if not checkValidGetReply(reply):
return
@ -407,16 +464,19 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
print_jobs_seen = []
job_list_changed = False
for print_job_data in result:
for idx, print_job_data in enumerate(result):
print_job = findByKey(self._print_jobs, print_job_data["uuid"])
if print_job is None:
print_job = self._createPrintJobModel(print_job_data)
job_list_changed = True
elif not job_list_changed:
# Check if the order of the jobs has changed since the last check
if self._print_jobs.index(print_job) != idx:
job_list_changed = True
self._updatePrintJob(print_job, print_job_data)
if print_job.state != "queued": # Print job should be assigned to a printer.
if print_job.state != "queued" and print_job.state != "error": # Print job should be assigned to a printer.
if print_job.state in ["failed", "finished", "aborted", "none"]:
# Print job was already completed, so don't attach it to a printer.
printer = None
@ -437,6 +497,8 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
job_list_changed = job_list_changed or self._removeJob(removed_job)
if job_list_changed:
# Override the old list with the new list (either because jobs were removed / added or order changed)
self._print_jobs = print_jobs_seen
self.printJobsChanged.emit() # Do a single emit for all print job changes.
def _onGetPrintersDataFinished(self, reply: QNetworkReply) -> None:
@ -478,16 +540,59 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
def _createPrintJobModel(self, data: Dict[str, Any]) -> PrintJobOutputModel:
print_job = PrintJobOutputModel(output_controller=ClusterUM3PrinterOutputController(self),
key=data["uuid"], name= data["name"])
configuration = ConfigurationModel()
extruders = [ExtruderConfigurationModel(position = idx) for idx in range(0, self._number_of_extruders)]
for index in range(0, self._number_of_extruders):
try:
extruder_data = data["configuration"][index]
except IndexError:
continue
extruder = extruders[int(data["configuration"][index]["extruder_index"])]
extruder.setHotendID(extruder_data.get("print_core_id", ""))
extruder.setMaterial(self._createMaterialOutputModel(extruder_data.get("material", {})))
configuration.setExtruderConfigurations(extruders)
print_job.updateConfiguration(configuration)
print_job.setCompatibleMachineFamilies(data.get("compatible_machine_families", []))
print_job.stateChanged.connect(self._printJobStateChanged)
self._print_jobs.append(print_job)
return print_job
def _updatePrintJob(self, print_job: PrintJobOutputModel, data: Dict[str, Any]) -> None:
print_job.updateTimeTotal(data["time_total"])
print_job.updateTimeElapsed(data["time_elapsed"])
print_job.updateState(data["status"])
impediments_to_printing = data.get("impediments_to_printing", [])
print_job.updateOwner(data["owner"])
status_set_by_impediment = False
for impediment in impediments_to_printing:
if impediment["severity"] == "UNFIXABLE":
status_set_by_impediment = True
print_job.updateState("error")
break
if not status_set_by_impediment:
print_job.updateState(data["status"])
def _createMaterialOutputModel(self, material_data) -> MaterialOutputModel:
containers = ContainerRegistry.getInstance().findInstanceContainers(type="material", GUID=material_data["guid"])
if containers:
color = containers[0].getMetaDataEntry("color_code")
brand = containers[0].getMetaDataEntry("brand")
material_type = containers[0].getMetaDataEntry("material")
name = containers[0].getName()
else:
Logger.log("w",
"Unable to find material with guid {guid}. Using data as provided by cluster".format(
guid=material_data["guid"]))
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)
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.
# Then we suddenly need the unique name. So in order to not have to mess up all the other code, we save a mapping.
@ -523,24 +628,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
material_data = extruder_data["material"]
if extruder.activeMaterial is None or extruder.activeMaterial.guid != material_data["guid"]:
containers = ContainerRegistry.getInstance().findInstanceContainers(type="material",
GUID=material_data["guid"])
if containers:
color = containers[0].getMetaDataEntry("color_code")
brand = containers[0].getMetaDataEntry("brand")
material_type = containers[0].getMetaDataEntry("material")
name = containers[0].getName()
else:
Logger.log("w",
"Unable to find material with guid {guid}. Using data as provided by cluster".format(
guid=material_data["guid"]))
color = material_data["color"]
brand = material_data["brand"]
material_type = material_data["material"]
name = "Empty" if material_data["material"] == "empty" else "Unknown"
material = MaterialOutputModel(guid=material_data["guid"], type=material_type,
brand=brand, color=color, name=name)
material = self._createMaterialOutputModel(material_data)
extruder.updateActiveMaterial(material)
def _removeJob(self, job: PrintJobOutputModel) -> bool:
@ -568,6 +656,7 @@ class ClusterUM3OutputDevice(NetworkedPrinterOutputDevice):
job = SendMaterialJob(device = self)
job.run()
def loadJsonFromReply(reply: QNetworkReply) -> Optional[List[Dict[str, Any]]]:
try:
result = json.loads(bytes(reply.readAll()).decode("utf-8"))
@ -586,8 +675,8 @@ def checkValidGetReply(reply: QNetworkReply) -> bool:
return True
def findByKey(list: List[Union[PrintJobOutputModel, PrinterOutputModel]], key: str) -> Optional[PrintJobOutputModel]:
for item in list:
def findByKey(lst: List[Union[PrintJobOutputModel, PrinterOutputModel]], key: str) -> Optional[PrintJobOutputModel]:
for item in lst:
if item.key == key:
return item
return None

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