Make Content Library Materials downloadable

Task-number: QDS-9267
Change-Id: Ib4da1871cd1d9f0bf52323793b7d8d1b028ae170
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
This commit is contained in:
Samuel Ghinet
2023-03-02 14:50:06 +02:00
parent 46b0aaeebe
commit 98be6d289f
20 changed files with 698 additions and 92 deletions

View File

@@ -10,25 +10,34 @@ import QtQuick.Controls
import StudioTheme 1.0 as StudioTheme import StudioTheme 1.0 as StudioTheme
import ContentLibraryBackend import ContentLibraryBackend
import WebFetcher 1.0
Item { Item {
id: root id: root
signal showContextMenu() signal showContextMenu()
// Download states: "" (ie default, not downloaded), "unavailable", "downloading", "downloaded",
// "failed"
property string downloadState: modelData.isDownloaded() ? "downloaded" : ""
visible: modelData.bundleMaterialVisible visible: modelData.bundleMaterialVisible
MouseArea { MouseArea {
id: mouseArea id: mouseArea
enabled: root.downloadState !== "downloading"
hoverEnabled: true hoverEnabled: true
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton acceptedButtons: Qt.LeftButton | Qt.RightButton
onPressed: (mouse) => { onPressed: (mouse) => {
if (mouse.button === Qt.LeftButton && !materialsModel.importerRunning) if (mouse.button === Qt.LeftButton && !materialsModel.importerRunning) {
ContentLibraryBackend.rootView.startDragMaterial(modelData, mapToGlobal(mouse.x, mouse.y)) if (root.downloadState === "downloaded")
else if (mouse.button === Qt.RightButton) ContentLibraryBackend.rootView.startDragMaterial(modelData, mapToGlobal(mouse.x, mouse.y))
} else if (mouse.button === Qt.RightButton && root.downloadState === "downloaded") {
root.showContextMenu() root.showContextMenu()
}
} }
} }
@@ -38,6 +47,15 @@ Item {
Item { width: 1; height: 5 } // spacer Item { width: 1; height: 5 } // spacer
DownloadPane {
id: downloadPane
width: root.width - 10
height: img.width
visible: root.downloadState === "downloading"
onRequestCancel: downloader.cancel()
}
Image { Image {
id: img id: img
@@ -46,6 +64,7 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
source: modelData.bundleMaterialIcon source: modelData.bundleMaterialIcon
cache: false cache: false
visible: root.downloadState != "downloading"
Rectangle { // circular indicator for imported bundle materials Rectangle { // circular indicator for imported bundle materials
width: 10 width: 10
@@ -83,13 +102,52 @@ Item {
anchors.right: img.right anchors.right: img.right
anchors.bottom: img.bottom anchors.bottom: img.bottom
enabled: !ContentLibraryBackend.materialsModel.importerRunning enabled: !ContentLibraryBackend.materialsModel.importerRunning
visible: containsMouse || mouseArea.containsMouse visible: root.downloadState === "downloaded"
&& (containsMouse || mouseArea.containsMouse)
onClicked: { onClicked: {
ContentLibraryBackend.materialsModel.addToProject(modelData) ContentLibraryBackend.materialsModel.addToProject(modelData)
} }
} // IconButton
Text { // download icon
color: root.downloadState === "unavailable" || root.downloadState === "failed"
? StudioTheme.Values.themeRedLight
: StudioTheme.Values.themeTextColor
font.family: StudioTheme.Constants.iconFont.family
text: root.downloadState === "unavailable"
? StudioTheme.Constants.downloadUnavailable
: StudioTheme.Constants.download
font.pixelSize: 22
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
anchors.bottomMargin: 0
anchors.right: parent.right
anchors.bottom: parent.bottom
style: Text.Outline
styleColor: "black"
visible: root.downloadState !== "downloaded"
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
onClicked: (mouse) => {
if (root.downloadState !== "" && root.downloadState !== "failed")
return
downloadPane.beginDownload(Qt.binding(function() { return downloader.progress }))
root.downloadState = ""
downloader.start()
}
}
} }
} } // Image
TextInput { TextInput {
id: matName id: matName
@@ -110,5 +168,44 @@ Item {
selectionColor: StudioTheme.Values.themeTextSelectionColor selectionColor: StudioTheme.Values.themeTextSelectionColor
selectedTextColor: StudioTheme.Values.themeTextSelectedTextColor selectedTextColor: StudioTheme.Values.themeTextSelectedTextColor
} }
} } // Column
MultiFileDownloader {
id: downloader
baseUrl: modelData.bundleMaterialBaseWebUrl
files: modelData.bundleMaterialFiles
targetDirPath: modelData.bundleMaterialParentPath
onDownloadStarting: {
root.downloadState = "downloading"
}
onFinishedChanged: {
downloadPane.endDownload()
root.downloadState = "downloaded"
}
onDownloadCanceled: {
downloadPane.endDownload()
root.downloadState = ""
}
onDownloadFailed: {
downloadPane.endDownload()
root.downloadState = "failed"
}
downloader: FileDownloader {
id: fileDownloader
url: downloader.nextUrl
probeUrl: false
downloadEnabled: true
targetFilePath: downloader.nextTargetPath
} // FileDownloader
} // MultiFileDownloader
} }

View File

