QmlDesigner: Make ContentLibrary textures downloadable

At this point the textures_bundle is still required, but only because
of the icons of the textures. Also, some changes should be done for the
visuals of the downloading.

Also, did a fix in FileDownloader: In case the URL given does not look
to be an image file, we should cancel the download instead of treating
it as a zip archive--it can be that eg we were redirected to a sign-in
page and we don't want to download the content of the page and save it
as a zip file.

Task-number: QDS-8664
Change-Id: Iec40e540c116030288df76e1922eab56ba323d1e
Reviewed-by: Miikka Heikkinen <miikka.heikkinen@qt.io>
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
This commit is contained in:
Samuel Ghinet
2023-01-18 00:15:38 +02:00
parent 519b9e3c52
commit 1a6cc6fa5e
20 changed files with 608 additions and 131 deletions

View File

@@ -9,37 +9,131 @@ import QtQuick.Controls
import StudioTheme 1.0 as StudioTheme
Image {
import WebFetcher 1.0
Item {
id: root
source: modelData.textureIcon
visible: modelData.textureVisible
cache: false
// Download states: "" (ie default, not downloaded), "unavailable", "downloading", "downloaded",
// "failed"
property string downloadState: modelData.isDownloaded() ? "downloaded" : ""
property bool delegateVisible: modelData.textureVisible
property alias allowCancel: progressBar.closeButtonVisible
property alias progressValue: progressBar.value
property alias progressText: progressLabel.text
signal showContextMenu()
MouseArea {
id: mouseArea
function statusText()
{
if (root.downloadState === "downloaded")
return qsTr("Texture was already downloaded.")
if (root.downloadState === "unavailable")
return qsTr("Network/Texture unavailable or broken Link.")
if (root.downloadState === "failed")
return qsTr("Could not download texture.")
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
onPressed: (mouse) => {
if (mouse.button === Qt.LeftButton)
rootView.startDragTexture(modelData, mapToGlobal(mouse.x, mouse.y))
else if (mouse.button === Qt.RightButton)
root.showContextMenu()
return qsTr("Click to download the texture.")
}
Rectangle {
id: downloadPane
anchors.fill: parent
color: StudioTheme.Values.themeThumbnailBackground
border.color: "#00000000"
visible: root.downloadState === "downloading"
TextureProgressBar {
id: progressBar
anchors.rightMargin: 10
anchors.leftMargin: 10
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
visible: false
onCancelRequested: {
downloader.cancel()
}
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
Image {
id: image
anchors.fill: parent
source: modelData.textureIcon
visible: root.delegateVisible && root.downloadState != "downloading"
cache: false
property string webUrl: modelData.textureWebUrl
Text {
id: downloadIcon
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
visible: root.downloadState !== "downloaded"
}
ToolTip {
visible: mouseArea.containsMouse
id: tooltip
// contentWidth is not calculated correctly by the toolTip (resulting in a wider tooltip than
// needed). Using a helper Text to calculate the correct width
contentWidth: helperText.width
bottomInset: -2
text: modelData.textureToolTip
text: modelData.textureToolTip + (downloadIcon.visible
? "\n\n" + root.statusText()
: "")
delay: 1000
Text {
@@ -48,4 +142,99 @@ Image {
visible: false
}
}
} // Image
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton
hoverEnabled: true
onEntered: tooltip.visible = image.visible
onExited: tooltip.visible = false
onPressed: (mouse) => {
if (mouse.button === Qt.LeftButton) {
if (root.downloadState === "downloaded")
rootView.startDragTexture(modelData, mapToGlobal(mouse.x, mouse.y))
} else if (mouse.button === Qt.RightButton) {
root.showContextMenu()
}
}
onClicked: {
if (!rootView.markTextureDownloading())
return
if (root.downloadState !== "" && root.downloadState !== "failed")
return
progressBar.visible = true
tooltip.visible = false
root.progressText = qsTr("Downloading...")
root.allowCancel = true
root.progressValue = Qt.binding(function() { return downloader.progress })
mouseArea.enabled = false
root.downloadState = ""
root.downloadStateChanged()
downloader.start()
}
}
FileDownloader {
id: downloader
url: image.webUrl
probeUrl: false
downloadEnabled: true
onDownloadStarting: {
root.downloadState = "downloading"
root.downloadStateChanged()
}
onFinishedChanged: {
root.progressText = qsTr("Extracting...")
root.allowCancel = false
root.progressValue = Qt.binding(function() { return extractor.progress })
extractor.extract()
}
onDownloadCanceled: {
root.progressText = ""
root.progressValue = 0
root.downloadState = "failed"
root.downloadStateChanged()
mouseArea.enabled = true
rootView.markNoTextureDownloading()
}
onDownloadFailed: {
root.downloadState = "failed"
root.downloadStateChanged()
mouseArea.enabled = true
rootView.markNoTextureDownloading()
}
}
FileExtractor {
id: extractor
archiveName: downloader.completeBaseName
sourceFile: downloader.tempFile
targetPath: modelData.textureParentPath
alwaysCreateDir: false
clearTargetPathContents: false
onFinishedChanged: {
mouseArea.enabled = true
modelData.setDownloaded()
root.downloadState = "downloaded"
root.downloadStateChanged()
rootView.markNoTextureDownloading()
}
}
}

View File

@@ -0,0 +1,62 @@
// 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 QtQuick.Controls
import StudioTheme as StudioTheme
Item {
id: root
width: 272
height: 25
property int value: 0
property bool closeButtonVisible
readonly property int margin: 4
readonly property string qdsBrand: "#57B9FC"
signal cancelRequested
Rectangle {
id: progressBarGroove
color: StudioTheme.Values.themeThumbnailLabelBackground
anchors.fill: parent
}
Rectangle {
id: progressBarTrack
width: root.value * ((root.width - closeButton.width) - 2 * root.margin) / 100
color: root.qdsBrand
border.color: "#002e769e"
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: root.margin
}
Text {
id: closeButton
visible: root.closeButtonVisible
width: 20
text: StudioTheme.Constants.closeCross
color: root.qdsBrand
horizontalAlignment: Qt.AlignHCenter
verticalAlignment: Qt.AlignVCenter
font.family: StudioTheme.Constants.iconFont.family
font.pixelSize: StudioTheme.Values.myIconFontSize
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.margins: root.margin
MouseArea {
anchors.fill: parent
onClicked: {
root.cancelRequested()
}
}
}
}

View File

@@ -4,20 +4,41 @@
#include "contentlibrarytexture.h"
#include "imageutils.h"
#include <utils/algorithm.h>
#include <QDir>
#include <QFileInfo>
namespace QmlDesigner {
ContentLibraryTexture::ContentLibraryTexture(QObject *parent, const QString &path, const QUrl &icon)
ContentLibraryTexture::ContentLibraryTexture(QObject *parent, const QFileInfo &iconFileInfo,
const QString &downloadPath, const QUrl &icon, const QString &webUrl)
: QObject(parent)
, m_path(path)
, m_iconPath(iconFileInfo.filePath())
, m_downloadPath(downloadPath)
, m_webUrl(webUrl)
, m_baseName{iconFileInfo.baseName()}
, m_icon(icon)
{
m_toolTip = QLatin1String("%1\n%2").arg(path.split('/').last(), ImageUtils::imageInfo(path));
m_fileExt = computeFileExt();
QString fileName;
QString imageInfo;
if (m_fileExt.isEmpty()) {
imageInfo = ImageUtils::imageInfo(m_iconPath, false);
fileName = m_baseName + m_defaultExt;
} else {
fileName = m_baseName + m_fileExt;
QString fullDownloadPath = m_downloadPath + "/" + fileName;
imageInfo = ImageUtils::imageInfo(fullDownloadPath, false);
}
m_toolTip = QLatin1String("%1\n%2").arg(fileName, imageInfo);
}
bool ContentLibraryTexture::filter(const QString &searchText)
{
if (m_visible != m_path.contains(searchText, Qt::CaseInsensitive)) {
if (m_visible != m_iconPath.contains(searchText, Qt::CaseInsensitive)) {
m_visible = !m_visible;
emit textureVisibleChanged();
}
@@ -32,7 +53,46 @@ QUrl ContentLibraryTexture::icon() const
QString ContentLibraryTexture::path() const
{
return m_path;
return m_iconPath;
}
QString ContentLibraryTexture::computeFileExt()
{
const QFileInfoList files = QDir(m_downloadPath).entryInfoList(QDir::Files);
const QFileInfoList textureFiles = Utils::filtered(files, [this](const QFileInfo &fi) {
return fi.baseName() == m_baseName;
});
if (textureFiles.isEmpty())
return {};
if (textureFiles.count() > 1) {
qWarning() << "Found multiple textures with the same name in the same directories: "
<< Utils::transform(textureFiles, [](const QFileInfo &fi) {
return fi.fileName();
});
}
return QString{"."} + textureFiles.at(0).completeSuffix();
}
bool ContentLibraryTexture::isDownloaded() const
{
if (m_fileExt.isEmpty())
return false;
QString fullPath = m_downloadPath + "/" + m_baseName + m_fileExt;
return QFileInfo(fullPath).isFile();
}
void ContentLibraryTexture::setDownloaded()
{
m_fileExt = computeFileExt();
}
QString ContentLibraryTexture::parentDirPath() const
{
return m_downloadPath;
}
} // namespace QmlDesigner

View File

@@ -3,6 +3,7 @@
#pragma once
#include <QFileInfo>
#include <QObject>
#include <QUrl>
@@ -12,25 +13,39 @@ class ContentLibraryTexture : public QObject
{
Q_OBJECT
Q_PROPERTY(QString texturePath MEMBER m_path CONSTANT)
Q_PROPERTY(QString textureIconPath MEMBER m_iconPath CONSTANT)
Q_PROPERTY(QString textureParentPath READ parentDirPath CONSTANT)
Q_PROPERTY(QString textureToolTip MEMBER m_toolTip CONSTANT)
Q_PROPERTY(QUrl textureIcon MEMBER m_icon CONSTANT)
Q_PROPERTY(bool textureVisible MEMBER m_visible NOTIFY textureVisibleChanged)
Q_PROPERTY(QString textureWebUrl MEMBER m_webUrl CONSTANT)
public:
ContentLibraryTexture(QObject *parent, const QString &path, const QUrl &icon);
ContentLibraryTexture(QObject *parent, const QFileInfo &iconFileInfo,
const QString &downloadPath, const QUrl &icon, const QString &webUrl);
Q_INVOKABLE bool isDownloaded() const;
Q_INVOKABLE void setDownloaded();
bool filter(const QString &searchText);
QUrl icon() const;
QString path() const;
QString parentDirPath() const;
signals:
void textureVisibleChanged();
private:
QString m_path;
inline static const QString m_defaultExt = ".png";
QString computeFileExt();
QString m_iconPath;
QString m_downloadPath;
QString m_webUrl;
QString m_toolTip;
QString m_baseName;
QString m_fileExt;
QUrl m_icon;
bool m_visible = true;

View File

@@ -12,10 +12,12 @@ namespace QmlDesigner {
ContentLibraryTexturesCategory::ContentLibraryTexturesCategory(QObject *parent, const QString &name)
: QObject(parent), m_name(name) {}
void ContentLibraryTexturesCategory::addTexture(const QFileInfo &tex)
void ContentLibraryTexturesCategory::addTexture(const QFileInfo &tex, const QString &downloadPath,
const QString &webUrl)
{
QUrl icon = QUrl::fromLocalFile(tex.path() + "/icon/" + tex.baseName() + ".png");
m_categoryTextures.append(new ContentLibraryTexture(this, tex.filePath(), icon));
QUrl icon = QUrl::fromLocalFile(tex.absoluteFilePath());
m_categoryTextures.append(new ContentLibraryTexture(this, tex, downloadPath, icon, webUrl));
}
bool ContentLibraryTexturesCategory::filter(const QString &searchText)

View File

@@ -26,7 +26,7 @@ class ContentLibraryTexturesCategory : public QObject
public:
ContentLibraryTexturesCategory(QObject *parent, const QString &name);
void addTexture(const QFileInfo &tex);
void addTexture(const QFileInfo &tex, const QString &subPath, const QString &webUrl);
bool filter(const QString &searchText);
QString name() const;

View File

@@ -8,16 +8,29 @@
#include "utils/algorithm.h"
#include "utils/qtcassert.h"
#include <qmldesigner/utils/fileextractor.h>
#include <qmldesigner/utils/filedownloader.h>
#include <QCoreApplication>
#include <QDir>
#include <QFileInfo>
#include <QUrl>
#include <QQmlEngine>
#include <QStandardPaths>
namespace QmlDesigner {
ContentLibraryTexturesModel::ContentLibraryTexturesModel(QObject *parent)
ContentLibraryTexturesModel::ContentLibraryTexturesModel(const QString &bundleSubpath, QObject *parent)
: QAbstractListModel(parent)
{
qmlRegisterType<QmlDesigner::FileDownloader>("WebFetcher", 1, 0, "FileDownloader");
qmlRegisterType<QmlDesigner::FileExtractor>("WebFetcher", 1, 0, "FileExtractor");
static const QString baseDownloadPath =
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation)
+ "/QtDesignStudio/Downloaded";
m_downloadPath = baseDownloadPath + "/" + bundleSubpath;
}
int ContentLibraryTexturesModel::rowCount(const QModelIndex &) const
@@ -83,7 +96,7 @@ QHash<int, QByteArray> ContentLibraryTexturesModel::roleNames() const
return roles;
}
void ContentLibraryTexturesModel::loadTextureBundle(const QString &bundlePath)
void ContentLibraryTexturesModel::loadTextureBundle(const QString &bundlePath, const QString &baseUrl)
{
QDir bundleDir = QDir(bundlePath);
if (!bundleDir.exists()) {
@@ -97,9 +110,12 @@ void ContentLibraryTexturesModel::loadTextureBundle(const QString &bundlePath)
const QFileInfoList dirs = bundleDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
for (const QFileInfo &dir : dirs) {
auto category = new ContentLibraryTexturesCategory(this, dir.fileName());
const QFileInfoList texFiles = QDir(dir.filePath()).entryInfoList(QDir::Files);
for (const QFileInfo &tex : texFiles)
category->addTexture(tex);
const QFileInfoList texFiles = QDir(dir.filePath() + "/icon").entryInfoList(QDir::Files);
for (const QFileInfo &tex : texFiles) {
QString urlPath = baseUrl + "/" + dir.fileName() + "/" + tex.baseName() + ".zip";
QString downloadPath = m_downloadPath + "/" + dir.fileName();
category->addTexture(tex, downloadPath, urlPath);
}
m_bundleCategories.append(category);
}

View File

@@ -18,7 +18,7 @@ class ContentLibraryTexturesModel : public QAbstractListModel
Q_PROPERTY(bool hasSceneEnv READ hasSceneEnv NOTIFY hasSceneEnvChanged)
public:
ContentLibraryTexturesModel(QObject *parent = nullptr);
ContentLibraryTexturesModel(const QString &bundleSubpath, QObject *parent = nullptr);
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
@@ -33,7 +33,7 @@ public:
void setHasSceneEnv(bool b);
void resetModel();
void loadTextureBundle(const QString &bundlePath);
void loadTextureBundle(const QString &bundlePath, const QString &baseUrl);
signals:
void isEmptyChanged();
@@ -45,6 +45,7 @@ private:
void updateIsEmpty();
QString m_searchText;
QString m_downloadPath;
QList<ContentLibraryTexturesCategory *> m_bundleCategories;
bool m_isEmpty = true;

View File

@@ -20,10 +20,11 @@
#include <QMimeData>
#include <QMouseEvent>
#include <QQmlContext>
#include <QQuickWidget>
#include <QQmlEngine>
#include <QQuickItem>
#include <QQuickWidget>
#include <QShortcut>
#include <QStandardPaths>
#include <QVBoxLayout>
namespace QmlDesigner {
@@ -88,8 +89,8 @@ bool ContentLibraryWidget::eventFilter(QObject *obj, QEvent *event)
ContentLibraryWidget::ContentLibraryWidget()
: m_quickWidget(new QQuickWidget(this))
, m_materialsModel(new ContentLibraryMaterialsModel(this))
, m_texturesModel(new ContentLibraryTexturesModel(this))
, m_environmentsModel(new ContentLibraryTexturesModel(this))
, m_texturesModel(new ContentLibraryTexturesModel("Textures", this))
, m_environmentsModel(new ContentLibraryTexturesModel("Environments", this))
{
setWindowTitle(tr("Content Library", "Title of content library widget"));
setMinimumWidth(120);
@@ -100,8 +101,11 @@ ContentLibraryWidget::ContentLibraryWidget()
m_quickWidget->setClearColor(Theme::getColor(Theme::Color::DSpanelBackground));
QString textureBundlePath = findTextureBundlePath();
m_texturesModel->loadTextureBundle(textureBundlePath + "/Textures");
m_environmentsModel->loadTextureBundle(textureBundlePath + "/Environments");
QString baseUrl = QmlDesignerPlugin::settings()
.value(DesignerSettingsKey::DOWNLOADABLE_BUNDLES_URL)
.toString();
m_texturesModel->loadTextureBundle(textureBundlePath + "/Textures", baseUrl + "/Textures");
m_environmentsModel->loadTextureBundle(textureBundlePath + "/Environments", baseUrl + "/Environments");
m_quickWidget->rootContext()->setContextProperties({
{"rootView", QVariant::fromValue(this)},
@@ -290,4 +294,18 @@ QPointer<ContentLibraryTexturesModel> ContentLibraryWidget::environmentsModel()
return m_environmentsModel;
}
bool ContentLibraryWidget::markTextureDownloading()
{
if (m_anyTextureBeingDownloaded)
return false;
m_anyTextureBeingDownloaded = true;
return true; // let the caller know it can begin download
}
void ContentLibraryWidget::markNoTextureDownloading()
{
m_anyTextureBeingDownloaded = false; // allow other textures to be downloaded
}
} // namespace QmlDesigner

View File

@@ -59,6 +59,8 @@ public:
Q_INVOKABLE void addTexture(QmlDesigner::ContentLibraryTexture *tex);
Q_INVOKABLE void addLightProbe(QmlDesigner::ContentLibraryTexture *tex);
Q_INVOKABLE void updateSceneEnvState();
Q_INVOKABLE bool markTextureDownloading();
Q_INVOKABLE void markNoTextureDownloading();
signals:
void bundleMaterialDragStarted(QmlDesigner::ContentLibraryMaterial *bundleMat);
@@ -94,6 +96,7 @@ private:
bool m_hasMaterialLibrary = false;
bool m_hasQuick3DImport = false;
bool m_isDragging = false;
bool m_anyTextureBeingDownloaded = false;
};
} // namespace QmlDesigner

View File

@@ -33,17 +33,40 @@ void FileDownloader::start()
m_reply = Utils::NetworkAccessManager::instance()->get(request);
QNetworkReply::connect(m_reply, &QNetworkReply::readyRead, this, [this]() {
bool isDownloadingFile = false;
QString contentType;
if (!m_reply->hasRawHeader("Content-Type")) {
isDownloadingFile = true;
} else {
contentType = QString::fromUtf8(m_reply->rawHeader("Content-Type"));
if (contentType.startsWith("application/")
|| contentType.startsWith("image/")
|| contentType.startsWith("binary/")) {
isDownloadingFile = true;
} else {
qWarning() << "FileDownloader: Content type '" << contentType << "' is not supported";
}
}
if (isDownloadingFile)
m_tempFile.write(m_reply->readAll());
else
m_reply->close();
});
QNetworkReply::connect(m_reply,
&QNetworkReply::downloadProgress,
this,
[this](qint64 current, qint64 max) {
if (max == 0)
if (max <= 0) {
// NOTE: according to doc, we might have the second arg
// of QNetworkReply::downloadProgress less than 0.
return;
}
m_progress = current * 100 / max;
emit progressChanged();
});
@@ -74,6 +97,20 @@ void FileDownloader::start()
});
}
void FileDownloader::setProbeUrl(bool value)
{
if (m_probeUrl == value)
return;
m_probeUrl = value;
emit probeUrlChanged();
}
bool FileDownloader::probeUrl() const
{
return m_probeUrl;
}
void FileDownloader::cancel()
{
if (m_reply)
@@ -82,10 +119,13 @@ void FileDownloader::cancel()
void FileDownloader::setUrl(const QUrl &url)
{
if (m_url != url) {
m_url = url;
emit nameChanged();
emit urlChanged();
}
probeUrl();
if (m_probeUrl)
doProbeUrl();
}
QUrl FileDownloader::url() const
@@ -99,9 +139,10 @@ void FileDownloader::setDownloadEnabled(bool value)
return;
m_downloadEnabled = value;
emit downloadEnabledChanged();
if (!m_url.isEmpty())
probeUrl();
if (!m_url.isEmpty() && m_probeUrl)
doProbeUrl();
}
bool FileDownloader::downloadEnabled() const
@@ -127,8 +168,7 @@ QString FileDownloader::name() const
QString FileDownloader::completeBaseName() const
{
const QFileInfo fileInfo(m_url.path());
return fileInfo.completeBaseName();
return QFileInfo(m_url.path()).completeBaseName();
}
int FileDownloader::progress() const
@@ -151,8 +191,11 @@ bool FileDownloader::available() const
return m_available;
}
void FileDownloader::probeUrl()
void FileDownloader::doProbeUrl()
{
if (!m_probeUrl)
return;
if (!m_downloadEnabled) {
m_available = false;
emit availableChanged();

View File

@@ -13,8 +13,9 @@ class FileDownloader : public QObject
{
Q_OBJECT
Q_PROPERTY(bool downloadEnabled WRITE setDownloadEnabled READ downloadEnabled)
Q_PROPERTY(QUrl url READ url WRITE setUrl)
Q_PROPERTY(bool downloadEnabled WRITE setDownloadEnabled READ downloadEnabled NOTIFY downloadEnabledChanged)
Q_PROPERTY(QUrl url READ url WRITE setUrl NOTIFY urlChanged)
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)
@@ -42,6 +43,9 @@ public:
void setDownloadEnabled(bool value);
bool downloadEnabled() const;
void setProbeUrl(bool value);
bool probeUrl() const;
Q_INVOKABLE void start();
Q_INVOKABLE void cancel();
@@ -49,19 +53,23 @@ signals:
void finishedChanged();
void errorChanged();
void nameChanged();
void urlChanged();
void progressChanged();
void tempFileChanged();
void downloadFailed();
void lastModifiedChanged();
void availableChanged();
void downloadEnabledChanged();
void downloadStarting();
void downloadCanceled();
void probeUrlChanged();
private:
void probeUrl();
void doProbeUrl();
QUrl m_url;
bool m_probeUrl = false;
bool m_finished = false;
bool m_error = false;
int m_progress = 0;

View File

@@ -29,6 +29,40 @@ FileExtractor::FileExtractor(QObject *parent)
emit birthTimeChanged();
});
QObject::connect(
&m_timer, &QTimer::timeout, this, [this]() {
static QHash<QString, int> hash;
QDirIterator it(m_targetFolder, {"*.*"}, QDir::Files, QDirIterator::Subdirectories);
int count = 0;
while (it.hasNext()) {
if (!hash.contains(it.fileName())) {
m_currentFile = it.fileName();
hash.insert(m_currentFile, 0);
emit currentFileChanged();
}
it.next();
count++;
}
qint64 currentSize = m_bytesBefore
- QStorageInfo(m_targetPath.toFileInfo().dir()).bytesAvailable();
// We can not get the uncompressed size of the archive yet, that is why we use an
// approximation. We assume a 50% compression rate.
int progress = std::min(100ll, currentSize * 100 / m_compressedSize * 2);
if (progress >= 0) {
m_progress = progress;
emit progressChanged();
} else {
qWarning() << "FileExtractor has got negative progress. Likely due to QStorageInfo.";
}
m_size = QString::number(currentSize);
m_count = QString::number(count);
emit sizeChanged();
});
}
FileExtractor::~FileExtractor() {}
@@ -74,11 +108,37 @@ void FileExtractor::setArchiveName(QString &filePath)
emit targetFolderExistsChanged();
}
const QString FileExtractor::detailedText()
const QString FileExtractor::detailedText() const
{
return m_detailedText;
}
void FileExtractor::setClearTargetPathContents(bool value)
{
if (m_clearTargetPathContents != value) {
m_clearTargetPathContents = value;
emit clearTargetPathContentsChanged();
}
}
bool FileExtractor::clearTargetPathContents() const
{
return m_clearTargetPathContents;
}
void FileExtractor::setAlwaysCreateDir(bool value)
{
if (m_alwaysCreateDir != value) {
m_alwaysCreateDir = value;
emit alwaysCreateDirChanged();
}
}
bool FileExtractor::alwaysCreateDir() const
{
return m_alwaysCreateDir;
}
bool FileExtractor::finished() const
{
return m_finished;
@@ -126,51 +186,24 @@ QString FileExtractor::sourceFile() const
void FileExtractor::extract()
{
const QString targetFolder = m_targetPath.toString() + "/" + m_archiveName;
m_targetFolder = m_targetPath.toString() + "/" + m_archiveName;
// If the target directory already exists, remove it and its content
QDir targetDir(targetFolder);
if (targetDir.exists())
QDir targetDir(m_targetFolder);
if (targetDir.exists() && m_clearTargetPathContents)
targetDir.removeRecursively();
if (m_alwaysCreateDir) {
// Create a new directory to generate a proper creation date
targetDir.mkdir(targetFolder);
targetDir.mkdir(m_targetFolder);
}
Utils::Archive *archive = new Utils::Archive(m_sourceFile, m_targetPath);
QTC_ASSERT(archive->isValid(), delete archive; return);
m_timer.start();
qint64 bytesBefore = QStorageInfo(m_targetPath.toFileInfo().dir()).bytesAvailable();
qint64 compressedSize = QFileInfo(m_sourceFile.toString()).size();
QTimer::connect(
&m_timer, &QTimer::timeout, this, [this, bytesBefore, targetFolder, compressedSize]() {
static QHash<QString, int> hash;
QDirIterator it(targetFolder, {"*.*"}, QDir::Files, QDirIterator::Subdirectories);
int count = 0;
while (it.hasNext()) {
if (!hash.contains(it.fileName())) {
m_currentFile = it.fileName();
hash.insert(m_currentFile, 0);
emit currentFileChanged();
}
it.next();
count++;
}
qint64 currentSize = bytesBefore
- QStorageInfo(m_targetPath.toFileInfo().dir()).bytesAvailable();
// We can not get the uncompressed size of the archive yet, that is why we use an
// approximation. We assume a 50% compression rate.
m_progress = std::min(100ll, currentSize * 100 / compressedSize * 2);
emit progressChanged();
m_size = QString::number(currentSize);
m_count = QString::number(count);
emit sizeChanged();
});
m_bytesBefore = QStorageInfo(m_targetPath.toFileInfo().dir()).bytesAvailable();
m_compressedSize = QFileInfo(m_sourceFile.toString()).size();
QObject::connect(archive, &Utils::Archive::outputReceived, this, [this](const QString &output) {
m_detailedText += output;

View File

@@ -25,6 +25,8 @@ class FileExtractor : public QObject
Q_PROPERTY(bool targetFolderExists READ targetFolderExists NOTIFY targetFolderExistsChanged)
Q_PROPERTY(int progress READ progress NOTIFY progressChanged)
Q_PROPERTY(QDateTime birthTime READ birthTime NOTIFY birthTimeChanged)
Q_PROPERTY(bool clearTargetPathContents READ clearTargetPathContents WRITE setClearTargetPathContents NOTIFY clearTargetPathContentsChanged)
Q_PROPERTY(bool alwaysCreateDir READ alwaysCreateDir WRITE setAlwaysCreateDir NOTIFY alwaysCreateDirChanged)
public:
explicit FileExtractor(QObject *parent = nullptr);
@@ -36,7 +38,7 @@ public:
void setTargetPath(const QString &path);
void setSourceFile(QString &sourceFilePath);
void setArchiveName(QString &filePath);
const QString detailedText();
const QString detailedText() const;
bool finished() const;
QString currentFile() const;
QString size() const;
@@ -44,6 +46,10 @@ public:
bool targetFolderExists() const;
int progress() const;
QDateTime birthTime() const;
void setClearTargetPathContents(bool value);
bool clearTargetPathContents() const;
void setAlwaysCreateDir(bool value);
bool alwaysCreateDir() const;
QString sourceFile() const;
QString archiveName() const;
@@ -60,9 +66,12 @@ signals:
void targetFolderExistsChanged();
void progressChanged();
void birthTimeChanged();
void clearTargetPathContentsChanged();
void alwaysCreateDirChanged();
private:
Utils::FilePath m_targetPath;
QString m_targetFolder; // The same as m_targetPath, but with the archive name also.
Utils::FilePath m_sourceFile;
QString m_detailedText;
bool m_finished = false;
@@ -73,6 +82,11 @@ private:
QString m_archiveName;
int m_progress = 0;
QDateTime m_birthTime;
bool m_clearTargetPathContents = false;
bool m_alwaysCreateDir = false;
qint64 m_bytesBefore = 0;
qint64 m_compressedSize = 0;
};
} // QmlDesigner

View File

@@ -11,12 +11,15 @@
namespace QmlDesigner {
QString QmlDesigner::ImageUtils::imageInfo(const QString &path)
QString QmlDesigner::ImageUtils::imageInfo(const QString &path, bool fetchSizeInfo)
{
QFileInfo info(path);
if (!info.exists())
return {};
if (!fetchSizeInfo)
return QLatin1String("(%1)").arg(info.suffix());
int width = 0;
int height = 0;
const QString suffix = info.suffix();

View File

@@ -11,7 +11,7 @@ class ImageUtils
public:
ImageUtils();
static QString imageInfo(const QString &path);
static QString imageInfo(const QString &path, bool sizeInfo = true);
};
} // namespace QmlDesigner

View File

@@ -88,6 +88,8 @@ void DesignerSettings::fromSettings(QSettings *settings)
restoreValue(settings, DesignerSettingsKey::OLD_STATES_EDITOR, false);
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");
settings->endGroup();
settings->endGroup();

View File

@@ -60,6 +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";
}
class QMLDESIGNERBASE_EXPORT DesignerSettings

View File

@@ -108,40 +108,10 @@ DataModelDownloader::DataModelDownloader(QObject * /* parent */)
this,
&DataModelDownloader::targetPathMustChange);
}
}
bool DataModelDownloader::start()
{
if (!downloadEnabled()) {
m_available = false;
emit availableChanged();
return false;
}
m_fileDownloader.setDownloadEnabled(true);
m_fileDownloader.setUrl(QUrl::fromUserInput(
"https://download.qt.io/learning/examples/qtdesignstudio/dataImports.zip"));
bool started = false;
connect(&m_fileDownloader, &QmlDesigner::FileDownloader::availableChanged, this, [this, &started]() {
m_available = m_fileDownloader.available();
emit availableChanged();
if (!m_available) {
qWarning() << m_fileDownloader.url() << "failed to download";
return;
}
if (!m_forceDownload && (m_fileDownloader.lastModified() <= m_birthTime))
return;
started = true;
m_fileDownloader.start();
connect(&m_fileDownloader, &QmlDesigner::FileDownloader::finishedChanged, this, [this]() {
m_started = false;
if (m_fileDownloader.finished()) {
const Utils::FilePath archiveFile = Utils::FilePath::fromString(
m_fileDownloader.tempFile());
@@ -156,8 +126,43 @@ bool DataModelDownloader::start()
archive->unarchive();
}
});
});
return started;
}
void DataModelDownloader::onAvailableChanged()
{
m_available = m_fileDownloader.available();
emit availableChanged();
if (!m_available) {
qWarning() << m_fileDownloader.url() << "failed to download";
return;
}
if (!m_forceDownload && (m_fileDownloader.lastModified() <= m_birthTime))
return;
m_started = true;
m_fileDownloader.start();
}
bool DataModelDownloader::start()
{
if (!downloadEnabled()) {
m_available = false;
emit availableChanged();
return false;
}
m_fileDownloader.setDownloadEnabled(true);
m_fileDownloader.setUrl(QUrl::fromUserInput(
"https://download.qt.io/learning/examples/qtdesignstudio/dataImports.zip"));
m_started = false;
connect(&m_fileDownloader, &QmlDesigner::FileDownloader::availableChanged, this, &DataModelDownloader::onAvailableChanged);
return m_started;
}
bool DataModelDownloader::exists() const

View File

@@ -42,9 +42,11 @@ signals:
void targetPathMustChange(const QString &newPath);
private:
void onAvailableChanged();
QmlDesigner::FileDownloader m_fileDownloader;
QDateTime m_birthTime;
bool m_exists = false;
bool m_available = false;
bool m_forceDownload = false;
bool m_started = false;
};