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_line_length = 20
|
||||
self._origin_line_width = 1.5
|
||||
self._origin_line_width = 1
|
||||
self._enabled = False
|
||||
|
||||
self._grid_mesh = None # type: Optional[MeshData]
|
||||
|
@ -601,6 +601,7 @@ class BuildVolume(SceneNode):
|
|||
if self._adhesion_type == "raft":
|
||||
self._raft_thickness = (
|
||||
self._global_container_stack.getProperty("raft_base_thickness", "value") +
|
||||
self._global_container_stack.getProperty("raft_interface_layers", "value") *
|
||||
self._global_container_stack.getProperty("raft_interface_thickness", "value") +
|
||||
self._global_container_stack.getProperty("raft_surface_layers", "value") *
|
||||
self._global_container_stack.getProperty("raft_surface_thickness", "value") +
|
||||
|
@ -1214,7 +1215,7 @@ class BuildVolume(SceneNode):
|
|||
|
||||
_machine_settings = ["machine_width", "machine_depth", "machine_height", "machine_shape", "machine_center_is_zero"]
|
||||
_skirt_settings = ["adhesion_type", "skirt_gap", "skirt_line_count", "skirt_brim_line_width", "brim_width", "brim_line_count", "raft_margin", "draft_shield_enabled", "draft_shield_dist", "initial_layer_line_width_factor"]
|
||||
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
|
||||
_raft_settings = ["adhesion_type", "raft_base_thickness", "raft_interface_layers", "raft_interface_thickness", "raft_surface_layers", "raft_surface_thickness", "raft_airgap", "layer_0_z_overlap"]
|
||||
_extra_z_settings = ["retraction_hop_enabled", "retraction_hop"]
|
||||
_prime_settings = ["extruder_prime_pos_x", "extruder_prime_pos_y", "prime_blob_enable"]
|
||||
_tower_settings = ["prime_tower_enable", "prime_tower_size", "prime_tower_position_x", "prime_tower_position_y", "prime_tower_brim_enable"]
|
||||
|
|
|
@ -494,7 +494,7 @@ class CuraApplication(QtApplication):
|
|||
"CuraEngineBackend", #Cura is useless without this one since you can't slice.
|
||||
"FileLogger", #You want to be able to read the log if something goes wrong.
|
||||
"XmlMaterialProfile", #Cura crashes without this one.
|
||||
"Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
|
||||
"Marketplace", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
|
||||
"PrepareStage", #Cura is useless without this one since you can't load models.
|
||||
"PreviewStage", #This shows the list of the plugin views that are installed in Cura.
|
||||
"MonitorStage", #Major part of Cura's functionality.
|
||||
|
@ -573,6 +573,10 @@ class CuraApplication(QtApplication):
|
|||
|
||||
preferences.addPreference("general/accepted_user_agreement", False)
|
||||
|
||||
preferences.addPreference("cura/market_place_show_plugin_banner", True)
|
||||
preferences.addPreference("cura/market_place_show_material_banner", True)
|
||||
preferences.addPreference("cura/market_place_show_manage_packages_banner", True)
|
||||
|
||||
for key in [
|
||||
"dialog_load_path", # dialog_save_path is in LocalFileOutputDevicePlugin
|
||||
"dialog_profile_path",
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
# Copyright (c) 2018 Ultimaker B.V.
|
||||
# Cura is released under the terms of the LGPLv3 or higher.
|
||||
|
||||
from typing import List, Tuple, TYPE_CHECKING, Optional
|
||||
from typing import Any, cast, Dict, List, Set, Tuple, TYPE_CHECKING, Optional
|
||||
|
||||
from cura.CuraApplication import CuraApplication #To find some resource types.
|
||||
from cura.CuraApplication import CuraApplication # To find some resource types.
|
||||
from cura.Settings.GlobalStack import GlobalStack
|
||||
|
||||
from UM.PackageManager import PackageManager #The class we're extending.
|
||||
from UM.Resources import Resources #To find storage paths for some resource types.
|
||||
from UM.PackageManager import PackageManager # The class we're extending.
|
||||
from UM.Resources import Resources # To find storage paths for some resource types.
|
||||
from UM.i18n import i18nCatalog
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from UM.Qt.QtApplication import QtApplication
|
||||
|
@ -17,6 +19,31 @@ if TYPE_CHECKING:
|
|||
class CuraPackageManager(PackageManager):
|
||||
def __init__(self, application: "QtApplication", parent: Optional["QObject"] = None) -> None:
|
||||
super().__init__(application, parent)
|
||||
self._local_packages: Optional[List[Dict[str, Any]]] = None
|
||||
self._local_packages_ids: Optional[Set[str]] = None
|
||||
self.installedPackagesChanged.connect(self._updateLocalPackages)
|
||||
|
||||
def _updateLocalPackages(self) -> None:
|
||||
self._local_packages = self.getAllLocalPackages()
|
||||
self._local_packages_ids = set(pkg["package_id"] for pkg in self._local_packages)
|
||||
|
||||
@property
|
||||
def local_packages(self) -> List[Dict[str, Any]]:
|
||||
"""locally installed packages, lazy execution"""
|
||||
if self._local_packages is None:
|
||||
self._updateLocalPackages()
|
||||
# _updateLocalPackages always results in a list of packages, not None.
|
||||
# It's guaranteed to be a list now.
|
||||
return cast(List[Dict[str, Any]], self._local_packages)
|
||||
|
||||
@property
|
||||
def local_packages_ids(self) -> Set[str]:
|
||||
"""locally installed packages, lazy execution"""
|
||||
if self._local_packages_ids is None:
|
||||
self._updateLocalPackages()
|
||||
# _updateLocalPackages always results in a list of packages, not None.
|
||||
# It's guaranteed to be a list now.
|
||||
return cast(Set[str], self._local_packages_ids)
|
||||
|
||||
def initialize(self) -> None:
|
||||
self._installation_dirs_dict["materials"] = Resources.getStoragePath(CuraApplication.ResourceTypes.MaterialInstanceContainer)
|
||||
|
@ -47,3 +74,12 @@ class CuraPackageManager(PackageManager):
|
|||
machine_with_qualities.append((global_stack, str(extruder_nr), container_id))
|
||||
|
||||
return machine_with_materials, machine_with_qualities
|
||||
|
||||
def getAllLocalPackages(self) -> List[Dict[str, Any]]:
|
||||
""" Returns an unordered list of all the package_info of installed, to be installed, or bundled packages"""
|
||||
packages: List[Dict[str, Any]] = []
|
||||
|
||||
for packages_to_add in self.getAllInstalledPackagesInfo().values():
|
||||
packages.extend(packages_to_add)
|
||||
|
||||
return packages
|
||||
|
|
|
@ -268,6 +268,7 @@ class ExtruderManager(QObject):
|
|||
used_adhesion_extruders.add("skirt_brim_extruder_nr") # There's a brim or prime tower brim.
|
||||
if adhesion_type == "raft":
|
||||
used_adhesion_extruders.add("raft_base_extruder_nr")
|
||||
if global_stack.getProperty("raft_interface_layers", "value") > 0:
|
||||
used_adhesion_extruders.add("raft_interface_extruder_nr")
|
||||
if global_stack.getProperty("raft_surface_layers", "value") > 0:
|
||||
used_adhesion_extruders.add("raft_surface_extruder_nr")
|
||||
|
|
|
@ -7,9 +7,9 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
|
|||
PROJECT_DIR="$( cd "${SCRIPT_DIR}/.." && pwd )"
|
||||
|
||||
# Make sure that environment variables are set properly
|
||||
source /opt/rh/devtoolset-8/enable
|
||||
export PATH="${CURA_BUILD_ENV_PATH}/bin:${PATH}"
|
||||
export PKG_CONFIG_PATH="${CURA_BUILD_ENV_PATH}/lib/pkgconfig:${PKG_CONFIG_PATH}"
|
||||
export LD_LIBRARY_PATH="${CURA_BUILD_ENV_PATH}/lib:${LD_LIBRARY_PATH}"
|
||||
|
||||
cd "${PROJECT_DIR}"
|
||||
|
||||
|
@ -60,7 +60,7 @@ export PYTHONPATH="${PROJECT_DIR}/Uranium:.:${PYTHONPATH}"
|
|||
|
||||
mkdir build
|
||||
cd build
|
||||
cmake3 \
|
||||
cmake \
|
||||
-DCMAKE_BUILD_TYPE=Debug \
|
||||
-DCMAKE_PREFIX_PATH="${CURA_BUILD_ENV_PATH}" \
|
||||
-DURANIUM_DIR="${PROJECT_DIR}/Uranium" \
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
#!/usr/bin/env bash
|
||||
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.Layouts 1.3
|
||||
|
||||
import UM 1.3 as UM
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
import "../components"
|
||||
|
@ -35,7 +35,7 @@ RowLayout
|
|||
busy: CuraDrive.isCreatingBackup
|
||||
}
|
||||
|
||||
Cura.CheckBoxWithTooltip
|
||||
UM.CheckBox
|
||||
{
|
||||
id: autoBackupEnabled
|
||||
checked: CuraDrive.autoBackupEnabled
|
||||
|
|
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
|
||||
fillMode: Image.PreserveAspectFit
|
||||
source: "../images/icon.png"
|
||||
source: "../images/backup.svg"
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
width: Math.round(parent.width / 4)
|
||||
}
|
||||
|
|
|
@ -205,6 +205,13 @@ class StartSliceJob(Job):
|
|||
# Get the objects in their groups to print.
|
||||
object_groups = []
|
||||
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()):
|
||||
temp_list = []
|
||||
|
||||
|
@ -221,7 +228,7 @@ class StartSliceJob(Job):
|
|||
temp_list.append(child_node)
|
||||
|
||||
if temp_list:
|
||||
object_groups.append(temp_list)
|
||||
object_groups.append(temp_list + modifier_mesh_nodes)
|
||||
Job.yieldThread()
|
||||
if len(object_groups) == 0:
|
||||
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
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: UM.Theme.getSize("toolbox_thumbnail_small").width
|
||||
width: UM.Theme.getSize("card_icon").width
|
||||
height: Math.round(width * 3/4)
|
||||
sourceSize.width: width
|
||||
sourceSize.height: height
|
||||
|
|
|
@ -6,7 +6,7 @@ import QtQuick 2.10
|
|||
import QtQuick.Window 2.2
|
||||
import QtQuick.Controls 2.3
|
||||
|
||||
import UM 1.2 as UM
|
||||
import UM 1.5 as UM
|
||||
import Cura 1.6 as Cura
|
||||
|
||||
import DigitalFactory 1.0 as DF
|
||||
|
@ -214,7 +214,7 @@ Item
|
|||
width: childrenRect.width
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
|
||||
Cura.CheckBox
|
||||
UM.CheckBox
|
||||
{
|
||||
id: asProjectCheckbox
|
||||
height: UM.Theme.getSize("checkbox").height
|
||||
|
@ -224,7 +224,7 @@ Item
|
|||
font: UM.Theme.getFont("medium")
|
||||
}
|
||||
|
||||
Cura.CheckBox
|
||||
UM.CheckBox
|
||||
{
|
||||
id: asSlicedCheckbox
|
||||
height: UM.Theme.getSize("checkbox").height
|
||||
|
|
|
@ -7,7 +7,7 @@ import QtQuick.Controls 2.3
|
|||
import QtQuick.Layouts 1.1
|
||||
|
||||
import UM 1.2 as UM
|
||||
import Cura 1.6 as Cura
|
||||
import Cura 1.7 as Cura
|
||||
|
||||
import DigitalFactory 1.0 as DF
|
||||
|
||||
|
@ -42,33 +42,13 @@ Item
|
|||
height: childrenRect.height
|
||||
spacing: UM.Theme.getSize("default_margin").width
|
||||
|
||||
Cura.TextField
|
||||
Cura.SearchBar
|
||||
{
|
||||
id: searchBar
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: createNewProjectButton.height
|
||||
leftPadding: searchIcon.width + UM.Theme.getSize("default_margin").width * 2
|
||||
focus: true
|
||||
|
||||
onTextEdited: manager.projectFilter = text //Update the search filter when editing this text field.
|
||||
|
||||
placeholderText: "Search"
|
||||
|
||||
UM.RecolorImage
|
||||
{
|
||||
id: searchIcon
|
||||
|
||||
anchors
|
||||
{
|
||||
verticalCenter: parent.verticalCenter
|
||||
left: parent.left
|
||||
leftMargin: UM.Theme.getSize("default_margin").width
|
||||
}
|
||||
source: UM.Theme.getIcon("search")
|
||||
height: UM.Theme.getSize("small_button_icon").height
|
||||
width: height
|
||||
color: UM.Theme.getColor("text")
|
||||
}
|
||||
}
|
||||
|
||||
Cura.SecondaryButton
|
||||
|
@ -220,7 +200,7 @@ Item
|
|||
LoadMoreProjectsCard
|
||||
{
|
||||
id: loadMoreProjectsCard
|
||||
height: UM.Theme.getSize("toolbox_thumbnail_small").height
|
||||
height: UM.Theme.getSize("card_icon").height
|
||||
width: parent.width
|
||||
visible: manager.digitalFactoryProjectModel.count > 0
|
||||
hasMoreProjectsToLoad: manager.hasMoreProjectsToLoad
|
||||
|
|
|
@ -12,7 +12,7 @@ from urllib.error import URLError
|
|||
from typing import Dict
|
||||
import ssl
|
||||
|
||||
import certifi
|
||||
import certifi # type: ignore
|
||||
|
||||
from .FirmwareUpdateCheckerLookup import FirmwareUpdateCheckerLookup, getSettingsKeyForMachine
|
||||
from .FirmwareUpdateCheckerMessage import FirmwareUpdateCheckerMessage
|
||||
|
|
|
@ -24,7 +24,7 @@ from cura.Settings.ExtruderManager import ExtruderManager
|
|||
|
||||
catalog = i18nCatalog("cura")
|
||||
|
||||
PositionOptional = NamedTuple("Position", [("x", Optional[float]), ("y", Optional[float]), ("z", Optional[float]), ("f", Optional[float]), ("e", Optional[float])])
|
||||
PositionOptional = NamedTuple("PositionOptional", [("x", Optional[float]), ("y", Optional[float]), ("z", Optional[float]), ("f", Optional[float]), ("e", Optional[float])])
|
||||
Position = NamedTuple("Position", [("x", float), ("y", float), ("z", float), ("f", float), ("e", List[float])])
|
||||
|
||||
|
||||
|
|
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.Controls 2.1
|
||||
|
||||
import UM 1.2 as UM
|
||||
import UM 1.5 as UM
|
||||
|
||||
import Cura 1.0 as Cura
|
||||
|
||||
UM.TooltipArea
|
||||
{
|
||||
|
@ -15,7 +17,7 @@ UM.TooltipArea
|
|||
width: childrenRect.width;
|
||||
height: childrenRect.height;
|
||||
|
||||
CheckBox
|
||||
UM.CheckBox
|
||||
{
|
||||
id: check
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
|
|||
all_instances = settings.findInstances()
|
||||
visibility_changed = False # Flag to check if at the end the signal needs to be emitted
|
||||
|
||||
# Remove all instances that are not in visibility list
|
||||
# Remove all SettingInstances that are not in visibility list
|
||||
for instance in all_instances:
|
||||
# exceptionally skip setting
|
||||
if instance.definition.key in self._skip_reset_setting_set:
|
||||
|
@ -71,29 +71,30 @@ class PerObjectSettingVisibilityHandler(UM.Settings.Models.SettingVisibilityHand
|
|||
settings.removeInstance(instance.definition.key)
|
||||
visibility_changed = True
|
||||
|
||||
# Add all instances that are not added, but are in visibility list
|
||||
# Add all SettingInstances that are not added, but are in visibility list
|
||||
for item in visible:
|
||||
if settings.getInstance(item) is not None: # Setting was added already.
|
||||
continue
|
||||
definition = self._stack.getSettingDefinition(item)
|
||||
if not definition:
|
||||
Logger.log("w", f"Unable to add instance ({item}) to per-object visibility because we couldn't find the matching definition.")
|
||||
Logger.log("w", f"Unable to add SettingInstance ({item}) to the per-object visibility because we couldn't find the matching SettingDefinition.")
|
||||
continue
|
||||
|
||||
new_instance = SettingInstance(definition, settings)
|
||||
stack_nr = -1
|
||||
stack = None
|
||||
# Check from what stack we should copy the raw property of the setting from.
|
||||
# Check from what ContainerStack we should copy the raw property of the setting from.
|
||||
if self._stack.getProperty("machine_extruder_count", "value") > 1:
|
||||
if definition.limit_to_extruder != "-1":
|
||||
# A limit to extruder function was set and it's a multi extrusion machine. Check what stack we do need to use.
|
||||
# A limit_to_extruder function was set and it's a multi extrusion machine. Check what stack we
|
||||
# do need to use.
|
||||
stack_nr = str(int(round(float(self._stack.getProperty(item, "limit_to_extruder")))))
|
||||
|
||||
# Check if the found stack_number is in the extruder list of extruders.
|
||||
if stack_nr not in ExtruderManager.getInstance().extruderIds and self._stack.getProperty("extruder_nr", "value") is not None:
|
||||
stack_nr = -1
|
||||
|
||||
# Use the found stack number to get the right stack to copy the value from.
|
||||
# Use the found stack_number to get the right ContainerStack to copy the value from.
|
||||
if stack_nr in ExtruderManager.getInstance().extruderIds:
|
||||
stack = ContainerRegistry.getInstance().findContainerStacks(id = ExtruderManager.getInstance().extruderIds[stack_nr])[0]
|
||||
else:
|
||||
|
|
|
@ -60,13 +60,14 @@ UM.Dialog
|
|||
onTextChanged: settingPickDialog.updateFilter()
|
||||
}
|
||||
|
||||
CheckBox
|
||||
UM.CheckBox
|
||||
{
|
||||
id: toggleShowAll
|
||||
anchors
|
||||
{
|
||||
top: parent.top
|
||||
right: parent.right
|
||||
verticalCenter: filterInput.verticalCenter
|
||||
}
|
||||
text: catalog.i18nc("@label:checkbox", "Show all")
|
||||
}
|
||||
|
|
|
@ -193,6 +193,8 @@ class PostProcessingPlugin(QObject, Extension):
|
|||
|
||||
spec = importlib.util.spec_from_file_location(__name__ + "." + script_name,
|
||||
file_path)
|
||||
if spec is None:
|
||||
continue
|
||||
loaded_script = importlib.util.module_from_spec(spec)
|
||||
if spec.loader is None:
|
||||
continue
|
||||
|
|
|
@ -499,10 +499,8 @@ UM.Dialog
|
|||
visible: activeScriptsList.count > 0
|
||||
anchors
|
||||
{
|
||||
top: parent.top
|
||||
right: parent.right
|
||||
rightMargin: (-0.5 * width) | 0
|
||||
topMargin: (-0.5 * height) | 0
|
||||
horizontalCenter: parent.right
|
||||
verticalCenter: parent.top
|
||||
}
|
||||
|
||||
labelText: activeScriptsList.count
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Cura PostProcessingPlugin
|
||||
# Author: Mathias Lyngklip Kjeldgaard, Alexander Gee, Kimmo Toivanen
|
||||
# Author: Mathias Lyngklip Kjeldgaard, Alexander Gee, Kimmo Toivanen, Inigo Martinez
|
||||
# Date: July 31, 2019
|
||||
# Modified: Okt 22, 2020
|
||||
# Modified: Nov 30, 2021
|
||||
|
||||
# Description: This plugin displays progress on the LCD. It can output the estimated time remaining and the completion percentage.
|
||||
|
||||
|
@ -37,7 +37,8 @@ class DisplayProgressOnLCD(Script):
|
|||
"type": "enum",
|
||||
"options": {
|
||||
"m117":"M117 - All printers",
|
||||
"m73":"M73 - Prusa, Marlin 2"
|
||||
"m73":"M73 - Prusa, Marlin 2",
|
||||
"m118":"M118 - Octoprint"
|
||||
},
|
||||
"enabled": "time_remaining",
|
||||
"default_value": "m117"
|
||||
|
@ -77,6 +78,10 @@ class DisplayProgressOnLCD(Script):
|
|||
current_time_string = "{:d}h{:02d}m{:02d}s".format(int(h), int(m), int(s))
|
||||
# And now insert that into the GCODE
|
||||
lines.insert(line_index, "M117 Time Left {}".format(current_time_string))
|
||||
elif mode == "m118":
|
||||
current_time_string = "{:d}h{:02d}m{:02d}s".format(int(h), int(m), int(s))
|
||||
# And now insert that into the GCODE
|
||||
lines.insert(line_index, "M118 A1 P0 action:notification Time Left {}".format(current_time_string))
|
||||
else:
|
||||
mins = int(60 * h + m + s / 30)
|
||||
lines.insert(line_index, "M73 R{}".format(mins))
|
||||
|
@ -107,6 +112,9 @@ class DisplayProgressOnLCD(Script):
|
|||
|
||||
if output_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")
|
||||
|
||||
elif line.startswith(";TIME_ELAPSED:"):
|
||||
|
@ -178,6 +186,9 @@ class DisplayProgressOnLCD(Script):
|
|||
output = min(percentage + previous_layer_end_percentage, 100)
|
||||
|
||||
# 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))
|
||||
|
||||
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)
|