@@ -232,7 +232,7 @@ Item {
FileExtractor { FileExtractor {
id: extractor id: extractor
archiveName: downloader.completeBaseName archiveName: downloader.completeBaseName
sourceFile: downloader.tempFile sourceFile: downloader.outputFile
targetPath: modelData.textureParentPath targetPath: modelData.textureParentPath
alwaysCreateDir: false alwaysCreateDir: false
clearTargetPathContents: false clearTargetPathContents: false

View File

@@ -0,0 +1,81 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
import QtQuick
import StudioTheme 1.0 as StudioTheme
Rectangle {
id: root
color: StudioTheme.Values.themeThumbnailBackground
border.color: "#00000000"
signal requestCancel
property alias allowCancel: progressBar.closeButtonVisible
property alias progressValue: progressBar.value
property alias progressLabel: progressLabel.text
function beginDownload(progressFunction)
{
progressBar.visible = true
root.progressLabel = qsTr("Downloading...")
root.allowCancel = true
root.progressValue = progressFunction
}
function endDownload()
{
root.allowCancel = false
root.progressLabel = ""
root.progressValue = 0
}
TextureProgressBar {
id: progressBar
anchors.rightMargin: 10
anchors.leftMargin: 10
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: false
onCancelRequested: {
root.requestCancel()
}
Text {
id: progressLabel
color: StudioTheme.Values.themeTextColor
text: qsTr("Progress:")
anchors.bottom: parent.top
anchors.bottomMargin: 5
anchors.left: parent.left
font.pixelSize: 12
}
Row {
anchors.top: parent.bottom
anchors.topMargin: 5
anchors.horizontalCenter: parent.horizontalCenter
Text {
id: progressAmount
color: StudioTheme.Values.themeTextColor
text: progressBar.value.toFixed(1)
font.pixelSize: 12
}
Text {
id: percentSign
color: StudioTheme.Values.themeTextColor
text: qsTr("%")
font.pixelSize: 12
}
}
} // TextureProgressBar
} // Rectangle

View File

@@ -22,6 +22,7 @@ add_qtc_library(QmlDesignerUtils STATIC
SOURCES SOURCES
asset.cpp asset.h asset.cpp asset.h
filedownloader.cpp filedownloader.h filedownloader.cpp filedownloader.h
multifiledownloader.cpp multifiledownloader.h
fileextractor.cpp fileextractor.h fileextractor.cpp fileextractor.h
hdrimage.cpp hdrimage.h hdrimage.cpp hdrimage.h
ktximage.cpp ktximage.h ktximage.cpp ktximage.h

View File

@@ -3,6 +3,8 @@
#include "contentlibrarymaterial.h" #include "contentlibrarymaterial.h"
#include <QFileInfo>
namespace QmlDesigner { namespace QmlDesigner {
ContentLibraryMaterial::ContentLibraryMaterial(QObject *parent, ContentLibraryMaterial::ContentLibraryMaterial(QObject *parent,
@@ -10,8 +12,15 @@ ContentLibraryMaterial::ContentLibraryMaterial(QObject *parent,
const QString &qml, const QString &qml,
const TypeName &type, const TypeName &type,
const QUrl &icon, const QUrl &icon,
const QStringList &files) const QStringList &files,
: QObject(parent), m_name(name), m_qml(qml), m_type(type), m_icon(icon), m_files(files) {} const QString &downloadPath,
const QString &baseWebUrl)
: QObject(parent), m_name(name), m_qml(qml), m_type(type), m_icon(icon), m_files(files)
, m_downloadPath(downloadPath), m_baseWebUrl(baseWebUrl)
{
m_allFiles = m_files;
m_allFiles.push_back(m_qml);
}
bool ContentLibraryMaterial::filter(const QString &searchText) bool ContentLibraryMaterial::filter(const QString &searchText)
{ {
@@ -64,4 +73,25 @@ bool ContentLibraryMaterial::imported() const
return m_imported; return m_imported;
} }
bool ContentLibraryMaterial::isDownloaded() const
{
QString fullPath = qmlFilePath();
return QFileInfo(fullPath).isFile();
}
QString ContentLibraryMaterial::qmlFilePath() const
{
return m_downloadPath + "/" + m_qml;
}
QString ContentLibraryMaterial::parentDirPath() const
{
return m_downloadPath;
}
QStringList ContentLibraryMaterial::allFiles() const
{
return m_allFiles;
}
} // namespace QmlDesigner } // namespace QmlDesigner

View File

@@ -19,6 +19,9 @@ class ContentLibraryMaterial : public QObject
Q_PROPERTY(QUrl bundleMaterialIcon MEMBER m_icon CONSTANT) Q_PROPERTY(QUrl bundleMaterialIcon MEMBER m_icon CONSTANT)
Q_PROPERTY(bool bundleMaterialVisible MEMBER m_visible NOTIFY materialVisibleChanged) Q_PROPERTY(bool bundleMaterialVisible MEMBER m_visible NOTIFY materialVisibleChanged)
Q_PROPERTY(bool bundleMaterialImported READ imported WRITE setImported NOTIFY materialImportedChanged) Q_PROPERTY(bool bundleMaterialImported READ imported WRITE setImported NOTIFY materialImportedChanged)
Q_PROPERTY(QString bundleMaterialBaseWebUrl MEMBER m_baseWebUrl CONSTANT)
Q_PROPERTY(QString bundleMaterialParentPath READ parentDirPath CONSTANT)
Q_PROPERTY(QStringList bundleMaterialFiles READ allFiles CONSTANT)
public: public:
ContentLibraryMaterial(QObject *parent, ContentLibraryMaterial(QObject *parent,
@@ -26,18 +29,25 @@ public:
const QString &qml, const QString &qml,
const TypeName &type, const TypeName &type,
const QUrl &icon, const QUrl &icon,
const QStringList &files); const QStringList &files,
const QString &downloadPath,
const QString &baseWebUrl);
bool filter(const QString &searchText); bool filter(const QString &searchText);
Q_INVOKABLE bool isDownloaded() const;
QUrl icon() const; QUrl icon() const;
QString qml() const; QString qml() const;
TypeName type() const; TypeName type() const;
QStringList files() const; QStringList files() const;
bool visible() const; bool visible() const;
QString qmlFilePath() const;
bool setImported(bool imported); bool setImported(bool imported);
bool imported() const; bool imported() const;
QString parentDirPath() const;
QStringList allFiles() const;
signals: signals:
void materialVisibleChanged(); void materialVisibleChanged();
@@ -52,6 +62,10 @@ private:
bool m_visible = true; bool m_visible = true;
bool m_imported = false; bool m_imported = false;
QString m_downloadPath;
QString m_baseWebUrl;
QStringList m_allFiles;
}; };
} // namespace QmlDesigner } // namespace QmlDesigner

View File

@@ -7,7 +7,11 @@
#include "contentlibrarymaterial.h" #include "contentlibrarymaterial.h"
#include "contentlibrarymaterialscategory.h" #include "contentlibrarymaterialscategory.h"
#include "contentlibrarywidget.h" #include "contentlibrarywidget.h"
#include "filedownloader.h"
#include "fileextractor.h"
#include "multifiledownloader.h"
#include "qmldesignerconstants.h" #include "qmldesignerconstants.h"
#include "qmldesignerplugin.h"
#include <utils/algorithm.h> #include <utils/algorithm.h>
#include <utils/hostosinfo.h> #include <utils/hostosinfo.h>
@@ -16,6 +20,8 @@
#include <QCoreApplication> #include <QCoreApplication>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonDocument> #include <QJsonDocument>
#include <QQmlEngine>
#include <QStandardPaths>
#include <QUrl> #include <QUrl>
namespace QmlDesigner { namespace QmlDesigner {
@@ -24,7 +30,19 @@ ContentLibraryMaterialsModel::ContentLibraryMaterialsModel(ContentLibraryWidget
: QAbstractListModel(parent) : QAbstractListModel(parent)
, m_widget(parent) , m_widget(parent)
{ {
loadMaterialBundle(); m_downloadPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)
+ "/QtDesignStudio/bundles/Materials";
m_baseUrl = QmlDesignerPlugin::settings()
.value(DesignerSettingsKey::DOWNLOADABLE_BUNDLES_URL)
.toString() + "/materials/v1";
qmlRegisterType<QmlDesigner::FileDownloader>("WebFetcher", 1, 0, "FileDownloader");
qmlRegisterType<QmlDesigner::MultiFileDownloader>("WebFetcher", 1, 0, "MultiFileDownloader");
QDir bundleDir{m_downloadPath};
if (fetchBundleMetadata(bundleDir) && fetchBundleIcons(bundleDir))
loadMaterialBundle(bundleDir);
} }
int ContentLibraryMaterialsModel::rowCount(const QModelIndex &) const int ContentLibraryMaterialsModel::rowCount(const QModelIndex &) const
@@ -91,30 +109,127 @@ QHash<int, QByteArray> ContentLibraryMaterialsModel::roleNames() const
return roles; return roles;
} }
void ContentLibraryMaterialsModel::loadMaterialBundle() bool ContentLibraryMaterialsModel::fetchBundleIcons(const QDir &bundleDir)
{ {
if (m_matBundleExists || m_probeMatBundleDir) QString iconsPath = bundleDir.filePath("icons");
QDir iconsDir(iconsPath);
if (iconsDir.exists() && iconsDir.entryList().length() > 0)
return true;
QString zipFileUrl = m_baseUrl + "/icons.zip";
FileDownloader *downloader = new FileDownloader(this);
downloader->setUrl(zipFileUrl);
downloader->setProbeUrl(false);
downloader->setDownloadEnabled(true);
QObject::connect(downloader, &FileDownloader::finishedChanged, this, [=]() {
FileExtractor *extractor = new FileExtractor(this);
extractor->setArchiveName(downloader->completeBaseName());
extractor->setSourceFile(downloader->outputFile());
extractor->setTargetPath(bundleDir.absolutePath());
extractor->setAlwaysCreateDir(false);
extractor->setClearTargetPathContents(false);
QObject::connect(extractor, &FileExtractor::finishedChanged, this, [=]() {
downloader->deleteLater();
extractor->deleteLater();
loadMaterialBundle(bundleDir);
});
extractor->extract();
});
downloader->start();
return false;
}
bool ContentLibraryMaterialsModel::fetchBundleMetadata(const QDir &bundleDir)
{
QString matBundlePath = bundleDir.filePath("material_bundle.json");
QFileInfo fi(matBundlePath);
if (fi.exists() && fi.size() > 0)
return true;
QString metaFileUrl = m_baseUrl + "/material_bundle.json";
FileDownloader *downloader = new FileDownloader(this);
downloader->setUrl(metaFileUrl);
downloader->setProbeUrl(false);
downloader->setDownloadEnabled(true);
downloader->setTargetFilePath(matBundlePath);
QObject::connect(downloader, &FileDownloader::finishedChanged, this, [=]() {
if (fetchBundleIcons(bundleDir))
loadMaterialBundle(bundleDir);
downloader->deleteLater();
});
downloader->start();
return false;
}
void ContentLibraryMaterialsModel::downloadSharedFiles(const QDir &targetDir, const QStringList &files)
{
QString metaFileUrl = m_baseUrl + "/shared_files.zip";
FileDownloader *downloader = new FileDownloader(this);
downloader->setUrl(metaFileUrl);
downloader->setProbeUrl(false);
downloader->setDownloadEnabled(true);
QObject::connect(downloader, &FileDownloader::finishedChanged, this, [=]() {
FileExtractor *extractor = new FileExtractor(this);
extractor->setArchiveName(downloader->completeBaseName());
extractor->setSourceFile(downloader->outputFile());
extractor->setTargetPath(targetDir.absolutePath());
extractor->setAlwaysCreateDir(false);
extractor->setClearTargetPathContents(false);
QObject::connect(extractor, &FileExtractor::finishedChanged, this, [this, downloader, extractor]() {
downloader->deleteLater();
extractor->deleteLater();
createImporter(m_importerBundlePath, m_importerBundleId, m_importerSharedFiles);
});
extractor->extract();
});
downloader->start();
}
void ContentLibraryMaterialsModel::createImporter(const QString &bundlePath, const QString &bundleId,
const QStringList &sharedFiles)
{
m_importer = new Internal::ContentLibraryBundleImporter(bundlePath, bundleId, sharedFiles);
connect(m_importer, &Internal::ContentLibraryBundleImporter::importFinished, this,
[&](const QmlDesigner::NodeMetaInfo &metaInfo) {
m_importerRunning = false;
emit importerRunningChanged();
if (metaInfo.isValid())
emit bundleMaterialImported(metaInfo);
});
connect(m_importer, &Internal::ContentLibraryBundleImporter::unimportFinished, this,
[&](const QmlDesigner::NodeMetaInfo &metaInfo) {
Q_UNUSED(metaInfo)
m_importerRunning = false;
emit importerRunningChanged();
emit bundleMaterialUnimported(metaInfo);
});
resetModel();
updateIsEmpty();
}
void ContentLibraryMaterialsModel::loadMaterialBundle(const QDir &matBundleDir)
{
if (m_matBundleExists)
return; return;
QDir matBundleDir;
if (!qEnvironmentVariable("MATERIAL_BUNDLE_PATH").isEmpty())
matBundleDir.setPath(qEnvironmentVariable("MATERIAL_BUNDLE_PATH"));
else if (Utils::HostOsInfo::isMacHost())
matBundleDir.setPath(QCoreApplication::applicationDirPath() + "/../Resources/material_bundle");
// search for matBundleDir from exec dir and up
if (matBundleDir.dirName() == ".") {
m_probeMatBundleDir = true; // probe only once
matBundleDir.setPath(QCoreApplication::applicationDirPath());
while (!matBundleDir.cd("material_bundle") && matBundleDir.cdUp())
; // do nothing
if (matBundleDir.dirName() != "material_bundle") // bundlePathDir not found
return;
}
QString matBundlePath = matBundleDir.filePath("material_bundle.json"); QString matBundlePath = matBundleDir.filePath("material_bundle.json");
if (m_matBundleObj.isEmpty()) { if (m_matBundleObj.isEmpty()) {
@@ -160,7 +275,8 @@ void ContentLibraryMaterialsModel::loadMaterialBundle()
bundleId, bundleId,
qml.chopped(4)).toLatin1(); // chopped(4): remove .qml qml.chopped(4)).toLatin1(); // chopped(4): remove .qml
auto bundleMat = new ContentLibraryMaterial(category, mat, qml, type, icon, files); auto bundleMat = new ContentLibraryMaterial(category, mat, qml, type, icon, files,
m_downloadPath, m_baseUrl);
category->addBundleMaterial(bundleMat); category->addBundleMaterial(bundleMat);
} }
@@ -172,24 +288,22 @@ void ContentLibraryMaterialsModel::loadMaterialBundle()
for (const auto /*QJson{Const,}ValueRef*/ &file : sharedFilesArr) for (const auto /*QJson{Const,}ValueRef*/ &file : sharedFilesArr)
sharedFiles.append(file.toString()); sharedFiles.append(file.toString());
m_importer = new Internal::ContentLibraryBundleImporter(matBundleDir.path(), bundleId, sharedFiles); QStringList missingSharedFiles;
connect(m_importer, &Internal::ContentLibraryBundleImporter::importFinished, this, for (const QString &s : std::as_const(sharedFiles)) {
[&](const QmlDesigner::NodeMetaInfo &metaInfo) { const QString fullSharedFilePath = matBundleDir.filePath(s);
m_importerRunning = false;
emit importerRunningChanged();
if (metaInfo.isValid())
emit bundleMaterialImported(metaInfo);
});
connect(m_importer, &Internal::ContentLibraryBundleImporter::unimportFinished, this, if (!QFile::exists(fullSharedFilePath))
[&](const QmlDesigner::NodeMetaInfo &metaInfo) { missingSharedFiles.push_back(s);
Q_UNUSED(metaInfo) }
m_importerRunning = false;
emit importerRunningChanged();
emit bundleMaterialUnimported(metaInfo);
});
updateIsEmpty(); if (missingSharedFiles.length() > 0) {
m_importerBundlePath = matBundleDir.path();
m_importerBundleId = bundleId;
m_importerSharedFiles = sharedFiles;
downloadSharedFiles(matBundleDir, missingSharedFiles);
} else {
createImporter(matBundleDir.path(), bundleId, sharedFiles);
}
} }
bool ContentLibraryMaterialsModel::hasRequiredQuick3DImport() const bool ContentLibraryMaterialsModel::hasRequiredQuick3DImport() const

View File

@@ -6,6 +6,7 @@
#include "nodemetainfo.h" #include "nodemetainfo.h"
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QDir>
#include <QJsonObject> #include <QJsonObject>
namespace QmlDesigner { namespace QmlDesigner {
@@ -70,8 +71,13 @@ signals:
void matBundleExistsChanged(); void matBundleExistsChanged();
private: private:
void loadMaterialBundle(); void loadMaterialBundle(const QDir &matBundleDir);
bool fetchBundleIcons(const QDir &bundleDir);
bool fetchBundleMetadata(const QDir &bundleDir);
bool isValidIndex(int idx) const; bool isValidIndex(int idx) const;
void downloadSharedFiles(const QDir &targetDir, const QStringList &files);
void createImporter(const QString &bundlePath, const QString &bundleId,
const QStringList &sharedFiles);
ContentLibraryWidget *m_widget = nullptr; ContentLibraryWidget *m_widget = nullptr;
QString m_searchText; QString m_searchText;
@@ -82,11 +88,17 @@ private:
bool m_isEmpty = true; bool m_isEmpty = true;
bool m_matBundleExists = false; bool m_matBundleExists = false;
bool m_hasModelSelection = false; bool m_hasModelSelection = false;
bool m_probeMatBundleDir = false;
bool m_importerRunning = false; bool m_importerRunning = false;
int m_quick3dMajorVersion = -1; int m_quick3dMajorVersion = -1;
int m_quick3dMinorVersion = -1; int m_quick3dMinorVersion = -1;
QString m_downloadPath;
QString m_baseUrl;
QString m_importerBundlePath;
QString m_importerBundleId;
QStringList m_importerSharedFiles;
}; };
} // namespace QmlDesigner } // namespace QmlDesigner

View File

@@ -116,7 +116,7 @@ void ContentLibraryTexturesModel::loadTextureBundle(const QString &bundlePath, c
QString remoteBaseUrl = QmlDesignerPlugin::settings() QString remoteBaseUrl = QmlDesignerPlugin::settings()
.value(DesignerSettingsKey::DOWNLOADABLE_BUNDLES_URL).toString() .value(DesignerSettingsKey::DOWNLOADABLE_BUNDLES_URL).toString()
+ '/' + m_category; + "/textures/" + m_category;
const QFileInfoList dirs = bundleDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); const QFileInfoList dirs = bundleDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QFileInfo &dir : dirs) { for (const QFileInfo &dir : dirs) {

View File

@@ -54,7 +54,8 @@ bool ContentLibraryWidget::eventFilter(QObject *obj, QEvent *event)
if (m_materialToDrag) { if (m_materialToDrag) {
QMouseEvent *me = static_cast<QMouseEvent *>(event); QMouseEvent *me = static_cast<QMouseEvent *>(event);
if ((me->globalPos() - m_dragStartPoint).manhattanLength() > 20) { if ((me->globalPos() - m_dragStartPoint).manhattanLength() > 20
&& m_materialToDrag->isDownloaded()) {
QByteArray data; QByteArray data;
QMimeData *mimeData = new QMimeData; QMimeData *mimeData = new QMimeData;
QDataStream stream(&data, QIODevice::WriteOnly); QDataStream stream(&data, QIODevice::WriteOnly);

View File

@@ -4,6 +4,7 @@
#include <private/qqmldata_p.h> #include <private/qqmldata_p.h>
#include <utils/networkaccessmanager.h> #include <utils/networkaccessmanager.h>
#include <utils/filepath.h>
#include <QDir> #include <QDir>
#include <QQmlEngine> #include <QQmlEngine>
@@ -12,33 +13,52 @@ namespace QmlDesigner {
FileDownloader::FileDownloader(QObject *parent) FileDownloader::FileDownloader(QObject *parent)
: QObject(parent) : QObject(parent)
{} {
QObject::connect(this, &FileDownloader::downloadFailed, this, [this]() {
if (m_outputFile.exists())
m_outputFile.remove();
});
QObject::connect(this, &FileDownloader::downloadCanceled, this, [this]() {
if (m_outputFile.exists())
m_outputFile.remove();
});
}
FileDownloader::~FileDownloader() FileDownloader::~FileDownloader()
{ {
if (m_tempFile.exists()) // Delete the temp file only if a target Path was set (i.e. file will be moved)
m_tempFile.remove(); if (deleteFileAtTheEnd() && m_outputFile.exists())
m_outputFile.remove();
}
bool FileDownloader::deleteFileAtTheEnd() const
{
return m_targetFilePath.isEmpty();
} }
void FileDownloader::start() void FileDownloader::start()
{ {
emit downloadStarting(); emit downloadStarting();
m_tempFile.setFileName(QDir::tempPath() + "/" + name() + ".XXXXXX" + ".zip"); QString tempFileName = QDir::tempPath() + "/.qds_download_" + url().fileName();
m_tempFile.open(QIODevice::WriteOnly);
m_outputFile.setFileName(tempFileName);
m_outputFile.open(QIODevice::WriteOnly);
auto request = QNetworkRequest(m_url); auto request = QNetworkRequest(m_url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::UserVerifiedRedirectPolicy); QNetworkRequest::UserVerifiedRedirectPolicy);
m_reply = Utils::NetworkAccessManager::instance()->get(request); QNetworkReply *reply = Utils::NetworkAccessManager::instance()->get(request);
m_reply = reply;
QNetworkReply::connect(m_reply, &QNetworkReply::readyRead, this, [this]() { QNetworkReply::connect(reply, &QNetworkReply::readyRead, this, [this, reply]() {
bool isDownloadingFile = false; bool isDownloadingFile = false;
QString contentType; QString contentType;
if (!m_reply->hasRawHeader("Content-Type")) { if (!reply->hasRawHeader("Content-Type")) {
isDownloadingFile = true; isDownloadingFile = true;
} else { } else {
contentType = QString::fromUtf8(m_reply->rawHeader("Content-Type")); contentType = QString::fromUtf8(reply->rawHeader("Content-Type"));
if (contentType.startsWith("application/") if (contentType.startsWith("application/")
|| contentType.startsWith("image/") || contentType.startsWith("image/")
@@ -50,12 +70,12 @@ void FileDownloader::start()
} }
if (isDownloadingFile) if (isDownloadingFile)
m_tempFile.write(m_reply->readAll()); m_outputFile.write(reply->readAll());
else else
m_reply->close(); reply->close();
}); });
QNetworkReply::connect(m_reply, QNetworkReply::connect(reply,
&QNetworkReply::downloadProgress, &QNetworkReply::downloadProgress,
this, this,
[this](qint64 current, qint64 max) { [this](qint64 current, qint64 max) {
@@ -66,33 +86,44 @@ void FileDownloader::start()
} }
m_progress = current * 100 / max; m_progress = current * 100 / max;
emit progressChanged(); emit progressChanged();
}); });
QNetworkReply::connect(m_reply, &QNetworkReply::redirected, [this](const QUrl &) { QNetworkReply::connect(reply, &QNetworkReply::redirected, [reply](const QUrl &) {
emit m_reply->redirectAllowed(); emit reply->redirectAllowed();
}); });
QNetworkReply::connect(m_reply, &QNetworkReply::finished, this, [this]() { QNetworkReply::connect(reply, &QNetworkReply::finished, this, [this, reply]() {
if (m_reply->error()) { if (reply->error()) {
if (m_tempFile.exists()) if (reply->error() != QNetworkReply::OperationCanceledError) {
m_tempFile.remove(); qWarning() << Q_FUNC_INFO << m_url << reply->errorString();
if (m_reply->error() != QNetworkReply::OperationCanceledError) {
qWarning() << Q_FUNC_INFO << m_url << m_reply->errorString();
emit downloadFailed(); emit downloadFailed();
} else { } else {
emit downloadCanceled(); emit downloadCanceled();
} }
} else { } else {
m_tempFile.flush(); m_outputFile.flush();
m_tempFile.close(); m_outputFile.close();
QString dirPath = QFileInfo(m_targetFilePath).dir().absolutePath();
if (!deleteFileAtTheEnd()) {
if (!QDir{}.mkpath(dirPath)) {
emit downloadFailed();
return;
}
if (!QFileInfo().exists(m_targetFilePath) && !m_outputFile.rename(m_targetFilePath)) {
emit downloadFailed();
return;
}
}
m_finished = true; m_finished = true;
emit tempFileChanged(); emit outputFileChanged();
emit finishedChanged(); emit finishedChanged();
} }
reply->deleteLater();
m_reply = nullptr; m_reply = nullptr;
}); });
} }
@@ -176,9 +207,9 @@ int FileDownloader::progress() const
return m_progress; return m_progress;
} }
QString FileDownloader::tempFile() const QString FileDownloader::outputFile() const
{ {
return QFileInfo(m_tempFile).canonicalFilePath(); return QFileInfo(m_outputFile).canonicalFilePath();
} }
QDateTime FileDownloader::lastModified() const QDateTime FileDownloader::lastModified() const
@@ -242,4 +273,14 @@ void FileDownloader::doProbeUrl()
}); });
} }
void FileDownloader::setTargetFilePath(const QString &path)
{
m_targetFilePath = path;
}
QString FileDownloader::targetFilePath() const
{
return m_targetFilePath;
}
} // namespace QmlDesigner } // namespace QmlDesigner

