forked from qt-creator/qt-creator
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:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
@@ -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)
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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();
|
||||
|
@@ -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;
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
||||
|
@@ -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();
|
||||
|
@@ -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
|
||||
|
@@ -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();
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
};
|
||||
|
Reference in New Issue
Block a user