Merge branch 'master' into PyQt6_upgrade

Conflicts:
	cura/PlatformPhysics.py -> Removed shapely on master, while QTimer import got updated to Qt6.
	plugins/Toolbox -> Entire folder is deleted in master, but it was updated to Qt6 here. This can all be removed.
This commit is contained in:
Ghostkeeper 2022-02-21 11:52:28 +01:00
commit c7d7dd11d1
No known key found for this signature in database
GPG Key ID: D2A8871EE34EC59A
415 changed files with 43396 additions and 36375 deletions

3
.gitignore vendored
View File

@ -10,6 +10,8 @@ resources/i18n/en_7S
resources/i18n/x-test
resources/firmware
resources/materials
resources/images/whats_new
resources/texts/whats_new
CuraEngine.exe
LC_MESSAGES
.cache
@ -37,6 +39,7 @@ cura.desktop
#Externally located plug-ins commonly installed by our devs.
plugins/cura-big-flame-graph
plugins/cura-camera-position
plugins/cura-god-mode-plugin
plugins/cura-siemensnx-plugin
plugins/CuraBlenderPlugin

View File

@ -40,7 +40,7 @@ def findNodePlacement(nodes_to_arrange: List["SceneNode"], build_volume: "BuildV
machine_width = build_volume.getWidth()
machine_depth = build_volume.getDepth()
build_plate_bounding_box = Box(machine_width * factor, machine_depth * factor)
build_plate_bounding_box = Box(int(machine_width * factor), int(machine_depth * factor))
if fixed_nodes is None:
fixed_nodes = []

View File

@ -72,7 +72,7 @@ class BuildVolume(SceneNode):
self._origin_mesh = None # type: Optional[MeshData]
self._origin_line_length = 20
self._origin_line_width = 1.5
self._origin_line_width = 1
self._enabled = False
self._grid_mesh = None # type: Optional[MeshData]
@ -601,6 +601,7 @@ class BuildVolume(SceneNode):
if self._adhesion_type == "raft":
self._raft_thickness = (
self._global_container_stack.getProperty("raft_base_thickness", "value") +
self._global_container_stack.getProperty("raft_interface_layers", "value") *
self._global_container_stack.getProperty("raft_interface_thickness", "value") +
self._global_container_stack.getProperty("raft_surface_layers", "value") *
self._global_container_stack.getProperty("raft_surface_thickness", "value") +
@ -848,10 +849,10 @@ class BuildVolume(SceneNode):
"""
result = {}
adhesion_extruder = None #type: ExtruderStack
skirt_brim_extruder: ExtruderStack = None
for extruder in used_extruders:
if int(extruder.getProperty("extruder_nr", "value")) == int(self._global_container_stack.getProperty("adhesion_extruder_nr", "value")):
adhesion_extruder = extruder
if int(extruder.getProperty("extruder_nr", "value")) == int(self._global_container_stack.getProperty("skirt_brim_extruder_nr", "value")):
skirt_brim_extruder = extruder
result[extruder.getId()] = []
# Currently, the only normally printed object is the prime tower.
@ -865,11 +866,11 @@ class BuildVolume(SceneNode):
prime_tower_x = prime_tower_x - machine_width / 2 #Offset by half machine_width and _depth to put the origin in the front-left.
prime_tower_y = prime_tower_y + machine_depth / 2
if adhesion_extruder is not None and self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft":
if skirt_brim_extruder is not None and self._global_container_stack.getProperty("prime_tower_brim_enable", "value") and self._global_container_stack.getProperty("adhesion_type", "value") != "raft":
brim_size = (
adhesion_extruder.getProperty("brim_line_count", "value") *
adhesion_extruder.getProperty("skirt_brim_line_width", "value") / 100.0 *
adhesion_extruder.getProperty("initial_layer_line_width_factor", "value")
skirt_brim_extruder.getProperty("brim_line_count", "value") *
skirt_brim_extruder.getProperty("skirt_brim_line_width", "value") / 100.0 *
skirt_brim_extruder.getProperty("initial_layer_line_width_factor", "value")
)
prime_tower_x -= brim_size
prime_tower_y += brim_size
@ -1100,18 +1101,18 @@ class BuildVolume(SceneNode):
# with the adhesion extruder, but it also prints one extra line by all other extruders. As such, the
# setting does *not* have a limit_to_extruder setting (which means that we can't ask the global extruder what
# the value is.
adhesion_extruder = self._global_container_stack.getProperty("adhesion_extruder_nr", "value")
skirt_brim_extruder_nr = self._global_container_stack.getProperty("skirt_brim_extruder_nr", "value")
try:
adhesion_stack = self._global_container_stack.extruderList[int(adhesion_extruder)]
skirt_brim_stack = self._global_container_stack.extruderList[int(skirt_brim_extruder_nr)]
except IndexError:
Logger.warning(f"Couldn't find extruder with index '{adhesion_extruder}', defaulting to 0 instead.")
adhesion_stack = self._global_container_stack.extruderList[0]
skirt_brim_line_width = adhesion_stack.getProperty("skirt_brim_line_width", "value")
Logger.warning(f"Couldn't find extruder with index '{skirt_brim_extruder_nr}', defaulting to 0 instead.")
skirt_brim_stack = self._global_container_stack.extruderList[0]
skirt_brim_line_width = skirt_brim_stack.getProperty("skirt_brim_line_width", "value")
initial_layer_line_width_factor = adhesion_stack.getProperty("initial_layer_line_width_factor", "value")
initial_layer_line_width_factor = skirt_brim_stack.getProperty("initial_layer_line_width_factor", "value")
# Use brim width if brim is enabled OR the prime tower has a brim.
if adhesion_type == "brim":
brim_line_count = self._global_container_stack.getProperty("brim_line_count", "value")
brim_line_count = skirt_brim_stack.getProperty("brim_line_count", "value")
bed_adhesion_size = skirt_brim_line_width * brim_line_count * initial_layer_line_width_factor / 100.0
for extruder_stack in used_extruders:
@ -1120,8 +1121,8 @@ class BuildVolume(SceneNode):
# We don't create an additional line for the extruder we're printing the brim with.
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
elif adhesion_type == "skirt":
skirt_distance = self._global_container_stack.getProperty("skirt_gap", "value")
skirt_line_count = self._global_container_stack.getProperty("skirt_line_count", "value")
skirt_distance = skirt_brim_stack.getProperty("skirt_gap", "value")
skirt_line_count = skirt_brim_stack.getProperty("skirt_line_count", "value")
bed_adhesion_size = skirt_distance + (
skirt_brim_line_width * skirt_line_count) * initial_layer_line_width_factor / 100.0
@ -1132,7 +1133,7 @@ class BuildVolume(SceneNode):
# We don't create an additional line for the extruder we're printing the skirt with.
bed_adhesion_size -= skirt_brim_line_width * initial_layer_line_width_factor / 100.0
elif adhesion_type == "raft":
bed_adhesion_size = self._global_container_stack.getProperty("raft_margin", "value")
bed_adhesion_size = self._global_container_stack.getProperty("raft_margin", "value") # Should refer to the raft extruder if set.
elif adhesion_type == "none":
bed_adhesion_size = 0
else:
@ -1214,13 +1215,13 @@ class BuildVolume(SceneNode):
_machine_settings = ["machine_width", "machine_depth", "machine_height", "machine_shape", "machine_center_is_zero"]
_skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "initial_layer_line_width_factor"]
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_layers", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
_extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "prime_blob_enable"]
_tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable"]
_ooze_shield_settings = ["ooze_shield_enabled", "ooze_shield_dist"]
_distance_settings = ["infill_wipe_dist", "travel_avoid_distance", "support_offset", "support_enable", "travel_avoid_other_parts", "travel_avoid_supports", "wall_line_count", "wall_line_width_0", "wall_line_width_x"]
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "adhesion_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
_limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "adhesion_extruder_nr"]
_extruder_settings = ["support_enable", "support_bottom_enable", "support_roof_enable", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "brim_line_count", "skirt_brim_extruder_nr", "raft_base_extruder_nr", "raft_interface_extruder_nr", "raft_surface_extruder_nr", "adhesion_type"] #Settings that can affect which extruders are used.
_limit_to_extruder_settings = ["wall_extruder_nr", "wall_0_extruder_nr", "wall_x_extruder_nr", "top_bottom_extruder_nr", "infill_extruder_nr", "support_infill_extruder_nr", "support_extruder_nr_layer_0", "support_bottom_extruder_nr", "support_roof_extruder_nr", "skirt_brim_extruder_nr", "raft_base_extruder_nr", "raft_interface_extruder_nr", "raft_surface_extruder_nr"]
_material_size_settings = ["material_shrinkage_percentage", "material_shrinkage_percentage_xy", "material_shrinkage_percentage_z"]
_disallowed_area_settings = _skirt_settings + _prime_settings + _tower_settings + _ooze_shield_settings + _distance_settings + _extruder_settings + _material_size_settings

View File

@ -15,7 +15,7 @@ from typing import cast, Any
try:
from sentry_sdk.hub import Hub
from sentry_sdk.utils import event_from_exception
from sentry_sdk import configure_scope
from sentry_sdk import configure_scope, add_breadcrumb
with_sentry_sdk = True
except ImportError:
with_sentry_sdk = False
@ -424,6 +424,13 @@ class CrashHandler:
if with_sentry_sdk:
try:
hub = Hub.current
if not Logger.getLoggers():
# No loggers have been loaded yet, so we don't have any breadcrumbs :(
# So add them manually so we at least have some info...
add_breadcrumb(level = "info", message = "SentryLogging was not initialised yet")
for log_type, line in Logger.getUnloggedLines():
add_breadcrumb(message=line)
event, hint = event_from_exception((self.exception_type, self.value, self.traceback))
hub.capture_event(event, hint=hint)
hub.flush()

View File

@ -494,7 +494,7 @@ class CuraApplication(QtApplication):
"CuraEngineBackend", #Cura is useless without this one since you can't slice.
"FileLogger", #You want to be able to read the log if something goes wrong.
"XmlMaterialProfile", #Cura crashes without this one.
"Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
"Marketplace", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
"PrepareStage", #Cura is useless without this one since you can't load models.
"PreviewStage", #This shows the list of the plugin views that are installed in Cura.
"MonitorStage", #Major part of Cura's functionality.
@ -573,6 +573,10 @@ class CuraApplication(QtApplication):
preferences.addPreference("general/accepted_user_agreement", False)
preferences.addPreference("cura/market_place_show_plugin_banner", True)
preferences.addPreference("cura/market_place_show_material_banner", True)
preferences.addPreference("cura/market_place_show_manage_packages_banner", True)
for key in [
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
"dialog_profile_path",
@ -777,10 +781,14 @@ class CuraApplication(QtApplication):
lib_suffixes = {""}
for suffix in lib_suffixes:
self._plugin_registry.addPluginLocation(os.path.join(QtApplication.getInstallPrefix(), "lib" + suffix, "cura"))
if not hasattr(sys, "frozen"):
self._plugin_registry.addPluginLocation(os.path.join(os.path.abspath(os.path.dirname(__file__)), "..", "plugins"))
self._plugin_registry.preloaded_plugins.append("ConsoleLogger")
# Since it's possible to get crashes in code before the sentrylogger is loaded, we want to start this plugin
# as quickly as possible, as we might get unsolvable crash reports without it.
self._plugin_registry.preloaded_plugins.append("SentryLogger")
self._plugin_registry.loadPlugins()
if self.getBackend() is None:

View File

@ -1,13 +1,15 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import List, Tuple, TYPE_CHECKING, Optional
from typing import Any, cast, Dict, List, Set, Tuple, TYPE_CHECKING, Optional
from cura.CuraApplication import CuraApplication #To find some resource types.
from cura.CuraApplication import CuraApplication # To find some resource types.
from cura.Settings.GlobalStack import GlobalStack
from UM.PackageManager import PackageManager #The class we're extending.
from UM.Resources import Resources #To find storage paths for some resource types.
from UM.PackageManager import PackageManager # The class we're extending.
from UM.Resources import Resources # To find storage paths for some resource types.
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
if TYPE_CHECKING:
from UM.Qt.QtApplication import QtApplication
@ -17,6 +19,31 @@ if TYPE_CHECKING:
class CuraPackageManager(PackageManager):
def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None:
super().__init__(application, parent)
self._local_packages: Optional[List[Dict[str, Any]]] = None
self._local_packages_ids: Optional[Set[str]] = None
self.installedPackagesChanged.connect(self._updateLocalPackages)
def _updateLocalPackages(self) -> None:
self._local_packages = self.getAllLocalPackages()
self._local_packages_ids = set(pkg["package_id"] for pkg in self._local_packages)
@property
def local_packages(self) -> List[Dict[str, Any]]:
"""locally installed packages, lazy execution"""
if self._local_packages is None:
self._updateLocalPackages()
# _updateLocalPackages always results in a list of packages, not None.
# It's guaranteed to be a list now.
return cast(List[Dict[str, Any]], self._local_packages)
@property
def local_packages_ids(self) -> Set[str]:
"""locally installed packages, lazy execution"""
if self._local_packages_ids is None:
self._updateLocalPackages()
# _updateLocalPackages always results in a list of packages, not None.
# It's guaranteed to be a list now.
return cast(Set[str], self._local_packages_ids)
def initialize(self) -> None:
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
@ -47,3 +74,12 @@ class CuraPackageManager(PackageManager):
machine_with_qualities.append((global_stack, str(extruder_nr), container_id))
return machine_with_materials, machine_with_qualities
def getAllLocalPackages(self) -> List[Dict[str, Any]]:
""" Returns an unordered list of all the package_info of installed, to be installed, or bundled packages"""
packages: List[Dict[str, Any]] = []
for packages_to_add in self.getAllInstalledPackagesInfo().values():
packages.extend(packages_to_add)
return packages

View File

@ -60,7 +60,7 @@ class MaterialManagementModel(QObject):
sync_materials_message.addAction(
"sync",
name = catalog.i18nc("@action:button", "Sync materials with printers"),
name = catalog.i18nc("@action:button", "Sync materials"),
icon = "",
description = "Sync your newly installed materials with your printers.",
button_align = Message.ActionButtonAlignment.ALIGN_RIGHT

View File

@ -1,8 +1,7 @@
# Copyright (c) 2020 Ultimaker B.V.
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt6.QtCore import QTimer
from shapely.errors import TopologicalError # To capture errors if Shapely messes up.
from UM.Application import Application
from UM.Logger import Logger
@ -138,11 +137,7 @@ class PlatformPhysics:
own_convex_hull = node.callDecoration("getConvexHull")
other_convex_hull = other_node.callDecoration("getConvexHull")
if own_convex_hull and other_convex_hull:
try:
overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
except TopologicalError as e: # Can happen if the convex hull is degenerate?
Logger.warning("Got a topological error when calculating convex hull intersection: {err}".format(err = str(e)))
overlap = False
overlap = own_convex_hull.translate(move_vector.x, move_vector.z).intersectsPolygon(other_convex_hull)
if overlap: # Moving ensured that overlap was still there. Try anew!
temp_move_vector = move_vector.set(x = move_vector.x + overlap[0] * self._move_factor,
z = move_vector.z + overlap[1] * self._move_factor)

View File

@ -259,11 +259,21 @@ class ExtruderManager(QObject):
if support_roof_enabled:
used_extruder_stack_ids.add(self.extruderIds[self.extruderValueWithDefault(str(global_stack.getProperty("support_roof_extruder_nr", "value")))])
# The platform adhesion extruder. Not used if using none.
if global_stack.getProperty("adhesion_type", "value") != "none" or (
global_stack.getProperty("prime_tower_brim_enable", "value") and
global_stack.getProperty("adhesion_type", "value") != 'raft'):
extruder_str_nr = str(global_stack.getProperty("adhesion_extruder_nr", "value"))
# The platform adhesion extruders.
used_adhesion_extruders = set()
adhesion_type = global_stack.getProperty("adhesion_type", "value")
if adhesion_type == "skirt" and (global_stack.getProperty("skirt_line_count", "value") > 0 or global_stack.getProperty("skirt_brim_minimal_length", "value") > 0):
used_adhesion_extruders.add("skirt_brim_extruder_nr") # There's a skirt.
if (adhesion_type == "brim" or global_stack.getProperty("prime_tower_brim_enable", "value")) and (global_stack.getProperty("brim_line_count", "value") > 0 or global_stack.getProperty("skirt_brim_minimal_length", "value") > 0):
used_adhesion_extruders.add("skirt_brim_extruder_nr") # There's a brim or prime tower brim.
if adhesion_type == "raft":
used_adhesion_extruders.add("raft_base_extruder_nr")
if global_stack.getProperty("raft_interface_layers", "value") > 0:
used_adhesion_extruders.add("raft_interface_extruder_nr")
if global_stack.getProperty("raft_surface_layers", "value") > 0:
used_adhesion_extruders.add("raft_surface_extruder_nr")
for extruder_setting in used_adhesion_extruders:
extruder_str_nr = str(global_stack.getProperty(extruder_setting, "value"))
if extruder_str_nr == "-1":
extruder_str_nr = self._application.getMachineManager().defaultExtruderPosition
if extruder_str_nr in self.extruderIds:
@ -286,8 +296,11 @@ class ExtruderManager(QObject):
global_stack = application.getGlobalContainerStack()
# Starts with the adhesion extruder.
if global_stack.getProperty("adhesion_type", "value") != "none":
return global_stack.getProperty("adhesion_extruder_nr", "value")
adhesion_type = global_stack.getProperty("adhesion_type", "value")
if adhesion_type in {"skirt", "brim"}:
return global_stack.getProperty("skirt_brim_extruder_nr", "value")
if adhesion_type == "raft":
return global_stack.getProperty("raft_base_extruder_nr", "value")
# No adhesion? Well maybe there is still support brim.
if (global_stack.getProperty("support_enable", "value") or global_stack.getProperty("support_structure", "value") == "tree") and global_stack.getProperty("support_brim_enable", "value"):
@ -422,7 +435,10 @@ class ExtruderManager(QObject):
Logger.log("w", "Could not find the variant %s", active_variant_name)
return True
active_variant_node = machine_node.variants[active_variant_name]
active_material_node = active_variant_node.materials[extruder_stack.material.getMetaDataEntry("base_file")]
try:
active_material_node = active_variant_node.materials[extruder_stack.material.getMetaDataEntry("base_file")]
except KeyError: # The material in this stack is not a supported material (e.g. wrong filament diameter, as loaded from a project file).
return False
active_material_node_qualities = active_material_node.qualities
if not active_material_node_qualities:

View File

@ -71,7 +71,7 @@ class CloudMaterialSync(QObject):
sync_materials_message.addAction(
"sync",
name = catalog.i18nc("@action:button", "Sync materials with printers"),
name = catalog.i18nc("@action:button", "Sync materials"),
icon = "",
description = "Sync your newly installed materials with your printers.",
button_align = Message.ActionButtonAlignment.ALIGN_RIGHT

View File

@ -1,6 +1,6 @@
#!/usr/bin/env python3
# Copyright (c) 2020 Ultimaker B.V.
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
# Remove the working directory from sys.path.
@ -15,6 +15,9 @@ if "" in sys.path:
import argparse
import faulthandler
import os
if sys.platform != "linux": # Turns out the Linux build _does_ use this, but we're not making an Enterprise release for that system anyway.
os.environ["QT_PLUGIN_PATH"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
os.environ["QML2_IMPORT_PATH"] = "" # Security workaround: Don't need it, and introduces an attack vector, so set to nul.
from PyQt6.QtNetwork import QSslConfiguration, QSslSocket

View File

@ -7,9 +7,9 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
PROJECT_DIR="$( cd "${SCRIPT_DIR}/.." && pwd )"
# Make sure that environment variables are set properly
source /opt/rh/devtoolset-8/enable
export PATH="${CURA_BUILD_ENV_PATH}/bin:${PATH}"
export PKG_CONFIG_PATH="${CURA_BUILD_ENV_PATH}/lib/pkgconfig:${PKG_CONFIG_PATH}"
export LD_LIBRARY_PATH="${CURA_BUILD_ENV_PATH}/lib:${LD_LIBRARY_PATH}"
cd "${PROJECT_DIR}"
@ -60,7 +60,7 @@ export PYTHONPATH="${PROJECT_DIR}/Uranium:.:${PYTHONPATH}"
mkdir build
cd build
cmake3 \
cmake \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_PREFIX_PATH="${CURA_BUILD_ENV_PATH}" \
-DURANIUM_DIR="${PROJECT_DIR}/Uranium" \

View File

@ -1,3 +1,3 @@
#!/usr/bin/env bash
cd build
ctest3 -j4 --output-on-failure -T Test
ctest -j4 --output-on-failure -T Test

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 361 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

@ -5,7 +5,7 @@ import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Layouts 1.3
import UM 1.3 as UM
import UM 1.5 as UM
import Cura 1.0 as Cura
import "../components"
@ -35,7 +35,7 @@ RowLayout
busy: CuraDrive.isCreatingBackup
}
Cura.CheckBoxWithTooltip
UM.CheckBox
{
id: autoBackupEnabled
checked: CuraDrive.autoBackupEnabled

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View File

@ -23,7 +23,7 @@ Column
{
id: profileImage
fillMode: Image.PreserveAspectFit
source: "../images/icon.png"
source: "../images/backup.svg"
anchors.horizontalCenter: parent.horizontalCenter
width: Math.round(parent.width / 4)
}

View File

@ -44,7 +44,7 @@ Cura.RoundedRectangle
{
id: projectImage
anchors.verticalCenter: parent.verticalCenter
width: UM.Theme.getSize("toolbox_thumbnail_small").width
width: UM.Theme.getSize("card_icon").width
height: Math.round(width * 3/4)
sourceSize.width: width
sourceSize.height: height

View File

@ -6,7 +6,7 @@ import QtQuick.Controls 1.4 as OldControls // TableView doesn't exist in the QtQ
import QtQuick.Controls 2.3
import QtQuick.Controls.Styles 1.4
import UM 1.2 as UM
import UM 1.5 as UM
import Cura 1.6 as Cura
import DigitalFactory 1.0 as DF
@ -228,7 +228,7 @@ Item
width: childrenRect.width
spacing: UM.Theme.getSize("default_margin").width
Cura.CheckBox
UM.CheckBox
{
id: asProjectCheckbox
height: UM.Theme.getSize("checkbox").height
@ -238,7 +238,7 @@ Item
font: UM.Theme.getFont("medium")
}
Cura.CheckBox
UM.CheckBox
{
id: asSlicedCheckbox
height: UM.Theme.getSize("checkbox").height

View File

@ -1,4 +1,4 @@
// Copyright (C) 2021 Ultimaker B.V.
// Copyright (C) 2022 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
@ -9,7 +9,7 @@ import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.1
import UM 1.2 as UM
import Cura 1.6 as Cura
import Cura 1.7 as Cura
import DigitalFactory 1.0 as DF
@ -44,32 +44,13 @@ Item
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
Cura.TextField
Cura.SearchBar
{
id: searchBar
Layout.fillWidth: true
implicitHeight: createNewProjectButton.height
leftPadding: searchIcon.width + UM.Theme.getSize("default_margin").width * 2
focus: true
onTextEdited: manager.projectFilter = text //Update the search filter when editing this text field.
placeholderText: "Search"
UM.RecolorImage
{
id: searchIcon
anchors
{
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
}
source: UM.Theme.getIcon("search")
height: UM.Theme.getSize("small_button_icon").height
width: height
color: UM.Theme.getColor("text")
}
}
Cura.SecondaryButton
@ -221,7 +202,7 @@ Item
LoadMoreProjectsCard
{
id: loadMoreProjectsCard
height: UM.Theme.getSize("toolbox_thumbnail_small").height
height: UM.Theme.getSize("card_icon").height
width: parent.width
visible: manager.digitalFactoryProjectModel.count > 0
hasMoreProjectsToLoad: manager.hasMoreProjectsToLoad

View File

@ -12,7 +12,7 @@ from urllib.error import URLError
from typing import Dict
import ssl
import certifi
import certifi # type: ignore
from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage

View File

@ -24,7 +24,7 @@ from cura.Settings.ExtruderManager import ExtruderManager
catalog = i18nCatalog("cura")
PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optional[float]), ("z", Optional[float]), ("f", Optional[float]), ("e", Optional[float])])
PositionOptional = NamedTuple("PositionOptional", [("x", Optional[float]), ("y", Optional[float]), ("z", Optional[float]), ("f", Optional[float]), ("e", Optional[float])])
Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])])

View File

@ -0,0 +1,12 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from cura.UltimakerCloud import UltimakerCloudConstants
from cura.ApplicationMetadata import CuraSDKVersion
ROOT_URL = f"{UltimakerCloudConstants.CuraCloudAPIRoot}/cura-packages/v{UltimakerCloudConstants.CuraCloudAPIVersion}"
ROOT_CURA_URL = f"{ROOT_URL}/cura/v{CuraSDKVersion}" # Root of all Marketplace API requests.
ROOT_USER_URL = f"{ROOT_URL}/user"
PACKAGES_URL = f"{ROOT_CURA_URL}/packages" # URL to use for requesting the list of packages.
PACKAGE_UPDATES_URL = f"{PACKAGES_URL}/package-updates" # URL to use for requesting the list of packages that can be updated.
USER_PACKAGES_URL = f"{ROOT_USER_URL}/packages"

View File

@ -0,0 +1,126 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from typing import Any, Dict, List, Optional, TYPE_CHECKING
from PyQt5.QtCore import pyqtSlot, QObject
from UM.Version import Version
from UM.i18n import i18nCatalog
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from UM.Logger import Logger
from .PackageList import PackageList
from .PackageModel import PackageModel
from .Constants import PACKAGE_UPDATES_URL
if TYPE_CHECKING:
from PyQt5.QtCore import QObject
from PyQt5.QtNetwork import QNetworkReply
catalog = i18nCatalog("cura")
class LocalPackageList(PackageList):
PACKAGE_CATEGORIES = {
"installed":
{
"plugin": catalog.i18nc("@label", "Installed Plugins"),
"material": catalog.i18nc("@label", "Installed Materials")
},
"bundled":
{
"plugin": catalog.i18nc("@label", "Bundled Plugins"),
"material": catalog.i18nc("@label", "Bundled Materials")
}
} # The section headers to be used for the different package categories
def __init__(self, parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._has_footer = False
self._ongoing_requests["check_updates"] = None
self._package_manager.packagesWithUpdateChanged.connect(self._sortSectionsOnUpdate)
self._package_manager.packageUninstalled.connect(self._removePackageModel)
def _sortSectionsOnUpdate(self) -> None:
section_order = dict(zip([i for k, v in self.PACKAGE_CATEGORIES.items() for i in self.PACKAGE_CATEGORIES[k].values()], ["a", "b", "c", "d"]))
self.sort(lambda model: (section_order[model.sectionTitle], model.canUpdate, model.displayName.lower()), key = "package")
def _removePackageModel(self, package_id: str) -> None:
"""
Cleanup function to remove the package model from the list. Note that this is only done if the package can't
be updated, it is in the to remove list and isn't in the to be installed list
"""
package = self.getPackageModel(package_id)
if package and not package.canUpdate and \
package_id in self._package_manager.getToRemovePackageIDs() and \
package_id not in self._package_manager.getPackagesToInstall():
index = self.find("package", package_id)
if index < 0:
Logger.error(f"Could not find card in Listview corresponding with {package_id}")
self.updatePackages()
return
self.removeItem(index)
@pyqtSlot()
def updatePackages(self) -> None:
"""Update the list with local packages, these are materials or plugin, either bundled or user installed. The list
will also contain **to be removed** or **to be installed** packages since the user might still want to interact
with these.
"""
self.setErrorMessage("") # Clear any previous errors.
self.setIsLoading(True)
# Obtain and sort the local packages
self.setItems([{"package": p} for p in [self._makePackageModel(p) for p in self._package_manager.local_packages]])
self._sortSectionsOnUpdate()
self.checkForUpdates(self._package_manager.local_packages)
self.setIsLoading(False)
self.setHasMore(False) # All packages should have been loaded at this time
def _makePackageModel(self, package_info: Dict[str, Any]) -> PackageModel:
""" Create a PackageModel from the package_info and determine its section_title"""
package_id = package_info["package_id"]
bundled_or_installed = "bundled" if self._package_manager.isBundledPackage(package_id) else "installed"
package_type = package_info["package_type"]
section_title = self.PACKAGE_CATEGORIES[bundled_or_installed][package_type]
package = PackageModel(package_info, section_title = section_title, parent = self)
self._connectManageButtonSignals(package)
return package
def checkForUpdates(self, packages: List[Dict[str, Any]]) -> None:
installed_packages = "&".join([f"installed_packages={package['package_id']}:{package['package_version']}" for package in packages])
request_url = f"{PACKAGE_UPDATES_URL}?{installed_packages}"
self._ongoing_requests["check_updates"] = HttpRequestManager.getInstance().get(
request_url,
scope = self._scope,
callback = self._parseResponse
)
def _parseResponse(self, reply: "QNetworkReply") -> None:
"""
Parse the response from the package list API request which can update.
:param reply: A reply containing information about a number of packages.
"""
response_data = HttpRequestManager.readJSON(reply)
if "data" not in response_data:
Logger.error(
f"Could not interpret the server's response. Missing 'data' from response data. Keys in response: {response_data.keys()}")
return
if len(response_data["data"]) == 0:
return
packages = response_data["data"]
for package in packages:
self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"]))
package_model = self.getPackageModel(package["package_id"])
if package_model:
# Also make sure that the local list knows where to get an update
package_model.setDownloadUrl(package["download_url"])
self._ongoing_requests["check_updates"] = None

View File

@ -0,0 +1,114 @@
# Copyright (c) 2022 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os.path
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, QObject
from typing import Optional, cast
from cura.CuraApplication import CuraApplication # Creating QML objects and managing packages.
from UM.Extension import Extension # We are implementing the main object of an extension here.
from UM.PluginRegistry import PluginRegistry # To find out where we are stored (the proper way).
from .RemotePackageList import RemotePackageList # To register this type with QML.
from .LocalPackageList import LocalPackageList # To register this type with QML.
class Marketplace(Extension, QObject):
"""
The main managing object for the Marketplace plug-in.
"""
def __init__(self, parent: Optional[QObject] = None) -> None:
QObject.__init__(self, parent)
Extension.__init__(self)
self._window: Optional["QObject"] = None # If the window has been loaded yet, it'll be cached in here.
self._plugin_registry: Optional[PluginRegistry] = None
self._package_manager = CuraApplication.getInstance().getPackageManager()
self._material_package_list: Optional[RemotePackageList] = None
self._plugin_package_list: Optional[RemotePackageList] = None
# Not entirely the cleanest code, since the localPackage list also checks the server if there are updates
# Since that in turn will trigger notifications to be shown, we do need to construct it here and make sure
# that it checks for updates...
self._local_package_list = LocalPackageList(self)
self._local_package_list.checkForUpdates(self._package_manager.local_packages)
self._package_manager.installedPackagesChanged.connect(self.checkIfRestartNeeded)
self._tab_shown: int = 0
self._restart_needed = False
def getTabShown(self) -> int:
return self._tab_shown
def setTabShown(self, tab_shown: int) -> None:
if tab_shown != self._tab_shown:
self._tab_shown = tab_shown
self.tabShownChanged.emit()
tabShownChanged = pyqtSignal()
tabShown = pyqtProperty(int, fget=getTabShown, fset=setTabShown, notify=tabShownChanged)
@pyqtProperty(QObject, constant=True)
def MaterialPackageList(self):
if self._material_package_list is None:
self._material_package_list = RemotePackageList()
self._material_package_list.packageTypeFilter = "material"
return self._material_package_list
@pyqtProperty(QObject, constant=True)
def PluginPackageList(self):
if self._plugin_package_list is None:
self._plugin_package_list = RemotePackageList()
self._plugin_package_list.packageTypeFilter = "plugin"
return self._plugin_package_list
@pyqtProperty(QObject, constant=True)
def LocalPackageList(self):
return self._local_package_list
@pyqtSlot()
def show(self) -> None:
"""
Opens the window of the Marketplace.
If the window hadn't been loaded yet into Qt, it will be created lazily.
"""
if self._window is None:
self._plugin_registry = PluginRegistry.getInstance()
self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.checkIfRestartNeeded)
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
if plugin_path is None:
plugin_path = os.path.dirname(__file__)
path = os.path.join(plugin_path, "resources", "qml", "Marketplace.qml")
self._window = CuraApplication.getInstance().createQmlComponent(path, {"manager": self})
if self._window is None: # Still None? Failed to load the QML then.
return
if not self._window.isVisible():
self.setTabShown(0)
self._window.show()
self._window.requestActivate() # Bring window into focus, if it was already open in the background.
@pyqtSlot()
def setVisibleTabToMaterials(self) -> None:
"""
Set the tab shown to the remote materials one.
Not implemented in a more generic way because it needs the ability to be called with 'callExtensionMethod'.
"""
self.setTabShown(1)
def checkIfRestartNeeded(self) -> None:
if self._package_manager.hasPackagesToRemoveOrInstall or \
cast(PluginRegistry, self._plugin_registry).getCurrentSessionActivationChangedPlugins():
self._restart_needed = True
else:
self._restart_needed = False
self.showRestartNotificationChanged.emit()
showRestartNotificationChanged = pyqtSignal()
@pyqtProperty(bool, notify=showRestartNotificationChanged)
def showRestartNotification(self) -> bool:
return self._restart_needed

View File

@ -0,0 +1,305 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import tempfile
import json
import os.path
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot, Qt
from typing import cast, Dict, Optional, Set, TYPE_CHECKING
from UM.i18n import i18nCatalog
from UM.Qt.ListModel import ListModel
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from UM.TaskManagement.HttpRequestManager import HttpRequestData, HttpRequestManager
from UM.Logger import Logger
from UM import PluginRegistry
from cura.CuraApplication import CuraApplication
from cura.CuraPackageManager import CuraPackageManager
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope # To make requests to the Ultimaker API with correct authorization.
from .PackageModel import PackageModel
from .Constants import USER_PACKAGES_URL, PACKAGES_URL
if TYPE_CHECKING:
from PyQt5.QtCore import QObject
from PyQt5.QtNetwork import QNetworkReply
catalog = i18nCatalog("cura")
class PackageList(ListModel):
""" A List model for Packages, this class serves as parent class for more detailed implementations.
such as Packages obtained from Remote or Local source
"""
PackageRole = Qt.UserRole + 1
DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB
def __init__(self, parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
self._account = CuraApplication.getInstance().getCuraAPI().account
self._error_message = ""
self.addRoleName(self.PackageRole, "package")
self._is_loading = False
self._has_more = False
self._has_footer = True
self._to_install: Dict[str, str] = {}
self._ongoing_requests: Dict[str, Optional[HttpRequestData]] = {"download_package": None}
self._scope = JsonDecoratorScope(UltimakerCloudScope(CuraApplication.getInstance()))
self._license_dialogs: Dict[str, QObject] = {}
def __del__(self) -> None:
""" When this object is deleted it will loop through all registered API requests and aborts them """
try:
self.isLoadingChanged.disconnect()
self.hasMoreChanged.disconnect()
except RuntimeError:
pass
self.cleanUpAPIRequest()
def abortRequest(self, request_id: str) -> None:
"""Aborts a single request"""
if request_id in self._ongoing_requests and self._ongoing_requests[request_id]:
HttpRequestManager.getInstance().abortRequest(self._ongoing_requests[request_id])
self._ongoing_requests[request_id] = None
@pyqtSlot()
def cleanUpAPIRequest(self) -> None:
for request_id in self._ongoing_requests:
self.abortRequest(request_id)
@pyqtSlot()
def updatePackages(self) -> None:
""" A Qt slot which will update the List from a source. Actual implementation should be done in the child class"""
pass
def reset(self) -> None:
""" Resets and clears the list"""
self.clear()
isLoadingChanged = pyqtSignal()
def setIsLoading(self, value: bool) -> None:
if self._is_loading != value:
self._is_loading = value
self.isLoadingChanged.emit()
@pyqtProperty(bool, fset = setIsLoading, notify = isLoadingChanged)
def isLoading(self) -> bool:
""" Indicating if the the packages are loading
:return" ``True`` if the list is being obtained, otherwise ``False``
"""
return self._is_loading
hasMoreChanged = pyqtSignal()
def setHasMore(self, value: bool) -> None:
if self._has_more != value:
self._has_more = value
self.hasMoreChanged.emit()
@pyqtProperty(bool, fset = setHasMore, notify = hasMoreChanged)
def hasMore(self) -> bool:
""" Indicating if there are more packages available to load.
:return: ``True`` if there are more packages to load, or ``False``.
"""
return self._has_more
errorMessageChanged = pyqtSignal()
def setErrorMessage(self, error_message: str) -> None:
if self._error_message != error_message:
self._error_message = error_message
self.errorMessageChanged.emit()
@pyqtProperty(str, notify = errorMessageChanged, fset = setErrorMessage)
def errorMessage(self) -> str:
""" If an error occurred getting the list of packages, an error message will be held here.
If no error occurred (yet), this will be an empty string.
:return: An error message, if any, or an empty string if everything went okay.
"""
return self._error_message
@pyqtProperty(bool, constant = True)
def hasFooter(self) -> bool:
""" Indicating if the PackageList should have a Footer visible. For paginated PackageLists
:return: ``True`` if a Footer should be displayed in the ListView, e.q.: paginated lists, ``False`` Otherwise"""
return self._has_footer
def getPackageModel(self, package_id: str) -> Optional[PackageModel]:
index = self.find("package", package_id)
data = self.getItem(index)
if data:
return data.get("package")
return None
def _openLicenseDialog(self, package_id: str, license_content: str) -> None:
plugin_path = self._plugin_registry.getPluginPath("Marketplace")
if plugin_path is None:
plugin_path = os.path.dirname(__file__)
# create a QML component for the license dialog
license_dialog_component_path = os.path.join(plugin_path, "resources", "qml", "LicenseDialog.qml")
dialog = CuraApplication.getInstance().createQmlComponent(license_dialog_component_path, {
"licenseContent": license_content,
"packageId": package_id,
"handler": self
})
dialog.show()
# place dialog in class such that it does not get remove by garbage collector
self._license_dialogs[package_id] = dialog
@pyqtSlot(str)
def onLicenseAccepted(self, package_id: str) -> None:
# close dialog
dialog = self._license_dialogs.pop(package_id)
if dialog is not None:
dialog.deleteLater()
# install relevant package
self._install(package_id)
@pyqtSlot(str)
def onLicenseDeclined(self, package_id: str) -> None:
# close dialog
dialog = self._license_dialogs.pop(package_id)
if dialog is not None:
dialog.deleteLater()
# reset package card
self._package_manager.packageInstallingFailed.emit(package_id)
def _requestInstall(self, package_id: str, update: bool = False) -> None:
package_path = self._to_install[package_id]
license_content = self._package_manager.getPackageLicense(package_path)
if not update and license_content is not None and license_content != "":
# If installation is not and update, and the packages contains a license then
# open dialog, prompting the using to accept the plugin license
self._openLicenseDialog(package_id, license_content)
else:
# Otherwise continue the installation
self._install(package_id, update)
def _install(self, package_id: str, update: bool = False) -> None:
package_path = self._to_install.pop(package_id)
to_be_installed = self._package_manager.installPackage(package_path) is not None
if not to_be_installed:
Logger.warning(f"Could not install {package_id}")
return
package = self.getPackageModel(package_id)
if package:
self.subscribeUserToPackage(package_id, str(package.sdk_version))
else:
Logger.log("w", f"Unable to get data on package {package_id}")
def download(self, package_id: str, url: str, update: bool = False) -> None:
"""Initiate the download request
:param package_id: the package identification string
:param url: the URL from which the package needs to be obtained
:param update: A flag if this is download request is an update process
"""
if url == "":
url = f"{PACKAGES_URL}/{package_id}/download"
def downloadFinished(reply: "QNetworkReply") -> None:
self._downloadFinished(package_id, reply, update)
def downloadError(reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None:
self._downloadError(package_id, update, reply, error)
self._ongoing_requests["download_package"] = HttpRequestManager.getInstance().get(
url,
scope = self._scope,
callback = downloadFinished,
error_callback = downloadError
)
def _downloadFinished(self, package_id: str, reply: "QNetworkReply", update: bool = False) -> None:
with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file:
try:
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
while bytes_read:
temp_file.write(bytes_read)
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
except IOError as e:
Logger.error(f"Failed to write downloaded package to temp file {e}")
temp_file.close()
self._downloadError(package_id, update)
except RuntimeError:
# Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling
# between de-/constructing Remote or Local PackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object
# was deleted when it was still parsing the response
temp_file.close()
return
temp_file.close()
self._to_install[package_id] = temp_file.name
self._ongoing_requests["download_package"] = None
self._requestInstall(package_id, update)
def _downloadError(self, package_id: str, update: bool = False, reply: Optional["QNetworkReply"] = None, error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if reply:
reply_string = bytes(reply.readAll()).decode()
Logger.error(f"Failed to download package: {package_id} due to {reply_string}")
self._package_manager.packageInstallingFailed.emit(package_id)
def subscribeUserToPackage(self, package_id: str, sdk_version: str) -> None:
"""Subscribe the user (if logged in) to the package for a given SDK
:param package_id: the package identification string
:param sdk_version: the SDK version
"""
if self._account.isLoggedIn:
HttpRequestManager.getInstance().put(
url = USER_PACKAGES_URL,
data = json.dumps({"data": {"package_id": package_id, "sdk_version": sdk_version}}).encode(),
scope = self._scope
)
def unsunscribeUserFromPackage(self, package_id: str) -> None:
"""Unsubscribe the user (if logged in) from the package
:param package_id: the package identification string
"""
if self._account.isLoggedIn:
HttpRequestManager.getInstance().delete(url = f"{USER_PACKAGES_URL}/{package_id}", scope = self._scope)
# --- Handle the manage package buttons ---
def _connectManageButtonSignals(self, package: PackageModel) -> None:
package.installPackageTriggered.connect(self.installPackage)
package.uninstallPackageTriggered.connect(self.uninstallPackage)
package.updatePackageTriggered.connect(self.updatePackage)
def installPackage(self, package_id: str, url: str) -> None:
"""Install a package from the Marketplace
:param package_id: the package identification string
"""
if not self._package_manager.reinstallPackage(package_id):
self.download(package_id, url, False)
else:
package = self.getPackageModel(package_id)
if package:
self.subscribeUserToPackage(package_id, str(package.sdk_version))
def uninstallPackage(self, package_id: str) -> None:
"""Uninstall a package from the Marketplace
:param package_id: the package identification string
"""
self._package_manager.removePackage(package_id)
self.unsunscribeUserFromPackage(package_id)
def updatePackage(self, package_id: str, url: str) -> None:
"""Update a package from the Marketplace
:param package_id: the package identification string
"""
self._package_manager.removePackage(package_id, force_add = not self._package_manager.isBundledPackage(package_id))
self.download(package_id, url, True)

View File

@ -0,0 +1,382 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import re
from enum import Enum
from typing import Any, cast, Dict, List, Optional
from PyQt5.QtCore import pyqtProperty, QObject, pyqtSignal, pyqtSlot
from PyQt5.QtQml import QQmlEngine
from cura.CuraApplication import CuraApplication
from cura.CuraPackageManager import CuraPackageManager
from cura.Settings.CuraContainerRegistry import CuraContainerRegistry # To get names of materials we're compatible with.
from UM.i18n import i18nCatalog # To translate placeholder names if data is not present.
from UM.Logger import Logger
from UM.PluginRegistry import PluginRegistry
catalog = i18nCatalog("cura")
class PackageModel(QObject):
"""
Represents a package, containing all the relevant information to be displayed about a package.
"""
def __init__(self, package_data: Dict[str, Any], section_title: Optional[str] = None, parent: Optional[QObject] = None) -> None:
"""
Constructs a new model for a single package.
:param package_data: The data received from the Marketplace API about the package to create.
:param section_title: If the packages are to be categorized per section provide the section_title
:param parent: The parent QML object that controls the lifetime of this model (normally a PackageList).
"""
super().__init__(parent)
QQmlEngine.setObjectOwnership(self, QQmlEngine.CppOwnership)
self._package_manager: CuraPackageManager = cast(CuraPackageManager, CuraApplication.getInstance().getPackageManager())
self._plugin_registry: PluginRegistry = CuraApplication.getInstance().getPluginRegistry()
self._package_id = package_data.get("package_id", "UnknownPackageId")
self._package_type = package_data.get("package_type", "")
self._is_bundled = package_data.get("is_bundled", False)
self._icon_url = package_data.get("icon_url", "")
self._display_name = package_data.get("display_name", catalog.i18nc("@label:property", "Unknown Package"))
tags = package_data.get("tags", [])
self._is_checked_by_ultimaker = (self._package_type == "plugin" and "verified" in tags) or (
self._package_type == "material" and "certified" in tags)
self._package_version = package_data.get("package_version", "") # Display purpose, no need for 'UM.Version'.
self._package_info_url = package_data.get("website", "") # Not to be confused with 'download_url'.
self._download_count = package_data.get("download_count", 0)
self._description = package_data.get("description", "")
self._formatted_description = self._format(self._description)
self._download_url = package_data.get("download_url", "")
self._release_notes = package_data.get("release_notes", "") # Not used yet, propose to add to description?
subdata = package_data.get("data", {})
self._technical_data_sheet = self._findLink(subdata, "technical_data_sheet")
self._safety_data_sheet = self._findLink(subdata, "safety_data_sheet")
self._where_to_buy = self._findLink(subdata, "where_to_buy")
self._compatible_printers = self._getCompatiblePrinters(subdata)
self._compatible_support_materials = self._getCompatibleSupportMaterials(subdata)
self._is_compatible_material_station = self._isCompatibleMaterialStation(subdata)
self._is_compatible_air_manager = self._isCompatibleAirManager(subdata)
author_data = package_data.get("author", {})
self._author_name = author_data.get("display_name", catalog.i18nc("@label:property", "Unknown Author"))
self._author_info_url = author_data.get("website", "")
if not self._icon_url or self._icon_url == "":
self._icon_url = author_data.get("icon_url", "")
self._can_update = False
self._section_title = section_title
self.sdk_version = package_data.get("sdk_version_semver", "")
# Note that there's a lot more info in the package_data than just these specified here.
self.enablePackageTriggered.connect(self._plugin_registry.enablePlugin)
self.disablePackageTriggered.connect(self._plugin_registry.disablePlugin)
self._plugin_registry.pluginsEnabledOrDisabledChanged.connect(self.stateManageButtonChanged)
self._package_manager.packageInstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id))
self._package_manager.packageUninstalled.connect(lambda pkg_id: self._packageInstalled(pkg_id))
self._package_manager.packageInstallingFailed.connect(lambda pkg_id: self._packageInstalled(pkg_id))
self._package_manager.packagesWithUpdateChanged.connect(self._processUpdatedPackages)
self._is_busy = False
@pyqtSlot()
def _processUpdatedPackages(self):
self.setCanUpdate(self._package_manager.checkIfPackageCanUpdate(self._package_id))
def __del__(self):
self._package_manager.packagesWithUpdateChanged.disconnect(self._processUpdatedPackages)
def __eq__(self, other: object) -> bool:
if isinstance(other, PackageModel):
return other == self
elif isinstance(other, str):
return other == self._package_id
else:
return False
def __repr__(self) -> str:
return f"<{self._package_id} : {self._package_version} : {self._section_title}>"
def _findLink(self, subdata: Dict[str, Any], link_type: str) -> str:
"""
Searches the package data for a link of a certain type.
The links are not in a fixed path in the package data. We need to iterate over the available links to find them.
:param subdata: The "data" element in the package data, which should contain links.
:param link_type: The type of link to find.
:return: A URL of where the link leads, or an empty string if there is no link of that type in the package data.
"""
links = subdata.get("links", [])
for link in links:
if link.get("type", "") == link_type:
return link.get("url", "")
else:
return "" # No link with the correct type was found.
def _format(self, text: str) -> str:
"""
Formats a user-readable block of text for display.
:return: A block of rich text with formatting embedded.
"""
# Turn all in-line hyperlinks into actual links.
url_regex = re.compile(r"(((http|https)://)[a-zA-Z0-9@:%.\-_+~#?&/=]{2,256}\.[a-z]{2,12}(/[a-zA-Z0-9@:%.\-_+~#?&/=]*)?)")
text = re.sub(url_regex, r'<a href="\1">\1</a>', text)
# Turn newlines into <br> so that they get displayed as newlines when rendering as rich text.
text = text.replace("\n", "<br>")
return text
def _getCompatiblePrinters(self, subdata: Dict[str, Any]) -> List[str]:
"""
Gets the list of printers that this package provides material compatibility with.
Any printer is listed, even if it's only for a single nozzle on a single material in the package.
:param subdata: The "data" element in the package data, which should contain this compatibility information.
:return: A list of printer names that this package provides material compatibility with.
"""
result = set()
for material in subdata.get("materials", []):
for compatibility in material.get("compatibility", []):
printer_name = compatibility.get("machine_name")
if printer_name is None:
continue # Missing printer name information. Skip this one.
for subcompatibility in compatibility.get("compatibilities", []):
if subcompatibility.get("hardware_compatible", False):
result.add(printer_name)
break
return list(sorted(result))
def _getCompatibleSupportMaterials(self, subdata: Dict[str, Any]) -> List[str]:
"""
Gets the list of support materials that the materials in this package are compatible with.
Since the materials are individually encoded as keys in the API response, only PVA and Breakaway are currently
supported.
:param subdata: The "data" element in the package data, which should contain this compatibility information.
:return: A list of support materials that the materials in this package are compatible with.
"""
result = set()
container_registry = CuraContainerRegistry.getInstance()
try:
pva_name = container_registry.findContainersMetadata(id = "ultimaker_pva")[0].get("name", "Ultimaker PVA")
except IndexError:
pva_name = "Ultimaker PVA"
try:
breakaway_name = container_registry.findContainersMetadata(id = "ultimaker_bam")[0].get("name", "Ultimaker Breakaway")
except IndexError:
breakaway_name = "Ultimaker Breakaway"
for material in subdata.get("materials", []):
if material.get("pva_compatible", False):
result.add(pva_name)
if material.get("breakaway_compatible", False):
result.add(breakaway_name)
return list(sorted(result))
def _isCompatibleMaterialStation(self, subdata: Dict[str, Any]) -> bool:
"""
Finds out if this package provides any material that is compatible with the material station.
:param subdata: The "data" element in the package data, which should contain this compatibility information.
:return: Whether this package provides any material that is compatible with the material station.
"""
for material in subdata.get("materials", []):
for compatibility in material.get("compatibility", []):
if compatibility.get("material_station_optimized", False):
return True
return False
def _isCompatibleAirManager(self, subdata: Dict[str, Any]) -> bool:
"""
Finds out if this package provides any material that is compatible with the air manager.
:param subdata: The "data" element in the package data, which should contain this compatibility information.
:return: Whether this package provides any material that is compatible with the air manager.
"""
for material in subdata.get("materials", []):
for compatibility in material.get("compatibility", []):
if compatibility.get("air_manager_optimized", False):
return True
return False
@pyqtProperty(str, constant = True)
def packageId(self) -> str:
return self._package_id
@pyqtProperty(str, constant = True)
def packageType(self) -> str:
return self._package_type
@pyqtProperty(str, constant = True)
def iconUrl(self) -> str:
return self._icon_url
@pyqtProperty(str, constant = True)
def displayName(self) -> str:
return self._display_name
@pyqtProperty(bool, constant = True)
def isCheckedByUltimaker(self):
return self._is_checked_by_ultimaker
@pyqtProperty(str, constant = True)
def packageVersion(self) -> str:
return self._package_version
@pyqtProperty(str, constant = True)
def packageInfoUrl(self) -> str:
return self._package_info_url
@pyqtProperty(int, constant = True)
def downloadCount(self) -> str:
return self._download_count
@pyqtProperty(str, constant = True)
def description(self) -> str:
return self._description
@pyqtProperty(str, constant = True)
def formattedDescription(self) -> str:
return self._formatted_description
@pyqtProperty(str, constant = True)
def authorName(self) -> str:
return self._author_name
@pyqtProperty(str, constant = True)
def authorInfoUrl(self) -> str:
return self._author_info_url
@pyqtProperty(str, constant = True)
def sectionTitle(self) -> Optional[str]:
return self._section_title
@pyqtProperty(str, constant = True)
def technicalDataSheet(self) -> str:
return self._technical_data_sheet
@pyqtProperty(str, constant = True)
def safetyDataSheet(self) -> str:
return self._safety_data_sheet
@pyqtProperty(str, constant = True)
def whereToBuy(self) -> str:
return self._where_to_buy
@pyqtProperty("QStringList", constant = True)
def compatiblePrinters(self) -> List[str]:
return self._compatible_printers
@pyqtProperty("QStringList", constant = True)
def compatibleSupportMaterials(self) -> List[str]:
return self._compatible_support_materials
@pyqtProperty(bool, constant = True)
def isCompatibleMaterialStation(self) -> bool:
return self._is_compatible_material_station
@pyqtProperty(bool, constant = True)
def isCompatibleAirManager(self) -> bool:
return self._is_compatible_air_manager
@pyqtProperty(bool, constant = True)
def isBundled(self) -> bool:
return self._is_bundled
def setDownloadUrl(self, download_url):
self._download_url = download_url
# --- manage buttons signals ---
stateManageButtonChanged = pyqtSignal()
installPackageTriggered = pyqtSignal(str, str)
uninstallPackageTriggered = pyqtSignal(str)
updatePackageTriggered = pyqtSignal(str, str)
enablePackageTriggered = pyqtSignal(str)
disablePackageTriggered = pyqtSignal(str)
busyChanged = pyqtSignal()
@pyqtSlot()
def install(self):
self.setBusy(True)
self.installPackageTriggered.emit(self.packageId, self._download_url)
@pyqtSlot()
def update(self):
self.setBusy(True)
self.updatePackageTriggered.emit(self.packageId, self._download_url)
@pyqtSlot()
def uninstall(self):
self.uninstallPackageTriggered.emit(self.packageId)
@pyqtProperty(bool, notify= busyChanged)
def busy(self):
"""
Property indicating that some kind of upgrade is active.
"""
return self._is_busy
@pyqtSlot()
def enable(self):
self.enablePackageTriggered.emit(self.packageId)
@pyqtSlot()
def disable(self):
self.disablePackageTriggered.emit(self.packageId)
def setBusy(self, value: bool):
if self._is_busy != value:
self._is_busy = value
try:
self.busyChanged.emit()
except RuntimeError:
pass
def _packageInstalled(self, package_id: str) -> None:
if self._package_id != package_id:
return
self.setBusy(False)
try:
self.stateManageButtonChanged.emit()
except RuntimeError:
pass
@pyqtProperty(bool, notify = stateManageButtonChanged)
def isInstalled(self) -> bool:
return self._package_id in self._package_manager.getAllInstalledPackageIDs()
@pyqtProperty(bool, notify = stateManageButtonChanged)
def isToBeInstalled(self) -> bool:
return self._package_id in self._package_manager.getPackagesToInstall()
@pyqtProperty(bool, notify = stateManageButtonChanged)
def isActive(self) -> bool:
return not self._package_id in self._plugin_registry.getDisabledPlugins()
@pyqtProperty(bool, notify = stateManageButtonChanged)
def canDowngrade(self) -> bool:
"""Flag if the installed package can be downgraded to a bundled version"""
return self._package_manager.canDowngrade(self._package_id)
def setCanUpdate(self, value: bool) -> None:
self._can_update = value
self.stateManageButtonChanged.emit()
@pyqtProperty(bool, fset = setCanUpdate, notify = stateManageButtonChanged)
def canUpdate(self) -> bool:
"""Flag indicating if the package can be updated"""
return self._can_update

View File

@ -0,0 +1,151 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from PyQt5.QtCore import pyqtProperty, pyqtSignal, pyqtSlot
from PyQt5.QtNetwork import QNetworkReply
from typing import Optional, TYPE_CHECKING
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.TaskManagement.HttpRequestManager import HttpRequestManager # To request the package list from the API.
from .Constants import PACKAGES_URL # To get the list of packages. Imported this way to prevent circular imports.
from .PackageList import PackageList
from .PackageModel import PackageModel # The contents of this list.
if TYPE_CHECKING:
from PyQt5.QtCore import QObject
catalog = i18nCatalog("cura")
class RemotePackageList(PackageList):
ITEMS_PER_PAGE = 20 # Pagination of number of elements to download at once.
def __init__(self, parent: Optional["QObject"] = None) -> None:
super().__init__(parent)
self._package_type_filter = ""
self._requested_search_string = ""
self._current_search_string = ""
self._request_url = self._initialRequestUrl()
self._ongoing_requests["get_packages"] = None
self.isLoadingChanged.connect(self._onLoadingChanged)
self.isLoadingChanged.emit()
@pyqtSlot()
def updatePackages(self) -> None:
"""
Make a request for the first paginated page of packages.
When the request is done, the list will get updated with the new package models.
"""
self.setErrorMessage("") # Clear any previous errors.
self.setIsLoading(True)
self._ongoing_requests["get_packages"] = HttpRequestManager.getInstance().get(
self._request_url,
scope = self._scope,
callback = self._parseResponse,
error_callback = self._onError
)
def reset(self) -> None:
self.clear()
self._request_url = self._initialRequestUrl()
packageTypeFilterChanged = pyqtSignal()
searchStringChanged = pyqtSignal()
def setPackageTypeFilter(self, new_filter: str) -> None:
if new_filter != self._package_type_filter:
self._package_type_filter = new_filter
self.reset()
self.packageTypeFilterChanged.emit()
def setSearchString(self, new_search: str) -> None:
self._requested_search_string = new_search
self._onLoadingChanged()
@pyqtProperty(str, fset = setPackageTypeFilter, notify = packageTypeFilterChanged)
def packageTypeFilter(self) -> str:
"""
Get the package type this package list is filtering on, like ``plugin`` or ``material``.
:return: The package type this list is filtering on.
"""
return self._package_type_filter
@pyqtProperty(str, fset = setSearchString, notify = searchStringChanged)
def searchString(self) -> str:
"""
Get the string the user is currently searching for (as in: the list is updating) within the packages,
or an empty string if no extra search filter has to be applied. Does not override package-type filter!
:return: String the user is searching for. Empty denotes 'no search filter'.
"""
return self._current_search_string
def _onLoadingChanged(self) -> None:
if self._requested_search_string != self._current_search_string and not self._is_loading:
self._current_search_string = self._requested_search_string
self.reset()
self.updatePackages()
self.searchStringChanged.emit()
def _initialRequestUrl(self) -> str:
"""
Get the URL to request the first paginated page with.
:return: A URL to request.
"""
request_url = f"{PACKAGES_URL}?limit={self.ITEMS_PER_PAGE}"
if self._package_type_filter != "":
request_url += f"&package_type={self._package_type_filter}"
if self._current_search_string != "":
request_url += f"&search={self._current_search_string}"
return request_url
def _parseResponse(self, reply: "QNetworkReply") -> None:
"""
Parse the response from the package list API request.
This converts that response into PackageModels, and triggers the ListModel to update.
:param reply: A reply containing information about a number of packages.
"""
response_data = HttpRequestManager.readJSON(reply)
if "data" not in response_data or "links" not in response_data:
Logger.error(f"Could not interpret the server's response. Missing 'data' or 'links' from response data. Keys in response: {response_data.keys()}")
self.setErrorMessage(catalog.i18nc("@info:error", "Could not interpret the server's response."))
return
for package_data in response_data["data"]:
package_id = package_data["package_id"]
if package_id in self._package_manager.local_packages_ids:
continue # We should only show packages which are not already installed
try:
package = PackageModel(package_data, parent = self)
self._connectManageButtonSignals(package)
self.appendItem({"package": package}) # Add it to this list model.
except RuntimeError:
# Setting the ownership of this object to not qml can still result in a RuntimeError. Which can occur when quickly toggling
# between de-/constructing RemotePackageLists. This try-except is here to prevent a hard crash when the wrapped C++ object
# was deleted when it was still parsing the response
continue
self._request_url = response_data["links"].get("next", "") # Use empty string to signify that there is no next page.
self._ongoing_requests["get_packages"] = None
self.setIsLoading(False)
self.setHasMore(self._request_url != "")
def _onError(self, reply: "QNetworkReply", error: Optional[QNetworkReply.NetworkError]) -> None:
"""
Handles networking and server errors when requesting the list of packages.
:param reply: The reply with packages. This will most likely be incomplete and should be ignored.
:param error: The error status of the request.
"""
if error == QNetworkReply.NetworkError.OperationCanceledError:
Logger.debug("Cancelled request for packages.")
self._ongoing_requests["get_packages"] = None
return # Don't show an error about this to the user.
Logger.error("Could not reach Marketplace server.")
self.setErrorMessage(catalog.i18nc("@info:error", "Could not reach Marketplace."))
self._ongoing_requests["get_packages"] = None
self.setIsLoading(False)

View File

@ -0,0 +1,17 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
from .Marketplace import Marketplace
def getMetaData():
"""
Extension-type plug-ins don't have any specific metadata being used by Cura.
"""
return {}
def register(app):
"""
Register the plug-in object with Uranium.
"""
return { "extension": Marketplace() }

View File

@ -0,0 +1,8 @@
{
"name": "Marketplace",
"author": "Ultimaker B.V.",
"version": "1.0.0",
"api": 7,
"description": "Manages extensions to the application and allows browsing extensions from the Ultimaker website.",
"i18n-catalog": "cura"
}

View File

Before

Width:  |  Height:  |  Size: 184 B

After

Width:  |  Height:  |  Size: 184 B

View File

@ -0,0 +1,91 @@
//Copyright (c) 2021 Ultimaker B.V.
//Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import UM 1.6 as UM
import Cura 1.6 as Cura
UM.Dialog
{
id: licenseDialog
title: catalog.i18nc("@button", "Plugin license agreement")
minimumWidth: UM.Theme.getSize("license_window_minimum").width
minimumHeight: UM.Theme.getSize("license_window_minimum").height
width: minimumWidth
height: minimumHeight
backgroundColor: UM.Theme.getColor("main_background")
property variant catalog: UM.I18nCatalog { name: "cura" }
ColumnLayout
{
anchors.fill: parent
spacing: UM.Theme.getSize("thick_margin").height
Row
{
Layout.fillWidth: true
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
leftPadding: UM.Theme.getSize("narrow_margin").width
UM.RecolorImage
{
id: icon
width: UM.Theme.getSize("marketplace_large_icon").width
height: UM.Theme.getSize("marketplace_large_icon").height
color: UM.Theme.getColor("text")
source: UM.Theme.getIcon("Certificate", "high")
}
Label
{
text: catalog.i18nc("@text", "Please read and agree with the plugin licence.")
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("large")
anchors.verticalCenter: icon.verticalCenter
height: UM.Theme.getSize("marketplace_large_icon").height
verticalAlignment: Qt.AlignVCenter
wrapMode: Text.Wrap
renderType: Text.NativeRendering
}
}
Cura.ScrollableTextArea
{
Layout.fillWidth: true
Layout.fillHeight: true
anchors.topMargin: UM.Theme.getSize("default_margin").height
textArea.text: licenseContent
textArea.readOnly: true
}
}
rightButtons:
[
Cura.PrimaryButton
{
text: catalog.i18nc("@button", "Accept")
onClicked: handler.onLicenseAccepted(packageId)
}
]
leftButtons:
[
Cura.SecondaryButton
{
text: catalog.i18nc("@button", "Decline")
onClicked: handler.onLicenseDeclined(packageId)
}
]
onAccepted: handler.onLicenseAccepted(packageId)
onRejected: handler.onLicenseDeclined(packageId)
onClosing: handler.onLicenseDeclined(packageId)
}

View File

@ -0,0 +1,114 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.1
import UM 1.6 as UM
import Cura 1.6 as Cura
Item
{
id: manageButton
property bool button_style: true
property string text
property bool busy: false
property bool confirmed: false
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
signal clicked()
property Component primaryButton: Component
{
Cura.PrimaryButton
{
text: manageButton.text
onClicked: manageButton.clicked()
}
}
property Component secondaryButton: Component
{
Cura.SecondaryButton
{
text: manageButton.text
onClicked: manageButton.clicked()
}
}
property Component busyButton: Component
{
Item
{
height: UM.Theme.getSize("action_button").height
width: childrenRect.width
UM.RecolorImage
{
id: busyIndicator
visible: parent.visible
height: UM.Theme.getSize("action_button").height - 2 * UM.Theme.getSize("narrow_margin").height
width: height
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
source: UM.Theme.getIcon("Spinner")
color: UM.Theme.getColor("primary")
RotationAnimator
{
target: busyIndicator
running: parent.visible
from: 0
to: 360
loops: Animation.Infinite
duration: 2500
}
}
Label
{
visible: parent.visible
anchors.left: busyIndicator.right
anchors.leftMargin: UM.Theme.getSize("narrow_margin").width
anchors.verticalCenter: parent.verticalCenter
text: manageButton.text
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("primary")
}
}
}
property Component confirmButton: Component
{
Item
{
height: UM.Theme.getSize("action_button").height
width: childrenRect.width
Label
{
anchors.verticalCenter: parent.verticalCenter
text: manageButton.text
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("primary")
}
}
}
Loader
{
sourceComponent:
{
if (busy) { return manageButton.busyButton; }
else if (confirmed) { return manageButton.confirmButton; }
else if (manageButton.button_style) { return manageButton.primaryButton; }
else { return manageButton.secondaryButton; }
}
}
}

View File

@ -0,0 +1,49 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import UM 1.2 as UM
import Cura 1.6 as Cura
import QtQuick 2.15
import QtQuick.Controls 2.15
TabButton
{
id: root
width: UM.Theme.getSize("button_icon").width + UM.Theme.getSize("narrow_margin").width
height: UM.Theme.getSize("button_icon").height
hoverEnabled: true
property color inactiveBackgroundColor : hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("detail_background")
property color activeBackgroundColor : UM.Theme.getColor("main_background")
leftInset: UM.Theme.getSize("narrow_margin").width
background: Rectangle
{
color: parent.checked ? activeBackgroundColor : inactiveBackgroundColor
border.color: parent.checked ? UM.Theme.getColor("detail_background") : "transparent"
border.width: UM.Theme.getSize("thick_lining").width
radius: Math.round(width * 0.5)
}
Cura.ToolTip
{
id: tooltip
tooltipText: catalog.i18nc("@info:tooltip", "Manage packages")
visible: root.hovered
}
UM.RecolorImage
{
id: icon
width: UM.Theme.getSize("section_icon").width
height: UM.Theme.getSize("section_icon").height
color: UM.Theme.getColor("icon")
source: UM.Theme.getIcon("Settings")
anchors.horizontalCenter: parent.horizontalCenter
anchors.horizontalCenterOffset: Math.round(UM.Theme.getSize("narrow_margin").width /2)
anchors.verticalCenter: parent.verticalCenter
}
}

View File

@ -0,0 +1,25 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import UM 1.4 as UM
Packages
{
pageTitle: catalog.i18nc("@header", "Manage packages")
bannerVisible: UM.Preferences.getValue("cura/market_place_show_manage_packages_banner");
bannerIcon: UM.Theme.getIcon("ArrowDoubleCircleRight")
bannerText: catalog.i18nc("@text", "Manage your Ultimaker Cura plugins and material profiles here. Make sure to keep your plugins up to date and backup your setup regularly.")
bannerReadMoreUrl: "" // TODO add when support page is ready
onRemoveBanner: function() {
UM.Preferences.setValue("cura/market_place_show_manage_packages_banner", false);
bannerVisible = false;
}
searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/plugins?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-plugins-browser"
packagesManageableInListView: true
model: manager.LocalPackageList
}

View File

@ -0,0 +1,299 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import QtQuick.Window 2.2
import UM 1.2 as UM
import Cura 1.6 as Cura
Window
{
id: marketplaceDialog
property variant catalog: UM.I18nCatalog { name: "cura" }
signal searchStringChanged(string new_search)
minimumWidth: UM.Theme.getSize("modal_window_minimum").width
minimumHeight: UM.Theme.getSize("modal_window_minimum").height
width: minimumWidth
height: minimumHeight
onVisibleChanged:
{
while(contextStack.depth > 1)
{
contextStack.pop(); //Do NOT use the StackView.Immediate transition here, since it causes the window to stay empty. Seemingly a Qt bug: https://bugreports.qt.io/browse/QTBUG-60670?
}
}
Connections
{
target: Cura.API.account
function onLoginStateChanged()
{
close();
}
}
title: "Marketplace" //Seen by Ultimaker as a brand name, so this doesn't get translated.
modality: Qt.NonModal
// Background color
Rectangle
{
anchors.fill: parent
color: UM.Theme.getColor("main_background")
//The Marketplace can have a page in front of everything with package details. The stack view controls its visibility.
StackView
{
id: contextStack
anchors.fill: parent
initialItem: packageBrowse
ColumnLayout
{
id: packageBrowse
spacing: UM.Theme.getSize("default_margin").height
// Page title.
Item
{
Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height + UM.Theme.getSize("default_margin").height
Label
{
id: pageTitle
anchors
{
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
right: parent.right
rightMargin: UM.Theme.getSize("default_margin").width
bottom: parent.bottom
}
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
text: content.item ? content.item.pageTitle: catalog.i18nc("@title", "Loading...")
}
}
OnboardBanner
{
visible: content.item && content.item.bannerVisible
text: content.item && content.item.bannerText
icon: content.item && content.item.bannerIcon
onRemove: content.item && content.item.onRemoveBanner
readMoreUrl: content.item && content.item.bannerReadMoreUrl
}
// Search & Top-Level Tabs
Item
{
Layout.preferredHeight: childrenRect.height
Layout.preferredWidth: parent.width - 2 * UM.Theme.getSize("thin_margin").width
RowLayout
{
width: parent.width
height: UM.Theme.getSize("button_icon").height + UM.Theme.getSize("default_margin").height
spacing: UM.Theme.getSize("thin_margin").width
Item
{
Layout.preferredHeight: parent.height
Layout.preferredWidth: searchBar.visible ? UM.Theme.getSize("thin_margin").width : 0
Layout.fillWidth: ! searchBar.visible
}
Cura.SearchBar
{
id: searchBar
Layout.preferredHeight: UM.Theme.getSize("button_icon").height
Layout.fillWidth: true
onTextEdited: searchStringChanged(text)
}
// Page selection.
TabBar
{
id: pageSelectionTabBar
Layout.alignment: Qt.AlignRight
height: UM.Theme.getSize("button_icon").height
spacing: 0
background: Rectangle { color: "transparent" }
currentIndex: manager.tabShown
onCurrentIndexChanged:
{
manager.tabShown = currentIndex
searchBar.text = "";
searchBar.visible = currentItem.hasSearch;
content.source = currentItem.sourcePage;
}
PackageTypeTab
{
id: pluginTabText
width: implicitWidth
text: catalog.i18nc("@button", "Plugins")
property string sourcePage: "Plugins.qml"
property bool hasSearch: true
}
PackageTypeTab
{
id: materialsTabText
width: implicitWidth
text: catalog.i18nc("@button", "Materials")
property string sourcePage: "Materials.qml"
property bool hasSearch: true
}
ManagePackagesButton
{
property string sourcePage: "ManagedPackages.qml"
property bool hasSearch: false
Cura.NotificationIcon
{
anchors
{
horizontalCenter: parent.right
verticalCenter: parent.top
}
visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0
labelText:
{
const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length
return itemCount > 9 ? "9+" : itemCount
}
}
}
}
TextMetrics
{
id: pluginTabTextMetrics
text: pluginTabText.text
font: pluginTabText.font
}
TextMetrics
{
id: materialsTabTextMetrics
text: materialsTabText.text
font: materialsTabText.font
}
}
}
FontMetrics
{
id: fontMetrics
font: UM.Theme.getFont("default")
}
Cura.TertiaryButton
{
text: catalog.i18nc("@info", "Search in the browser")
iconSource: UM.Theme.getIcon("LinkExternal")
visible: pageSelectionTabBar.currentItem.hasSearch
isIconOnRightSide: true
height: fontMetrics.height
textFont: fontMetrics.font
textColor: UM.Theme.getColor("text")
onClicked: content.item && Qt.openUrlExternally(content.item.searchInBrowserUrl)
}
// Page contents.
Rectangle
{
Layout.preferredWidth: parent.width
Layout.fillHeight: true
color: UM.Theme.getColor("detail_background")
// Page contents.
Loader
{
id: content
anchors.fill: parent
anchors.margins: UM.Theme.getSize("default_margin").width
source: "Plugins.qml"
Connections
{
target: content
function onLoaded()
{
pageTitle.text = content.item.pageTitle
searchStringChanged.connect(handleSearchStringChanged)
}
function handleSearchStringChanged(new_search)
{
content.item.model.searchString = new_search
}
}
}
}
}
}
}
Rectangle
{
height: quitButton.height + 2 * UM.Theme.getSize("default_margin").width
color: UM.Theme.getColor("primary")
visible: manager.showRestartNotification
anchors
{
left: parent.left
right: parent.right
bottom: parent.bottom
}
RowLayout
{
anchors
{
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
margins: UM.Theme.getSize("default_margin").width
}
spacing: UM.Theme.getSize("default_margin").width
UM.RecolorImage
{
id: bannerIcon
source: UM.Theme.getIcon("Plugin")
color: UM.Theme.getColor("primary_button_text")
implicitWidth: UM.Theme.getSize("banner_icon_size").width
implicitHeight: UM.Theme.getSize("banner_icon_size").height
}
Text
{
color: UM.Theme.getColor("primary_button_text")
text: catalog.i18nc("@button", "In order to use the package you will need to restart Cura")
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
Layout.fillWidth: true
}
Cura.SecondaryButton
{
id: quitButton
text: catalog.i18nc("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName)
onClicked:
{
marketplaceDialog.hide();
CuraApplication.closeApplication();
}
}
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import UM 1.4 as UM
Packages
{
pageTitle: catalog.i18nc("@header", "Install Materials")
bannerVisible: UM.Preferences.getValue("cura/market_place_show_material_banner")
bannerIcon: UM.Theme.getIcon("Spool")
bannerText: catalog.i18nc("@text", "Select and install material profiles optimised for your Ultimaker 3D printers.")
bannerReadMoreUrl: "" // TODO add when support page is ready
onRemoveBanner: function() {
UM.Preferences.setValue("cura/market_place_show_material_banner", false);
bannerVisible = false;
}
searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/materials?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-materials-browser"
packagesManageableInListView: false
model: manager.MaterialPackageList
}

View File

@ -0,0 +1,119 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.1
import UM 1.6 as UM
import Cura 1.6 as Cura
// Onboarding banner.
Rectangle
{
property alias icon: onboardingIcon.source
property alias text: infoText.text
property var onRemove
property string readMoreUrl
Layout.preferredHeight: childrenRect.height + 2 * UM.Theme.getSize("default_margin").height
Layout.fillWidth: true
Layout.margins: UM.Theme.getSize("default_margin").width
color: UM.Theme.getColor("action_panel_secondary")
// Icon
UM.RecolorImage
{
id: onboardingIcon
anchors
{
top: parent.top
left: parent.left
margins: UM.Theme.getSize("default_margin").width
}
width: UM.Theme.getSize("banner_icon_size").width
height: UM.Theme.getSize("banner_icon_size").height
}
// Close button
UM.SimpleButton
{
id: onboardingClose
anchors
{
top: parent.top
right: parent.right
margins: UM.Theme.getSize("default_margin").width
}
width: UM.Theme.getSize("message_close").width
height: UM.Theme.getSize("message_close").height
color: UM.Theme.getColor("primary_text")
hoverColor: UM.Theme.getColor("primary_text_hover")
iconSource: UM.Theme.getIcon("Cancel")
onClicked: onRemove()
}
// Body
Label {
id: infoText
anchors
{
top: parent.top
left: onboardingIcon.right
right: onboardingClose.left
margins: UM.Theme.getSize("default_margin").width
}
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
color: UM.Theme.getColor("primary_text")
wrapMode: Text.Wrap
elide: Text.ElideRight
onLineLaidOut:
{
if(line.isLast)
{
// Check if read more button still fits after the body text
if (line.implicitWidth + readMoreButton.width + UM.Theme.getSize("default_margin").width > width)
{
// If it does place it after the body text
readMoreButton.anchors.bottomMargin = -(fontMetrics.height);
readMoreButton.anchors.leftMargin = UM.Theme.getSize("thin_margin").width;
}
else
{
// Otherwise place it under the text
readMoreButton.anchors.leftMargin = line.implicitWidth + UM.Theme.getSize("default_margin").width;
readMoreButton.anchors.bottomMargin = 0;
}
}
}
}
FontMetrics
{
id: fontMetrics
font: UM.Theme.getFont("default")
}
Cura.TertiaryButton
{
id: readMoreButton
anchors.left: infoText.left
anchors.bottom: infoText.bottom
text: "Learn More"
textFont: UM.Theme.getFont("default")
textColor: infoText.color
leftPadding: 0
rightPadding: 0
iconSource: UM.Theme.getIcon("LinkExternal")
isIconOnRightSide: true
height: fontMetrics.height
onClicked: Qt.openUrlExternally(readMoreUrl)
}
}

View File

@ -0,0 +1,101 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.1
import UM 1.6 as UM
import Cura 1.6 as Cura
Rectangle
{
property alias packageData: packageCardHeader.packageData
property alias manageableInListView: packageCardHeader.showManageButtons
height: childrenRect.height
color: UM.Theme.getColor("main_background")
radius: UM.Theme.getSize("default_radius").width
PackageCardHeader
{
id: packageCardHeader
Item
{
id: shortDescription
anchors.fill: parent
Label
{
id: descriptionLabel
width: parent.width
property real lastLineWidth: 0; //Store the width of the last line, to properly position the elision.
text: packageData.description
textFormat: Text.PlainText //Must be plain text, or we won't get onLineLaidOut signals. Don't auto-detect!
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
maximumLineCount: 2
wrapMode: Text.Wrap
elide: Text.ElideRight
visible: text !== ""
onLineLaidOut:
{
if(truncated && line.isLast)
{
let max_line_width = parent.width - readMoreButton.width - fontMetrics.advanceWidth("… ") - 2 * UM.Theme.getSize("default_margin").width;
if(line.implicitWidth > max_line_width)
{
line.width = max_line_width;
}
else
{
line.width = line.implicitWidth - fontMetrics.advanceWidth("…"); //Truncate the ellipsis. We're adding this ourselves.
}
descriptionLabel.lastLineWidth = line.implicitWidth;
}
}
}
Label
{
id: tripleDotLabel
anchors.left: parent.left
anchors.leftMargin: descriptionLabel.lastLineWidth
anchors.bottom: descriptionLabel.bottom
text: "… "
font: descriptionLabel.font
color: descriptionLabel.color
visible: descriptionLabel.truncated && descriptionLabel.text !== ""
}
Cura.TertiaryButton
{
id: readMoreButton
anchors.right: parent.right
anchors.bottom: descriptionLabel.bottom
height: fontMetrics.height //Height of a single line.
text: catalog.i18nc("@info", "Read more")
iconSource: UM.Theme.getIcon("LinkExternal")
visible: descriptionLabel.truncated && descriptionLabel.text !== ""
enabled: visible
leftPadding: UM.Theme.getSize("default_margin").width
rightPadding: UM.Theme.getSize("wide_margin").width
textFont: descriptionLabel.font
isIconOnRightSide: true
onClicked: Qt.openUrlExternally(packageData.packageInfoUrl)
}
}
}
FontMetrics
{
id: fontMetrics
font: UM.Theme.getFont("default")
}
}

View File

@ -0,0 +1,215 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.1
import UM 1.6 as UM
import Cura 1.6 as Cura
// As both the PackageCard and Package contain similar components; a package icon, title, author bar. These components
// are combined into the reusable "PackageCardHeader" component
Item
{
default property alias contents: contentItem.children;
property var packageData
property bool showManageButtons: false
width: parent.width
height: UM.Theme.getSize("card").height
// card icon
Image
{
id: packageItem
anchors
{
top: parent.top
left: parent.left
margins: UM.Theme.getSize("default_margin").width
}
width: UM.Theme.getSize("card_icon").width
height: width
source: packageData.iconUrl != "" ? packageData.iconUrl : "../images/placeholder.svg"
}
ColumnLayout
{
anchors
{
left: packageItem.right
leftMargin: UM.Theme.getSize("default_margin").width
right: parent.right
rightMargin: UM.Theme.getSize("default_margin").width
top: parent.top
topMargin: UM.Theme.getSize("narrow_margin").height
}
height: packageItem.height + packageItem.anchors.margins * 2
// Title row.
RowLayout
{
id: titleBar
Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height
Label
{
text: packageData.displayName
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignTop
}
VerifiedIcon
{
enabled: packageData.isCheckedByUltimaker
visible: packageData.isCheckedByUltimaker
}
Label
{
id: packageVersionLabel
text: packageData.packageVersion
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
Layout.fillWidth: true
}
Button
{
id: externalLinkButton
// For some reason if i set padding, they don't match up. If i set all of them explicitly, it does work?
leftPadding: UM.Theme.getSize("narrow_margin").width
rightPadding: UM.Theme.getSize("narrow_margin").width
topPadding: UM.Theme.getSize("narrow_margin").width
bottomPadding: UM.Theme.getSize("narrow_margin").width
Layout.preferredWidth: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
Layout.preferredHeight: UM.Theme.getSize("card_tiny_icon").width + 2 * padding
contentItem: UM.RecolorImage
{
source: UM.Theme.getIcon("LinkExternal")
color: UM.Theme.getColor("icon")
implicitWidth: UM.Theme.getSize("card_tiny_icon").width
implicitHeight: UM.Theme.getSize("card_tiny_icon").height
}
background: Rectangle
{
color: externalLinkButton.hovered ? UM.Theme.getColor("action_button_hovered"): "transparent"
radius: externalLinkButton.width / 2
}
onClicked: Qt.openUrlExternally(packageData.authorInfoUrl)
}
}
// When a package Card companent is created and children are provided to it they are rendered here
Item {
id: contentItem
Layout.fillHeight: true
Layout.preferredWidth: parent.width
}
// Author and action buttons.
RowLayout
{
id: authorAndActionButton
Layout.preferredWidth: parent.width
Layout.preferredHeight: childrenRect.height
spacing: UM.Theme.getSize("narrow_margin").width
// label "By"
Label
{
id: authorBy
Layout.alignment: Qt.AlignCenter
text: catalog.i18nc("@label", "By")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
}
// clickable author name
Item
{
Layout.fillWidth: true
implicitHeight: authorBy.height
Layout.alignment: Qt.AlignTop
Cura.TertiaryButton
{
text: packageData.authorName
textFont: UM.Theme.getFont("default_bold")
textColor: UM.Theme.getColor("text") // override normal link color
leftPadding: 0
rightPadding: 0
iconSource: UM.Theme.getIcon("LinkExternal")
isIconOnRightSide: true
onClicked: Qt.openUrlExternally(packageData.authorInfoUrl)
}
}
ManageButton
{
id: enableManageButton
visible: showManageButtons && packageData.isInstalled && !packageData.isToBeInstalled && packageData.packageType != "material"
enabled: !packageData.busy
button_style: !packageData.isActive
Layout.alignment: Qt.AlignTop
text: button_style ? catalog.i18nc("@button", "Enable") : catalog.i18nc("@button", "Disable")
onClicked: packageData.isActive ? packageData.disable(): packageData.enable()
}
ManageButton
{
id: installManageButton
visible: showManageButtons && (packageData.canDowngrade || !packageData.isBundled)
enabled: !packageData.busy
busy: packageData.busy
button_style: !(packageData.isInstalled || packageData.isToBeInstalled)
Layout.alignment: Qt.AlignTop
text:
{
if (packageData.canDowngrade)
{
if (busy) { return catalog.i18nc("@button", "Downgrading..."); }
else { return catalog.i18nc("@button", "Downgrade"); }
}
if (!(packageData.isInstalled || packageData.isToBeInstalled))
{
if (busy) { return catalog.i18nc("@button", "Installing..."); }
else { return catalog.i18nc("@button", "Install"); }
}
else
{
return catalog.i18nc("@button", "Uninstall");
}
}
onClicked: packageData.isInstalled || packageData.isToBeInstalled ? packageData.uninstall(): packageData.install()
}
ManageButton
{
id: updateManageButton
visible: showManageButtons && packageData.canUpdate
enabled: !packageData.busy
busy: packageData.busy
Layout.alignment: Qt.AlignTop
text: busy ? catalog.i18nc("@button", "Updating..."): catalog.i18nc("@button", "Update")
onClicked: packageData.update()
}
}
}
}

View File

@ -0,0 +1,96 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3
import Cura 1.0 as Cura
import UM 1.0 as UM
Item
{
id: detailPage
property var packageData: packages.selectedPackage
property string title: catalog.i18nc("@header", "Package details")
RowLayout
{
id: header
anchors
{
top: parent.top
topMargin: UM.Theme.getSize("default_margin").height
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
right: parent.right
rightMargin: anchors.leftMargin
}
spacing: UM.Theme.getSize("default_margin").width
Cura.SecondaryButton
{
Layout.alignment: Qt.AlignVCenter
Layout.preferredHeight: UM.Theme.getSize("action_button").height
Layout.preferredWidth: height
onClicked: contextStack.pop() //Remove this page, returning to the main package list or whichever thing is beneath it.
tooltip: catalog.i18nc("@button:tooltip", "Back")
toolTipContentAlignment: Cura.ToolTip.ContentAlignment.AlignRight
leftPadding: UM.Theme.getSize("narrow_margin").width
rightPadding: leftPadding
iconSource: UM.Theme.getIcon("ArrowLeft")
iconSize: height - leftPadding * 2
}
Label
{
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
text: detailPage.title
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
}
}
Rectangle
{
anchors
{
top: header.bottom
topMargin: UM.Theme.getSize("default_margin").height
left: parent.left
right: parent.right
bottom: parent.bottom
}
color: UM.Theme.getColor("detail_background")
ScrollView
{
anchors.fill: parent
clip: true //Need to clip, not for the bottom (which is off the window) but for the top (which would overlap the header).
ScrollBar.horizontal.policy: ScrollBar.AlwaysOff
contentHeight: packagePage.height + UM.Theme.getSize("default_margin").height * 2
PackagePage
{
id: packagePage
anchors
{
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
right: parent.right
rightMargin: anchors.leftMargin
top: parent.top
topMargin: UM.Theme.getSize("default_margin").height
}
packageData: detailPage.packageData
}
}
}
}

View File

@ -0,0 +1,295 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.1
import UM 1.6 as UM
import Cura 1.6 as Cura
Rectangle
{
id: root
property alias packageData: packageCardHeader.packageData
height: childrenRect.height
color: UM.Theme.getColor("main_background")
radius: UM.Theme.getSize("default_radius").width
Column
{
width: parent.width
spacing: 0
Item
{
width: parent.width
height: UM.Theme.getSize("card").height
PackageCardHeader
{
id: packageCardHeader
showManageButtons: true
anchors.fill: parent
Row
{
id: downloadCount
Layout.preferredWidth: parent.width
Layout.fillHeight: true
UM.RecolorImage
{
id: downloadsIcon
width: UM.Theme.getSize("card_tiny_icon").width
height: UM.Theme.getSize("card_tiny_icon").height
source: UM.Theme.getIcon("Download")
color: UM.Theme.getColor("text")
}
Label
{
anchors.verticalCenter: downloadsIcon.verticalCenter
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
text: packageData.downloadCount
}
}
}
}
Column
{
id: extendedDescription
width: parent.width
padding: UM.Theme.getSize("default_margin").width
topPadding: 0
spacing: UM.Theme.getSize("default_margin").height
Label
{
width: parent.width - parent.padding * 2
text: catalog.i18nc("@header", "Description")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
{
width: parent.width - parent.padding * 2
text: packageData.formattedDescription
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
wrapMode: Text.Wrap
textFormat: Text.RichText
onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
}
Column //Separate column to have no spacing between compatible printers.
{
id: compatiblePrinterColumn
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible printers")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Repeater
{
model: packageData.compatiblePrinters
Label
{
width: compatiblePrinterColumn.width
text: modelData
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Label
{
width: parent.width
visible: packageData.compatiblePrinters.length == 0
text: "(" + catalog.i18nc("@info", "No compatibility information") + ")"
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Column
{
id: compatibleSupportMaterialColumn
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible support materials")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Repeater
{
model: packageData.compatibleSupportMaterials
Label
{
width: compatibleSupportMaterialColumn.width
text: modelData
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Label
{
width: parent.width
visible: packageData.compatibleSupportMaterials.length == 0
text: "(" + catalog.i18nc("@info No materials", "None") + ")"
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Column
{
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Compatible with Material Station")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
{
width: parent.width
text: packageData.isCompatibleMaterialStation ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Column
{
width: parent.width - parent.padding * 2
visible: packageData.packageType === "material"
spacing: 0
Label
{
width: parent.width
text: catalog.i18nc("@header", "Optimized for Air Manager")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
Label
{
width: parent.width
text: packageData.isCompatibleAirManager ? catalog.i18nc("@info", "Yes") : catalog.i18nc("@info", "No")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
Row
{
id: externalButtonRow
anchors.horizontalCenter: parent.horizontalCenter
spacing: UM.Theme.getSize("narrow_margin").width
Cura.SecondaryButton
{
text: packageData.packageType === "plugin" ? catalog.i18nc("@button", "Visit plug-in website") : catalog.i18nc("@button", "Website")
iconSource: UM.Theme.getIcon("Globe")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.packageInfoUrl)
}
Cura.SecondaryButton
{
visible: packageData.packageType === "material"
text: catalog.i18nc("@button", "Buy spool")
iconSource: UM.Theme.getIcon("ShoppingCart")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.whereToBuy)
}
Cura.SecondaryButton
{
visible: packageData.packageType === "material"
text: catalog.i18nc("@button", "Safety datasheet")
iconSource: UM.Theme.getIcon("Warning")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.safetyDataSheet)
}
Cura.SecondaryButton
{
visible: packageData.packageType === "material"
text: catalog.i18nc("@button", "Technical datasheet")
iconSource: UM.Theme.getIcon("DocumentFilled")
outlineColor: "transparent"
onClicked: Qt.openUrlExternally(packageData.technicalDataSheet)
}
}
}
}
FontMetrics
{
id: fontMetrics
font: UM.Theme.getFont("default")
}
}

View File

@ -0,0 +1,33 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import UM 1.0 as UM
TabButton
{
property string pageTitle
padding: UM.Theme.getSize("narrow_margin").width
horizontalPadding: UM.Theme.getSize("default_margin").width
hoverEnabled: true
property color inactiveBackgroundColor : hovered ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("detail_background")
property color activeBackgroundColor : UM.Theme.getColor("main_background")
background: Rectangle
{
anchors.fill: parent
color: parent.checked ? activeBackgroundColor : inactiveBackgroundColor
border.color: UM.Theme.getColor("detail_background")
border.width: UM.Theme.getSize("thick_lining").width
}
contentItem: Label
{
text: parent.text
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
width: contentWidth
anchors.centerIn: parent
}
}

View File

@ -0,0 +1,232 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import UM 1.4 as UM
ListView
{
id: packages
width: parent.width
property string pageTitle
property var selectedPackage
property string searchInBrowserUrl
property bool bannerVisible
property var bannerIcon
property string bannerText
property string bannerReadMoreUrl
property var onRemoveBanner
property bool packagesManageableInListView
clip: true
Component.onCompleted: model.updatePackages()
Component.onDestruction: model.cleanUpAPIRequest()
spacing: UM.Theme.getSize("default_margin").height
section.property: "package.sectionTitle"
section.delegate: Rectangle
{
width: packages.width
height: sectionHeaderText.height + UM.Theme.getSize("default_margin").height
color: UM.Theme.getColor("detail_background")
Label
{
id: sectionHeaderText
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
text: section
font: UM.Theme.getFont("large")
color: UM.Theme.getColor("text")
}
}
ScrollBar.vertical: ScrollBar
{
// Vertical ScrollBar, styled similarly to the scrollBar in the settings panel
id: verticalScrollBar
visible: packages.contentHeight > packages.height
background: Item{}
contentItem: Rectangle
{
id: scrollViewHandle
implicitWidth: UM.Theme.getSize("scrollbar").width
radius: Math.round(implicitWidth / 2)
color: verticalScrollBar.pressed ? UM.Theme.getColor("scrollbar_handle_down") : verticalScrollBar.hovered ? UM.Theme.getColor("scrollbar_handle_hover") : UM.Theme.getColor("scrollbar_handle")
Behavior on color { ColorAnimation { duration: 50; } }
}
}
delegate: MouseArea
{
id: cardMouseArea
width: parent ? parent.width : 0
height: childrenRect.height
hoverEnabled: true
onClicked:
{
packages.selectedPackage = model.package;
contextStack.push(packageDetailsComponent);
}
PackageCard
{
manageableInListView: packages.packagesManageableInListView
packageData: model.package
width: parent.width - UM.Theme.getSize("default_margin").width - UM.Theme.getSize("narrow_margin").width
color: cardMouseArea.containsMouse ? UM.Theme.getColor("action_button_hovered") : UM.Theme.getColor("main_background")
}
}
Component
{
id: packageDetailsComponent
PackageDetails
{
packageData: packages.selectedPackage
title: packages.pageTitle
}
}
//Wrapper item to add spacing between content and footer.
footer: Item
{
width: parent.width - UM.Theme.getSize("default_margin").width - UM.Theme.getSize("narrow_margin").width
height: model.hasFooter || packages.model.errorMessage != "" ? UM.Theme.getSize("card").height + packages.spacing : 0
visible: model.hasFooter || packages.model.errorMessage != ""
Button
{
id: loadMoreButton
width: parent.width
height: UM.Theme.getSize("card").height
anchors.bottom: parent.bottom
enabled: packages.model.hasMore && !packages.model.isLoading || packages.model.errorMessage != ""
onClicked: packages.model.updatePackages() //Load next page in plug-in list.
background: Rectangle
{
anchors.fill: parent
radius: UM.Theme.getSize("default_radius").width
color: UM.Theme.getColor("main_background")
}
Row
{
anchors.centerIn: parent
spacing: UM.Theme.getSize("thin_margin").width
states:
[
State
{
name: "Error"
when: packages.model.errorMessage != ""
PropertyChanges
{
target: errorIcon
visible: true
}
PropertyChanges
{
target: loadMoreIcon
visible: false
}
PropertyChanges
{
target: loadMoreLabel
text: catalog.i18nc("@button", "Failed to load packages:") + " " + packages.model.errorMessage + "\n" + catalog.i18nc("@button", "Retry?")
}
},
State
{
name: "Loading"
when: packages.model.isLoading
PropertyChanges
{
target: loadMoreIcon
source: UM.Theme.getIcon("ArrowDoubleCircleRight")
color: UM.Theme.getColor("action_button_disabled_text")
}
PropertyChanges
{
target: loadMoreLabel
text: catalog.i18nc("@button", "Loading")
color: UM.Theme.getColor("action_button_disabled_text")
}
},
State
{
name: "LastPage"
when: !packages.model.hasMore
PropertyChanges
{
target: loadMoreIcon
visible: false
}
PropertyChanges
{
target: loadMoreLabel
text: packages.model.count > 0 ? catalog.i18nc("@message", "No more results to load") : catalog.i18nc("@message", "No results found with current filter")
color: UM.Theme.getColor("action_button_disabled_text")
}
}
]
Item
{
width: (errorIcon.visible || loadMoreIcon.visible) ? UM.Theme.getSize("small_button_icon").width : 0
height: UM.Theme.getSize("small_button_icon").height
anchors.verticalCenter: loadMoreLabel.verticalCenter
UM.StatusIcon
{
id: errorIcon
anchors.fill: parent
status: UM.StatusIcon.Status.ERROR
visible: false
}
UM.RecolorImage
{
id: loadMoreIcon
anchors.fill: parent
source: UM.Theme.getIcon("ArrowDown")
color: UM.Theme.getColor("secondary_button_text")
RotationAnimator
{
target: loadMoreIcon
from: 0
to: 360
duration: 1000
loops: Animation.Infinite
running: packages.model.isLoading
alwaysRunToEnd: true
}
}
}
Label
{
id: loadMoreLabel
text: catalog.i18nc("@button", "Load more")
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("secondary_button_text")
}
}
}
}
}

View File

@ -0,0 +1,22 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import UM 1.4 as UM
Packages
{
pageTitle: catalog.i18nc("@header", "Install Plugins")
bannerVisible: UM.Preferences.getValue("cura/market_place_show_plugin_banner")
bannerIcon: UM.Theme.getIcon("Shop")
bannerText: catalog.i18nc("@text", "Streamline your workflow and customize your Ultimaker Cura experience with plugins contributed by our amazing community of users.")
bannerReadMoreUrl: "" // TODO add when support page is ready
onRemoveBanner: function() {
UM.Preferences.setValue("cura/market_place_show_plugin_banner", false)
bannerVisible = false;
}
searchInBrowserUrl: "https://marketplace.ultimaker.com/app/cura/plugins?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search-plugins-browser"
packagesManageableInListView: false
model: manager.PluginPackageList
}

View File

@ -0,0 +1,45 @@
// Copyright (c) 2021 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.1
import UM 1.6 as UM
import Cura 1.6 as Cura
Control
{
implicitWidth: UM.Theme.getSize("card_tiny_icon").width
implicitHeight: UM.Theme.getSize("card_tiny_icon").height
Cura.ToolTip
{
tooltipText:
{
switch(packageData.packageType)
{
case "plugin": return catalog.i18nc("@info", "Ultimaker Verified Plug-in");
case "material": return catalog.i18nc("@info", "Ultimaker Certified Material");
default: return catalog.i18nc("@info", "Ultimaker Verified Package");
}
}
visible: parent.hovered
targetPoint: Qt.point(0, Math.round(parent.y + parent.height / 4))
}
Rectangle
{
anchors.fill: parent
color: UM.Theme.getColor("action_button_hovered")
radius: width
UM.RecolorImage
{
anchors.fill: parent
color: UM.Theme.getColor("primary")
source: packageData.packageType == "plugin" ? UM.Theme.getIcon("CheckCircle") : UM.Theme.getIcon("Certified")
}
}
//NOTE: Can we link to something here? (Probably a static link explaining what verified is):
// onClicked: Qt.openUrlExternally( XXXXXX )
}

View File

@ -6,7 +6,9 @@ import QtQuick.Layouts 1.1
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import UM 1.2 as UM
import UM 1.5 as UM
import Cura 1.0 as Cura
UM.TooltipArea
{
@ -16,7 +18,7 @@ UM.TooltipArea
width: childrenRect.width;
height: childrenRect.height;
CheckBox
UM.CheckBox
{
id: check

View File

@ -62,7 +62,7 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
all_instances = settings.findInstances()
visibility_changed = False # Flag to check if at the end the signal needs to be emitted
# Remove all instances that are not in visibility list
# Remove all SettingInstances that are not in visibility list
for instance in all_instances:
# exceptionally skip setting
if instance.definition.key in self._skip_reset_setting_set:
@ -71,29 +71,30 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
settings.removeInstance(instance.definition.key)
visibility_changed = True
# Add all instances that are not added, but are in visibility list
# Add all SettingInstances that are not added, but are in visibility list
for item in visible:
if settings.getInstance(item) is not None: # Setting was added already.
continue
definition = self._stack.getSettingDefinition(item)
if not definition:
Logger.log("w", f"Unable to add instance ({item}) to per-object visibility because we couldn't find the matching definition.")
Logger.log("w", f"Unable to add SettingInstance ({item}) to the per-object visibility because we couldn't find the matching SettingDefinition.")
continue
new_instance = SettingInstance(definition, settings)
stack_nr = -1
stack = None
# Check from what stack we should copy the raw property of the setting from.
# Check from what ContainerStack we should copy the raw property of the setting from.
if self._stack.getProperty("machine_extruder_count", "value") > 1:
if definition.limit_to_extruder != "-1":
# A limit to extruder function was set and it's a multi extrusion machine. Check what stack we do need to use.
# A limit_to_extruder function was set and it's a multi extrusion machine. Check what stack we
# do need to use.
stack_nr = str(int(round(float(self._stack.getProperty(item, "limit_to_extruder")))))
# Check if the found stack_number is in the extruder list of extruders.
if stack_nr not in ExtruderManager.getInstance().extruderIds and self._stack.getProperty("extruder_nr", "value") is not None:
stack_nr = -1
# Use the found stack number to get the right stack to copy the value from.
# Use the found stack_number to get the right ContainerStack to copy the value from.
if stack_nr in ExtruderManager.getInstance().extruderIds:
stack = ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0]
else:

View File

@ -2,7 +2,7 @@ import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Controls.Styles 1.2
import UM 1.2 as UM
import UM 1.5 as UM
import Cura 1.0 as Cura
import ".."
@ -57,13 +57,14 @@ UM.Dialog
onTextChanged: settingPickDialog.updateFilter()
}
CheckBox
UM.CheckBox
{
id: toggleShowAll
anchors
{
top: parent.top
right: parent.right
verticalCenter: filterInput.verticalCenter
}
text: catalog.i18nc("@label:checkbox", "Show all")
}

View File

@ -193,6 +193,8 @@ class PostProcessingPlugin(QObject, Extension):
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name,
file_path)
if spec is None:
continue
loaded_script = importlib.util.module_from_spec(spec)
if spec.loader is None:
continue

View File

@ -527,10 +527,8 @@ UM.Dialog
visible: activeScriptsList.count > 0
anchors
{
top: parent.top
right: parent.right
rightMargin: (-0.5 * width) | 0
topMargin: (-0.5 * height) | 0
horizontalCenter: parent.right
verticalCenter: parent.top
}
labelText: activeScriptsList.count

View File

@ -1,7 +1,7 @@
# Cura PostProcessingPlugin
# Author: Mathias Lyngklip Kjeldgaard, Alexander Gee, Kimmo Toivanen
# Author: Mathias Lyngklip Kjeldgaard, Alexander Gee, Kimmo Toivanen, Inigo Martinez
# Date: July 31, 2019
# Modified: Okt 22, 2020
# Modified: Nov 30, 2021
# Description: This plugin displays progress on the LCD. It can output the estimated time remaining and the completion percentage.
@ -37,7 +37,8 @@ class DisplayProgressOnLCD(Script):
"type": "enum",
"options": {
"m117":"M117 - All printers",
"m73":"M73 - Prusa, Marlin 2"
"m73":"M73 - Prusa, Marlin 2",
"m118":"M118 - Octoprint"
},
"enabled": "time_remaining",
"default_value": "m117"
@ -77,6 +78,10 @@ class DisplayProgressOnLCD(Script):
current_time_string = "{:d}h{:02d}m{:02d}s".format(int(h), int(m), int(s))
# And now insert that into the GCODE
lines.insert(line_index, "M117 Time Left {}".format(current_time_string))
elif mode == "m118":
current_time_string = "{:d}h{:02d}m{:02d}s".format(int(h), int(m), int(s))
# And now insert that into the GCODE
lines.insert(line_index, "M118 A1 P0 action:notification Time Left {}".format(current_time_string))
else:
mins = int(60 * h + m + s / 30)
lines.insert(line_index, "M73 R{}".format(mins))
@ -107,7 +112,10 @@ class DisplayProgressOnLCD(Script):
if output_percentage:
# Emit 0 percent to sure Marlin knows we are overriding the completion percentage
lines.insert(line_index, "M73 P0")
if output_time_method == "m118":
lines.insert(line_index, "M118 A1 P0 action:notification Data Left 0/100")
else:
lines.insert(line_index, "M73 P0")
elif line.startswith(";TIME_ELAPSED:"):
# We've found one of the time elapsed values which are added at the end of layers
@ -178,7 +186,10 @@ class DisplayProgressOnLCD(Script):
output = min(percentage + previous_layer_end_percentage, 100)
# Now insert the sanitized percentage into the GCODE
lines.insert(percentage_line_index, "M73 P{}".format(output))
if output_time_method == "m118":
lines.insert(percentage_line_index, "M118 A1 P0 action:notification Data Left {}/100".format(output))
else:
lines.insert(percentage_line_index, "M73 P{}".format(output))
previous_layer_end_percentage = layer_end_percentage

View File

@ -11,7 +11,6 @@ try:
except ImportError:
pass
from typing import Optional
import os
class SentryLogger(LogOutput):

View File

@ -1,15 +0,0 @@
# Copyright (c) 2018 Ultimaker B.V.
# Toolbox is released under the terms of the LGPLv3 or higher.
from .src import Toolbox
from .src.CloudSync.SyncOrchestrator import SyncOrchestrator
def getMetaData():
return {}
def register(app):
return {
"extension": [Toolbox.Toolbox(app), SyncOrchestrator(app)]
}

View File

@ -1,7 +0,0 @@
{
"name": "Toolbox",
"author": "Ultimaker B.V.",
"version": "1.0.1",
"api": 7,
"description": "Find, manage and install new Cura packages."
}

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<path d="M19,3H5C3.3,3,2,4.3,2,6v3c0,1.5,0.8,2.7,2,3.4V22h16v-9.6c1.2-0.7,2-2,2-3.4V6C22,4.3,20.7,3,19,3z
M10,5h4v4c0,1.1-0.9,2-2,2s-2-0.9-2-2V5z M4,9V5h4v4c0,1.1-0.9,2-2,2S4,10.1,4,9z M18,20h-4v-5h-4v5H6v-7c1.2,0,2.3-0.5,3-1.4
c0.7,0.8,1.8,1.4,3,1.4s2.3-0.5,3-1.4c0.7,0.8,1.8,1.4,3,1.4V20z M20,9c0,1.1-0.9,2-2,2s-2-0.9-2-2V5h4V9z" />
</svg>

Before

Width:  |  Height:  |  Size: 458 B

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_3" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<path d="M0,512h512V0L0,512z M440.4,318.3L331.2,431.6c-1.4,1.4-2.7,2-4.8,2c-2,0-3.4-0.7-4.8-2l-53.3-57.3l-1.4-2
c-1.4-1.4-2-3.4-2-4.8c0-1.4,0.7-3.4,2-4.8l9.6-9.6c2.7-2.7,6.8-2.7,9.6,0l0.7,0.7l37.6,40.2c1.4,1.4,3.4,1.4,4.8,0l91.4-94.9h0.7
c2.7-2.7,6.8-2.7,9.6,0l9.5,9.6C443.1,311.5,443.1,315.6,440.4,318.3z"/>
</svg>

Before

Width:  |  Height:  |  Size: 667 B

View File

@ -1,112 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
// Main window for the Toolbox
import QtQuick 2.2
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import UM 1.1 as UM
import "./pages"
import "./dialogs"
import "./components"
Window
{
id: base
property var selection: null
title: catalog.i18nc("@title", "Marketplace")
modality: Qt.ApplicationModal
flags: Qt.Dialog | Qt.CustomizeWindowHint | Qt.WindowTitleHint | Qt.WindowCloseButtonHint
width: UM.Theme.getSize("large_popup_dialog").width
height: UM.Theme.getSize("large_popup_dialog").height
minimumWidth: width
maximumWidth: minimumWidth
minimumHeight: height
maximumHeight: minimumHeight
color: UM.Theme.getColor("main_background")
UM.I18nCatalog
{
id: catalog
name: "cura"
}
Item
{
anchors.fill: parent
WelcomePage
{
visible: toolbox.viewPage === "welcome"
}
ToolboxHeader
{
id: header
visible: toolbox.viewPage !== "welcome"
}
Item
{
id: mainView
width: parent.width
z: parent.z - 1
anchors
{
top: header.bottom
bottom: footer.top
}
// TODO: This could be improved using viewFilter instead of viewCategory
ToolboxLoadingPage
{
id: viewLoading
visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "loading"
}
ToolboxErrorPage
{
id: viewErrored
visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "errored"
}
ToolboxDownloadsPage
{
id: viewDownloads
visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "overview"
}
ToolboxDetailPage
{
id: viewDetail
visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "detail"
}
ToolboxAuthorPage
{
id: viewAuthor
visible: toolbox.viewCategory !== "installed" && toolbox.viewPage === "author"
}
ToolboxInstalledPage
{
id: installedPluginList
visible: toolbox.viewCategory === "installed"
}
}
ToolboxFooter
{
id: footer
visible: toolbox.restartRequired
height: visible ? UM.Theme.getSize("toolbox_footer").height : 0
}
Connections
{
target: toolbox
function onShowLicenseDialog() { licenseDialog.show() }
function onCloseLicenseDialog() { licenseDialog.close() }
}
ToolboxLicenseDialog
{
id: licenseDialog
}
}
}

View File

@ -1,29 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
ButtonStyle
{
background: Rectangle
{
implicitWidth: UM.Theme.getSize("toolbox_action_button").width
implicitHeight: UM.Theme.getSize("toolbox_action_button").height
color: "transparent"
border
{
width: UM.Theme.getSize("default_lining").width
color: UM.Theme.getColor("lining")
}
}
label: Label
{
text: control.text
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
}
}

View File

@ -1,84 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
Item
{
id: sidebar
height: parent.height
width: UM.Theme.getSize("toolbox_back_column").width
anchors
{
top: parent.top
left: parent.left
topMargin: UM.Theme.getSize("wide_margin").height
leftMargin: UM.Theme.getSize("default_margin").width
rightMargin: UM.Theme.getSize("default_margin").width
}
Button
{
id: button
text: catalog.i18nc("@action:button", "Back")
enabled: !toolbox.isDownloading
UM.RecolorImage
{
id: backArrow
anchors
{
verticalCenter: parent.verticalCenter
left: parent.left
rightMargin: UM.Theme.getSize("default_margin").width
}
width: UM.Theme.getSize("standard_arrow").width
height: UM.Theme.getSize("standard_arrow").height
sourceSize
{
width: width
height: height
}
color: button.enabled ? (button.hovered ? UM.Theme.getColor("primary") : UM.Theme.getColor("text")) : UM.Theme.getColor("text_inactive")
source: UM.Theme.getIcon("ChevronSingleLeft")
}
width: UM.Theme.getSize("toolbox_back_button").width
height: UM.Theme.getSize("toolbox_back_button").height
onClicked:
{
toolbox.viewPage = "overview"
if (toolbox.viewCategory == "material")
{
toolbox.filterModelByProp("authors", "package_types", "material")
}
else if (toolbox.viewCategory == "plugin")
{
toolbox.filterModelByProp("packages", "type", "plugin")
}
}
style: ButtonStyle
{
background: Rectangle
{
color: "transparent"
}
label: Label
{
id: labelStyle
text: control.text
color: control.enabled ? (control.hovered ? UM.Theme.getColor("primary") : UM.Theme.getColor("text")) : UM.Theme.getColor("text_inactive")
font: UM.Theme.getFont("medium_bold")
horizontalAlignment: Text.AlignLeft
anchors
{
left: parent.left
leftMargin: UM.Theme.getSize("default_margin").width
}
width: control.width
renderType: Text.NativeRendering
}
}
}
}

View File

@ -1,209 +0,0 @@
// Copyright (c) 2019 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
import UM 1.5 as UM
Item
{
id: base
property var packageData
property var technicalDataSheetUrl: packageData.links.technicalDataSheet
property var safetyDataSheetUrl: packageData.links.safetyDataSheet
property var printingGuidelinesUrl: packageData.links.printingGuidelines
property var materialWebsiteUrl: packageData.links.website
height: childrenRect.height
onVisibleChanged: packageData.type === "material" && (compatibilityItem.visible || dataSheetLinks.visible)
Column
{
id: compatibilityItem
visible: packageData.has_configs
width: parent.width
// This is a bit of a hack, but the whole QML is pretty messy right now. This needs a big overhaul.
height: visible ? heading.height + table.height: 0
Label
{
id: heading
width: parent.width
text: catalog.i18nc("@label", "Compatibility")
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
}
TableView
{
id: table
width: parent.width
frameVisible: false
// Workaround for scroll issues (QTBUG-49652)
flickableItem.interactive: false
Component.onCompleted:
{
for (var i = 0; i < flickableItem.children.length; ++i)
{
flickableItem.children[i].enabled = false
}
}
selectionMode: 0
model: packageData.supported_configs
headerDelegate: Rectangle
{
color: UM.Theme.getColor("main_background")
height: UM.Theme.getSize("toolbox_chart_row").height
Label
{
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
text: styleData.value || ""
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default_bold")
renderType: Text.NativeRendering
}
Rectangle
{
anchors.bottom: parent.bottom
height: UM.Theme.getSize("default_lining").height
width: parent.width
color: "black"
}
}
rowDelegate: Item
{
height: UM.Theme.getSize("toolbox_chart_row").height
Label
{
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
text: styleData.value || ""
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
}
}
itemDelegate: Item
{
height: UM.Theme.getSize("toolbox_chart_row").height
Label
{
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
text: styleData.value || ""
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
}
}
Component
{
id: columnTextDelegate
Label
{
anchors.fill: parent
verticalAlignment: Text.AlignVCenter
text: styleData.value || ""
elide: Text.ElideRight
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
}
}
TableViewColumn
{
role: "machine"
title: catalog.i18nc("@label:table_header", "Machine")
width: Math.floor(table.width * 0.25)
delegate: columnTextDelegate
}
TableViewColumn
{
role: "print_core"
title: "Print Core" //This term should not be translated.
width: Math.floor(table.width * 0.2)
}
TableViewColumn
{
role: "build_plate"
title: catalog.i18nc("@label:table_header", "Build Plate")
width: Math.floor(table.width * 0.225)
}
TableViewColumn
{
role: "support_material"
title: catalog.i18nc("@label:table_header", "Support")
width: Math.floor(table.width * 0.225)
}
TableViewColumn
{
role: "quality"
title: catalog.i18nc("@label:table_header", "Quality")
width: Math.floor(table.width * 0.1)
}
}
}
Label
{
id: dataSheetLinks
anchors.top: compatibilityItem.bottom
anchors.topMargin: UM.Theme.getSize("narrow_margin").height
visible: base.technicalDataSheetUrl !== undefined ||
base.safetyDataSheetUrl !== undefined ||
base.printingGuidelinesUrl !== undefined ||
base.materialWebsiteUrl !== undefined
text:
{
var result = ""
if (base.technicalDataSheetUrl !== undefined)
{
var tds_name = catalog.i18nc("@action:label", "Technical Data Sheet")
result += "<a href='%1'>%2</a>".arg(base.technicalDataSheetUrl).arg(tds_name)
}
if (base.safetyDataSheetUrl !== undefined)
{
if (result.length > 0)
{
result += "<br/>"
}
var sds_name = catalog.i18nc("@action:label", "Safety Data Sheet")
result += "<a href='%1'>%2</a>".arg(base.safetyDataSheetUrl).arg(sds_name)
}
if (base.printingGuidelinesUrl !== undefined)
{
if (result.length > 0)
{
result += "<br/>"
}
var pg_name = catalog.i18nc("@action:label", "Printing Guidelines")
result += "<a href='%1'>%2</a>".arg(base.printingGuidelinesUrl).arg(pg_name)
}
if (base.materialWebsiteUrl !== undefined)
{
if (result.length > 0)
{
result += "<br/>"
}
var pg_name = catalog.i18nc("@action:label", "Website")
result += "<a href='%1'>%2</a>".arg(base.materialWebsiteUrl).arg(pg_name)
}
return result
}
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
renderType: Text.NativeRendering
}
}

View File

@ -1,43 +0,0 @@
// Copyright (c) 2019 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.1 as UM
Item
{
id: detailList
ScrollView
{
clip: true
anchors.fill: detailList
Column
{
anchors
{
right: parent.right
topMargin: UM.Theme.getSize("wide_margin").height
bottomMargin: UM.Theme.getSize("wide_margin").height
top: parent.top
}
height: childrenRect.height + 2 * UM.Theme.getSize("wide_margin").height
spacing: UM.Theme.getSize("default_margin").height
Repeater
{
model: toolbox.packagesModel
delegate: Loader
{
// FIXME: When using asynchronous loading, on Mac and Windows, the tile may fail to load complete,
// leaving an empty space below the title part. We turn it off for now to make it work on Mac and
// Windows.
// Can be related to this QT bug: https://bugreports.qt.io/browse/QTBUG-50992
asynchronous: false
source: "ToolboxDetailTile.qml"
}
}
}
}
}

View File

@ -1,74 +0,0 @@
// Copyright (c) 2019 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.1 as UM
Item
{
id: tile
width: detailList.width - UM.Theme.getSize("wide_margin").width
height: normalData.height + 2 * UM.Theme.getSize("wide_margin").height
Column
{
id: normalData
anchors
{
top: parent.top
left: parent.left
right: controls.left
rightMargin: UM.Theme.getSize("wide_margin").width
}
Label
{
width: parent.width
height: UM.Theme.getSize("toolbox_property_label").height
text: model.name
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("medium_bold")
renderType: Text.NativeRendering
}
Label
{
width: parent.width
text: model.description
maximumLineCount: 25
elide: Text.ElideRight
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
}
ToolboxCompatibilityChart
{
width: parent.width
packageData: model
}
}
ToolboxDetailTileActions
{
id: controls
anchors.right: tile.right
anchors.top: tile.top
width: childrenRect.width
height: childrenRect.height
packageData: model
}
Rectangle
{
color: UM.Theme.getColor("lining")
width: tile.width
height: UM.Theme.getSize("default_lining").height
anchors.top: normalData.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height + UM.Theme.getSize("wide_margin").height //Normal margin for spacing after chart, wide margin between items.
}
}

View File

@ -1,121 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.5 as UM
import Cura 1.1 as Cura
Column
{
property bool installed: toolbox.isInstalled(model.id)
property bool canUpdate: CuraApplication.getPackageManager().packagesWithUpdate.indexOf(model.id) != -1
property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
property var packageData
width: UM.Theme.getSize("toolbox_action_button").width
spacing: UM.Theme.getSize("narrow_margin").height
Item
{
width: installButton.width
height: installButton.height
ToolboxProgressButton
{
id: installButton
active: toolbox.isDownloading && toolbox.activePackage == model
onReadyAction:
{
toolbox.activePackage = model
toolbox.startDownload(model.download_url)
}
onActiveAction: toolbox.cancelDownload()
// Don't allow installing while another download is running
enabled: installed || (!(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired)
opacity: enabled ? 1.0 : 0.5
visible: !updateButton.visible && !installed // Don't show when the update button is visible
}
Cura.SecondaryButton
{
id: installedButton
visible: installed
onClicked: toolbox.viewCategory = "installed"
text: catalog.i18nc("@action:button", "Installed")
fixedWidthMode: true
width: installButton.width
height: installButton.height
}
}
Label
{
wrapMode: Text.WordWrap
text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Log in</a> is required to install or update")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
visible: loginRequired
width: installButton.width
renderType: Text.NativeRendering
MouseArea
{
anchors.fill: parent
onClicked: Cura.API.account.login()
}
}
Label
{
property var whereToBuyUrl:
{
var pg_name = "whereToBuy"
return (pg_name in packageData.links) ? packageData.links[pg_name] : undefined
}
renderType: Text.NativeRendering
text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Buy material spools</a>")
linkColor: UM.Theme.getColor("text_link")
visible: whereToBuyUrl != undefined
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
MouseArea
{
anchors.fill: parent
onClicked: UM.UrlUtil.openUrl(parent.whereToBuyUrl, ["https", "http"])
}
}
ToolboxProgressButton
{
id: updateButton
active: toolbox.isDownloading && toolbox.activePackage == model
readyLabel: catalog.i18nc("@action:button", "Update")
activeLabel: catalog.i18nc("@action:button", "Updating")
completeLabel: catalog.i18nc("@action:button", "Updated")
onReadyAction:
{
toolbox.activePackage = model
toolbox.update(model.id)
}
onActiveAction: toolbox.cancelDownload()
// Don't allow installing while another download is running
enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired
opacity: enabled ? 1.0 : 0.5
visible: canUpdate
}
Connections
{
target: toolbox
function onInstallChanged() { installed = toolbox.isInstalled(model.id) }
function onFilterChanged()
{
installed = toolbox.isInstalled(model.id)
}
}
}

View File

@ -1,45 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.1 as UM
Column
{
property var heading: ""
property var model
id: gridArea
height: childrenRect.height + 2 * padding
width: parent.width
spacing: UM.Theme.getSize("default_margin").height
padding: UM.Theme.getSize("wide_margin").height
Label
{
id: heading
text: gridArea.heading
width: parent.width
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("large")
renderType: Text.NativeRendering
}
Grid
{
id: grid
width: parent.width - 2 * parent.padding
columns: 2
columnSpacing: UM.Theme.getSize("default_margin").height
rowSpacing: UM.Theme.getSize("default_margin").width
Repeater
{
model: gridArea.model
delegate: Loader
{
asynchronous: true
width: Math.round((grid.width - (grid.columns - 1) * grid.columnSpacing) / grid.columns)
height: UM.Theme.getSize("toolbox_thumbnail_small").height
source: "ToolboxDownloadsGridTile.qml"
}
}
}
}

View File

@ -1,125 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.3
import UM 1.1 as UM
import Cura 1.1 as Cura
Item
{
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.AlignmentFlag.AlignTop | Qt.AlignmentFlag.AlignLeft
MouseArea
{
anchors.fill: parent
hoverEnabled: true
onEntered: thumbnail.border.color = UM.Theme.getColor("primary")
onExited: thumbnail.border.color = UM.Theme.getColor("lining")
onClicked:
{
base.selection = model
switch(toolbox.viewCategory)
{
case "material":
// If model has a type, it must be a package
if (model.type !== undefined)
{
toolbox.viewPage = "detail"
toolbox.filterModelByProp("packages", "id", model.id)
}
else
{
toolbox.viewPage = "author"
toolbox.setFilters("packages", {
"author_id": model.id,
"type": "material"
})
}
break
default:
toolbox.viewPage = "detail"
toolbox.filterModelByProp("packages", "id", model.id)
break
}
}
}
Rectangle
{
id: thumbnail
width: UM.Theme.getSize("toolbox_thumbnail_small").width
height: UM.Theme.getSize("toolbox_thumbnail_small").height
color: UM.Theme.getColor("main_background")
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
Image
{
anchors.centerIn: parent
width: UM.Theme.getSize("toolbox_thumbnail_small").width - UM.Theme.getSize("wide_margin").width
height: UM.Theme.getSize("toolbox_thumbnail_small").height - UM.Theme.getSize("wide_margin").width
sourceSize.width: width
sourceSize.height: height
fillMode: Image.PreserveAspectFit
source: model.icon_url || "../../images/placeholder.svg"
mipmap: true
}
UM.RecolorImage
{
width: (parent.width * 0.4) | 0
height: (parent.height * 0.4) | 0
anchors
{
bottom: parent.bottom
right: parent.right
}
sourceSize.height: height
visible: installedPackages != 0
color: (installedPackages >= packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border")
source: "../../images/installed_check.svg"
}
}
Item
{
anchors
{
left: thumbnail.right
leftMargin: Math.floor(UM.Theme.getSize("narrow_margin").width)
right: parent.right
top: parent.top
bottom: parent.bottom
}
Label
{
id: name
text: model.name
width: parent.width
elide: Text.ElideRight
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default_bold")
}
Label
{
id: info
text: model.description
elide: Text.ElideRight
width: parent.width
wrapMode: Text.WordWrap
color: UM.Theme.getColor("text")
font: UM.Theme.getFont("default")
anchors.top: name.bottom
anchors.bottom: parent.bottom
verticalAlignment: Text.AlignVCenter
maximumLineCount: 2
}
}
}

View File

@ -1,84 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
Rectangle
{
color: UM.Theme.getColor("toolbox_premium_packages_background")
height: childrenRect.height
width: parent.width
Column
{
height: childrenRect.height + 2 * padding
spacing: UM.Theme.getSize("default_margin").height
width: parent.width
padding: UM.Theme.getSize("wide_margin").height
Item
{
width: parent.width - parent.padding * 2
height: childrenRect.height
Label
{
id: heading
text: catalog.i18nc("@label", "Premium")
width: contentWidth
height: contentHeight
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("large")
renderType: Text.NativeRendering
}
UM.TooltipArea
{
width: childrenRect.width
height: childrenRect.height
anchors.right: parent.right
text: catalog.i18nc("@info:tooltip", "Go to Web Marketplace")
Label
{
text: "<a href='%2'>".arg(toolbox.getWebMarketplaceUrl("materials") + "?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search") + catalog.i18nc("@label", "Search materials") + "</a>"
width: contentWidth
height: contentHeight
horizontalAlignment: Text.AlignRight
font: UM.Theme.getFont("default")
renderType: Text.NativeRendering
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
visible: toolbox.viewCategory === "material"
}
}
}
Grid
{
height: childrenRect.height
spacing: UM.Theme.getSize("wide_margin").width
columns: 3
anchors.horizontalCenter: parent.horizontalCenter
Repeater
{
model:
{
if (toolbox.viewCategory == "plugin")
{
return toolbox.pluginsShowcaseModel
}
if (toolbox.viewCategory == "material")
{
return toolbox.materialsShowcaseModel
}
}
delegate: Loader
{
asynchronous: true
source: "ToolboxDownloadsShowcaseTile.qml"
}
}
}
}
}

View File

@ -1,114 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
Rectangle
{
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)
height: thumbnail.height + packageName.height + UM.Theme.getSize("default_margin").width
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("lining")
color: UM.Theme.getColor("main_background")
Image
{
id: thumbnail
height: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height
width: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height
sourceSize.height: height
sourceSize.width: width
fillMode: Image.PreserveAspectFit
source: model.icon_url || "../../images/placeholder.svg"
mipmap: true
anchors
{
top: parent.top
topMargin: UM.Theme.getSize("default_margin").height
horizontalCenter: parent.horizontalCenter
}
}
Label
{
id: packageName
text: model.name
anchors
{
horizontalCenter: parent.horizontalCenter
top: thumbnail.bottom
}
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
renderType: Text.NativeRendering
height: UM.Theme.getSize("toolbox_heading_label").height
width: parent.width - UM.Theme.getSize("default_margin").width
wrapMode: Text.WordWrap
elide: Text.ElideRight
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("text")
}
UM.RecolorImage
{
width: (parent.width * 0.20) | 0
height: width
anchors
{
bottom: bottomBorder.top
right: parent.right
}
visible: installedPackages != 0
color: (installedPackages >= packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border")
source: "../../images/installed_check.svg"
}
Rectangle
{
id: bottomBorder
color: UM.Theme.getColor("primary")
anchors.bottom: parent.bottom
width: parent.width
height: UM.Theme.getSize("toolbox_header_highlight").height
}
MouseArea
{
anchors.fill: parent
hoverEnabled: true
onEntered: tileBase.border.color = UM.Theme.getColor("primary")
onExited: tileBase.border.color = UM.Theme.getColor("lining")
onClicked:
{
base.selection = model
switch(toolbox.viewCategory)
{
case "material":
// If model has a type, it must be a package
if (model.type !== undefined)
{
toolbox.viewPage = "detail"
toolbox.filterModelByProp("packages", "id", model.id)
}
else
{
toolbox.viewPage = "author"
toolbox.setFilters("packages", {
"author_id": model.id,
"type": "material"
})
}
break
default:
toolbox.viewPage = "detail"
toolbox.filterModelByProp("packages", "id", model.id)
break
}
}
}
}

View File

@ -1,60 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.1 as UM
import Cura 1.0 as Cura
Item
{
id: footer
width: parent.width
anchors.bottom: parent.bottom
height: visible ? UM.Theme.getSize("toolbox_footer").height : 0
Label
{
text: catalog.i18nc("@info", "You will need to restart Cura before changes in packages have effect.")
color: UM.Theme.getColor("text")
height: UM.Theme.getSize("toolbox_footer_button").height
verticalAlignment: Text.AlignVCenter
wrapMode: Text.WordWrap
anchors
{
top: restartButton.top
left: parent.left
leftMargin: UM.Theme.getSize("wide_margin").width
right: restartButton.left
rightMargin: UM.Theme.getSize("default_margin").width
}
renderType: Text.NativeRendering
}
Cura.PrimaryButton
{
id: restartButton
anchors
{
top: parent.top
topMargin: UM.Theme.getSize("default_margin").height
right: parent.right
rightMargin: UM.Theme.getSize("wide_margin").width
}
height: UM.Theme.getSize("toolbox_footer_button").height
text: catalog.i18nc("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName)
onClicked:
{
base.hide()
toolbox.restart()
}
}
ToolboxShadow
{
visible: footer.visible
anchors.bottom: footer.top
reversed: true
}
}

View File

@ -1,112 +0,0 @@
// Copyright (c) 2020 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
import UM 1.4 as UM
import Cura 1.0 as Cura
Item
{
id: header
width: parent.width
height: UM.Theme.getSize("toolbox_header").height
Row
{
id: bar
spacing: UM.Theme.getSize("default_margin").width
height: childrenRect.height
width: childrenRect.width
anchors
{
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.isDownloading && toolbox.viewPage != "loading" && toolbox.viewPage != "errored"
onClicked:
{
toolbox.filterModelByProp("packages", "type", "plugin")
toolbox.viewCategory = "plugin"
toolbox.viewPage = "overview"
}
}
ToolboxTabButton
{
id: materialsTabButton
text: catalog.i18nc("@title:tab", "Materials")
active: toolbox.viewCategory == "material" && enabled
enabled: !toolbox.isDownloading && toolbox.viewPage != "loading" && toolbox.viewPage != "errored"
onClicked:
{
toolbox.filterModelByProp("authors", "package_types", "material")
toolbox.viewCategory = "material"
toolbox.viewPage = "overview"
}
}
ToolboxTabButton
{
id: installedTabButton
text: catalog.i18nc("@title:tab", "Installed")
active: toolbox.viewCategory == "installed"
enabled: !toolbox.isDownloading
onClicked: toolbox.viewCategory = "installed"
width: UM.Theme.getSize("toolbox_header_tab").width + marketplaceNotificationIcon.width - UM.Theme.getSize("default_margin").width
}
}
Cura.NotificationIcon
{
id: marketplaceNotificationIcon
visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0
anchors.right: bar.right
labelText:
{
const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length
return itemCount > 9 ? "9+" : itemCount
}
}
UM.TooltipArea
{
id: webMarketplaceButtonTooltipArea
width: childrenRect.width
height: parent.height
text: catalog.i18nc("@info:tooltip", "Go to Web Marketplace")
anchors
{
right: parent.right
rightMargin: UM.Theme.getSize("default_margin").width
verticalCenter: parent.verticalCenter
}
acceptedButtons: Qt.MouseButton.LeftButton
onClicked: Qt.openUrlExternally(toolbox.getWebMarketplaceUrl("plugins") + "?utm_source=cura&utm_medium=software&utm_campaign=marketplace-button")
UM.RecolorImage
{
id: cloudMarketplaceButton
source: "../../images/Shop.svg"
color: UM.Theme.getColor(webMarketplaceButtonTooltipArea.containsMouse ? "primary" : "text")
height: parent.height / 2
width: height
anchors.verticalCenter: parent.verticalCenter
sourceSize.width: width
sourceSize.height: height
}
}
ToolboxShadow
{
anchors.top: bar.bottom
}
}

View File

@ -1,123 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
Item
{
height: UM.Theme.getSize("toolbox_installed_tile").height
width: parent.width
property bool isEnabled: true
Rectangle
{
color: UM.Theme.getColor("lining")
width: parent.width
height: Math.floor(UM.Theme.getSize("default_lining").height)
anchors.bottom: parent.top
visible: index != 0
}
Row
{
id: tileRow
height: parent.height
width: parent.width
spacing: UM.Theme.getSize("default_margin").width
topPadding: UM.Theme.getSize("default_margin").height
CheckBox
{
id: disableButton
anchors.verticalCenter: pluginInfo.verticalCenter
checked: isEnabled
visible: model.type == "plugin"
width: visible ? UM.Theme.getSize("checkbox").width : 0
enabled: !toolbox.isDownloading
style: UM.Theme.styles.checkbox
onClicked: toolbox.isEnabled(model.id) ? toolbox.disable(model.id) : toolbox.enable(model.id)
}
Column
{
id: pluginInfo
topPadding: UM.Theme.getSize("narrow_margin").height
property var color: model.type === "plugin" && !isEnabled ? UM.Theme.getColor("lining") : UM.Theme.getColor("text")
width: Math.floor(tileRow.width - (authorInfo.width + pluginActions.width + 2 * tileRow.spacing + ((disableButton.visible) ? disableButton.width + tileRow.spacing : 0)))
Label
{
text: model.name
width: parent.width
maximumLineCount: 1
elide: Text.ElideRight
wrapMode: Text.WordWrap
font: UM.Theme.getFont("large_bold")
color: pluginInfo.color
renderType: Text.NativeRendering
}
Label
{
text: model.description
font: UM.Theme.getFont("default")
maximumLineCount: 3
elide: Text.ElideRight
width: parent.width
wrapMode: Text.WordWrap
color: pluginInfo.color
renderType: Text.NativeRendering
}
}
Column
{
id: authorInfo
width: Math.floor(UM.Theme.getSize("toolbox_action_button").width * 1.25)
Label
{
text:
{
if (model.author_email)
{
return "<a href=\"mailto:" + model.author_email + "?Subject=Cura: " + model.name + "\">" + model.author_name + "</a>"
}
else
{
return model.author_name
}
}
font: UM.Theme.getFont("medium")
width: parent.width
height: Math.floor(UM.Theme.getSize("toolbox_property_label").height)
wrapMode: Text.WordWrap
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
onLinkActivated: Qt.openUrlExternally("mailto:" + model.author_email + "?Subject=Cura: " + model.name + " Plugin")
color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining")
linkColor: UM.Theme.getColor("text_link")
renderType: Text.NativeRendering
}
Label
{
text: model.version
font: UM.Theme.getFont("default")
width: parent.width
height: UM.Theme.getSize("toolbox_property_label").height
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignLeft
renderType: Text.NativeRendering
}
}
ToolboxInstalledTileActions
{
id: pluginActions
}
Connections
{
target: toolbox
function onToolboxEnabledChanged() { isEnabled = toolbox.isEnabled(model.id) }
}
}
}

View File

@ -1,90 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
import Cura 1.1 as Cura
Column
{
property bool canUpdate: CuraApplication.getPackageManager().packagesWithUpdate.indexOf(model.id) != -1
property bool canDowngrade: false
property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
width: UM.Theme.getSize("toolbox_action_button").width
spacing: UM.Theme.getSize("narrow_margin").height
Label
{
visible: !model.is_installed
text: catalog.i18nc("@label", "Will install upon restarting")
color: UM.Theme.getColor("lining")
font: UM.Theme.getFont("default")
wrapMode: Text.WordWrap
width: parent.width
renderType: Text.NativeRendering
}
ToolboxProgressButton
{
id: updateButton
active: toolbox.isDownloading && toolbox.activePackage == model
readyLabel: catalog.i18nc("@action:button", "Update")
activeLabel: catalog.i18nc("@action:button", "Updating")
completeLabel: catalog.i18nc("@action:button", "Updated")
onReadyAction:
{
toolbox.activePackage = model
toolbox.update(model.id)
}
onActiveAction: toolbox.cancelDownload()
// Don't allow installing while another download is running
enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired
opacity: enabled ? 1.0 : 0.5
visible: canUpdate
}
Label
{
wrapMode: Text.WordWrap
text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Log in</a> is required to update")
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
visible: loginRequired
width: updateButton.width
renderType: Text.NativeRendering
MouseArea
{
anchors.fill: parent
onClicked: Cura.API.account.login()
}
}
Cura.SecondaryButton
{
id: removeButton
text: canDowngrade ? catalog.i18nc("@action:button", "Downgrade") : catalog.i18nc("@action:button", "Uninstall")
visible: !model.is_bundled && model.is_installed
enabled: !toolbox.isDownloading
width: UM.Theme.getSize("toolbox_action_button").width
height: UM.Theme.getSize("toolbox_action_button").height
fixedWidthMode: true
onClicked: toolbox.checkPackageUsageAndUninstall(model.id)
Connections
{
target: toolbox
function onMetadataChanged()
{
canDowngrade = toolbox.canDowngrade(model.id)
}
}
}
}

View File

@ -1,60 +0,0 @@
// Copyright (c) 2019 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.1 as UM
import Cura 1.0 as Cura
Cura.PrimaryButton
{
id: button
property var active: false
property var complete: false
property var readyLabel: catalog.i18nc("@action:button", "Install")
property var activeLabel: catalog.i18nc("@action:button", "Cancel")
property var completeLabel: catalog.i18nc("@action:button", "Installed")
signal readyAction() // Action when button is ready and clicked (likely install)
signal activeAction() // Action when button is active and clicked (likely cancel)
signal completeAction() // Action when button is complete and clicked (likely go to installed)
width: UM.Theme.getSize("toolbox_action_button").width
height: UM.Theme.getSize("toolbox_action_button").height
fixedWidthMode: true
text:
{
if (complete)
{
return completeLabel
}
else if (active)
{
return activeLabel
}
else
{
return readyLabel
}
}
onClicked:
{
if (complete)
{
completeAction()
}
else if (active)
{
activeAction()
}
else
{
readyAction()
}
}
busy: active
}

View File

@ -1,24 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.2
Rectangle
{
property bool reversed: false
width: parent.width
height: 8
gradient: Gradient
{
GradientStop
{
position: reversed ? 1.0 : 0.0
color: reversed ? Qt.rgba(0,0,0,0.05) : Qt.rgba(0,0,0,0.2)
}
GradientStop
{
position: reversed ? 0.0 : 1.0
color: Qt.rgba(0,0,0,0)
}
}
}

View File

@ -1,68 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.1 as UM
Button
{
id: control
property bool active: false
implicitWidth: UM.Theme.getSize("toolbox_header_tab").width
implicitHeight: UM.Theme.getSize("toolbox_header_tab").height
background: Item
{
id: backgroundItem
Rectangle
{
id: highlight
visible: control.active
color: UM.Theme.getColor("primary")
anchors.bottom: parent.bottom
width: parent.width
height: UM.Theme.getSize("toolbox_header_highlight").height
}
}
contentItem: Label
{
id: label
text: control.text
color: UM.Theme.getColor("toolbox_header_button_text_inactive")
font: UM.Theme.getFont("medium")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
renderType: Text.NativeRendering
}
states:
[
State
{
name: "disabled"
when: !control.enabled
PropertyChanges
{
target: label
font: UM.Theme.getFont("default_italic")
}
},
State
{
name: "active"
when: control.active
PropertyChanges
{
target: label
font: UM.Theme.getFont("medium_bold")
color: UM.Theme.getColor("action_button_text")
}
}
]
}

View File

@ -1,153 +0,0 @@
// Copyright (c) 2020 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import UM 1.1 as UM
import Cura 1.6 as Cura
UM.Dialog{
visible: true
title: catalog.i18nc("@title", "Changes from your account")
width: UM.Theme.getSize("popup_dialog").width
height: UM.Theme.getSize("popup_dialog").height
minimumWidth: width
maximumWidth: minimumWidth
minimumHeight: height
maximumHeight: minimumHeight
margin: 0
property string actionButtonText: subscribedPackagesModel.hasIncompatiblePackages && !subscribedPackagesModel.hasCompatiblePackages ? catalog.i18nc("@button", "Dismiss") : catalog.i18nc("@button", "Next")
Rectangle
{
id: root
anchors.fill: parent
color: UM.Theme.getColor("main_background")
UM.I18nCatalog
{
id: catalog
name: "cura"
}
ScrollView
{
width: parent.width
height: parent.height - nextButton.height - nextButton.anchors.margins * 2 // We want some leftover space for the button at the bottom
clip: true
Column
{
anchors.fill: parent
anchors.margins: UM.Theme.getSize("default_margin").width
// Compatible packages
Label
{
font: UM.Theme.getFont("default")
text: catalog.i18nc("@label", "The following packages will be added:")
visible: subscribedPackagesModel.hasCompatiblePackages
color: UM.Theme.getColor("text")
height: contentHeight + UM.Theme.getSize("default_margin").height
}
Repeater
{
model: subscribedPackagesModel
Component
{
Item
{
width: parent.width
property int lineHeight: 60
visible: model.is_compatible
height: visible ? (lineHeight + UM.Theme.getSize("default_margin").height) : 0 // We only show the compatible packages here
Image
{
id: packageIcon
source: model.icon_url || "../../images/placeholder.svg"
height: lineHeight
width: height
sourceSize.height: height
sourceSize.width: width
mipmap: true
fillMode: Image.PreserveAspectFit
}
Label
{
text: model.display_name
font: UM.Theme.getFont("medium_bold")
anchors.left: packageIcon.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: packageIcon.verticalCenter
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
}
}
// Incompatible packages
Label
{
font: UM.Theme.getFont("default")
text: catalog.i18nc("@label", "The following packages can not be installed because of an incompatible Cura version:")
visible: subscribedPackagesModel.hasIncompatiblePackages
color: UM.Theme.getColor("text")
height: contentHeight + UM.Theme.getSize("default_margin").height
}
Repeater
{
model: subscribedPackagesModel
Component
{
Item
{
width: parent.width
property int lineHeight: 60
visible: !model.is_compatible && !model.is_dismissed
height: visible ? (lineHeight + UM.Theme.getSize("default_margin").height) : 0 // We only show the incompatible packages here
Image
{
id: packageIcon
source: model.icon_url || "../../images/placeholder.svg"
height: lineHeight
width: height
sourceSize.height: height
sourceSize.width: width
mipmap: true
fillMode: Image.PreserveAspectFit
}
Label
{
text: model.display_name
font: UM.Theme.getFont("medium_bold")
anchors.left: packageIcon.right
anchors.leftMargin: UM.Theme.getSize("default_margin").width
anchors.verticalCenter: packageIcon.verticalCenter
color: UM.Theme.getColor("text")
elide: Text.ElideRight
}
}
}
}
}
} // End of ScrollView
Cura.PrimaryButton
{
id: nextButton
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.margins: UM.Theme.getSize("default_margin").height
text: actionButtonText
onClicked: accept()
leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width
rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width
}
}
}

View File

@ -1,96 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
import QtQuick.Layouts 1.1
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.1
import UM 1.3 as UM
import Cura 1.0 as Cura
UM.Dialog
{
// This dialog asks the user to confirm he/she wants to uninstall materials/pprofiles which are currently in use
id: base
title: catalog.i18nc("@title:window", "Confirm uninstall") + toolbox.pluginToUninstall
width: 450 * screenScaleFactor
height: 50 * screenScaleFactor + dialogText.height + buttonBar.height
maximumWidth: 450 * screenScaleFactor
maximumHeight: 450 * screenScaleFactor
minimumWidth: 450 * screenScaleFactor
minimumHeight: 150 * screenScaleFactor
modality: Qt.WindowModal
Column
{
UM.I18nCatalog { id: catalog; name: "cura" }
anchors
{
fill: parent
leftMargin: Math.round(20 * screenScaleFactor)
rightMargin: Math.round(20 * screenScaleFactor)
topMargin: Math.round(10 * screenScaleFactor)
bottomMargin: Math.round(10 * screenScaleFactor)
}
spacing: Math.round(15 * screenScaleFactor)
Label
{
id: dialogText
text:
{
var base_text = catalog.i18nc("@text:window", "You are uninstalling materials and/or profiles that are still in use. Confirming will reset the following materials/profiles to their defaults.")
var materials_text = catalog.i18nc("@text:window", "Materials")
var qualities_text = catalog.i18nc("@text:window", "Profiles")
var machines_with_materials = toolbox.uninstallUsedMaterials
var machines_with_qualities = toolbox.uninstallUsedQualities
if (machines_with_materials != "")
{
base_text += "\n\n" + materials_text +": \n" + machines_with_materials
}
if (machines_with_qualities != "")
{
base_text += "\n\n" + qualities_text + ": \n" + machines_with_qualities
}
return base_text
}
anchors.left: parent.left
anchors.right: parent.right
font: UM.Theme.getFont("default")
wrapMode: Text.WordWrap
renderType: Text.NativeRendering
}
// Buttons
Item {
id: buttonBar
anchors.right: parent.right
anchors.left: parent.left
height: childrenRect.height
Button {
id: cancelButton
text: catalog.i18nc("@action:button", "Cancel")
anchors.right: confirmButton.left
anchors.rightMargin: UM.Theme.getSize("default_margin").width
isDefault: true
onClicked: toolbox.closeConfirmResetDialog()
}
Button {
id: confirmButton
text: catalog.i18nc("@action:button", "Confirm")
anchors.right: parent.right
onClicked: toolbox.resetMaterialsQualitiesAndUninstall()
}
}
}
}

View File

@ -1,110 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Dialogs 1.1
import QtQuick.Window 2.2
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import QtQuick.Controls.Styles 1.4
import UM 1.1 as UM
import Cura 1.6 as Cura
UM.Dialog
{
id: licenseDialog
title: licenseModel.dialogTitle
minimumWidth: UM.Theme.getSize("license_window_minimum").width
minimumHeight: UM.Theme.getSize("license_window_minimum").height
width: minimumWidth
height: minimumHeight
backgroundColor: UM.Theme.getColor("main_background")
margin: screenScaleFactor * 10
ColumnLayout
{
anchors.fill: parent
spacing: UM.Theme.getSize("thick_margin").height
UM.I18nCatalog{id: catalog; name: "cura"}
Label
{
id: licenseHeader
Layout.fillWidth: true
text: catalog.i18nc("@label", "You need to accept the license to install the package")
color: UM.Theme.getColor("text")
wrapMode: Text.Wrap
renderType: Text.NativeRendering
}
Row {
id: packageRow
Layout.fillWidth: true
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
leftPadding: UM.Theme.getSize("narrow_margin").width
Image
{
id: icon
width: 30 * screenScaleFactor
height: width
sourceSize.width: width
sourceSize.height: height
fillMode: Image.PreserveAspectFit
source: licenseModel.iconUrl || "../../images/placeholder.svg"
mipmap: true
}
Label
{
id: packageName
text: licenseModel.packageName
color: UM.Theme.getColor("text")
font.bold: true
anchors.verticalCenter: icon.verticalCenter
height: contentHeight
wrapMode: Text.Wrap
renderType: Text.NativeRendering
}
}
Cura.ScrollableTextArea
{
Layout.fillWidth: true
Layout.fillHeight: true
anchors.topMargin: UM.Theme.getSize("default_margin").height
textArea.text: licenseModel.licenseText
textArea.readOnly: true
}
}
rightButtons:
[
Cura.PrimaryButton
{
leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width
rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width
text: licenseModel.acceptButtonText
onClicked: { handler.onLicenseAccepted() }
}
]
leftButtons:
[
Cura.SecondaryButton
{
id: declineButton
text: licenseModel.declineButtonText
onClicked: { handler.onLicenseDeclined() }
}
]
}

View File

@ -1,176 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.5 as UM
import "../components"
Item
{
id: page
property var details: base.selection || {}
anchors.fill: parent
ToolboxBackColumn
{
id: sidebar
}
Item
{
id: header
anchors
{
left: sidebar.right
right: parent.right
rightMargin: UM.Theme.getSize("wide_margin").width
}
height: UM.Theme.getSize("toolbox_detail_header").height
Image
{
id: thumbnail
width: UM.Theme.getSize("toolbox_thumbnail_medium").width
height: UM.Theme.getSize("toolbox_thumbnail_medium").height
fillMode: Image.PreserveAspectFit
source: details && details.icon_url ? details.icon_url : "../../images/placeholder.svg"
mipmap: true
anchors
{
top: parent.top
left: parent.left
leftMargin: UM.Theme.getSize("wide_margin").width
topMargin: UM.Theme.getSize("wide_margin").height
}
}
Label
{
id: title
anchors
{
top: thumbnail.top
left: thumbnail.right
leftMargin: UM.Theme.getSize("default_margin").width
right: parent.right
rightMargin: UM.Theme.getSize("wide_margin").width
bottomMargin: UM.Theme.getSize("default_margin").height
}
text: details && details.name ? details.name : ""
font: UM.Theme.getFont("large_bold")
color: UM.Theme.getColor("text_medium")
wrapMode: Text.WordWrap
width: parent.width
height: UM.Theme.getSize("toolbox_property_label").height
renderType: Text.NativeRendering
}
Label
{
id: description
text: details && details.description ? details.description : ""
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
anchors
{
top: title.bottom
left: title.left
topMargin: UM.Theme.getSize("default_margin").height
}
renderType: Text.NativeRendering
}
Column
{
id: properties
anchors
{
top: description.bottom
left: description.left
topMargin: UM.Theme.getSize("default_margin").height
}
spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
width: childrenRect.width
Label
{
text: catalog.i18nc("@label", "Website") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Email") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
renderType: Text.NativeRendering
}
}
Column
{
id: values
anchors
{
top: description.bottom
left: properties.right
leftMargin: UM.Theme.getSize("default_margin").width
right: parent.right
rightMargin: UM.Theme.getSize("default_margin").width
topMargin: UM.Theme.getSize("default_margin").height
}
spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
Label
{
text:
{
if (details && details.website)
{
return "<a href=\"" + details.website + "\">" + details.website + "</a>"
}
return ""
}
width: parent.width
elide: Text.ElideRight
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: UM.UrlUtil.openUrl(link, ["https", "http"])
renderType: Text.NativeRendering
}
Label
{
text:
{
if (details && details.email)
{
return "<a href=\"mailto:" + details.email + "\">" + details.email + "</a>"
}
return ""
}
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: Qt.openUrlExternally(link)
renderType: Text.NativeRendering
}
}
Rectangle
{
color: UM.Theme.getColor("lining")
width: parent.width
height: UM.Theme.getSize("default_lining").height
anchors.bottom: parent.bottom
}
}
ToolboxDetailList
{
anchors
{
top: header.bottom
bottom: page.bottom
left: header.left
right: page.right
}
}
}

View File

@ -1,188 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.5 as UM
import Cura 1.1 as Cura
import "../components"
Item
{
id: page
property var details: base.selection || {}
anchors.fill: parent
ToolboxBackColumn
{
id: sidebar
}
Item
{
id: header
anchors
{
left: sidebar.right
right: parent.right
rightMargin: UM.Theme.getSize("wide_margin").width
}
height: childrenRect.height + 3 * UM.Theme.getSize("default_margin").width
Rectangle
{
id: thumbnail
width: UM.Theme.getSize("toolbox_thumbnail_medium").width
height: UM.Theme.getSize("toolbox_thumbnail_medium").height
anchors
{
top: parent.top
left: parent.left
leftMargin: UM.Theme.getSize("wide_margin").width
topMargin: UM.Theme.getSize("wide_margin").height
}
color: UM.Theme.getColor("main_background")
Image
{
anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: details === null ? "" : (details.icon_url || "../../images/placeholder.svg")
mipmap: true
height: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height
width: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height
sourceSize.height: height
sourceSize.width: width
}
}
Label
{
id: title
anchors
{
top: thumbnail.top
left: thumbnail.right
leftMargin: UM.Theme.getSize("default_margin").width
}
text: details === null ? "" : (details.name || "")
font: UM.Theme.getFont("large_bold")
color: UM.Theme.getColor("text")
width: contentWidth
height: contentHeight
renderType: Text.NativeRendering
}
Column
{
id: properties
anchors
{
top: title.bottom
left: title.left
topMargin: UM.Theme.getSize("default_margin").height
}
spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
width: childrenRect.width
height: childrenRect.height
Label
{
text: catalog.i18nc("@label", "Version") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Last updated") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Brand") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
renderType: Text.NativeRendering
}
Label
{
text: catalog.i18nc("@label", "Downloads") + ":"
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text_medium")
renderType: Text.NativeRendering
}
}
Column
{
id: values
anchors
{
top: title.bottom
left: properties.right
leftMargin: UM.Theme.getSize("default_margin").width
topMargin: UM.Theme.getSize("default_margin").height
}
spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
height: childrenRect.height
Label
{
text: details === null ? "" : (details.version || catalog.i18nc("@label", "Unknown"))
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
}
Label
{
text:
{
if (details === null)
{
return ""
}
var date = new Date(details.last_updated)
return date.toLocaleString(UM.Preferences.getValue("general/language"))
}
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
}
Label
{
text:
{
if (details === null)
{
return ""
}
else
{
return "<a href=\"" + details.website + "\">" + details.author_name + "</a>"
}
}
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
linkColor: UM.Theme.getColor("text_link")
onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
renderType: Text.NativeRendering
}
Label
{
text: details === null ? "" : (details.download_count || catalog.i18nc("@label", "Unknown"))
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
renderType: Text.NativeRendering
}
}
}
ToolboxDetailList
{
anchors
{
top: header.bottom
bottom: page.bottom
left: header.left
right: page.right
}
}
}

View File

@ -1,46 +0,0 @@
// Copyright (c) 2019 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.1 as UM
import "../components"
ScrollView
{
clip: true
width: parent.width
height: parent.height
contentHeight: mainColumn.height
Column
{
id: mainColumn
width: base.width
spacing: UM.Theme.getSize("default_margin").height
ToolboxDownloadsShowcase
{
id: showcase
width: parent.width
}
ToolboxDownloadsGrid
{
id: allPlugins
width: parent.width
heading: toolbox.viewCategory === "material" ? catalog.i18nc("@label", "Community Contributions") : catalog.i18nc("@label", "Community Plugins")
model: toolbox.viewCategory === "material" ? toolbox.materialsAvailableModel : toolbox.pluginsAvailableModel
}
ToolboxDownloadsGrid
{
id: genericMaterials
visible: toolbox.viewCategory === "material"
width: parent.width
heading: catalog.i18nc("@label", "Generic Materials")
model: toolbox.materialsGenericModel
}
}
}

View File

@ -1,23 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
Rectangle
{
id: page
width: parent.width
height: parent.height
color: "transparent"
Label
{
text: catalog.i18nc("@info", "Could not connect to the Cura Package database. Please check your connection.")
anchors
{
centerIn: parent
}
renderType: Text.NativeRendering
}
}

View File

@ -1,223 +0,0 @@
// Copyright (c) 2019 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3
import UM 1.1 as UM
import "../components"
ScrollView
{
id: page
clip: true
width: parent.width
height: parent.height
Column
{
width: page.width
spacing: UM.Theme.getSize("default_margin").height
padding: UM.Theme.getSize("wide_margin").width
height: childrenRect.height + 2 * UM.Theme.getSize("wide_margin").height
Label
{
anchors
{
left: parent.left
right: parent.right
margins: parent.padding
}
text: catalog.i18nc("@title:tab", "Installed plugins")
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
}
Rectangle
{
anchors
{
left: parent.left
right: parent.right
margins: parent.padding
}
id: installedPlugins
color: "transparent"
height: childrenRect.height + UM.Theme.getSize("default_margin").width
border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width
Column
{
anchors
{
top: parent.top
right: parent.right
left: parent.left
margins: UM.Theme.getSize("default_margin").width
}
Repeater
{
id: pluginList
model: toolbox.pluginsInstalledModel
delegate: ToolboxInstalledTile { }
}
}
Label
{
visible: toolbox.pluginsInstalledModel.count < 1
padding: UM.Theme.getSize("default_margin").width
text: catalog.i18nc("@info", "No plugin has been installed.")
font: UM.Theme.getFont("medium")
color: UM.Theme.getColor("lining")
renderType: Text.NativeRendering
}
}
Label
{
anchors
{
left: parent.left
right: parent.right
margins: parent.padding
}
text: catalog.i18nc("@title:tab", "Installed materials")
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
}
Rectangle
{
anchors
{
left: parent.left
right: parent.right
margins: parent.padding
}
id: installedMaterials
color: "transparent"
height: childrenRect.height + UM.Theme.getSize("default_margin").width
border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width
Column
{
anchors
{
top: parent.top
right: parent.right
left: parent.left
margins: UM.Theme.getSize("default_margin").width
}
Repeater
{
id: installedMaterialsList
model: toolbox.materialsInstalledModel
delegate: ToolboxInstalledTile { }
}
}
Label
{
visible: toolbox.materialsInstalledModel.count < 1
padding: UM.Theme.getSize("default_margin").width
text: catalog.i18nc("@info", "No material has been installed.")
color: UM.Theme.getColor("lining")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
}
}
Label
{
anchors
{
left: parent.left
right: parent.right
margins: parent.padding
}
text: catalog.i18nc("@title:tab", "Bundled plugins")
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
}
Rectangle
{
anchors
{
left: parent.left
right: parent.right
margins: parent.padding
}
id: bundledPlugins
color: "transparent"
height: childrenRect.height + UM.Theme.getSize("default_margin").width
border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width
Column
{
anchors
{
top: parent.top
right: parent.right
left: parent.left
margins: UM.Theme.getSize("default_margin").width
}
Repeater
{
id: bundledPluginsList
model: toolbox.pluginsBundledModel
delegate: ToolboxInstalledTile { }
}
}
}
Label
{
anchors
{
left: parent.left
right: parent.right
margins: parent.padding
}
text: catalog.i18nc("@title:tab", "Bundled materials")
color: UM.Theme.getColor("text_medium")
font: UM.Theme.getFont("medium")
renderType: Text.NativeRendering
}
Rectangle
{
anchors
{
left: parent.left
right: parent.right
margins: parent.padding
}
id: bundledMaterials
color: "transparent"
height: childrenRect.height + UM.Theme.getSize("default_margin").width
border.color: UM.Theme.getColor("lining")
border.width: UM.Theme.getSize("default_lining").width
Column
{
anchors
{
top: parent.top
right: parent.right
left: parent.left
margins: UM.Theme.getSize("default_margin").width
}
Repeater
{
id: bundledMaterialsList
model: toolbox.materialsBundledModel
delegate: ToolboxInstalledTile {}
}
}
}
}
}

View File

@ -1,25 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import UM 1.3 as UM
Rectangle
{
id: page
width: parent.width
height: parent.height
color: "transparent"
Label
{
text: catalog.i18nc("@info", "Fetching packages...")
color: UM.Theme.getColor("text")
anchors
{
centerIn: parent
}
renderType: Text.NativeRendering
}
}

View File

@ -1,44 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.7
import QtQuick.Controls 2.1
import QtQuick.Window 2.2
import UM 1.3 as UM
import Cura 1.1 as Cura
Column
{
id: welcomePage
spacing: UM.Theme.getSize("wide_margin").height
width: parent.width
height: childrenRect.height
anchors.centerIn: parent
Label
{
id: welcomeTextLabel
text: catalog.i18nc("@description", "Please sign in to get verified plugins and materials for Ultimaker Cura Enterprise")
width: Math.round(parent.width / 2)
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("text")
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
anchors.horizontalCenter: parent.horizontalCenter
wrapMode: Label.WordWrap
renderType: Text.NativeRendering
}
Cura.PrimaryButton
{
id: loginButton
width: UM.Theme.getSize("account_button").width
height: UM.Theme.getSize("account_button").height
anchors.horizontalCenter: parent.horizontalCenter
text: catalog.i18nc("@button", "Sign in")
onClicked: Cura.API.account.login()
fixedWidthMode: true
}
}

View File

@ -1,104 +0,0 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import re
from typing import Dict, List, Optional, Union, cast
from PyQt6.QtCore import Qt, pyqtProperty
from UM.Qt.ListModel import ListModel
class AuthorsModel(ListModel):
"""Model that holds cura packages.
By setting the filter property the instances held by this model can be changed.
"""
def __init__(self, parent = None) -> None:
super().__init__(parent)
self._metadata = None # type: Optional[List[Dict[str, Union[str, List[str], int]]]]
self.addRoleName(Qt.ItemDataRole.UserRole + 1, "id")
self.addRoleName(Qt.ItemDataRole.UserRole + 2, "name")
self.addRoleName(Qt.ItemDataRole.UserRole + 3, "email")
self.addRoleName(Qt.ItemDataRole.UserRole + 4, "website")
self.addRoleName(Qt.ItemDataRole.UserRole + 5, "package_count")
self.addRoleName(Qt.ItemDataRole.UserRole + 6, "package_types")
self.addRoleName(Qt.ItemDataRole.UserRole + 7, "icon_url")
self.addRoleName(Qt.ItemDataRole.UserRole + 8, "description")
# List of filters for queries. The result is the union of the each list of results.
self._filter = {} # type: Dict[str, str]
def setMetadata(self, data: List[Dict[str, Union[str, List[str], int]]]):
if self._metadata != data:
self._metadata = data
self._update()
def _update(self) -> None:
items = [] # type: List[Dict[str, Union[str, List[str], int, None]]]
if not self._metadata:
self.setItems(items)
return
for author in self._metadata:
items.append({
"id": author.get("author_id"),
"name": author.get("display_name"),
"email": author.get("email"),
"website": author.get("website"),
"package_count": author.get("package_count", 0),
"package_types": author.get("package_types", []),
"icon_url": author.get("icon_url"),
"description": "Material and quality profiles from {author_name}".format(author_name = author.get("display_name", ""))
})
# Filter on all the key-word arguments.
for key, value in self._filter.items():
if key == "package_types":
key_filter = lambda item, value = value: value in item["package_types"] # type: ignore
elif "*" in value:
key_filter = lambda item, key = key, value = value: self._matchRegExp(item, key, value) # type: ignore
else:
key_filter = lambda item, key = key, value = value: self._matchString(item, key, value) # type: ignore
items = filter(key_filter, items) # type: ignore
# Execute all filters.
filtered_items = list(items)
filtered_items.sort(key = lambda k: cast(str, k["name"]))
self.setItems(filtered_items)
def setFilter(self, filter_dict: Dict[str, str]) -> None:
"""Set the filter of this model based on a string.
:param filter_dict: Dictionary to do the filtering by.
"""
if filter_dict != self._filter:
self._filter = filter_dict
self._update()
@pyqtProperty("QVariantMap", fset = setFilter, constant = True)
def filter(self) -> Dict[str, str]:
return self._filter
# Check to see if a container matches with a regular expression
def _matchRegExp(self, metadata, property_name, value):
if property_name not in metadata:
return False
value = re.escape(value) #Escape for regex patterns.
value = "^" + value.replace("\\*", ".*") + "$" #Instead of (now escaped) asterisks, match on any string. Also add anchors for a complete match.
if self._ignore_case:
value_pattern = re.compile(value, re.IGNORECASE)
else:
value_pattern = re.compile(value)
return value_pattern.match(str(metadata[property_name]))
# Check to see if a container matches with a string
def _matchString(self, metadata, property_name, value):
if property_name not in metadata:
return False
return value.lower() == str(metadata[property_name]).lower()

View File

@ -1,29 +0,0 @@
from typing import Union
from cura import ApplicationMetadata
from cura.UltimakerCloud import UltimakerCloudConstants
class CloudApiModel:
sdk_version = ApplicationMetadata.CuraSDKVersion # type: Union[str, int]
cloud_api_version = UltimakerCloudConstants.CuraCloudAPIVersion # type: str
cloud_api_root = UltimakerCloudConstants.CuraCloudAPIRoot # type: str
api_url = "{cloud_api_root}/cura-packages/v{cloud_api_version}/cura/v{sdk_version}".format(
cloud_api_root = cloud_api_root,
cloud_api_version = cloud_api_version,
sdk_version = sdk_version
) # type: str
# https://api.ultimaker.com/cura-packages/v1/user/packages
api_url_user_packages = "{cloud_api_root}/cura-packages/v{cloud_api_version}/user/packages".format(
cloud_api_root=cloud_api_root,
cloud_api_version=cloud_api_version,
)
@classmethod
def userPackageUrl(cls, package_id: str) -> str:
"""https://api.ultimaker.com/cura-packages/v1/user/packages/{package_id}"""
return (CloudApiModel.api_url_user_packages + "/{package_id}").format(
package_id=package_id
)

View File

@ -1,52 +0,0 @@
from UM.Logger import Logger
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from cura.CuraApplication import CuraApplication
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
from ..CloudApiModel import CloudApiModel
class CloudApiClient:
"""Manages Cloud subscriptions
When a package is added to a user's account, the user is 'subscribed' to that package.
Whenever the user logs in on another instance of Cura, these subscriptions can be used to sync the user's plugins
Singleton: use CloudApiClient.getInstance() instead of CloudApiClient()
"""
__instance = None
@classmethod
def getInstance(cls, app: CuraApplication):
if not cls.__instance:
cls.__instance = CloudApiClient(app)
return cls.__instance
def __init__(self, app: CuraApplication) -> None:
if self.__instance is not None:
raise RuntimeError("This is a Singleton. use getInstance()")
self._scope = JsonDecoratorScope(UltimakerCloudScope(app)) # type: JsonDecoratorScope
app.getPackageManager().packageInstalled.connect(self._onPackageInstalled)
def unsubscribe(self, package_id: str) -> None:
url = CloudApiModel.userPackageUrl(package_id)
HttpRequestManager.getInstance().delete(url = url, scope = self._scope)
def _subscribe(self, package_id: str) -> None:
"""You probably don't want to use this directly. All installed packages will be automatically subscribed."""
Logger.debug("Subscribing to {}", package_id)
data = "{\"data\": {\"package_id\": \"%s\", \"sdk_version\": \"%s\"}}" % (package_id, CloudApiModel.sdk_version)
HttpRequestManager.getInstance().put(
url = CloudApiModel.api_url_user_packages,
data = data.encode(),
scope = self._scope
)
def _onPackageInstalled(self, package_id: str):
if CuraApplication.getInstance().getCuraAPI().account.isLoggedIn:
# We might already be subscribed, but checking would take one extra request. Instead, simply subscribe
self._subscribe(package_id)

View File

@ -1,164 +0,0 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
from typing import List, Dict, Any, Set
from typing import Optional
from PyQt6.QtCore import QObject
from PyQt6.QtNetwork import QNetworkReply, QNetworkRequest
from UM import i18nCatalog
from UM.Logger import Logger
from UM.Message import Message
from UM.Signal import Signal
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
from cura.API.Account import SyncState
from cura.CuraApplication import CuraApplication, ApplicationMetadata
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
from .SubscribedPackagesModel import SubscribedPackagesModel
from ..CloudApiModel import CloudApiModel
class CloudPackageChecker(QObject):
SYNC_SERVICE_NAME = "CloudPackageChecker"
def __init__(self, application: CuraApplication) -> None:
super().__init__()
self.discrepancies = Signal() # Emits SubscribedPackagesModel
self._application = application # type: CuraApplication
self._scope = JsonDecoratorScope(UltimakerCloudScope(application))
self._model = SubscribedPackagesModel()
self._message = None # type: Optional[Message]
self._application.initializationFinished.connect(self._onAppInitialized)
self._i18n_catalog = i18nCatalog("cura")
self._sdk_version = ApplicationMetadata.CuraSDKVersion
self._last_notified_packages = set() # type: Set[str]
"""Packages for which a notification has been shown. No need to bother the user twice for equal content"""
# This is a plugin, so most of the components required are not ready when
# this is initialized. Therefore, we wait until the application is ready.
def _onAppInitialized(self) -> None:
self._package_manager = self._application.getPackageManager()
# initial check
self._getPackagesIfLoggedIn()
self._application.getCuraAPI().account.loginStateChanged.connect(self._onLoginStateChanged)
self._application.getCuraAPI().account.syncRequested.connect(self._getPackagesIfLoggedIn)
def _onLoginStateChanged(self) -> None:
# reset session
self._last_notified_packages = set()
self._getPackagesIfLoggedIn()
def _getPackagesIfLoggedIn(self) -> None:
if self._application.getCuraAPI().account.isLoggedIn:
self._getUserSubscribedPackages()
else:
self._hideSyncMessage()
def _getUserSubscribedPackages(self) -> None:
self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SYNCING)
url = CloudApiModel.api_url_user_packages
self._application.getHttpRequestManager().get(url,
callback = self._onUserPackagesRequestFinished,
error_callback = self._onUserPackagesRequestFinished,
timeout = 10,
scope = self._scope)
def _onUserPackagesRequestFinished(self, reply: "QNetworkReply", error: Optional["QNetworkReply.NetworkError"] = None) -> None:
if error is not None or reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute) != 200:
Logger.log("w",
"Requesting user packages failed, response code %s while trying to connect to %s",
reply.attribute(QNetworkRequest.Attribute.HttpStatusCodeAttribute), reply.url())
self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
return
try:
json_data = json.loads(bytes(reply.readAll()).decode("utf-8"))
# Check for errors:
if "errors" in json_data:
for error in json_data["errors"]:
Logger.log("e", "%s", error["title"])
self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.ERROR)
return
self._handleCompatibilityData(json_data["data"])
except json.decoder.JSONDecodeError:
Logger.log("w", "Received invalid JSON for user subscribed packages from the Web Marketplace")
self._application.getCuraAPI().account.setSyncState(self.SYNC_SERVICE_NAME, SyncState.SUCCESS)
def _handleCompatibilityData(self, subscribed_packages_payload: List[Dict[str, Any]]) -> None:
user_subscribed_packages = {plugin["package_id"] for plugin in subscribed_packages_payload}
user_installed_packages = self._package_manager.getAllInstalledPackageIDs()
# We need to re-evaluate the dismissed packages
# (i.e. some package might got updated to the correct SDK version in the meantime,
# hence remove them from the Dismissed Incompatible list)
self._package_manager.reEvaluateDismissedPackages(subscribed_packages_payload, self._sdk_version)
user_dismissed_packages = self._package_manager.getDismissedPackages()
if user_dismissed_packages:
user_installed_packages.update(user_dismissed_packages)
# We check if there are packages installed in Web Marketplace but not in Cura marketplace
package_discrepancy = list(user_subscribed_packages.difference(user_installed_packages))
if user_subscribed_packages != self._last_notified_packages:
# scenario:
# 1. user subscribes to a package
# 2. dismisses the license/unsubscribes
# 3. subscribes to the same package again
# in this scenario we want to notify the user again. To capture that there was a change during
# step 2, we clear the last_notified after step 2. This way, the user will be notified after
# step 3 even though the list of packages for step 1 and 3 are equal
self._last_notified_packages = set()
if package_discrepancy:
account = self._application.getCuraAPI().account
account.setUpdatePackagesAction(lambda: self._onSyncButtonClicked(None, None))
if user_subscribed_packages == self._last_notified_packages:
# already notified user about these
return
Logger.log("d", "Discrepancy found between Cloud subscribed packages and Cura installed packages")
self._model.addDiscrepancies(package_discrepancy)
self._model.initialize(self._package_manager, subscribed_packages_payload)
self._showSyncMessage()
self._last_notified_packages = user_subscribed_packages
def _showSyncMessage(self) -> None:
"""Show the message if it is not already shown"""
if self._message is not None:
self._message.show()
return
sync_message = Message(self._i18n_catalog.i18nc(
"@info:generic",
"Do you want to sync material and software packages with your account?"),
title = self._i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account", ))
sync_message.addAction("sync",
name = self._i18n_catalog.i18nc("@action:button", "Sync"),
icon = "",
description = "Sync your plugins and print profiles to Ultimaker Cura.",
button_align = Message.ActionButtonAlignment.ALIGN_RIGHT)
sync_message.actionTriggered.connect(self._onSyncButtonClicked)
sync_message.show()
self._message = sync_message
def _hideSyncMessage(self) -> None:
"""Hide the message if it is showing"""
if self._message is not None:
self._message.hide()
self._message = None
def _onSyncButtonClicked(self, sync_message: Optional[Message], sync_message_action: Optional[str]) -> None:
if sync_message is not None:
sync_message.hide()
self._hideSyncMessage() # Should be the same message, but also sets _message to None
self.discrepancies.emit(self._model)

View File

@ -1,41 +0,0 @@
import os
from typing import Optional
from PyQt6.QtCore import QObject, pyqtSlot
from UM.Qt.QtApplication import QtApplication
from UM.Signal import Signal
from .SubscribedPackagesModel import SubscribedPackagesModel
class DiscrepanciesPresenter(QObject):
"""Shows a list of packages to be added or removed. The user can select which packages to (un)install. The user's
choices are emitted on the `packageMutations` Signal.
"""
def __init__(self, app: QtApplication) -> None:
super().__init__(app)
self.packageMutations = Signal() # Emits SubscribedPackagesModel
self._app = app
self._package_manager = app.getPackageManager()
self._dialog = None # type: Optional[QObject]
self._compatibility_dialog_path = "resources/qml/dialogs/CompatibilityDialog.qml"
def present(self, plugin_path: str, model: SubscribedPackagesModel) -> None:
path = os.path.join(plugin_path, self._compatibility_dialog_path)
self._dialog = self._app.createQmlComponent(path, {"subscribedPackagesModel": model, "handler": self})
assert self._dialog
self._dialog.accepted.connect(lambda: self._onConfirmClicked(model))
def _onConfirmClicked(self, model: SubscribedPackagesModel) -> None:
# If there are incompatible packages - automatically dismiss them
if model.getIncompatiblePackages():
self._package_manager.dismissAllIncompatiblePackages(model.getIncompatiblePackages())
# For now, all compatible packages presented to the user should be installed.
# Later, we might remove items for which the user unselected the package
if model.getCompatiblePackages():
model.setItems(model.getCompatiblePackages())
self.packageMutations.emit(model)

View File

@ -1,153 +0,0 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import tempfile
from typing import Dict, List, Any
from PyQt6.QtNetwork import QNetworkReply
from UM.i18n import i18nCatalog
from UM.Logger import Logger
from UM.Message import Message
from UM.Signal import Signal
from UM.TaskManagement.HttpRequestManager import HttpRequestManager
from cura.CuraApplication import CuraApplication
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
from .SubscribedPackagesModel import SubscribedPackagesModel
i18n_catalog = i18nCatalog("cura")
class DownloadPresenter:
"""Downloads a set of packages from the Ultimaker Cloud Marketplace
use download() exactly once: should not be used for multiple sets of downloads since this class contains state
"""
DISK_WRITE_BUFFER_SIZE = 256 * 1024 # 256 KB
def __init__(self, app: CuraApplication) -> None:
# Emits (Dict[str, str], List[str]) # (success_items, error_items)
# Dict{success_package_id, temp_file_path}
# List[errored_package_id]
self.done = Signal()
self._app = app
self._scope = UltimakerCloudScope(app)
self._started = False
self._progress_message = self._createProgressMessage()
self._progress = {} # type: Dict[str, Dict[str, Any]] # package_id, Dict
self._error = [] # type: List[str] # package_id
def download(self, model: SubscribedPackagesModel) -> None:
if self._started:
Logger.error("Download already started. Create a new %s instead", self.__class__.__name__)
return
manager = HttpRequestManager.getInstance()
for item in model.items:
package_id = item["package_id"]
def finishedCallback(reply: QNetworkReply, pid = package_id) -> None:
self._onFinished(pid, reply)
def progressCallback(rx: int, rt: int, pid = package_id) -> None:
self._onProgress(pid, rx, rt)
def errorCallback(reply: QNetworkReply, error: QNetworkReply.NetworkError, pid = package_id) -> None:
self._onError(pid)
request_data = manager.get(
item["download_url"],
callback = finishedCallback,
download_progress_callback = progressCallback,
error_callback = errorCallback,
scope = self._scope)
self._progress[package_id] = {
"received": 0,
"total": 1, # make sure this is not considered done yet. Also divByZero-safe
"file_written": None,
"request_data": request_data,
"package_model": item
}
self._started = True
self._progress_message.show()
def abort(self) -> None:
manager = HttpRequestManager.getInstance()
for item in self._progress.values():
manager.abortRequest(item["request_data"])
# Aborts all current operations and returns a copy with the same settings such as app and scope
def resetCopy(self) -> "DownloadPresenter":
self.abort()
self.done.disconnectAll()
return DownloadPresenter(self._app)
def _createProgressMessage(self) -> Message:
return Message(i18n_catalog.i18nc("@info:generic", "Syncing..."),
lifetime = 0,
use_inactivity_timer = False,
progress = 0.0,
title = i18n_catalog.i18nc("@info:title", "Changes detected from your Ultimaker account"))
def _onFinished(self, package_id: str, reply: QNetworkReply) -> None:
self._progress[package_id]["received"] = self._progress[package_id]["total"]
try:
with tempfile.NamedTemporaryFile(mode = "wb+", suffix = ".curapackage", delete = False) as temp_file:
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
while bytes_read:
temp_file.write(bytes_read)
bytes_read = reply.read(self.DISK_WRITE_BUFFER_SIZE)
self._app.processEvents()
self._progress[package_id]["file_written"] = temp_file.name
except IOError as e:
Logger.logException("e", "Failed to write downloaded package to temp file", e)
self._onError(package_id)
temp_file.close()
self._checkDone()
def _onProgress(self, package_id: str, rx: int, rt: int) -> None:
self._progress[package_id]["received"] = rx
self._progress[package_id]["total"] = rt
received = 0
total = 0
for item in self._progress.values():
received += item["received"]
total += item["total"]
if total == 0: # Total download size is 0, or unknown, or there are no progress items at all.
self._progress_message.setProgress(100.0)
return
self._progress_message.setProgress(100.0 * (received / total)) # [0 .. 100] %
def _onError(self, package_id: str) -> None:
self._progress.pop(package_id)
self._error.append(package_id)
self._checkDone()
def _checkDone(self) -> bool:
for item in self._progress.values():
if not item["file_written"]:
return False
success_items = {
package_id:
{
"package_path": value["file_written"],
"icon_url": value["package_model"]["icon_url"]
}
for package_id, value in self._progress.items()
}
error_items = [package_id for package_id in self._error]
self._progress_message.hide()
self.done.emit(success_items, error_items)
return True

View File

@ -1,77 +0,0 @@
from PyQt6.QtCore import QObject, pyqtProperty, pyqtSignal
from UM.i18n import i18nCatalog
catalog = i18nCatalog("cura")
# Model for the ToolboxLicenseDialog
class LicenseModel(QObject):
DEFAULT_DECLINE_BUTTON_TEXT = catalog.i18nc("@button", "Decline")
ACCEPT_BUTTON_TEXT = catalog.i18nc("@button", "Agree")
dialogTitleChanged = pyqtSignal()
packageNameChanged = pyqtSignal()
licenseTextChanged = pyqtSignal()
iconChanged = pyqtSignal()
def __init__(self, decline_button_text: str = DEFAULT_DECLINE_BUTTON_TEXT) -> None:
super().__init__()
self._current_page_idx = 0
self._page_count = 1
self._dialogTitle = ""
self._license_text = ""
self._package_name = ""
self._icon_url = ""
self._decline_button_text = decline_button_text
@pyqtProperty(str, constant = True)
def acceptButtonText(self):
return self.ACCEPT_BUTTON_TEXT
@pyqtProperty(str, constant = True)
def declineButtonText(self):
return self._decline_button_text
@pyqtProperty(str, notify=dialogTitleChanged)
def dialogTitle(self) -> str:
return self._dialogTitle
@pyqtProperty(str, notify=packageNameChanged)
def packageName(self) -> str:
return self._package_name
def setPackageName(self, name: str) -> None:
self._package_name = name
self.packageNameChanged.emit()
@pyqtProperty(str, notify=iconChanged)
def iconUrl(self) -> str:
return self._icon_url
def setIconUrl(self, url: str):
self._icon_url = url
self.iconChanged.emit()
@pyqtProperty(str, notify=licenseTextChanged)
def licenseText(self) -> str:
return self._license_text
def setLicenseText(self, license_text: str) -> None:
if self._license_text != license_text:
self._license_text = license_text
self.licenseTextChanged.emit()
def setCurrentPageIdx(self, idx: int) -> None:
self._current_page_idx = idx
self._updateDialogTitle()
def setPageCount(self, count: int) -> None:
self._page_count = count
self._updateDialogTitle()
def _updateDialogTitle(self):
self._dialogTitle = catalog.i18nc("@title:window", "Plugin License Agreement")
if self._page_count > 1:
self._dialogTitle = self._dialogTitle + " ({}/{})".format(self._current_page_idx + 1, self._page_count)
self.dialogTitleChanged.emit()

View File

@ -1,142 +0,0 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from collections import OrderedDict
from typing import Dict, Optional, List, Any
from PyQt6.QtCore import QObject, pyqtSlot
from UM.Logger import Logger
from UM.PackageManager import PackageManager
from UM.Signal import Signal
from cura.CuraApplication import CuraApplication
from UM.i18n import i18nCatalog
from .LicenseModel import LicenseModel
class LicensePresenter(QObject):
"""Presents licenses for a set of packages for the user to accept or reject.
Call present() exactly once to show a licenseDialog for a set of packages
Before presenting another set of licenses, create a new instance using resetCopy().
licenseAnswers emits a list of Dicts containing answers when the user has made a choice for all provided packages.
"""
def __init__(self, app: CuraApplication) -> None:
super().__init__()
self._presented = False
"""Whether present() has been called and state is expected to be initialized"""
self._catalog = i18nCatalog("cura")
self._dialog = None # type: Optional[QObject]
self._package_manager = app.getPackageManager() # type: PackageManager
# Emits List[Dict[str, [Any]] containing for example
# [{ "package_id": "BarbarianPlugin", "package_path" : "/tmp/dg345as", "accepted" : True }]
self.licenseAnswers = Signal()
self._current_package_idx = 0
self._package_models = [] # type: List[Dict]
decline_button_text = self._catalog.i18nc("@button", "Decline and remove from account")
self._license_model = LicenseModel(decline_button_text=decline_button_text) # type: LicenseModel
self._page_count = 0
self._app = app
self._compatibility_dialog_path = "resources/qml/dialogs/ToolboxLicenseDialog.qml"
def present(self, plugin_path: str, packages: Dict[str, Dict[str, str]]) -> None:
"""Show a license dialog for multiple packages where users can read a license and accept or decline them
:param plugin_path: Root directory of the Toolbox plugin
:param packages: Dict[package id, file path]
"""
if self._presented:
Logger.error("{clazz} is single-use. Create a new {clazz} instead", clazz=self.__class__.__name__)
return
path = os.path.join(plugin_path, self._compatibility_dialog_path)
self._initState(packages)
if self._page_count == 0:
self.licenseAnswers.emit(self._package_models)
return
if self._dialog is None:
context_properties = {
"catalog": self._catalog,
"licenseModel": self._license_model,
"handler": self
}
self._dialog = self._app.createQmlComponent(path, context_properties)
self._presentCurrentPackage()
self._presented = True
def resetCopy(self) -> "LicensePresenter":
"""Clean up and return a new copy with the same settings such as app"""
if self._dialog:
self._dialog.close()
self.licenseAnswers.disconnectAll()
return LicensePresenter(self._app)
@pyqtSlot()
def onLicenseAccepted(self) -> None:
self._package_models[self._current_package_idx]["accepted"] = True
self._checkNextPage()
@pyqtSlot()
def onLicenseDeclined(self) -> None:
self._package_models[self._current_package_idx]["accepted"] = False
self._checkNextPage()
def _initState(self, packages: Dict[str, Dict[str, Any]]) -> None:
implicitly_accepted_count = 0
for package_id, item in packages.items():
item["package_id"] = package_id
try:
item["licence_content"] = self._package_manager.getPackageLicense(item["package_path"])
except EnvironmentError as e:
Logger.error(f"Could not open downloaded package {package_id} to read license file! {type(e)} - {e}")
continue # Skip this package.
if item["licence_content"] is None:
# Implicitly accept when there is no license
item["accepted"] = True
implicitly_accepted_count = implicitly_accepted_count + 1
self._package_models.append(item)
else:
item["accepted"] = None #: None: no answer yet
# When presenting the packages, we want to show packages which have a license first.
# In fact, we don't want to show the others at all because they are implicitly accepted
self._package_models.insert(0, item)
CuraApplication.getInstance().processEvents()
self._page_count = len(self._package_models) - implicitly_accepted_count
self._license_model.setPageCount(self._page_count)
def _presentCurrentPackage(self) -> None:
package_model = self._package_models[self._current_package_idx]
package_info = self._package_manager.getPackageInfo(package_model["package_path"])
self._license_model.setCurrentPageIdx(self._current_package_idx)
self._license_model.setPackageName(package_info["display_name"])
self._license_model.setIconUrl(package_model["icon_url"])
self._license_model.setLicenseText(package_model["licence_content"])
if self._dialog:
self._dialog.open() # Does nothing if already open
def _checkNextPage(self) -> None:
if self._current_package_idx + 1 < self._page_count:
self._current_package_idx += 1
self._presentCurrentPackage()
else:
if self._dialog:
self._dialog.close()
self.licenseAnswers.emit(self._package_models)

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