View File

@@ -15,13 +15,14 @@ class FileDownloader : public QObject
Q_PROPERTY(bool downloadEnabled WRITE setDownloadEnabled READ downloadEnabled NOTIFY downloadEnabledChanged) Q_PROPERTY(bool downloadEnabled WRITE setDownloadEnabled READ downloadEnabled NOTIFY downloadEnabledChanged)
Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged) Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged)
Q_PROPERTY(QString targetFilePath READ targetFilePath WRITE setTargetFilePath NOTIFY targetFilePathChanged)
Q_PROPERTY(bool probeUrl READ probeUrl WRITE setProbeUrl NOTIFY probeUrlChanged) Q_PROPERTY(bool probeUrl READ probeUrl WRITE setProbeUrl NOTIFY probeUrlChanged)
Q_PROPERTY(bool finished READ finished NOTIFY finishedChanged) Q_PROPERTY(bool finished READ finished NOTIFY finishedChanged)
Q_PROPERTY(bool error READ error NOTIFY errorChanged) Q_PROPERTY(bool error READ error NOTIFY errorChanged)
Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged)
Q_PROPERTY(QString completeBaseName READ completeBaseName NOTIFY nameChanged) Q_PROPERTY(QString completeBaseName READ completeBaseName NOTIFY nameChanged)
Q_PROPERTY(int progress READ progress NOTIFY progressChanged) Q_PROPERTY(int progress READ progress NOTIFY progressChanged)
Q_PROPERTY(QString tempFile READ tempFile NOTIFY tempFileChanged) Q_PROPERTY(QString outputFile READ outputFile NOTIFY outputFileChanged)
Q_PROPERTY(QDateTime lastModified READ lastModified NOTIFY lastModifiedChanged) Q_PROPERTY(QDateTime lastModified READ lastModified NOTIFY lastModifiedChanged)
Q_PROPERTY(bool available READ available NOTIFY availableChanged) Q_PROPERTY(bool available READ available NOTIFY availableChanged)
@@ -32,12 +33,14 @@ public:
void setUrl(const QUrl &url); void setUrl(const QUrl &url);
QUrl url() const; QUrl url() const;
void setTargetFilePath(const QString &path);
QString targetFilePath() const;
bool finished() const; bool finished() const;
bool error() const; bool error() const;
QString name() const; QString name() const;
QString completeBaseName() const; QString completeBaseName() const;
int progress() const; int progress() const;
QString tempFile() const; QString outputFile() const;
QDateTime lastModified() const; QDateTime lastModified() const;
bool available() const; bool available() const;
void setDownloadEnabled(bool value); void setDownloadEnabled(bool value);
@@ -55,7 +58,7 @@ signals:
void nameChanged(); void nameChanged();
void urlChanged(); void urlChanged();
void progressChanged(); void progressChanged();
void tempFileChanged(); void outputFileChanged();
void downloadFailed(); void downloadFailed();
void lastModifiedChanged(); void lastModifiedChanged();
void availableChanged(); void availableChanged();
@@ -64,21 +67,24 @@ signals:
void downloadStarting(); void downloadStarting();
void downloadCanceled(); void downloadCanceled();
void probeUrlChanged(); void probeUrlChanged();
void targetFilePathChanged();
private: private:
void doProbeUrl(); void doProbeUrl();
bool deleteFileAtTheEnd() const;
QUrl m_url; QUrl m_url;
bool m_probeUrl = false; bool m_probeUrl = false;
bool m_finished = false; bool m_finished = false;
bool m_error = false; bool m_error = false;
int m_progress = 0; int m_progress = 0;
QFile m_tempFile; QFile m_outputFile;
QDateTime m_lastModified; QDateTime m_lastModified;
bool m_available = false; bool m_available = false;
QNetworkReply *m_reply = nullptr; QNetworkReply *m_reply = nullptr;
bool m_downloadEnabled = false; bool m_downloadEnabled = false;
QString m_targetFilePath;
}; };
} // namespace QmlDesigner } // namespace QmlDesigner

