Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/Ultimaker/Cura.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'plugins')
-rw-r--r--plugins/CuraDrive/src/qml/components/BackupListFooter.qml4
-rw-r--r--plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml2
-rw-r--r--plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml6
-rw-r--r--plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml28
-rw-r--r--plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py2
-rw-r--r--plugins/GCodeReader/FlavorParser.py2
-rw-r--r--plugins/Marketplace/Constants.py12
-rw-r--r--plugins/Marketplace/LocalPackageList.py126
-rw-r--r--plugins/Marketplace/Marketplace.py114
-rw-r--r--plugins/Marketplace/PackageList.py305
-rw-r--r--plugins/Marketplace/PackageModel.py382
-rw-r--r--plugins/Marketplace/RemotePackageList.py151
-rw-r--r--plugins/Marketplace/__init__.py17
-rw-r--r--plugins/Marketplace/plugin.json8
-rw-r--r--plugins/Marketplace/resources/images/placeholder.svg (renamed from plugins/Toolbox/resources/images/placeholder.svg)0
-rw-r--r--plugins/Marketplace/resources/qml/LicenseDialog.qml91
-rw-r--r--plugins/Marketplace/resources/qml/ManageButton.qml114
-rw-r--r--plugins/Marketplace/resources/qml/ManagePackagesButton.qml49
-rw-r--r--plugins/Marketplace/resources/qml/ManagedPackages.qml25
-rw-r--r--plugins/Marketplace/resources/qml/Marketplace.qml299
-rw-r--r--plugins/Marketplace/resources/qml/Materials.qml22
-rw-r--r--plugins/Marketplace/resources/qml/OnboardBanner.qml119
-rw-r--r--plugins/Marketplace/resources/qml/PackageCard.qml101
-rw-r--r--plugins/Marketplace/resources/qml/PackageCardHeader.qml215
-rw-r--r--plugins/Marketplace/resources/qml/PackageDetails.qml96
-rw-r--r--plugins/Marketplace/resources/qml/PackagePage.qml295
-rw-r--r--plugins/Marketplace/resources/qml/PackageTypeTab.qml33
-rw-r--r--plugins/Marketplace/resources/qml/Packages.qml232
-rw-r--r--plugins/Marketplace/resources/qml/Plugins.qml22
-rw-r--r--plugins/Marketplace/resources/qml/VerifiedIcon.qml45
-rw-r--r--plugins/PerObjectSettingsTool/PerObjectItem.qml6
-rw-r--r--plugins/PerObjectSettingsTool/SettingPickDialog.qml5
-rw-r--r--plugins/PostProcessingPlugin/PostProcessingPlugin.py2
-rw-r--r--plugins/PostProcessingPlugin/PostProcessingPlugin.qml6
-rw-r--r--plugins/Toolbox/__init__.py15
-rw-r--r--plugins/Toolbox/plugin.json7
-rwxr-xr-xplugins/Toolbox/resources/images/Shop.svg6
-rw-r--r--plugins/Toolbox/resources/images/installed_check.svg8
-rw-r--r--plugins/Toolbox/resources/qml/Toolbox.qml112
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxActionButtonStyle.qml29
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxBackColumn.qml84
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxCompatibilityChart.qml209
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxDetailList.qml43
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxDetailTile.qml74
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxDetailTileActions.qml121
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxDownloadsGrid.qml45
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxDownloadsGridTile.qml125
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcase.qml84
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcaseTile.qml114
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxFooter.qml60
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxHeader.qml112
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxInstalledTile.qml123
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxInstalledTileActions.qml90
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxProgressButton.qml60
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxShadow.qml24
-rw-r--r--plugins/Toolbox/resources/qml/components/ToolboxTabButton.qml68
-rw-r--r--plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml153
-rw-r--r--plugins/Toolbox/resources/qml/dialogs/ToolboxConfirmUninstallResetDialog.qml96
-rw-r--r--plugins/Toolbox/resources/qml/dialogs/ToolboxLicenseDialog.qml110
-rw-r--r--plugins/Toolbox/resources/qml/pages/ToolboxAuthorPage.qml176
-rw-r--r--plugins/Toolbox/resources/qml/pages/ToolboxDetailPage.qml188
-rw-r--r--plugins/Toolbox/resources/qml/pages/ToolboxDownloadsPage.qml46
-rw-r--r--plugins/Toolbox/resources/qml/pages/ToolboxErrorPage.qml23
-rw-r--r--plugins/Toolbox/resources/qml/pages/ToolboxInstalledPage.qml223
-rw-r--r--plugins/Toolbox/resources/qml/pages/ToolboxLoadingPage.qml25
-rw-r--r--plugins/Toolbox/resources/qml/pages/WelcomePage.qml44
-rw-r--r--plugins/Toolbox/src/AuthorsModel.py104
-rw-r--r--plugins/Toolbox/src/CloudApiModel.py29
-rw-r--r--plugins/Toolbox/src/CloudSync/CloudApiClient.py52
-rw-r--r--plugins/Toolbox/src/CloudSync/CloudPackageChecker.py164
-rw-r--r--plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py41
-rw-r--r--plugins/Toolbox/src/CloudSync/DownloadPresenter.py153
-rw-r--r--plugins/Toolbox/src/CloudSync/LicenseModel.py77
-rw-r--r--plugins/Toolbox/src/CloudSync/LicensePresenter.py142
-rw-r--r--plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py32
-rw-r--r--plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py74
-rw-r--r--plugins/Toolbox/src/CloudSync/SyncOrchestrator.py114
-rw-r--r--plugins/Toolbox/src/CloudSync/__init__.py0
-rw-r--r--plugins/Toolbox/src/ConfigsModel.py38
-rw-r--r--plugins/Toolbox/src/PackagesModel.py161
-rw-r--r--plugins/Toolbox/src/Toolbox.py878
-rw-r--r--plugins/Toolbox/src/__init__.py0
-rw-r--r--plugins/UltimakerMachineActions/UMOUpgradeSelectionMachineAction.qml4
83 files changed, 2898 insertions, 4798 deletions
diff --git a/plugins/CuraDrive/src/qml/components/BackupListFooter.qml b/plugins/CuraDrive/src/qml/components/BackupListFooter.qml
index 15af7521ed..76bd10bd66 100644
--- a/plugins/CuraDrive/src/qml/components/BackupListFooter.qml
+++ b/plugins/CuraDrive/src/qml/components/BackupListFooter.qml
@@ -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
diff --git a/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml b/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml
index 4374b2f998..ba2abf22a9 100644
--- a/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml
+++ b/plugins/DigitalLibrary/resources/qml/ProjectSummaryCard.qml
@@ -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
diff --git a/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml b/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml
index d8ae78d96d..c66556071f 100644
--- a/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml
+++ b/plugins/DigitalLibrary/resources/qml/SaveProjectFilesPage.qml
@@ -6,7 +6,7 @@ import QtQuick.Controls 1.4 as OldControls // TableView doesn't exist in the QtQ
import QtQuick.Controls 2.3
import QtQuick.Controls.Styles 1.4
-import UM 1.2 as UM
+import UM 1.5 as UM
import Cura 1.6 as Cura
import DigitalFactory 1.0 as DF
@@ -228,7 +228,7 @@ Item
width: childrenRect.width
spacing: UM.Theme.getSize("default_margin").width
- Cura.CheckBox
+ UM.CheckBox
{
id: asProjectCheckbox
height: UM.Theme.getSize("checkbox").height
@@ -238,7 +238,7 @@ Item
font: UM.Theme.getFont("medium")
}
- Cura.CheckBox
+ UM.CheckBox
{
id: asSlicedCheckbox
height: UM.Theme.getSize("checkbox").height
diff --git a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml
index 89d282ab83..24d4cbfade 100644
--- a/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml
+++ b/plugins/DigitalLibrary/resources/qml/SelectProjectPage.qml
@@ -1,4 +1,4 @@
-// Copyright (C) 2021 Ultimaker B.V.
+// Copyright (C) 2022 Ultimaker B.V.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
@@ -9,7 +9,7 @@ import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.1
import UM 1.2 as UM
-import Cura 1.6 as Cura
+import Cura 1.7 as Cura
import DigitalFactory 1.0 as DF
@@ -44,33 +44,13 @@ Item
height: childrenRect.height
spacing: UM.Theme.getSize("default_margin").width
- Cura.TextField
+ Cura.SearchBar
{
id: searchBar
Layout.fillWidth: true
implicitHeight: createNewProjectButton.height
- leftPadding: searchIcon.width + UM.Theme.getSize("default_margin").width * 2
focus: true
-
onTextEdited: manager.projectFilter = text //Update the search filter when editing this text field.
-
- placeholderText: "Search"
-
- UM.RecolorImage
- {
- id: searchIcon
-
- anchors
- {
- verticalCenter: parent.verticalCenter
- left: parent.left
- leftMargin: UM.Theme.getSize("default_margin").width
- }
- source: UM.Theme.getIcon("search")
- height: UM.Theme.getSize("small_button_icon").height
- width: height
- color: UM.Theme.getColor("text")
- }
}
Cura.SecondaryButton
@@ -222,7 +202,7 @@ Item
LoadMoreProjectsCard
{
id: loadMoreProjectsCard
- height: UM.Theme.getSize("toolbox_thumbnail_small").height
+ height: UM.Theme.getSize("card_icon").height
width: parent.width
visible: manager.digitalFactoryProjectModel.count > 0
hasMoreProjectsToLoad: manager.hasMoreProjectsToLoad
diff --git a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py
index d726cc04a9..8d08cde37b 100644
--- a/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py
+++ b/plugins/FirmwareUpdateChecker/FirmwareUpdateCheckerJob.py
@@ -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
diff --git a/plugins/GCodeReader/FlavorParser.py b/plugins/GCodeReader/FlavorParser.py
index bb99aa59ec..8d35bd3345 100644
--- a/plugins/GCodeReader/FlavorParser.py
+++ b/plugins/GCodeReader/FlavorParser.py
@@ -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])])
diff --git a/plugins/Marketplace/Constants.py b/plugins/Marketplace/Constants.py
new file mode 100644
index 0000000000..9f0f78b966
--- /dev/null
+++ b/plugins/Marketplace/Constants.py
@@ -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"
diff --git a/plugins/Marketplace/LocalPackageList.py b/plugins/Marketplace/LocalPackageList.py
new file mode 100644
index 0000000000..a609e72d33
--- /dev/null
+++ b/plugins/Marketplace/LocalPackageList.py
@@ -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
diff --git a/plugins/Marketplace/Marketplace.py b/plugins/Marketplace/Marketplace.py
new file mode 100644
index 0000000000..2d98947572
--- /dev/null
+++ b/plugins/Marketplace/Marketplace.py
@@ -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
diff --git a/plugins/Marketplace/PackageList.py b/plugins/Marketplace/PackageList.py
new file mode 100644
index 0000000000..04b602002c
--- /dev/null
+++ b/plugins/Marketplace/PackageList.py
@@ -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)
diff --git a/plugins/Marketplace/PackageModel.py b/plugins/Marketplace/PackageModel.py
new file mode 100644
index 0000000000..7c2a5d9ae1
--- /dev/null
+++ b/plugins/Marketplace/PackageModel.py
@@ -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
diff --git a/plugins/Marketplace/RemotePackageList.py b/plugins/Marketplace/RemotePackageList.py
new file mode 100644
index 0000000000..16b0e721ad
--- /dev/null
+++ b/plugins/Marketplace/RemotePackageList.py
@@ -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)
diff --git a/plugins/Marketplace/__init__.py b/plugins/Marketplace/__init__.py
new file mode 100644
index 0000000000..bd65062ba6
--- /dev/null
+++ b/plugins/Marketplace/__init__.py
@@ -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() }
diff --git a/plugins/Marketplace/plugin.json b/plugins/Marketplace/plugin.json
new file mode 100644
index 0000000000..7eeeb5c986
--- /dev/null
+++ b/plugins/Marketplace/plugin.json
@@ -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"
+}
diff --git a/plugins/Toolbox/resources/images/placeholder.svg b/plugins/Marketplace/resources/images/placeholder.svg
index cc674a4b38..cc674a4b38 100644
--- a/plugins/Toolbox/resources/images/placeholder.svg
+++ b/plugins/Marketplace/resources/images/placeholder.svg
diff --git a/plugins/Marketplace/resources/qml/LicenseDialog.qml b/plugins/Marketplace/resources/qml/LicenseDialog.qml
new file mode 100644
index 0000000000..1c99569793
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/LicenseDialog.qml
@@ -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)
+}
diff --git a/plugins/Marketplace/resources/qml/ManageButton.qml b/plugins/Marketplace/resources/qml/ManageButton.qml
new file mode 100644
index 0000000000..36022ffd54
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/ManageButton.qml
@@ -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; }
+ }
+ }
+}
diff --git a/plugins/Marketplace/resources/qml/ManagePackagesButton.qml b/plugins/Marketplace/resources/qml/ManagePackagesButton.qml
new file mode 100644
index 0000000000..92e2196beb
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/ManagePackagesButton.qml
@@ -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
+ }
+}
diff --git a/plugins/Marketplace/resources/qml/ManagedPackages.qml b/plugins/Marketplace/resources/qml/ManagedPackages.qml
new file mode 100644
index 0000000000..8ccaacea46
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/ManagedPackages.qml
@@ -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
+}
diff --git a/plugins/Marketplace/resources/qml/Marketplace.qml b/plugins/Marketplace/resources/qml/Marketplace.qml
new file mode 100644
index 0000000000..fc6d3cd755
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/Marketplace.qml
@@ -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();
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/Marketplace/resources/qml/Materials.qml b/plugins/Marketplace/resources/qml/Materials.qml
new file mode 100644
index 0000000000..39fae7042a
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/Materials.qml
@@ -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
+}
diff --git a/plugins/Marketplace/resources/qml/OnboardBanner.qml b/plugins/Marketplace/resources/qml/OnboardBanner.qml
new file mode 100644
index 0000000000..25e4b53241
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/OnboardBanner.qml
@@ -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)
+ }
+} \ No newline at end of file
diff --git a/plugins/Marketplace/resources/qml/PackageCard.qml b/plugins/Marketplace/resources/qml/PackageCard.qml
new file mode 100644
index 0000000000..633d2b25b9
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/PackageCard.qml
@@ -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")
+ }
+}
diff --git a/plugins/Marketplace/resources/qml/PackageCardHeader.qml b/plugins/Marketplace/resources/qml/PackageCardHeader.qml
new file mode 100644
index 0000000000..3a76f7a959
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/PackageCardHeader.qml
@@ -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()
+ }
+ }
+ }
+}
diff --git a/plugins/Marketplace/resources/qml/PackageDetails.qml b/plugins/Marketplace/resources/qml/PackageDetails.qml
new file mode 100644
index 0000000000..2599c7f28c
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/PackageDetails.qml
@@ -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
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/plugins/Marketplace/resources/qml/PackagePage.qml b/plugins/Marketplace/resources/qml/PackagePage.qml
new file mode 100644
index 0000000000..21c400fff2
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/PackagePage.qml
@@ -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")
+ }
+}
diff --git a/plugins/Marketplace/resources/qml/PackageTypeTab.qml b/plugins/Marketplace/resources/qml/PackageTypeTab.qml
new file mode 100644
index 0000000000..79eaa9a16c
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/PackageTypeTab.qml
@@ -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
+ }
+} \ No newline at end of file
diff --git a/plugins/Marketplace/resources/qml/Packages.qml b/plugins/Marketplace/resources/qml/Packages.qml
new file mode 100644
index 0000000000..194c90c248
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/Packages.qml
@@ -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")
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/Marketplace/resources/qml/Plugins.qml b/plugins/Marketplace/resources/qml/Plugins.qml
new file mode 100644
index 0000000000..9983a827d8
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/Plugins.qml
@@ -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
+}
diff --git a/plugins/Marketplace/resources/qml/VerifiedIcon.qml b/plugins/Marketplace/resources/qml/VerifiedIcon.qml
new file mode 100644
index 0000000000..30ef3080a0
--- /dev/null
+++ b/plugins/Marketplace/resources/qml/VerifiedIcon.qml
@@ -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 )
+} \ No newline at end of file
diff --git a/plugins/PerObjectSettingsTool/PerObjectItem.qml b/plugins/PerObjectSettingsTool/PerObjectItem.qml
index bb1c31e1f3..9700b2265b 100644
--- a/plugins/PerObjectSettingsTool/PerObjectItem.qml
+++ b/plugins/PerObjectSettingsTool/PerObjectItem.qml
@@ -6,7 +6,9 @@ import QtQuick.Layouts 1.1
import QtQuick.Controls 1.1
import QtQuick.Controls.Styles 1.1
-import UM 1.2 as UM
+import UM 1.5 as UM
+
+import Cura 1.0 as Cura
UM.TooltipArea
{
@@ -16,7 +18,7 @@ UM.TooltipArea
width: childrenRect.width;
height: childrenRect.height;
- CheckBox
+ UM.CheckBox
{
id: check
diff --git a/plugins/PerObjectSettingsTool/SettingPickDialog.qml b/plugins/PerObjectSettingsTool/SettingPickDialog.qml
index 1bba094e49..0b03ef5008 100644
--- a/plugins/PerObjectSettingsTool/SettingPickDialog.qml
+++ b/plugins/PerObjectSettingsTool/SettingPickDialog.qml
@@ -2,7 +2,7 @@ import QtQuick 2.2
import QtQuick.Controls 1.2
import QtQuick.Controls.Styles 1.2
-import UM 1.2 as UM
+import UM 1.5 as UM
import Cura 1.0 as Cura
import ".."
@@ -57,13 +57,14 @@ UM.Dialog
onTextChanged: settingPickDialog.updateFilter()
}
- CheckBox
+ UM.CheckBox
{
id: toggleShowAll
anchors
{
top: parent.top
right: parent.right
+ verticalCenter: filterInput.verticalCenter
}
text: catalog.i18nc("@label:checkbox", "Show all")
}
diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.py b/plugins/PostProcessingPlugin/PostProcessingPlugin.py
index 8968e2c547..755d815d0a 100644
--- a/plugins/PostProcessingPlugin/PostProcessingPlugin.py
+++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.py
@@ -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
diff --git a/plugins/PostProcessingPlugin/PostProcessingPlugin.qml b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml
index bbba2e7621..bd94d1fdfd 100644
--- a/plugins/PostProcessingPlugin/PostProcessingPlugin.qml
+++ b/plugins/PostProcessingPlugin/PostProcessingPlugin.qml
@@ -527,10 +527,8 @@ UM.Dialog
visible: activeScriptsList.count > 0
anchors
{
- top: parent.top
- right: parent.right
- rightMargin: (-0.5 * width) | 0
- topMargin: (-0.5 * height) | 0
+ horizontalCenter: parent.right
+ verticalCenter: parent.top
}
labelText: activeScriptsList.count
diff --git a/plugins/Toolbox/__init__.py b/plugins/Toolbox/__init__.py
deleted file mode 100644
index 51f1b643d0..0000000000
--- a/plugins/Toolbox/__init__.py
+++ /dev/null
@@ -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)]
- }
diff --git a/plugins/Toolbox/plugin.json b/plugins/Toolbox/plugin.json
deleted file mode 100644
index ed4a3eae97..0000000000
--- a/plugins/Toolbox/plugin.json
+++ /dev/null
@@ -1,7 +0,0 @@
-{
- "name": "Toolbox",
- "author": "Ultimaker B.V.",
- "version": "1.0.1",
- "api": 7,
- "description": "Find, manage and install new Cura packages."
-}
diff --git a/plugins/Toolbox/resources/images/Shop.svg b/plugins/Toolbox/resources/images/Shop.svg
deleted file mode 100755
index 5056a25c51..0000000000
--- a/plugins/Toolbox/resources/images/Shop.svg
+++ /dev/null
@@ -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>
diff --git a/plugins/Toolbox/resources/images/installed_check.svg b/plugins/Toolbox/resources/images/installed_check.svg
deleted file mode 100644
index 1f1302770b..0000000000
--- a/plugins/Toolbox/resources/images/installed_check.svg
+++ /dev/null
@@ -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>
diff --git a/plugins/Toolbox/resources/qml/Toolbox.qml b/plugins/Toolbox/resources/qml/Toolbox.qml
deleted file mode 100644
index b67d175194..0000000000
--- a/plugins/Toolbox/resources/qml/Toolbox.qml
+++ /dev/null
@@ -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
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxActionButtonStyle.qml b/plugins/Toolbox/resources/qml/components/ToolboxActionButtonStyle.qml
deleted file mode 100644
index eff74278c9..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxActionButtonStyle.qml
+++ /dev/null
@@ -1,29 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.2
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
-import UM 1.1 as UM
-
-ButtonStyle
-{
- background: Rectangle
- {
- implicitWidth: UM.Theme.getSize("toolbox_action_button").width
- implicitHeight: UM.Theme.getSize("toolbox_action_button").height
- color: "transparent"
- border
- {
- width: UM.Theme.getSize("default_lining").width
- color: UM.Theme.getColor("lining")
- }
- }
- label: Label
- {
- text: control.text
- color: UM.Theme.getColor("text")
- verticalAlignment: Text.AlignVCenter
- horizontalAlignment: Text.AlignHCenter
- }
-} \ No newline at end of file
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxBackColumn.qml b/plugins/Toolbox/resources/qml/components/ToolboxBackColumn.qml
deleted file mode 100644
index 9874a977f5..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxBackColumn.qml
+++ /dev/null
@@ -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
- }
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxCompatibilityChart.qml b/plugins/Toolbox/resources/qml/components/ToolboxCompatibilityChart.qml
deleted file mode 100644
index e1f88a473f..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxCompatibilityChart.qml
+++ /dev/null
@@ -1,209 +0,0 @@
-// Copyright (c) 2019 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 1.4
-
-import UM 1.5 as UM
-
-Item
-{
- id: base
-
- property var packageData
- property var technicalDataSheetUrl: packageData.links.technicalDataSheet
- property var safetyDataSheetUrl: packageData.links.safetyDataSheet
- property var printingGuidelinesUrl: packageData.links.printingGuidelines
- property var materialWebsiteUrl: packageData.links.website
-
- height: childrenRect.height
- onVisibleChanged: packageData.type === "material" && (compatibilityItem.visible || dataSheetLinks.visible)
-
- Column
- {
- id: compatibilityItem
- visible: packageData.has_configs
- width: parent.width
- // This is a bit of a hack, but the whole QML is pretty messy right now. This needs a big overhaul.
- height: visible ? heading.height + table.height: 0
-
- Label
- {
- id: heading
- width: parent.width
- text: catalog.i18nc("@label", "Compatibility")
- wrapMode: Text.WordWrap
- color: UM.Theme.getColor("text_medium")
- font: UM.Theme.getFont("medium")
- renderType: Text.NativeRendering
- }
-
- TableView
- {
- id: table
- width: parent.width
- frameVisible: false
-
- // Workaround for scroll issues (QTBUG-49652)
- flickableItem.interactive: false
- Component.onCompleted:
- {
- for (var i = 0; i < flickableItem.children.length; ++i)
- {
- flickableItem.children[i].enabled = false
- }
- }
- selectionMode: 0
- model: packageData.supported_configs
- headerDelegate: Rectangle
- {
- color: UM.Theme.getColor("main_background")
- height: UM.Theme.getSize("toolbox_chart_row").height
- Label
- {
- anchors.verticalCenter: parent.verticalCenter
- elide: Text.ElideRight
- text: styleData.value || ""
- color: UM.Theme.getColor("text")
- font: UM.Theme.getFont("default_bold")
- renderType: Text.NativeRendering
- }
- Rectangle
- {
- anchors.bottom: parent.bottom
- height: UM.Theme.getSize("default_lining").height
- width: parent.width
- color: "black"
- }
- }
- rowDelegate: Item
- {
- height: UM.Theme.getSize("toolbox_chart_row").height
- Label
- {
- anchors.verticalCenter: parent.verticalCenter
- elide: Text.ElideRight
- text: styleData.value || ""
- color: UM.Theme.getColor("text_medium")
- font: UM.Theme.getFont("default")
- renderType: Text.NativeRendering
- }
- }
- itemDelegate: Item
- {
- height: UM.Theme.getSize("toolbox_chart_row").height
- Label
- {
- anchors.verticalCenter: parent.verticalCenter
- elide: Text.ElideRight
- text: styleData.value || ""
- color: UM.Theme.getColor("text_medium")
- font: UM.Theme.getFont("default")
- renderType: Text.NativeRendering
- }
- }
-
- Component
- {
- id: columnTextDelegate
- Label
- {
- anchors.fill: parent
- verticalAlignment: Text.AlignVCenter
- text: styleData.value || ""
- elide: Text.ElideRight
- color: UM.Theme.getColor("text_medium")
- font: UM.Theme.getFont("default")
- renderType: Text.NativeRendering
- }
- }
-
- TableViewColumn
- {
- role: "machine"
- title: catalog.i18nc("@label:table_header", "Machine")
- width: Math.floor(table.width * 0.25)
- delegate: columnTextDelegate
- }
- TableViewColumn
- {
- role: "print_core"
- title: "Print Core" //This term should not be translated.
- width: Math.floor(table.width * 0.2)
- }
- TableViewColumn
- {
- role: "build_plate"
- title: catalog.i18nc("@label:table_header", "Build Plate")
- width: Math.floor(table.width * 0.225)
- }
- TableViewColumn
- {
- role: "support_material"
- title: catalog.i18nc("@label:table_header", "Support")
- width: Math.floor(table.width * 0.225)
- }
- TableViewColumn
- {
- role: "quality"
- title: catalog.i18nc("@label:table_header", "Quality")
- width: Math.floor(table.width * 0.1)
- }
- }
- }
-
- Label
- {
- id: dataSheetLinks
- anchors.top: compatibilityItem.bottom
- anchors.topMargin: UM.Theme.getSize("narrow_margin").height
- visible: base.technicalDataSheetUrl !== undefined ||
- base.safetyDataSheetUrl !== undefined ||
- base.printingGuidelinesUrl !== undefined ||
- base.materialWebsiteUrl !== undefined
-
- text:
- {
- var result = ""
- if (base.technicalDataSheetUrl !== undefined)
- {
- var tds_name = catalog.i18nc("@action:label", "Technical Data Sheet")
- result += "<a href='%1'>%2</a>".arg(base.technicalDataSheetUrl).arg(tds_name)
- }
- if (base.safetyDataSheetUrl !== undefined)
- {
- if (result.length > 0)
- {
- result += "<br/>"
- }
- var sds_name = catalog.i18nc("@action:label", "Safety Data Sheet")
- result += "<a href='%1'>%2</a>".arg(base.safetyDataSheetUrl).arg(sds_name)
- }
- if (base.printingGuidelinesUrl !== undefined)
- {
- if (result.length > 0)
- {
- result += "<br/>"
- }
- var pg_name = catalog.i18nc("@action:label", "Printing Guidelines")
- result += "<a href='%1'>%2</a>".arg(base.printingGuidelinesUrl).arg(pg_name)
- }
- if (base.materialWebsiteUrl !== undefined)
- {
- if (result.length > 0)
- {
- result += "<br/>"
- }
- var pg_name = catalog.i18nc("@action:label", "Website")
- result += "<a href='%1'>%2</a>".arg(base.materialWebsiteUrl).arg(pg_name)
- }
-
- return result
- }
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text")
- linkColor: UM.Theme.getColor("text_link")
- onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
- renderType: Text.NativeRendering
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDetailList.qml b/plugins/Toolbox/resources/qml/components/ToolboxDetailList.qml
deleted file mode 100644
index 22c6b6045f..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxDetailList.qml
+++ /dev/null
@@ -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"
- }
- }
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDetailTile.qml b/plugins/Toolbox/resources/qml/components/ToolboxDetailTile.qml
deleted file mode 100644
index 5badc6b66d..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxDetailTile.qml
+++ /dev/null
@@ -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.
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDetailTileActions.qml b/plugins/Toolbox/resources/qml/components/ToolboxDetailTileActions.qml
deleted file mode 100644
index d683877605..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxDetailTileActions.qml
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
-import UM 1.5 as UM
-import Cura 1.1 as Cura
-
-Column
-{
- property bool installed: toolbox.isInstalled(model.id)
- property bool canUpdate: CuraApplication.getPackageManager().packagesWithUpdate.indexOf(model.id) != -1
- property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
- property var packageData
-
- width: UM.Theme.getSize("toolbox_action_button").width
- spacing: UM.Theme.getSize("narrow_margin").height
-
- Item
- {
- width: installButton.width
- height: installButton.height
- ToolboxProgressButton
- {
- id: installButton
- active: toolbox.isDownloading && toolbox.activePackage == model
- onReadyAction:
- {
- toolbox.activePackage = model
- toolbox.startDownload(model.download_url)
- }
- onActiveAction: toolbox.cancelDownload()
-
- // Don't allow installing while another download is running
- enabled: installed || (!(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired)
- opacity: enabled ? 1.0 : 0.5
- visible: !updateButton.visible && !installed // Don't show when the update button is visible
- }
-
- Cura.SecondaryButton
- {
- id: installedButton
- visible: installed
- onClicked: toolbox.viewCategory = "installed"
- text: catalog.i18nc("@action:button", "Installed")
- fixedWidthMode: true
- width: installButton.width
- height: installButton.height
- }
- }
-
- Label
- {
- wrapMode: Text.WordWrap
- text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Log in</a> is required to install or update")
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text")
- linkColor: UM.Theme.getColor("text_link")
- visible: loginRequired
- width: installButton.width
- renderType: Text.NativeRendering
-
- MouseArea
- {
- anchors.fill: parent
- onClicked: Cura.API.account.login()
- }
- }
-
- Label
- {
- property var whereToBuyUrl:
- {
- var pg_name = "whereToBuy"
- return (pg_name in packageData.links) ? packageData.links[pg_name] : undefined
- }
-
- renderType: Text.NativeRendering
- text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Buy material spools</a>")
- linkColor: UM.Theme.getColor("text_link")
- visible: whereToBuyUrl != undefined
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text")
- MouseArea
- {
- anchors.fill: parent
- onClicked: UM.UrlUtil.openUrl(parent.whereToBuyUrl, ["https", "http"])
- }
- }
-
- ToolboxProgressButton
- {
- id: updateButton
- active: toolbox.isDownloading && toolbox.activePackage == model
- readyLabel: catalog.i18nc("@action:button", "Update")
- activeLabel: catalog.i18nc("@action:button", "Updating")
- completeLabel: catalog.i18nc("@action:button", "Updated")
-
- onReadyAction:
- {
- toolbox.activePackage = model
- toolbox.update(model.id)
- }
- onActiveAction: toolbox.cancelDownload()
- // Don't allow installing while another download is running
- enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired
- opacity: enabled ? 1.0 : 0.5
- visible: canUpdate
- }
-
- Connections
- {
- target: toolbox
- function onInstallChanged() { installed = toolbox.isInstalled(model.id) }
- function onFilterChanged()
- {
- installed = toolbox.isInstalled(model.id)
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGrid.qml b/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGrid.qml
deleted file mode 100644
index 6682281a31..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGrid.qml
+++ /dev/null
@@ -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"
- }
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGridTile.qml b/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGridTile.qml
deleted file mode 100644
index c310bd7121..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsGridTile.qml
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
-import QtQuick.Layouts 1.3
-import UM 1.1 as UM
-import Cura 1.1 as Cura
-
-Item
-{
- id: toolboxDownloadsGridTile
- property int packageCount: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getTotalNumberOfMaterialPackagesByAuthor(model.id) : 1
- property int installedPackages: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0)
- height: childrenRect.height
- Layout.alignment: Qt.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
- }
-
- Label
- {
- id: name
- text: model.name
- width: parent.width
- elide: Text.ElideRight
- color: UM.Theme.getColor("text")
- font: UM.Theme.getFont("default_bold")
- }
- Label
- {
- id: info
- text: model.description
- elide: Text.ElideRight
- width: parent.width
- wrapMode: Text.WordWrap
- color: UM.Theme.getColor("text")
- font: UM.Theme.getFont("default")
- anchors.top: name.bottom
- anchors.bottom: parent.bottom
- verticalAlignment: Text.AlignVCenter
- maximumLineCount: 2
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcase.qml b/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcase.qml
deleted file mode 100644
index a42a10aa29..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcase.qml
+++ /dev/null
@@ -1,84 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
-import UM 1.1 as UM
-
-Rectangle
-{
- color: UM.Theme.getColor("toolbox_premium_packages_background")
- height: childrenRect.height
- width: parent.width
- Column
- {
- height: childrenRect.height + 2 * padding
- spacing: UM.Theme.getSize("default_margin").height
- width: parent.width
- padding: UM.Theme.getSize("wide_margin").height
- Item
- {
- width: parent.width - parent.padding * 2
- height: childrenRect.height
- Label
- {
- id: heading
- text: catalog.i18nc("@label", "Premium")
- width: contentWidth
- height: contentHeight
- color: UM.Theme.getColor("text_medium")
- font: UM.Theme.getFont("large")
- renderType: Text.NativeRendering
- }
- UM.TooltipArea
- {
- width: childrenRect.width
- height: childrenRect.height
- anchors.right: parent.right
- text: catalog.i18nc("@info:tooltip", "Go to Web Marketplace")
- Label
- {
- text: "<a href='%2'>".arg(toolbox.getWebMarketplaceUrl("materials") + "?utm_source=cura&utm_medium=software&utm_campaign=marketplace-search") + catalog.i18nc("@label", "Search materials") + "</a>"
- width: contentWidth
- height: contentHeight
- horizontalAlignment: Text.AlignRight
- font: UM.Theme.getFont("default")
- renderType: Text.NativeRendering
-
- linkColor: UM.Theme.getColor("text_link")
- onLinkActivated: Qt.openUrlExternally(link)
-
- visible: toolbox.viewCategory === "material"
- }
- }
- }
- Grid
- {
- height: childrenRect.height
- spacing: UM.Theme.getSize("wide_margin").width
- columns: 3
- anchors.horizontalCenter: parent.horizontalCenter
-
- Repeater
- {
- model:
- {
- if (toolbox.viewCategory == "plugin")
- {
- return toolbox.pluginsShowcaseModel
- }
- if (toolbox.viewCategory == "material")
- {
- return toolbox.materialsShowcaseModel
- }
- }
- delegate: Loader
- {
- asynchronous: true
- source: "ToolboxDownloadsShowcaseTile.qml"
- }
- }
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcaseTile.qml b/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcaseTile.qml
deleted file mode 100644
index 6695921126..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxDownloadsShowcaseTile.qml
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
-
-import UM 1.1 as UM
-
-Rectangle
-{
- property int packageCount: toolbox.viewCategory == "material" ? toolbox.getTotalNumberOfMaterialPackagesByAuthor(model.id) : 1
- property int installedPackages: toolbox.viewCategory == "material" ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0)
- id: tileBase
- width: UM.Theme.getSize("toolbox_thumbnail_large").width + (2 * UM.Theme.getSize("default_lining").width)
- height: thumbnail.height + packageName.height + UM.Theme.getSize("default_margin").width
- border.width: UM.Theme.getSize("default_lining").width
- border.color: UM.Theme.getColor("lining")
- color: UM.Theme.getColor("main_background")
- Image
- {
- id: thumbnail
- height: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height
- width: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height
- sourceSize.height: height
- sourceSize.width: width
- fillMode: Image.PreserveAspectFit
- source: model.icon_url || "../../images/placeholder.svg"
- mipmap: true
- anchors
- {
- top: parent.top
- topMargin: UM.Theme.getSize("default_margin").height
- horizontalCenter: parent.horizontalCenter
- }
- }
- Label
- {
- id: packageName
- text: model.name
- anchors
- {
- horizontalCenter: parent.horizontalCenter
- top: thumbnail.bottom
- }
- verticalAlignment: Text.AlignVCenter
- horizontalAlignment: Text.AlignHCenter
- renderType: Text.NativeRendering
- height: UM.Theme.getSize("toolbox_heading_label").height
- width: parent.width - UM.Theme.getSize("default_margin").width
- wrapMode: Text.WordWrap
- elide: Text.ElideRight
- font: UM.Theme.getFont("medium_bold")
- color: UM.Theme.getColor("text")
- }
- UM.RecolorImage
- {
- width: (parent.width * 0.20) | 0
- height: width
- anchors
- {
- bottom: bottomBorder.top
- right: parent.right
- }
- visible: installedPackages != 0
- color: (installedPackages >= packageCount) ? UM.Theme.getColor("primary") : UM.Theme.getColor("border")
- source: "../../images/installed_check.svg"
- }
-
- Rectangle
- {
- id: bottomBorder
- color: UM.Theme.getColor("primary")
- anchors.bottom: parent.bottom
- width: parent.width
- height: UM.Theme.getSize("toolbox_header_highlight").height
- }
-
- MouseArea
- {
- anchors.fill: parent
- hoverEnabled: true
- onEntered: tileBase.border.color = UM.Theme.getColor("primary")
- onExited: tileBase.border.color = UM.Theme.getColor("lining")
- onClicked:
- {
- base.selection = model
- switch(toolbox.viewCategory)
- {
- case "material":
-
- // If model has a type, it must be a package
- if (model.type !== undefined)
- {
- toolbox.viewPage = "detail"
- toolbox.filterModelByProp("packages", "id", model.id)
- }
- else
- {
- toolbox.viewPage = "author"
- toolbox.setFilters("packages", {
- "author_id": model.id,
- "type": "material"
- })
- }
- break
- default:
- toolbox.viewPage = "detail"
- toolbox.filterModelByProp("packages", "id", model.id)
- break
- }
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxFooter.qml b/plugins/Toolbox/resources/qml/components/ToolboxFooter.qml
deleted file mode 100644
index 9863bd9a93..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxFooter.qml
+++ /dev/null
@@ -1,60 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 2.3
-
-import UM 1.1 as UM
-import Cura 1.0 as Cura
-
-Item
-{
- id: footer
- width: parent.width
- anchors.bottom: parent.bottom
- height: visible ? UM.Theme.getSize("toolbox_footer").height : 0
-
- Label
- {
- text: catalog.i18nc("@info", "You will need to restart Cura before changes in packages have effect.")
- color: UM.Theme.getColor("text")
- height: UM.Theme.getSize("toolbox_footer_button").height
- verticalAlignment: Text.AlignVCenter
- wrapMode: Text.WordWrap
- anchors
- {
- top: restartButton.top
- left: parent.left
- leftMargin: UM.Theme.getSize("wide_margin").width
- right: restartButton.left
- rightMargin: UM.Theme.getSize("default_margin").width
- }
- renderType: Text.NativeRendering
- }
-
- Cura.PrimaryButton
- {
- id: restartButton
- anchors
- {
- top: parent.top
- topMargin: UM.Theme.getSize("default_margin").height
- right: parent.right
- rightMargin: UM.Theme.getSize("wide_margin").width
- }
- height: UM.Theme.getSize("toolbox_footer_button").height
- text: catalog.i18nc("@info:button, %1 is the application name", "Quit %1").arg(CuraApplication.applicationDisplayName)
- onClicked:
- {
- base.hide()
- toolbox.restart()
- }
- }
-
- ToolboxShadow
- {
- visible: footer.visible
- anchors.bottom: footer.top
- reversed: true
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxHeader.qml b/plugins/Toolbox/resources/qml/components/ToolboxHeader.qml
deleted file mode 100644
index 2c43110af9..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxHeader.qml
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright (c) 2020 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 1.4
-
-import UM 1.4 as UM
-import Cura 1.0 as Cura
-
-Item
-{
- id: header
- width: parent.width
- height: UM.Theme.getSize("toolbox_header").height
- Row
- {
- id: bar
- spacing: UM.Theme.getSize("default_margin").width
- height: childrenRect.height
- width: childrenRect.width
- anchors
- {
- left: parent.left
- leftMargin: UM.Theme.getSize("default_margin").width
- }
-
- ToolboxTabButton
- {
- id: pluginsTabButton
- text: catalog.i18nc("@title:tab", "Plugins")
- active: toolbox.viewCategory == "plugin" && enabled
- enabled: !toolbox.isDownloading && toolbox.viewPage != "loading" && toolbox.viewPage != "errored"
- onClicked:
- {
- toolbox.filterModelByProp("packages", "type", "plugin")
- toolbox.viewCategory = "plugin"
- toolbox.viewPage = "overview"
- }
- }
-
- ToolboxTabButton
- {
- id: materialsTabButton
- text: catalog.i18nc("@title:tab", "Materials")
- active: toolbox.viewCategory == "material" && enabled
- enabled: !toolbox.isDownloading && toolbox.viewPage != "loading" && toolbox.viewPage != "errored"
- onClicked:
- {
- toolbox.filterModelByProp("authors", "package_types", "material")
- toolbox.viewCategory = "material"
- toolbox.viewPage = "overview"
- }
- }
-
- ToolboxTabButton
- {
- id: installedTabButton
- text: catalog.i18nc("@title:tab", "Installed")
- active: toolbox.viewCategory == "installed"
- enabled: !toolbox.isDownloading
- onClicked: toolbox.viewCategory = "installed"
- width: UM.Theme.getSize("toolbox_header_tab").width + marketplaceNotificationIcon.width - UM.Theme.getSize("default_margin").width
- }
-
-
- }
-
- Cura.NotificationIcon
- {
- id: marketplaceNotificationIcon
- visible: CuraApplication.getPackageManager().packagesWithUpdate.length > 0
- anchors.right: bar.right
- labelText:
- {
- const itemCount = CuraApplication.getPackageManager().packagesWithUpdate.length
- return itemCount > 9 ? "9+" : itemCount
- }
- }
-
-
- UM.TooltipArea
- {
- id: webMarketplaceButtonTooltipArea
- width: childrenRect.width
- height: parent.height
- text: catalog.i18nc("@info:tooltip", "Go to Web Marketplace")
- anchors
- {
- right: parent.right
- rightMargin: UM.Theme.getSize("default_margin").width
- verticalCenter: parent.verticalCenter
- }
- acceptedButtons: Qt.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
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxInstalledTile.qml b/plugins/Toolbox/resources/qml/components/ToolboxInstalledTile.qml
deleted file mode 100644
index e5c94fc996..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxInstalledTile.qml
+++ /dev/null
@@ -1,123 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
-import UM 1.1 as UM
-
-Item
-{
- height: UM.Theme.getSize("toolbox_installed_tile").height
- width: parent.width
- property bool isEnabled: true
-
- Rectangle
- {
- color: UM.Theme.getColor("lining")
- width: parent.width
- height: Math.floor(UM.Theme.getSize("default_lining").height)
- anchors.bottom: parent.top
- visible: index != 0
- }
- Row
- {
- id: tileRow
- height: parent.height
- width: parent.width
- spacing: UM.Theme.getSize("default_margin").width
- topPadding: UM.Theme.getSize("default_margin").height
-
- CheckBox
- {
- id: disableButton
- anchors.verticalCenter: pluginInfo.verticalCenter
- checked: isEnabled
- visible: model.type == "plugin"
- width: visible ? UM.Theme.getSize("checkbox").width : 0
- enabled: !toolbox.isDownloading
- style: UM.Theme.styles.checkbox
- onClicked: toolbox.isEnabled(model.id) ? toolbox.disable(model.id) : toolbox.enable(model.id)
- }
- Column
- {
- id: pluginInfo
- topPadding: UM.Theme.getSize("narrow_margin").height
- property var color: model.type === "plugin" && !isEnabled ? UM.Theme.getColor("lining") : UM.Theme.getColor("text")
- width: Math.floor(tileRow.width - (authorInfo.width + pluginActions.width + 2 * tileRow.spacing + ((disableButton.visible) ? disableButton.width + tileRow.spacing : 0)))
- Label
- {
- text: model.name
- width: parent.width
- maximumLineCount: 1
- elide: Text.ElideRight
- wrapMode: Text.WordWrap
- font: UM.Theme.getFont("large_bold")
- color: pluginInfo.color
- renderType: Text.NativeRendering
- }
- Label
- {
- text: model.description
- font: UM.Theme.getFont("default")
- maximumLineCount: 3
- elide: Text.ElideRight
- width: parent.width
- wrapMode: Text.WordWrap
- color: pluginInfo.color
- renderType: Text.NativeRendering
- }
- }
- Column
- {
- id: authorInfo
- width: Math.floor(UM.Theme.getSize("toolbox_action_button").width * 1.25)
-
- Label
- {
- text:
- {
- if (model.author_email)
- {
- return "<a href=\"mailto:" + model.author_email + "?Subject=Cura: " + model.name + "\">" + model.author_name + "</a>"
- }
- else
- {
- return model.author_name
- }
- }
- font: UM.Theme.getFont("medium")
- width: parent.width
- height: Math.floor(UM.Theme.getSize("toolbox_property_label").height)
- wrapMode: Text.WordWrap
- verticalAlignment: Text.AlignVCenter
- horizontalAlignment: Text.AlignLeft
- onLinkActivated: Qt.openUrlExternally("mailto:" + model.author_email + "?Subject=Cura: " + model.name + " Plugin")
- color: model.enabled ? UM.Theme.getColor("text") : UM.Theme.getColor("lining")
- linkColor: UM.Theme.getColor("text_link")
- renderType: Text.NativeRendering
- }
-
- Label
- {
- text: model.version
- font: UM.Theme.getFont("default")
- width: parent.width
- height: UM.Theme.getSize("toolbox_property_label").height
- color: UM.Theme.getColor("text")
- verticalAlignment: Text.AlignVCenter
- horizontalAlignment: Text.AlignLeft
- renderType: Text.NativeRendering
- }
- }
- ToolboxInstalledTileActions
- {
- id: pluginActions
- }
- Connections
- {
- target: toolbox
- function onToolboxEnabledChanged() { isEnabled = toolbox.isEnabled(model.id) }
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxInstalledTileActions.qml b/plugins/Toolbox/resources/qml/components/ToolboxInstalledTileActions.qml
deleted file mode 100644
index 1726497c00..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxInstalledTileActions.qml
+++ /dev/null
@@ -1,90 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
-import UM 1.1 as UM
-
-import Cura 1.1 as Cura
-
-Column
-{
- property bool canUpdate: CuraApplication.getPackageManager().packagesWithUpdate.indexOf(model.id) != -1
- property bool canDowngrade: false
- property bool loginRequired: model.login_required && !Cura.API.account.isLoggedIn
- width: UM.Theme.getSize("toolbox_action_button").width
- spacing: UM.Theme.getSize("narrow_margin").height
-
- Label
- {
- visible: !model.is_installed
- text: catalog.i18nc("@label", "Will install upon restarting")
- color: UM.Theme.getColor("lining")
- font: UM.Theme.getFont("default")
- wrapMode: Text.WordWrap
- width: parent.width
- renderType: Text.NativeRendering
- }
-
- ToolboxProgressButton
- {
- id: updateButton
- active: toolbox.isDownloading && toolbox.activePackage == model
- readyLabel: catalog.i18nc("@action:button", "Update")
- activeLabel: catalog.i18nc("@action:button", "Updating")
- completeLabel: catalog.i18nc("@action:button", "Updated")
- onReadyAction:
- {
- toolbox.activePackage = model
- toolbox.update(model.id)
- }
- onActiveAction: toolbox.cancelDownload()
-
- // Don't allow installing while another download is running
- enabled: !(toolbox.isDownloading && toolbox.activePackage != model) && !loginRequired
- opacity: enabled ? 1.0 : 0.5
- visible: canUpdate
- }
-
- Label
- {
- wrapMode: Text.WordWrap
- text: catalog.i18nc("@label:The string between <a href=> and </a> is the highlighted link", "<a href='%1'>Log in</a> is required to update")
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text")
- linkColor: UM.Theme.getColor("text_link")
- visible: loginRequired
- width: updateButton.width
- renderType: Text.NativeRendering
-
- MouseArea
- {
- anchors.fill: parent
- onClicked: Cura.API.account.login()
- }
- }
-
- Cura.SecondaryButton
- {
- id: removeButton
- text: canDowngrade ? catalog.i18nc("@action:button", "Downgrade") : catalog.i18nc("@action:button", "Uninstall")
- visible: !model.is_bundled && model.is_installed
- enabled: !toolbox.isDownloading
-
- width: UM.Theme.getSize("toolbox_action_button").width
- height: UM.Theme.getSize("toolbox_action_button").height
-
- fixedWidthMode: true
-
- onClicked: toolbox.checkPackageUsageAndUninstall(model.id)
- Connections
- {
- target: toolbox
- function onMetadataChanged()
- {
- canDowngrade = toolbox.canDowngrade(model.id)
- }
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxProgressButton.qml b/plugins/Toolbox/resources/qml/components/ToolboxProgressButton.qml
deleted file mode 100644
index 40d6c1af47..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxProgressButton.qml
+++ /dev/null
@@ -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
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxShadow.qml b/plugins/Toolbox/resources/qml/components/ToolboxShadow.qml
deleted file mode 100644
index 0f2f98beb9..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxShadow.qml
+++ /dev/null
@@ -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)
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/components/ToolboxTabButton.qml b/plugins/Toolbox/resources/qml/components/ToolboxTabButton.qml
deleted file mode 100644
index 7a7d2be48a..0000000000
--- a/plugins/Toolbox/resources/qml/components/ToolboxTabButton.qml
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 2.3
-import UM 1.1 as UM
-
-Button
-{
- id: control
- property bool active: false
-
- implicitWidth: UM.Theme.getSize("toolbox_header_tab").width
- implicitHeight: UM.Theme.getSize("toolbox_header_tab").height
-
- background: Item
- {
- id: backgroundItem
- Rectangle
- {
- id: highlight
-
- visible: control.active
- color: UM.Theme.getColor("primary")
- anchors.bottom: parent.bottom
- width: parent.width
- height: UM.Theme.getSize("toolbox_header_highlight").height
- }
- }
-
- contentItem: Label
- {
- id: label
- text: control.text
- color: UM.Theme.getColor("toolbox_header_button_text_inactive")
- font: UM.Theme.getFont("medium")
-
- verticalAlignment: Text.AlignVCenter
- horizontalAlignment: Text.AlignHCenter
-
- renderType: Text.NativeRendering
- }
-
- states:
- [
- State
- {
- name: "disabled"
- when: !control.enabled
- PropertyChanges
- {
- target: label
- font: UM.Theme.getFont("default_italic")
- }
- },
- State
- {
- name: "active"
- when: control.active
- PropertyChanges
- {
- target: label
- font: UM.Theme.getFont("medium_bold")
- color: UM.Theme.getColor("action_button_text")
- }
- }
- ]
-} \ No newline at end of file
diff --git a/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml b/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml
deleted file mode 100644
index b33036847b..0000000000
--- a/plugins/Toolbox/resources/qml/dialogs/CompatibilityDialog.qml
+++ /dev/null
@@ -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
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/dialogs/ToolboxConfirmUninstallResetDialog.qml b/plugins/Toolbox/resources/qml/dialogs/ToolboxConfirmUninstallResetDialog.qml
deleted file mode 100644
index 1b5e4d1d46..0000000000
--- a/plugins/Toolbox/resources/qml/dialogs/ToolboxConfirmUninstallResetDialog.qml
+++ /dev/null
@@ -1,96 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 1.1
-import QtQuick.Controls.Styles 1.1
-import QtQuick.Layouts 1.1
-import QtQuick.Dialogs 1.1
-import QtQuick.Window 2.1
-
-import UM 1.3 as UM
-import Cura 1.0 as Cura
-
-
-UM.Dialog
-{
- // This dialog asks the user to confirm he/she wants to uninstall materials/pprofiles which are currently in use
- id: base
-
- title: catalog.i18nc("@title:window", "Confirm uninstall") + toolbox.pluginToUninstall
- width: 450 * screenScaleFactor
- height: 50 * screenScaleFactor + dialogText.height + buttonBar.height
-
- maximumWidth: 450 * screenScaleFactor
- maximumHeight: 450 * screenScaleFactor
- minimumWidth: 450 * screenScaleFactor
- minimumHeight: 150 * screenScaleFactor
-
- modality: Qt.WindowModal
-
- Column
- {
- UM.I18nCatalog { id: catalog; name: "cura" }
-
- anchors
- {
- fill: parent
- leftMargin: Math.round(20 * screenScaleFactor)
- rightMargin: Math.round(20 * screenScaleFactor)
- topMargin: Math.round(10 * screenScaleFactor)
- bottomMargin: Math.round(10 * screenScaleFactor)
- }
- spacing: Math.round(15 * screenScaleFactor)
-
- Label
- {
- id: dialogText
- text:
- {
- var base_text = catalog.i18nc("@text:window", "You are uninstalling materials and/or profiles that are still in use. Confirming will reset the following materials/profiles to their defaults.")
- var materials_text = catalog.i18nc("@text:window", "Materials")
- var qualities_text = catalog.i18nc("@text:window", "Profiles")
- var machines_with_materials = toolbox.uninstallUsedMaterials
- var machines_with_qualities = toolbox.uninstallUsedQualities
- if (machines_with_materials != "")
- {
- base_text += "\n\n" + materials_text +": \n" + machines_with_materials
- }
- if (machines_with_qualities != "")
- {
- base_text += "\n\n" + qualities_text + ": \n" + machines_with_qualities
- }
- return base_text
- }
- anchors.left: parent.left
- anchors.right: parent.right
- font: UM.Theme.getFont("default")
- wrapMode: Text.WordWrap
- renderType: Text.NativeRendering
- }
-
- // Buttons
- Item {
- id: buttonBar
- anchors.right: parent.right
- anchors.left: parent.left
- height: childrenRect.height
-
- Button {
- id: cancelButton
- text: catalog.i18nc("@action:button", "Cancel")
- anchors.right: confirmButton.left
- anchors.rightMargin: UM.Theme.getSize("default_margin").width
- isDefault: true
- onClicked: toolbox.closeConfirmResetDialog()
- }
-
- Button {
- id: confirmButton
- text: catalog.i18nc("@action:button", "Confirm")
- anchors.right: parent.right
- onClicked: toolbox.resetMaterialsQualitiesAndUninstall()
- }
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/dialogs/ToolboxLicenseDialog.qml b/plugins/Toolbox/resources/qml/dialogs/ToolboxLicenseDialog.qml
deleted file mode 100644
index 9219f4ed32..0000000000
--- a/plugins/Toolbox/resources/qml/dialogs/ToolboxLicenseDialog.qml
+++ /dev/null
@@ -1,110 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Dialogs 1.1
-import QtQuick.Window 2.2
-import QtQuick.Controls 2.3
-import QtQuick.Layouts 1.3
-import QtQuick.Controls.Styles 1.4
-
-import UM 1.1 as UM
-import Cura 1.6 as Cura
-
-UM.Dialog
-{
- id: licenseDialog
- title: licenseModel.dialogTitle
- minimumWidth: UM.Theme.getSize("license_window_minimum").width
- minimumHeight: UM.Theme.getSize("license_window_minimum").height
- width: minimumWidth
- height: minimumHeight
- backgroundColor: UM.Theme.getColor("main_background")
- margin: screenScaleFactor * 10
-
- ColumnLayout
- {
- anchors.fill: parent
- spacing: UM.Theme.getSize("thick_margin").height
-
- UM.I18nCatalog{id: catalog; name: "cura"}
-
- Label
- {
- id: licenseHeader
- Layout.fillWidth: true
- text: catalog.i18nc("@label", "You need to accept the license to install the package")
- color: UM.Theme.getColor("text")
- wrapMode: Text.Wrap
- renderType: Text.NativeRendering
- }
-
- Row {
- id: packageRow
-
- Layout.fillWidth: true
- height: childrenRect.height
- spacing: UM.Theme.getSize("default_margin").width
- leftPadding: UM.Theme.getSize("narrow_margin").width
-
- Image
- {
- id: icon
- width: 30 * screenScaleFactor
- height: width
- sourceSize.width: width
- sourceSize.height: height
- fillMode: Image.PreserveAspectFit
- source: licenseModel.iconUrl || "../../images/placeholder.svg"
- mipmap: true
- }
-
- Label
- {
- id: packageName
- text: licenseModel.packageName
- color: UM.Theme.getColor("text")
- font.bold: true
- anchors.verticalCenter: icon.verticalCenter
- height: contentHeight
- wrapMode: Text.Wrap
- renderType: Text.NativeRendering
- }
-
-
- }
-
- Cura.ScrollableTextArea
- {
-
- Layout.fillWidth: true
- Layout.fillHeight: true
- anchors.topMargin: UM.Theme.getSize("default_margin").height
-
- textArea.text: licenseModel.licenseText
- textArea.readOnly: true
- }
-
- }
- rightButtons:
- [
- Cura.PrimaryButton
- {
- leftPadding: UM.Theme.getSize("dialog_primary_button_padding").width
- rightPadding: UM.Theme.getSize("dialog_primary_button_padding").width
-
- text: licenseModel.acceptButtonText
- onClicked: { handler.onLicenseAccepted() }
- }
- ]
-
- leftButtons:
- [
- Cura.SecondaryButton
- {
- id: declineButton
- text: licenseModel.declineButtonText
- onClicked: { handler.onLicenseDeclined() }
- }
- ]
-}
diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxAuthorPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxAuthorPage.qml
deleted file mode 100644
index 2fa4224388..0000000000
--- a/plugins/Toolbox/resources/qml/pages/ToolboxAuthorPage.qml
+++ /dev/null
@@ -1,176 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
-import UM 1.5 as UM
-
-import "../components"
-
-Item
-{
- id: page
- property var details: base.selection || {}
- anchors.fill: parent
- ToolboxBackColumn
- {
- id: sidebar
- }
- Item
- {
- id: header
- anchors
- {
- left: sidebar.right
- right: parent.right
- rightMargin: UM.Theme.getSize("wide_margin").width
- }
- height: UM.Theme.getSize("toolbox_detail_header").height
- Image
- {
- id: thumbnail
- width: UM.Theme.getSize("toolbox_thumbnail_medium").width
- height: UM.Theme.getSize("toolbox_thumbnail_medium").height
- fillMode: Image.PreserveAspectFit
- source: details && details.icon_url ? details.icon_url : "../../images/placeholder.svg"
- mipmap: true
- anchors
- {
- top: parent.top
- left: parent.left
- leftMargin: UM.Theme.getSize("wide_margin").width
- topMargin: UM.Theme.getSize("wide_margin").height
- }
- }
-
- Label
- {
- id: title
- anchors
- {
- top: thumbnail.top
- left: thumbnail.right
- leftMargin: UM.Theme.getSize("default_margin").width
- right: parent.right
- rightMargin: UM.Theme.getSize("wide_margin").width
- bottomMargin: UM.Theme.getSize("default_margin").height
- }
- text: details && details.name ? details.name : ""
- font: UM.Theme.getFont("large_bold")
- color: UM.Theme.getColor("text_medium")
- wrapMode: Text.WordWrap
- width: parent.width
- height: UM.Theme.getSize("toolbox_property_label").height
- renderType: Text.NativeRendering
- }
- Label
- {
- id: description
- text: details && details.description ? details.description : ""
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text_medium")
- anchors
- {
- top: title.bottom
- left: title.left
- topMargin: UM.Theme.getSize("default_margin").height
- }
- renderType: Text.NativeRendering
- }
- Column
- {
- id: properties
- anchors
- {
- top: description.bottom
- left: description.left
- topMargin: UM.Theme.getSize("default_margin").height
- }
- spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
- width: childrenRect.width
-
- Label
- {
- text: catalog.i18nc("@label", "Website") + ":"
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text_medium")
- renderType: Text.NativeRendering
- }
- Label
- {
- text: catalog.i18nc("@label", "Email") + ":"
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text_medium")
- renderType: Text.NativeRendering
- }
- }
- Column
- {
- id: values
- anchors
- {
- top: description.bottom
- left: properties.right
- leftMargin: UM.Theme.getSize("default_margin").width
- right: parent.right
- rightMargin: UM.Theme.getSize("default_margin").width
- topMargin: UM.Theme.getSize("default_margin").height
- }
- spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
-
- Label
- {
- text:
- {
- if (details && details.website)
- {
- return "<a href=\"" + details.website + "\">" + details.website + "</a>"
- }
- return ""
- }
- width: parent.width
- elide: Text.ElideRight
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text")
- linkColor: UM.Theme.getColor("text_link")
- onLinkActivated: UM.UrlUtil.openUrl(link, ["https", "http"])
- renderType: Text.NativeRendering
- }
-
- Label
- {
- text:
- {
- if (details && details.email)
- {
- return "<a href=\"mailto:" + details.email + "\">" + details.email + "</a>"
- }
- return ""
- }
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text")
- linkColor: UM.Theme.getColor("text_link")
- onLinkActivated: Qt.openUrlExternally(link)
- renderType: Text.NativeRendering
- }
- }
- Rectangle
- {
- color: UM.Theme.getColor("lining")
- width: parent.width
- height: UM.Theme.getSize("default_lining").height
- anchors.bottom: parent.bottom
- }
- }
- ToolboxDetailList
- {
- anchors
- {
- top: header.bottom
- bottom: page.bottom
- left: header.left
- right: page.right
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxDetailPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxDetailPage.qml
deleted file mode 100644
index 645b77a8c9..0000000000
--- a/plugins/Toolbox/resources/qml/pages/ToolboxDetailPage.qml
+++ /dev/null
@@ -1,188 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
-import UM 1.5 as UM
-
-import Cura 1.1 as Cura
-
-import "../components"
-
-Item
-{
- id: page
- property var details: base.selection || {}
- anchors.fill: parent
- ToolboxBackColumn
- {
- id: sidebar
- }
- Item
- {
- id: header
- anchors
- {
- left: sidebar.right
- right: parent.right
- rightMargin: UM.Theme.getSize("wide_margin").width
- }
- height: childrenRect.height + 3 * UM.Theme.getSize("default_margin").width
- Rectangle
- {
- id: thumbnail
- width: UM.Theme.getSize("toolbox_thumbnail_medium").width
- height: UM.Theme.getSize("toolbox_thumbnail_medium").height
- anchors
- {
- top: parent.top
- left: parent.left
- leftMargin: UM.Theme.getSize("wide_margin").width
- topMargin: UM.Theme.getSize("wide_margin").height
- }
- color: UM.Theme.getColor("main_background")
- Image
- {
- anchors.fill: parent
- fillMode: Image.PreserveAspectFit
- source: details === null ? "" : (details.icon_url || "../../images/placeholder.svg")
- mipmap: true
- height: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height
- width: UM.Theme.getSize("toolbox_thumbnail_large").height - 4 * UM.Theme.getSize("default_margin").height
- sourceSize.height: height
- sourceSize.width: width
- }
- }
-
- Label
- {
- id: title
- anchors
- {
- top: thumbnail.top
- left: thumbnail.right
- leftMargin: UM.Theme.getSize("default_margin").width
- }
- text: details === null ? "" : (details.name || "")
- font: UM.Theme.getFont("large_bold")
- color: UM.Theme.getColor("text")
- width: contentWidth
- height: contentHeight
- renderType: Text.NativeRendering
- }
-
- Column
- {
- id: properties
- anchors
- {
- top: title.bottom
- left: title.left
- topMargin: UM.Theme.getSize("default_margin").height
- }
- spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
- width: childrenRect.width
- height: childrenRect.height
- Label
- {
- text: catalog.i18nc("@label", "Version") + ":"
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text_medium")
- renderType: Text.NativeRendering
- }
- Label
- {
- text: catalog.i18nc("@label", "Last updated") + ":"
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text_medium")
- renderType: Text.NativeRendering
- }
- Label
- {
- text: catalog.i18nc("@label", "Brand") + ":"
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text_medium")
- renderType: Text.NativeRendering
- }
- Label
- {
- text: catalog.i18nc("@label", "Downloads") + ":"
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text_medium")
- renderType: Text.NativeRendering
- }
- }
- Column
- {
- id: values
- anchors
- {
- top: title.bottom
- left: properties.right
- leftMargin: UM.Theme.getSize("default_margin").width
- topMargin: UM.Theme.getSize("default_margin").height
- }
- spacing: Math.floor(UM.Theme.getSize("narrow_margin").height)
- height: childrenRect.height
- Label
- {
- text: details === null ? "" : (details.version || catalog.i18nc("@label", "Unknown"))
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text")
- renderType: Text.NativeRendering
- }
- Label
- {
- text:
- {
- if (details === null)
- {
- return ""
- }
- var date = new Date(details.last_updated)
- return date.toLocaleString(UM.Preferences.getValue("general/language"))
- }
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text")
- renderType: Text.NativeRendering
- }
- Label
- {
- text:
- {
- if (details === null)
- {
- return ""
- }
- else
- {
- return "<a href=\"" + details.website + "\">" + details.author_name + "</a>"
- }
- }
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text")
- linkColor: UM.Theme.getColor("text_link")
- onLinkActivated: UM.UrlUtil.openUrl(link, ["http", "https"])
- renderType: Text.NativeRendering
- }
- Label
- {
- text: details === null ? "" : (details.download_count || catalog.i18nc("@label", "Unknown"))
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text")
- renderType: Text.NativeRendering
- }
- }
- }
- ToolboxDetailList
- {
- anchors
- {
- top: header.bottom
- bottom: page.bottom
- left: header.left
- right: page.right
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxDownloadsPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxDownloadsPage.qml
deleted file mode 100644
index 9be8cbe2b9..0000000000
--- a/plugins/Toolbox/resources/qml/pages/ToolboxDownloadsPage.qml
+++ /dev/null
@@ -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
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxErrorPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxErrorPage.qml
deleted file mode 100644
index e57e63dbb9..0000000000
--- a/plugins/Toolbox/resources/qml/pages/ToolboxErrorPage.qml
+++ /dev/null
@@ -1,23 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
-
-Rectangle
-{
- id: page
- width: parent.width
- height: parent.height
- color: "transparent"
- Label
- {
- text: catalog.i18nc("@info", "Could not connect to the Cura Package database. Please check your connection.")
- anchors
- {
- centerIn: parent
- }
- renderType: Text.NativeRendering
- }
-}
diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxInstalledPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxInstalledPage.qml
deleted file mode 100644
index fa7bd24c9d..0000000000
--- a/plugins/Toolbox/resources/qml/pages/ToolboxInstalledPage.qml
+++ /dev/null
@@ -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 {}
- }
- }
- }
- }
-}
diff --git a/plugins/Toolbox/resources/qml/pages/ToolboxLoadingPage.qml b/plugins/Toolbox/resources/qml/pages/ToolboxLoadingPage.qml
deleted file mode 100644
index a30af6b335..0000000000
--- a/plugins/Toolbox/resources/qml/pages/ToolboxLoadingPage.qml
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Toolbox is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.10
-import QtQuick.Controls 1.4
-import QtQuick.Controls.Styles 1.4
-import UM 1.3 as UM
-
-Rectangle
-{
- id: page
- width: parent.width
- height: parent.height
- color: "transparent"
- Label
- {
- text: catalog.i18nc("@info", "Fetching packages...")
- color: UM.Theme.getColor("text")
- anchors
- {
- centerIn: parent
- }
- renderType: Text.NativeRendering
- }
-}
diff --git a/plugins/Toolbox/resources/qml/pages/WelcomePage.qml b/plugins/Toolbox/resources/qml/pages/WelcomePage.qml
deleted file mode 100644
index 04110cbc0f..0000000000
--- a/plugins/Toolbox/resources/qml/pages/WelcomePage.qml
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (c) 2018 Ultimaker B.V.
-// Cura is released under the terms of the LGPLv3 or higher.
-
-import QtQuick 2.7
-import QtQuick.Controls 2.1
-import QtQuick.Window 2.2
-
-import UM 1.3 as UM
-import Cura 1.1 as Cura
-
-Column
-{
- id: welcomePage
- spacing: UM.Theme.getSize("wide_margin").height
- width: parent.width
- height: childrenRect.height
- anchors.centerIn: parent
-
- Label
- {
- id: welcomeTextLabel
- text: catalog.i18nc("@description", "Please sign in to get verified plugins and materials for Ultimaker Cura Enterprise")
- width: Math.round(parent.width / 2)
- font: UM.Theme.getFont("default")
- color: UM.Theme.getColor("text")
- verticalAlignment: Text.AlignVCenter
- horizontalAlignment: Text.AlignHCenter
- anchors.horizontalCenter: parent.horizontalCenter
- wrapMode: Label.WordWrap
- renderType: Text.NativeRendering
- }
-
- Cura.PrimaryButton
- {
- id: loginButton
- width: UM.Theme.getSize("account_button").width
- height: UM.Theme.getSize("account_button").height
- anchors.horizontalCenter: parent.horizontalCenter
- text: catalog.i18nc("@button", "Sign in")
- onClicked: Cura.API.account.login()
- fixedWidthMode: true
- }
-}
-
diff --git a/plugins/Toolbox/src/AuthorsModel.py b/plugins/Toolbox/src/AuthorsModel.py
deleted file mode 100644
index 04c8ed3a40..0000000000
--- a/plugins/Toolbox/src/AuthorsModel.py
+++ /dev/null
@@ -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()
diff --git a/plugins/Toolbox/src/CloudApiModel.py b/plugins/Toolbox/src/CloudApiModel.py
deleted file mode 100644
index bef37d8173..0000000000
--- a/plugins/Toolbox/src/CloudApiModel.py
+++ /dev/null
@@ -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
- )
diff --git a/plugins/Toolbox/src/CloudSync/CloudApiClient.py b/plugins/Toolbox/src/CloudSync/CloudApiClient.py
deleted file mode 100644
index 21eb1bdbd2..0000000000
--- a/plugins/Toolbox/src/CloudSync/CloudApiClient.py
+++ /dev/null
@@ -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)
diff --git a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py b/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py
deleted file mode 100644
index 6d2ed1dcbd..0000000000
--- a/plugins/Toolbox/src/CloudSync/CloudPackageChecker.py
+++ /dev/null
@@ -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)
diff --git a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py b/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py
deleted file mode 100644
index cee2f6318a..0000000000
--- a/plugins/Toolbox/src/CloudSync/DiscrepanciesPresenter.py
+++ /dev/null
@@ -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)
diff --git a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py b/plugins/Toolbox/src/CloudSync/DownloadPresenter.py
deleted file mode 100644
index 8a5e763f3c..0000000000
--- a/plugins/Toolbox/src/CloudSync/DownloadPresenter.py
+++ /dev/null
@@ -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
diff --git a/plugins/Toolbox/src/CloudSync/LicenseModel.py b/plugins/Toolbox/src/CloudSync/LicenseModel.py
deleted file mode 100644
index 335a91ef84..0000000000
--- a/plugins/Toolbox/src/CloudSync/LicenseModel.py
+++ /dev/null
@@ -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()
diff --git a/plugins/Toolbox/src/CloudSync/LicensePresenter.py b/plugins/Toolbox/src/CloudSync/LicensePresenter.py
deleted file mode 100644
index 39ce11c8d3..0000000000
--- a/plugins/Toolbox/src/CloudSync/LicensePresenter.py
+++ /dev/null
@@ -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)
-
-
-
diff --git a/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py b/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py
deleted file mode 100644
index 8776d1782a..0000000000
--- a/plugins/Toolbox/src/CloudSync/RestartApplicationPresenter.py
+++ /dev/null
@@ -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()
diff --git a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py b/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py
deleted file mode 100644
index db16c5ea84..0000000000
--- a/plugins/Toolbox/src/CloudSync/SubscribedPackagesModel.py
+++ /dev/null
@@ -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)
diff --git a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py b/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py
deleted file mode 100644
index bb37c6d4a9..0000000000
--- a/plugins/Toolbox/src/CloudSync/SyncOrchestrator.py
+++ /dev/null
@@ -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()
diff --git a/plugins/Toolbox/src/CloudSync/__init__.py b/plugins/Toolbox/src/CloudSync/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
--- a/plugins/Toolbox/src/CloudSync/__init__.py
+++ /dev/null
diff --git a/plugins/Toolbox/src/ConfigsModel.py b/plugins/Toolbox/src/ConfigsModel.py
deleted file mode 100644
index a53817653f..0000000000
--- a/plugins/Toolbox/src/ConfigsModel.py
+++ /dev/null
@@ -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)
diff --git a/plugins/Toolbox/src/PackagesModel.py b/plugins/Toolbox/src/PackagesModel.py
deleted file mode 100644
index 97645ae466..0000000000
--- a/plugins/Toolbox/src/PackagesModel.py
+++ /dev/null
@@ -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()
diff --git a/plugins/Toolbox/src/Toolbox.py b/plugins/Toolbox/src/Toolbox.py
deleted file mode 100644
index e525a88d89..0000000000
--- a/plugins/Toolbox/src/Toolbox.py
+++ /dev/null
@@ -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)
diff --git a/plugins/Toolbox/src/__init__.py b/plugins/Toolbox/src/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
--- a/plugins/Toolbox/src/__init__.py
+++ /dev/null
diff --git a/plugins/UltimakerMachineActions/UMOUpgradeSelectionMachineAction.qml b/plugins/UltimakerMachineActions/UMOUpgradeSelectionMachineAction.qml
index 565ba2fa0e..3d7b4a054f 100644
--- a/plugins/UltimakerMachineActions/UMOUpgradeSelectionMachineAction.qml
+++ b/plugins/UltimakerMachineActions/UMOUpgradeSelectionMachineAction.qml
@@ -4,7 +4,7 @@
import QtQuick 2.10
import QtQuick.Controls 2.3
-import UM 1.3 as UM
+import UM 1.5 as UM
import Cura 1.1 as Cura
@@ -33,7 +33,7 @@ Cura.MachineAction
renderType: Text.NativeRendering
}
- Cura.CheckBox
+ UM.CheckBox
{
anchors.top: pageDescription.bottom
anchors.topMargin: UM.Theme.getSize("default_margin").height