diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterial.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterial.qml index 980001933a6..3b58f14a162 100644 --- a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterial.qml +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryMaterial.qml @@ -10,25 +10,34 @@ import QtQuick.Controls import StudioTheme 1.0 as StudioTheme import ContentLibraryBackend +import WebFetcher 1.0 + Item { id: root signal showContextMenu() + // Download states: "" (ie default, not downloaded), "unavailable", "downloading", "downloaded", + // "failed" + property string downloadState: modelData.isDownloaded() ? "downloaded" : "" + visible: modelData.bundleMaterialVisible MouseArea { id: mouseArea + enabled: root.downloadState !== "downloading" hoverEnabled: true anchors.fill: parent acceptedButtons: Qt.LeftButton | Qt.RightButton onPressed: (mouse) => { - if (mouse.button === Qt.LeftButton && !materialsModel.importerRunning) - ContentLibraryBackend.rootView.startDragMaterial(modelData, mapToGlobal(mouse.x, mouse.y)) - else if (mouse.button === Qt.RightButton) + if (mouse.button === Qt.LeftButton && !materialsModel.importerRunning) { + if (root.downloadState === "downloaded") + ContentLibraryBackend.rootView.startDragMaterial(modelData, mapToGlobal(mouse.x, mouse.y)) + } else if (mouse.button === Qt.RightButton && root.downloadState === "downloaded") { root.showContextMenu() + } } } @@ -38,6 +47,15 @@ Item { Item { width: 1; height: 5 } // spacer + DownloadPane { + id: downloadPane + width: root.width - 10 + height: img.width + visible: root.downloadState === "downloading" + + onRequestCancel: downloader.cancel() + } + Image { id: img @@ -46,6 +64,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter source: modelData.bundleMaterialIcon cache: false + visible: root.downloadState != "downloading" Rectangle { // circular indicator for imported bundle materials width: 10 @@ -83,13 +102,52 @@ Item { anchors.right: img.right anchors.bottom: img.bottom enabled: !ContentLibraryBackend.materialsModel.importerRunning - visible: containsMouse || mouseArea.containsMouse + visible: root.downloadState === "downloaded" + && (containsMouse || mouseArea.containsMouse) onClicked: { 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 { id: matName @@ -110,5 +168,44 @@ Item { selectionColor: StudioTheme.Values.themeTextSelectionColor 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 } diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryTexture.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryTexture.qml index 533924a8328..fef0504682f 100644 --- a/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryTexture.qml +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/ContentLibraryTexture.qml @@ -232,7 +232,7 @@ Item { FileExtractor { id: extractor archiveName: downloader.completeBaseName - sourceFile: downloader.tempFile + sourceFile: downloader.outputFile targetPath: modelData.textureParentPath alwaysCreateDir: false clearTargetPathContents: false diff --git a/share/qtcreator/qmldesigner/contentLibraryQmlSource/DownloadPane.qml b/share/qtcreator/qmldesigner/contentLibraryQmlSource/DownloadPane.qml new file mode 100644 index 00000000000..d7e040efa83 --- /dev/null +++ b/share/qtcreator/qmldesigner/contentLibraryQmlSource/DownloadPane.qml @@ -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 diff --git a/src/plugins/qmldesigner/CMakeLists.txt b/src/plugins/qmldesigner/CMakeLists.txt index a5644fc66e5..08572e6ba45 100644 --- a/src/plugins/qmldesigner/CMakeLists.txt +++ b/src/plugins/qmldesigner/CMakeLists.txt @@ -22,6 +22,7 @@ add_qtc_library(QmlDesignerUtils STATIC SOURCES asset.cpp asset.h filedownloader.cpp filedownloader.h + multifiledownloader.cpp multifiledownloader.h fileextractor.cpp fileextractor.h hdrimage.cpp hdrimage.h ktximage.cpp ktximage.h diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterial.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterial.cpp index 8f034d7a08d..ee94d8fd3ce 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterial.cpp +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterial.cpp @@ -3,6 +3,8 @@ #include "contentlibrarymaterial.h" +#include + namespace QmlDesigner { ContentLibraryMaterial::ContentLibraryMaterial(QObject *parent, @@ -10,8 +12,15 @@ ContentLibraryMaterial::ContentLibraryMaterial(QObject *parent, const QString &qml, const TypeName &type, const QUrl &icon, - const QStringList &files) - : QObject(parent), m_name(name), m_qml(qml), m_type(type), m_icon(icon), m_files(files) {} + const QStringList &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) { @@ -64,4 +73,25 @@ bool ContentLibraryMaterial::imported() const 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 diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterial.h b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterial.h index 5d156809583..39f8ea0f4d8 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterial.h +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterial.h @@ -19,6 +19,9 @@ class ContentLibraryMaterial : public QObject Q_PROPERTY(QUrl bundleMaterialIcon MEMBER m_icon CONSTANT) Q_PROPERTY(bool bundleMaterialVisible MEMBER m_visible NOTIFY materialVisibleChanged) 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: ContentLibraryMaterial(QObject *parent, @@ -26,18 +29,25 @@ public: const QString &qml, const TypeName &type, const QUrl &icon, - const QStringList &files); + const QStringList &files, + const QString &downloadPath, + const QString &baseWebUrl); bool filter(const QString &searchText); + Q_INVOKABLE bool isDownloaded() const; + QUrl icon() const; QString qml() const; TypeName type() const; QStringList files() const; bool visible() const; + QString qmlFilePath() const; bool setImported(bool imported); bool imported() const; + QString parentDirPath() const; + QStringList allFiles() const; signals: void materialVisibleChanged(); @@ -52,6 +62,10 @@ private: bool m_visible = true; bool m_imported = false; + + QString m_downloadPath; + QString m_baseWebUrl; + QStringList m_allFiles; }; } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterialsmodel.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterialsmodel.cpp index be2900008b7..c5620097cb8 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterialsmodel.cpp +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterialsmodel.cpp @@ -7,7 +7,11 @@ #include "contentlibrarymaterial.h" #include "contentlibrarymaterialscategory.h" #include "contentlibrarywidget.h" +#include "filedownloader.h" +#include "fileextractor.h" +#include "multifiledownloader.h" #include "qmldesignerconstants.h" +#include "qmldesignerplugin.h" #include #include @@ -16,6 +20,8 @@ #include #include #include +#include +#include #include namespace QmlDesigner { @@ -24,7 +30,19 @@ ContentLibraryMaterialsModel::ContentLibraryMaterialsModel(ContentLibraryWidget : QAbstractListModel(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("WebFetcher", 1, 0, "FileDownloader"); + qmlRegisterType("WebFetcher", 1, 0, "MultiFileDownloader"); + + QDir bundleDir{m_downloadPath}; + if (fetchBundleMetadata(bundleDir) && fetchBundleIcons(bundleDir)) + loadMaterialBundle(bundleDir); } int ContentLibraryMaterialsModel::rowCount(const QModelIndex &) const @@ -91,30 +109,127 @@ QHash ContentLibraryMaterialsModel::roleNames() const 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; - 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"); if (m_matBundleObj.isEmpty()) { @@ -160,7 +275,8 @@ void ContentLibraryMaterialsModel::loadMaterialBundle() bundleId, 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); } @@ -172,24 +288,22 @@ void ContentLibraryMaterialsModel::loadMaterialBundle() for (const auto /*QJson{Const,}ValueRef*/ &file : sharedFilesArr) sharedFiles.append(file.toString()); - m_importer = new Internal::ContentLibraryBundleImporter(matBundleDir.path(), bundleId, sharedFiles); - connect(m_importer, &Internal::ContentLibraryBundleImporter::importFinished, this, - [&](const QmlDesigner::NodeMetaInfo &metaInfo) { - m_importerRunning = false; - emit importerRunningChanged(); - if (metaInfo.isValid()) - emit bundleMaterialImported(metaInfo); - }); + QStringList missingSharedFiles; + for (const QString &s : std::as_const(sharedFiles)) { + const QString fullSharedFilePath = matBundleDir.filePath(s); - connect(m_importer, &Internal::ContentLibraryBundleImporter::unimportFinished, this, - [&](const QmlDesigner::NodeMetaInfo &metaInfo) { - Q_UNUSED(metaInfo) - m_importerRunning = false; - emit importerRunningChanged(); - emit bundleMaterialUnimported(metaInfo); - }); + if (!QFile::exists(fullSharedFilePath)) + missingSharedFiles.push_back(s); + } - 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 diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterialsmodel.h b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterialsmodel.h index 91ec476c51f..6a9e87f6aad 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterialsmodel.h +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarymaterialsmodel.h @@ -6,6 +6,7 @@ #include "nodemetainfo.h" #include +#include #include namespace QmlDesigner { @@ -70,8 +71,13 @@ signals: void matBundleExistsChanged(); private: - void loadMaterialBundle(); + void loadMaterialBundle(const QDir &matBundleDir); + bool fetchBundleIcons(const QDir &bundleDir); + bool fetchBundleMetadata(const QDir &bundleDir); 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; QString m_searchText; @@ -82,11 +88,17 @@ private: bool m_isEmpty = true; bool m_matBundleExists = false; bool m_hasModelSelection = false; - bool m_probeMatBundleDir = false; bool m_importerRunning = false; int m_quick3dMajorVersion = -1; int m_quick3dMinorVersion = -1; + + QString m_downloadPath; + QString m_baseUrl; + + QString m_importerBundlePath; + QString m_importerBundleId; + QStringList m_importerSharedFiles; }; } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarytexturesmodel.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarytexturesmodel.cpp index ce852da2e74..54592f2c283 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarytexturesmodel.cpp +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarytexturesmodel.cpp @@ -116,7 +116,7 @@ void ContentLibraryTexturesModel::loadTextureBundle(const QString &bundlePath, c QString remoteBaseUrl = QmlDesignerPlugin::settings() .value(DesignerSettingsKey::DOWNLOADABLE_BUNDLES_URL).toString() - + '/' + m_category; + + "/textures/" + m_category; const QFileInfoList dirs = bundleDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot); for (const QFileInfo &dir : dirs) { diff --git a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.cpp b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.cpp index 311dfcb522f..89a6a049a53 100644 --- a/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.cpp +++ b/src/plugins/qmldesigner/components/contentlibrary/contentlibrarywidget.cpp @@ -54,7 +54,8 @@ bool ContentLibraryWidget::eventFilter(QObject *obj, QEvent *event) if (m_materialToDrag) { QMouseEvent *me = static_cast(event); - if ((me->globalPos() - m_dragStartPoint).manhattanLength() > 20) { + if ((me->globalPos() - m_dragStartPoint).manhattanLength() > 20 + && m_materialToDrag->isDownloaded()) { QByteArray data; QMimeData *mimeData = new QMimeData; QDataStream stream(&data, QIODevice::WriteOnly); diff --git a/src/plugins/qmldesigner/utils/filedownloader.cpp b/src/plugins/qmldesigner/utils/filedownloader.cpp index e079f47ca3d..b5aaab59d80 100644 --- a/src/plugins/qmldesigner/utils/filedownloader.cpp +++ b/src/plugins/qmldesigner/utils/filedownloader.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -12,33 +13,52 @@ namespace QmlDesigner { FileDownloader::FileDownloader(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() { - if (m_tempFile.exists()) - m_tempFile.remove(); + // Delete the temp file only if a target Path was set (i.e. file will be moved) + if (deleteFileAtTheEnd() && m_outputFile.exists()) + m_outputFile.remove(); +} + +bool FileDownloader::deleteFileAtTheEnd() const +{ + return m_targetFilePath.isEmpty(); } void FileDownloader::start() { emit downloadStarting(); - m_tempFile.setFileName(QDir::tempPath() + "/" + name() + ".XXXXXX" + ".zip"); - m_tempFile.open(QIODevice::WriteOnly); + QString tempFileName = QDir::tempPath() + "/.qds_download_" + url().fileName(); + + m_outputFile.setFileName(tempFileName); + m_outputFile.open(QIODevice::WriteOnly); auto request = QNetworkRequest(m_url); request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, 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; QString contentType; - if (!m_reply->hasRawHeader("Content-Type")) { + if (!reply->hasRawHeader("Content-Type")) { isDownloadingFile = true; } else { - contentType = QString::fromUtf8(m_reply->rawHeader("Content-Type")); + contentType = QString::fromUtf8(reply->rawHeader("Content-Type")); if (contentType.startsWith("application/") || contentType.startsWith("image/") @@ -50,12 +70,12 @@ void FileDownloader::start() } if (isDownloadingFile) - m_tempFile.write(m_reply->readAll()); + m_outputFile.write(reply->readAll()); else - m_reply->close(); + reply->close(); }); - QNetworkReply::connect(m_reply, + QNetworkReply::connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 current, qint64 max) { @@ -66,33 +86,44 @@ void FileDownloader::start() } m_progress = current * 100 / max; - emit progressChanged(); }); - QNetworkReply::connect(m_reply, &QNetworkReply::redirected, [this](const QUrl &) { - emit m_reply->redirectAllowed(); + QNetworkReply::connect(reply, &QNetworkReply::redirected, [reply](const QUrl &) { + emit reply->redirectAllowed(); }); - QNetworkReply::connect(m_reply, &QNetworkReply::finished, this, [this]() { - if (m_reply->error()) { - if (m_tempFile.exists()) - m_tempFile.remove(); - - if (m_reply->error() != QNetworkReply::OperationCanceledError) { - qWarning() << Q_FUNC_INFO << m_url << m_reply->errorString(); + QNetworkReply::connect(reply, &QNetworkReply::finished, this, [this, reply]() { + if (reply->error()) { + if (reply->error() != QNetworkReply::OperationCanceledError) { + qWarning() << Q_FUNC_INFO << m_url << reply->errorString(); emit downloadFailed(); } else { emit downloadCanceled(); } } else { - m_tempFile.flush(); - m_tempFile.close(); + m_outputFile.flush(); + 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; - emit tempFileChanged(); + emit outputFileChanged(); emit finishedChanged(); } + reply->deleteLater(); m_reply = nullptr; }); } @@ -176,9 +207,9 @@ int FileDownloader::progress() const 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 @@ -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 diff --git a/src/plugins/qmldesigner/utils/filedownloader.h b/src/plugins/qmldesigner/utils/filedownloader.h index 32c0fb5c87c..b1202e57cb8 100644 --- a/src/plugins/qmldesigner/utils/filedownloader.h +++ b/src/plugins/qmldesigner/utils/filedownloader.h @@ -15,13 +15,14 @@ class FileDownloader : public QObject Q_PROPERTY(bool downloadEnabled WRITE setDownloadEnabled READ downloadEnabled NOTIFY downloadEnabledChanged) 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 finished READ finished NOTIFY finishedChanged) Q_PROPERTY(bool error READ error NOTIFY errorChanged) Q_PROPERTY(QString name READ name NOTIFY nameChanged) Q_PROPERTY(QString completeBaseName READ completeBaseName NOTIFY nameChanged) 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(bool available READ available NOTIFY availableChanged) @@ -32,12 +33,14 @@ public: void setUrl(const QUrl &url); QUrl url() const; + void setTargetFilePath(const QString &path); + QString targetFilePath() const; bool finished() const; bool error() const; QString name() const; QString completeBaseName() const; int progress() const; - QString tempFile() const; + QString outputFile() const; QDateTime lastModified() const; bool available() const; void setDownloadEnabled(bool value); @@ -55,7 +58,7 @@ signals: void nameChanged(); void urlChanged(); void progressChanged(); - void tempFileChanged(); + void outputFileChanged(); void downloadFailed(); void lastModifiedChanged(); void availableChanged(); @@ -64,21 +67,24 @@ signals: void downloadStarting(); void downloadCanceled(); void probeUrlChanged(); + void targetFilePathChanged(); private: void doProbeUrl(); + bool deleteFileAtTheEnd() const; QUrl m_url; bool m_probeUrl = false; bool m_finished = false; bool m_error = false; int m_progress = 0; - QFile m_tempFile; + QFile m_outputFile; QDateTime m_lastModified; bool m_available = false; QNetworkReply *m_reply = nullptr; bool m_downloadEnabled = false; + QString m_targetFilePath; }; } // namespace QmlDesigner diff --git a/src/plugins/qmldesigner/utils/fileextractor.cpp b/src/plugins/qmldesigner/utils/fileextractor.cpp index a7e32b6bc1d..c6aa83fb7ae 100644 --- a/src/plugins/qmldesigner/utils/fileextractor.cpp +++ b/src/plugins/qmldesigner/utils/fileextractor.cpp @@ -104,13 +104,13 @@ void FileExtractor::browse() emit targetFolderExistsChanged(); } -void FileExtractor::setSourceFile(QString &sourceFilePath) +void FileExtractor::setSourceFile(const QString &sourceFilePath) { m_sourceFile = Utils::FilePath::fromString(sourceFilePath); emit targetFolderExistsChanged(); } -void FileExtractor::setArchiveName(QString &filePath) +void FileExtractor::setArchiveName(const QString &filePath) { m_archiveName = filePath; emit targetFolderExistsChanged(); diff --git a/src/plugins/qmldesigner/utils/fileextractor.h b/src/plugins/qmldesigner/utils/fileextractor.h index 2579ddcda8c..5dbf12e903a 100644 --- a/src/plugins/qmldesigner/utils/fileextractor.h +++ b/src/plugins/qmldesigner/utils/fileextractor.h @@ -36,8 +36,8 @@ public: QString targetPath() const; void setTargetPath(const QString &path); - void setSourceFile(QString &sourceFilePath); - void setArchiveName(QString &filePath); + void setSourceFile(const QString &sourceFilePath); + void setArchiveName(const QString &filePath); const QString detailedText() const; bool finished() const; QString currentFile() const; diff --git a/src/plugins/qmldesigner/utils/multifiledownloader.cpp b/src/plugins/qmldesigner/utils/multifiledownloader.cpp new file mode 100644 index 00000000000..346c1675f6e --- /dev/null +++ b/src/plugins/qmldesigner/utils/multifiledownloader.cpp @@ -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 diff --git a/src/plugins/qmldesigner/utils/multifiledownloader.h b/src/plugins/qmldesigner/utils/multifiledownloader.h new file mode 100644 index 00000000000..548896dbc8d --- /dev/null +++ b/src/plugins/qmldesigner/utils/multifiledownloader.h @@ -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 +#include + +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 + diff --git a/src/plugins/qmldesignerbase/utils/designersettings.cpp b/src/plugins/qmldesignerbase/utils/designersettings.cpp index c5db098cd40..8c94e040f97 100644 --- a/src/plugins/qmldesignerbase/utils/designersettings.cpp +++ b/src/plugins/qmldesignerbase/utils/designersettings.cpp @@ -89,7 +89,7 @@ void DesignerSettings::fromSettings(QSettings *settings) restoreValue(settings, DesignerSettingsKey::EDITOR_ZOOM_FACTOR, 1.0); restoreValue(settings, DesignerSettingsKey::ACTIONS_MERGE_TEMPLATE_ENABLED, false); restoreValue(settings, DesignerSettingsKey::DOWNLOADABLE_BUNDLES_URL, - "https://cdn.qt.io/designstudio/bundles/textures"); + "https://cdn.qt.io/designstudio/bundles"); settings->endGroup(); settings->endGroup(); diff --git a/src/plugins/qmldesignerbase/utils/designersettings.h b/src/plugins/qmldesignerbase/utils/designersettings.h index b189b825011..78b43705268 100644 --- a/src/plugins/qmldesignerbase/utils/designersettings.h +++ b/src/plugins/qmldesignerbase/utils/designersettings.h @@ -60,7 +60,7 @@ inline constexpr char SMOOTH_RENDERING[] = "SmoothRendering"; inline constexpr char OLD_STATES_EDITOR[] = "ForceOldStatesEditor"; inline constexpr char EDITOR_ZOOM_FACTOR[] = "EditorZoomFactor"; 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 diff --git a/src/plugins/studiowelcome/examplecheckout.cpp b/src/plugins/studiowelcome/examplecheckout.cpp index 37cbca16b81..575e45f7610 100644 --- a/src/plugins/studiowelcome/examplecheckout.cpp +++ b/src/plugins/studiowelcome/examplecheckout.cpp @@ -117,7 +117,7 @@ DataModelDownloader::DataModelDownloader(QObject * /* parent */) if (m_fileDownloader.finished()) { const Utils::FilePath archiveFile = Utils::FilePath::fromString( - m_fileDownloader.tempFile()); + m_fileDownloader.outputFile()); QTC_ASSERT(Utils::Archive::supportsFile(archiveFile), return ); auto archive = new Utils::Archive(archiveFile, tempFilePath()); QTC_ASSERT(archive->isValid(), delete archive; return ); diff --git a/src/plugins/studiowelcome/qml/downloaddialog/main.qml b/src/plugins/studiowelcome/qml/downloaddialog/main.qml index acaa8959e60..82a23224656 100644 --- a/src/plugins/studiowelcome/qml/downloaddialog/main.qml +++ b/src/plugins/studiowelcome/qml/downloaddialog/main.qml @@ -40,7 +40,7 @@ Rectangle { FileExtractor { id: fileExtractor 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 {