View File

@@ -104,13 +104,13 @@ void FileExtractor::browse()
emit targetFolderExistsChanged(); emit targetFolderExistsChanged();
} }
void FileExtractor::setSourceFile(QString &sourceFilePath) void FileExtractor::setSourceFile(const QString &sourceFilePath)
{ {
m_sourceFile = Utils::FilePath::fromString(sourceFilePath); m_sourceFile = Utils::FilePath::fromString(sourceFilePath);
emit targetFolderExistsChanged(); emit targetFolderExistsChanged();
} }
void FileExtractor::setArchiveName(QString &filePath) void FileExtractor::setArchiveName(const QString &filePath)
{ {
m_archiveName = filePath; m_archiveName = filePath;
emit targetFolderExistsChanged(); emit targetFolderExistsChanged();

View File

@@ -36,8 +36,8 @@ public:
QString targetPath() const; QString targetPath() const;
void setTargetPath(const QString &path); void setTargetPath(const QString &path);
void setSourceFile(QString &sourceFilePath); void setSourceFile(const QString &sourceFilePath);
void setArchiveName(QString &filePath); void setArchiveName(const QString &filePath);
const QString detailedText() const; const QString detailedText() const;
bool finished() const; bool finished() const;
QString currentFile() const; QString currentFile() const;

View File

@@ -0,0 +1,132 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
#include "multifiledownloader.h"
#include "filedownloader.h"
namespace QmlDesigner {
MultiFileDownloader::MultiFileDownloader(QObject *parent)
: QObject(parent)
{}
MultiFileDownloader::~MultiFileDownloader()
{}
void MultiFileDownloader::setDownloader(FileDownloader *downloader)
{
m_downloader = downloader;
QObject::connect(this, &MultiFileDownloader::downloadStarting, [this]() {
m_nextFile = 0;
if (m_files.length() > 0)
m_downloader->start();
});
QObject::connect(m_downloader, &FileDownloader::progressChanged, this, [this]() {
m_progress = (m_nextFile + m_downloader->progress()) / m_files.count();
});
QObject::connect(m_downloader, &FileDownloader::downloadFailed, this, [this]() {
m_failed = true;
emit downloadFailed();
});
QObject::connect(m_downloader, &FileDownloader::downloadCanceled, this, [this]() {
m_canceled = true;
emit downloadCanceled();
});
QObject::connect(m_downloader, &FileDownloader::finishedChanged, this, [this]() {
switchToNextFile();
});
}
void MultiFileDownloader::start()
{
emit downloadStarting();
}
void MultiFileDownloader::cancel()
{
m_canceled = true;
m_downloader->cancel();
}
void MultiFileDownloader::setBaseUrl(const QUrl &baseUrl)
{
if (m_baseUrl != baseUrl) {
m_baseUrl = baseUrl;
emit baseUrlChanged();
}
}
QUrl MultiFileDownloader::baseUrl() const
{
return m_baseUrl;
}
bool MultiFileDownloader::finished() const
{
return m_finished;
}
int MultiFileDownloader::progress() const
{
return m_progress;
}
void MultiFileDownloader::setTargetDirPath(const QString &path)
{
m_targetDirPath = path;
}
QString MultiFileDownloader::targetDirPath() const
{
return m_targetDirPath;
}
QString MultiFileDownloader::nextUrl() const
{
if (m_nextFile >= m_files.length())
return {};
return m_baseUrl.toString() + "/" + m_files[m_nextFile];
}
QString MultiFileDownloader::nextTargetPath() const
{
if (m_nextFile >= m_files.length())
return {};
return m_targetDirPath + "/" + m_files[m_nextFile];
}
void MultiFileDownloader::setFiles(const QStringList &files)
{
m_files = files;
}
QStringList MultiFileDownloader::files() const
{
return m_files;
}
void MultiFileDownloader::switchToNextFile()
{
++m_nextFile;
if (m_nextFile < m_files.length()) {
if (m_canceled) {
emit downloadCanceled();
} else {
emit nextUrlChanged();
emit nextTargetPathChanged();
m_downloader->start();
}
} else {
m_finished = true;
emit finishedChanged();
}
}
} // namespace QmlDesigner

View File

@@ -0,0 +1,77 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
#pragma once
#include <QObject>
#include <QUrl>
namespace QmlDesigner {
class FileDownloader;
class MultiFileDownloader : public QObject
{
Q_OBJECT
Q_PROPERTY(FileDownloader *downloader WRITE setDownloader)
Q_PROPERTY(bool finished READ finished NOTIFY finishedChanged)
Q_PROPERTY(int progress READ progress NOTIFY progressChanged)
Q_PROPERTY(QUrl baseUrl READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged)
Q_PROPERTY(QString targetDirPath READ targetDirPath WRITE setTargetDirPath NOTIFY targetDirPathChanged)
Q_PROPERTY(QString nextUrl READ nextUrl NOTIFY nextUrlChanged)
Q_PROPERTY(QString nextTargetPath READ nextTargetPath NOTIFY nextTargetPathChanged)
Q_PROPERTY(QStringList files READ files WRITE setFiles NOTIFY filesChanged)
public:
explicit MultiFileDownloader(QObject *parent = nullptr);
~MultiFileDownloader();
void setBaseUrl(const QUrl &url);
QUrl baseUrl() const;
void setTargetDirPath(const QString &path);
QString targetDirPath() const;
void setDownloader(FileDownloader *downloader);
bool finished() const;
int progress() const;
QString nextUrl() const;
QString nextTargetPath() const;
void setFiles(const QStringList &files);
QStringList files() const;
void switchToNextFile();
Q_INVOKABLE void start();
Q_INVOKABLE void cancel();
signals:
void finishedChanged();
void baseUrlChanged();
void progressChanged();
void downloadFailed();
void downloadStarting();
void downloadCanceled();
void targetDirPathChanged();
void nextUrlChanged();
void filesChanged();
void nextTargetPathChanged();
private:
QUrl m_baseUrl;
bool m_finished = false;
int m_progress = 0;
QString m_targetDirPath;
FileDownloader *m_downloader = nullptr;
bool m_canceled = false;
bool m_failed = false;
QStringList m_files;
int m_nextFile = 0;
};
} // namespace QmlDesigner

View File

@@ -89,7 +89,7 @@ void DesignerSettings::fromSettings(QSettings *settings)
restoreValue(settings, DesignerSettingsKey::EDITOR_ZOOM_FACTOR, 1.0); restoreValue(settings, DesignerSettingsKey::EDITOR_ZOOM_FACTOR, 1.0);
restoreValue(settings, DesignerSettingsKey::ACTIONS_MERGE_TEMPLATE_ENABLED, false); restoreValue(settings, DesignerSettingsKey::ACTIONS_MERGE_TEMPLATE_ENABLED, false);
restoreValue(settings, DesignerSettingsKey::DOWNLOADABLE_BUNDLES_URL, restoreValue(settings, DesignerSettingsKey::DOWNLOADABLE_BUNDLES_URL,
"https://cdn.qt.io/designstudio/bundles/textures"); "https://cdn.qt.io/designstudio/bundles");
settings->endGroup(); settings->endGroup();
settings->endGroup(); settings->endGroup();

View File

@@ -60,7 +60,7 @@ inline constexpr char SMOOTH_RENDERING[] = "SmoothRendering";
inline constexpr char OLD_STATES_EDITOR[] = "ForceOldStatesEditor"; inline constexpr char OLD_STATES_EDITOR[] = "ForceOldStatesEditor";
inline constexpr char EDITOR_ZOOM_FACTOR[] = "EditorZoomFactor"; inline constexpr char EDITOR_ZOOM_FACTOR[] = "EditorZoomFactor";
inline constexpr char ACTIONS_MERGE_TEMPLATE_ENABLED[] = "ActionsMergeTemplateEnabled"; inline constexpr char ACTIONS_MERGE_TEMPLATE_ENABLED[] = "ActionsMergeTemplateEnabled";
inline constexpr char DOWNLOADABLE_BUNDLES_URL[] = "DownloadableBundlesUrl"; inline constexpr char DOWNLOADABLE_BUNDLES_URL[] = "DownloadableBundlesLocation";
} }
class QMLDESIGNERBASE_EXPORT DesignerSettings class QMLDESIGNERBASE_EXPORT DesignerSettings

