From 3a8b238052163952831fb5924b2483a375e86ebd Mon Sep 17 00:00:00 2001 From: Jan Dalheimer Date: Thu, 28 May 2015 19:38:29 +0200 Subject: [PATCH] NOISSUE Various changes from multiauth that are unrelated to it --- .travis.yml | 3 +- application/CMakeLists.txt | 2 + application/MainWindow.cpp | 3 +- application/MultiMC.cpp | 34 ++ application/MultiMC.h | 5 +- application/main.cpp | 1 - application/pages/VersionPage.cpp | 10 +- application/pages/global/AccountListPage.h | 2 +- .../resources/multimc/150x150/hourglass.png | Bin 0 -> 11831 bytes .../resources/multimc/16x16/hourglass.png | Bin 0 -> 705 bytes .../resources/multimc/22x22/hourglass.png | Bin 0 -> 1037 bytes .../resources/multimc/32x32/hourglass.png | Bin 0 -> 1574 bytes .../resources/multimc/48x48/hourglass.png | Bin 0 -> 2679 bytes application/resources/multimc/index.theme | 3 + application/resources/multimc/multimc.qrc | 7 + application/widgets/ProgressWidget.cpp | 74 +++ application/widgets/ProgressWidget.h | 32 ++ logic/AbstractCommonModel.cpp | 133 +++++ logic/AbstractCommonModel.h | 462 ++++++++++++++++++ logic/BaseConfigObject.cpp | 119 +++++ logic/BaseConfigObject.h | 50 ++ logic/CMakeLists.txt | 30 +- logic/Env.cpp | 3 +- logic/Exception.h | 41 ++ logic/FileSystem.cpp | 56 +++ logic/FileSystem.h | 13 + logic/Json.cpp | 278 +++++++++++ logic/Json.h | 239 +++++++++ logic/MMCError.h | 25 - logic/MMCJson.cpp | 142 ------ logic/MMCJson.h | 101 ---- logic/QObjectPtr.h | 5 + logic/forge/ForgeInstaller.cpp | 3 +- logic/liteloader/LiteLoaderInstaller.cpp | 3 +- logic/liteloader/LiteLoaderVersionList.cpp | 4 +- logic/minecraft/JarMod.cpp | 4 +- logic/minecraft/MinecraftProfile.cpp | 7 +- logic/minecraft/MinecraftVersionList.cpp | 31 +- logic/minecraft/OneSixInstance.cpp | 3 +- logic/minecraft/OneSixProfileStrategy.cpp | 4 +- logic/minecraft/OneSixUpdate.cpp | 3 +- logic/minecraft/ParseUtils.cpp | 1 - logic/minecraft/ProfileUtils.cpp | 10 +- logic/minecraft/RawLibrary.cpp | 6 +- logic/minecraft/VersionBuildError.h | 8 +- logic/minecraft/VersionFile.cpp | 4 +- logic/minecraft/VersionFile.h | 3 +- logic/net/CacheDownload.h | 29 ++ logic/resources/IconResourceHandler.cpp | 60 +++ logic/resources/IconResourceHandler.h | 22 + logic/resources/Resource.cpp | 121 +++++ logic/resources/Resource.h | 116 +++++ logic/resources/ResourceHandler.cpp | 28 ++ logic/resources/ResourceHandler.h | 33 ++ logic/resources/ResourceObserver.cpp | 55 +++ logic/resources/ResourceObserver.h | 67 +++ logic/resources/ResourceProxyModel.cpp | 103 ++++ logic/resources/ResourceProxyModel.h | 36 ++ logic/resources/WebResourceHandler.cpp | 67 +++ logic/resources/WebResourceHandler.h | 23 + logic/tasks/StandardTask.cpp | 120 +++++ logic/tasks/StandardTask.h | 43 ++ logic/tasks/Task.cpp | 1 + logic/tasks/Task.h | 2 + tests/tst_Resource.cpp | 101 ++++ 65 files changed, 2661 insertions(+), 333 deletions(-) create mode 100644 application/resources/multimc/150x150/hourglass.png create mode 100644 application/resources/multimc/16x16/hourglass.png create mode 100644 application/resources/multimc/22x22/hourglass.png create mode 100644 application/resources/multimc/32x32/hourglass.png create mode 100644 application/resources/multimc/48x48/hourglass.png create mode 100644 application/widgets/ProgressWidget.cpp create mode 100644 application/widgets/ProgressWidget.h create mode 100644 logic/AbstractCommonModel.cpp create mode 100644 logic/AbstractCommonModel.h create mode 100644 logic/BaseConfigObject.cpp create mode 100644 logic/BaseConfigObject.h create mode 100644 logic/Exception.h create mode 100644 logic/FileSystem.cpp create mode 100644 logic/FileSystem.h create mode 100644 logic/Json.cpp create mode 100644 logic/Json.h delete mode 100644 logic/MMCError.h delete mode 100644 logic/MMCJson.cpp delete mode 100644 logic/MMCJson.h create mode 100644 logic/resources/IconResourceHandler.cpp create mode 100644 logic/resources/IconResourceHandler.h create mode 100644 logic/resources/Resource.cpp create mode 100644 logic/resources/Resource.h create mode 100644 logic/resources/ResourceHandler.cpp create mode 100644 logic/resources/ResourceHandler.h create mode 100644 logic/resources/ResourceObserver.cpp create mode 100644 logic/resources/ResourceObserver.h create mode 100644 logic/resources/ResourceProxyModel.cpp create mode 100644 logic/resources/ResourceProxyModel.h create mode 100644 logic/resources/WebResourceHandler.cpp create mode 100644 logic/resources/WebResourceHandler.h create mode 100644 logic/tasks/StandardTask.cpp create mode 100644 logic/tasks/StandardTask.h create mode 100644 tests/tst_Resource.cpp diff --git a/.travis.yml b/.travis.yml index ad0bdee50..9ed7a0454 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 d7cb57776..d3962819d 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 9ff120bdf..99c94bf83 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 39cc85037..2c6b387c8 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 8215e4adf..e4a541153 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 111a61acc..12c97f096 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 cbb5c107b..efc0b4460 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 bfadc1bda..7803e044a 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 0000000000000000000000000000000000000000..f2623d1e0b71312774db3cc9082d634ffe0238e3 GIT binary patch literal 11831 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 L`njxgN@xNA-4Q-0 literal 0 HcmV?d00001 diff --git a/application/resources/multimc/22x22/hourglass.png b/application/resources/multimc/22x22/hourglass.png new file mode 100644 index 0000000000000000000000000000000000000000..8cb343ac82027d1629b82a86968adc1d33facac2 GIT binary patch literal 1037 zcmeAS@N?(olHy`uVBq!ia0y~yU=Rag4mJh`h9g^YtQi;>Sc;uILpV4%IBGajIv5xj zI14-?iy0UgVnCR&UHwb}0|SF(iEBhjaDG}zd16s2LwR|*US?i)adKios$PCk`s{Z$ zQVa~tU7jwEAr`$$FGXj_gi5e~_&?Klv+?fDn?1|T4YPBE*IwKzCMIomTjSC!f!A9_ zMON`IO^SAmf5qFS(7~UzR_TWdhh66aI}sNr-i|L#nbSKDZe08EQpTmTsqd%1d3WYr zT3R1}toN>WX}op%?B|aEKW=&7vc<4%t0zwc<#6X0WM5p;4mvGwXE-H2r; zeJ;18&eRCI@g}6?m9&Y-{T&N;v^3|--mKj->x!ABDyQFa;Z3JmBg-78rM_6oWVF)4 zXr=0s$%QdHf`XkNdiVq{wqMIF{`KnlXI-Zj9?VU?Ju@?)QAo{7Ls-UU@uCF5?({1Q zZ!d^kopkw=&%O0#i+mG(d!>~5ZU20E^lswEiyPg|j!&CDStB}g<*A?`(ahT))WR(- z69ck;dh6QHx%6_Q+hR=~hWqu^uhu`@+wO1w>q;^EoNd>w$}Rt@A4uxizr;K{Xp4=!|CALol-x=e8XpI39|IjMbIw#?1@Kli%F2ODJ9jjA>IA2!B(xTl_+xAgPY01KyEPp{b)YRF8qX4+oAC;7Z_E#vj| zchBgZ(M|cKzU-KNxm4B99n0LJ+h+fJTz~5B)~}0y%i9|iMK5?#%)Lvs(DP;5`AH8Y z?poHBJ-Q>vFn`ObZ8Ar{RkxMhwcV0^f8N7J2KTqG!{2|Kb*cG#?92U2-9NM+n)r_Q z@V>JL`RCu=e?H#S_r{rh3*9vn-Q(ZPWBYKVE%D^tl`MOV!oGN2wiBIsQ;T~`#h&m! zyZ?t|Yxix>|D@feabTKDlIEh+ohm_wr)EwqQk$o(@=Q41KkekxDX)9EpV!aH<91hS zJ~*B4t?kZSZ+CunVJ6jW&FVfPD|9<;E{7ZG%#PV|`D3N^MvG|cpT_kc`{z7gu*{7+ zdv5)Iu2cLWUxY5}9rc>>c*cQXH_cS0^TzE82C?ypM{A~u7KKhbZJBdONAQ3egZTkp zcR?<98-|$IXA68qO-!@4$g(mBIL%@B#hAl*fqMbFgF1tt!`ixCEDf#<;f#OINv!@W zF2Qhz!KU;=d-iov29ehV>-MEdCa>;2c3<`FloY$G;ma5p7#KWV{an^LB{Ts5GY{WY literal 0 HcmV?d00001 diff --git a/application/resources/multimc/32x32/hourglass.png b/application/resources/multimc/32x32/hourglass.png new file mode 100644 index 0000000000000000000000000000000000000000..a558ec997557f7d806245c60ae81554e0229dd57 GIT binary patch literal 1574 zcmeAS@N?(olHy`uVBq!ia0y~yU{C;I4mJh`hT^KKFANL}EX7WqAsieW95oy%9SjT% zoCO|{#S9D#tsu9gP2 zNHH+5F7|YB42fvnJ3Tr_Bvs`2|NCd&oO$zR+wEN4+=U_Ztu$R-py1boU!wZQ~7r5>iauo z<#%;*lrU;!-Qc`Nrnc&KB!l1-!`J^eH>Y7u#f~{sQNKs02y}G2* zQYY@!n^$)Y1REx>zx(Jbef`XP%N>yuRXMvJwLB^~w9aCI#j*3EsWyxXa{Wqj!Gis+ z$Ag0=PI%O@blGgV_3LdUKDc+WX59XGwDG{V@BF#~YeFP-nhOm&4Y_5Mg(MCh6J(h; zF*D^vh;z!#!zo>dryLcinEPe3xp&jyOjV1(LIr>BhL2`@c9fO=RxVAN8!V>c*r57h zk0#fd4V-%vJNG1tSKhs{xys0FmHMK^Q8q?PR!($W`@E>fzVK|V?SuDm^Pj7IW`3fe z^(6DN|GwV`zujUr_P)8WPdfeT7BikC#bb&*Ns3Bb#v2S~NIp8)njU7k_iQhs_H}d8>Puu~UwkdE3)&*sRl)X-%la{^`f+Wa zuo$uAoJ-}0r!ZFRvD^Nn;BDr;4XRl;V^^)-n3~{uIx6qvCsAv8mIXK3H~56aRM)<~ zr`YGJb@$VPoUEIUqP}lC8#fni*nGD1*Y|U}|2Z3uzt$ENz0P?4mHFvxt+hLgSGjw6 z9dUF{5iHF(BC*5d(wE0CwkH1ddGO@RzTnvRqFez5+!FeEv-iLHYT5hsU8}cv$d24Y z_OqAVk_wKwc;oJ!3j&X`)2_JL#@(MB%=O^ouU9ftzxwOPS!`dJdQqp^X!1_?#|1{& zM;8TL&0eo1I4@n_Uy~vE&pKD#^X6+J?RK=+G>o>dp>)+o@Vf0w{ z>Z$+QPhE=+C{Mkrel@>h;VNl|&r=vVnl#L=?qp!-{Id6QefgO+VwV`6H0@CrHrc*y z-(8FKo-gc*8D%ytNj<)z=d{sGb-!mS_l{Y*@-1qP%-lY8<0=p1N1Zn^+=MPX`S>fU znX$gw?*GYeExPmTeq5XPqYtJ=`cFon5*S%dY#ksm>i86p@=7(5va8Il>47@QcKTuOFlFdUO$ z4q!`QYp`N4Wmxt8M5d(%tAkqj2eSr224{vg#u)YlgJVH1Q+DLfb&8(A{D|=eLr1v^ m^MdC=r)7B=(iHF2Gh6hm4k@|H^n-zcfx*+&&t;ucLK6T9*2nPx literal 0 HcmV?d00001 diff --git a/application/resources/multimc/48x48/hourglass.png b/application/resources/multimc/48x48/hourglass.png new file mode 100644 index 0000000000000000000000000000000000000000..8f10ab7ae60980975c3734e7ce0b0e8e295a3d7e GIT binary patch literal 2679 zcmeAS@N?(olHy`uVBq!ia0y~yU@!n-4mJh`hH$2z?Fyl8e=GC;Odyas^yksw#^-BbRA~ zwaoAe3X2R2OuZ74)U$q%PmrqNESI$^Sv@P3dYZHzWM3p`(bsIv$Jh8lAVBWk!@Kv+ zojY^xTuJ%A8@7%vH}6JkO+6LWv`X6kN51x1(s$^>saq@4_tb4>DyAxjoiN=f3eLeTO(^S+uXv(Ec!m(DYXSt>nY9dD72fi+{E>cKZGC%T#^9S+dp^B2_6 z-I%}QmC6FE;)ARVQ?dojw{F?`+Uoz`Wd3{BdS4G7<9eueP_ogGDV@bP`N15=$1+mM z7fkvZjtO#PT(EFH<)@UYo@mW^V;SdhzXL|?2{Sn(!erLXol^g5cAd)8WZzR4_U}(W z%w{YeV|o0*x3#m)OMK#l};Zk1!?$C}=PUaRqs3ymbB{ z*Ye)^N9K~?Oc@!INtunww|8&5Gf8{d9u9^B-`1_0*OqZR_VKH%_p|Qo5xJZen{p*| z%CeI%0O(?)qmxdpnbo$uj6myliU8t#e?Zo zh{EKFMvO*NL(~rVu%;Fn1Wzg}jmFWu08#C~t(on7B6VjdsiIB;ZEK<2u)mg-dx%kA@8gV=v<%e@t^(bvS8 zbAQXr6+-`JpI;Mao3Olg;w%31C3=S|~OTg1x|V9ctF-rA$gHfr@!U!C?`!10pUXZUr@Z9<(YevvXrURw63q}1R2_`;-H+NVUUV#`WO?cVG_Y7Zf{a^eg;l9bToan$0 zD;{k9zi!R^x*Hw)qrZQ;ynkPyXU*R7Z1-}*H)5?0$=SEpdubon&-(c(*YmnnoZLnK zp8tnZj76TPFP-nV>=*ly1M?P7j?sR4=WKPfveeKi9!I`}TRK zr;n$f+gr8t!(9H?>G2*{A6}Tib78`jOoxm!n=JP{thm#ie_QyS+tbo)uQPW|*YEjp zi#9+!i)T#bGy?_B)HTr$jsfY5fF9TAobasSEm;Tf7L7veZsvk=ep(d zIcwHkytL6${pa*)SN6N9yjNV*R48)iLinSHzqiIB5($`RKw&Apx^X)-gVOmqe?Oo=@6jfd=Uh-mg z>jMTZl?UFIdIIaXb@FyG9CS#NpZ~3O66>Ce&yHyIF|E*NVGmGXTpBW0jA!#q@hQ$) zti6UyY6Art3T8eORD7>IQ&3Zft!35%$wUFhAgv>7MZU@X-LQbQ_4AYvhpc~pex~0& zI@kM?UDTY*X8->!YrAQF-FMZp?N(L`vI82HubSaMYo1!~(uIZs>T^rDCLiN%cJ2wh zG}&ueM~%M1pO8+E$fffpYi*yb{z}==Y(~k^=(87JthJ4emHvEi@^!B}7UpFq_mn4253Js1ODCKw_$5^QlY z+wZKP^jLcHH~+Zu9cdr;Jv=4KAg3wS*w*&b^zt&lE|tEwDRb-h{tw={!T(Bg`pz_# zhs$Iv`dE)|duSmR)W>^#+XIW4Pv$?55SZ7@eb{*``yK39o_4=fFpE(2qIU9-?CK{jG zl(Uz|LBb((S^y`5?98iisg27SZZJ$@@L}j&!I_E8r&c;(RON-%C&5d9D`3ojTaPWqK8i@>^u6{1- HoD!M48x48/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 000000000..7b51eca02 --- /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 000000000..08d8a157c --- /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 000000000..71d758295 --- /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 000000000..31b86a23c --- /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 000000000..ff698ad0c --- /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 000000000..1c96b3d1d --- /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 de1940ad8..d91fc694e 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 0607c7eaa..2f26f2117 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 000000000..2664910eb --- /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 000000000..b8d82c513 --- /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 000000000..e70f31656 --- /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 000000000..460559094 --- /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 000000000..d22aa6062 --- /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 e81054a67..000000000 --- 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 23af4fff1..000000000 --- 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 dc0b42245..000000000 --- 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 2bde1bd81..32e59bd9c 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 18527c49f..32ce57886 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 9a06d6204..e255921fa 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 8b3c13e03..21bb4f503 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 bf711c1fc..bf985707b 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 0661aec13..1baf008ef 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 c20534e93..44be281b9 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 ffccc2597..b7937e31f 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 173cd4d66..f5de690b1 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 485727ecd..8463ead61 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 49e0e0caf..8fccf403c 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 3eaca9207..68fe0f148 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 c4cd97a11..908833128 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 ae4798518..fda453e5f 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 227ba8bef..426cba8c2 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 dd5c962fd..e5ce40266 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 49d2d99fa..7f95a69dc 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 000000000..d47dcc3d8 --- /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 000000000..dedfecb29 --- /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 000000000..16ed3d2d8 --- /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 000000000..d566b2a24 --- /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 000000000..46a4422c3 --- /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 000000000..c1105efc2 --- /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 000000000..4f168fd24 --- /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 000000000..27430d426 --- /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 000000000..6ff113670 --- /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 000000000..9db09545a --- /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 000000000..7ced5bc66 --- /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 000000000..88804af35 --- /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 000000000..3201d674b --- /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 000000000..6f283dcd0 --- /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 8fed810b7..eaeff4c24 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 3ab85d7df..93ca620d4 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 000000000..ba6f05095 --- /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"