Merge branch 'master' of github.com:Ultimaker/Cura into replace_controls_1_for_controls_2
|
@ -72,7 +72,7 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
self._origin_mesh = None # type: Optional[MeshData]
|
self._origin_mesh = None # type: Optional[MeshData]
|
||||||
self._origin_line_length = 20
|
self._origin_line_length = 20
|
||||||
self._origin_line_width = 1.5
|
self._origin_line_width = 1
|
||||||
self._enabled = False
|
self._enabled = False
|
||||||
|
|
||||||
self._grid_mesh = None # type: Optional[MeshData]
|
self._grid_mesh = None # type: Optional[MeshData]
|
||||||
|
@ -601,6 +601,7 @@ class BuildVolume(SceneNode):
|
||||||
if self._adhesion_type == "raft":
|
if self._adhesion_type == "raft":
|
||||||
self._raft_thickness = (
|
self._raft_thickness = (
|
||||||
self._global_container_stack.getProperty("raft_base_thickness", "value") +
|
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_interface_thickness", "value") +
|
||||||
self._global_container_stack.getProperty("raft_surface_layers", "value") *
|
self._global_container_stack.getProperty("raft_surface_layers", "value") *
|
||||||
self._global_container_stack.getProperty("raft_surface_thickness", "value") +
|
self._global_container_stack.getProperty("raft_surface_thickness", "value") +
|
||||||
|
@ -1214,7 +1215,7 @@ class BuildVolume(SceneNode):
|
||||||
|
|
||||||
_machine_settings = ["machine_width", "machine_depth", "machine_height", "machine_shape", "machine_center_is_zero"]
|
_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"]
|
_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"]
|
_extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
|
||||||
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "prime_blob_enable"]
|
_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"]
|
_tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable"]
|
||||||
|
|
|
@ -494,7 +494,7 @@ class CuraApplication(QtApplication):
|
||||||
"CuraEngineBackend", #Cura is useless without this one since you can't slice.
|
"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.
|
"FileLogger", #You want to be able to read the log if something goes wrong.
|
||||||
"XmlMaterialProfile", #Cura crashes without this one.
|
"XmlMaterialProfile", #Cura crashes without this one.
|
||||||
"Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
|
"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.
|
"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.
|
"PreviewStage", #This shows the list of the plugin views that are installed in Cura.
|
||||||
"MonitorStage", #Major part of Cura's functionality.
|
"MonitorStage", #Major part of Cura's functionality.
|
||||||
|
@ -573,6 +573,10 @@ class CuraApplication(QtApplication):
|
||||||
|
|
||||||
preferences.addPreference("general/accepted_user_agreement", False)
|
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 [
|
for key in [
|
||||||
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
|
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
|
||||||
"dialog_profile_path",
|
"dialog_profile_path",
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
# Copyright (c) 2018 Ultimaker B.V.
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
# Cura is released under the terms of the LGPLv3 or higher.
|
||||||
|
|
||||||
from typing import 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 cura.Settings.GlobalStack import GlobalStack
|
||||||
|
|
||||||
from UM.PackageManager import PackageManager # The class we're extending.
|
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.Resources import Resources # To find storage paths for some resource types.
|
||||||
|
from UM.i18n import i18nCatalog
|
||||||
|
catalog = i18nCatalog("cura")
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from UM.Qt.QtApplication import QtApplication
|
from UM.Qt.QtApplication import QtApplication
|
||||||
|
@ -17,6 +19,31 @@ if TYPE_CHECKING:
|
||||||
class CuraPackageManager(PackageManager):
|
class CuraPackageManager(PackageManager):
|
||||||
def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None:
|
def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None:
|
||||||
super().__init__(application, parent)
|
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:
|
def initialize(self) -> None:
|
||||||
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
|
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))
|
machine_with_qualities.append((global_stack, str(extruder_nr), container_id))
|
||||||
|
|
||||||
return machine_with_materials, machine_with_qualities
|
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
|
||||||
|
|
|
@ -268,6 +268,7 @@ class ExtruderManager(QObject):
|
||||||
used_adhesion_extruders.add("skirt_brim_extruder_nr") # There's a brim or prime tower brim.
|
used_adhesion_extruders.add("skirt_brim_extruder_nr") # There's a brim or prime tower brim.
|
||||||
if adhesion_type == "raft":
|
if adhesion_type == "raft":
|
||||||
used_adhesion_extruders.add("raft_base_extruder_nr")
|
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")
|
used_adhesion_extruders.add("raft_interface_extruder_nr")
|
||||||
if global_stack.getProperty("raft_surface_layers", "value") > 0:
|
if global_stack.getProperty("raft_surface_layers", "value") > 0:
|
||||||
used_adhesion_extruders.add("raft_surface_extruder_nr")
|
used_adhesion_extruders.add("raft_surface_extruder_nr")
|
||||||
|
|
|
@ -7,9 +7,9 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
||||||
PROJECT_DIR="$( cd "${SCRIPT_DIR}/.." && pwd )"
|
PROJECT_DIR="$( cd "${SCRIPT_DIR}/.." && pwd )"
|
||||||
|
|
||||||
# Make sure that environment variables are set properly
|
# Make sure that environment variables are set properly
|
||||||
source /opt/rh/devtoolset-8/enable
|
|
||||||
export PATH="${CURA_BUILD_ENV_PATH}/bin:${PATH}"
|
export PATH="${CURA_BUILD_ENV_PATH}/bin:${PATH}"
|
||||||
export PKG_CONFIG_PATH="${CURA_BUILD_ENV_PATH}/lib/pkgconfig:${PKG_CONFIG_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}"
|
cd "${PROJECT_DIR}"
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ export PYTHONPATH="${PROJECT_DIR}/Uranium:.:${PYTHONPATH}"
|
||||||
|
|
||||||
mkdir build
|
mkdir build
|
||||||
cd build
|
cd build
|
||||||
cmake3 \
|
cmake \
|
||||||
-DCMAKE_BUILD_TYPE=Debug \
|
-DCMAKE_BUILD_TYPE=Debug \
|
||||||
-DCMAKE_PREFIX_PATH="${CURA_BUILD_ENV_PATH}" \
|
-DCMAKE_PREFIX_PATH="${CURA_BUILD_ENV_PATH}" \
|
||||||
-DURANIUM_DIR="${PROJECT_DIR}/Uranium" \
|
-DURANIUM_DIR="${PROJECT_DIR}/Uranium" \
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
cd build
|
cd build
|
||||||
ctest3 -j4 --output-on-failure -T Test
|
ctest -j4 --output-on-failure -T Test
|
||||||
|
|
BIN
docs/resources/PerObjectStack.png
Normal file
After Width: | Height: | Size: 60 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 1.8 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 2.3 KiB |
BIN
icons/cura.icns
BIN
icons/cura.ico
Before Width: | Height: | Size: 361 KiB After Width: | Height: | Size: 35 KiB |
|
@ -5,7 +5,7 @@ import QtQuick 2.7
|
||||||
import QtQuick.Controls 2.1
|
import QtQuick.Controls 2.1
|
||||||
import QtQuick.Layouts 1.3
|
import QtQuick.Layouts 1.3
|
||||||
|
|
||||||
import UM 1.3 as UM
|
import UM 1.5 as UM
|
||||||
import Cura 1.0 as Cura
|
import Cura 1.0 as Cura
|
||||||
|
|
||||||
import "../components"
|
import "../components"
|
||||||
|
@ -35,7 +35,7 @@ RowLayout
|
||||||
busy: CuraDrive.isCreatingBackup
|
busy: CuraDrive.isCreatingBackup
|
||||||
}
|
}
|
||||||
|
|
||||||
Cura.CheckBoxWithTooltip
|
UM.CheckBox
|
||||||
{
|
{
|
||||||
id: autoBackupEnabled
|
id: autoBackupEnabled
|
||||||
checked: CuraDrive.autoBackupEnabled
|
checked: CuraDrive.autoBackupEnabled
|
||||||
|
|
1
plugins/CuraDrive/src/qml/images/backup.svg
Normal file
After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 21 KiB |
|
@ -23,7 +23,7 @@ Column
|
||||||
{
|
{
|
||||||
id: profileImage
|
id: profileImage
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
source: "../images/icon.png"
|
source: "../images/backup.svg"
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
width: Math.round(parent.width / 4)
|
width: Math.round(parent.width / 4)
|
||||||
}
|
}
|
||||||
|
|
|
@ -205,6 +205,13 @@ class StartSliceJob(Job):
|
||||||
# Get the objects in their groups to print.
|
# Get the objects in their groups to print.
|
||||||
object_groups = []
|
object_groups = []
|
||||||
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
if stack.getProperty("print_sequence", "value") == "one_at_a_time":
|
||||||
|
modifier_mesh_nodes = []
|
||||||
|
|
||||||
|
for node in DepthFirstIterator(self._scene.getRoot()):
|
||||||
|
build_plate_number = node.callDecoration("getBuildPlateNumber")
|
||||||
|
if node.callDecoration("isNonPrintingMesh") and build_plate_number == self._build_plate_number:
|
||||||
|
modifier_mesh_nodes.append(node)
|
||||||
|
|
||||||
for node in OneAtATimeIterator(self._scene.getRoot()):
|
for node in OneAtATimeIterator(self._scene.getRoot()):
|
||||||
temp_list = []
|
temp_list = []
|
||||||
|
|
||||||
|
@ -221,7 +228,7 @@ class StartSliceJob(Job):
|
||||||
temp_list.append(child_node)
|
temp_list.append(child_node)
|
||||||
|
|
||||||
if temp_list:
|
if temp_list:
|
||||||
object_groups.append(temp_list)
|
object_groups.append(temp_list + modifier_mesh_nodes)
|
||||||
Job.yieldThread()
|
Job.yieldThread()
|
||||||
if len(object_groups) == 0:
|
if len(object_groups) == 0:
|
||||||
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
|
Logger.log("w", "No objects suitable for one at a time found, or no correct order found")
|
||||||
|
|
|
@ -44,7 +44,7 @@ Cura.RoundedRectangle
|
||||||
{
|
{
|
||||||
id: projectImage
|
id: projectImage
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
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)
|
height: Math.round(width * 3/4)
|
||||||
sourceSize.width: width
|
sourceSize.width: width
|
||||||
sourceSize.height: height
|
sourceSize.height: height
|
||||||
|
|
|
@ -6,7 +6,7 @@ import QtQuick 2.10
|
||||||
import QtQuick.Window 2.2
|
import QtQuick.Window 2.2
|
||||||
import QtQuick.Controls 2.3
|
import QtQuick.Controls 2.3
|
||||||
|
|
||||||
import UM 1.2 as UM
|
import UM 1.5 as UM
|
||||||
import Cura 1.6 as Cura
|
import Cura 1.6 as Cura
|
||||||
|
|
||||||
import DigitalFactory 1.0 as DF
|
import DigitalFactory 1.0 as DF
|
||||||
|
@ -214,7 +214,7 @@ Item
|
||||||
width: childrenRect.width
|
width: childrenRect.width
|
||||||
spacing: UM.Theme.getSize("default_margin").width
|
spacing: UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
Cura.CheckBox
|
UM.CheckBox
|
||||||
{
|
{
|
||||||
id: asProjectCheckbox
|
id: asProjectCheckbox
|
||||||
height: UM.Theme.getSize("checkbox").height
|
height: UM.Theme.getSize("checkbox").height
|
||||||
|
@ -224,7 +224,7 @@ Item
|
||||||
font: UM.Theme.getFont("medium")
|
font: UM.Theme.getFont("medium")
|
||||||
}
|
}
|
||||||
|
|
||||||
Cura.CheckBox
|
UM.CheckBox
|
||||||
{
|
{
|
||||||
id: asSlicedCheckbox
|
id: asSlicedCheckbox
|
||||||
height: UM.Theme.getSize("checkbox").height
|
height: UM.Theme.getSize("checkbox").height
|
||||||
|
|
|
@ -7,7 +7,7 @@ import QtQuick.Controls 2.3
|
||||||
import QtQuick.Layouts 1.1
|
import QtQuick.Layouts 1.1
|
||||||
|
|
||||||
import UM 1.2 as UM
|
import UM 1.2 as UM
|
||||||
import Cura 1.6 as Cura
|
import Cura 1.7 as Cura
|
||||||
|
|
||||||
import DigitalFactory 1.0 as DF
|
import DigitalFactory 1.0 as DF
|
||||||
|
|
||||||
|
@ -42,33 +42,13 @@ Item
|
||||||
height: childrenRect.height
|
height: childrenRect.height
|
||||||
spacing: UM.Theme.getSize("default_margin").width
|
spacing: UM.Theme.getSize("default_margin").width
|
||||||
|
|
||||||
Cura.TextField
|
Cura.SearchBar
|
||||||
{
|
{
|
||||||
id: searchBar
|
id: searchBar
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
implicitHeight: createNewProjectButton.height
|
implicitHeight: createNewProjectButton.height
|
||||||
leftPadding: searchIcon.width + UM.Theme.getSize("default_margin").width * 2
|
|
||||||
focus: true
|
focus: true
|
||||||
|
|
||||||
onTextEdited: manager.projectFilter = text //Update the search filter when editing this text field.
|
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
|
Cura.SecondaryButton
|
||||||
|
@ -220,7 +200,7 @@ Item
|
||||||
LoadMoreProjectsCard
|
LoadMoreProjectsCard
|
||||||
{
|
{
|
||||||
id: loadMoreProjectsCard
|
id: loadMoreProjectsCard
|
||||||
height: UM.Theme.getSize("toolbox_thumbnail_small").height
|
height: UM.Theme.getSize("card_icon").height
|
||||||
width: parent.width
|
width: parent.width
|
||||||
visible: manager.digitalFactoryProjectModel.count > 0
|
visible: manager.digitalFactoryProjectModel.count > 0
|
||||||
hasMoreProjectsToLoad: manager.hasMoreProjectsToLoad
|
hasMoreProjectsToLoad: manager.hasMoreProjectsToLoad
|
||||||
|
|
|
@ -12,7 +12,7 @@ from urllib.error import URLError
|
||||||
from typing import Dict
|
from typing import Dict
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
import certifi
|
import certifi # type: ignore
|
||||||
|
|
||||||
from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine
|
from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine
|
||||||
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
|
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
|
||||||
|
|
|
@ -24,7 +24,7 @@ from cura.Settings.ExtruderManager import ExtruderManager
|
||||||
|
|
||||||
catalog = i18nCatalog("cura")
|
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])])
|
Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])])
|
||||||
|
|
||||||
|
|
||||||
|
|
12
plugins/Marketplace/Constants.py
Normal 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"
|
126
plugins/Marketplace/LocalPackageList.py
Normal 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
|
114
plugins/Marketplace/Marketplace.py
Normal 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
|
305
plugins/Marketplace/PackageList.py
Normal 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)
|
382
plugins/Marketplace/PackageModel.py
Normal 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
|
151
plugins/Marketplace/RemotePackageList.py
Normal 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)
|
17
plugins/Marketplace/__init__.py
Normal 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() }
|
8
plugins/Marketplace/plugin.json
Normal 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"
|
||||||
|
}
|
Before Width: | Height: | Size: 184 B After Width: | Height: | Size: 184 B |
91
plugins/Marketplace/resources/qml/LicenseDialog.qml
Normal 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)
|
||||||
|
}
|
114
plugins/Marketplace/resources/qml/ManageButton.qml
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
49
plugins/Marketplace/resources/qml/ManagePackagesButton.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
25
plugins/Marketplace/resources/qml/ManagedPackages.qml
Normal 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
|
||||||
|
}
|
299
plugins/Marketplace/resources/qml/Marketplace.qml
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
plugins/Marketplace/resources/qml/Materials.qml
Normal 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
|
||||||
|
}
|
119
plugins/Marketplace/resources/qml/OnboardBanner.qml
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
101
plugins/Marketplace/resources/qml/PackageCard.qml
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
215
plugins/Marketplace/resources/qml/PackageCardHeader.qml
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
96
plugins/Marketplace/resources/qml/PackageDetails.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
295
plugins/Marketplace/resources/qml/PackagePage.qml
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
33
plugins/Marketplace/resources/qml/PackageTypeTab.qml
Normal 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
|
||||||
|
}
|
||||||
|
}
|
232
plugins/Marketplace/resources/qml/Packages.qml
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
22
plugins/Marketplace/resources/qml/Plugins.qml
Normal 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
|
||||||
|
}
|
45
plugins/Marketplace/resources/qml/VerifiedIcon.qml
Normal 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 )
|
||||||
|
}
|
|
@ -5,7 +5,9 @@ import QtQuick 2.1
|
||||||
import QtQuick.Layouts 1.1
|
import QtQuick.Layouts 1.1
|
||||||
import QtQuick.Controls 2.1
|
import QtQuick.Controls 2.1
|
||||||
|
|
||||||
import UM 1.2 as UM
|
import UM 1.5 as UM
|
||||||
|
|
||||||
|
import Cura 1.0 as Cura
|
||||||
|
|
||||||
UM.TooltipArea
|
UM.TooltipArea
|
||||||
{
|
{
|
||||||
|
@ -15,7 +17,7 @@ UM.TooltipArea
|
||||||
width: childrenRect.width;
|
width: childrenRect.width;
|
||||||
height: childrenRect.height;
|
height: childrenRect.height;
|
||||||
|
|
||||||
CheckBox
|
UM.CheckBox
|
||||||
{
|
{
|
||||||
id: check
|
id: check
|
||||||
|
|
||||||
|
|
|
@ -62,7 +62,7 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
|
||||||
all_instances = settings.findInstances()
|
all_instances = settings.findInstances()
|
||||||
visibility_changed = False # Flag to check if at the end the signal needs to be emitted
|
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:
|
for instance in all_instances:
|
||||||
# exceptionally skip setting
|
# exceptionally skip setting
|
||||||
if instance.definition.key in self._skip_reset_setting_set:
|
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)
|
settings.removeInstance(instance.definition.key)
|
||||||
visibility_changed = True
|
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:
|
for item in visible:
|
||||||
if settings.getInstance(item) is not None: # Setting was added already.
|
if settings.getInstance(item) is not None: # Setting was added already.
|
||||||
continue
|
continue
|
||||||
definition = self._stack.getSettingDefinition(item)
|
definition = self._stack.getSettingDefinition(item)
|
||||||
if not definition:
|
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
|
continue
|
||||||
|
|
||||||
new_instance = SettingInstance(definition, settings)
|
new_instance = SettingInstance(definition, settings)
|
||||||
stack_nr = -1
|
stack_nr = -1
|
||||||
stack = None
|
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 self._stack.getProperty("machine_extruder_count", "value") > 1:
|
||||||
if definition.limit_to_extruder != "-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")))))
|
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.
|
# 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:
|
if stack_nr not in ExtruderManager.getInstance().extruderIds and self._stack.getProperty("extruder_nr", "value") is not None:
|
||||||
stack_nr = -1
|
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:
|
if stack_nr in ExtruderManager.getInstance().extruderIds:
|
||||||
stack = ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0]
|
stack = ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0]
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -60,13 +60,14 @@ UM.Dialog
|
||||||
onTextChanged: settingPickDialog.updateFilter()
|
onTextChanged: settingPickDialog.updateFilter()
|
||||||
}
|
}
|
||||||
|
|
||||||
CheckBox
|
UM.CheckBox
|
||||||
{
|
{
|
||||||
id: toggleShowAll
|
id: toggleShowAll
|
||||||
anchors
|
anchors
|
||||||
{
|
{
|
||||||
top: parent.top
|
top: parent.top
|
||||||
right: parent.right
|
right: parent.right
|
||||||
|
verticalCenter: filterInput.verticalCenter
|
||||||
}
|
}
|
||||||
text: catalog.i18nc("@label:checkbox", "Show all")
|
text: catalog.i18nc("@label:checkbox", "Show all")
|
||||||
}
|
}
|
||||||
|
|
|
@ -193,6 +193,8 @@ class PostProcessingPlugin(QObject, Extension):
|
||||||
|
|
||||||
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name,
|
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name,
|
||||||
file_path)
|
file_path)
|
||||||
|
if spec is None:
|
||||||
|
continue
|
||||||
loaded_script = importlib.util.module_from_spec(spec)
|
loaded_script = importlib.util.module_from_spec(spec)
|
||||||
if spec.loader is None:
|
if spec.loader is None:
|
||||||
continue
|
continue
|
||||||
|
|
|
@ -499,10 +499,8 @@ UM.Dialog
|
||||||
visible: activeScriptsList.count > 0
|
visible: activeScriptsList.count > 0
|
||||||
anchors
|
anchors
|
||||||
{
|
{
|
||||||
top: parent.top
|
horizontalCenter: parent.right
|
||||||
right: parent.right
|
verticalCenter: parent.top
|
||||||
rightMargin: (-0.5 * width) | 0
|
|
||||||
topMargin: (-0.5 * height) | 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
labelText: activeScriptsList.count
|
labelText: activeScriptsList.count
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
# Cura PostProcessingPlugin
|
# Cura PostProcessingPlugin
|
||||||
# Author: Mathias Lyngklip Kjeldgaard, Alexander Gee, Kimmo Toivanen
|
# Author: Mathias Lyngklip Kjeldgaard, Alexander Gee, Kimmo Toivanen, Inigo Martinez
|
||||||
# Date: July 31, 2019
|
# 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.
|
# 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",
|
"type": "enum",
|
||||||
"options": {
|
"options": {
|
||||||
"m117":"M117 - All printers",
|
"m117":"M117 - All printers",
|
||||||
"m73":"M73 - Prusa, Marlin 2"
|
"m73":"M73 - Prusa, Marlin 2",
|
||||||
|
"m118":"M118 - Octoprint"
|
||||||
},
|
},
|
||||||
"enabled": "time_remaining",
|
"enabled": "time_remaining",
|
||||||
"default_value": "m117"
|
"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))
|
current_time_string = "{:d}h{:02d}m{:02d}s".format(int(h), int(m), int(s))
|
||||||
# And now insert that into the GCODE
|
# And now insert that into the GCODE
|
||||||
lines.insert(line_index, "M117 Time Left {}".format(current_time_string))
|
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:
|
else:
|
||||||
mins = int(60 * h + m + s / 30)
|
mins = int(60 * h + m + s / 30)
|
||||||
lines.insert(line_index, "M73 R{}".format(mins))
|
lines.insert(line_index, "M73 R{}".format(mins))
|
||||||
|
@ -107,6 +112,9 @@ class DisplayProgressOnLCD(Script):
|
||||||
|
|
||||||
if output_percentage:
|
if output_percentage:
|
||||||
# Emit 0 percent to sure Marlin knows we are overriding the completion percentage
|
# Emit 0 percent to sure Marlin knows we are overriding the completion percentage
|
||||||
|
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")
|
lines.insert(line_index, "M73 P0")
|
||||||
|
|
||||||
elif line.startswith(";TIME_ELAPSED:"):
|
elif line.startswith(";TIME_ELAPSED:"):
|
||||||
|
@ -178,6 +186,9 @@ class DisplayProgressOnLCD(Script):
|
||||||
output = min(percentage + previous_layer_end_percentage, 100)
|
output = min(percentage + previous_layer_end_percentage, 100)
|
||||||
|
|
||||||
# Now insert the sanitized percentage into the GCODE
|
# Now insert the sanitized percentage into the GCODE
|
||||||
|
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))
|
lines.insert(percentage_line_index, "M73 P{}".format(output))
|
||||||
|
|
||||||
previous_layer_end_percentage = layer_end_percentage
|
previous_layer_end_percentage = layer_end_percentage
|
||||||
|
|
|
@ -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)]
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"name": "Toolbox",
|
|
||||||
"author": "Ultimaker B.V.",
|
|
||||||
"version": "1.0.1",
|
|
||||||
"api": 7,
|
|
||||||
"description": "Find, manage and install new Cura packages."
|
|
||||||
}
|
|
|
@ -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 |
|
@ -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 |
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,196 +0,0 @@
|
||||||
// Copyright (c) 2021 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
|
|
||||||
|
|
||||||
UM.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")
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
elide: Text.ElideRight
|
|
||||||
text: styleData.value || ""
|
|
||||||
font: UM.Theme.getFont("default_bold")
|
|
||||||
}
|
|
||||||
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
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
elide: Text.ElideRight
|
|
||||||
text: styleData.value || ""
|
|
||||||
color: UM.Theme.getColor("text_medium")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
itemDelegate: Item
|
|
||||||
{
|
|
||||||
height: UM.Theme.getSize("toolbox_chart_row").height
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
elide: Text.ElideRight
|
|
||||||
text: styleData.value || ""
|
|
||||||
color: UM.Theme.getColor("text_medium")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component
|
|
||||||
{
|
|
||||||
id: columnTextDelegate
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
anchors.fill: parent
|
|
||||||
|
|
||||||
text: styleData.value || ""
|
|
||||||
elide: Text.ElideRight
|
|
||||||
color: UM.Theme.getColor("text_medium")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.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
|
|
||||||
}
|
|
||||||
onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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.
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
// Copyright (c) 2021 Ultimaker B.V.
|
|
||||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import QtQuick 2.10
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.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")
|
|
||||||
visible: loginRequired
|
|
||||||
width: installButton.width
|
|
||||||
|
|
||||||
MouseArea
|
|
||||||
{
|
|
||||||
anchors.fill: parent
|
|
||||||
onClicked: Cura.API.account.login()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
property var whereToBuyUrl:
|
|
||||||
{
|
|
||||||
var pg_name = "whereToBuy"
|
|
||||||
return (pg_name in packageData.links) ? packageData.links[pg_name] : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Buy material spools</a>")
|
|
||||||
visible: whereToBuyUrl != undefined
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,119 +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.Layouts 1.3
|
|
||||||
import UM 1.5 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.AlignTop | Qt.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
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
id: name
|
|
||||||
text: model.name
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
font: UM.Theme.getFont("default_bold")
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
id: info
|
|
||||||
text: model.description
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
anchors.top: name.bottom
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
maximumLineCount: 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,77 +0,0 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
|
||||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import QtQuick 2.10
|
|
||||||
import UM 1.5 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
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
id: heading
|
|
||||||
text: catalog.i18nc("@label", "Premium")
|
|
||||||
width: contentWidth
|
|
||||||
height: contentHeight
|
|
||||||
color: UM.Theme.getColor("text_medium")
|
|
||||||
font: UM.Theme.getFont("large")
|
|
||||||
}
|
|
||||||
UM.TooltipArea
|
|
||||||
{
|
|
||||||
width: childrenRect.width
|
|
||||||
height: childrenRect.height
|
|
||||||
anchors.right: parent.right
|
|
||||||
text: catalog.i18nc("@info:tooltip", "Go to Web Marketplace")
|
|
||||||
UM.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
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import QtQuick 2.10
|
|
||||||
|
|
||||||
import UM 1.5 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
id: packageName
|
|
||||||
text: model.name
|
|
||||||
anchors
|
|
||||||
{
|
|
||||||
horizontalCenter: parent.horizontalCenter
|
|
||||||
top: thumbnail.bottom
|
|
||||||
}
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
height: UM.Theme.getSize("toolbox_heading_label").height
|
|
||||||
width: parent.width - UM.Theme.getSize("default_margin").width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
elide: Text.ElideRight
|
|
||||||
font: UM.Theme.getFont("medium_bold")
|
|
||||||
}
|
|
||||||
UM.RecolorImage
|
|
||||||
{
|
|
||||||
width: (parent.width * 0.20) | 0
|
|
||||||
height: width
|
|
||||||
anchors
|
|
||||||
{
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,57 +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.5 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
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@info", "You will need to restart Cura before changes in packages have effect.")
|
|
||||||
height: UM.Theme.getSize("toolbox_footer_button").height
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,111 +0,0 @@
|
||||||
// Copyright (c) 2020 Ultimaker B.V.
|
|
||||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import QtQuick 2.10
|
|
||||||
|
|
||||||
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.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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,112 +0,0 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
|
||||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import QtQuick 2.10
|
|
||||||
import UM 1.5 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
|
|
||||||
|
|
||||||
UM.CheckBox
|
|
||||||
{
|
|
||||||
id: disableButton
|
|
||||||
anchors.verticalCenter: pluginInfo.verticalCenter
|
|
||||||
checked: isEnabled
|
|
||||||
visible: model.type == "plugin"
|
|
||||||
width: visible ? UM.Theme.getSize("checkbox").width : 0
|
|
||||||
enabled: !toolbox.isDownloading
|
|
||||||
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)))
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: model.name
|
|
||||||
width: parent.width
|
|
||||||
maximumLineCount: 1
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
font: UM.Theme.getFont("large_bold")
|
|
||||||
color: pluginInfo.color
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: model.description
|
|
||||||
font: UM.Theme.getFont("default")
|
|
||||||
maximumLineCount: 3
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
color: pluginInfo.color
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Column
|
|
||||||
{
|
|
||||||
id: authorInfo
|
|
||||||
width: Math.floor(UM.Theme.getSize("toolbox_action_button").width * 1.25)
|
|
||||||
|
|
||||||
UM.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
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: model.version
|
|
||||||
width: parent.width
|
|
||||||
height: UM.Theme.getSize("toolbox_property_label").height
|
|
||||||
horizontalAlignment: Text.AlignLeft
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ToolboxInstalledTileActions
|
|
||||||
{
|
|
||||||
id: pluginActions
|
|
||||||
}
|
|
||||||
Connections
|
|
||||||
{
|
|
||||||
target: toolbox
|
|
||||||
function onToolboxEnabledChanged() { isEnabled = toolbox.isEnabled(model.id) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,82 +0,0 @@
|
||||||
// Copyright (c) 2018 Ultimaker B.V.
|
|
||||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import QtQuick 2.10
|
|
||||||
import UM 1.5 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
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
visible: !model.is_installed
|
|
||||||
text: catalog.i18nc("@label", "Will install upon restarting")
|
|
||||||
color: UM.Theme.getColor("lining")
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.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")
|
|
||||||
visible: loginRequired
|
|
||||||
width: updateButton.width
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,65 +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.5 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: UM.Label
|
|
||||||
{
|
|
||||||
id: label
|
|
||||||
text: control.text
|
|
||||||
color: UM.Theme.getColor("toolbox_header_button_text_inactive")
|
|
||||||
font: UM.Theme.getFont("medium")
|
|
||||||
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,95 +0,0 @@
|
||||||
// Copyright (c) 2021 Ultimaker B.V.
|
|
||||||
// Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import QtQuick 2.10
|
|
||||||
import QtQuick.Controls 2.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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,109 +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 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() }
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,162 +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.1
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.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
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
id: description
|
|
||||||
text: details && details.description ? details.description : ""
|
|
||||||
color: UM.Theme.getColor("text_medium")
|
|
||||||
anchors
|
|
||||||
{
|
|
||||||
top: title.bottom
|
|
||||||
left: title.left
|
|
||||||
topMargin: UM.Theme.getSize("default_margin").height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "Website") + ":"
|
|
||||||
font: UM.Theme.getFont("default")
|
|
||||||
color: UM.Theme.getColor("text_medium")
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "Email") + ":"
|
|
||||||
font: UM.Theme.getFont("default")
|
|
||||||
color: UM.Theme.getColor("text_medium")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text:
|
|
||||||
{
|
|
||||||
if (details && details.website)
|
|
||||||
{
|
|
||||||
return "<a href=\"" + details.website + "\">" + details.website + "</a>"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
width: parent.width
|
|
||||||
elide: Text.ElideRight
|
|
||||||
onLinkActivated: UM.UrlUtil.openUrl(link, ["https", "http"])
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text:
|
|
||||||
{
|
|
||||||
if (details && details.email)
|
|
||||||
{
|
|
||||||
return "<a href=\"mailto:" + details.email + "\">" + details.email + "</a>"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
onLinkActivated: Qt.openUrlExternally(link)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,156 +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.1
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.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")
|
|
||||||
width: contentWidth
|
|
||||||
height: contentHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "Version") + ":"
|
|
||||||
color: UM.Theme.getColor("text_medium")
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "Last updated") + ":"
|
|
||||||
color: UM.Theme.getColor("text_medium")
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "Brand") + ":"
|
|
||||||
color: UM.Theme.getColor("text_medium")
|
|
||||||
renderType: Text.NativeRendering
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@label", "Downloads") + ":"
|
|
||||||
color: UM.Theme.getColor("text_medium")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: details === null ? "" : (details.version || catalog.i18nc("@label", "Unknown"))
|
|
||||||
}
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text:
|
|
||||||
{
|
|
||||||
if (details === null)
|
|
||||||
{
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
var date = new Date(details.last_updated)
|
|
||||||
return date.toLocaleString(UM.Preferences.getValue("general/language"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: details === null ? "" : "<a href=\"" + details.website + "\">" + details.author_name + "</a>"
|
|
||||||
onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
|
|
||||||
}
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: details === null ? "" : (details.download_count || catalog.i18nc("@label", "Unknown"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ToolboxDetailList
|
|
||||||
{
|
|
||||||
anchors
|
|
||||||
{
|
|
||||||
top: header.bottom
|
|
||||||
bottom: page.bottom
|
|
||||||
left: header.left
|
|
||||||
right: page.right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,12 +0,0 @@
|
||||||
// Copyright (c) 2021 Ultimaker B.V.
|
|
||||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import QtQuick 2.10
|
|
||||||
import QtQuick.Controls 2.1
|
|
||||||
import UM 1.5 as UM
|
|
||||||
|
|
||||||
UM.Label
|
|
||||||
{
|
|
||||||
text: catalog.i18nc("@info", "Could not connect to the Cura Package database. Please check your connection.")
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
|
@ -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 {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,24 +0,0 @@
|
||||||
// Copyright (c) 2021 Ultimaker B.V.
|
|
||||||
// Toolbox is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import QtQuick 2.10
|
|
||||||
import QtQuick.Controls 2.1
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,40 +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.5 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
|
|
||||||
|
|
||||||
UM.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)
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
wrapMode: Label.WordWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -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 PyQt5.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.UserRole + 1, "id")
|
|
||||||
self.addRoleName(Qt.UserRole + 2, "name")
|
|
||||||
self.addRoleName(Qt.UserRole + 3, "email")
|
|
||||||
self.addRoleName(Qt.UserRole + 4, "website")
|
|
||||||
self.addRoleName(Qt.UserRole + 5, "package_count")
|
|
||||||
self.addRoleName(Qt.UserRole + 6, "package_types")
|
|
||||||
self.addRoleName(Qt.UserRole + 7, "icon_url")
|
|
||||||
self.addRoleName(Qt.UserRole + 8, "description")
|
|
||||||
|
|
||||||
# List of filters for queries. The result is the union of the each list of results.
|
|
||||||
self._filter = {} # type: Dict[str, str]
|
|
||||||
|
|
||||||
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()
|
|
|
@ -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
|
|
||||||
)
|
|
|
@ -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)
|
|
|
@ -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 PyQt5.QtCore import QObject
|
|
||||||
from PyQt5.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.HttpStatusCodeAttribute) != 200:
|
|
||||||
Logger.log("w",
|
|
||||||
"Requesting user packages failed, response code %s while trying to connect to %s",
|
|
||||||
reply.attribute(QNetworkRequest.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)
|
|
|
@ -1,41 +0,0 @@
|
||||||
import os
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from PyQt5.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)
|
|
|
@ -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 PyQt5.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
|
|
|
@ -1,77 +0,0 @@
|
||||||
from PyQt5.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()
|
|
|
@ -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 PyQt5.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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,32 +0,0 @@
|
||||||
from UM import i18nCatalog
|
|
||||||
from UM.Message import Message
|
|
||||||
from cura.CuraApplication import CuraApplication
|
|
||||||
|
|
||||||
|
|
||||||
class RestartApplicationPresenter:
|
|
||||||
"""Presents a dialog telling the user that a restart is required to apply changes
|
|
||||||
|
|
||||||
Since we cannot restart Cura, the app is closed instead when the button is clicked
|
|
||||||
"""
|
|
||||||
def __init__(self, app: CuraApplication) -> None:
|
|
||||||
self._app = app
|
|
||||||
self._i18n_catalog = i18nCatalog("cura")
|
|
||||||
|
|
||||||
def present(self) -> None:
|
|
||||||
app_name = self._app.getApplicationDisplayName()
|
|
||||||
|
|
||||||
message = Message(self._i18n_catalog.i18nc("@info:generic",
|
|
||||||
"You need to quit and restart {} before changes have effect.",
|
|
||||||
app_name))
|
|
||||||
|
|
||||||
message.addAction("quit",
|
|
||||||
name="Quit " + app_name,
|
|
||||||
icon = "",
|
|
||||||
description="Close the application",
|
|
||||||
button_align=Message.ActionButtonAlignment.ALIGN_RIGHT)
|
|
||||||
|
|
||||||
message.actionTriggered.connect(self._quitClicked)
|
|
||||||
message.show()
|
|
||||||
|
|
||||||
def _quitClicked(self, *_):
|
|
||||||
self._app.windowClosed()
|
|
|
@ -1,74 +0,0 @@
|
||||||
# Copyright (c) 2020 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, pyqtProperty, pyqtSlot
|
|
||||||
|
|
||||||
from UM.PackageManager import PackageManager
|
|
||||||
from UM.Qt.ListModel import ListModel
|
|
||||||
from UM.Version import Version
|
|
||||||
|
|
||||||
from cura import ApplicationMetadata
|
|
||||||
from typing import List, Dict, Any
|
|
||||||
|
|
||||||
|
|
||||||
class SubscribedPackagesModel(ListModel):
|
|
||||||
def __init__(self, parent = None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self._items = []
|
|
||||||
self._metadata = None
|
|
||||||
self._discrepancies = None
|
|
||||||
self._sdk_version = ApplicationMetadata.CuraSDKVersion
|
|
||||||
|
|
||||||
self.addRoleName(Qt.UserRole + 1, "package_id")
|
|
||||||
self.addRoleName(Qt.UserRole + 2, "display_name")
|
|
||||||
self.addRoleName(Qt.UserRole + 3, "icon_url")
|
|
||||||
self.addRoleName(Qt.UserRole + 4, "is_compatible")
|
|
||||||
self.addRoleName(Qt.UserRole + 5, "is_dismissed")
|
|
||||||
|
|
||||||
@pyqtProperty(bool, constant=True)
|
|
||||||
def hasCompatiblePackages(self) -> bool:
|
|
||||||
for item in self._items:
|
|
||||||
if item['is_compatible']:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
@pyqtProperty(bool, constant=True)
|
|
||||||
def hasIncompatiblePackages(self) -> bool:
|
|
||||||
for item in self._items:
|
|
||||||
if not item['is_compatible']:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
def addDiscrepancies(self, discrepancy: List[str]) -> None:
|
|
||||||
self._discrepancies = discrepancy
|
|
||||||
|
|
||||||
def getCompatiblePackages(self) -> List[Dict[str, Any]]:
|
|
||||||
return [package for package in self._items if package["is_compatible"]]
|
|
||||||
|
|
||||||
def getIncompatiblePackages(self) -> List[str]:
|
|
||||||
return [package["package_id"] for package in self._items if not package["is_compatible"]]
|
|
||||||
|
|
||||||
def initialize(self, package_manager: PackageManager, subscribed_packages_payload: List[Dict[str, Any]]) -> None:
|
|
||||||
self._items.clear()
|
|
||||||
for item in subscribed_packages_payload:
|
|
||||||
if item["package_id"] not in self._discrepancies:
|
|
||||||
continue
|
|
||||||
package = {
|
|
||||||
"package_id": item["package_id"],
|
|
||||||
"display_name": item["display_name"],
|
|
||||||
"sdk_versions": item["sdk_versions"],
|
|
||||||
"download_url": item["download_url"],
|
|
||||||
"md5_hash": item["md5_hash"],
|
|
||||||
"is_dismissed": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
compatible = any(package_manager.isPackageCompatible(Version(version)) for version in item["sdk_versions"])
|
|
||||||
package.update({"is_compatible": compatible})
|
|
||||||
|
|
||||||
try:
|
|
||||||
package.update({"icon_url": item["icon_url"]})
|
|
||||||
except KeyError: # There is no 'icon_url" in the response payload for this package
|
|
||||||
package.update({"icon_url": ""})
|
|
||||||
self._items.append(package)
|
|
||||||
self.setItems(self._items)
|
|
|
@ -1,114 +0,0 @@
|
||||||
# Copyright (c) 2021 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import os
|
|
||||||
from typing import List, Dict, Any, cast
|
|
||||||
|
|
||||||
from UM import i18n_catalog
|
|
||||||
from UM.Extension import Extension
|
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.Message import Message
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
|
||||||
from cura.CuraApplication import CuraApplication
|
|
||||||
from .CloudPackageChecker import CloudPackageChecker
|
|
||||||
from .CloudApiClient import CloudApiClient
|
|
||||||
from .DiscrepanciesPresenter import DiscrepanciesPresenter
|
|
||||||
from .DownloadPresenter import DownloadPresenter
|
|
||||||
from .LicensePresenter import LicensePresenter
|
|
||||||
from .RestartApplicationPresenter import RestartApplicationPresenter
|
|
||||||
from .SubscribedPackagesModel import SubscribedPackagesModel
|
|
||||||
|
|
||||||
|
|
||||||
class SyncOrchestrator(Extension):
|
|
||||||
"""Orchestrates the synchronizing of packages from the user account to the installed packages
|
|
||||||
|
|
||||||
Example flow:
|
|
||||||
|
|
||||||
- CloudPackageChecker compares a list of packages the user `subscribed` to in their account
|
|
||||||
If there are `discrepancies` between the account and locally installed packages, they are emitted
|
|
||||||
- DiscrepanciesPresenter shows a list of packages to be added or removed to the user. It emits the `packageMutations`
|
|
||||||
the user selected to be performed
|
|
||||||
- The SyncOrchestrator uses PackageManager to remove local packages the users wants to see removed
|
|
||||||
- The DownloadPresenter shows a download progress dialog. It emits A tuple of succeeded and failed downloads
|
|
||||||
- The LicensePresenter extracts licenses from the downloaded packages and presents a license for each package to
|
|
||||||
be installed. It emits the `licenseAnswers` signal for accept or declines
|
|
||||||
- The CloudApiClient removes the declined packages from the account
|
|
||||||
- The SyncOrchestrator uses PackageManager to install the downloaded packages and delete temp files.
|
|
||||||
- The RestartApplicationPresenter notifies the user that a restart is required for changes to take effect
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, app: CuraApplication) -> None:
|
|
||||||
super().__init__()
|
|
||||||
# Differentiate This PluginObject from the Toolbox. self.getId() includes _name.
|
|
||||||
# getPluginId() will return the same value for The toolbox extension and this one
|
|
||||||
self._name = "SyncOrchestrator"
|
|
||||||
|
|
||||||
self._package_manager = app.getPackageManager()
|
|
||||||
# Keep a reference to the CloudApiClient. it watches for installed packages and subscribes to them
|
|
||||||
self._cloud_api = CloudApiClient.getInstance(app) # type: CloudApiClient
|
|
||||||
|
|
||||||
self._checker = CloudPackageChecker(app) # type: CloudPackageChecker
|
|
||||||
self._checker.discrepancies.connect(self._onDiscrepancies)
|
|
||||||
|
|
||||||
self._discrepancies_presenter = DiscrepanciesPresenter(app) # type: DiscrepanciesPresenter
|
|
||||||
self._discrepancies_presenter.packageMutations.connect(self._onPackageMutations)
|
|
||||||
|
|
||||||
self._download_presenter = DownloadPresenter(app) # type: DownloadPresenter
|
|
||||||
|
|
||||||
self._license_presenter = LicensePresenter(app) # type: LicensePresenter
|
|
||||||
self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers)
|
|
||||||
|
|
||||||
self._restart_presenter = RestartApplicationPresenter(app)
|
|
||||||
|
|
||||||
def _onDiscrepancies(self, model: SubscribedPackagesModel) -> None:
|
|
||||||
plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId()))
|
|
||||||
self._discrepancies_presenter.present(plugin_path, model)
|
|
||||||
|
|
||||||
def _onPackageMutations(self, mutations: SubscribedPackagesModel) -> None:
|
|
||||||
self._download_presenter = self._download_presenter.resetCopy()
|
|
||||||
self._download_presenter.done.connect(self._onDownloadFinished)
|
|
||||||
self._download_presenter.download(mutations)
|
|
||||||
|
|
||||||
def _onDownloadFinished(self, success_items: Dict[str, Dict[str, str]], error_items: List[str]) -> None:
|
|
||||||
"""Called when a set of packages have finished downloading
|
|
||||||
|
|
||||||
:param success_items:: Dict[package_id, Dict[str, str]]
|
|
||||||
:param error_items:: List[package_id]
|
|
||||||
"""
|
|
||||||
if error_items:
|
|
||||||
message = i18n_catalog.i18nc("@info:generic", "{} plugins failed to download".format(len(error_items)))
|
|
||||||
self._showErrorMessage(message)
|
|
||||||
|
|
||||||
plugin_path = cast(str, PluginRegistry.getInstance().getPluginPath(self.getPluginId()))
|
|
||||||
self._license_presenter = self._license_presenter.resetCopy()
|
|
||||||
self._license_presenter.licenseAnswers.connect(self._onLicenseAnswers)
|
|
||||||
self._license_presenter.present(plugin_path, success_items)
|
|
||||||
|
|
||||||
# Called when user has accepted / declined all licenses for the downloaded packages
|
|
||||||
def _onLicenseAnswers(self, answers: List[Dict[str, Any]]) -> None:
|
|
||||||
has_changes = False # True when at least one package is installed
|
|
||||||
|
|
||||||
for item in answers:
|
|
||||||
if item["accepted"]:
|
|
||||||
# install and subscribe packages
|
|
||||||
if not self._package_manager.installPackage(item["package_path"]):
|
|
||||||
message = "Could not install {}".format(item["package_id"])
|
|
||||||
self._showErrorMessage(message)
|
|
||||||
continue
|
|
||||||
has_changes = True
|
|
||||||
else:
|
|
||||||
self._cloud_api.unsubscribe(item["package_id"])
|
|
||||||
# delete temp file
|
|
||||||
try:
|
|
||||||
os.remove(item["package_path"])
|
|
||||||
except EnvironmentError as e: # File was already removed, no access rights, etc.
|
|
||||||
Logger.error("Can't delete temporary package file: {err}".format(err = str(e)))
|
|
||||||
|
|
||||||
if has_changes:
|
|
||||||
self._restart_presenter.present()
|
|
||||||
|
|
||||||
def _showErrorMessage(self, text: str):
|
|
||||||
"""Logs an error and shows it to the user"""
|
|
||||||
|
|
||||||
Logger.error(text)
|
|
||||||
Message(text, lifetime = 0, message_type = Message.MessageType.ERROR).show()
|
|
|
@ -1,38 +0,0 @@
|
||||||
# Copyright (c) 2018 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt
|
|
||||||
|
|
||||||
from UM.Qt.ListModel import ListModel
|
|
||||||
|
|
||||||
|
|
||||||
class ConfigsModel(ListModel):
|
|
||||||
"""Model that holds supported configurations (for material/quality packages)."""
|
|
||||||
|
|
||||||
def __init__(self, parent = None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self._configs = None
|
|
||||||
|
|
||||||
self.addRoleName(Qt.UserRole + 1, "machine")
|
|
||||||
self.addRoleName(Qt.UserRole + 2, "print_core")
|
|
||||||
self.addRoleName(Qt.UserRole + 3, "build_plate")
|
|
||||||
self.addRoleName(Qt.UserRole + 4, "support_material")
|
|
||||||
self.addRoleName(Qt.UserRole + 5, "quality")
|
|
||||||
|
|
||||||
def setConfigs(self, configs):
|
|
||||||
self._configs = configs
|
|
||||||
self._update()
|
|
||||||
|
|
||||||
def _update(self):
|
|
||||||
items = []
|
|
||||||
for item in self._configs:
|
|
||||||
items.append({
|
|
||||||
"machine": item["machine"],
|
|
||||||
"print_core": item["print_core"],
|
|
||||||
"build_plate": item["build_plate"],
|
|
||||||
"support_material": item["support_material"],
|
|
||||||
"quality": item["quality"]
|
|
||||||
})
|
|
||||||
|
|
||||||
self.setItems(items)
|
|
|
@ -1,161 +0,0 @@
|
||||||
# Copyright (c) 2021 Ultimaker B.V.
|
|
||||||
# Cura is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import re
|
|
||||||
from typing import Dict
|
|
||||||
|
|
||||||
from PyQt5.QtCore import Qt, pyqtProperty
|
|
||||||
|
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.Qt.ListModel import ListModel
|
|
||||||
|
|
||||||
from .ConfigsModel import ConfigsModel
|
|
||||||
|
|
||||||
|
|
||||||
class PackagesModel(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):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self._metadata = None
|
|
||||||
|
|
||||||
self.addRoleName(Qt.UserRole + 1, "id")
|
|
||||||
self.addRoleName(Qt.UserRole + 2, "type")
|
|
||||||
self.addRoleName(Qt.UserRole + 3, "name")
|
|
||||||
self.addRoleName(Qt.UserRole + 4, "version")
|
|
||||||
self.addRoleName(Qt.UserRole + 5, "author_id")
|
|
||||||
self.addRoleName(Qt.UserRole + 6, "author_name")
|
|
||||||
self.addRoleName(Qt.UserRole + 7, "author_email")
|
|
||||||
self.addRoleName(Qt.UserRole + 8, "description")
|
|
||||||
self.addRoleName(Qt.UserRole + 9, "icon_url")
|
|
||||||
self.addRoleName(Qt.UserRole + 10, "image_urls")
|
|
||||||
self.addRoleName(Qt.UserRole + 11, "download_url")
|
|
||||||
self.addRoleName(Qt.UserRole + 12, "last_updated")
|
|
||||||
self.addRoleName(Qt.UserRole + 13, "is_bundled")
|
|
||||||
self.addRoleName(Qt.UserRole + 14, "is_active")
|
|
||||||
self.addRoleName(Qt.UserRole + 15, "is_installed") # Scheduled pkgs are included in the model but should not be marked as actually installed
|
|
||||||
self.addRoleName(Qt.UserRole + 16, "has_configs")
|
|
||||||
self.addRoleName(Qt.UserRole + 17, "supported_configs")
|
|
||||||
self.addRoleName(Qt.UserRole + 18, "download_count")
|
|
||||||
self.addRoleName(Qt.UserRole + 19, "tags")
|
|
||||||
self.addRoleName(Qt.UserRole + 20, "links")
|
|
||||||
self.addRoleName(Qt.UserRole + 21, "website")
|
|
||||||
self.addRoleName(Qt.UserRole + 22, "login_required")
|
|
||||||
|
|
||||||
# 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):
|
|
||||||
if self._metadata != data:
|
|
||||||
self._metadata = data
|
|
||||||
self._update()
|
|
||||||
|
|
||||||
def _update(self):
|
|
||||||
items = []
|
|
||||||
|
|
||||||
if self._metadata is None:
|
|
||||||
self.setItems(items)
|
|
||||||
return
|
|
||||||
|
|
||||||
for package in self._metadata:
|
|
||||||
has_configs = False
|
|
||||||
configs_model = None
|
|
||||||
|
|
||||||
links_dict = {}
|
|
||||||
if "data" in package:
|
|
||||||
# Links is a list of dictionaries with "title" and "url". Convert this list into a dict so it's easier
|
|
||||||
# to process.
|
|
||||||
link_list = package["data"]["links"] if "links" in package["data"] else []
|
|
||||||
links_dict = {d["title"]: d["url"] for d in link_list}
|
|
||||||
|
|
||||||
# This code never gets executed because the API response does not contain "supported_configs" in it
|
|
||||||
# It is so because 2y ago when this was created - it did contain it. But it was a prototype only
|
|
||||||
# and never got to production. As agreed with the team, it'll stay here for now, in case we decide to rework and use it
|
|
||||||
# The response payload has been changed. Please see:
|
|
||||||
# https://github.com/Ultimaker/Cura/compare/CURA-7072-temp?expand=1
|
|
||||||
if "supported_configs" in package["data"]:
|
|
||||||
if len(package["data"]["supported_configs"]) > 0:
|
|
||||||
has_configs = True
|
|
||||||
configs_model = ConfigsModel()
|
|
||||||
configs_model.setConfigs(package["data"]["supported_configs"])
|
|
||||||
|
|
||||||
if "author_id" not in package["author"] or "display_name" not in package["author"]:
|
|
||||||
package["author"]["author_id"] = ""
|
|
||||||
package["author"]["display_name"] = ""
|
|
||||||
|
|
||||||
items.append({
|
|
||||||
"id": package["package_id"],
|
|
||||||
"type": package["package_type"],
|
|
||||||
"name": package["display_name"].strip(),
|
|
||||||
"version": package["package_version"],
|
|
||||||
"author_id": package["author"]["author_id"],
|
|
||||||
"author_name": package["author"]["display_name"],
|
|
||||||
"author_email": package["author"]["email"] if "email" in package["author"] else None,
|
|
||||||
"description": package["description"] if "description" in package else None,
|
|
||||||
"icon_url": package["icon_url"] if "icon_url" in package else None,
|
|
||||||
"image_urls": package["image_urls"] if "image_urls" in package else None,
|
|
||||||
"download_url": package["download_url"] if "download_url" in package else None,
|
|
||||||
"last_updated": package["last_updated"] if "last_updated" in package else None,
|
|
||||||
"is_bundled": package["is_bundled"] if "is_bundled" in package else False,
|
|
||||||
"is_active": package["is_active"] if "is_active" in package else False,
|
|
||||||
"is_installed": package["is_installed"] if "is_installed" in package else False,
|
|
||||||
"has_configs": has_configs,
|
|
||||||
"supported_configs": configs_model,
|
|
||||||
"download_count": package["download_count"] if "download_count" in package else 0,
|
|
||||||
"tags": package["tags"] if "tags" in package else [],
|
|
||||||
"links": links_dict,
|
|
||||||
"website": package["website"] if "website" in package else None,
|
|
||||||
"login_required": "login-required" in package.get("tags", []),
|
|
||||||
})
|
|
||||||
|
|
||||||
# Filter on all the key-word arguments.
|
|
||||||
for key, value in self._filter.items():
|
|
||||||
if key == "tags":
|
|
||||||
key_filter = lambda item, v = value: v in item["tags"]
|
|
||||||
elif "*" in value:
|
|
||||||
key_filter = lambda candidate, k = key, v = value: self._matchRegExp(candidate, k, v)
|
|
||||||
else:
|
|
||||||
key_filter = lambda candidate, k = key, v = value: self._matchString(candidate, k, v)
|
|
||||||
items = filter(key_filter, items)
|
|
||||||
|
|
||||||
# Execute all filters.
|
|
||||||
filtered_items = list(items)
|
|
||||||
|
|
||||||
filtered_items.sort(key = lambda k: 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()
|
|
|
@ -1,878 +0,0 @@
|
||||||
# Copyright (c) 2021 Ultimaker B.V.
|
|
||||||
# Toolbox is released under the terms of the LGPLv3 or higher.
|
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import tempfile
|
|
||||||
from typing import cast, Any, Dict, List, Set, TYPE_CHECKING, Tuple, Optional, Union
|
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtProperty, pyqtSignal, pyqtSlot
|
|
||||||
from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply
|
|
||||||
|
|
||||||
from UM.Extension import Extension
|
|
||||||
from UM.Logger import Logger
|
|
||||||
from UM.PluginRegistry import PluginRegistry
|
|
||||||
from UM.TaskManagement.HttpRequestScope import JsonDecoratorScope
|
|
||||||
from UM.Version import Version
|
|
||||||
from UM.i18n import i18nCatalog
|
|
||||||
from cura import ApplicationMetadata
|
|
||||||
from cura.CuraApplication import CuraApplication
|
|
||||||
from cura.Machines.ContainerTree import ContainerTree
|
|
||||||
from cura.UltimakerCloud.UltimakerCloudScope import UltimakerCloudScope
|
|
||||||
from .AuthorsModel import AuthorsModel
|
|
||||||
from .CloudApiModel import CloudApiModel
|
|
||||||
from .CloudSync.LicenseModel import LicenseModel
|
|
||||||
from .PackagesModel import PackagesModel
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from UM.TaskManagement.HttpRequestData import HttpRequestData
|
|
||||||
from cura.Settings.GlobalStack import GlobalStack
|
|
||||||
|
|
||||||
i18n_catalog = i18nCatalog("cura")
|
|
||||||
|
|
||||||
DEFAULT_MARKETPLACE_ROOT = "https://marketplace.ultimaker.com" # type: str
|
|
||||||
|
|
||||||
try:
|
|
||||||
from cura.CuraVersion import CuraMarketplaceRoot
|
|
||||||
except ImportError:
|
|
||||||
CuraMarketplaceRoot = DEFAULT_MARKETPLACE_ROOT
|
|
||||||
|
|
||||||
|
|
||||||
class Toolbox(QObject, Extension):
|
|
||||||
"""Provides a marketplace for users to download plugins an materials"""
|
|
||||||
|
|
||||||
def __init__(self, application: CuraApplication) -> None:
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self._application = application # type: CuraApplication
|
|
||||||
|
|
||||||
# Network:
|
|
||||||
self._download_request_data = None # type: Optional[HttpRequestData]
|
|
||||||
self._download_progress = 0 # type: float
|
|
||||||
self._is_downloading = False # type: bool
|
|
||||||
self._cloud_scope = UltimakerCloudScope(application) # type: UltimakerCloudScope
|
|
||||||
self._json_scope = JsonDecoratorScope(self._cloud_scope) # type: JsonDecoratorScope
|
|
||||||
|
|
||||||
self._request_urls = {} # type: Dict[str, str]
|
|
||||||
self._to_update = [] # type: List[str] # Package_ids that are waiting to be updated
|
|
||||||
self._old_plugin_ids = set() # type: Set[str]
|
|
||||||
self._old_plugin_metadata = dict() # type: Dict[str, Dict[str, Any]]
|
|
||||||
|
|
||||||
# The responses as given by the server parsed to a list.
|
|
||||||
self._server_response_data = {
|
|
||||||
"authors": [],
|
|
||||||
"packages": [],
|
|
||||||
"updates": []
|
|
||||||
} # type: Dict[str, List[Any]]
|
|
||||||
|
|
||||||
# Models:
|
|
||||||
self._models = {
|
|
||||||
"authors": AuthorsModel(self),
|
|
||||||
"packages": PackagesModel(self),
|
|
||||||
"updates": PackagesModel(self)
|
|
||||||
} # type: Dict[str, Union[AuthorsModel, PackagesModel]]
|
|
||||||
|
|
||||||
self._plugins_showcase_model = PackagesModel(self)
|
|
||||||
self._plugins_available_model = PackagesModel(self)
|
|
||||||
self._plugins_installed_model = PackagesModel(self)
|
|
||||||
self._plugins_installed_model.setFilter({"is_bundled": "False"})
|
|
||||||
self._plugins_bundled_model = PackagesModel(self)
|
|
||||||
self._plugins_bundled_model.setFilter({"is_bundled": "True"})
|
|
||||||
self._materials_showcase_model = AuthorsModel(self)
|
|
||||||
self._materials_available_model = AuthorsModel(self)
|
|
||||||
self._materials_installed_model = PackagesModel(self)
|
|
||||||
self._materials_installed_model.setFilter({"is_bundled": "False"})
|
|
||||||
self._materials_bundled_model = PackagesModel(self)
|
|
||||||
self._materials_bundled_model.setFilter({"is_bundled": "True"})
|
|
||||||
self._materials_generic_model = PackagesModel(self)
|
|
||||||
|
|
||||||
self._license_model = LicenseModel()
|
|
||||||
|
|
||||||
# These properties are for keeping track of the UI state:
|
|
||||||
# ----------------------------------------------------------------------
|
|
||||||
# View category defines which filter to use, and therefore effectively
|
|
||||||
# which category is currently being displayed. For example, possible
|
|
||||||
# values include "plugin" or "material", but also "installed".
|
|
||||||
self._view_category = "plugin" # type: str
|
|
||||||
|
|
||||||
# View page defines which type of page layout to use. For example,
|
|
||||||
# possible values include "overview", "detail" or "author".
|
|
||||||
self._view_page = "welcome" # type: str
|
|
||||||
|
|
||||||
# Active package refers to which package is currently being downloaded,
|
|
||||||
# installed, or otherwise modified.
|
|
||||||
self._active_package = None # type: Optional[Dict[str, Any]]
|
|
||||||
|
|
||||||
self._dialog = None # type: Optional[QObject]
|
|
||||||
self._confirm_reset_dialog = None # type: Optional[QObject]
|
|
||||||
self._resetUninstallVariables()
|
|
||||||
|
|
||||||
self._restart_required = False # type: bool
|
|
||||||
|
|
||||||
# variables for the license agreement dialog
|
|
||||||
self._license_dialog_plugin_file_location = "" # type: str
|
|
||||||
|
|
||||||
self._application.initializationFinished.connect(self._onAppInitialized)
|
|
||||||
|
|
||||||
# Signals:
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
# Downloading changes
|
|
||||||
activePackageChanged = pyqtSignal()
|
|
||||||
onDownloadProgressChanged = pyqtSignal()
|
|
||||||
onIsDownloadingChanged = pyqtSignal()
|
|
||||||
restartRequiredChanged = pyqtSignal()
|
|
||||||
installChanged = pyqtSignal()
|
|
||||||
toolboxEnabledChanged = pyqtSignal()
|
|
||||||
|
|
||||||
# UI changes
|
|
||||||
viewChanged = pyqtSignal()
|
|
||||||
detailViewChanged = pyqtSignal()
|
|
||||||
filterChanged = pyqtSignal()
|
|
||||||
metadataChanged = pyqtSignal()
|
|
||||||
showLicenseDialog = pyqtSignal()
|
|
||||||
closeLicenseDialog = pyqtSignal()
|
|
||||||
uninstallVariablesChanged = pyqtSignal()
|
|
||||||
|
|
||||||
def _restart(self):
|
|
||||||
"""Go back to the start state (welcome screen or loading if no login required)"""
|
|
||||||
|
|
||||||
# For an Essentials build, login is mandatory
|
|
||||||
if not self._application.getCuraAPI().account.isLoggedIn and ApplicationMetadata.IsEnterpriseVersion:
|
|
||||||
self.setViewPage("welcome")
|
|
||||||
else:
|
|
||||||
self.setViewPage("loading")
|
|
||||||
self._fetchPackageData()
|
|
||||||
|
|
||||||
def _resetUninstallVariables(self) -> None:
|
|
||||||
self._package_id_to_uninstall = None # type: Optional[str]
|
|
||||||
self._package_name_to_uninstall = ""
|
|
||||||
self._package_used_materials = [] # type: List[Tuple[GlobalStack, str, str]]
|
|
||||||
self._package_used_qualities = [] # type: List[Tuple[GlobalStack, str, str]]
|
|
||||||
|
|
||||||
def getLicenseDialogPluginFileLocation(self) -> str:
|
|
||||||
return self._license_dialog_plugin_file_location
|
|
||||||
|
|
||||||
def openLicenseDialog(self, plugin_name: str, license_content: str, plugin_file_location: str, icon_url: str) -> None:
|
|
||||||
# Set page 1/1 when opening the dialog for a single package
|
|
||||||
self._license_model.setCurrentPageIdx(0)
|
|
||||||
self._license_model.setPageCount(1)
|
|
||||||
self._license_model.setIconUrl(icon_url)
|
|
||||||
|
|
||||||
self._license_model.setPackageName(plugin_name)
|
|
||||||
self._license_model.setLicenseText(license_content)
|
|
||||||
self._license_dialog_plugin_file_location = plugin_file_location
|
|
||||||
self.showLicenseDialog.emit()
|
|
||||||
|
|
||||||
# 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._plugin_registry = self._application.getPluginRegistry()
|
|
||||||
self._package_manager = self._application.getPackageManager()
|
|
||||||
|
|
||||||
# We need to construct a query like installed_packages=ID:VERSION&installed_packages=ID:VERSION, etc.
|
|
||||||
installed_package_ids_with_versions = [":".join(items) for items in
|
|
||||||
self._package_manager.getAllInstalledPackageIdsAndVersions()]
|
|
||||||
installed_packages_query = "&installed_packages=".join(installed_package_ids_with_versions)
|
|
||||||
|
|
||||||
self._request_urls = {
|
|
||||||
"authors": "{base_url}/authors".format(base_url = CloudApiModel.api_url),
|
|
||||||
"packages": "{base_url}/packages".format(base_url = CloudApiModel.api_url),
|
|
||||||
"updates": "{base_url}/packages/package-updates?installed_packages={query}".format(
|
|
||||||
base_url = CloudApiModel.api_url, query = installed_packages_query)
|
|
||||||
}
|
|
||||||
|
|
||||||
self._application.getCuraAPI().account.loginStateChanged.connect(self._restart)
|
|
||||||
|
|
||||||
preferences = CuraApplication.getInstance().getPreferences()
|
|
||||||
|
|
||||||
preferences.addPreference("info/automatic_plugin_update_check", True)
|
|
||||||
|
|
||||||
# On boot we check which packages have updates.
|
|
||||||
if preferences.getValue("info/automatic_plugin_update_check") and len(installed_package_ids_with_versions) > 0:
|
|
||||||
# Request the latest and greatest!
|
|
||||||
self._makeRequestByType("updates")
|
|
||||||
|
|
||||||
def _fetchPackageData(self) -> None:
|
|
||||||
self._makeRequestByType("packages")
|
|
||||||
self._makeRequestByType("authors")
|
|
||||||
self._updateInstalledModels()
|
|
||||||
|
|
||||||
# Displays the toolbox
|
|
||||||
@pyqtSlot()
|
|
||||||
def launch(self) -> None:
|
|
||||||
if not self._dialog:
|
|
||||||
self._dialog = self._createDialog("Toolbox.qml")
|
|
||||||
|
|
||||||
if not self._dialog:
|
|
||||||
Logger.log("e", "Unexpected error trying to create the 'Marketplace' dialog.")
|
|
||||||
return
|
|
||||||
|
|
||||||
self._restart()
|
|
||||||
|
|
||||||
self._dialog.show()
|
|
||||||
# Apply enabled/disabled state to installed plugins
|
|
||||||
self.toolboxEnabledChanged.emit()
|
|
||||||
|
|
||||||
def _createDialog(self, qml_name: str) -> Optional[QObject]:
|
|
||||||
Logger.log("d", "Marketplace: Creating dialog [%s].", qml_name)
|
|
||||||
plugin_path = PluginRegistry.getInstance().getPluginPath(self.getPluginId())
|
|
||||||
if not plugin_path:
|
|
||||||
return None
|
|
||||||
path = os.path.join(plugin_path, "resources", "qml", qml_name)
|
|
||||||
|
|
||||||
dialog = self._application.createQmlComponent(path, {
|
|
||||||
"toolbox": self,
|
|
||||||
"handler": self,
|
|
||||||
"licenseModel": self._license_model
|
|
||||||
})
|
|
||||||
if not dialog:
|
|
||||||
return None
|
|
||||||
return dialog
|
|
||||||
|
|
||||||
def _convertPluginMetadata(self, plugin_data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
||||||
try:
|
|
||||||
highest_sdk_version_supported = Version(0)
|
|
||||||
for supported_version in plugin_data["plugin"]["supported_sdk_versions"]:
|
|
||||||
if supported_version > highest_sdk_version_supported:
|
|
||||||
highest_sdk_version_supported = supported_version
|
|
||||||
|
|
||||||
formatted = {
|
|
||||||
"package_id": plugin_data["id"],
|
|
||||||
"package_type": "plugin",
|
|
||||||
"display_name": plugin_data["plugin"]["name"],
|
|
||||||
"package_version": plugin_data["plugin"]["version"],
|
|
||||||
"sdk_version": highest_sdk_version_supported,
|
|
||||||
"author": {
|
|
||||||
"author_id": plugin_data["plugin"]["author"],
|
|
||||||
"display_name": plugin_data["plugin"]["author"]
|
|
||||||
},
|
|
||||||
"is_installed": True,
|
|
||||||
"description": plugin_data["plugin"]["description"]
|
|
||||||
}
|
|
||||||
return formatted
|
|
||||||
except KeyError:
|
|
||||||
Logger.log("w", "Unable to convert plugin meta data %s", str(plugin_data))
|
|
||||||
return None
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def _updateInstalledModels(self) -> None:
|
|
||||||
# This is moved here to avoid code duplication and so that after installing plugins they get removed from the
|
|
||||||
# list of old plugins
|
|
||||||
old_plugin_ids = self._plugin_registry.getInstalledPlugins()
|
|
||||||
installed_package_ids = self._package_manager.getAllInstalledPackageIDs()
|
|
||||||
scheduled_to_remove_package_ids = self._package_manager.getToRemovePackageIDs()
|
|
||||||
|
|
||||||
self._old_plugin_ids = set()
|
|
||||||
self._old_plugin_metadata = dict()
|
|
||||||
|
|
||||||
for plugin_id in old_plugin_ids:
|
|
||||||
# Neither the installed packages nor the packages that are scheduled to remove are old plugins
|
|
||||||
if plugin_id not in installed_package_ids and plugin_id not in scheduled_to_remove_package_ids:
|
|
||||||
Logger.log("d", "Found a plugin that was installed with the old plugin browser: %s", plugin_id)
|
|
||||||
|
|
||||||
old_metadata = self._plugin_registry.getMetaData(plugin_id)
|
|
||||||
new_metadata = self._convertPluginMetadata(old_metadata)
|
|
||||||
if new_metadata is None:
|
|
||||||
# Something went wrong converting it.
|
|
||||||
continue
|
|
||||||
self._old_plugin_ids.add(plugin_id)
|
|
||||||
self._old_plugin_metadata[new_metadata["package_id"]] = new_metadata
|
|
||||||
|
|
||||||
all_packages = self._package_manager.getAllInstalledPackagesInfo()
|
|
||||||
if "plugin" in all_packages:
|
|
||||||
# For old plugins, we only want to include the old custom plugin that were installed via the old toolbox.
|
|
||||||
# The bundled plugins will be included in JSON files in the "bundled_packages" folder, so the bundled
|
|
||||||
# plugins should be excluded from the old plugins list/dict.
|
|
||||||
all_plugin_package_ids = set(package["package_id"] for package in all_packages["plugin"])
|
|
||||||
self._old_plugin_ids = set(plugin_id for plugin_id in self._old_plugin_ids
|
|
||||||
if plugin_id not in all_plugin_package_ids)
|
|
||||||
self._old_plugin_metadata = {k: v for k, v in self._old_plugin_metadata.items() if k in self._old_plugin_ids}
|
|
||||||
|
|
||||||
self._plugins_installed_model.setMetadata(all_packages["plugin"] + list(self._old_plugin_metadata.values()))
|
|
||||||
self._plugins_bundled_model.setMetadata(all_packages["plugin"] + list(self._old_plugin_metadata.values()))
|
|
||||||
self.metadataChanged.emit()
|
|
||||||
if "material" in all_packages:
|
|
||||||
self._materials_installed_model.setMetadata(all_packages["material"])
|
|
||||||
self._materials_bundled_model.setMetadata(all_packages["material"])
|
|
||||||
self.metadataChanged.emit()
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def install(self, file_path: str) -> Optional[str]:
|
|
||||||
package_id = self._package_manager.installPackage(file_path)
|
|
||||||
self.installChanged.emit()
|
|
||||||
self._updateInstalledModels()
|
|
||||||
self.metadataChanged.emit()
|
|
||||||
self._restart_required = True
|
|
||||||
self.restartRequiredChanged.emit()
|
|
||||||
return package_id
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def checkPackageUsageAndUninstall(self, package_id: str) -> None:
|
|
||||||
"""Check package usage and uninstall
|
|
||||||
|
|
||||||
If the package is in use, you'll get a confirmation dialog to set everything to default
|
|
||||||
"""
|
|
||||||
|
|
||||||
package_used_materials, package_used_qualities = self._package_manager.getMachinesUsingPackage(package_id)
|
|
||||||
if package_used_materials or package_used_qualities:
|
|
||||||
# Set up "uninstall variables" for resetMaterialsQualitiesAndUninstall
|
|
||||||
self._package_id_to_uninstall = package_id
|
|
||||||
package_info = self._package_manager.getInstalledPackageInfo(package_id)
|
|
||||||
self._package_name_to_uninstall = package_info.get("display_name", package_info.get("package_id"))
|
|
||||||
self._package_used_materials = package_used_materials
|
|
||||||
self._package_used_qualities = package_used_qualities
|
|
||||||
# Ask change to default material / profile
|
|
||||||
if self._confirm_reset_dialog is None:
|
|
||||||
self._confirm_reset_dialog = self._createDialog("dialogs/ToolboxConfirmUninstallResetDialog.qml")
|
|
||||||
self.uninstallVariablesChanged.emit()
|
|
||||||
if self._confirm_reset_dialog is None:
|
|
||||||
Logger.log("e", "ToolboxConfirmUninstallResetDialog should have been initialized, but it is not. Not showing dialog and not uninstalling package.")
|
|
||||||
else:
|
|
||||||
self._confirm_reset_dialog.show()
|
|
||||||
else:
|
|
||||||
# Plain uninstall
|
|
||||||
self.uninstall(package_id)
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = uninstallVariablesChanged)
|
|
||||||
def pluginToUninstall(self) -> str:
|
|
||||||
return self._package_name_to_uninstall
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = uninstallVariablesChanged)
|
|
||||||
def uninstallUsedMaterials(self) -> str:
|
|
||||||
return "\n".join(["%s (%s)" % (str(global_stack.getName()), material) for global_stack, extruder_nr, material in self._package_used_materials])
|
|
||||||
|
|
||||||
@pyqtProperty(str, notify = uninstallVariablesChanged)
|
|
||||||
def uninstallUsedQualities(self) -> str:
|
|
||||||
return "\n".join(["%s (%s)" % (str(global_stack.getName()), quality) for global_stack, extruder_nr, quality in self._package_used_qualities])
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def closeConfirmResetDialog(self) -> None:
|
|
||||||
if self._confirm_reset_dialog is not None:
|
|
||||||
self._confirm_reset_dialog.close()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def resetMaterialsQualitiesAndUninstall(self) -> None:
|
|
||||||
"""Uses "uninstall variables" to reset qualities and materials, then uninstall
|
|
||||||
|
|
||||||
It's used as an action on Confirm reset on Uninstall
|
|
||||||
"""
|
|
||||||
|
|
||||||
application = CuraApplication.getInstance()
|
|
||||||
machine_manager = application.getMachineManager()
|
|
||||||
container_tree = ContainerTree.getInstance()
|
|
||||||
|
|
||||||
for global_stack, extruder_nr, container_id in self._package_used_materials:
|
|
||||||
extruder = global_stack.extruderList[int(extruder_nr)]
|
|
||||||
approximate_diameter = extruder.getApproximateMaterialDiameter()
|
|
||||||
variant_node = container_tree.machines[global_stack.definition.getId()].variants[extruder.variant.getName()]
|
|
||||||
default_material_node = variant_node.preferredMaterial(approximate_diameter)
|
|
||||||
machine_manager.setMaterial(extruder_nr, default_material_node, global_stack = global_stack)
|
|
||||||
for global_stack, extruder_nr, container_id in self._package_used_qualities:
|
|
||||||
variant_names = [extruder.variant.getName() for extruder in global_stack.extruderList]
|
|
||||||
material_bases = [extruder.material.getMetaDataEntry("base_file") for extruder in global_stack.extruderList]
|
|
||||||
extruder_enabled = [extruder.isEnabled for extruder in global_stack.extruderList]
|
|
||||||
definition_id = global_stack.definition.getId()
|
|
||||||
machine_node = container_tree.machines[definition_id]
|
|
||||||
default_quality_group = machine_node.getQualityGroups(variant_names, material_bases, extruder_enabled)[machine_node.preferred_quality_type]
|
|
||||||
machine_manager.setQualityGroup(default_quality_group, global_stack = global_stack)
|
|
||||||
|
|
||||||
if self._package_id_to_uninstall is not None:
|
|
||||||
self._markPackageMaterialsAsToBeUninstalled(self._package_id_to_uninstall)
|
|
||||||
self.uninstall(self._package_id_to_uninstall)
|
|
||||||
self._resetUninstallVariables()
|
|
||||||
self.closeConfirmResetDialog()
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def onLicenseAccepted(self):
|
|
||||||
self.closeLicenseDialog.emit()
|
|
||||||
package_id = self.install(self.getLicenseDialogPluginFileLocation())
|
|
||||||
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def onLicenseDeclined(self):
|
|
||||||
self.closeLicenseDialog.emit()
|
|
||||||
|
|
||||||
def _markPackageMaterialsAsToBeUninstalled(self, package_id: str) -> None:
|
|
||||||
container_registry = self._application.getContainerRegistry()
|
|
||||||
|
|
||||||
all_containers = self._package_manager.getPackageContainerIds(package_id)
|
|
||||||
for container_id in all_containers:
|
|
||||||
containers = container_registry.findInstanceContainers(id = container_id)
|
|
||||||
if not containers:
|
|
||||||
continue
|
|
||||||
container = containers[0]
|
|
||||||
if container.getMetaDataEntry("type") != "material":
|
|
||||||
continue
|
|
||||||
root_material_id = container.getMetaDataEntry("base_file")
|
|
||||||
root_material_containers = container_registry.findInstanceContainers(id = root_material_id)
|
|
||||||
if not root_material_containers:
|
|
||||||
continue
|
|
||||||
root_material_container = root_material_containers[0]
|
|
||||||
root_material_container.setMetaDataEntry("removed", True)
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def uninstall(self, package_id: str) -> None:
|
|
||||||
self._package_manager.removePackage(package_id, force_add = True)
|
|
||||||
self.installChanged.emit()
|
|
||||||
self._updateInstalledModels()
|
|
||||||
self.metadataChanged.emit()
|
|
||||||
self._restart_required = True
|
|
||||||
self.restartRequiredChanged.emit()
|
|
||||||
|
|
||||||
def _update(self) -> None:
|
|
||||||
"""Actual update packages that are in self._to_update"""
|
|
||||||
|
|
||||||
if self._to_update:
|
|
||||||
plugin_id = self._to_update.pop(0)
|
|
||||||
remote_package = self.getRemotePackage(plugin_id)
|
|
||||||
if remote_package:
|
|
||||||
download_url = remote_package["download_url"]
|
|
||||||
Logger.log("d", "Updating package [%s]..." % plugin_id)
|
|
||||||
self.startDownload(download_url)
|
|
||||||
else:
|
|
||||||
Logger.log("e", "Could not update package [%s] because there is no remote package info available.", plugin_id)
|
|
||||||
|
|
||||||
if self._to_update:
|
|
||||||
self._application.callLater(self._update)
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def update(self, plugin_id: str) -> None:
|
|
||||||
"""Update a plugin by plugin_id"""
|
|
||||||
|
|
||||||
self._to_update.append(plugin_id)
|
|
||||||
self._application.callLater(self._update)
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def enable(self, plugin_id: str) -> None:
|
|
||||||
self._plugin_registry.enablePlugin(plugin_id)
|
|
||||||
self.toolboxEnabledChanged.emit()
|
|
||||||
Logger.log("i", "%s was set as 'active'.", plugin_id)
|
|
||||||
self._restart_required = True
|
|
||||||
self.restartRequiredChanged.emit()
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def disable(self, plugin_id: str) -> None:
|
|
||||||
self._plugin_registry.disablePlugin(plugin_id)
|
|
||||||
self.toolboxEnabledChanged.emit()
|
|
||||||
Logger.log("i", "%s was set as 'deactive'.", plugin_id)
|
|
||||||
self._restart_required = True
|
|
||||||
self.restartRequiredChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = metadataChanged)
|
|
||||||
def dataReady(self) -> bool:
|
|
||||||
return self._packages_model is not None
|
|
||||||
|
|
||||||
@pyqtProperty(bool, notify = restartRequiredChanged)
|
|
||||||
def restartRequired(self) -> bool:
|
|
||||||
return self._restart_required
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def restart(self) -> None:
|
|
||||||
self._application.windowClosed()
|
|
||||||
|
|
||||||
def getRemotePackage(self, package_id: str) -> Optional[Dict]:
|
|
||||||
# TODO: make the lookup in a dict, not a loop. canUpdate is called for every item.
|
|
||||||
remote_package = None
|
|
||||||
for package in self._server_response_data["packages"]:
|
|
||||||
if package["package_id"] == package_id:
|
|
||||||
remote_package = package
|
|
||||||
break
|
|
||||||
return remote_package
|
|
||||||
|
|
||||||
@pyqtSlot(str, result = bool)
|
|
||||||
def canDowngrade(self, package_id: str) -> bool:
|
|
||||||
# If the currently installed version is higher than the bundled version (if present), the we can downgrade
|
|
||||||
# this package.
|
|
||||||
local_package = self._package_manager.getInstalledPackageInfo(package_id)
|
|
||||||
if local_package is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
bundled_package = self._package_manager.getBundledPackageInfo(package_id)
|
|
||||||
if bundled_package is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
local_version = Version(local_package["package_version"])
|
|
||||||
bundled_version = Version(bundled_package["package_version"])
|
|
||||||
return bundled_version < local_version
|
|
||||||
|
|
||||||
@pyqtSlot(str, result = bool)
|
|
||||||
def isInstalled(self, package_id: str) -> bool:
|
|
||||||
result = self._package_manager.isPackageInstalled(package_id)
|
|
||||||
# Also check the old plugins list if it's not found in the package manager.
|
|
||||||
if not result:
|
|
||||||
result = self.isOldPlugin(package_id)
|
|
||||||
return result
|
|
||||||
|
|
||||||
@pyqtSlot(str, result = int)
|
|
||||||
def getNumberOfInstalledPackagesByAuthor(self, author_id: str) -> int:
|
|
||||||
count = 0
|
|
||||||
for package in self._materials_installed_model.items:
|
|
||||||
if package["author_id"] == author_id:
|
|
||||||
count += 1
|
|
||||||
return count
|
|
||||||
|
|
||||||
# This slot is only used to get the number of material packages by author, not any other type of packages.
|
|
||||||
@pyqtSlot(str, result = int)
|
|
||||||
def getTotalNumberOfMaterialPackagesByAuthor(self, author_id: str) -> int:
|
|
||||||
count = 0
|
|
||||||
for package in self._server_response_data["packages"]:
|
|
||||||
if package["package_type"] == "material":
|
|
||||||
if package["author"]["author_id"] == author_id:
|
|
||||||
count += 1
|
|
||||||
return count
|
|
||||||
|
|
||||||
@pyqtSlot(str, result = bool)
|
|
||||||
def isEnabled(self, package_id: str) -> bool:
|
|
||||||
return package_id in self._plugin_registry.getActivePlugins()
|
|
||||||
|
|
||||||
# Check for plugins that were installed with the old plugin browser
|
|
||||||
def isOldPlugin(self, plugin_id: str) -> bool:
|
|
||||||
return plugin_id in self._old_plugin_ids
|
|
||||||
|
|
||||||
def getOldPluginPackageMetadata(self, plugin_id: str) -> Optional[Dict[str, Any]]:
|
|
||||||
return self._old_plugin_metadata.get(plugin_id)
|
|
||||||
|
|
||||||
def isLoadingComplete(self) -> bool:
|
|
||||||
populated = 0
|
|
||||||
for metadata_list in self._server_response_data.items():
|
|
||||||
if metadata_list:
|
|
||||||
populated += 1
|
|
||||||
return populated == len(self._server_response_data.items())
|
|
||||||
|
|
||||||
# Make API Calls
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
def _makeRequestByType(self, request_type: str) -> None:
|
|
||||||
Logger.debug(f"Requesting {request_type} metadata from server.")
|
|
||||||
url = self._request_urls[request_type]
|
|
||||||
|
|
||||||
callback = lambda r, rt = request_type: self._onDataRequestFinished(rt, r)
|
|
||||||
error_callback = lambda r, e, rt = request_type: self._onDataRequestError(rt, r, e)
|
|
||||||
self._application.getHttpRequestManager().get(url,
|
|
||||||
callback = callback,
|
|
||||||
error_callback = error_callback,
|
|
||||||
scope=self._json_scope)
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def startDownload(self, url: str) -> None:
|
|
||||||
Logger.info(f"Attempting to download & install package from {url}.")
|
|
||||||
|
|
||||||
callback = lambda r: self._onDownloadFinished(r)
|
|
||||||
error_callback = lambda r, e: self._onDownloadFailed(r, e)
|
|
||||||
download_progress_callback = self._onDownloadProgress
|
|
||||||
request_data = self._application.getHttpRequestManager().get(url,
|
|
||||||
callback = callback,
|
|
||||||
error_callback = error_callback,
|
|
||||||
download_progress_callback = download_progress_callback,
|
|
||||||
scope=self._cloud_scope
|
|
||||||
)
|
|
||||||
|
|
||||||
self._download_request_data = request_data
|
|
||||||
self.setDownloadProgress(0)
|
|
||||||
self.setIsDownloading(True)
|
|
||||||
|
|
||||||
@pyqtSlot()
|
|
||||||
def cancelDownload(self) -> None:
|
|
||||||
Logger.info(f"User cancelled the download of a package. request {self._download_request_data}")
|
|
||||||
if self._download_request_data is not None:
|
|
||||||
self._application.getHttpRequestManager().abortRequest(self._download_request_data)
|
|
||||||
self._download_request_data = None
|
|
||||||
self.resetDownload()
|
|
||||||
|
|
||||||
def resetDownload(self) -> None:
|
|
||||||
self.setDownloadProgress(0)
|
|
||||||
self.setIsDownloading(False)
|
|
||||||
|
|
||||||
# Handlers for Network Events
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
def _onDataRequestError(self, request_type: str, reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None:
|
|
||||||
Logger.error(f"Request {request_type} failed due to error {error}: {reply.errorString()}")
|
|
||||||
self.setViewPage("errored")
|
|
||||||
|
|
||||||
def _onDataRequestFinished(self, request_type: str, reply: "QNetworkReply") -> None:
|
|
||||||
if reply.operation() != QNetworkAccessManager.GetOperation:
|
|
||||||
Logger.log("e", "_onDataRequestFinished() only handles GET requests but got [%s] instead", reply.operation())
|
|
||||||
return
|
|
||||||
|
|
||||||
http_status_code = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
|
|
||||||
if http_status_code != 200:
|
|
||||||
Logger.log("e", "Request type [%s] got non-200 HTTP response: [%s]", http_status_code)
|
|
||||||
self.setViewPage("errored")
|
|
||||||
return
|
|
||||||
|
|
||||||
data = bytes(reply.readAll())
|
|
||||||
try:
|
|
||||||
json_data = json.loads(data.decode("utf-8"))
|
|
||||||
except json.decoder.JSONDecodeError:
|
|
||||||
Logger.log("e", "Failed to decode response data as JSON for request type [%s], response data [%s]",
|
|
||||||
request_type, data)
|
|
||||||
self.setViewPage("errored")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check for errors:
|
|
||||||
if "errors" in json_data:
|
|
||||||
for error in json_data["errors"]:
|
|
||||||
Logger.log("e", "Request type [%s] got response showing error: %s", error.get("title", "No error title found"))
|
|
||||||
self.setViewPage("errored")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Create model and apply metadata:
|
|
||||||
if not self._models[request_type]:
|
|
||||||
Logger.log("e", "Could not find the model for request type [%s].", request_type)
|
|
||||||
self.setViewPage("errored")
|
|
||||||
return
|
|
||||||
|
|
||||||
self._server_response_data[request_type] = json_data["data"]
|
|
||||||
self._models[request_type].setMetadata(self._server_response_data[request_type])
|
|
||||||
|
|
||||||
if request_type == "packages":
|
|
||||||
self._models[request_type].setFilter({"type": "plugin"})
|
|
||||||
self.reBuildMaterialsModels()
|
|
||||||
self.reBuildPluginsModels()
|
|
||||||
self._notifyPackageManager()
|
|
||||||
elif request_type == "authors":
|
|
||||||
self._models[request_type].setFilter({"package_types": "material"})
|
|
||||||
self._models[request_type].setFilter({"tags": "generic"})
|
|
||||||
elif request_type == "updates":
|
|
||||||
# Tell the package manager that there's a new set of updates available.
|
|
||||||
packages = set([pkg["package_id"] for pkg in self._server_response_data[request_type]])
|
|
||||||
self._package_manager.setPackagesWithUpdate(packages)
|
|
||||||
|
|
||||||
self.metadataChanged.emit()
|
|
||||||
|
|
||||||
if self.isLoadingComplete():
|
|
||||||
self.setViewPage("overview")
|
|
||||||
|
|
||||||
# This function goes through all known remote versions of a package and notifies the package manager of this change
|
|
||||||
def _notifyPackageManager(self):
|
|
||||||
for package in self._server_response_data["packages"]:
|
|
||||||
self._package_manager.addAvailablePackageVersion(package["package_id"], Version(package["package_version"]))
|
|
||||||
|
|
||||||
def _onDownloadFinished(self, reply: "QNetworkReply") -> None:
|
|
||||||
self.resetDownload()
|
|
||||||
|
|
||||||
if reply.attribute(QNetworkRequest.HttpStatusCodeAttribute) != 200:
|
|
||||||
try:
|
|
||||||
reply_error = json.loads(reply.readAll().data().decode("utf-8"))
|
|
||||||
except Exception as e:
|
|
||||||
reply_error = str(e)
|
|
||||||
Logger.log("w", "Failed to download package. The following error was returned: %s", reply_error)
|
|
||||||
return
|
|
||||||
# Must not delete the temporary file on Windows
|
|
||||||
self._temp_plugin_file = tempfile.NamedTemporaryFile(mode = "w+b", suffix = ".curapackage", delete = False)
|
|
||||||
file_path = self._temp_plugin_file.name
|
|
||||||
# Write first and close, otherwise on Windows, it cannot read the file
|
|
||||||
self._temp_plugin_file.write(reply.readAll())
|
|
||||||
self._temp_plugin_file.close()
|
|
||||||
self._onDownloadComplete(file_path)
|
|
||||||
|
|
||||||
def _onDownloadFailed(self, reply: "QNetworkReply", error: "QNetworkReply.NetworkError") -> None:
|
|
||||||
Logger.log("w", "Failed to download package. The following error was returned: %s", error)
|
|
||||||
|
|
||||||
self.resetDownload()
|
|
||||||
|
|
||||||
def _onDownloadProgress(self, bytes_sent: int, bytes_total: int) -> None:
|
|
||||||
if bytes_total > 0:
|
|
||||||
new_progress = bytes_sent / bytes_total * 100
|
|
||||||
self.setDownloadProgress(new_progress)
|
|
||||||
Logger.log("d", "new download progress %s / %s : %s%%", bytes_sent, bytes_total, new_progress)
|
|
||||||
|
|
||||||
def _onDownloadComplete(self, file_path: str) -> None:
|
|
||||||
Logger.log("i", "Download complete.")
|
|
||||||
package_info = self._package_manager.getPackageInfo(file_path)
|
|
||||||
if not package_info:
|
|
||||||
Logger.log("w", "Package file [%s] was not a valid CuraPackage.", file_path)
|
|
||||||
return
|
|
||||||
package_id = package_info["package_id"]
|
|
||||||
|
|
||||||
try:
|
|
||||||
license_content = self._package_manager.getPackageLicense(file_path)
|
|
||||||
except EnvironmentError as e:
|
|
||||||
Logger.error(f"Could not open downloaded package {package_id} to read license file! {type(e)} - {e}")
|
|
||||||
return
|
|
||||||
if license_content is not None:
|
|
||||||
# get the icon url for package_id, make sure the result is a string, never None
|
|
||||||
icon_url = next((x["icon_url"] for x in self.packagesModel.items if x["id"] == package_id), None) or ""
|
|
||||||
self.openLicenseDialog(package_info["display_name"], license_content, file_path, icon_url)
|
|
||||||
return
|
|
||||||
|
|
||||||
installed_id = self.install(file_path)
|
|
||||||
if installed_id != package_id:
|
|
||||||
Logger.error("Installed package {} does not match {}".format(installed_id, package_id))
|
|
||||||
|
|
||||||
# Getter & Setters for Properties:
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
def setDownloadProgress(self, progress: float) -> None:
|
|
||||||
if progress != self._download_progress:
|
|
||||||
self._download_progress = progress
|
|
||||||
self.onDownloadProgressChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(int, fset = setDownloadProgress, notify = onDownloadProgressChanged)
|
|
||||||
def downloadProgress(self) -> float:
|
|
||||||
return self._download_progress
|
|
||||||
|
|
||||||
def setIsDownloading(self, is_downloading: bool) -> None:
|
|
||||||
if self._is_downloading != is_downloading:
|
|
||||||
self._is_downloading = is_downloading
|
|
||||||
self.onIsDownloadingChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(bool, fset = setIsDownloading, notify = onIsDownloadingChanged)
|
|
||||||
def isDownloading(self) -> bool:
|
|
||||||
return self._is_downloading
|
|
||||||
|
|
||||||
def setActivePackage(self, package: QObject) -> None:
|
|
||||||
if self._active_package != package:
|
|
||||||
self._active_package = package
|
|
||||||
self.activePackageChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, fset = setActivePackage, notify = activePackageChanged)
|
|
||||||
def activePackage(self) -> Optional[QObject]:
|
|
||||||
"""The active package is the package that is currently being downloaded"""
|
|
||||||
|
|
||||||
return self._active_package
|
|
||||||
|
|
||||||
def setViewCategory(self, category: str = "plugin") -> None:
|
|
||||||
if self._view_category != category:
|
|
||||||
self._view_category = category
|
|
||||||
self.viewChanged.emit()
|
|
||||||
|
|
||||||
# Function explicitly defined so that it can be called through the callExtensionsMethod
|
|
||||||
# which cannot receive arguments.
|
|
||||||
def setViewCategoryToMaterials(self) -> None:
|
|
||||||
self.setViewCategory("material")
|
|
||||||
|
|
||||||
@pyqtProperty(str, fset = setViewCategory, notify = viewChanged)
|
|
||||||
def viewCategory(self) -> str:
|
|
||||||
return self._view_category
|
|
||||||
|
|
||||||
def setViewPage(self, page: str = "overview") -> None:
|
|
||||||
if self._view_page != page:
|
|
||||||
self._view_page = page
|
|
||||||
self.viewChanged.emit()
|
|
||||||
|
|
||||||
@pyqtProperty(str, fset = setViewPage, notify = viewChanged)
|
|
||||||
def viewPage(self) -> str:
|
|
||||||
return self._view_page
|
|
||||||
|
|
||||||
# Exposed Models:
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def authorsModel(self) -> AuthorsModel:
|
|
||||||
return cast(AuthorsModel, self._models["authors"])
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def packagesModel(self) -> PackagesModel:
|
|
||||||
return cast(PackagesModel, self._models["packages"])
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def pluginsShowcaseModel(self) -> PackagesModel:
|
|
||||||
return self._plugins_showcase_model
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def pluginsAvailableModel(self) -> PackagesModel:
|
|
||||||
return self._plugins_available_model
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def pluginsInstalledModel(self) -> PackagesModel:
|
|
||||||
return self._plugins_installed_model
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def pluginsBundledModel(self) -> PackagesModel:
|
|
||||||
return self._plugins_bundled_model
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def materialsShowcaseModel(self) -> AuthorsModel:
|
|
||||||
return self._materials_showcase_model
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def materialsAvailableModel(self) -> AuthorsModel:
|
|
||||||
return self._materials_available_model
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def materialsInstalledModel(self) -> PackagesModel:
|
|
||||||
return self._materials_installed_model
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def materialsBundledModel(self) -> PackagesModel:
|
|
||||||
return self._materials_bundled_model
|
|
||||||
|
|
||||||
@pyqtProperty(QObject, constant = True)
|
|
||||||
def materialsGenericModel(self) -> PackagesModel:
|
|
||||||
return self._materials_generic_model
|
|
||||||
|
|
||||||
@pyqtSlot(str, result = str)
|
|
||||||
def getWebMarketplaceUrl(self, page: str) -> str:
|
|
||||||
root = CuraMarketplaceRoot
|
|
||||||
if root == "":
|
|
||||||
root = DEFAULT_MARKETPLACE_ROOT
|
|
||||||
return root + "/app/cura/" + page
|
|
||||||
|
|
||||||
# Filter Models:
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
@pyqtSlot(str, str, str)
|
|
||||||
def filterModelByProp(self, model_type: str, filter_type: str, parameter: str) -> None:
|
|
||||||
if not self._models[model_type]:
|
|
||||||
Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type)
|
|
||||||
return
|
|
||||||
self._models[model_type].setFilter({filter_type: parameter})
|
|
||||||
self.filterChanged.emit()
|
|
||||||
|
|
||||||
@pyqtSlot(str, "QVariantMap")
|
|
||||||
def setFilters(self, model_type: str, filter_dict: dict) -> None:
|
|
||||||
if not self._models[model_type]:
|
|
||||||
Logger.log("w", "Couldn't filter %s model because it doesn't exist.", model_type)
|
|
||||||
return
|
|
||||||
self._models[model_type].setFilter(filter_dict)
|
|
||||||
self.filterChanged.emit()
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def removeFilters(self, model_type: str) -> None:
|
|
||||||
if not self._models[model_type]:
|
|
||||||
Logger.log("w", "Couldn't remove filters on %s model because it doesn't exist.", model_type)
|
|
||||||
return
|
|
||||||
self._models[model_type].setFilter({})
|
|
||||||
self.filterChanged.emit()
|
|
||||||
|
|
||||||
# HACK(S):
|
|
||||||
# --------------------------------------------------------------------------
|
|
||||||
def reBuildMaterialsModels(self) -> None:
|
|
||||||
materials_showcase_metadata = []
|
|
||||||
materials_available_metadata = []
|
|
||||||
materials_generic_metadata = []
|
|
||||||
|
|
||||||
processed_authors = [] # type: List[str]
|
|
||||||
|
|
||||||
for item in self._server_response_data["packages"]:
|
|
||||||
if item["package_type"] == "material":
|
|
||||||
|
|
||||||
author = item["author"]
|
|
||||||
if author["author_id"] in processed_authors:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Generic materials to be in the same section
|
|
||||||
if "generic" in item["tags"]:
|
|
||||||
materials_generic_metadata.append(item)
|
|
||||||
else:
|
|
||||||
if "showcase" in item["tags"]:
|
|
||||||
materials_showcase_metadata.append(author)
|
|
||||||
else:
|
|
||||||
materials_available_metadata.append(author)
|
|
||||||
|
|
||||||
processed_authors.append(author["author_id"])
|
|
||||||
|
|
||||||
self._materials_showcase_model.setMetadata(materials_showcase_metadata)
|
|
||||||
self._materials_available_model.setMetadata(materials_available_metadata)
|
|
||||||
self._materials_generic_model.setMetadata(materials_generic_metadata)
|
|
||||||
|
|
||||||
def reBuildPluginsModels(self) -> None:
|
|
||||||
plugins_showcase_metadata = []
|
|
||||||
plugins_available_metadata = []
|
|
||||||
|
|
||||||
for item in self._server_response_data["packages"]:
|
|
||||||
if item["package_type"] == "plugin":
|
|
||||||
if "showcase" in item["tags"]:
|
|
||||||
plugins_showcase_metadata.append(item)
|
|
||||||
else:
|
|
||||||
plugins_available_metadata.append(item)
|
|
||||||
|
|
||||||
self._plugins_showcase_model.setMetadata(plugins_showcase_metadata)
|
|
||||||
self._plugins_available_model.setMetadata(plugins_available_metadata)
|
|