summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.travis.yml3
-rw-r--r--application/CMakeLists.txt2
-rw-r--r--application/MainWindow.cpp3
-rw-r--r--application/MultiMC.cpp34
-rw-r--r--application/MultiMC.h5
-rw-r--r--application/main.cpp1
-rw-r--r--application/pages/VersionPage.cpp10
-rw-r--r--application/pages/global/AccountListPage.h2
-rw-r--r--application/resources/multimc/150x150/hourglass.pngbin0 -> 11831 bytes
-rw-r--r--application/resources/multimc/16x16/hourglass.pngbin0 -> 705 bytes
-rw-r--r--application/resources/multimc/22x22/hourglass.pngbin0 -> 1037 bytes
-rw-r--r--application/resources/multimc/32x32/hourglass.pngbin0 -> 1574 bytes
-rw-r--r--application/resources/multimc/48x48/hourglass.pngbin0 -> 2679 bytes
-rw-r--r--application/resources/multimc/index.theme3
-rw-r--r--application/resources/multimc/multimc.qrc7
-rw-r--r--application/widgets/ProgressWidget.cpp74
-rw-r--r--application/widgets/ProgressWidget.h32
-rw-r--r--logic/AbstractCommonModel.cpp133
-rw-r--r--logic/AbstractCommonModel.h462
-rw-r--r--logic/BaseConfigObject.cpp119
-rw-r--r--logic/BaseConfigObject.h50
-rw-r--r--logic/CMakeLists.txt30
-rw-r--r--logic/Env.cpp3
-rw-r--r--logic/Exception.h41
-rw-r--r--logic/FileSystem.cpp56
-rw-r--r--logic/FileSystem.h13
-rw-r--r--logic/Json.cpp278
-rw-r--r--logic/Json.h239
-rw-r--r--logic/MMCError.h25
-rw-r--r--logic/MMCJson.cpp142
-rw-r--r--logic/MMCJson.h101
-rw-r--r--logic/QObjectPtr.h5
-rw-r--r--logic/forge/ForgeInstaller.cpp3
-rw-r--r--logic/liteloader/LiteLoaderInstaller.cpp3
-rw-r--r--logic/liteloader/LiteLoaderVersionList.cpp4
-rw-r--r--logic/minecraft/JarMod.cpp4
-rw-r--r--logic/minecraft/MinecraftProfile.cpp7
-rw-r--r--logic/minecraft/MinecraftVersionList.cpp31
-rw-r--r--logic/minecraft/OneSixInstance.cpp3
-rw-r--r--logic/minecraft/OneSixProfileStrategy.cpp4
-rw-r--r--logic/minecraft/OneSixUpdate.cpp3
-rw-r--r--logic/minecraft/ParseUtils.cpp1
-rw-r--r--logic/minecraft/ProfileUtils.cpp10
-rw-r--r--logic/minecraft/RawLibrary.cpp6
-rw-r--r--logic/minecraft/VersionBuildError.h8
-rw-r--r--logic/minecraft/VersionFile.cpp4
-rw-r--r--logic/minecraft/VersionFile.h3
-rw-r--r--logic/net/CacheDownload.h29
-rw-r--r--logic/resources/IconResourceHandler.cpp60
-rw-r--r--logic/resources/IconResourceHandler.h22
-rw-r--r--logic/resources/Resource.cpp121
-rw-r--r--logic/resources/Resource.h116
-rw-r--r--logic/resources/ResourceHandler.cpp28
-rw-r--r--logic/resources/ResourceHandler.h33
-rw-r--r--logic/resources/ResourceObserver.cpp55
-rw-r--r--logic/resources/ResourceObserver.h67
-rw-r--r--logic/resources/ResourceProxyModel.cpp103
-rw-r--r--logic/resources/ResourceProxyModel.h36
-rw-r--r--logic/resources/WebResourceHandler.cpp67
-rw-r--r--logic/resources/WebResourceHandler.h23
-rw-r--r--logic/tasks/StandardTask.cpp120
-rw-r--r--logic/tasks/StandardTask.h43
-rw-r--r--logic/tasks/Task.cpp1
-rw-r--r--logic/tasks/Task.h2
-rw-r--r--tests/tst_Resource.cpp101
65 files changed, 2661 insertions, 333 deletions
diff --git a/.travis.yml b/.travis.yml
index ad0bdee5..9ed7a045 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -17,7 +17,6 @@ before_script:
- cd build
- cmake -DCMAKE_PREFIX_PATH=/opt/qt53/lib/cmake ..
script:
- - make -j4
- - make test ARGS="-V"
+ - make -j4 && make test ARGS="-V"
notifications:
email: false
diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt
index d7cb5777..d3962819 100644
--- a/application/CMakeLists.txt
+++ b/application/CMakeLists.txt
@@ -251,6 +251,8 @@ SET(MULTIMC_SOURCES
widgets/ServerStatus.h
widgets/VersionListView.cpp
widgets/VersionListView.h
+ widgets/ProgressWidget.h
+ widgets/ProgressWidget.cpp
# GUI - instance group view
diff --git a/application/MainWindow.cpp b/application/MainWindow.cpp
index 9ff120bd..99c94bf8 100644
--- a/application/MainWindow.cpp
+++ b/application/MainWindow.cpp
@@ -383,6 +383,7 @@ namespace Ui {
#include "JavaCommon.h"
#include "InstancePageProvider.h"
#include "minecraft/SkinUtils.h"
+#include "resources/Resource.h"
//#include "minecraft/LegacyInstance.h"
@@ -1758,7 +1759,7 @@ void MainWindow::launchInstance(InstancePtr instance, AuthSessionPtr session,
this->hide();
console = new ConsoleWindow(proc);
- connect(console, SIGNAL(isClosing()), this, SLOT(instanceEnded()));
+ connect(console, &ConsoleWindow::isClosing, this, &MainWindow::instanceEnded);
proc->setHeader("MultiMC version: " + BuildConfig.printableVersionString() + "\n\n");
proc->arm();
diff --git a/application/MultiMC.cpp b/application/MultiMC.cpp
index 39cc8503..2c6b387c 100644
--- a/application/MultiMC.cpp
+++ b/application/MultiMC.cpp
@@ -40,6 +40,8 @@
#include "settings/Setting.h"
#include "trans/TranslationDownloader.h"
+#include "resources/Resource.h"
+#include "resources/IconResourceHandler.h"
#include "ftb/FTBPlugin.h"
@@ -331,6 +333,37 @@ void MultiMC::initIcons()
{
ENV.m_icons->directoryChanged(value.toString());
});
+
+ Resource::registerTransformer([](const QVariantMap &map) -> QIcon
+ {
+ QIcon icon;
+ for (auto it = map.constBegin(); it != map.constEnd(); ++it)
+ {
+ icon.addFile(it.key(), QSize(it.value().toInt(), it.value().toInt()));
+ }
+ return icon;
+ });
+ Resource::registerTransformer([](const QVariantMap &map) -> QPixmap
+ {
+ QVariantList sizes = map.values();
+ if (sizes.isEmpty())
+ {
+ return QPixmap();
+ }
+ std::sort(sizes.begin(), sizes.end());
+ if (sizes.last().toInt() != -1) // only scalable available
+ {
+ return QPixmap(map.key(sizes.last()));
+ }
+ else
+ {
+ return QPixmap();
+ }
+ });
+ Resource::registerTransformer([](const QByteArray &data) -> QPixmap
+ { return QPixmap::fromImage(QImage::fromData(data)); });
+ Resource::registerTransformer([](const QByteArray &data) -> QIcon
+ { return QIcon(QPixmap::fromImage(QImage::fromData(data))); });
}
@@ -610,6 +643,7 @@ void MultiMC::installUpdates(const QString updateFilesDir, UpdateFlags flags)
void MultiMC::setIconTheme(const QString& name)
{
XdgIcon::setThemeName(name);
+ IconResourceHandler::setTheme(name);
}
QIcon MultiMC::getThemedIcon(const QString& name)
diff --git a/application/MultiMC.h b/application/MultiMC.h
index 8215e4ad..e4a54115 100644
--- a/application/MultiMC.h
+++ b/application/MultiMC.h
@@ -146,13 +146,10 @@ private slots:
private:
void initLogger();
-
void initIcons();
-
void initGlobalSettings(bool test_mode);
-
void initTranslations();
- void initSSL();
+ void initSSL();
private:
friend class UpdateCheckerTest;
diff --git a/application/main.cpp b/application/main.cpp
index 111a61ac..12c97f09 100644
--- a/application/main.cpp
+++ b/application/main.cpp
@@ -13,7 +13,6 @@ int main_gui(MultiMC &app)
mainWin.checkInstancePathForProblems();
return app.exec();
}
-
int main(int argc, char *argv[])
{
// initialize Qt
diff --git a/application/pages/VersionPage.cpp b/application/pages/VersionPage.cpp
index cbb5c107..efc0b446 100644
--- a/application/pages/VersionPage.cpp
+++ b/application/pages/VersionPage.cpp
@@ -47,7 +47,7 @@
#include <minecraft/MinecraftVersion.h>
#include <minecraft/MinecraftVersionList.h>
#include "icons/IconList.h"
-
+#include "Exception.h"
QIcon VersionPage::icon() const
{
@@ -118,7 +118,7 @@ bool VersionPage::reloadMinecraftProfile()
m_inst->reloadProfile();
return true;
}
- catch (MMCError &e)
+ catch (Exception &e)
{
QMessageBox::critical(this, tr("Error"), e.cause());
return false;
@@ -199,7 +199,7 @@ void VersionPage::on_resetOrderBtn_clicked()
{
m_version->resetOrder();
}
- catch (MMCError &e)
+ catch (Exception &e)
{
QMessageBox::critical(this, tr("Error"), e.cause());
}
@@ -212,7 +212,7 @@ void VersionPage::on_moveUpBtn_clicked()
{
m_version->move(currentRow(), MinecraftProfile::MoveUp);
}
- catch (MMCError &e)
+ catch (Exception &e)
{
QMessageBox::critical(this, tr("Error"), e.cause());
}
@@ -225,7 +225,7 @@ void VersionPage::on_moveDownBtn_clicked()
{
m_version->move(currentRow(), MinecraftProfile::MoveDown);
}
- catch (MMCError &e)
+ catch (Exception &e)
{
QMessageBox::critical(this, tr("Error"), e.cause());
}
diff --git a/application/pages/global/AccountListPage.h b/application/pages/global/AccountListPage.h
index bfadc1bd..7803e044 100644
--- a/application/pages/global/AccountListPage.h
+++ b/application/pages/global/AccountListPage.h
@@ -21,7 +21,7 @@
#include "pages/BasePage.h"
#include "auth/MojangAccountList.h"
-#include <MultiMC.h>
+#include "MultiMC.h"
namespace Ui
{
diff --git a/application/resources/multimc/150x150/hourglass.png b/application/resources/multimc/150x150/hourglass.png
new file mode 100644
index 00000000..f2623d1e
--- /dev/null
+++ b/application/resources/multimc/150x150/hourglass.png
Binary files differ
diff --git a/application/resources/multimc/16x16/hourglass.png b/application/resources/multimc/16x16/hourglass.png
new file mode 100644
index 00000000..ab36234b
--- /dev/null
+++ b/application/resources/multimc/16x16/hourglass.png
Binary files differ
diff --git a/application/resources/multimc/22x22/hourglass.png b/application/resources/multimc/22x22/hourglass.png
new file mode 100644
index 00000000..8cb343ac
--- /dev/null
+++ b/application/resources/multimc/22x22/hourglass.png
Binary files differ
diff --git a/application/resources/multimc/32x32/hourglass.png b/application/resources/multimc/32x32/hourglass.png
new file mode 100644
index 00000000..a558ec99
--- /dev/null
+++ b/application/resources/multimc/32x32/hourglass.png
Binary files differ
diff --git a/application/resources/multimc/48x48/hourglass.png b/application/resources/multimc/48x48/hourglass.png
new file mode 100644
index 00000000..8f10ab7a
--- /dev/null
+++ b/application/resources/multimc/48x48/hourglass.png
Binary files differ
diff --git a/application/resources/multimc/index.theme b/application/resources/multimc/index.theme
index 5f7d3f3f..a21fea2c 100644
--- a/application/resources/multimc/index.theme
+++ b/application/resources/multimc/index.theme
@@ -35,6 +35,9 @@ Size=64
[256x256]
Size=256
+[150x150]
+Size=150
+
[scalable]
Size=48
Type=Scalable
diff --git a/application/resources/multimc/multimc.qrc b/application/resources/multimc/multimc.qrc
index 4ced586a..31a7b44f 100644
--- a/application/resources/multimc/multimc.qrc
+++ b/application/resources/multimc/multimc.qrc
@@ -207,6 +207,13 @@
<file>48x48/log.png</file>
<file>64x64/log.png</file>
+ <!-- Hour glass, CC-BY, http://findicons.com/icon/84653/hourglass?id=360551 -->
+ <file>16x16/hourglass.png</file>
+ <file>22x22/hourglass.png</file>
+ <file>32x32/hourglass.png</file>
+ <file>48x48/hourglass.png</file>
+ <file>150x150/hourglass.png</file>
+
<!-- placeholder when loading screenshot images -->
<file>scalable/screenshot-placeholder.svg</file>
</qresource>
diff --git a/application/widgets/ProgressWidget.cpp b/application/widgets/ProgressWidget.cpp
new file mode 100644
index 00000000..7b51eca0
--- /dev/null
+++ b/application/widgets/ProgressWidget.cpp
@@ -0,0 +1,74 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#include "ProgressWidget.h"
+
+#include <QProgressBar>
+#include <QLabel>
+#include <QVBoxLayout>
+#include <QEventLoop>
+
+#include "tasks/Task.h"
+
+ProgressWidget::ProgressWidget(QWidget *parent)
+ : QWidget(parent)
+{
+ m_label = new QLabel(this);
+ m_label->setWordWrap(true);
+ m_bar = new QProgressBar(this);
+ m_bar->setMinimum(0);
+ m_bar->setMaximum(100);
+ QVBoxLayout *layout = new QVBoxLayout(this);
+ layout->addWidget(m_label);
+ layout->addWidget(m_bar);
+ layout->addStretch();
+ setLayout(layout);
+}
+
+void ProgressWidget::start(std::shared_ptr<Task> task)
+{
+ if (m_task)
+ {
+ disconnect(m_task.get(), 0, this, 0);
+ }
+ m_task = task;
+ connect(m_task.get(), &Task::finished, this, &ProgressWidget::handleTaskFinish);
+ connect(m_task.get(), &Task::status, this, &ProgressWidget::handleTaskStatus);
+ connect(m_task.get(), &Task::progress, this, &ProgressWidget::handleTaskProgress);
+ connect(m_task.get(), &Task::destroyed, this, &ProgressWidget::taskDestroyed);
+ if (!m_task->isRunning())
+ {
+ QMetaObject::invokeMethod(m_task.get(), "start", Qt::QueuedConnection);
+ }
+}
+bool ProgressWidget::exec(std::shared_ptr<Task> task)
+{
+ QEventLoop loop;
+ connect(task.get(), &Task::finished, &loop, &QEventLoop::quit);
+ start(task);
+ if (task->isRunning())
+ {
+ loop.exec();
+ }
+ return task->successful();
+}
+
+void ProgressWidget::handleTaskFinish()
+{
+ if (!m_task->successful())
+ {
+ m_label->setText(m_task->failReason());
+ }
+}
+void ProgressWidget::handleTaskStatus(const QString &status)
+{
+ m_label->setText(status);
+}
+void ProgressWidget::handleTaskProgress(qint64 current, qint64 total)
+{
+ m_bar->setMaximum(total);
+ m_bar->setValue(current);
+}
+void ProgressWidget::taskDestroyed()
+{
+ m_task = nullptr;
+}
diff --git a/application/widgets/ProgressWidget.h b/application/widgets/ProgressWidget.h
new file mode 100644
index 00000000..08d8a157
--- /dev/null
+++ b/application/widgets/ProgressWidget.h
@@ -0,0 +1,32 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#pragma once
+
+#include <QWidget>
+#include <memory>
+
+class Task;
+class QProgressBar;
+class QLabel;
+
+class ProgressWidget : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit ProgressWidget(QWidget *parent = nullptr);
+
+public slots:
+ void start(std::shared_ptr<Task> task);
+ bool exec(std::shared_ptr<Task> task);
+
+private slots:
+ void handleTaskFinish();
+ void handleTaskStatus(const QString &status);
+ void handleTaskProgress(qint64 current, qint64 total);
+ void taskDestroyed();
+
+private:
+ QLabel *m_label;
+ QProgressBar *m_bar;
+ std::shared_ptr<Task> m_task;
+};
diff --git a/logic/AbstractCommonModel.cpp b/logic/AbstractCommonModel.cpp
new file mode 100644
index 00000000..71d75829
--- /dev/null
+++ b/logic/AbstractCommonModel.cpp
@@ -0,0 +1,133 @@
+/* Copyright 2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "AbstractCommonModel.h"
+
+BaseAbstractCommonModel::BaseAbstractCommonModel(const Qt::Orientation orientation, QObject *parent)
+ : QAbstractListModel(parent), m_orientation(orientation)
+{
+}
+
+int BaseAbstractCommonModel::rowCount(const QModelIndex &parent) const
+{
+ return m_orientation == Qt::Horizontal ? entryCount() : size();
+}
+int BaseAbstractCommonModel::columnCount(const QModelIndex &parent) const
+{
+ return m_orientation == Qt::Horizontal ? size() : entryCount();
+}
+QVariant BaseAbstractCommonModel::data(const QModelIndex &index, int role) const
+{
+ if (!hasIndex(index.row(), index.column(), index.parent()))
+ {
+ return QVariant();
+ }
+ const int i = m_orientation == Qt::Horizontal ? index.column() : index.row();
+ const int entry = m_orientation == Qt::Horizontal ? index.row() : index.column();
+ return formatData(i, role, get(i, entry, role));
+}
+QVariant BaseAbstractCommonModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (orientation != m_orientation && role == Qt::DisplayRole)
+ {
+ return entryTitle(section);
+ }
+ else
+ {
+ return QVariant();
+ }
+}
+bool BaseAbstractCommonModel::setData(const QModelIndex &index, const QVariant &value, int role)
+{
+ const int i = m_orientation == Qt::Horizontal ? index.column() : index.row();
+ const int entry = m_orientation == Qt::Horizontal ? index.row() : index.column();
+ const bool result = set(i, entry, role, sanetizeData(i, role, value));
+ if (result)
+ {
+ emit dataChanged(index, index, QVector<int>() << role);
+ }
+ return result;
+}
+Qt::ItemFlags BaseAbstractCommonModel::flags(const QModelIndex &index) const
+{
+ if (!hasIndex(index.row(), index.column(), index.parent()))
+ {
+ return Qt::NoItemFlags;
+ }
+
+ const int entry = m_orientation == Qt::Horizontal ? index.row() : index.column();
+ if (canSet(entry))
+ {
+ return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEnabled;
+ }
+ else
+ {
+ return Qt::ItemIsEnabled | Qt::ItemIsSelectable;
+ }
+}
+
+void BaseAbstractCommonModel::notifyAboutToAddObject(const int at)
+{
+ if (m_orientation == Qt::Horizontal)
+ {
+ beginInsertColumns(QModelIndex(), at, at);
+ }
+ else
+ {
+ beginInsertRows(QModelIndex(), at, at);
+ }
+}
+void BaseAbstractCommonModel::notifyObjectAdded()
+{
+ if (m_orientation == Qt::Horizontal)
+ {
+ endInsertColumns();
+ }
+ else
+ {
+ endInsertRows();
+ }
+}
+void BaseAbstractCommonModel::notifyAboutToRemoveObject(const int at)
+{
+ if (m_orientation == Qt::Horizontal)
+ {
+ beginRemoveColumns(QModelIndex(), at, at);
+ }
+ else
+ {
+ beginRemoveRows(QModelIndex(), at, at);
+ }
+}
+void BaseAbstractCommonModel::notifyObjectRemoved()
+{
+ if (m_orientation == Qt::Horizontal)
+ {
+ endRemoveColumns();
+ }
+ else
+ {
+ endRemoveRows();
+ }
+}
+
+void BaseAbstractCommonModel::notifyBeginReset()
+{
+ beginResetModel();
+}
+void BaseAbstractCommonModel::notifyEndReset()
+{
+ endResetModel();
+}
diff --git a/logic/AbstractCommonModel.h b/logic/AbstractCommonModel.h
new file mode 100644
index 00000000..31b86a23
--- /dev/null
+++ b/logic/AbstractCommonModel.h
@@ -0,0 +1,462 @@
+/* Copyright 2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QAbstractListModel>
+#include <type_traits>
+#include <functional>
+#include <memory>
+
+class BaseAbstractCommonModel : public QAbstractListModel
+{
+ Q_OBJECT
+public:
+ explicit BaseAbstractCommonModel(const Qt::Orientation orientation, QObject *parent = nullptr);
+
+ // begin QAbstractItemModel interface
+ int rowCount(const QModelIndex &parent = QModelIndex()) const override;
+ int columnCount(const QModelIndex &parent = QModelIndex()) const override;
+ QVariant data(const QModelIndex &index, int role) const override;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const override;
+ bool setData(const QModelIndex &index, const QVariant &value, int role) override;
+ Qt::ItemFlags flags(const QModelIndex &index) const override;
+ // end QAbstractItemModel interface
+
+ virtual int size() const = 0;
+ virtual int entryCount() const = 0;
+
+ virtual QVariant formatData(const int index, int role, const QVariant &data) const { return data; }
+ virtual QVariant sanetizeData(const int index, int role, const QVariant &data) const { return data; }
+
+protected:
+ virtual QVariant get(const int index, const int entry, const int role) const = 0;
+ virtual bool set(const int index, const int entry, const int role, const QVariant &value) = 0;
+ virtual bool canSet(const int entry) const = 0;
+ virtual QString entryTitle(const int entry) const = 0;
+
+ void notifyAboutToAddObject(const int at);
+ void notifyObjectAdded();
+ void notifyAboutToRemoveObject(const int at);
+ void notifyObjectRemoved();
+ void notifyBeginReset();
+ void notifyEndReset();
+
+ const Qt::Orientation m_orientation;
+};
+
+template<typename Object>
+class AbstractCommonModel : public BaseAbstractCommonModel
+{
+public:
+ explicit AbstractCommonModel(const Qt::Orientation orientation)
+ : BaseAbstractCommonModel(orientation) {}
+ virtual ~AbstractCommonModel() {}
+
+ int size() const override { return m_objects.size(); }
+ int entryCount() const override { return m_entries.size(); }
+
+ void append(const Object &object)
+ {
+ notifyAboutToAddObject(size());
+ m_objects.append(object);
+ notifyObjectAdded();
+ }
+ void prepend(const Object &object)
+ {
+ notifyAboutToAddObject(0);
+ m_objects.prepend(object);
+ notifyObjectAdded();
+ }
+ void insert(const Object &object, const int index)
+ {
+ if (index >= size())
+ {
+ prepend(object);
+ }
+ else if (index <= 0)
+ {
+ append(object);
+ }
+ else
+ {
+ notifyAboutToAddObject(index);
+ m_objects.insert(index, object);
+ notifyObjectAdded();
+ }
+ }
+ void remove(const int index)
+ {
+ notifyAboutToRemoveObject(index);
+ m_objects.removeAt(index);
+ notifyObjectRemoved();
+ }
+ Object get(const int index) const
+ {
+ return m_objects.at(index);
+ }
+
+private:
+ friend class CommonModel;
+ QVariant get(const int index, const int entry, const int role) const override
+ {
+ if (m_entries.size() < entry || !m_entries[entry].second.contains(role))
+ {
+ return QVariant();
+ }
+ return m_entries[entry].second.value(role)->get(m_objects.at(index));
+ }
+ bool set(const int index, const int entry, const int role, const QVariant &value) override
+ {
+ if (m_entries.size() < entry || !m_entries[entry].second.contains(role))
+ {
+ return false;
+ }
+ IEntry *e = m_entries[entry].second.value(role);
+ if (!e->canSet())
+ {
+ return false;
+ }
+ e->set(m_objects[index], value);
+ return true;
+ }
+ bool canSet(const int entry) const override
+ {
+ if (m_entries.size() < entry || !m_entries[entry].second.contains(Qt::EditRole))
+ {
+ return false;
+ }
+ IEntry *e = m_entries[entry].second.value(Qt::EditRole);
+ return e->canSet();
+ }
+
+ QString entryTitle(const int entry) const override
+ {
+ return m_entries.at(entry).first;
+ }
+
+private:
+ struct IEntry
+ {
+ virtual ~IEntry() {}
+ virtual void set(Object &object, const QVariant &value) = 0;
+ virtual QVariant get(const Object &object) const = 0;
+ virtual bool canSet() const = 0;
+ };
+ template<typename T>
+ struct VariableEntry : public IEntry
+ {
+ typedef T (Object::*Member);
+
+ explicit VariableEntry(Member member)
+ : m_member(member) {}
+
+ void set(Object &object, const QVariant &value) override
+ {
+ object.*m_member = value.value<T>();
+ }
+ QVariant get(const Object &object) const override
+ {
+ return QVariant::fromValue<T>(object.*m_member);
+ }
+ bool canSet() const override { return true; }
+
+ private:
+ Member m_member;
+ };
+ template<typename T>
+ struct FunctionEntry : public IEntry
+ {
+ typedef T (Object::*Getter)() const;
+ typedef void (Object::*Setter)(T);
+
+ explicit FunctionEntry(Getter getter, Setter setter)
+ : m_getter(m_getter), m_setter(m_setter) {}
+
+ void set(Object &object, const QVariant &value) override
+ {
+ object.*m_setter(value.value<T>());
+ }
+ QVariant get(const Object &object) const override
+ {
+ return QVariant::fromValue<T>(object.*m_getter());
+ }
+ bool canSet() const override { return !!m_setter; }
+
+ private:
+ Getter m_getter;
+ Setter m_setter;
+ };
+
+ QList<Object> m_objects;
+ QVector<QPair<QString, QMap<int, IEntry *>>> m_entries;
+
+ void addEntryInternal(IEntry *e, const int entry, const int role)
+ {
+ if (m_entries.size() <= entry)
+ {
+ m_entries.resize(entry + 1);
+ }
+ m_entries[entry].second.insert(role, e);
+ }
+
+protected:
+ template<typename Getter, typename Setter>
+ typename std::enable_if<std::is_member_function_pointer<Getter>::value && std::is_member_function_pointer<Getter>::value, void>::type
+ addEntry(Getter getter, Setter setter, const int entry, const int role)
+ {
+ addEntryInternal(new FunctionEntry<typename std::result_of<Getter>::type>(getter, setter), entry, role);
+ }
+ template<typename Getter>
+ typename std::enable_if<std::is_member_function_pointer<Getter>::value, void>::type
+ addEntry(Getter getter, const int entry, const int role)
+ {
+ addEntryInternal(new FunctionEntry<typename std::result_of<Getter>::type>(getter, nullptr), entry, role);
+ }
+ template<typename T>
+ typename std::enable_if<!std::is_member_function_pointer<T (Object::*)>::value, void>::type
+ addEntry(T (Object::*member), const int entry, const int role)
+ {
+ addEntryInternal(new VariableEntry<T>(member), entry, role);
+ }
+
+ void setEntryTitle(const int entry, const QString &title)
+ {
+ m_entries[entry].first = title;
+ }
+};
+template<typename Object>
+class AbstractCommonModel<Object *> : public BaseAbstractCommonModel
+{
+public:
+ explicit AbstractCommonModel(const Qt::Orientation orientation)
+ : BaseAbstractCommonModel(orientation) {}
+ virtual ~AbstractCommonModel()
+ {
+ qDeleteAll(m_objects);
+ }
+
+ int size() const override { return m_objects.size(); }
+ int entryCount() const override { return m_entries.size(); }
+
+ void append(Object *object)
+ {
+ notifyAboutToAddObject(size());
+ m_objects.append(object);
+ notifyObjectAdded();
+ }
+ void prepend(Object *object)
+ {
+ notifyAboutToAddObject(0);
+ m_objects.prepend(object);
+ notifyObjectAdded();
+ }
+ void insert(Object *object, const int index)
+ {
+ if (index >= size())
+ {
+ prepend(object);
+ }
+ else if (index <= 0)
+ {
+ append(object);
+ }
+ else
+ {
+ notifyAboutToAddObject(index);
+ m_objects.insert(index, object);
+ notifyObjectAdded();
+ }
+ }
+ void remove(const int index)
+ {
+ notifyAboutToRemoveObject(index);
+ m_objects.removeAt(index);
+ notifyObjectRemoved();
+ }
+ Object *get(const int index) const
+ {
+ return m_objects.at(index);
+ }
+ int find(Object * const obj) const
+ {
+ return m_objects.indexOf(obj);
+ }
+
+ QList<Object *> getAll() const
+ {
+ return m_objects;
+ }
+
+private:
+ friend class CommonModel;
+ QVariant get(const int index, const int entry, const int role) const override
+ {
+ if (m_entries.size() < entry || !m_entries[entry].second.contains(role))
+ {
+ return QVariant();
+ }
+ return m_entries[entry].second.value(role)->get(m_objects.at(index));
+ }
+ bool set(const int index, const int entry, const int role, const QVariant &value) override
+ {
+ if (m_entries.size() < entry || !m_entries[entry].second.contains(role))
+ {
+ return false;
+ }
+ IEntry *e = m_entries[entry].second.value(role);
+ if (!e->canSet())
+ {
+ return false;
+ }
+ e->set(m_objects[index], value);
+ return true;
+ }
+ bool canSet(const int entry) const override
+ {
+ if (m_entries.size() < entry || !m_entries[entry].second.contains(Qt::EditRole))
+ {
+ return false;
+ }
+ IEntry *e = m_entries[entry].second.value(Qt::EditRole);
+ return e->canSet();
+ }
+
+ QString entryTitle(const int entry) const override
+ {
+ return m_entries.at(entry).first;
+ }
+
+private:
+ struct IEntry
+ {
+ virtual ~IEntry() {}
+ virtual void set(Object *object, const QVariant &value) = 0;
+ virtual QVariant get(Object *object) const = 0;
+ virtual bool canSet() const = 0;
+ };
+ template<typename T>
+ struct VariableEntry : public IEntry
+ {
+ typedef T (Object::*Member);
+
+ explicit VariableEntry(Member member)
+ : m_member(member) {}
+
+ void set(Object *object, const QVariant &value) override
+ {
+ object->*m_member = value.value<T>();
+ }
+ QVariant get(Object *object) const override
+ {
+ return QVariant::fromValue<T>(object->*m_member);
+ }
+ bool canSet() const override { return true; }
+
+ private:
+ Member m_member;
+ };
+ template<typename T>
+ struct FunctionEntry : public IEntry
+ {
+ typedef T (Object::*Getter)() const;
+ typedef void (Object::*Setter)(T);
+
+ explicit FunctionEntry(Getter getter, Setter setter)
+ : m_getter(getter), m_setter(setter) {}
+
+ void set(Object *object, const QVariant &value) override
+ {
+ (object->*m_setter)(value.value<T>());
+ }
+ QVariant get(Object *object) const override
+ {
+ return QVariant::fromValue<T>((object->*m_getter)());
+ }
+ bool canSet() const override { return !!m_setter; }
+
+ private:
+ Getter m_getter;
+ Setter m_setter;
+ };
+ template<typename T>
+ struct LambdaEntry : public IEntry
+ {
+ using Getter = std::function<T(Object *)>;
+
+ explicit LambdaEntry(Getter getter)
+ : m_getter(getter) {}
+
+ void set(Object *object, const QVariant &value) override {}
+ QVariant get(Object *object) const override
+ {
+ return QVariant::fromValue<T>(m_getter(object));
+ }
+ bool canSet() const override { return false; }
+
+ private:
+ Getter m_getter;
+ };
+
+ QList<Object *> m_objects;
+ QVector<QPair<QString, QMap<int, IEntry *>>> m_entries;
+
+ void addEntryInternal(IEntry *e, const int entry, const int role)
+ {
+ if (m_entries.size() <= entry)
+ {
+ m_entries.resize(entry + 1);
+ }
+ m_entries[entry].second.insert(role, e);
+ }
+
+protected:
+ template<typename Getter, typename Setter>
+ typename std::enable_if<std::is_member_function_pointer<Getter>::value && std::is_member_function_pointer<Getter>::value, void>::type
+ addEntry(const int entry, const int role, Getter getter, Setter setter)
+ {
+ addEntryInternal(new FunctionEntry<typename std::result_of<Getter>::type>(getter, setter), entry, role);
+ }
+ template<typename T>
+ typename std::enable_if<std::is_member_function_pointer<typename FunctionEntry<T>::Getter>::value, void>::type
+ addEntry(const int entry, const int role, typename FunctionEntry<T>::Getter getter)
+ {
+ addEntryInternal(new FunctionEntry<T>(getter, nullptr), entry, role);
+ }
+ template<typename T>
+ typename std::enable_if<!std::is_member_function_pointer<T (Object::*)>::value, void>::type
+ addEntry(const int entry, const int role, T (Object::*member))
+ {
+ addEntryInternal(new VariableEntry<T>(member), entry, role);
+ }
+ template<typename T>
+ void addEntry(const int entry, const int role, typename LambdaEntry<T>::Getter lambda)
+ {
+ addEntryInternal(new LambdaEntry<T>(lambda), entry, role);
+ }
+
+ void setEntryTitle(const int entry, const QString &title)
+ {
+ m_entries[entry].first = title;
+ }
+
+ void setAll(const QList<Object *> objects)
+ {
+ notifyBeginReset();
+ qDeleteAll(m_objects);
+ m_objects = objects;
+ notifyEndReset();
+ }
+};
diff --git a/logic/BaseConfigObject.cpp b/logic/BaseConfigObject.cpp
new file mode 100644
index 00000000..ff698ad0
--- /dev/null
+++ b/logic/BaseConfigObject.cpp
@@ -0,0 +1,119 @@
+/* Copyright 2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "BaseConfigObject.h"
+
+#include <QTimer>
+#include <QSaveFile>
+#include <QFile>
+#include <QCoreApplication>
+#include <QDebug>
+
+#include "Exception.h"
+
+BaseConfigObject::BaseConfigObject(const QString &filename)
+ : m_filename(filename)
+{
+ m_saveTimer = new QTimer;
+ m_saveTimer->setSingleShot(true);
+ // cppcheck-suppress pureVirtualCall
+ QObject::connect(m_saveTimer, &QTimer::timeout, [this](){saveNow();});
+ setSaveTimeout(250);
+
+ m_initialReadTimer = new QTimer;
+ m_initialReadTimer->setSingleShot(true);
+ QObject::connect(m_initialReadTimer, &QTimer::timeout, [this]()
+ {
+ loadNow();
+ m_initialReadTimer->deleteLater();
+ m_initialReadTimer = 0;
+ });
+ m_initialReadTimer->start(0);
+
+ // cppcheck-suppress pureVirtualCall
+ m_appQuitConnection = QObject::connect(qApp, &QCoreApplication::aboutToQuit, [this](){saveNow();});
+}
+BaseConfigObject::~BaseConfigObject()
+{
+ delete m_saveTimer;
+ if (m_initialReadTimer)
+ {
+ delete m_initialReadTimer;
+ }
+ QObject::disconnect(m_appQuitConnection);
+}
+
+void BaseConfigObject::setSaveTimeout(int msec)
+{
+ m_saveTimer->setInterval(msec);
+}
+
+void BaseConfigObject::scheduleSave()
+{
+ m_saveTimer->stop();
+ m_saveTimer->start();
+}
+void BaseConfigObject::saveNow()
+{
+ if (m_saveTimer->isActive())
+ {
+ m_saveTimer->stop();
+ }
+ if (m_disableSaving)
+ {
+ return;
+ }
+
+ QSaveFile file(m_filename);
+ if (!file.open(QFile::WriteOnly))
+ {
+ qWarning() << "Couldn't open" << m_filename << "for writing:" << file.errorString();
+ return;
+ }
+ // cppcheck-suppress pureVirtualCall
+ file.write(doSave());
+
+ if (!file.commit())
+ {
+ qCritical() << "Unable to commit the file" << file.fileName() << ":" << file.errorString();
+ file.cancelWriting();
+ }
+}
+void BaseConfigObject::loadNow()
+{
+ if (m_saveTimer->isActive())
+ {
+ saveNow();
+ }
+
+ QFile file(m_filename);
+ if (!file.exists())
+ {
+ return;
+ }
+ if (!file.open(QFile::ReadOnly))
+ {
+ qWarning() << "Couldn't open" << m_filename << "for reading:" << file.errorString();
+ return;
+ }
+ try
+ {
+ doLoad(file.readAll());
+ }
+ catch (Exception &e)
+ {
+ qWarning() << "Error loading" << m_filename << ":" << e.cause();
+ }
+}
diff --git a/logic/BaseConfigObject.h b/logic/BaseConfigObject.h
new file mode 100644
index 00000000..1c96b3d1
--- /dev/null
+++ b/logic/BaseConfigObject.h
@@ -0,0 +1,50 @@
+/* Copyright 2015 MultiMC Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include <QObject>
+
+class QTimer;
+
+class BaseConfigObject
+{
+public:
+ void setSaveTimeout(int msec);
+
+protected:
+ explicit BaseConfigObject(const QString &filename);
+ virtual ~BaseConfigObject();
+
+ // cppcheck-suppress pureVirtualCall
+ virtual QByteArray doSave() const = 0;
+ virtual void doLoad(const QByteArray &data) = 0;
+
+ void setSavingDisabled(bool savingDisabled) { m_disableSaving = savingDisabled; }
+
+ QString fileName() const { return m_filename; }
+
+public:
+ void scheduleSave();
+ void saveNow();
+ void loadNow();
+
+private:
+ QTimer *m_saveTimer;
+ QTimer *m_initialReadTimer;
+ QString m_filename;
+ QMetaObject::Connection m_appQuitConnection;
+ bool m_disableSaving = false;
+};
diff --git a/logic/CMakeLists.txt b/logic/CMakeLists.txt
index de1940ad..d91fc694 100644
--- a/logic/CMakeLists.txt
+++ b/logic/CMakeLists.txt
@@ -1,6 +1,6 @@
project(MultiMC-Logic)
-SET(LOGIC_SOURCES
+set(LOGIC_SOURCES
# LOGIC - Base classes and infrastructure
BaseInstaller.h
BaseInstaller.cpp
@@ -14,11 +14,14 @@ SET(LOGIC_SOURCES
BaseInstance.h
BaseInstance.cpp
NullInstance.h
- MMCError.h
MMCZip.h
MMCZip.cpp
MMCStrings.h
MMCStrings.cpp
+ BaseConfigObject.h
+ BaseConfigObject.cpp
+ AbstractCommonModel.h
+ AbstractCommonModel.cpp
# Prefix tree where node names are strings between separators
SeparatorPrefixTree.h
@@ -28,8 +31,11 @@ SET(LOGIC_SOURCES
Env.cpp
# JSON parsing helpers
- MMCJson.h
- MMCJson.cpp
+ Json.h
+ Json.cpp
+ FileSystem.h
+ FileSystem.cpp
+ Exception.h
# RW lock protected map
RWStorage.h
@@ -40,6 +46,20 @@ SET(LOGIC_SOURCES
# a smart pointer wrapper intended for safer use with Qt signal/slot mechanisms
QObjectPtr.h
+ # Resources
+ resources/IconResourceHandler.cpp
+ resources/IconResourceHandler.h
+ resources/Resource.cpp
+ resources/Resource.h
+ resources/ResourceHandler.cpp
+ resources/ResourceHandler.h
+ resources/ResourceObserver.cpp
+ resources/ResourceObserver.h
+ resources/WebResourceHandler.cpp
+ resources/WebResourceHandler.h
+ resources/ResourceProxyModel.h
+ resources/ResourceProxyModel.cpp
+
# network stuffs
net/NetAction.h
net/MD5EtagDownload.h
@@ -183,6 +203,8 @@ SET(LOGIC_SOURCES
tasks/ThreadTask.cpp
tasks/SequentialTask.h
tasks/SequentialTask.cpp
+ tasks/StandardTask.h
+ tasks/StandardTask.cpp
# Settings
settings/INIFile.cpp
diff --git a/logic/Env.cpp b/logic/Env.cpp
index 0607c7ea..2f26f211 100644
--- a/logic/Env.cpp
+++ b/logic/Env.cpp
@@ -148,6 +148,7 @@ void Env::initHttpMetaCache(QString rootPath, QString staticDataPath)
m_metacache->addBase("skins", QDir("accounts/skins").absolutePath());
m_metacache->addBase("root", QDir(rootPath).absolutePath());
m_metacache->addBase("translations", QDir(staticDataPath + "/translations").absolutePath());
+ m_metacache->addBase("icons", QDir("cache/icons").absolutePath());
m_metacache->Load();
}
@@ -214,4 +215,4 @@ void Env::updateProxySettings(QString proxyTypeStr, QString addr, int port, QStr
qDebug() << proxyDesc;
}
-#include "Env.moc" \ No newline at end of file
+#include "Env.moc"
diff --git a/logic/Exception.h b/logic/Exception.h
new file mode 100644
index 00000000..2664910e
--- /dev/null
+++ b/logic/Exception.h
@@ -0,0 +1,41 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#pragma once
+
+#include <QString>
+#include <QLoggingCategory>
+#include <exception>
+
+class Exception : public std::exception
+{
+public:
+ Exception(const QString &message) : std::exception(), m_message(message)
+ {
+ qCritical() << "Exception:" << message;
+ }
+ Exception(const Exception &other)
+ : std::exception(), m_message(other.cause())
+ {
+ }
+ virtual ~Exception() noexcept {}
+ const char *what() const noexcept
+ {
+ return m_message.toLatin1().constData();
+ }
+ QString cause() const
+ {
+ return m_message;
+ }
+
+private:
+ QString m_message;
+};
+
+#define DECLARE_EXCEPTION(name) \
+ class name##Exception : public ::Exception \
+ { \
+ public: \
+ name##Exception(const QString &message) : Exception(message) \
+ { \
+ } \
+ }
diff --git a/logic/FileSystem.cpp b/logic/FileSystem.cpp
new file mode 100644
index 00000000..b8d82c51
--- /dev/null
+++ b/logic/FileSystem.cpp
@@ -0,0 +1,56 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#include "FileSystem.h"
+
+#include <QDir>
+#include <QSaveFile>
+#include <QFileInfo>
+
+void ensureExists(const QDir &dir)
+{
+ if (!QDir().mkpath(dir.absolutePath()))
+ {
+ throw FS::FileSystemException("Unable to create directory " + dir.dirName() + " (" +
+ dir.absolutePath() + ")");
+ }
+}
+
+void FS::write(const QString &filename, const QByteArray &data)
+{
+ ensureExists(QFileInfo(filename).dir());
+ QSaveFile file(filename);
+ if (!file.open(QSaveFile::WriteOnly))
+ {
+ throw FileSystemException("Couldn't open " + filename + " for writing: " +
+ file.errorString());
+ }
+ if (data.size() != file.write(data))
+ {
+ throw FileSystemException("Error writing data to " + filename + ": " +
+ file.errorString());
+ }
+ if (!file.commit())
+ {
+ throw FileSystemException("Error while committing data to " + filename + ": " +
+ file.errorString());
+ }
+}
+
+QByteArray FS::read(const QString &filename)
+{
+ QFile file(filename);
+ if (!file.open(QFile::ReadOnly))
+ {
+ throw FileSystemException("Unable to open " + filename + " for reading: " +
+ file.errorString());
+ }
+ const qint64 size = file.size();
+ QByteArray data(int(size), 0);
+ const qint64 ret = file.read(data.data(), size);
+ if (ret == -1 || ret != size)
+ {
+ throw FileSystemException("Error reading data from " + filename + ": " +
+ file.errorString());
+ }
+ return data;
+}
diff --git a/logic/FileSystem.h b/logic/FileSystem.h
new file mode 100644
index 00000000..e70f3165
--- /dev/null
+++ b/logic/FileSystem.h
@@ -0,0 +1,13 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#pragma once
+
+#include "Exception.h"
+
+namespace FS
+{
+DECLARE_EXCEPTION(FileSystem);
+
+void write(const QString &filename, const QByteArray &data);
+QByteArray read(const QString &filename);
+}
diff --git a/logic/Json.cpp b/logic/Json.cpp
new file mode 100644
index 00000000..46055909
--- /dev/null
+++ b/logic/Json.cpp
@@ -0,0 +1,278 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#include "Json.h"
+
+#include <QFile>
+#include <QSaveFile>
+
+#include "FileSystem.h"
+#include <math.h>
+
+namespace Json
+{
+void write(const QJsonDocument &doc, const QString &filename)
+{
+ FS::write(filename, doc.toJson());
+}
+void write(const QJsonObject &object, const QString &filename)
+{
+ write(QJsonDocument(object), filename);
+}
+void write(const QJsonArray &array, const QString &filename)
+{
+ write(QJsonDocument(array), filename);
+}
+
+QByteArray toBinary(const QJsonObject &obj)
+{
+ return QJsonDocument(obj).toBinaryData();
+}
+QByteArray toBinary(const QJsonArray &array)
+{
+ return QJsonDocument(array).toBinaryData();
+}
+QByteArray toText(const QJsonObject &obj)
+{
+ return QJsonDocument(obj).toJson(QJsonDocument::Compact);
+}
+QByteArray toText(const QJsonArray &array)
+{
+ return QJsonDocument(array).toJson(QJsonDocument::Compact);
+}
+
+static bool isBinaryJson(const QByteArray &data)
+{
+ decltype(QJsonDocument::BinaryFormatTag) tag = QJsonDocument::BinaryFormatTag;
+ return memcmp(data.constData(), &tag, sizeof(QJsonDocument::BinaryFormatTag)) == 0;
+}
+QJsonDocument ensureDocument(const QByteArray &data, const QString &what)
+{
+ if (isBinaryJson(data))
+ {
+ QJsonDocument doc = QJsonDocument::fromBinaryData(data);
+ if (doc.isNull())
+ {
+ throw JsonException(what + ": Invalid JSON (binary JSON detected)");
+ }
+ return doc;
+ }
+ else
+ {
+ QJsonParseError error;
+ QJsonDocument doc = QJsonDocument::fromJson(data, &error);
+ if (error.error != QJsonParseError::NoError)
+ {
+ throw JsonException(what + ": Error parsing JSON: " + error.errorString());
+ }
+ return doc;
+ }
+}
+QJsonDocument ensureDocument(const QString &filename, const QString &what)
+{
+ return ensureDocument(FS::read(filename), what);
+}
+QJsonObject ensureObject(const QJsonDocument &doc, const QString &what)
+{
+ if (!doc.isObject())
+ {
+ throw JsonException(what + " is not an object");
+ }
+ return doc.object();
+}
+QJsonArray ensureArray(const QJsonDocument &doc, const QString &what)
+{
+ if (!doc.isArray())
+ {
+ throw JsonException(what + " is not an array");
+ }
+ return doc.array();
+}
+
+void writeString(QJsonObject &to, const QString &key, const QString &value)
+{
+ if (!value.isEmpty())
+ {
+ to.insert(key, value);
+ }
+}
+
+void writeStringList(QJsonObject &to, const QString &key, const QStringList &values)
+{
+ if (!values.isEmpty())
+ {
+ QJsonArray array;
+ for(auto value: values)
+ {
+ array.append(value);
+ }
+ to.insert(key, array);
+ }
+}
+
+template<>
+QJsonValue toJson<QUrl>(const QUrl &url)
+{
+ return QJsonValue(url.toString(QUrl::FullyEncoded));
+}
+template<>
+QJsonValue toJson<QByteArray>(const QByteArray &data)
+{
+ return QJsonValue(QString::fromLatin1(data.toHex()));
+}
+template<>
+QJsonValue toJson<QDateTime>(const QDateTime &datetime)
+{
+ return QJsonValue(datetime.toString(Qt::ISODate));
+}
+template<>
+QJsonValue toJson<QDir>(const QDir &dir)
+{
+ return QDir::current().relativeFilePath(dir.absolutePath());
+}
+template<>
+QJsonValue toJson<QUuid>(const QUuid &uuid)
+{
+ return uuid.toString();
+}
+template<>
+QJsonValue toJson<QVariant>(const QVariant &variant)
+{
+ return QJsonValue::fromVariant(variant);
+}
+
+
+template<> QByteArray ensureIsType<QByteArray>(const QJsonValue &value, const Requirement,
+ const QString &what)
+{
+ const QString string = ensureIsType<QString>(value, Required, what);
+ // ensure that the string can be safely cast to Latin1
+ if (string != QString::fromLatin1(string.toLatin1()))
+ {
+ throw JsonException(what + " is not encodable as Latin1");
+ }
+ return QByteArray::fromHex(string.toLatin1());
+}
+
+template<> QJsonArray ensureIsType<QJsonArray>(const QJsonValue &value, const Requirement, const QString &what)
+{
+ if (!value.isArray())
+ {
+ throw JsonException(what + " is not an array");
+ }
+ return value.toArray();
+}
+
+
+template<> QString ensureIsType<QString>(const QJsonValue &value, const Requirement, const QString &what)
+{
+ if (!value.isString())
+ {
+ throw JsonException(what + " is not a string");
+ }
+ return value.toString();
+}
+
+template<> bool ensureIsType<bool>(const QJsonValue &value, const Requirement,
+ const QString &what)
+{
+ if (!value.isBool())
+ {
+ throw JsonException(what + " is not a bool");
+ }
+ return value.toBool();
+}
+
+template<> double ensureIsType<double>(const QJsonValue &value, const Requirement,
+ const QString &what)
+{
+ if (!value.isDouble())
+ {
+ throw JsonException(what + " is not a double");
+ }
+ return value.toDouble();
+}
+
+template<> int ensureIsType<int>(const QJsonValue &value, const Requirement,
+ const QString &what)
+{
+ const double doubl = ensureIsType<double>(value, Required, what);
+ if (fmod(doubl, 1) != 0)
+ {
+ throw JsonException(what + " is not an integer");
+ }
+ return int(doubl);
+}
+
+template<> QDateTime ensureIsType<QDateTime>(const QJsonValue &value, const Requirement,
+ const QString &what)
+{
+ const QString string = ensureIsType<QString>(value, Required, what);
+ const QDateTime datetime = QDateTime::fromString(string, Qt::ISODate);
+ if (!datetime.isValid())
+ {
+ throw JsonException(what + " is not a ISO formatted date/time value");
+ }
+ return datetime;
+}
+
+template<> QUrl ensureIsType<QUrl>(const QJsonValue &value, const Requirement,
+ const QString &what)
+{
+ const QString string = ensureIsType<QString>(value, Required, what);
+ if (string.isEmpty())
+ {
+ return QUrl();
+ }
+ const QUrl url = QUrl(string, QUrl::StrictMode);
+ if (!url.isValid())
+ {
+ throw JsonException(what + " is not a correctly formatted URL");
+ }
+ return url;
+}
+
+template<> QDir ensureIsType<QDir>(const QJsonValue &value, const Requirement, const QString &what)
+{
+ const QString string = ensureIsType<QString>(value, Required, what);
+ return QDir::current().absoluteFilePath(string);
+}
+
+template<> QUuid ensureIsType<QUuid>(const QJsonValue &value, const Requirement, const QString &what)
+{
+ const QString string = ensureIsType<QString>(value, Required, what);
+ const QUuid uuid = QUuid(string);
+ if (uuid.toString() != string) // converts back => valid
+ {
+ throw JsonException(what + " is not a valid UUID");
+ }
+ return uuid;
+}
+
+template<> QJsonObject ensureIsType<QJsonObject>(const QJsonValue &value, const Requirement, const QString &what)
+{
+ if (!value.isObject())
+ {
+ throw JsonException(what + " is not an object");
+ }
+ return value.toObject();
+}
+
+template<> QVariant ensureIsType<QVariant>(const QJsonValue &value, const Requirement, const QString &what)
+{
+ if (value.isNull() || value.isUndefined())
+ {
+ throw JsonException(what + " is null or undefined");
+ }
+ return value.toVariant();
+}
+
+template<> QJsonValue ensureIsType<QJsonValue>(const QJsonValue &value, const Requirement, const QString &what)
+{
+ if (value.isNull() || value.isUndefined())
+ {
+ throw JsonException(what + " is null or undefined");
+ }
+ return value;
+}
+
+}
diff --git a/logic/Json.h b/logic/Json.h
new file mode 100644
index 00000000..d22aa606
--- /dev/null
+++ b/logic/Json.h
@@ -0,0 +1,239 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#pragma once
+
+#include <QJsonDocument>
+#include <QJsonArray>
+#include <QJsonObject>
+#include <QDateTime>
+#include <QUrl>
+#include <QDir>
+#include <QUuid>
+#include <QVariant>
+#include <memory>
+
+#include "Exception.h"
+
+namespace Json
+{
+DECLARE_EXCEPTION(Json);
+
+enum Requirement
+{
+ Required
+};
+
+void write(const QJsonDocument &doc, const QString &filename);
+void write(const QJsonObject &object, const QString &filename);
+void write(const QJsonArray &array, const QString &filename);
+QByteArray toBinary(const QJsonObject &obj);
+QByteArray toBinary(const QJsonArray &array);
+QByteArray toText(const QJsonObject &obj);
+QByteArray toText(const QJsonArray &array);
+
+QJsonDocument ensureDocument(const QByteArray &data, const QString &what = "Document");
+QJsonDocument ensureDocument(const QString &filename, const QString &what = "Document");
+QJsonObject ensureObject(const QJsonDocument &doc, const QString &what = "Document");
+QJsonArray ensureArray(const QJsonDocument &doc, const QString &what = "Document");
+
+/////////////////// WRITING ////////////////////
+
+void writeString(QJsonObject & to, const QString &key, const QString &value);
+void writeStringList(QJsonObject & to, const QString &key, const QStringList &values);
+
+template <typename T>
+void writeObjectList(QJsonObject & to, QString key, QList<std::shared_ptr<T>> values)
+{
+ if (!values.isEmpty())
+ {
+ QJsonArray array;
+ for (auto value: values)
+ {
+ array.append(value->toJson());
+ }
+ to.insert(key, array);
+ }
+}
+template <typename T>
+void writeObjectList(QJsonObject & to, QString key, QList<T> values)
+{
+ if (!values.isEmpty())
+ {
+ QJsonArray array;
+ for (auto value: values)
+ {
+ array.append(value.toJson());
+ }
+ to.insert(key, array);
+ }
+}
+
+template<typename T>
+QJsonValue toJson(const T &t)
+{
+ return QJsonValue(t);
+}
+template<>
+QJsonValue toJson<QUrl>(const QUrl &url);
+template<>
+QJsonValue toJson<QByteArray>(const QByteArray &data);
+template<>
+QJsonValue toJson<QDateTime>(const QDateTime &datetime);
+template<>
+QJsonValue toJson<QDir>(const QDir &dir);
+template<>
+QJsonValue toJson<QUuid>(const QUuid &uuid);
+template<>
+QJsonValue toJson<QVariant>(const QVariant &variant);
+
+template<typename T>
+QJsonArray toJsonArray(const QList<T> &container)
+{
+ QJsonArray array;
+ for (const T item : container)
+ {
+ array.append(toJson<T>(item));
+ }
+ return array;
+}
+
+////////////////// READING ////////////////////
+
+template <typename T>
+T ensureIsType(const QJsonValue &value, const Requirement requirement = Required, const QString &what = "Value");
+
+template<> double ensureIsType<double>(const QJsonValue &value, const Requirement, const QString &what);
+template<> bool ensureIsType<bool>(const QJsonValue &value, const Requirement, const QString &what);
+template<> int ensureIsType<int>(const QJsonValue &value, const Requirement, const QString &what);
+template<> QJsonObject ensureIsType<QJsonObject>(const QJsonValue &value, const Requirement, const QString &what);
+template<> QJsonArray ensureIsType<QJsonArray>(const QJsonValue &value, const Requirement, const QString &what);
+template<> QJsonValue ensureIsType<QJsonValue>(const QJsonValue &value, const Requirement, const QString &what);
+template<> QByteArray ensureIsType<QByteArray>(const QJsonValue &value, const Requirement, const QString &what);
+template<> QDateTime ensureIsType<QDateTime>(const QJsonValue &value, const Requirement, const QString &what);
+template<> QVariant ensureIsType<QVariant>(const QJsonValue &value, const Requirement, const QString &what);
+template<> QString ensureIsType<QString>(const QJsonValue &value, const Requirement, const QString &what);
+template<> QUuid ensureIsType<QUuid>(const QJsonValue &value, const Requirement, const QString &what);
+template<> QDir ensureIsType<QDir>(const QJsonValue &value, const Requirement, const QString &what);
+template<> QUrl ensureIsType<QUrl>(const QJsonValue &value, const Requirement, const QString &what);
+
+// the following functions are higher level functions, that make use of the above functions for
+// type conversion
+template <typename T>
+T ensureIsType(const QJsonValue &value, const T default_, const QString &what = "Value")
+{
+ if (value.isUndefined())
+ {
+ return default_;
+ }
+ return ensureIsType<T>(value, Required, what);
+}
+template <typename T>
+T ensureIsType(const QJsonObject &parent, const QString &key,
+ const Requirement requirement = Required,
+ const QString &what = "__placeholder__")
+{
+ const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
+ if (!parent.contains(key))
+ {
+ throw JsonException(localWhat + "s parent does not contain " + localWhat);
+ }
+ return ensureIsType<T>(parent.value(key), requirement, localWhat);
+}
+template <typename T>
+T ensureIsType(const QJsonObject &parent, const QString &key, const T default_,
+ const QString &what = "__placeholder__")
+{
+ const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
+ if (!parent.contains(key))
+ {
+ return default_;
+ }
+ return ensureIsType<T>(parent.value(key), default_, localWhat);
+}
+
+template <typename T>
+QList<T> ensureIsArrayOf(const QJsonDocument &doc)
+{
+ const QJsonArray array = ensureArray(doc);
+ QList<T> out;
+ for (const QJsonValue val : array)
+ {
+ out.append(ensureIsType<T>(val, Required, "Document"));
+ }
+ return out;
+}
+template <typename T>
+QList<T> ensureIsArrayOf(const QJsonValue &value, const Requirement = Required,
+ const QString &what = "Value")
+{
+ const QJsonArray array = ensureIsType<QJsonArray>(value, Required, what);
+ QList<T> out;
+ for (const QJsonValue val : array)
+ {
+ out.append(ensureIsType<T>(val, Required, what));
+ }
+ return out;
+}
+template <typename T>
+QList<T> ensureIsArrayOf(const QJsonValue &value, const QList<T> default_,
+ const QString &what = "Value")
+{
+ if (value.isUndefined())
+ {
+ return default_;
+ }
+ return ensureIsArrayOf<T>(value, Required, what);
+}
+template <typename T>
+QList<T> ensureIsArrayOf(const QJsonObject &parent, const QString &key,
+ const Requirement requirement = Required,
+ const QString &what = "__placeholder__")
+{
+ const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
+ if (!parent.contains(key))
+ {
+ throw JsonException(localWhat + "s parent does not contain " + localWhat);
+ }
+ return ensureIsArrayOf<T>(parent.value(key), requirement, localWhat);
+}
+template <typename T>
+QList<T> ensureIsArrayOf(const QJsonObject &parent, const QString &key,
+ const QList<T> &default_, const QString &what = "__placeholder__")
+{
+ const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\'');
+ if (!parent.contains(key))
+ {
+ return default_;
+ }
+ return ensureIsArrayOf<T>(parent.value(key), default_, localWhat);
+}
+
+// this macro part could be replaced by variadic functions that just pass on their arguments, but that wouldn't work well with IDE helpers
+#define JSON_HELPERFUNCTIONS(NAME, TYPE) \
+ inline TYPE ensure##NAME(const QJsonValue &value, const Requirement requirement = Required, const QString &what = "Value") \
+{ return ensureIsType<TYPE>(value, requirement, what); } \
+ inline TYPE ensure##NAME(const QJsonValue &value, const TYPE default_, const QString &what = "Value") \
+{ return ensureIsType<TYPE>(value, default_, what); } \
+ inline TYPE ensure##NAME(const QJsonObject &parent, const QString &key, const Requirement requirement = Required, const QString &what = "__placeholder__") \
+{ return ensureIsType<TYPE>(parent, key, requirement, what); } \
+ inline TYPE ensure##NAME(const QJsonObject &parent, const QString &key, const TYPE default_, const QString &what = "__placeholder") \
+{ return ensureIsType<TYPE>(parent, key, default_, what); }
+
+JSON_HELPERFUNCTIONS(Array, QJsonArray)
+JSON_HELPERFUNCTIONS(Object, QJsonObject)
+JSON_HELPERFUNCTIONS(JsonValue, QJsonValue)
+JSON_HELPERFUNCTIONS(String, QString)
+JSON_HELPERFUNCTIONS(Boolean, bool)
+JSON_HELPERFUNCTIONS(Double, double)
+JSON_HELPERFUNCTIONS(Integer, int)
+JSON_HELPERFUNCTIONS(DateTime, QDateTime)
+JSON_HELPERFUNCTIONS(Url, QUrl)
+JSON_HELPERFUNCTIONS(ByteArray, QByteArray)
+JSON_HELPERFUNCTIONS(Dir, QDir)
+JSON_HELPERFUNCTIONS(Uuid, QUuid)
+JSON_HELPERFUNCTIONS(Variant, QVariant)
+
+#undef JSON_HELPERFUNCTIONS
+
+}
+using JSONValidationError = Json::JsonException;
diff --git a/logic/MMCError.h b/logic/MMCError.h
deleted file mode 100644
index e81054a6..00000000
--- a/logic/MMCError.h
+++ /dev/null
@@ -1,25 +0,0 @@
-#pragma once
-#include <exception>
-#include <QString>
-#include <QDebug>
-
-class MMCError : public std::exception
-{
-public:
- MMCError(QString cause)
- {
- exceptionCause = cause;
- qCritical() << "Exception: " + cause;
- };
- virtual ~MMCError() noexcept {}
- virtual const char *what() const noexcept
- {
- return exceptionCause.toLocal8Bit();
- };
- virtual QString cause() const
- {
- return exceptionCause;
- }
-private:
- QString exceptionCause;
-};
diff --git a/logic/MMCJson.cpp b/logic/MMCJson.cpp
deleted file mode 100644
index 23af4fff..00000000
--- a/logic/MMCJson.cpp
+++ /dev/null
@@ -1,142 +0,0 @@
-#include "MMCJson.h"
-
-#include <QString>
-#include <QUrl>
-#include <QStringList>
-#include <math.h>
-
-QJsonDocument MMCJson::parseDocument(const QByteArray &data, const QString &what)
-{
- QJsonParseError error;
- QJsonDocument doc = QJsonDocument::fromJson(data, &error);
- if (error.error != QJsonParseError::NoError)
- {
- throw JSONValidationError(what + " is not valid JSON: " + error.errorString() + " at " + error.offset);
- }
- return doc;
-}
-
-bool MMCJson::ensureBoolean(const QJsonValue val, const QString what)
-{
- if (!val.isBool())
- throw JSONValidationError(what + " is not boolean");
- return val.toBool();
-}
-
-QJsonValue MMCJson::ensureExists(QJsonValue val, const QString what)
-{
- if(val.isUndefined() || val.isUndefined())
- throw JSONValidationError(what + " does not exist");
- return val;
-}
-
-QJsonArray MMCJson::ensureArray(const QJsonValue val, const QString what)
-{
- if (!val.isArray())
- throw JSONValidationError(what + " is not an array");
- return val.toArray();
-}
-
-QJsonArray MMCJson::ensureArray(const QJsonDocument &val, const QString &what)
-{
- if (!val.isArray())
- {
- throw JSONValidationError(what + " is not an array");
- }
- return val.array();
-}
-
-double MMCJson::ensureDouble(const QJsonValue val, const QString what)
-{
- if (!val.isDouble())
- throw JSONValidationError(what + " is not a number");
- return val.toDouble();
-}
-
-int MMCJson::ensureInteger(const QJsonValue val, const QString what)
-{
- double ret = ensureDouble(val, what);
- if (fmod(ret, 1) != 0)
- throw JSONValidationError(what + " is not an integer");
- return ret;
-}
-
-QJsonObject MMCJson::ensureObject(const QJsonValue val, const QString what)
-{
- if (!val.isObject())
- throw JSONValidationError(what + " is not an object");
- return val.toObject();
-}
-
-QJsonObject MMCJson::ensureObject(const QJsonDocument val, const QString what)
-{
- if (!val.isObject())
- throw JSONValidationError(what + " is not an object");
- return val.object();
-}
-
-QString MMCJson::ensureString(const QJsonValue val, const QString what)
-{
- if (!val.isString())
- throw JSONValidationError(what + " is not a string");
- return val.toString();
-}
-
-QUrl MMCJson::ensureUrl(const QJsonValue &val, const QString &what)
-{
- const QUrl url = QUrl(ensureString(val, what));
- if (!url.isValid())
- {
- throw JSONValidationError(what + " is not an url");
- }
- return url;
-}
-
-QJsonDocument MMCJson::parseFile(const QString &filename, const QString &what)
-{
- QFile f(filename);
- if (!f.open(QFile::ReadOnly))
- {
- throw FileOpenError(f);
- }
- return parseDocument(f.readAll(), what);
-}
-
-int MMCJson::ensureInteger(const QJsonValue val, QString what, const int def)
-{
- if (val.isUndefined())
- return def;
- return ensureInteger(val, what);
-}
-
-void MMCJson::writeString(QJsonObject &to, QString key, QString value)
-{
- if (!value.isEmpty())
- {
- to.insert(key, value);
- }
-}
-
-void MMCJson::writeStringList(QJsonObject &to, QString key, QStringList values)
-{
- if (!values.isEmpty())
- {
- QJsonArray array;
- for(auto value: values)
- {
- array.append(value);
- }
- to.insert(key, array);
- }
-}
-
-QStringList MMCJson::ensureStringList(const QJsonValue val, QString what)
-{
- const QJsonArray array = ensureArray(val, what);
- QStringList out;
- for (const auto value : array)
- {
- out.append(ensureString(value));
- }
- return out;
-}
diff --git a/logic/MMCJson.h b/logic/MMCJson.h
deleted file mode 100644
index dc0b4224..00000000
--- a/logic/MMCJson.h
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * Some de-bullshitting for Qt JSON failures.
- *
- * Simple exception-throwing
- */
-
-#pragma once
-#include <QJsonValue>
-#include <QJsonObject>
-#include <QJsonDocument>
-#include <QJsonArray>
-#include <QFile>
-#include <memory>
-#include "MMCError.h"
-
-class JSONValidationError : public MMCError
-{
-public:
- JSONValidationError(QString cause) : MMCError(cause) {}
-};
-class FileOpenError : public MMCError
-{
-public:
- FileOpenError(const QFile &file) : MMCError(QObject::tr("Error opening %1: %2").arg(file.fileName(), file.errorString())) {}
-};
-
-namespace MMCJson
-{
-/// parses the data into a json document. throws if there's a parse error
-QJsonDocument parseDocument(const QByteArray &data, const QString &what);
-
-/// tries to open and then parses the specified file. throws if there's an error
-QJsonDocument parseFile(const QString &filename, const QString &what);
-
-/// make sure the value exists. throw otherwise.
-QJsonValue ensureExists(QJsonValue val, const QString what = "value");
-
-/// make sure the value is converted into an object. throw otherwise.
-QJsonObject ensureObject(const QJsonValue val, const QString what = "value");
-
-/// make sure the document is converted into an object. throw otherwise.
-QJsonObject ensureObject(const QJsonDocument val, const QString what = "document");
-
-/// make sure the value is converted into an array. throw otherwise.
-QJsonArray ensureArray(const QJsonValue val, QString what = "value");
-
-/// make sure the document is converted into an array. throw otherwise.
-QJsonArray ensureArray(const QJsonDocument &val, const QString &what = "document");
-
-/// make sure the value is converted into a string. throw otherwise.
-QString ensureString(const QJsonValue val, QString what = "value");
-
-/// make sure the value is converted into a string that's parseable as an url. throw otherwise.
-QUrl ensureUrl(const QJsonValue &val, const QString &what = "value");
-
-/// make sure the value is converted into a boolean. throw otherwise.
-bool ensureBoolean(const QJsonValue val, QString what = "value");
-
-/// make sure the value is converted into an integer. throw otherwise.
-int ensureInteger(const QJsonValue val, QString what = "value");
-
-/// make sure the value is converted into an integer. throw otherwise. this version will return the default value if the field is undefined.
-int ensureInteger(const QJsonValue val, QString what, const int def);
-
-/// make sure the value is converted into a double precision floating number. throw otherwise.
-double ensureDouble(const QJsonValue val, QString what = "value");
-
-QStringList ensureStringList(const QJsonValue val, QString what);
-
-void writeString(QJsonObject & to, QString key, QString value);
-
-void writeStringList(QJsonObject & to, QString key, QStringList values);
-
-template <typename T>
-void writeObjectList(QJsonObject & to, QString key, QList<std::shared_ptr<T>> values)
-{
- if (!values.isEmpty())
- {
- QJsonArray array;
- for (auto value: values)
- {
- array.append(value->toJson());
- }
- to.insert(key, array);
- }
-}
-template <typename T>
-void writeObjectList(QJsonObject & to, QString key, QList<T> values)
-{
- if (!values.isEmpty())
- {
- QJsonArray array;
- for (auto value: values)
- {
- array.append(value.toJson());
- }
- to.insert(key, array);
- }
-}
-}
-
diff --git a/logic/QObjectPtr.h b/logic/QObjectPtr.h
index 2bde1bd8..32e59bd9 100644
--- a/logic/QObjectPtr.h
+++ b/logic/QObjectPtr.h
@@ -19,6 +19,11 @@ public:
{
m_ptr = other.m_ptr;
}
+ template<typename Derived>
+ QObjectPtr(const QObjectPtr<Derived> &other)
+ {
+ m_ptr = other.unwrap();
+ }
public:
void reset(T * wrap)
diff --git a/logic/forge/ForgeInstaller.cpp b/logic/forge/ForgeInstaller.cpp
index 18527c49..32ce5788 100644
--- a/logic/forge/ForgeInstaller.cpp
+++ b/logic/forge/ForgeInstaller.cpp
@@ -22,6 +22,7 @@
#include "forge/ForgeVersionList.h"
#include "minecraft/VersionFilterData.h"
#include "Env.h"
+#include "Exception.h"
#include <quazip.h>
#include <quazipfile.h>
@@ -412,7 +413,7 @@ protected:
m_instance->reloadProfile();
emitSucceeded();
}
- catch (MMCError &e)
+ catch (Exception &e)
{
emitFailed(e.cause());
}
diff --git a/logic/liteloader/LiteLoaderInstaller.cpp b/logic/liteloader/LiteLoaderInstaller.cpp
index 9a06d620..e255921f 100644
--- a/logic/liteloader/LiteLoaderInstaller.cpp
+++ b/logic/liteloader/LiteLoaderInstaller.cpp
@@ -24,6 +24,7 @@
#include "minecraft/OneSixLibrary.h"
#include "minecraft/OneSixInstance.h"
#include "liteloader/LiteLoaderVersionList.h"
+#include "Exception.h"
LiteLoaderInstaller::LiteLoaderInstaller() : BaseInstaller()
{
@@ -118,7 +119,7 @@ protected:
m_instance->reloadProfile();
emitSucceeded();
}
- catch (MMCError &e)
+ catch (Exception &e)
{
emitFailed(e.cause());
}
diff --git a/logic/liteloader/LiteLoaderVersionList.cpp b/logic/liteloader/LiteLoaderVersionList.cpp
index 8b3c13e0..21bb4f50 100644
--- a/logic/liteloader/LiteLoaderVersionList.cpp
+++ b/logic/liteloader/LiteLoaderVersionList.cpp
@@ -16,7 +16,7 @@
#include "LiteLoaderVersionList.h"
#include "Env.h"
#include "net/URLConstants.h"
-#include "MMCError.h"
+#include "Exception.h"
#include <QtXml>
@@ -254,7 +254,7 @@ void LLListLoadTask::listDownloaded()
}
version->libraries.append(lib);
}
- catch (MMCError &e)
+ catch (Exception &e)
{
qCritical() << "Couldn't read JSON object:";
continue;
diff --git a/logic/minecraft/JarMod.cpp b/logic/minecraft/JarMod.cpp
index bf711c1f..bf985707 100644
--- a/logic/minecraft/JarMod.cpp
+++ b/logic/minecraft/JarMod.cpp
@@ -1,6 +1,6 @@
#include "JarMod.h"
-#include "MMCJson.h"
-using namespace MMCJson;
+#include "Json.h"
+using namespace Json;
JarmodPtr Jarmod::fromJson(const QJsonObject &libObj, const QString &filename, const QString &originalName)
{
diff --git a/logic/minecraft/MinecraftProfile.cpp b/logic/minecraft/MinecraftProfile.cpp
index 0661aec1..1baf008e 100644
--- a/logic/minecraft/MinecraftProfile.cpp
+++ b/logic/minecraft/MinecraftProfile.cpp
@@ -17,12 +17,13 @@
#include <QDir>
#include <QJsonDocument>
#include <QJsonArray>
+#include <QDebug>
#include <pathutils.h>
#include "minecraft/MinecraftProfile.h"
#include "ProfileUtils.h"
#include "NullProfileStrategy.h"
-#include "VersionBuildError.h"
+#include "Exception.h"
MinecraftProfile::MinecraftProfile(ProfileStrategy *strategy)
: QAbstractListModel()
@@ -277,7 +278,7 @@ std::shared_ptr<MinecraftProfile> MinecraftProfile::fromJson(const QJsonObject &
file->applyTo(version.get());
version->appendPatch(file);
}
- catch(MMCError & err)
+ catch(Exception &err)
{
return 0;
}
@@ -424,7 +425,7 @@ bool MinecraftProfile::reapplySafe()
{
reapply();
}
- catch(MMCError & error)
+ catch (Exception & error)
{
clear();
qWarning() << "Couldn't apply profile patches because: " << error.cause();
diff --git a/logic/minecraft/MinecraftVersionList.cpp b/logic/minecraft/MinecraftVersionList.cpp
index c20534e9..44be281b 100644
--- a/logic/minecraft/MinecraftVersionList.cpp
+++ b/logic/minecraft/MinecraftVersionList.cpp
@@ -14,12 +14,12 @@
*/
#include <QtXml>
-#include "MMCJson.h"
+#include "Json.h"
#include <QtAlgorithms>
#include <QtNetwork>
#include "Env.h"
-#include "MMCError.h"
+#include "Exception.h"
#include "MinecraftVersionList.h"
#include "net/URLConstants.h"
@@ -71,10 +71,10 @@ protected:
MinecraftVersionList *m_list;
};
-class ListLoadError : public MMCError
+class ListLoadError : public Exception
{
public:
- ListLoadError(QString cause) : MMCError(cause) {};
+ ListLoadError(QString cause) : Exception(cause) {};
virtual ~ListLoadError() noexcept
{
}
@@ -142,7 +142,7 @@ void MinecraftVersionList::loadCachedList()
}
loadMojangList(jsonDoc, Local);
}
- catch (MMCError &e)
+ catch (Exception &e)
{
// the cache has gone bad for some reason... flush it.
qCritical() << "The minecraft version cache is corrupted. Flushing cache.";
@@ -157,12 +157,11 @@ void MinecraftVersionList::loadBuiltinList()
qDebug() << "Loading builtin version list.";
// grab the version list data from internal resources.
const QJsonDocument doc =
- MMCJson::parseFile(":/versions/minecraft.json",
- "builtin version list");
+ Json::ensureDocument(QString(":/versions/minecraft.json"), "builtin version list");
const QJsonObject root = doc.object();
// parse all the versions
- for (const auto version : MMCJson::ensureArray(root.value("versions")))
+ for (const auto version : Json::ensureArray(root.value("versions")))
{
QJsonObject versionObj = version.toObject();
QString versionID = versionObj.value("id").toString("");
@@ -204,9 +203,9 @@ void MinecraftVersionList::loadBuiltinList()
mcVersion->m_processArguments = versionObj.value("processArguments").toString("legacy");
if (versionObj.contains("+traits"))
{
- for (auto traitVal : MMCJson::ensureArray(versionObj.value("+traits")))
+ for (auto traitVal : Json::ensureArray(versionObj.value("+traits")))
{
- mcVersion->m_traits.insert(MMCJson::ensureString(traitVal));
+ mcVersion->m_traits.insert(Json::ensureString(traitVal));
}
}
m_lookup[versionID] = mcVersion;
@@ -227,11 +226,11 @@ void MinecraftVersionList::loadMojangList(QJsonDocument jsonDoc, VersionSource s
try
{
- QJsonObject latest = MMCJson::ensureObject(root.value("latest"));
- m_latestReleaseID = MMCJson::ensureString(latest.value("release"));
- m_latestSnapshotID = MMCJson::ensureString(latest.value("snapshot"));
+ QJsonObject latest = Json::ensureObject(root.value("latest"));
+ m_latestReleaseID = Json::ensureString(latest.value("release"));
+ m_latestSnapshotID = Json::ensureString(latest.value("snapshot"));
}
- catch (MMCError &err)
+ catch (Exception &err)
{
qCritical()
<< tr("Error parsing version list JSON: couldn't determine latest versions");
@@ -481,7 +480,7 @@ void MCVListLoadTask::list_downloaded()
}
m_list->loadMojangList(jsonDoc, Remote);
}
- catch (MMCError &e)
+ catch (Exception &e)
{
emitFailed(e.cause());
return;
@@ -532,7 +531,7 @@ void MCVListVersionUpdateTask::json_downloaded()
{
file = VersionFile::fromJson(jsonDoc, "net.minecraft.json", false);
}
- catch (MMCError &e)
+ catch (Exception &e)
{
emitFailed(tr("Couldn't process version file: %1").arg(e.cause()));
return;
diff --git a/logic/minecraft/OneSixInstance.cpp b/logic/minecraft/OneSixInstance.cpp
index ffccc259..b7937e31 100644
--- a/logic/minecraft/OneSixInstance.cpp
+++ b/logic/minecraft/OneSixInstance.cpp
@@ -16,7 +16,6 @@
#include <QIcon>
#include <pathutils.h>
#include <QDebug>
-#include "MMCError.h"
#include "minecraft/OneSixInstance.h"
@@ -338,7 +337,7 @@ void OneSixInstance::reloadProfile()
catch (VersionIncomplete &error)
{
}
- catch (MMCError &error)
+ catch (Exception &error)
{
m_version->clear();
setFlag(VersionBrokenFlag);
diff --git a/logic/minecraft/OneSixProfileStrategy.cpp b/logic/minecraft/OneSixProfileStrategy.cpp
index 173cd4d6..f5de690b 100644
--- a/logic/minecraft/OneSixProfileStrategy.cpp
+++ b/logic/minecraft/OneSixProfileStrategy.cpp
@@ -294,7 +294,7 @@ bool OneSixProfileStrategy::customizePatch(ProfilePatchPtr patch)
{
qDebug() << "Version was incomplete:" << error.cause();
}
- catch (MMCError &error)
+ catch (Exception &error)
{
qWarning() << "Version could not be loaded:" << error.cause();
}
@@ -324,7 +324,7 @@ bool OneSixProfileStrategy::revertPatch(ProfilePatchPtr patch)
{
qDebug() << "Version was incomplete:" << error.cause();
}
- catch (MMCError &error)
+ catch (Exception &error)
{
qWarning() << "Version could not be loaded:" << error.cause();
}
diff --git a/logic/minecraft/OneSixUpdate.cpp b/logic/minecraft/OneSixUpdate.cpp
index 485727ec..8463ead6 100644
--- a/logic/minecraft/OneSixUpdate.cpp
+++ b/logic/minecraft/OneSixUpdate.cpp
@@ -33,6 +33,7 @@
#include "forge/ForgeMirrors.h"
#include "net/URLConstants.h"
#include "minecraft/AssetsUtils.h"
+#include "Exception.h"
#include "MMCZip.h"
OneSixUpdate::OneSixUpdate(OneSixInstance *inst, QObject *parent) : Task(parent), m_inst(inst)
@@ -182,7 +183,7 @@ void OneSixUpdate::jarlibStart()
{
inst->reloadProfile();
}
- catch (MMCError &e)
+ catch (Exception &e)
{
emitFailed(e.cause());
return;
diff --git a/logic/minecraft/ParseUtils.cpp b/logic/minecraft/ParseUtils.cpp
index 49e0e0ca..8fccf403 100644
--- a/logic/minecraft/ParseUtils.cpp
+++ b/logic/minecraft/ParseUtils.cpp
@@ -1,7 +1,6 @@
#include <QDateTime>
#include <QString>
#include "ParseUtils.h"
-#include <MMCJson.h>
QDateTime timeFromS3Time(QString str)
{
diff --git a/logic/minecraft/ProfileUtils.cpp b/logic/minecraft/ProfileUtils.cpp
index 3eaca920..68fe0f14 100644
--- a/logic/minecraft/ProfileUtils.cpp
+++ b/logic/minecraft/ProfileUtils.cpp
@@ -1,6 +1,6 @@
#include "ProfileUtils.h"
#include "minecraft/VersionFilterData.h"
-#include "MMCJson.h"
+#include "Json.h"
#include <QDebug>
#include <QJsonDocument>
@@ -74,18 +74,18 @@ bool readOverrideOrders(QString path, PatchOrder &order)
// and then read it and process it if all above is true.
try
{
- auto obj = MMCJson::ensureObject(doc);
+ auto obj = Json::ensureObject(doc);
// check order file version.
- auto version = MMCJson::ensureInteger(obj.value("version"), "version");
+ auto version = Json::ensureInteger(obj.value("version"));
if (version != currentOrderFileVersion)
{
throw JSONValidationError(QObject::tr("Invalid order file version, expected %1")
.arg(currentOrderFileVersion));
}
- auto orderArray = MMCJson::ensureArray(obj.value("order"));
+ auto orderArray = Json::ensureArray(obj.value("order"));
for(auto item: orderArray)
{
- order.append(MMCJson::ensureString(item));
+ order.append(Json::ensureString(item));
}
}
catch (JSONValidationError &err)
diff --git a/logic/minecraft/RawLibrary.cpp b/logic/minecraft/RawLibrary.cpp
index c4cd97a1..90883312 100644
--- a/logic/minecraft/RawLibrary.cpp
+++ b/logic/minecraft/RawLibrary.cpp
@@ -1,5 +1,5 @@
-#include "MMCJson.h"
-using namespace MMCJson;
+#include "Json.h"
+using namespace Json;
#include "RawLibrary.h"
#include <pathutils.h>
@@ -74,7 +74,7 @@ RawLibraryPtr RawLibrary::fromJsonPlus(const QJsonObject &libObj, const QString
auto lib = RawLibrary::fromJson(libObj, filename);
if (libObj.contains("insert"))
{
- QJsonValue insertVal = ensureExists(libObj.value("insert"), "library insert rule");
+ QJsonValue insertVal = ensureJsonValue(libObj.value("insert"), "library insert rule");
if (insertVal.isString())
{
// it's just a simple string rule. OK.
diff --git a/logic/minecraft/VersionBuildError.h b/logic/minecraft/VersionBuildError.h
index ae479851..fda453e5 100644
--- a/logic/minecraft/VersionBuildError.h
+++ b/logic/minecraft/VersionBuildError.h
@@ -1,9 +1,9 @@
-#include "MMCError.h"
+#include "Exception.h"
-class VersionBuildError : public MMCError
+class VersionBuildError : public Exception
{
public:
- VersionBuildError(QString cause) : MMCError(cause) {};
+ explicit VersionBuildError(QString cause) : Exception(cause) {}
virtual ~VersionBuildError() noexcept
{
}
@@ -55,4 +55,4 @@ public:
virtual ~VersionIncomplete() noexcept
{
}
-}; \ No newline at end of file
+};
diff --git a/logic/minecraft/VersionFile.cpp b/logic/minecraft/VersionFile.cpp
index 227ba8be..426cba8c 100644
--- a/logic/minecraft/VersionFile.cpp
+++ b/logic/minecraft/VersionFile.cpp
@@ -10,8 +10,8 @@
#include "minecraft/JarMod.h"
#include "ParseUtils.h"
-#include "MMCJson.h"
-using namespace MMCJson;
+#include "Json.h"
+using namespace Json;
#include "VersionBuildError.h"
diff --git a/logic/minecraft/VersionFile.h b/logic/minecraft/VersionFile.h
index dd5c962f..e5ce4026 100644
--- a/logic/minecraft/VersionFile.h
+++ b/logic/minecraft/VersionFile.h
@@ -3,11 +3,12 @@
#include <QString>
#include <QStringList>
#include <QDateTime>
+#include <QSet>
+
#include <memory>
#include "minecraft/OpSys.h"
#include "minecraft/OneSixRule.h"
#include "ProfilePatch.h"
-#include "MMCError.h"
#include "OneSixLibrary.h"
#include "JarMod.h"
diff --git a/logic/net/CacheDownload.h b/logic/net/CacheDownload.h
index 49d2d99f..7f95a69d 100644
--- a/logic/net/CacheDownload.h
+++ b/logic/net/CacheDownload.h
@@ -20,6 +20,29 @@
#include <QCryptographicHash>
#include <QSaveFile>
+class INetworkValidator
+{
+public:
+ virtual ~INetworkValidator() {}
+
+ virtual void validate(const QByteArray &data) = 0;
+};
+class JsonValidator : public INetworkValidator
+{
+public:
+ void validate(const QByteArray &data) override;
+};
+class MD5HashValidator : public INetworkValidator
+{
+public:
+ explicit MD5HashValidator(const QByteArray &expected)
+ : m_expected(expected) {}
+ void validate(const QByteArray &data) override;
+
+private:
+ QByteArray m_expected;
+};
+
typedef std::shared_ptr<class CacheDownload> CacheDownloadPtr;
class CacheDownload : public NetAction
{
@@ -33,6 +56,8 @@ private:
/// the hash-as-you-download
QCryptographicHash md5sum;
+ INetworkValidator *m_validator = nullptr;
+
bool wroteAnyData = false;
public:
@@ -46,6 +71,10 @@ public:
{
return m_target_path;
}
+ void setValidator(INetworkValidator *validator)
+ {
+ m_validator = validator;
+ }
protected
slots:
virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal);
diff --git a/logic/resources/IconResourceHandler.cpp b/logic/resources/IconResourceHandler.cpp
new file mode 100644
index 00000000..d47dcc3d
--- /dev/null
+++ b/logic/resources/IconResourceHandler.cpp
@@ -0,0 +1,60 @@
+#include "IconResourceHandler.h"
+
+#include <QDir>
+#include <QDebug>
+
+QString IconResourceHandler::m_theme = "multimc";
+QList<std::weak_ptr<IconResourceHandler>> IconResourceHandler::m_iconHandlers;
+
+IconResourceHandler::IconResourceHandler(const QString &key)
+ : m_key(key)
+{
+}
+
+void IconResourceHandler::setTheme(const QString &theme)
+{
+ m_theme = theme;
+
+ for (auto handler : m_iconHandlers)
+ {
+ std::shared_ptr<IconResourceHandler> ptr = handler.lock();
+ if (ptr)
+ {
+ ptr->setResult(ptr->get());
+ }
+ }
+}
+
+void IconResourceHandler::init(std::shared_ptr<ResourceHandler> &ptr)
+{
+ m_iconHandlers.append(std::dynamic_pointer_cast<IconResourceHandler>(ptr));
+ setResult(get());
+}
+
+QVariant IconResourceHandler::get() const
+{
+ const QDir iconsDir = QDir(":/icons/" + m_theme);
+
+ QVariantMap out;
+ for (const QFileInfo &sizeInfo : iconsDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot))
+ {
+ const QDir dir = QDir(sizeInfo.absoluteFilePath());
+ const QString dirName = sizeInfo.fileName();
+ const int size = dirName.left(dirName.indexOf('x')).toInt();
+ if (dir.exists(m_key + ".png") && dirName != "scalable")
+ {
+ out.insert(dir.absoluteFilePath(m_key + ".png"), size);
+ }
+ else if (dir.exists(m_key + ".svg") && dirName == "scalable")
+ {
+ out.insert(dir.absoluteFilePath(m_key + ".svg"), size);
+ }
+ }
+
+ if (out.isEmpty())
+ {
+ qWarning() << "Couldn't find any icons for" << m_key;
+ }
+
+ return out;
+}
diff --git a/logic/resources/IconResourceHandler.h b/logic/resources/IconResourceHandler.h
new file mode 100644
index 00000000..dedfecb2
--- /dev/null
+++ b/logic/resources/IconResourceHandler.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <memory>
+
+#include "ResourceHandler.h"
+
+class IconResourceHandler : public ResourceHandler
+{
+public:
+ explicit IconResourceHandler(const QString &key);
+
+ static void setTheme(const QString &theme);
+
+private:
+ void init(std::shared_ptr<ResourceHandler> &ptr) override;
+
+ QString m_key;
+ static QString m_theme;
+ static QList<std::weak_ptr<IconResourceHandler>> m_iconHandlers;
+
+ QVariant get() const;
+};
diff --git a/logic/resources/Resource.cpp b/logic/resources/Resource.cpp
new file mode 100644
index 00000000..16ed3d2d
--- /dev/null
+++ b/logic/resources/Resource.cpp
@@ -0,0 +1,121 @@
+#include "Resource.h"
+
+#include <QDebug>
+
+#include "WebResourceHandler.h"
+#include "IconResourceHandler.h"
+#include "ResourceObserver.h"
+
+QMap<QString, std::function<std::shared_ptr<ResourceHandler>(const QString &)>> Resource::m_handlers;
+QMap<QPair<int, int>, std::function<QVariant(QVariant)>> Resource::m_transfomers;
+QMap<QString, std::weak_ptr<Resource>> Resource::m_resources;
+
+Resource::Resource(const QString &resource)
+{
+ if (!m_handlers.contains("web"))
+ {
+ registerHandler<WebResourceHandler>("web");
+ }
+ if (!m_handlers.contains("icon"))
+ {
+ registerHandler<IconResourceHandler>("icon");
+ }
+
+ Q_ASSERT(resource.contains(':'));
+ const QString resourceId = resource.left(resource.indexOf(':'));
+ Q_ASSERT(m_handlers.contains(resourceId));
+ m_handler = m_handlers.value(resourceId)(resource.mid(resource.indexOf(':') + 1));
+ m_handler->init(m_handler);
+ m_handler->setResource(this);
+ Q_ASSERT(m_handler);
+}
+Resource::~Resource()
+{
+ qDeleteAll(m_observers);
+}
+
+Resource::Ptr Resource::create(const QString &resource)
+{
+ Resource::Ptr ptr = m_resources.contains(resource)
+ ? m_resources.value(resource).lock()
+ : nullptr;
+ if (!ptr)
+ {
+ struct ConstructableResource : public Resource
+ {
+ explicit ConstructableResource(const QString &resource)
+ : Resource(resource) {}
+ };
+ ptr = std::make_shared<ConstructableResource>(resource);
+ m_resources.insert(resource, ptr);
+ }
+ return ptr;
+}
+
+Resource::Ptr Resource::applyTo(ResourceObserver *observer)
+{
+ m_observers.append(observer);
+ observer->setSource(shared_from_this()); // give the observer a shared_ptr for us so we don't get deleted
+ observer->resourceUpdated();
+ return shared_from_this();
+}
+Resource::Ptr Resource::applyTo(QObject *target, const char *property)
+{
+ // the cast to ResourceObserver* is required to ensure the right overload gets choosen
+ return applyTo(static_cast<ResourceObserver *>(new QObjectResourceObserver(target, property)));
+}
+
+Resource::Ptr Resource::placeholder(Resource::Ptr other)
+{
+ m_placeholder = other;
+ for (ResourceObserver *observer : m_observers)
+ {
+ observer->resourceUpdated();
+ }
+ return shared_from_this();
+}
+
+QVariant Resource::getResourceInternal(const int typeId) const
+{
+ if (m_handler->result().isNull() && m_placeholder)
+ {
+ return m_placeholder->getResourceInternal(typeId);
+ }
+ const QVariant variant = m_handler->result();
+ const auto typePair = qMakePair(int(variant.type()), typeId);
+ if (m_transfomers.contains(typePair))
+ {
+ return m_transfomers.value(typePair)(variant);
+ }
+ else
+ {
+ return variant;
+ }
+}
+
+void Resource::reportResult()
+{
+ for (ResourceObserver *observer : m_observers)
+ {
+ observer->resourceUpdated();
+ }
+}
+void Resource::reportFailure(const QString &reason)
+{
+ for (ResourceObserver *observer : m_observers)
+ {
+ observer->setFailure(reason);
+ }
+}
+void Resource::reportProgress(const int progress)
+{
+ for (ResourceObserver *observer : m_observers)
+ {
+ observer->setProgress(progress);
+ }
+}
+
+void Resource::notifyObserverDeleted(ResourceObserver *observer)
+{
+ m_observers.removeAll(observer);
+}
diff --git a/logic/resources/Resource.h b/logic/resources/Resource.h
new file mode 100644
index 00000000..d566b2a2
--- /dev/null
+++ b/logic/resources/Resource.h
@@ -0,0 +1,116 @@
+#pragma once
+
+#include <QString>
+#include <QMap>
+#include <QVariant>
+#include <functional>
+#include <memory>
+
+#include "ResourceObserver.h"
+
+class ResourceHandler;
+
+namespace Detail
+{
+template <typename T> struct Function : public Function<decltype(&T::operator())> {};
+template <typename Ret, typename Arg> struct Function<Ret(*)(Arg)> : public Function<Ret(Arg)> {};
+template <typename Ret, typename Arg> struct Function<Ret(Arg)>
+{
+ using ReturnType = Ret;
+ using Argument = Arg;
+};
+template <class C, typename Ret, typename Arg> struct Function<Ret(C::*)(Arg)> : public Function<Ret(Arg)> {};
+template <class C, typename Ret, typename Arg> struct Function<Ret(C::*)(Arg) const> : public Function<Ret(Arg)> {};
+template <typename F> struct Function<F&> : public Function<F> {};
+template <typename F> struct Function<F&&> : public Function<F> {};
+}
+
+/** Frontend class for resources
+ *
+ * Usage:
+ * Resource::create("icon:noaccount")->applyTo(accountsAction);
+ * Resource::create("web:http://asdf.com/image.png")->applyTo(imageLbl)->placeholder(Resource::create("icon:loading"));
+ *
+ * Memory management:
+ * Resource caches ResourcePtrs using weak pointers, so while a resource is still existing
+ * when a new resource is created the resources will be the same (including the same handler).
+ *
+ * ResourceObservers keep a shared pointer to the resource, as does the Resource itself to it's
+ * placeholder (if present). This means a resource stays valid while it's still used ("applied to" etc.)
+ * by something. When nothing uses it anymore it gets deleted.
+ *
+ * \note Always pass resource around using ResourcePtr! Copy and move constructors are disabled for a reason.
+ */
+class Resource : public std::enable_shared_from_this<Resource>
+{
+ explicit Resource(const QString &resource);
+ Resource(const Resource &) = delete;
+ Resource(Resource &&) = delete;
+public:
+ using Ptr = std::shared_ptr<Resource>;
+
+ ~Resource();
+
+ /// The returned pointer needs to be stored until either Resource::then is called, or it is used as the argument to Resource::placeholder.
+ static Ptr create(const QString &resource);
+
+ /// This can e.g. be used to set a local icon as the placeholder while a slow (remote) icon is fetched
+ Ptr placeholder(Ptr other);
+
+ /// Use these functions to specify what should happen when e.g. the resource changes
+ Ptr applyTo(ResourceObserver *observer);
+ Ptr applyTo(QObject *target, const char *property = nullptr);
+ template<typename Func>
+ Ptr then(Func &&func)
+ {
+ using Arg = typename std::remove_cv<
+ typename std::remove_reference<typename Detail::Function<Func>::Argument>::type
+ >::type;
+ return applyTo(new FunctionResourceObserver<
+ typename Detail::Function<Func>::ReturnType,
+ Arg, Func
+ >(std::forward<Func>(func)));
+ }
+
+ /// Retrieve the currently active resource. If it's type is different from T a conversion will be attempted.
+ template<typename T>
+ T getResource() const { return getResourceInternal(qMetaTypeId<T>()).template value<T>(); }
+ QVariant getResourceInternal(const int typeId) const;
+
+ template<typename T>
+ static void registerHandler(const QString &id)
+ {
+ m_handlers.insert(id, [](const QString &res) { return std::make_shared<T>(res); });
+ }
+ template<typename Func>
+ static void registerTransformer(Func &&func)
+ {
+ using Out = typename Detail::Function<Func>::ReturnType;
+ using In = typename std::remove_cv<typename std::remove_reference<typename Detail::Function<Func>::Argument>::type>::type;
+ static_assert(!std::is_same<Out, In>::value, "It does not make sense to transform a value to itself");
+ m_transfomers.insert(qMakePair(qMetaTypeId<In>(), qMetaTypeId<Out>()), [func](const QVariant &in)
+ {
+ return QVariant::fromValue<Out>(func(in.value<In>()));
+ });
+ }
+
+private:
+ friend class ResourceHandler;
+ void reportResult();
+ void reportFailure(const QString &reason);
+ void reportProgress(const int progress);
+
+ friend class ResourceObserver;
+ void notifyObserverDeleted(ResourceObserver *observer);
+
+private:
+ QList<ResourceObserver *> m_observers;
+ std::shared_ptr<ResourceHandler> m_handler = nullptr;
+ Ptr m_placeholder = nullptr;
+
+ // a list of resource handler factories, registered using registerHandler
+ static QMap<QString, std::function<std::shared_ptr<ResourceHandler>(const QString &)>> m_handlers;
+ // a list of resource transformers, registered using registerTransformer
+ static QMap<QPair<int, int>, std::function<QVariant(QVariant)>> m_transfomers;
+ static QMap<QString, std::weak_ptr<Resource>> m_resources;
+};
diff --git a/logic/resources/ResourceHandler.cpp b/logic/resources/ResourceHandler.cpp
new file mode 100644
index 00000000..46a4422c
--- /dev/null
+++ b/logic/resources/ResourceHandler.cpp
@@ -0,0 +1,28 @@
+#include "ResourceHandler.h"
+
+#include "Resource.h"
+
+void ResourceHandler::setResult(const QVariant &result)
+{
+ m_result = result;
+ if (m_resource)
+ {
+ m_resource->reportResult();
+ }
+}
+
+void ResourceHandler::setFailure(const QString &reason)
+{
+ if (m_resource)
+ {
+ m_resource->reportFailure(reason);
+ }
+}
+
+void ResourceHandler::setProgress(const int progress)
+{
+ if (m_resource)
+ {
+ m_resource->reportProgress(progress);
+ }
+}
diff --git a/logic/resources/ResourceHandler.h b/logic/resources/ResourceHandler.h
new file mode 100644
index 00000000..c1105efc
--- /dev/null
+++ b/logic/resources/ResourceHandler.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include <QVariant>
+#include <memory>
+
+class Resource;
+
+/** Base class for things that can retrieve a resource.
+ *
+ * Subclass, provide a constructor that takes a single QString as argument, and
+ * call Resource::registerHandler<MyResourceHandler>("<id>"), where <id> is the
+ * prefix of the resource ("web", "icon", etc.)
+ */
+class ResourceHandler
+{
+public:
+ virtual ~ResourceHandler() {}
+
+ void setResource(Resource *resource) { m_resource = resource; }
+ // reimplement this if you need to do something after you have been put in a shared pointer
+ virtual void init(std::shared_ptr<ResourceHandler>&) {}
+
+ QVariant result() const { return m_result; }
+
+protected: // use these methods to notify the resource of changes
+ void setResult(const QVariant &result);
+ void setFailure(const QString &reason);
+ void setProgress(const int progress);
+
+private:
+ QVariant m_result;
+ Resource *m_resource = nullptr;
+};
diff --git a/logic/resources/ResourceObserver.cpp b/logic/resources/ResourceObserver.cpp
new file mode 100644
index 00000000..4f168fd2
--- /dev/null
+++ b/logic/resources/ResourceObserver.cpp
@@ -0,0 +1,55 @@
+#include "ResourceObserver.h"
+
+#include <QDebug>
+
+#include "Resource.h"
+
+static const char *defaultPropertyForTarget(QObject *target)
+{
+ if (target->inherits("QLabel"))
+ {
+ return "pixmap";
+ }
+ else if (target->inherits("QAction") ||
+ target->inherits("QMenu") ||
+ target->inherits("QAbstractButton"))
+ {
+ return "icon";
+ }
+ // for unit tests
+ else if (target->inherits("DummyObserverObject"))
+ {
+ return "property";
+ }
+ else
+ {
+ Q_ASSERT_X(false, "ResourceObserver.cpp: defaultPropertyForTarget", "Unrecognized QObject subclass");
+ return nullptr;
+ }
+}
+
+QObjectResourceObserver::QObjectResourceObserver(QObject *target, const char *property)
+ : QObject(target), m_target(target)
+{
+ const QMetaObject *mo = m_target->metaObject();
+ m_property = mo->property(mo->indexOfProperty(
+ property ?
+ property
+ : defaultPropertyForTarget(target)));
+}
+void QObjectResourceObserver::resourceUpdated()
+{
+ m_property.write(m_target, getInternal(m_property.type()));
+}
+
+
+ResourceObserver::~ResourceObserver()
+{
+ m_resource->notifyObserverDeleted(this);
+}
+
+QVariant ResourceObserver::getInternal(const int typeId) const
+{
+ Q_ASSERT(m_resource);
+ return m_resource->getResourceInternal(typeId);
+}
diff --git a/logic/resources/ResourceObserver.h b/logic/resources/ResourceObserver.h
new file mode 100644
index 00000000..27430d42
--- /dev/null
+++ b/logic/resources/ResourceObserver.h
@@ -0,0 +1,67 @@
+#pragma once
+
+#include <memory>
+#include <functional>
+
+#include <QObject>
+#include <QMetaProperty>
+
+class QVariant;
+class Resource;
+
+/// Base class for things that can use a resource
+class ResourceObserver
+{
+public:
+ virtual ~ResourceObserver();
+
+protected: // these methods are called by the Resource when something changes
+ virtual void resourceUpdated() = 0;
+ virtual void setFailure(const QString &) {}
+ virtual void setProgress(const int) {}
+
+private:
+ friend class Resource;
+ void setSource(std::shared_ptr<Resource> resource) { m_resource = resource; }
+
+protected:
+ template<typename T>
+ T get() const { return getInternal(qMetaTypeId<T>()).template value<T>(); }
+ QVariant getInternal(const int typeId) const;
+
+private:
+ std::shared_ptr<Resource> m_resource;
+};
+
+/** Observer for QObject properties
+ *
+ * Give it a target and the name of a property, and that property will be set when the resource changes.
+ *
+ * If no name is given an attempt to find a default property for some common classes is done.
+ */
+class QObjectResourceObserver : public QObject, public ResourceObserver
+{
+public:
+ explicit QObjectResourceObserver(QObject *target, const char *property = nullptr);
+
+ void resourceUpdated() override;
+
+private:
+ QObject *m_target;
+ QMetaProperty m_property;
+};
+
+template <typename Ret, typename Arg, typename Func>
+class FunctionResourceObserver : public ResourceObserver
+{
+ std::function<Ret(Arg)> m_function;
+public:
+ template <typename T>
+ explicit FunctionResourceObserver(T &&func)
+ : m_function(std::forward<Func>(func)) {}
+
+ void resourceUpdated() override
+ {
+ m_function(get<Arg>());
+ }
+};
diff --git a/logic/resources/ResourceProxyModel.cpp b/logic/resources/ResourceProxyModel.cpp
new file mode 100644
index 00000000..6ff11367
--- /dev/null
+++ b/logic/resources/ResourceProxyModel.cpp
@@ -0,0 +1,103 @@
+#include "ResourceProxyModel.h"
+
+#include <QItemSelectionRange>
+
+#include "Resource.h"
+#include "ResourceObserver.h"
+
+//Q_DECLARE_METATYPE(QVector<int>)
+
+class ModelResourceObserver : public ResourceObserver
+{
+public:
+ explicit ModelResourceObserver(const QModelIndex &index, const int role)
+ : m_index(index), m_role(role)
+ {
+ qRegisterMetaType<QVector<int>>("QVector<int>");
+ }
+
+ void resourceUpdated() override
+ {
+ if (m_index.isValid())
+ {
+ QMetaObject::invokeMethod(const_cast<QAbstractItemModel *>(m_index.model()),
+ "dataChanged", Qt::QueuedConnection,
+ Q_ARG(QModelIndex, m_index), Q_ARG(QModelIndex, m_index), Q_ARG(QVector<int>, QVector<int>() << m_role));
+ }
+ }
+
+private:
+ QPersistentModelIndex m_index;
+ int m_role;
+};
+
+ResourceProxyModel::ResourceProxyModel(const int resultTypeId, QObject *parent)
+ : QIdentityProxyModel(parent), m_resultTypeId(resultTypeId)
+{
+}
+
+QVariant ResourceProxyModel::data(const QModelIndex &proxyIndex, int role) const
+{
+ const QModelIndex mapped = mapToSource(proxyIndex);
+ if (mapped.isValid() && role == Qt::DecorationRole && !mapToSource(proxyIndex).data(role).toString().isEmpty())
+ {
+ if (!m_resources.contains(mapped))
+ {
+ Resource::Ptr res = Resource::create(mapToSource(proxyIndex).data(role).toString())
+ ->applyTo(new ModelResourceObserver(proxyIndex, role));
+
+ const QVariant placeholder = mapped.data(PlaceholderRole);
+ if (!placeholder.isNull() && placeholder.type() == QVariant::String)
+ {
+ res->placeholder(Resource::create(placeholder.toString()));
+ }
+
+ m_resources.insert(mapped, res);
+ }
+
+ return m_resources.value(mapped)->getResourceInternal(m_resultTypeId);
+ }
+ return mapped.data(role);
+}
+
+void ResourceProxyModel::setSourceModel(QAbstractItemModel *model)
+{
+ if (sourceModel())
+ {
+ disconnect(sourceModel(), 0, this, 0);
+ }
+ if (model)
+ {
+ connect(model, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &tl, const QModelIndex &br, const QVector<int> &roles)
+ {
+ if (roles.contains(Qt::DecorationRole) || roles.isEmpty())
+ {
+ const QItemSelectionRange range(tl, br);
+ for (const QModelIndex &index : range.indexes())
+ {
+ m_resources.remove(index);
+ }
+ }
+ else if (roles.contains(PlaceholderRole))
+ {
+ const QItemSelectionRange range(tl, br);
+ for (const QModelIndex &index : range.indexes())
+ {
+ if (m_resources.contains(index))
+ {
+ const QVariant placeholder = index.data(PlaceholderRole);
+ if (!placeholder.isNull() && placeholder.type() == QVariant::String)
+ {
+ m_resources.value(index)->placeholder(Resource::create(placeholder.toString()));
+ }
+ else
+ {
+ m_resources.value(index)->placeholder(nullptr);
+ }
+ }
+ }
+ }
+ });
+ }
+ QIdentityProxyModel::setSourceModel(model);
+}
diff --git a/logic/resources/ResourceProxyModel.h b/logic/resources/ResourceProxyModel.h
new file mode 100644
index 00000000..9db09545
--- /dev/null
+++ b/logic/resources/ResourceProxyModel.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include <QIdentityProxyModel>
+#include <memory>
+
+/// Convenience proxy model that transforms resource identifiers (strings) for Qt::DecorationRole into other types.
+class ResourceProxyModel : public QIdentityProxyModel
+{
+ Q_OBJECT
+public:
+ // resultTypeId is found using qMetaTypeId<T>()
+ explicit ResourceProxyModel(const int resultTypeId, QObject *parent = nullptr);
+
+ enum
+ {
+ // provide this role from your model if you want to show a placeholder
+ PlaceholderRole = Qt::UserRole + 0xabc // some random offset to not collide with other stuff
+ };
+
+ QVariant data(const QModelIndex &proxyIndex, int role) const override;
+ void setSourceModel(QAbstractItemModel *model) override;
+
+ template <typename T>
+ static QAbstractItemModel *mixin(QAbstractItemModel *model)
+ {
+ ResourceProxyModel *proxy = new ResourceProxyModel(qMetaTypeId<T>(), model);
+ proxy->setSourceModel(model);
+ return proxy;
+ }
+
+private:
+ // mutable because it needs to be available from the const data()
+ mutable QMap<QPersistentModelIndex, std::shared_ptr<class Resource>> m_resources;
+
+ const int m_resultTypeId;
+};
diff --git a/logic/resources/WebResourceHandler.cpp b/logic/resources/WebResourceHandler.cpp
new file mode 100644
index 00000000..7ced5bc6
--- /dev/null
+++ b/logic/resources/WebResourceHandler.cpp
@@ -0,0 +1,67 @@
+#include "WebResourceHandler.h"
+
+#include "net/CacheDownload.h"
+#include "net/HttpMetaCache.h"
+#include "net/NetJob.h"
+#include "FileSystem.h"
+#include "Env.h"
+
+QMap<QString, NetJob *> WebResourceHandler::m_activeDownloads;
+
+WebResourceHandler::WebResourceHandler(const QString &url)
+ : QObject(), m_url(url)
+{
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("icons", url);
+ if (!entry->stale)
+ {
+ setResultFromFile(entry->getFullPath());
+ }
+ else if (m_activeDownloads.contains(url))
+ {
+ NetJob *job = m_activeDownloads.value(url);
+ connect(job, &NetJob::succeeded, this, &WebResourceHandler::succeeded);
+ connect(job, &NetJob::failed, this, [job, this]() {setFailure(job->failReason());});
+ connect(job, &NetJob::progress, this, &WebResourceHandler::progress);
+ }
+ else
+ {
+ NetJob *job = new NetJob("Icon download");
+ job->addNetAction(CacheDownload::make(QUrl(url), entry));
+ connect(job, &NetJob::succeeded, this, &WebResourceHandler::succeeded);
+ connect(job, &NetJob::failed, this, [job, this]() {setFailure(job->failReason());});
+ connect(job, &NetJob::progress, this, &WebResourceHandler::progress);
+ connect(job, &NetJob::finished, job, [job](){m_activeDownloads.remove(m_activeDownloads.key(job));job->deleteLater();});
+ m_activeDownloads.insert(url, job);
+ job->start();
+ }
+}
+
+void WebResourceHandler::succeeded()
+{
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry("icons", m_url);
+ setResultFromFile(entry->getFullPath());
+ m_activeDownloads.remove(m_activeDownloads.key(qobject_cast<NetJob *>(sender())));
+}
+void WebResourceHandler::progress(qint64 current, qint64 total)
+{
+ if (total == 0)
+ {
+ setProgress(101);
+ }
+ else
+ {
+ setProgress(current / total);
+ }
+}
+
+void WebResourceHandler::setResultFromFile(const QString &file)
+{
+ try
+ {
+ setResult(FS::read(file));
+ }
+ catch (Exception &e)
+ {
+ setFailure(e.cause());
+ }
+}
diff --git a/logic/resources/WebResourceHandler.h b/logic/resources/WebResourceHandler.h
new file mode 100644
index 00000000..88804af3
--- /dev/null
+++ b/logic/resources/WebResourceHandler.h
@@ -0,0 +1,23 @@
+#pragma once
+
+#include <QObject>
+#include "ResourceHandler.h"
+
+class NetJob;
+
+class WebResourceHandler : public QObject, public ResourceHandler
+{
+public:
+ explicit WebResourceHandler(const QString &url);
+
+private slots:
+ void succeeded();
+ void progress(qint64 current, qint64 total);
+
+private:
+ static QMap<QString, NetJob *> m_activeDownloads;
+
+ QString m_url;
+
+ void setResultFromFile(const QString &file);
+};
diff --git a/logic/tasks/StandardTask.cpp b/logic/tasks/StandardTask.cpp
new file mode 100644
index 00000000..3201d674
--- /dev/null
+++ b/logic/tasks/StandardTask.cpp
@@ -0,0 +1,120 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#include "StandardTask.h"
+
+#include <QEventLoop>
+#include <QProcess>
+
+#include "net/CacheDownload.h"
+#include "net/ByteArrayDownload.h"
+#include "net/NetJob.h"
+#include "FileSystem.h"
+#include "Exception.h"
+#include "Env.h"
+
+StandardTask::StandardTask(QObject *parent)
+ : Task(parent)
+{
+ m_loop = new QEventLoop(this);
+}
+
+void StandardTask::runTask(QObjectPtr<Task> other)
+{
+ connect(other.get(), &Task::succeeded, m_loop, &QEventLoop::quit);
+ connect(other.get(), &Task::failed, m_loop, &QEventLoop::quit);
+ connect(other.get(), &Task::progress, this, [this](qint64 current, qint64 total){setProgress(current / total);});
+ connect(other.get(), &Task::status, this, &StandardTask::setStatus);
+ if (!other->isRunning())
+ {
+ other->start();
+ }
+ if (other->isRunning())
+ {
+ m_loop->exec();
+ }
+ disconnect(other.get(), 0, m_loop, 0);
+ disconnect(other.get(), 0, this, 0);
+ other->deleteLater();
+ if (!other->successful())
+ {
+ throw Exception(other->failReason());
+ }
+}
+void StandardTask::runTaskNonBlocking(QObjectPtr<Task> other)
+{
+ if (!other)
+ {
+ return;
+ }
+ m_pendingTasks.append(other.get());
+ m_pendingTaskPtrs.append(other);
+ other->start();
+}
+QByteArray StandardTask::networkGet(const QUrl &url)
+{
+ ByteArrayDownloadPtr task = ByteArrayDownload::make(url);
+ runTask(wrapDownload("", task));
+ return task->m_data;
+}
+QByteArray StandardTask::networkGetCached(const QString &name, const QString &base, const QString &path, const QUrl &url, const bool alwaysRefetch,
+ INetworkValidator *validator)
+{
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry(base, path);
+ if (!alwaysRefetch && !entry->stale)
+ {
+ if (validator) { delete validator; }
+ return FS::read(entry->getFullPath());
+ }
+ else if (alwaysRefetch)
+ {
+ entry->stale = true;
+ }
+ CacheDownloadPtr task = CacheDownload::make(url, entry);
+ task->setValidator(validator);
+ runTask(wrapDownload(name, task));
+ return FS::read(entry->getFullPath());
+}
+QByteArray StandardTask::networkGetCached(const QString &name, const QString &base, const QString &path, const QUrl &url, const QMap<QString, QString> &headers,
+ INetworkValidator *validator)
+{
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry(base, path);
+ if (!entry->stale)
+ {
+ if (validator) { delete validator; }
+ return FS::read(entry->getFullPath());
+ }
+ CacheDownloadPtr task = CacheDownload::make(url, entry);
+ //task->setHeaders(headers);
+ task->setValidator(validator);
+ runTask(wrapDownload(name, task));
+ return FS::read(entry->getFullPath());
+}
+void StandardTask::networkGetCachedNonBlocking(const QString &name, const QString &base, const QString &path, const QUrl &url, const bool alwaysRefetch,
+ INetworkValidator *validator)
+{
+ MetaEntryPtr entry = ENV.metacache()->resolveEntry(base, path);
+ if (!alwaysRefetch && !entry->stale)
+ {
+ return;
+ }
+ CacheDownloadPtr dl = CacheDownload::make(url, entry);
+ dl->setValidator(validator);
+ runTaskNonBlocking(wrapDownload(name, dl));
+}
+void StandardTask::waitOnPending()
+{
+ for (int i = 0; i < m_pendingTasks.size(); ++i)
+ {
+ if (m_pendingTasks.at(i) && m_pendingTasks.at(i)->isRunning())
+ {
+ runTask(m_pendingTaskPtrs.at(i));
+ }
+ }
+}
+
+QObjectPtr<NetJob> StandardTask::wrapDownload(const QString &name, std::shared_ptr<NetAction> action)
+{
+ NetJobPtr task = NetJobPtr(new NetJob(name));
+ task->addNetAction(action);
+ return task;
+}
diff --git a/logic/tasks/StandardTask.h b/logic/tasks/StandardTask.h
new file mode 100644
index 00000000..6f283dcd
--- /dev/null
+++ b/logic/tasks/StandardTask.h
@@ -0,0 +1,43 @@
+// Licensed under the Apache-2.0 license. See README.md for details.
+
+#pragma once
+
+#include "Task.h"
+
+#include <QPointer>
+#include <memory>
+
+#include "QObjectPtr.h"
+
+class QEventLoop;
+class QDir;
+class NetAction;
+class NetJob;
+class INetworkValidator;
+
+class StandardTask : public Task
+{
+ Q_OBJECT
+public:
+ explicit StandardTask(QObject *parent = nullptr);
+
+protected:
+ // TODO: switch to a future-based system
+ void runTask(QObjectPtr<Task> other);
+ void runTaskNonBlocking(QObjectPtr<Task> other);
+ QByteArray networkGet(const QUrl &url);
+ QByteArray networkGetCached(const QString &name, const QString &base, const QString &path, const QUrl &url, const bool alwaysRefetch = false,
+ INetworkValidator *validator = nullptr);
+ QByteArray networkGetCached(const QString &name, const QString &base, const QString &path, const QUrl &url, const QMap<QString, QString> &headers,
+ INetworkValidator *validator = nullptr);
+ void networkGetCachedNonBlocking(const QString &name, const QString &base, const QString &path, const QUrl &url, const bool alwaysRefetch = false,
+ INetworkValidator *validator = nullptr);
+ void waitOnPending();
+
+private:
+ QEventLoop *m_loop;
+ QList<QPointer<Task>> m_pendingTasks; // only used to check if the object was deleted
+ QList<QObjectPtr<Task>> m_pendingTaskPtrs;
+
+ QObjectPtr<NetJob> wrapDownload(const QString &name, std::shared_ptr<NetAction> action);
+};
diff --git a/logic/tasks/Task.cpp b/logic/tasks/Task.cpp
index 8fed810b..eaeff4c2 100644
--- a/logic/tasks/Task.cpp
+++ b/logic/tasks/Task.cpp
@@ -14,6 +14,7 @@
*/
#include "Task.h"
+
#include <QDebug>
Task::Task(QObject *parent) : QObject(parent)
diff --git a/logic/tasks/Task.h b/logic/tasks/Task.h
index 3ab85d7d..93ca620d 100644
--- a/logic/tasks/Task.h
+++ b/logic/tasks/Task.h
@@ -39,6 +39,8 @@ public:
*/
virtual QString failReason() const;
+ virtual bool canAbort() const { return false; }
+
signals:
void started();
void progress(qint64 current, qint64 total);
diff --git a/tests/tst_Resource.cpp b/tests/tst_Resource.cpp
new file mode 100644
index 00000000..ba6f0509
--- /dev/null
+++ b/tests/tst_Resource.cpp
@@ -0,0 +1,101 @@
+#include <QTest>
+#include <QAction>
+#include "TestUtil.h"
+
+#include "resources/Resource.h"
+#include "resources/ResourceHandler.h"
+#include "resources/ResourceObserver.h"
+
+class DummyStringResourceHandler : public ResourceHandler
+{
+public:
+ explicit DummyStringResourceHandler(const QString &key)
+ : m_key(key) {}
+
+ void init(std::shared_ptr<ResourceHandler> &) override
+ {
+ setResult(m_key);
+ }
+
+ QString m_key;
+};
+class DummyObserver : public ResourceObserver
+{
+public:
+ void resourceUpdated() override
+ {
+ values += get<QString>();
+ }
+
+ QStringList values;
+};
+class DummyObserverObject : public QObject
+{
+ Q_OBJECT
+ Q_PROPERTY(QString property MEMBER property)
+
+public:
+ explicit DummyObserverObject(QObject *parent = nullptr) : QObject(parent) {}
+
+ QString property;
+};
+
+class ResourceTest : public QObject
+{
+ Q_OBJECT
+private
+slots:
+ void initTestCase()
+ {
+ Resource::registerHandler<DummyStringResourceHandler>("dummy");
+ }
+ void cleanupTestCase()
+ {
+ }
+
+ void test_Then()
+ {
+ QString val;
+ Resource::create("dummy:test_Then")
+ ->then([&val](const QString &key) { val = key; });
+ QCOMPARE(val, QStringLiteral("test_Then"));
+ }
+ void test_Object()
+ {
+ DummyObserver *observer = new DummyObserver;
+ Resource::create("dummy:test_Object")->applyTo(observer);
+ QCOMPARE(observer->values, QStringList() << "test_Object");
+ }
+ void test_QObjectProperty()
+ {
+ DummyObserverObject *object = new DummyObserverObject;
+ Resource::create("dummy:test_QObjectProperty")->applyTo(object);
+ QCOMPARE(object->property, QStringLiteral("test_QObjectProperty"));
+ }
+
+ void test_DontRequestPlaceholder()
+ {
+ auto resource = Resource::create("dummy:asdf")
+ ->then([](const QString &key) { QCOMPARE(key, QStringLiteral("asdf")); });
+ // the following call should not notify the observer. if it does the above QCOMPARE would fail.
+ resource->placeholder(Resource::create("dummy:fdsa"));
+ }
+
+ void test_MergedResources()
+ {
+ auto r1 = Resource::create("dummy:asdf");
+ auto r2 = Resource::create("dummy:asdf");
+ auto r3 = Resource::create("dummy:fdsa");
+ auto r4 = Resource::create("dummy:asdf");
+
+ QCOMPARE(r1, r2);
+ QCOMPARE(r1, r4);
+ QVERIFY(r1 != r3);
+ QVERIFY(r2 != r3);
+ QVERIFY(r4 != r3);
+ }
+};
+
+QTEST_GUILESS_MAIN(ResourceTest)
+
+#include "tst_Resource.moc"