View File

@@ -117,7 +117,7 @@ DataModelDownloader::DataModelDownloader(QObject * /* parent */)
if (m_fileDownloader.finished()) { if (m_fileDownloader.finished()) {
const Utils::FilePath archiveFile = Utils::FilePath::fromString( const Utils::FilePath archiveFile = Utils::FilePath::fromString(
m_fileDownloader.tempFile()); m_fileDownloader.outputFile());
QTC_ASSERT(Utils::Archive::supportsFile(archiveFile), return ); QTC_ASSERT(Utils::Archive::supportsFile(archiveFile), return );
auto archive = new Utils::Archive(archiveFile, tempFilePath()); auto archive = new Utils::Archive(archiveFile, tempFilePath());
QTC_ASSERT(archive->isValid(), delete archive; return ); QTC_ASSERT(archive->isValid(), delete archive; return );

View File

@@ -40,7 +40,7 @@ Rectangle {
FileExtractor { FileExtractor {
id: fileExtractor id: fileExtractor
archiveName: root.completeBaseName.length === 0 ? downloader.completeBaseName : root.completeBaseName archiveName: root.completeBaseName.length === 0 ? downloader.completeBaseName : root.completeBaseName
sourceFile: root.tempFile.length === 0 ? downloader.tempFile : root.tempFile sourceFile: root.tempFile.length === 0 ? downloader.outputFile : root.tempFile
} }
FileDownloader { FileDownloader {