Delete old 'Toolbox' in favour of new Marketplace.

part of CURA-8588
This commit is contained in:
Remco Burema 2021-12-30 11:51:54 +01:00
parent cf7772a40a
commit b794ad6ed2
No known key found for this signature in database
GPG key ID: 215C49431D43F98C
55 changed files with 8 additions and 4822 deletions

View file

@ -493,7 +493,7 @@ class CuraApplication(QtApplication):
"CuraEngineBackend", #Cura is useless without this one since you can't slice.
"FileLogger", #You want to be able to read the log if something goes wrong.
"XmlMaterialProfile", #Cura crashes without this one.
"Toolbox", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
"Marketplace", #This contains the interface to enable/disable plug-ins, so if you disable it you can't enable it back.
"PrepareStage", #Cura is useless without this one since you can't load models.
"PreviewStage", #This shows the list of the plugin views that are installed in Cura.
"MonitorStage", #Major part of Cura's functionality.

View file

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

View file

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

View file

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

Before

Width:  |  Height:  |  Size: 458 B

View file

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

Before

Width:  |  Height:  |  Size: 667 B

View file

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
<path d="M24,44,7,33.4V14.6L24,4,41,14.6V33.4ZM9,32.3l15,9.3,15-9.3V15.7L24,6.4,9,15.7Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 184 B

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,125 +0,0 @@
// Copyright (c) 2018 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 1.4
import QtQuick.Controls.Styles 1.4
import QtQuick.Layouts 1.3
import UM 1.1 as UM
import Cura 1.1 as Cura
Item
{
id: toolboxDownloadsGridTile
property int packageCount: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getTotalNumberOfMaterialPackagesByAuthor(model.id) : 1
property int installedPackages: (toolbox.viewCategory == "material" && model.type === undefined) ? toolbox.getNumberOfInstalledPackagesByAuthor(model.id) : (toolbox.isInstalled(model.id) ? 1 : 0)
height: childrenRect.height
Layout.alignment: Qt.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
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,104 +0,0 @@
# Copyright (c) 2018 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import re
from typing import Dict, List, Optional, Union, cast
from 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()

View file

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

View file

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

View file

@ -1,164 +0,0 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import json
from typing import List, Dict, Any, Set
from typing import Optional
from 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)

View file

@ -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)

View file

@ -1,153 +0,0 @@
# Copyright (c) 2020 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import tempfile
from typing import Dict, List, Any
from 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

View file

@ -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()

View file

@ -1,142 +0,0 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura is released under the terms of the LGPLv3 or higher.
import os
from collections import OrderedDict
from typing import Dict, Optional, List, Any
from 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)

View file

@ -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()

View file

@ -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)

View file

@ -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()

View file

@ -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)

View file

@ -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()

View file

@ -1,878 +0,0 @@
# Copyright (c) 2021 Ultimaker B.V.
# Cura 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 = self._server_response_data[request_type]
self._package_manager.setPackagesWithUpdate({p['package_id'] for p in 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)

View file

@ -526,13 +526,13 @@
}
}
},
"Toolbox": {
"Marketplace": {
"package_info": {
"package_id": "Toolbox",
"package_id": "Marketplace",
"package_type": "plugin",
"display_name": "Toolbox",
"display_name": "Marketplace",
"description": "Find, manage and install new Cura packages.",
"package_version": "1.0.1",
"package_version": "1.0.0",
"sdk_version": "7.8.0",
"website": "https://ultimaker.com",
"author": {

View file

@ -196,15 +196,7 @@ Item
}
}
// show the Toolbox
Connections
{
target: Cura.Actions.browsePackages
function onTriggered()
{
curaExtensions.callExtensionMethod("Toolbox", "launch")
}
}
// show the Marketplace
Connections
{
target: Cura.Actions.openMarketplace

View file

@ -83,53 +83,6 @@ Item
ExclusiveGroup { id: mainWindowHeaderMenuGroup }
}
// Shortcut button to quick access the Toolbox
Controls2.Button //TODO: Remove once new Marketplace is completed.
{
text: "Old Marketplace"
height: Math.round(0.5 * UM.Theme.getSize("main_window_header").height)
onClicked: Cura.Actions.browsePackages.trigger()
hoverEnabled: true
background: Rectangle
{
id: marketplaceButtonBorder
radius: UM.Theme.getSize("action_button_radius").width
color: UM.Theme.getColor("main_window_header_background")
border.width: UM.Theme.getSize("default_lining").width
border.color: UM.Theme.getColor("primary_text")
Rectangle
{
id: marketplaceButtonFill
anchors.fill: parent
radius: parent.radius
color: UM.Theme.getColor("primary_text")
opacity: parent.hovered ? 0.2 : 0
Behavior on opacity { NumberAnimation { duration: 100 } }
}
}
contentItem: Label
{
id: label
text: parent.text
font: UM.Theme.getFont("default")
color: UM.Theme.getColor("primary_text")
width: contentWidth
verticalAlignment: Text.AlignVCenter
renderType: Text.NativeRendering
}
anchors
{
right: marketplaceButton.left
rightMargin: UM.Theme.getSize("default_margin").width
verticalCenter: parent.verticalCenter
}
}
Controls2.Button
{
id: marketplaceButton

View file

@ -1,5 +1,5 @@
// Copyright (c) 2020 Ultimaker B.V.
// Toolbox is released under the terms of the LGPLv3 or higher.
// Cura is released under the terms of the LGPLv3 or higher.
import QtQuick 2.10
import QtQuick.Controls 2.3

View file

@ -8,7 +8,7 @@ Run this file with the Cura project root as the working directory
Checks for invalid imports. When importing from plugins, there will be no problems when running from source,
but for some build types the plugins dir is not on the path, so relative imports should be used instead. eg:
from ..UltimakerCloudScope import UltimakerCloudScope <-- OK
import plugins.Toolbox.src ... <-- NOT OK
import plugins.Marketplace.src ... <-- NOT OK
"""