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 #include #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 +#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 Binary files /dev/null and b/application/resources/multimc/150x150/hourglass.png differ diff --git a/application/resources/multimc/16x16/hourglass.png b/application/resources/multimc/16x16/hourglass.png new file mode 100644 index 00000000..ab36234b Binary files /dev/null and b/application/resources/multimc/16x16/hourglass.png differ diff --git a/application/resources/multimc/22x22/hourglass.png b/application/resources/multimc/22x22/hourglass.png new file mode 100644 index 00000000..8cb343ac Binary files /dev/null and b/application/resources/multimc/22x22/hourglass.png differ diff --git a/application/resources/multimc/32x32/hourglass.png b/application/resources/multimc/32x32/hourglass.png new file mode 100644 index 00000000..a558ec99 Binary files /dev/null and b/application/resources/multimc/32x32/hourglass.png differ diff --git a/application/resources/multimc/48x48/hourglass.png b/application/resources/multimc/48x48/hourglass.png new file mode 100644 index 00000000..8f10ab7a Binary files /dev/null and b/application/resources/multimc/48x48/hourglass.png 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 @@ 48x48/log.png 64x64/log.png + + 16x16/hourglass.png + 22x22/hourglass.png + 32x32/hourglass.png + 48x48/hourglass.png + 150x150/hourglass.png + scalable/screenshot-placeholder.svg 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 +#include +#include +#include + +#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) +{ + 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) +{ + 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 +#include + +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); + bool exec(std::shared_ptr 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 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() << 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 +#include +#include +#include + +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 +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 + 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(); + } + QVariant get(const Object &object) const override + { + return QVariant::fromValue(object.*m_member); + } + bool canSet() const override { return true; } + + private: + Member m_member; + }; + template + 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()); + } + QVariant get(const Object &object) const override + { + return QVariant::fromValue(object.*m_getter()); + } + bool canSet() const override { return !!m_setter; } + + private: + Getter m_getter; + Setter m_setter; + }; + + QList m_objects; + QVector>> 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 std::enable_if::value && std::is_member_function_pointer::value, void>::type + addEntry(Getter getter, Setter setter, const int entry, const int role) + { + addEntryInternal(new FunctionEntry::type>(getter, setter), entry, role); + } + template + typename std::enable_if::value, void>::type + addEntry(Getter getter, const int entry, const int role) + { + addEntryInternal(new FunctionEntry::type>(getter, nullptr), entry, role); + } + template + typename std::enable_if::value, void>::type + addEntry(T (Object::*member), const int entry, const int role) + { + addEntryInternal(new VariableEntry(member), entry, role); + } + + void setEntryTitle(const int entry, const QString &title) + { + m_entries[entry].first = title; + } +}; +template +class AbstractCommonModel : 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 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 + 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(); + } + QVariant get(Object *object) const override + { + return QVariant::fromValue(object->*m_member); + } + bool canSet() const override { return true; } + + private: + Member m_member; + }; + template + 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()); + } + QVariant get(Object *object) const override + { + return QVariant::fromValue((object->*m_getter)()); + } + bool canSet() const override { return !!m_setter; } + + private: + Getter m_getter; + Setter m_setter; + }; + template + struct LambdaEntry : public IEntry + { + using Getter = std::function; + + explicit LambdaEntry(Getter getter) + : m_getter(getter) {} + + void set(Object *object, const QVariant &value) override {} + QVariant get(Object *object) const override + { + return QVariant::fromValue(m_getter(object)); + } + bool canSet() const override { return false; } + + private: + Getter m_getter; + }; + + QList m_objects; + QVector>> 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 std::enable_if::value && std::is_member_function_pointer::value, void>::type + addEntry(const int entry, const int role, Getter getter, Setter setter) + { + addEntryInternal(new FunctionEntry::type>(getter, setter), entry, role); + } + template + typename std::enable_if::Getter>::value, void>::type + addEntry(const int entry, const int role, typename FunctionEntry::Getter getter) + { + addEntryInternal(new FunctionEntry(getter, nullptr), entry, role); + } + template + typename std::enable_if::value, void>::type + addEntry(const int entry, const int role, T (Object::*member)) + { + addEntryInternal(new VariableEntry(member), entry, role); + } + template + void addEntry(const int entry, const int role, typename LambdaEntry::Getter lambda) + { + addEntryInternal(new LambdaEntry(lambda), entry, role); + } + + void setEntryTitle(const int entry, const QString &title) + { + m_entries[entry].first = title; + } + + void setAll(const QList 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 +#include +#include +#include +#include + +#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 + +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 +#include +#include + +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 +#include +#include + +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 +#include + +#include "FileSystem.h" +#include + +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(const QUrl &url) +{ + return QJsonValue(url.toString(QUrl::FullyEncoded)); +} +template<> +QJsonValue toJson(const QByteArray &data) +{ + return QJsonValue(QString::fromLatin1(data.toHex())); +} +template<> +QJsonValue toJson(const QDateTime &datetime) +{ + return QJsonValue(datetime.toString(Qt::ISODate)); +} +template<> +QJsonValue toJson(const QDir &dir) +{ + return QDir::current().relativeFilePath(dir.absolutePath()); +} +template<> +QJsonValue toJson(const QUuid &uuid) +{ + return uuid.toString(); +} +template<> +QJsonValue toJson(const QVariant &variant) +{ + return QJsonValue::fromVariant(variant); +} + + +template<> QByteArray ensureIsType(const QJsonValue &value, const Requirement, + const QString &what) +{ + const QString string = ensureIsType(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(const QJsonValue &value, const Requirement, const QString &what) +{ + if (!value.isArray()) + { + throw JsonException(what + " is not an array"); + } + return value.toArray(); +} + + +template<> QString ensureIsType(const QJsonValue &value, const Requirement, const QString &what) +{ + if (!value.isString()) + { + throw JsonException(what + " is not a string"); + } + return value.toString(); +} + +template<> bool ensureIsType(const QJsonValue &value, const Requirement, + const QString &what) +{ + if (!value.isBool()) + { + throw JsonException(what + " is not a bool"); + } + return value.toBool(); +} + +template<> double ensureIsType(const QJsonValue &value, const Requirement, + const QString &what) +{ + if (!value.isDouble()) + { + throw JsonException(what + " is not a double"); + } + return value.toDouble(); +} + +template<> int ensureIsType(const QJsonValue &value, const Requirement, + const QString &what) +{ + const double doubl = ensureIsType(value, Required, what); + if (fmod(doubl, 1) != 0) + { + throw JsonException(what + " is not an integer"); + } + return int(doubl); +} + +template<> QDateTime ensureIsType(const QJsonValue &value, const Requirement, + const QString &what) +{ + const QString string = ensureIsType(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(const QJsonValue &value, const Requirement, + const QString &what) +{ + const QString string = ensureIsType(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(const QJsonValue &value, const Requirement, const QString &what) +{ + const QString string = ensureIsType(value, Required, what); + return QDir::current().absoluteFilePath(string); +} + +template<> QUuid ensureIsType(const QJsonValue &value, const Requirement, const QString &what) +{ + const QString string = ensureIsType(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(const QJsonValue &value, const Requirement, const QString &what) +{ + if (!value.isObject()) + { + throw JsonException(what + " is not an object"); + } + return value.toObject(); +} + +template<> QVariant ensureIsType(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(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 +#include +#include +#include +#include +#include +#include +#include +#include + +#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 +void writeObjectList(QJsonObject & to, QString key, QList> values) +{ + if (!values.isEmpty()) + { + QJsonArray array; + for (auto value: values) + { + array.append(value->toJson()); + } + to.insert(key, array); + } +} +template +void writeObjectList(QJsonObject & to, QString key, QList values) +{ + if (!values.isEmpty()) + { + QJsonArray array; + for (auto value: values) + { + array.append(value.toJson()); + } + to.insert(key, array); + } +} + +template +QJsonValue toJson(const T &t) +{ + return QJsonValue(t); +} +template<> +QJsonValue toJson(const QUrl &url); +template<> +QJsonValue toJson(const QByteArray &data); +template<> +QJsonValue toJson(const QDateTime &datetime); +template<> +QJsonValue toJson(const QDir &dir); +template<> +QJsonValue toJson(const QUuid &uuid); +template<> +QJsonValue toJson(const QVariant &variant); + +template +QJsonArray toJsonArray(const QList &container) +{ + QJsonArray array; + for (const T item : container) + { + array.append(toJson(item)); + } + return array; +} + +////////////////// READING //////////////////// + +template +T ensureIsType(const QJsonValue &value, const Requirement requirement = Required, const QString &what = "Value"); + +template<> double ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> bool ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> int ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QJsonObject ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QJsonArray ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QJsonValue ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QByteArray ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QDateTime ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QVariant ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QString ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QUuid ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QDir ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QUrl ensureIsType(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 +T ensureIsType(const QJsonValue &value, const T default_, const QString &what = "Value") +{ + if (value.isUndefined()) + { + return default_; + } + return ensureIsType(value, Required, what); +} +template +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(parent.value(key), requirement, localWhat); +} +template +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(parent.value(key), default_, localWhat); +} + +template +QList ensureIsArrayOf(const QJsonDocument &doc) +{ + const QJsonArray array = ensureArray(doc); + QList out; + for (const QJsonValue val : array) + { + out.append(ensureIsType(val, Required, "Document")); + } + return out; +} +template +QList ensureIsArrayOf(const QJsonValue &value, const Requirement = Required, + const QString &what = "Value") +{ + const QJsonArray array = ensureIsType(value, Required, what); + QList out; + for (const QJsonValue val : array) + { + out.append(ensureIsType(val, Required, what)); + } + return out; +} +template +QList ensureIsArrayOf(const QJsonValue &value, const QList default_, + const QString &what = "Value") +{ + if (value.isUndefined()) + { + return default_; + } + return ensureIsArrayOf(value, Required, what); +} +template +QList 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(parent.value(key), requirement, localWhat); +} +template +QList ensureIsArrayOf(const QJsonObject &parent, const QString &key, + const QList &default_, const QString &what = "__placeholder__") +{ + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) + { + return default_; + } + return ensureIsArrayOf(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(value, requirement, what); } \ + inline TYPE ensure##NAME(const QJsonValue &value, const TYPE default_, const QString &what = "Value") \ +{ return ensureIsType(value, default_, what); } \ + inline TYPE ensure##NAME(const QJsonObject &parent, const QString &key, const Requirement requirement = Required, const QString &what = "__placeholder__") \ +{ return ensureIsType(parent, key, requirement, what); } \ + inline TYPE ensure##NAME(const QJsonObject &parent, const QString &key, const TYPE default_, const QString &what = "__placeholder") \ +{ return ensureIsType(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 -#include -#include - -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 -#include -#include -#include - -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 -#include -#include -#include -#include -#include -#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 -void writeObjectList(QJsonObject & to, QString key, QList> values) -{ - if (!values.isEmpty()) - { - QJsonArray array; - for (auto value: values) - { - array.append(value->toJson()); - } - to.insert(key, array); - } -} -template -void writeObjectList(QJsonObject & to, QString key, QList 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 + QObjectPtr(const QObjectPtr &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 #include @@ -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 @@ -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 #include #include +#include #include #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::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 -#include "MMCJson.h" +#include "Json.h" #include #include #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 #include #include -#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 #include #include "ParseUtils.h" -#include 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 #include @@ -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 @@ -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 #include #include +#include + #include #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 #include +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 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 +#include + +QString IconResourceHandler::m_theme = "multimc"; +QList> 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 ptr = handler.lock(); + if (ptr) + { + ptr->setResult(ptr->get()); + } + } +} + +void IconResourceHandler::init(std::shared_ptr &ptr) +{ + m_iconHandlers.append(std::dynamic_pointer_cast(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 + +#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 &ptr) override; + + QString m_key; + static QString m_theme; + static QList> 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 + +#include "WebResourceHandler.h" +#include "IconResourceHandler.h" +#include "ResourceObserver.h" + +QMap(const QString &)>> Resource::m_handlers; +QMap, std::function> Resource::m_transfomers; +QMap> Resource::m_resources; + +Resource::Resource(const QString &resource) +{ + if (!m_handlers.contains("web")) + { + registerHandler("web"); + } + if (!m_handlers.contains("icon")) + { + registerHandler("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(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(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 +#include +#include +#include +#include + +#include "ResourceObserver.h" + +class ResourceHandler; + +namespace Detail +{ +template struct Function : public Function {}; +template struct Function : public Function {}; +template struct Function +{ + using ReturnType = Ret; + using Argument = Arg; +}; +template struct Function : public Function {}; +template struct Function : public Function {}; +template struct Function : public Function {}; +template struct Function : public Function {}; +} + +/** 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 +{ + explicit Resource(const QString &resource); + Resource(const Resource &) = delete; + Resource(Resource &&) = delete; +public: + using Ptr = std::shared_ptr; + + ~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 + Ptr then(Func &&func) + { + using Arg = typename std::remove_cv< + typename std::remove_reference::Argument>::type + >::type; + return applyTo(new FunctionResourceObserver< + typename Detail::Function::ReturnType, + Arg, Func + >(std::forward(func))); + } + + /// Retrieve the currently active resource. If it's type is different from T a conversion will be attempted. + template + T getResource() const { return getResourceInternal(qMetaTypeId()).template value(); } + QVariant getResourceInternal(const int typeId) const; + + template + static void registerHandler(const QString &id) + { + m_handlers.insert(id, [](const QString &res) { return std::make_shared(res); }); + } + template + static void registerTransformer(Func &&func) + { + using Out = typename Detail::Function::ReturnType; + using In = typename std::remove_cv::Argument>::type>::type; + static_assert(!std::is_same::value, "It does not make sense to transform a value to itself"); + m_transfomers.insert(qMakePair(qMetaTypeId(), qMetaTypeId()), [func](const QVariant &in) + { + return QVariant::fromValue(func(in.value())); + }); + } + +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 m_observers; + std::shared_ptr m_handler = nullptr; + Ptr m_placeholder = nullptr; + + // a list of resource handler factories, registered using registerHandler + static QMap(const QString &)>> m_handlers; + // a list of resource transformers, registered using registerTransformer + static QMap, std::function> m_transfomers; + static QMap> 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 +#include + +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(""), where 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&) {} + + 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 + +#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 +#include + +#include +#include + +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) { m_resource = resource; } + +protected: + template + T get() const { return getInternal(qMetaTypeId()).template value(); } + QVariant getInternal(const int typeId) const; + +private: + std::shared_ptr 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 +class FunctionResourceObserver : public ResourceObserver +{ + std::function m_function; +public: + template + explicit FunctionResourceObserver(T &&func) + : m_function(std::forward(func)) {} + + void resourceUpdated() override + { + m_function(get()); + } +}; 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 + +#include "Resource.h" +#include "ResourceObserver.h" + +//Q_DECLARE_METATYPE(QVector) + +class ModelResourceObserver : public ResourceObserver +{ +public: + explicit ModelResourceObserver(const QModelIndex &index, const int role) + : m_index(index), m_role(role) + { + qRegisterMetaType>("QVector"); + } + + void resourceUpdated() override + { + if (m_index.isValid()) + { + QMetaObject::invokeMethod(const_cast(m_index.model()), + "dataChanged", Qt::QueuedConnection, + Q_ARG(QModelIndex, m_index), Q_ARG(QModelIndex, m_index), Q_ARG(QVector, QVector() << 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 &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 +#include + +/// 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() + 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 + static QAbstractItemModel *mixin(QAbstractItemModel *model) + { + ResourceProxyModel *proxy = new ResourceProxyModel(qMetaTypeId(), model); + proxy->setSourceModel(model); + return proxy; + } + +private: + // mutable because it needs to be available from the const data() + mutable QMap> 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 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(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 +#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 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 +#include + +#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 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 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 &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 StandardTask::wrapDownload(const QString &name, std::shared_ptr 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 +#include + +#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 other); + void runTaskNonBlocking(QObjectPtr 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 &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> m_pendingTasks; // only used to check if the object was deleted + QList> m_pendingTaskPtrs; + + QObjectPtr wrapDownload(const QString &name, std::shared_ptr 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 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 +#include +#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 &) override + { + setResult(m_key); + } + + QString m_key; +}; +class DummyObserver : public ResourceObserver +{ +public: + void resourceUpdated() override + { + values += get(); + } + + 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("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"