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 ad0bdee5..9ed7a045 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,6 @@ before_script: - cd build - cmake -DCMAKE_PREFIX_PATH=/opt/qt53/lib/cmake .. script: - - make -j4 - - make test ARGS="-V" + - make -j4 && make test ARGS="-V" notifications: email: false diff --git a/application/CMakeLists.txt b/application/CMakeLists.txt index d7cb5777..d3962819 100644 --- a/application/CMakeLists.txt +++ b/application/CMakeLists.txt @@ -251,6 +251,8 @@ SET(MULTIMC_SOURCES widgets/ServerStatus.h widgets/VersionListView.cpp widgets/VersionListView.h + widgets/ProgressWidget.h + widgets/ProgressWidget.cpp # GUI - instance group view diff --git a/application/MainWindow.cpp b/application/MainWindow.cpp index 9ff120bd..99c94bf8 100644 --- a/application/MainWindow.cpp +++ b/application/MainWindow.cpp @@ -383,6 +383,7 @@ namespace Ui { #include "JavaCommon.h" #include "InstancePageProvider.h" #include "minecraft/SkinUtils.h" +#include "resources/Resource.h" //#include "minecraft/LegacyInstance.h" @@ -1758,7 +1759,7 @@ void MainWindow::launchInstance(InstancePtr instance, AuthSessionPtr session, this->hide(); console = new ConsoleWindow(proc); - connect(console, SIGNAL(isClosing()), this, SLOT(instanceEnded())); + connect(console, &ConsoleWindow::isClosing, this, &MainWindow::instanceEnded); proc->setHeader("MultiMC version: " + BuildConfig.printableVersionString() + "\n\n"); proc->arm(); diff --git a/application/MultiMC.cpp b/application/MultiMC.cpp index 39cc8503..2c6b387c 100644 --- a/application/MultiMC.cpp +++ b/application/MultiMC.cpp @@ -40,6 +40,8 @@ #include "settings/Setting.h" #include "trans/TranslationDownloader.h" +#include "resources/Resource.h" +#include "resources/IconResourceHandler.h" #include "ftb/FTBPlugin.h" @@ -331,6 +333,37 @@ void MultiMC::initIcons() { ENV.m_icons->directoryChanged(value.toString()); }); + + Resource::registerTransformer([](const QVariantMap &map) -> QIcon + { + QIcon icon; + for (auto it = map.constBegin(); it != map.constEnd(); ++it) + { + icon.addFile(it.key(), QSize(it.value().toInt(), it.value().toInt())); + } + return icon; + }); + Resource::registerTransformer([](const QVariantMap &map) -> QPixmap + { + QVariantList sizes = map.values(); + if (sizes.isEmpty()) + { + return QPixmap(); + } + std::sort(sizes.begin(), sizes.end()); + if (sizes.last().toInt() != -1) // only scalable available + { + return QPixmap(map.key(sizes.last())); + } + else + { + return QPixmap(); + } + }); + Resource::registerTransformer([](const QByteArray &data) -> QPixmap + { return QPixmap::fromImage(QImage::fromData(data)); }); + Resource::registerTransformer([](const QByteArray &data) -> QIcon + { return QIcon(QPixmap::fromImage(QImage::fromData(data))); }); } @@ -610,6 +643,7 @@ void MultiMC::installUpdates(const QString updateFilesDir, UpdateFlags flags) void MultiMC::setIconTheme(const QString& name) { XdgIcon::setThemeName(name); + IconResourceHandler::setTheme(name); } QIcon MultiMC::getThemedIcon(const QString& name) diff --git a/application/MultiMC.h b/application/MultiMC.h index 8215e4ad..e4a54115 100644 --- a/application/MultiMC.h +++ b/application/MultiMC.h @@ -146,13 +146,10 @@ private slots: private: void initLogger(); - void initIcons(); - void initGlobalSettings(bool test_mode); - void initTranslations(); - void initSSL(); + void initSSL(); private: friend class UpdateCheckerTest; diff --git a/application/main.cpp b/application/main.cpp index 111a61ac..12c97f09 100644 --- a/application/main.cpp +++ b/application/main.cpp @@ -13,7 +13,6 @@ int main_gui(MultiMC &app) mainWin.checkInstancePathForProblems(); return app.exec(); } - int main(int argc, char *argv[]) { // initialize Qt diff --git a/application/pages/VersionPage.cpp b/application/pages/VersionPage.cpp index cbb5c107..efc0b446 100644 --- a/application/pages/VersionPage.cpp +++ b/application/pages/VersionPage.cpp @@ -47,7 +47,7 @@ #include #include #include "icons/IconList.h" - +#include "Exception.h" QIcon VersionPage::icon() const { @@ -118,7 +118,7 @@ bool VersionPage::reloadMinecraftProfile() m_inst->reloadProfile(); return true; } - catch (MMCError &e) + catch (Exception &e) { QMessageBox::critical(this, tr("Error"), e.cause()); return false; @@ -199,7 +199,7 @@ void VersionPage::on_resetOrderBtn_clicked() { m_version->resetOrder(); } - catch (MMCError &e) + catch (Exception &e) { QMessageBox::critical(this, tr("Error"), e.cause()); } @@ -212,7 +212,7 @@ void VersionPage::on_moveUpBtn_clicked() { m_version->move(currentRow(), MinecraftProfile::MoveUp); } - catch (MMCError &e) + catch (Exception &e) { QMessageBox::critical(this, tr("Error"), e.cause()); } @@ -225,7 +225,7 @@ void VersionPage::on_moveDownBtn_clicked() { m_version->move(currentRow(), MinecraftProfile::MoveDown); } - catch (MMCError &e) + catch (Exception &e) { QMessageBox::critical(this, tr("Error"), e.cause()); } diff --git a/application/pages/global/AccountListPage.h b/application/pages/global/AccountListPage.h index bfadc1bd..7803e044 100644 --- a/application/pages/global/AccountListPage.h +++ b/application/pages/global/AccountListPage.h @@ -21,7 +21,7 @@ #include "pages/BasePage.h" #include "auth/MojangAccountList.h" -#include +#include "MultiMC.h" namespace Ui { diff --git a/application/resources/multimc/150x150/hourglass.png b/application/resources/multimc/150x150/hourglass.png new file mode 100644 index 0000000000000000000000000000000000000000..f2623d1e0b71312774db3cc9082d634ffe0238e3 GIT binary patch literal 11831 zcmX9^1ytPL(+%!!3&pLt7I&B8PH}g4U);60EKn9oacFV(Qi>I~0;M<EvG zVCQJ=iiTbQ0PpeN1{X&!#|8V5)LYKLTiean+t0$&2JrLq<8W|w^0KmUx8ZQ}v@7^0 zN(KPbfRe0~j(_1vh+VF=Lcl*GAL-G&gE8KMul%jws`W5R6Qfb_sjcYR+#9)-zIPaJ zBl5&xAj<|OC0RR5nvX9nS+`}dM`6LgmyNE_w6BHE?~>d4`f81Osp*E?j(+(k#4Ezv zxx%&H-FbRh__?Y_xb&g0wYn#;T8%QT;2_HEmHTlHN&^!_xQ+ZY1?S?V-wxZW;Ai8B{KQW+1BsJ+#c zt^6km@O?6+xZ*cG1%JoQt^*OEv`}bI9=@^?HSXJTAcOi@QZf`mrM3h}oDeAW+a;*U z%%cjXE10XqiYJ#Yi>2H4)HxVD5@MSMAXk? z5riVMG@_@P&}UWQTt#$NLk7dxCi9G{{RsY15c zm#*(_``h7M-9c->p7Cb{kIDXI$2IjgZ~v+nLxLwIL#bT~|B}zY0~DG09I9}PBo7*F zwRul?8#mBK3kSrL-f)4=jGlw6Ha1#W{LPM(YdXfT#&kMx`htPBo8ba>VCnoQsNv}R zbg^1d6^+>#kkoHOjbzp@;H)=`51!K={h*b6b$gu;a0TDpcMw(NY5LgFR_xi&W^@qA zmqCVB&`+pO)>}7_z%N;xRaPGPgh}Dt$nY{sOL#6Jb#*q<6!BT(wj;nXGWap{2^Ei- zZRN}pYIbXptJh{a?8w43Vv)-an49%rB^b=xg?m72%fV3U_C*v0 zawfA_E7^e!b>9k@rz}t89vuh?($gUy8w3!+<6g@ut{lZXI&%dEG=MsS0TSRAFj&1_ zuo3W)BDpmp6VVF!rv_qSK?&G-6Qe>hOHhYSLsA6`Hxa%=H#tfQ{_vgdm<)PSrTi}? zvL$+a7Rf}&^aVV#*(hzTiPFwEYER0J^pC2f%{$@jy*Wi_R2d%&%770|7f+Oa_@2&Y z0Fl2MDMUMBFOymn=N{c+Q4y3%adI7HncY*{y4RG~TR6TaNI@nQ&F1$%%GL1*vo`xg;4fDhcaXn$kh?i)+0;v#9x_DCX5 z!Z+i?3um`*b8)Fr*D|DQi90x2Rz?y2gMJvtF+pXf7DK73&~HzJfUk@d4-$(Zdv^|7 zeFX5oW(U5UH3ZSWyBm+}E{(AJrWz0`fpnH{%&#qOhjUD*=|+Lk?!3D=rX!5Dc?6is z%L;nm#d+8WuRE^b5ys$bcD9;C4xCAunehaRjZb%E*P9@{s|UN#p2`+jxVsNVY0g$u z2;HJBa$UVJVn#-CBhxU-YMLz-YOID76gCt!q>9e)GmG7?AtFy=5stfv1>{&slgA*s zzX7Z7G?DTR)Ryl_gmf*VrGoygNN05C3z)FlZV z+jkq@VtYa@I4o&40QNm|+!lg-<^_DWrrHA zQco78zICV@PEv3&+K2x`UW9lvu|dUT5}Hucu1hmBhYFrFHe zoT>PXEFz|mR_nIt0-J6tujeE+BrF9sK(XRi&cNvwJz(DdbZ^jM%?*?|%>2gbe z!ZO>X;)%pjBJ0U;DqyDy+iLqyl>h9J={*H)pzbEIARvylIh(A^QUdokCNFK=78~AQ zmnZ9Aj0DzSC!hp9Ha6U6OKX{%HeJKzwR+KeIMm5RJ+YtkW5!&g$LFW-Ev5Z-H>t^U zdnARVmNc_*OEC(vN8cRYf;Ujkyg3TK%r)A7j*4N>BZAF<0Z-No@zV+`8-@ZBo=*Gm zT;R7i#Ldy6j~DkRy4>0v==SzRjd)}`#2v55}_u-6H0Lcz^sJ# zoWlEClF6UwWQTU;!-D8-$&ysXEXqFdIp$fcYNTv;37UsixgrT;y(4H0HAwcCnMx** z`g_Sh03ge=MgY$lLx`eM34pBgpz$=ROa5*HyW6Hc<;%`r)FE*HJ8tGzww zk_G%5`cphb*!eLAWUp|52JOF;6OyciaTcOQLDAzygBTA>r#-ZCm5=Ay4Gu+V%V2{g zQnXfi6*qhP{du^{u(?{v&+)c&dMCw4n9f4VhA< z_wy`4H|UAd`-L!!V#YoTTVTcX+%!1vz_c3zy3HvCQyuX_$fU5%Mv zwp1X~Wh&aBbdAp+Af7u=j8CrtN_weerH@1Jpn;uPhr-BJ0=Mwi$T@>0TO z#$tL0A&7*bsO6j8Pj&WP;&Kqu_Z?+3220j{!50Yncr*AeFOD1qV~byn?IJ4Z^*9R7 zfnZWVam@dvZ`(?XW$%11b*XNzzR%WZ4VCgEXOJ4Lt(@Lf>$f*pb0U8W8v`h|WZjG^ z8{9U2JG#gLJsMM#xxt0~wlqV0D^HRf1%kQjK>P!2 zB&@mTT?mahoH;6R5eqRs&XmW9MV23&I;LVLVwI-1)Qh`Bvtu>8F&+WEAFBEO3u3VD zRBO{cBP;6MQT+7%G1Q1zw}|%g+BSb$fFM8LAaFQ86#(J}Mj`j=`pkTxr&GQS+X0Js z89KDKi@Tfzl5|L|Ym{HGs0;JfLd=61x*$ZM9_Wm#>4 z_uEu(q6omwc2G7+>`j)!XyQVzMAv6gy{ol`;bebvpkp6ZpXHP6Pjt@1gqFFozC!L6 zThm}ur#ZH@>-*IWC0P^UqZ=4msHC(JUbPRjJa8DF4-*x68uGJLa|m`nsw-p9JBb|L zia4UT^cBPhMNu-hmCOE2fWiNhcF|LN-{JjvPfO)+kMH8!#pEQ+4d?_4+|z_{&W>^E zK|B%=7`^ER558bAd8EA0Njoc~lL&Cu4?YTS960aN{^y7$H<)b!*JFO@uM9J!b*P=S z8q9WLGj%DsL?Oqc8wnEEe+i0_%=`S7UYT+xGIyKU_VD+R8sE9XpMik3;jQ~1s^(A@BPL>==X@vVX0{QnUK_h(^tRVK!S!O)=9_QJCXpgkelM3V zK@un$J{McBu)x1JU)8gR)p*tE+lygN1rWyga*CbJ;fPKVY)klC+xfq0a7$>N{eS^5 z(Z+M|qjEO+LCff|qMOlO;S`6e?8Ntolr!mZ3L)RyoPST5T8@y(2N*E!kGw*j+eZRq z{>QBl09UZYWK8LLuPI-{_DsZ4+n?~Cl$X=)%o2Lu;~}u29o)q~c#hAcS&R-=tdlh! zFHeOW9TSfMak!Q(@D+1(C17oYMHIIJl~XxlM1X@v;Z#~}f*$UzFYMf8G5MbaMY zv&v#yv2=f=Ja2A4CJ)l>Jozi>1g;LSlP3|e3@8m9E7ICVPYp||5AEJm{a*SZ`;+|n z*d^>DMw;doy{MU>FS*@$r0C?%b~pe=W?+<10`Y7Q0BD0A>w9{)vPWW+{p^%Bwe+ zH|H%{_re7PFs;&zhc=snKLBPyu>+V0Te>;3J-IeCT0*@KMSYa}IEWIKVew$?}a z$jWDULe{Lho5@jdwfF46XoE*w`~g3t8xN?5En0a(kNrL)NH8~pk?dMV-A`fNVTq&(H8o6DMutr?xiJR$I@KSf z3n_k|7xX{&T<;iVX3Pue;=tdn%NqxUaCk@&{Ld_8fCO>_3>D`?H%DQo&%KRSC#)D5 z5+{n}gM^mXL&-L^7K6V(J|28eJ$-!WjMMSSzw8D=IddiiXHAZz2LfuXiqK&Q&;vMQ zUWG+|M9=8r1Yh2aw|llU8+;r2lY8|cl=dgdkTXQE>GFJwYU8%q2p;-?9(;Zo@s`Lv z-2ec?SPxa-Jm3jvuq4$zoVT7DyLbxYe{E>%pt9Ozr)g>93G{!(>ABo##hu^PUQZvl z{aucU$Y*!*<|rB$NNS-xmXq~gQ?BBCc1-4CtO%u&_2ybWy+3jdehi}GQ9Wvy3`7FQ!fxJM%jx0v z=qF&!Q|_hg3xl_Z1NX?~h~8WXtanUlJ0G--w}&f z+SWTW^BhW_J*tFUD)LF*PXDK|ef6J~`rv;u0=uIM_0xNA;Li??`9Bt|n#~Ks5SFD< zP70o~{L>uV$R9E|rSBSQgIYNH!&TFp{TDM7T5@Uj7-d~xr>=Zhr6qT82d|YMN~GzCW-Z_ zU4Ib(1b(KgF0Mc6>^)^Y|Ky^uh=N4+9$VMi;@1sk;v*J2Qv&@Uzgi7JgEG~su}}Q1 zBz{r)EyF)$Htt8&YuC@8cw2$y=#3YioQXB@Sc%R$!PEHi=<(RiaX7A@)0?~c-H-Pz zkm$=ihn+0a6>L>pX5$|?INwO*kwwvjFp$ zPq6~M4`M-9@eri7rqn_7lNiRKrA8SOI)umUr5J(~gDn3cmR9#&?ny*$&;C3Gu$HXO zc~m+T>j*YE=QqlI!*9Ot_nvkXaH;jb|8+L>^OvJ_On&(b3EK}o-YO174gHD=oKD*C zn$-=Sx|92pj?bl5o{mZ432MdeY=CJmj7eXt#7>cz}D!n68NN4A(i*p3Zf20U(d zzrCEgw}a77w%3c7`jN1g=~Qu~z8DG7s}UHPR4;O!DBKKYT-e*}MZB^EZCajoyN6Jr?EIl8G*cbLfWFV1lC_Vpy?US0hBXm#_^7#n=# zwfuAH&?jt?Z*ttUlvv`G5C92WIMuZum_uuW(k)_R+9=2nfzR(KC0fE(12KkQWR1OR zyXn<1cBAK+>(~3hHi;7#4CC#_e5V>LpM2|=&+j6SiabkO8sc=IzU1sDWULrRBYtZg zUiS&_|IHcF0JjX<2k)nLPhIH?{r)RXoXYBI+SN4usoL0}IyIfMTPf`##il|6YlYH# zJWc;=wirM7r_$i>84~O4)T~y0`0t3X(5HIh&&5l4a(AQHk+1m6^vHSXU~?X}boxr@ zw{K20^p*m>x7xjjCr7Udla5GdgD}13_-Ckx)t@d}SQrUqf6CE>ljQgvybT%Z(lyC; z>GXXUhzcZ>jdqM8-_$28nJV~o5_Ncoq+-VYaO6Ss~bm*q6++gIzsz=C*)}vp8*L0oWa^%gitOfv+a5tD-p6tLYHpuGn_|s=2X|gh80@p zZi@SUDzby*Y@_nPz;~w(lBM68+hU`z`T+;E6<;{idWxl9bb+K%_b5ZoC3yA$i4ZHA zR3gz#ypc`*Xdy}=hm+CSpzAh)Nv!u_*(@aNdGp1^1>iCzp8VJU>xFyq_+fMOU)w~} z@#uAF!DiT-{jUB1DK;PsMNFx_JZ!zE=Xz0&Z9Hk1zy+o7kN?qr-wRP;VT%E%5z8qs zy~|pBm=4btgbWpa$` zxMsj4ULs&4)@^64QjI>?3&wCP76=6J@=aY~M6NzYLK7#3g2KS}q_{oZ9{RPc)5?rN zqR>6T$^|*6G&^@}v1VTjqtgfvD-ZX_utBH}H=_nqGE4c7P4{@-RFImKtND*@4GvHM z<_{a{9BxlIPtxyjm7k#Aw-l92k&%>CY79hHmf2)qDK( zS2;sprdu~($8g{ca$u#6qX6|OQwSAM_5!k`V{K@E z%~IGo>0f^-e>SmC9nI?(Lf^rUb}2w(E>$f0c#+3gw)1|_>dm4;%mN#;w^@7O9?{XR zo04!OPW@fK*oOUTvmIyWd2LuzeRK15@ih$wP)M>JuWDiigH|Wd^#=IV?c_VLdi5a7 zhL3I(#VC)Sx$Ew%;?gr1EU)0U-^Ssp;Dm?ax*Q|t`|B6+M*RYg7 zzhwl5{5hhrZ zB0CSt4*Ps3_{^uh`wbV}qupB5|LK?Y(dJ>fssH5#dyXL8>w3%Wbl+1JtA2>==?L(o z&6Ab#)3;7uK^q^oWO&^X+<*Y@ngG=?t_g@@9QQ1EW`WoWW6w`hlz3o-p}VG&ebRZn z&%H+yz)h7k81wjkZmj6zn%}={o~OcYLPoLCCN93V+{8Xdy4tkaG_L(9Q#yTn#lZ>$ z+fQQn63=7~!Jn}J)j*#b+X~S?gB2IU3cO@c*VnH^>qRc8hKw9$GgBP|9>gswn8_rH zc%=;Zw$kW2#w0|GwbfL(DIm-1!#l;LHSOb5;kJvzYW^^^hd~J7s#Ni-bUDS%8m1_{ zG?2w}28;#IHVwM4^EIh%dHW9W3O&_dJAgHVsiq<{m_jzD6DSUb@d6A6biFQC!o=nMLq|Eb z-A;yr?Awxhx|9X`IIQFx>MVFG%y@&$opBqM%yIX<=~iJPZ=V!$r_-b={VwRG-I>H9 zJmQPwL`(%8G1yNUXEo}PGm$iVBCulxH(4GPSYWdsO?cs7z{B~PE}bX3B~i9u4}zpI zWwL@;iI2>5RpHX1H&LL~u6C!jmbUfT4xB}odPTe^lf99N7L>fUnvcP&i>4I*DzShC`Jbd{F%6mbLl@vR3<`U`0PQl zMqp{YL_9R%W^#^dRaw{abYp8gQL!9%J?TiFucv$t>o%zTuNR=_ucK`)zcBvG+~@1W zm6?J#YHLyur|OHA5xsR|!qJR>J5Ou57?%IPt*LEG>fw?E2hzD-D`YInK&i7Rk)H*5Mt&fg)Xu48(e|&Kr;>LB`w&jd9ckQ93}0 zwOn2W*0{CNw=ZIO%AAnv5@eZK{P7kQE@WrZ-*B_$XMeA(LX)_0;gU<5idizNm`X8; z6Z88yWB7Dy=e>tA62grvmRisvdD;HOhw~zH(=`I{d_;{G^E|adNY{{Z&t2^t}K*w}%ui>=HkR{MJ zftq_$PwXuZ7enwjn#FlmQq*$L1L>lCgh>8#ByB3c*@9X4s~#U$qobp#*q~gz0dAIEmXqF%1UR94^yo8h{VW@aq~K1 zNw&1v@BqWp7MZ%@v54MKqP`yjfH_%0UwVxGcbo$oJ9+kZKOPy`R3z`b4ip}kWYc5Y zjja1IOm^ffJ@wp!xk9Bg0EgG_Bjzk7nDQ7J0yvSh^qAUaW39{6^PvX;O zCqmpNrV`{QreBDFL5-}h1y-#%Yt|pcf8V?Q1S?NVmK262%eh}?=DzjC+5RFnxLFv- z3vY*xTvoxLLsV#3lgo(`Hrx!fz=L2Z8x_*oy*#6V4F3DD>#NT|AY94UG6dG_$Bi>a zt$h>hp>3P1)}#pT{p3DL^L!oSQBrMljU*2w`2>%OzEV`L68oNs)f#UAO&kR5mh{pI?zDh;1FF@Khp5$ zfVxvhp&WGJ4n&`kOL$YDPKEEayPJ>%DX^hV$qi=-erWjXHA?W~qmD`B(KAdGaNB(# zEkY+tH*iBfQyp(Q>kGNnF#437o&lm-L)gmZi?5RXlA%x}N%`uhi^eNOj$E}2$}L8L zr|XSDsFICfB)C*UG~tctbL5X842>>_q6lRf)0z`dLfvVi_XsK-FRO-rmA;{0{wvBX znk2#qtN;EWN>~P10AX0Gg2N=$oYHih>Xt5W)|6u!d6S~1H;J?LBH}hQQ$*_S+Q)_I zJ_zpju?KCg0RneuE%99hE;0T6SOmy>oBdsz?dokYrMChWAo3(`R2sejL?|JyaACt3 z+A7-PcLJA>zdlZ3jm%goIone;Ktz9|02X%?1+ho>eHAivET?baHiz{qh=AjVi4egK zUhNHhKGaNz`T`LLa?wNbHS@%=J?ej{b+$_RD_fp272+8_;O~3H57WXHy9rATf=tF2 znan?s*D12vzew|BO~Ujk)(WvowaU&^SXM8x$ypE*GEJE(KOZUagkx(+vnwQbym7IC zYf6e4I^{l~UzOc^7AUY@<>D^X0Ciog!}Gca1j)&0}qu=wx5T5%79mHgKJV1JlE<- zgJ3X-o-~Mrrp|b624F*vB>jo}U58JZJBUy}V=l5=e8VIMeQk8ADfgt_z{g(uB>>}S zr=^pXtpDX9Z-8JNfcU0Gr^kEr?O@1zesE?~27wm*9wTTE?!x|$WH=#GU<8~rO@mt& zD2n+&(s*h)-2zBCh@@#F2s4C*Z@WJShuqmOi%SB~AX45Fwo zbU=!0qOnD%A}_vOQ5!2Rh96~r6Wtoe8}KTH#-0qa-wmhBB?K?p;`YJXKzKz*Q@!vH z*IAzcfJ^fCd85(b^EgMe5#{KD+0&bm)RP%cAl##(pe25nc}eH~rYbqys~bl{WPwCe z2(TQU58olqH0_hbyIK=I&!5>n$}G~ZT3S+*VO zHeJnE8_IjQhq2y;zMkaRPsmD82J&*bo;w%EENB-FDvb37ONns3<>b$+&D(WpbS~5t!Lc@ z;ya;$MFG)=xfC9}Qonm5gP!~5w?fCX5nPyNvn&CO#x}Ce_wf%|-xa!pqHzX_u_HJ` zuT0u5IwSz*c$ukJ^zG3b6*ADx_1nAB1&9WQ_i63nbeZt3ezWfA1H4+Vc<%hWDn}GB zBY9Z21P-OZSl+D}tG1Tp0pvJpR(z|idXr0?ulEp&p?Soe(AV}U&2k`M5xWal*THh& zOFedvgulCHfV#M*N$V93Oler(JDVvYyk=Y=0x03khv@Wn>Nj3n&aybec;n{i+e`V+ z$Wg1c;F))(%qwHyo_{R%RGr87@5MK;2xzcdJJV8R5*cKzB{AX=K)8$pA`U>#xH06!4mi^== zlcIjc1mEhz!x^1<87@m{w@v zE8le(F6)Armj3-dZ_~%ORmo9=_t%6Kf+6RTLNf2*8`*&3@Yj(37*MCP7CC~u7feO2 z|BVDk1~$;$_gR&|GK+LLsB@O}SX z*oOu!z+oxCx<@74XFj*bc|`=`NEiEXjewpcl1c}^cwqNRkR*L&J~!`mYato4&v&W$?@S=E!HX&#M)53pyF9u?MJp6pa-OTGXLU zsyf>f(mk+F60jt3@E=40tC%Zi`hpZ^-gH()9&=kyq|Xh=w&)9r1WDr#19_-~n4d>J zug6W!D3GWVV9Btl(i4cM*GP(j)RNV6_$%U3;%zJ=jA!v&DpoLr3#$gyYRhe_icC?{ zD^|M<|NJKG)OY(dpJkB5oq|R?@h$Hw^fTRtH{Y5H-*4~cWpBRaxqQ|oM771g6AMsP zg-^?hyc7Yp%IdB2<^0tcjkYwIVv!*oPhX-$NV(PJdk((yXBkUbA}JBoWK}qg*wT)x z3<(kq_ycPs83H-QA_bSGTkoJvc5%IFUD`^;=mDP>VzlG+81x!!^XqnnD6~{miyd8r z3GTU711FIyj%Wma&RynrOf0-F$QRJ%&kdaY{Gg-lqW#=?fU18z0Tk;GX{S z#YknS`W&n4S_%?fs4{#Y?I|8FWQhbW?9;ba!ELWdL_~cP?peYja~^ zaAhuUa%Y?FJQ@H10zOGZK~y-6os-{7Q&9lNzvtY$dv~|n80HUjVU%f&Oo`kk1Sx4D zWf_rg76lQ#^it17|A8;P_12pNQg0zFQB%4MZIcFV*oLB;cXzgBZf^75dwN=IvRL$a z{PO*N`JQt)gfWIPj~I3THWF%mCwtma)w6* zt<4P+osGs-yGVJPBr@49nGV@zs$2VJVqG`B>N~l0%@G7JRkPsT$KcZvKX7hfaL5ze zNo>o6)0~V??jIDAyZUxw=KB$DWU*HKCSW1=9xvfI{BK*`oE0094o>1Qj-LJ9x~AtXBY zx@$L`Qfhfg$YRtPjTD7j*qkA1~ zF30dc9l_qv_Wa@fiJk+!#Fe!Mhc4W_Vf)$Z@%ic&G&Qw&!3@ImJhM_t1Zw3hCNH_$ zMDWBsb=Xg;|H7SHcgHThlDLA?YN4jxN_E*L7^%&*(q=qdnqP^>XXEnYrKMoBoT<9R zCa2B4y8KDyEnHF*1ZEXQCWxY$+0o35cEE}TM;(0e1b}GG1^+|U0zwD?v;a5`pc8-_ nKr?`K01*Id0KPHC3gygiGh*s5Xm>UU00000NkvXXu0mjf-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 zcmV+o1oHcdP)7oSt78^F( zDAJk`>$MdWst=`NTR~sy4T>O$4{EI<_#_BEiJ(3dL_!US^nz)ZiNUe0@zQA0&1LVG z?C#9&W@e5LT}!&|W(|HX2hPX;$2s3QgfWJ-6$(d0#mVjWxP&ec4g`b|LO{s1)v_&E zrpZjxgk_m1R@m%Rbm|*46yLa^xP*gm9oc=nn$$KqB?(Ta6SB(%S$08|W$2{>GO0K+ zsU@V7aa_1kQ$yiMOjDyjtqT|R)z9XN%Avt+W80Z&P=OLAIw%tyJfosu5sPQkq1&KY zt76mgOx~|HqD^5YbiKCh?}GMoa$-uH(Uh9iy+8vrCnwxtUknp+K8a)KSG7*m|sQ@UdL* z-X~*jrSr8=I6~Ux);)139FaQ$w|~52&og}i*RS2oF3c@t`X{EwKX)2>`M9P=TZWtd zA3rd1TcG4~3%Vt7N$FsRmgn$m;!UFpB2NAQQjk?sw{zRkf8pWVODUo}nG%)X&VDPc!n?4Abj#D-zX z-Khk;GFJ3T(>@iM*(wXPc)VARKL5fUeZIGE@+*ocz?MW}DWYY$BvLWTnU!)To+~8g z6JJke$>o&mjSJ_0oNfz8B7?z~4(&a;ZA(WF2NMD%wu2{36sRqUJkwV)ESp@k)A4yb zu{c{h`7?j&gQi5JH>)`T=YK&2HE+EP)dq3ae|7FlEx$RZ*lfGm(j0LUn7pkyLuMb)Hv zIV@i4?CRYx5rB{f?(t0<*UaxZxLJ`U35w!`;&8&QIG{Mj&lP1n0U#caA|6u_k448* zQjw72h^3~R9*WQxO-#SnO%^s_fhg2QPgI8kvRPYZzYHoUlYb^9ZAVx3bKBtd{B@VqGs%&-Z( zA&Iicam{@_;O}0a=O*(5J^1g=bbdqu*@X_%VP@!GPb$=g;M>4j>rt zhmAy}U*w3DIc&B^XP1>vHs(~@x3vH+PFB1i001yjqsNaN@3>-GAeSPD`LqJZ*yW)A z830EL@h%7e!0x`2Kg86eX1Ekl5(RF;aZY2zl}V>&1q1#+rwkw%@F%nC&@W#fJa&cS zz#gtGt+<~~vT^*e?>~Quh`uVsw_pGOAj{bC+fQF!Jk}luj==FDgQnV3@cn0nYJL3f>$16-$Toz+&(s#p@0eHMVx8!i`Y2L6UYS~??tFdKZ zNy}Pp$?_FD9|WM-hpqu0<`+H>_FSAZWUQWF|df-=B;Lk3MGm5aS!uVGv8qTj;)&R3MiS8E z@%mbW0e^cw|H<}*EuVh))}HkFeX~T-21&9(mSo7X1X+@y*zJ%NI}9xYH5x)}EQDwz zgy`69L?dC^cYe-^xGt^>2K>pQ0X$w`({rt!!)u%8%_y&`6B$fmWP*bQo+IKJNZ<%I zo{?o_k%~nqp@yKUBcz6h5e*GmS8qksqkmQP$91VC81P&7tCOzQ6tb2V=%HwxW%-yLe31wb*Th^TI>wi91?ae zTiEC!0I-Mv5Fz(t1Q5~qwP~1S7&nT^QsV$v05kwe05Je#03rZF0767`yJ!H$7z0oSUW+C2x Y12~MURCLt>_y7O^07*qoM6N<$f(h2g@c;k- 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 zcmV--3W)WIP)6vY|;eciJ&d&}+Zy}1K<3I##A2i)Esa0)6V zB%(=KrYcs1m`YMsSyoYH)Kp@Oue?lBB{7;rtt4uUsHI6Xg~11+5jc*6I}Q$k@DNZQ z-r?Qe&d$uv&UAPF*gZi++1*A{gT! zQm4G0^)@2~p7lOw44eZRXJD=kax5@A0d6J1>;%Y)gObBgEDlc*C@ryXxauUJI&;AV z0F-MGH?_32>pK7aV-MatF7?623gITf8G$hZX9P`S;G99@92#TLG>(AA5eR6|I77-# zB4sCGI|YIKfHb2r3Z9`($ zzT0QEF4mW|PpPz0zWcYX4~7mNx8g)d0?Je%laVq3GL=*&z)S^YaxhboG{J=_OEt4; ze09olubm+rdv;%6UpikrEZ+tIpmD5sKRVzI#%&T(DPJnpJh`H(kiOPF|H~-^b+{1l zK!F6hH*bE|w+Ha<{$u^VkV+|~!X=uXZ$FWe06(&ScdG)uRR|8CKmr+2ubkZXufajn z6^w{9&RB`gc|JW%9~(51iQ(l!^5+XhQ7{40*e~J--l^QW_jp|6#0bE|now!3?KvlU z;dei7g;EFlwr$#9D2{>&Y~Qr$s1p90&#YcOV5fbN_NAD0L0L6p^f_Nhr(|+vDD>XJ z-8Tuveo`m~U{q5GU>bYic;CA4Gi!TJ1Or^-jM(IwF=gj!S20F6Ui-_>7YZSd@7&h0 zdsG-k)3~RjOrjeTI0 z1OT9S%a(tLv~%AdAH2igw(I?5Fu()VVWZ@1&z1zpINteU^N#M0pOa2^O`I#n}DN-NTyVxB&x0w6M=r)$rTts)8$QBySbo6_>Sh4Zdm zdB~H}N(XC?U3uLtYlyTLnxfIxV%h_WOrWu)^`Www2`vl0cIR(H_r zZhIc2JvloTYbmD9q6KkdOY5q#nhEn`*WNMc3jd?@VBa*RY+F(AsNl8#uB@Ma*oePd z_wv)fzEMehy?1LzcQNc1g+OC#+pkJ1$1iI6@@+}iI$j>pPmKSt|m`)#1>P z4A79&X+UVqvN^orc-)I`+Y|0a<#lUH%-uDwy!gcJMDnZKw`|!p8vCP)fyS13F}b`=J9gaI$XUOGDl3>+GCfI78U~5BW?#Q_`ntA>^+)tz(CBZB&bwwb7z!rP9G$l) zP+HqCW#;Va%cE2EDRouk3!!Y+Ip>_LuF=x5IVbs-mejvSB<~@jf^ihw&{yK&`im|v zyR>N%yLe&^lu|e`7+3kk)FtimP$;DJ&5lN|FBC^X1m?80&C-nUlo?Gi9-A?iT+fG6 z3RWtWS3g9aTylBUbEN2-h2Q`RB;ZRqE)cE@EN;30!9ZpopZ0~41-#Mb#43&gs4oNu zP#^)NRGk(KvzZr^Ln;N0F{FJ_5P)!*;SnM4BbgB?ECz&{pc!Eh0a7Z&trYei{ji_~ zh4YkDKx+Q{`Dfk5olEjQTAb51|Nf?i**Diss;0L(I-qM%fTyaMPnw=4jisS65GS2< z3X{eSgF}o_(vwQ2hLWomm~_Qk&C%$sJzZTJ^TCtXO58X% zcFDbW-uiM?dPliT4WD+j&ND~rj1bVYkB`YwWkK9{65S^8-XgZSC!k zJ@xA!P8&Mh#dx?9dZZ2n0V04wfsq1b^I?psEFl7MmLVV&G$El21tAGa5rjfEze6A* z;~TT~PhNMpX7Rq^$`t?}DH?%>hWRzqrcR0^j_qWff4%=xxv)>vgJ-#B1mUI}IF1F! zw&6GyQjQJBw%|CKr!iKoH;(;3)?wm4vHoq+n+1GKrK(!0}C_(h2l?ZeqiS)q_y@ zb1t}mJc78nxw%|w#*GBEPlWixmz|Abc zVPr;1!zV|`&k{`nkVJ%YIXLeaAR>;8ch4jTm!6 literal 0 HcmV?d00001 diff --git a/application/resources/multimc/index.theme b/application/resources/multimc/index.theme index 5f7d3f3f..a21fea2c 100644 --- a/application/resources/multimc/index.theme +++ b/application/resources/multimc/index.theme @@ -35,6 +35,9 @@ Size=64 [256x256] Size=256 +[150x150] +Size=150 + [scalable] Size=48 Type=Scalable diff --git a/application/resources/multimc/multimc.qrc b/application/resources/multimc/multimc.qrc index 4ced586a..31a7b44f 100644 --- a/application/resources/multimc/multimc.qrc +++ b/application/resources/multimc/multimc.qrc @@ -207,6 +207,13 @@ 48x48/log.png 64x64/log.png + + 16x16/hourglass.png + 22x22/hourglass.png + 32x32/hourglass.png + 48x48/hourglass.png + 150x150/hourglass.png + scalable/screenshot-placeholder.svg diff --git a/application/widgets/ProgressWidget.cpp b/application/widgets/ProgressWidget.cpp new file mode 100644 index 00000000..7b51eca0 --- /dev/null +++ b/application/widgets/ProgressWidget.cpp @@ -0,0 +1,74 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#include "ProgressWidget.h" + +#include +#include +#include +#include + +#include "tasks/Task.h" + +ProgressWidget::ProgressWidget(QWidget *parent) + : QWidget(parent) +{ + m_label = new QLabel(this); + m_label->setWordWrap(true); + m_bar = new QProgressBar(this); + m_bar->setMinimum(0); + m_bar->setMaximum(100); + QVBoxLayout *layout = new QVBoxLayout(this); + layout->addWidget(m_label); + layout->addWidget(m_bar); + layout->addStretch(); + setLayout(layout); +} + +void ProgressWidget::start(std::shared_ptr task) +{ + if (m_task) + { + disconnect(m_task.get(), 0, this, 0); + } + m_task = task; + connect(m_task.get(), &Task::finished, this, &ProgressWidget::handleTaskFinish); + connect(m_task.get(), &Task::status, this, &ProgressWidget::handleTaskStatus); + connect(m_task.get(), &Task::progress, this, &ProgressWidget::handleTaskProgress); + connect(m_task.get(), &Task::destroyed, this, &ProgressWidget::taskDestroyed); + if (!m_task->isRunning()) + { + QMetaObject::invokeMethod(m_task.get(), "start", Qt::QueuedConnection); + } +} +bool ProgressWidget::exec(std::shared_ptr task) +{ + QEventLoop loop; + connect(task.get(), &Task::finished, &loop, &QEventLoop::quit); + start(task); + if (task->isRunning()) + { + loop.exec(); + } + return task->successful(); +} + +void ProgressWidget::handleTaskFinish() +{ + if (!m_task->successful()) + { + m_label->setText(m_task->failReason()); + } +} +void ProgressWidget::handleTaskStatus(const QString &status) +{ + m_label->setText(status); +} +void ProgressWidget::handleTaskProgress(qint64 current, qint64 total) +{ + m_bar->setMaximum(total); + m_bar->setValue(current); +} +void ProgressWidget::taskDestroyed() +{ + m_task = nullptr; +} diff --git a/application/widgets/ProgressWidget.h b/application/widgets/ProgressWidget.h new file mode 100644 index 00000000..08d8a157 --- /dev/null +++ b/application/widgets/ProgressWidget.h @@ -0,0 +1,32 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#pragma once + +#include +#include + +class Task; +class QProgressBar; +class QLabel; + +class ProgressWidget : public QWidget +{ + Q_OBJECT +public: + explicit ProgressWidget(QWidget *parent = nullptr); + +public slots: + void start(std::shared_ptr task); + bool exec(std::shared_ptr task); + +private slots: + void handleTaskFinish(); + void handleTaskStatus(const QString &status); + void handleTaskProgress(qint64 current, qint64 total); + void taskDestroyed(); + +private: + QLabel *m_label; + QProgressBar *m_bar; + std::shared_ptr m_task; +}; diff --git a/logic/AbstractCommonModel.cpp b/logic/AbstractCommonModel.cpp new file mode 100644 index 00000000..71d75829 --- /dev/null +++ b/logic/AbstractCommonModel.cpp @@ -0,0 +1,133 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AbstractCommonModel.h" + +BaseAbstractCommonModel::BaseAbstractCommonModel(const Qt::Orientation orientation, QObject *parent) + : QAbstractListModel(parent), m_orientation(orientation) +{ +} + +int BaseAbstractCommonModel::rowCount(const QModelIndex &parent) const +{ + return m_orientation == Qt::Horizontal ? entryCount() : size(); +} +int BaseAbstractCommonModel::columnCount(const QModelIndex &parent) const +{ + return m_orientation == Qt::Horizontal ? size() : entryCount(); +} +QVariant BaseAbstractCommonModel::data(const QModelIndex &index, int role) const +{ + if (!hasIndex(index.row(), index.column(), index.parent())) + { + return QVariant(); + } + const int i = m_orientation == Qt::Horizontal ? index.column() : index.row(); + const int entry = m_orientation == Qt::Horizontal ? index.row() : index.column(); + return formatData(i, role, get(i, entry, role)); +} +QVariant BaseAbstractCommonModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (orientation != m_orientation && role == Qt::DisplayRole) + { + return entryTitle(section); + } + else + { + return QVariant(); + } +} +bool BaseAbstractCommonModel::setData(const QModelIndex &index, const QVariant &value, int role) +{ + const int i = m_orientation == Qt::Horizontal ? index.column() : index.row(); + const int entry = m_orientation == Qt::Horizontal ? index.row() : index.column(); + const bool result = set(i, entry, role, sanetizeData(i, role, value)); + if (result) + { + emit dataChanged(index, index, QVector() << role); + } + return result; +} +Qt::ItemFlags BaseAbstractCommonModel::flags(const QModelIndex &index) const +{ + if (!hasIndex(index.row(), index.column(), index.parent())) + { + return Qt::NoItemFlags; + } + + const int entry = m_orientation == Qt::Horizontal ? index.row() : index.column(); + if (canSet(entry)) + { + return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsEnabled; + } + else + { + return Qt::ItemIsEnabled | Qt::ItemIsSelectable; + } +} + +void BaseAbstractCommonModel::notifyAboutToAddObject(const int at) +{ + if (m_orientation == Qt::Horizontal) + { + beginInsertColumns(QModelIndex(), at, at); + } + else + { + beginInsertRows(QModelIndex(), at, at); + } +} +void BaseAbstractCommonModel::notifyObjectAdded() +{ + if (m_orientation == Qt::Horizontal) + { + endInsertColumns(); + } + else + { + endInsertRows(); + } +} +void BaseAbstractCommonModel::notifyAboutToRemoveObject(const int at) +{ + if (m_orientation == Qt::Horizontal) + { + beginRemoveColumns(QModelIndex(), at, at); + } + else + { + beginRemoveRows(QModelIndex(), at, at); + } +} +void BaseAbstractCommonModel::notifyObjectRemoved() +{ + if (m_orientation == Qt::Horizontal) + { + endRemoveColumns(); + } + else + { + endRemoveRows(); + } +} + +void BaseAbstractCommonModel::notifyBeginReset() +{ + beginResetModel(); +} +void BaseAbstractCommonModel::notifyEndReset() +{ + endResetModel(); +} diff --git a/logic/AbstractCommonModel.h b/logic/AbstractCommonModel.h new file mode 100644 index 00000000..31b86a23 --- /dev/null +++ b/logic/AbstractCommonModel.h @@ -0,0 +1,462 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include +#include +#include + +class BaseAbstractCommonModel : public QAbstractListModel +{ + Q_OBJECT +public: + explicit BaseAbstractCommonModel(const Qt::Orientation orientation, QObject *parent = nullptr); + + // begin QAbstractItemModel interface + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override; + QVariant data(const QModelIndex &index, int role) const override; + QVariant headerData(int section, Qt::Orientation orientation, int role) const override; + bool setData(const QModelIndex &index, const QVariant &value, int role) override; + Qt::ItemFlags flags(const QModelIndex &index) const override; + // end QAbstractItemModel interface + + virtual int size() const = 0; + virtual int entryCount() const = 0; + + virtual QVariant formatData(const int index, int role, const QVariant &data) const { return data; } + virtual QVariant sanetizeData(const int index, int role, const QVariant &data) const { return data; } + +protected: + virtual QVariant get(const int index, const int entry, const int role) const = 0; + virtual bool set(const int index, const int entry, const int role, const QVariant &value) = 0; + virtual bool canSet(const int entry) const = 0; + virtual QString entryTitle(const int entry) const = 0; + + void notifyAboutToAddObject(const int at); + void notifyObjectAdded(); + void notifyAboutToRemoveObject(const int at); + void notifyObjectRemoved(); + void notifyBeginReset(); + void notifyEndReset(); + + const Qt::Orientation m_orientation; +}; + +template +class AbstractCommonModel : public BaseAbstractCommonModel +{ +public: + explicit AbstractCommonModel(const Qt::Orientation orientation) + : BaseAbstractCommonModel(orientation) {} + virtual ~AbstractCommonModel() {} + + int size() const override { return m_objects.size(); } + int entryCount() const override { return m_entries.size(); } + + void append(const Object &object) + { + notifyAboutToAddObject(size()); + m_objects.append(object); + notifyObjectAdded(); + } + void prepend(const Object &object) + { + notifyAboutToAddObject(0); + m_objects.prepend(object); + notifyObjectAdded(); + } + void insert(const Object &object, const int index) + { + if (index >= size()) + { + prepend(object); + } + else if (index <= 0) + { + append(object); + } + else + { + notifyAboutToAddObject(index); + m_objects.insert(index, object); + notifyObjectAdded(); + } + } + void remove(const int index) + { + notifyAboutToRemoveObject(index); + m_objects.removeAt(index); + notifyObjectRemoved(); + } + Object get(const int index) const + { + return m_objects.at(index); + } + +private: + friend class CommonModel; + QVariant get(const int index, const int entry, const int role) const override + { + if (m_entries.size() < entry || !m_entries[entry].second.contains(role)) + { + return QVariant(); + } + return m_entries[entry].second.value(role)->get(m_objects.at(index)); + } + bool set(const int index, const int entry, const int role, const QVariant &value) override + { + if (m_entries.size() < entry || !m_entries[entry].second.contains(role)) + { + return false; + } + IEntry *e = m_entries[entry].second.value(role); + if (!e->canSet()) + { + return false; + } + e->set(m_objects[index], value); + return true; + } + bool canSet(const int entry) const override + { + if (m_entries.size() < entry || !m_entries[entry].second.contains(Qt::EditRole)) + { + return false; + } + IEntry *e = m_entries[entry].second.value(Qt::EditRole); + return e->canSet(); + } + + QString entryTitle(const int entry) const override + { + return m_entries.at(entry).first; + } + +private: + struct IEntry + { + virtual ~IEntry() {} + virtual void set(Object &object, const QVariant &value) = 0; + virtual QVariant get(const Object &object) const = 0; + virtual bool canSet() const = 0; + }; + template + struct VariableEntry : public IEntry + { + typedef T (Object::*Member); + + explicit VariableEntry(Member member) + : m_member(member) {} + + void set(Object &object, const QVariant &value) override + { + object.*m_member = value.value(); + } + QVariant get(const Object &object) const override + { + return QVariant::fromValue(object.*m_member); + } + bool canSet() const override { return true; } + + private: + Member m_member; + }; + template + struct FunctionEntry : public IEntry + { + typedef T (Object::*Getter)() const; + typedef void (Object::*Setter)(T); + + explicit FunctionEntry(Getter getter, Setter setter) + : m_getter(m_getter), m_setter(m_setter) {} + + void set(Object &object, const QVariant &value) override + { + object.*m_setter(value.value()); + } + QVariant get(const Object &object) const override + { + return QVariant::fromValue(object.*m_getter()); + } + bool canSet() const override { return !!m_setter; } + + private: + Getter m_getter; + Setter m_setter; + }; + + QList m_objects; + QVector>> m_entries; + + void addEntryInternal(IEntry *e, const int entry, const int role) + { + if (m_entries.size() <= entry) + { + m_entries.resize(entry + 1); + } + m_entries[entry].second.insert(role, e); + } + +protected: + template + typename std::enable_if::value && std::is_member_function_pointer::value, void>::type + addEntry(Getter getter, Setter setter, const int entry, const int role) + { + addEntryInternal(new FunctionEntry::type>(getter, setter), entry, role); + } + template + typename std::enable_if::value, void>::type + addEntry(Getter getter, const int entry, const int role) + { + addEntryInternal(new FunctionEntry::type>(getter, nullptr), entry, role); + } + template + typename std::enable_if::value, void>::type + addEntry(T (Object::*member), const int entry, const int role) + { + addEntryInternal(new VariableEntry(member), entry, role); + } + + void setEntryTitle(const int entry, const QString &title) + { + m_entries[entry].first = title; + } +}; +template +class AbstractCommonModel : public BaseAbstractCommonModel +{ +public: + explicit AbstractCommonModel(const Qt::Orientation orientation) + : BaseAbstractCommonModel(orientation) {} + virtual ~AbstractCommonModel() + { + qDeleteAll(m_objects); + } + + int size() const override { return m_objects.size(); } + int entryCount() const override { return m_entries.size(); } + + void append(Object *object) + { + notifyAboutToAddObject(size()); + m_objects.append(object); + notifyObjectAdded(); + } + void prepend(Object *object) + { + notifyAboutToAddObject(0); + m_objects.prepend(object); + notifyObjectAdded(); + } + void insert(Object *object, const int index) + { + if (index >= size()) + { + prepend(object); + } + else if (index <= 0) + { + append(object); + } + else + { + notifyAboutToAddObject(index); + m_objects.insert(index, object); + notifyObjectAdded(); + } + } + void remove(const int index) + { + notifyAboutToRemoveObject(index); + m_objects.removeAt(index); + notifyObjectRemoved(); + } + Object *get(const int index) const + { + return m_objects.at(index); + } + int find(Object * const obj) const + { + return m_objects.indexOf(obj); + } + + QList getAll() const + { + return m_objects; + } + +private: + friend class CommonModel; + QVariant get(const int index, const int entry, const int role) const override + { + if (m_entries.size() < entry || !m_entries[entry].second.contains(role)) + { + return QVariant(); + } + return m_entries[entry].second.value(role)->get(m_objects.at(index)); + } + bool set(const int index, const int entry, const int role, const QVariant &value) override + { + if (m_entries.size() < entry || !m_entries[entry].second.contains(role)) + { + return false; + } + IEntry *e = m_entries[entry].second.value(role); + if (!e->canSet()) + { + return false; + } + e->set(m_objects[index], value); + return true; + } + bool canSet(const int entry) const override + { + if (m_entries.size() < entry || !m_entries[entry].second.contains(Qt::EditRole)) + { + return false; + } + IEntry *e = m_entries[entry].second.value(Qt::EditRole); + return e->canSet(); + } + + QString entryTitle(const int entry) const override + { + return m_entries.at(entry).first; + } + +private: + struct IEntry + { + virtual ~IEntry() {} + virtual void set(Object *object, const QVariant &value) = 0; + virtual QVariant get(Object *object) const = 0; + virtual bool canSet() const = 0; + }; + template + struct VariableEntry : public IEntry + { + typedef T (Object::*Member); + + explicit VariableEntry(Member member) + : m_member(member) {} + + void set(Object *object, const QVariant &value) override + { + object->*m_member = value.value(); + } + QVariant get(Object *object) const override + { + return QVariant::fromValue(object->*m_member); + } + bool canSet() const override { return true; } + + private: + Member m_member; + }; + template + struct FunctionEntry : public IEntry + { + typedef T (Object::*Getter)() const; + typedef void (Object::*Setter)(T); + + explicit FunctionEntry(Getter getter, Setter setter) + : m_getter(getter), m_setter(setter) {} + + void set(Object *object, const QVariant &value) override + { + (object->*m_setter)(value.value()); + } + QVariant get(Object *object) const override + { + return QVariant::fromValue((object->*m_getter)()); + } + bool canSet() const override { return !!m_setter; } + + private: + Getter m_getter; + Setter m_setter; + }; + template + struct LambdaEntry : public IEntry + { + using Getter = std::function; + + explicit LambdaEntry(Getter getter) + : m_getter(getter) {} + + void set(Object *object, const QVariant &value) override {} + QVariant get(Object *object) const override + { + return QVariant::fromValue(m_getter(object)); + } + bool canSet() const override { return false; } + + private: + Getter m_getter; + }; + + QList m_objects; + QVector>> m_entries; + + void addEntryInternal(IEntry *e, const int entry, const int role) + { + if (m_entries.size() <= entry) + { + m_entries.resize(entry + 1); + } + m_entries[entry].second.insert(role, e); + } + +protected: + template + typename std::enable_if::value && std::is_member_function_pointer::value, void>::type + addEntry(const int entry, const int role, Getter getter, Setter setter) + { + addEntryInternal(new FunctionEntry::type>(getter, setter), entry, role); + } + template + typename std::enable_if::Getter>::value, void>::type + addEntry(const int entry, const int role, typename FunctionEntry::Getter getter) + { + addEntryInternal(new FunctionEntry(getter, nullptr), entry, role); + } + template + typename std::enable_if::value, void>::type + addEntry(const int entry, const int role, T (Object::*member)) + { + addEntryInternal(new VariableEntry(member), entry, role); + } + template + void addEntry(const int entry, const int role, typename LambdaEntry::Getter lambda) + { + addEntryInternal(new LambdaEntry(lambda), entry, role); + } + + void setEntryTitle(const int entry, const QString &title) + { + m_entries[entry].first = title; + } + + void setAll(const QList objects) + { + notifyBeginReset(); + qDeleteAll(m_objects); + m_objects = objects; + notifyEndReset(); + } +}; diff --git a/logic/BaseConfigObject.cpp b/logic/BaseConfigObject.cpp new file mode 100644 index 00000000..ff698ad0 --- /dev/null +++ b/logic/BaseConfigObject.cpp @@ -0,0 +1,119 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "BaseConfigObject.h" + +#include +#include +#include +#include +#include + +#include "Exception.h" + +BaseConfigObject::BaseConfigObject(const QString &filename) + : m_filename(filename) +{ + m_saveTimer = new QTimer; + m_saveTimer->setSingleShot(true); + // cppcheck-suppress pureVirtualCall + QObject::connect(m_saveTimer, &QTimer::timeout, [this](){saveNow();}); + setSaveTimeout(250); + + m_initialReadTimer = new QTimer; + m_initialReadTimer->setSingleShot(true); + QObject::connect(m_initialReadTimer, &QTimer::timeout, [this]() + { + loadNow(); + m_initialReadTimer->deleteLater(); + m_initialReadTimer = 0; + }); + m_initialReadTimer->start(0); + + // cppcheck-suppress pureVirtualCall + m_appQuitConnection = QObject::connect(qApp, &QCoreApplication::aboutToQuit, [this](){saveNow();}); +} +BaseConfigObject::~BaseConfigObject() +{ + delete m_saveTimer; + if (m_initialReadTimer) + { + delete m_initialReadTimer; + } + QObject::disconnect(m_appQuitConnection); +} + +void BaseConfigObject::setSaveTimeout(int msec) +{ + m_saveTimer->setInterval(msec); +} + +void BaseConfigObject::scheduleSave() +{ + m_saveTimer->stop(); + m_saveTimer->start(); +} +void BaseConfigObject::saveNow() +{ + if (m_saveTimer->isActive()) + { + m_saveTimer->stop(); + } + if (m_disableSaving) + { + return; + } + + QSaveFile file(m_filename); + if (!file.open(QFile::WriteOnly)) + { + qWarning() << "Couldn't open" << m_filename << "for writing:" << file.errorString(); + return; + } + // cppcheck-suppress pureVirtualCall + file.write(doSave()); + + if (!file.commit()) + { + qCritical() << "Unable to commit the file" << file.fileName() << ":" << file.errorString(); + file.cancelWriting(); + } +} +void BaseConfigObject::loadNow() +{ + if (m_saveTimer->isActive()) + { + saveNow(); + } + + QFile file(m_filename); + if (!file.exists()) + { + return; + } + if (!file.open(QFile::ReadOnly)) + { + qWarning() << "Couldn't open" << m_filename << "for reading:" << file.errorString(); + return; + } + try + { + doLoad(file.readAll()); + } + catch (Exception &e) + { + qWarning() << "Error loading" << m_filename << ":" << e.cause(); + } +} diff --git a/logic/BaseConfigObject.h b/logic/BaseConfigObject.h new file mode 100644 index 00000000..1c96b3d1 --- /dev/null +++ b/logic/BaseConfigObject.h @@ -0,0 +1,50 @@ +/* Copyright 2015 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include + +class QTimer; + +class BaseConfigObject +{ +public: + void setSaveTimeout(int msec); + +protected: + explicit BaseConfigObject(const QString &filename); + virtual ~BaseConfigObject(); + + // cppcheck-suppress pureVirtualCall + virtual QByteArray doSave() const = 0; + virtual void doLoad(const QByteArray &data) = 0; + + void setSavingDisabled(bool savingDisabled) { m_disableSaving = savingDisabled; } + + QString fileName() const { return m_filename; } + +public: + void scheduleSave(); + void saveNow(); + void loadNow(); + +private: + QTimer *m_saveTimer; + QTimer *m_initialReadTimer; + QString m_filename; + QMetaObject::Connection m_appQuitConnection; + bool m_disableSaving = false; +}; diff --git a/logic/CMakeLists.txt b/logic/CMakeLists.txt index de1940ad..d91fc694 100644 --- a/logic/CMakeLists.txt +++ b/logic/CMakeLists.txt @@ -1,6 +1,6 @@ project(MultiMC-Logic) -SET(LOGIC_SOURCES +set(LOGIC_SOURCES # LOGIC - Base classes and infrastructure BaseInstaller.h BaseInstaller.cpp @@ -14,11 +14,14 @@ SET(LOGIC_SOURCES BaseInstance.h BaseInstance.cpp NullInstance.h - MMCError.h MMCZip.h MMCZip.cpp MMCStrings.h MMCStrings.cpp + BaseConfigObject.h + BaseConfigObject.cpp + AbstractCommonModel.h + AbstractCommonModel.cpp # Prefix tree where node names are strings between separators SeparatorPrefixTree.h @@ -28,8 +31,11 @@ SET(LOGIC_SOURCES Env.cpp # JSON parsing helpers - MMCJson.h - MMCJson.cpp + Json.h + Json.cpp + FileSystem.h + FileSystem.cpp + Exception.h # RW lock protected map RWStorage.h @@ -40,6 +46,20 @@ SET(LOGIC_SOURCES # a smart pointer wrapper intended for safer use with Qt signal/slot mechanisms QObjectPtr.h + # Resources + resources/IconResourceHandler.cpp + resources/IconResourceHandler.h + resources/Resource.cpp + resources/Resource.h + resources/ResourceHandler.cpp + resources/ResourceHandler.h + resources/ResourceObserver.cpp + resources/ResourceObserver.h + resources/WebResourceHandler.cpp + resources/WebResourceHandler.h + resources/ResourceProxyModel.h + resources/ResourceProxyModel.cpp + # network stuffs net/NetAction.h net/MD5EtagDownload.h @@ -183,6 +203,8 @@ SET(LOGIC_SOURCES tasks/ThreadTask.cpp tasks/SequentialTask.h tasks/SequentialTask.cpp + tasks/StandardTask.h + tasks/StandardTask.cpp # Settings settings/INIFile.cpp diff --git a/logic/Env.cpp b/logic/Env.cpp index 0607c7ea..2f26f211 100644 --- a/logic/Env.cpp +++ b/logic/Env.cpp @@ -148,6 +148,7 @@ void Env::initHttpMetaCache(QString rootPath, QString staticDataPath) m_metacache->addBase("skins", QDir("accounts/skins").absolutePath()); m_metacache->addBase("root", QDir(rootPath).absolutePath()); m_metacache->addBase("translations", QDir(staticDataPath + "/translations").absolutePath()); + m_metacache->addBase("icons", QDir("cache/icons").absolutePath()); m_metacache->Load(); } @@ -214,4 +215,4 @@ void Env::updateProxySettings(QString proxyTypeStr, QString addr, int port, QStr qDebug() << proxyDesc; } -#include "Env.moc" \ No newline at end of file +#include "Env.moc" diff --git a/logic/Exception.h b/logic/Exception.h new file mode 100644 index 00000000..2664910e --- /dev/null +++ b/logic/Exception.h @@ -0,0 +1,41 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#pragma once + +#include +#include +#include + +class Exception : public std::exception +{ +public: + Exception(const QString &message) : std::exception(), m_message(message) + { + qCritical() << "Exception:" << message; + } + Exception(const Exception &other) + : std::exception(), m_message(other.cause()) + { + } + virtual ~Exception() noexcept {} + const char *what() const noexcept + { + return m_message.toLatin1().constData(); + } + QString cause() const + { + return m_message; + } + +private: + QString m_message; +}; + +#define DECLARE_EXCEPTION(name) \ + class name##Exception : public ::Exception \ + { \ + public: \ + name##Exception(const QString &message) : Exception(message) \ + { \ + } \ + } diff --git a/logic/FileSystem.cpp b/logic/FileSystem.cpp new file mode 100644 index 00000000..b8d82c51 --- /dev/null +++ b/logic/FileSystem.cpp @@ -0,0 +1,56 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#include "FileSystem.h" + +#include +#include +#include + +void ensureExists(const QDir &dir) +{ + if (!QDir().mkpath(dir.absolutePath())) + { + throw FS::FileSystemException("Unable to create directory " + dir.dirName() + " (" + + dir.absolutePath() + ")"); + } +} + +void FS::write(const QString &filename, const QByteArray &data) +{ + ensureExists(QFileInfo(filename).dir()); + QSaveFile file(filename); + if (!file.open(QSaveFile::WriteOnly)) + { + throw FileSystemException("Couldn't open " + filename + " for writing: " + + file.errorString()); + } + if (data.size() != file.write(data)) + { + throw FileSystemException("Error writing data to " + filename + ": " + + file.errorString()); + } + if (!file.commit()) + { + throw FileSystemException("Error while committing data to " + filename + ": " + + file.errorString()); + } +} + +QByteArray FS::read(const QString &filename) +{ + QFile file(filename); + if (!file.open(QFile::ReadOnly)) + { + throw FileSystemException("Unable to open " + filename + " for reading: " + + file.errorString()); + } + const qint64 size = file.size(); + QByteArray data(int(size), 0); + const qint64 ret = file.read(data.data(), size); + if (ret == -1 || ret != size) + { + throw FileSystemException("Error reading data from " + filename + ": " + + file.errorString()); + } + return data; +} diff --git a/logic/FileSystem.h b/logic/FileSystem.h new file mode 100644 index 00000000..e70f3165 --- /dev/null +++ b/logic/FileSystem.h @@ -0,0 +1,13 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#pragma once + +#include "Exception.h" + +namespace FS +{ +DECLARE_EXCEPTION(FileSystem); + +void write(const QString &filename, const QByteArray &data); +QByteArray read(const QString &filename); +} diff --git a/logic/Json.cpp b/logic/Json.cpp new file mode 100644 index 00000000..46055909 --- /dev/null +++ b/logic/Json.cpp @@ -0,0 +1,278 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#include "Json.h" + +#include +#include + +#include "FileSystem.h" +#include + +namespace Json +{ +void write(const QJsonDocument &doc, const QString &filename) +{ + FS::write(filename, doc.toJson()); +} +void write(const QJsonObject &object, const QString &filename) +{ + write(QJsonDocument(object), filename); +} +void write(const QJsonArray &array, const QString &filename) +{ + write(QJsonDocument(array), filename); +} + +QByteArray toBinary(const QJsonObject &obj) +{ + return QJsonDocument(obj).toBinaryData(); +} +QByteArray toBinary(const QJsonArray &array) +{ + return QJsonDocument(array).toBinaryData(); +} +QByteArray toText(const QJsonObject &obj) +{ + return QJsonDocument(obj).toJson(QJsonDocument::Compact); +} +QByteArray toText(const QJsonArray &array) +{ + return QJsonDocument(array).toJson(QJsonDocument::Compact); +} + +static bool isBinaryJson(const QByteArray &data) +{ + decltype(QJsonDocument::BinaryFormatTag) tag = QJsonDocument::BinaryFormatTag; + return memcmp(data.constData(), &tag, sizeof(QJsonDocument::BinaryFormatTag)) == 0; +} +QJsonDocument ensureDocument(const QByteArray &data, const QString &what) +{ + if (isBinaryJson(data)) + { + QJsonDocument doc = QJsonDocument::fromBinaryData(data); + if (doc.isNull()) + { + throw JsonException(what + ": Invalid JSON (binary JSON detected)"); + } + return doc; + } + else + { + QJsonParseError error; + QJsonDocument doc = QJsonDocument::fromJson(data, &error); + if (error.error != QJsonParseError::NoError) + { + throw JsonException(what + ": Error parsing JSON: " + error.errorString()); + } + return doc; + } +} +QJsonDocument ensureDocument(const QString &filename, const QString &what) +{ + return ensureDocument(FS::read(filename), what); +} +QJsonObject ensureObject(const QJsonDocument &doc, const QString &what) +{ + if (!doc.isObject()) + { + throw JsonException(what + " is not an object"); + } + return doc.object(); +} +QJsonArray ensureArray(const QJsonDocument &doc, const QString &what) +{ + if (!doc.isArray()) + { + throw JsonException(what + " is not an array"); + } + return doc.array(); +} + +void writeString(QJsonObject &to, const QString &key, const QString &value) +{ + if (!value.isEmpty()) + { + to.insert(key, value); + } +} + +void writeStringList(QJsonObject &to, const QString &key, const QStringList &values) +{ + if (!values.isEmpty()) + { + QJsonArray array; + for(auto value: values) + { + array.append(value); + } + to.insert(key, array); + } +} + +template<> +QJsonValue toJson(const QUrl &url) +{ + return QJsonValue(url.toString(QUrl::FullyEncoded)); +} +template<> +QJsonValue toJson(const QByteArray &data) +{ + return QJsonValue(QString::fromLatin1(data.toHex())); +} +template<> +QJsonValue toJson(const QDateTime &datetime) +{ + return QJsonValue(datetime.toString(Qt::ISODate)); +} +template<> +QJsonValue toJson(const QDir &dir) +{ + return QDir::current().relativeFilePath(dir.absolutePath()); +} +template<> +QJsonValue toJson(const QUuid &uuid) +{ + return uuid.toString(); +} +template<> +QJsonValue toJson(const QVariant &variant) +{ + return QJsonValue::fromVariant(variant); +} + + +template<> QByteArray ensureIsType(const QJsonValue &value, const Requirement, + const QString &what) +{ + const QString string = ensureIsType(value, Required, what); + // ensure that the string can be safely cast to Latin1 + if (string != QString::fromLatin1(string.toLatin1())) + { + throw JsonException(what + " is not encodable as Latin1"); + } + return QByteArray::fromHex(string.toLatin1()); +} + +template<> QJsonArray ensureIsType(const QJsonValue &value, const Requirement, const QString &what) +{ + if (!value.isArray()) + { + throw JsonException(what + " is not an array"); + } + return value.toArray(); +} + + +template<> QString ensureIsType(const QJsonValue &value, const Requirement, const QString &what) +{ + if (!value.isString()) + { + throw JsonException(what + " is not a string"); + } + return value.toString(); +} + +template<> bool ensureIsType(const QJsonValue &value, const Requirement, + const QString &what) +{ + if (!value.isBool()) + { + throw JsonException(what + " is not a bool"); + } + return value.toBool(); +} + +template<> double ensureIsType(const QJsonValue &value, const Requirement, + const QString &what) +{ + if (!value.isDouble()) + { + throw JsonException(what + " is not a double"); + } + return value.toDouble(); +} + +template<> int ensureIsType(const QJsonValue &value, const Requirement, + const QString &what) +{ + const double doubl = ensureIsType(value, Required, what); + if (fmod(doubl, 1) != 0) + { + throw JsonException(what + " is not an integer"); + } + return int(doubl); +} + +template<> QDateTime ensureIsType(const QJsonValue &value, const Requirement, + const QString &what) +{ + const QString string = ensureIsType(value, Required, what); + const QDateTime datetime = QDateTime::fromString(string, Qt::ISODate); + if (!datetime.isValid()) + { + throw JsonException(what + " is not a ISO formatted date/time value"); + } + return datetime; +} + +template<> QUrl ensureIsType(const QJsonValue &value, const Requirement, + const QString &what) +{ + const QString string = ensureIsType(value, Required, what); + if (string.isEmpty()) + { + return QUrl(); + } + const QUrl url = QUrl(string, QUrl::StrictMode); + if (!url.isValid()) + { + throw JsonException(what + " is not a correctly formatted URL"); + } + return url; +} + +template<> QDir ensureIsType(const QJsonValue &value, const Requirement, const QString &what) +{ + const QString string = ensureIsType(value, Required, what); + return QDir::current().absoluteFilePath(string); +} + +template<> QUuid ensureIsType(const QJsonValue &value, const Requirement, const QString &what) +{ + const QString string = ensureIsType(value, Required, what); + const QUuid uuid = QUuid(string); + if (uuid.toString() != string) // converts back => valid + { + throw JsonException(what + " is not a valid UUID"); + } + return uuid; +} + +template<> QJsonObject ensureIsType(const QJsonValue &value, const Requirement, const QString &what) +{ + if (!value.isObject()) + { + throw JsonException(what + " is not an object"); + } + return value.toObject(); +} + +template<> QVariant ensureIsType(const QJsonValue &value, const Requirement, const QString &what) +{ + if (value.isNull() || value.isUndefined()) + { + throw JsonException(what + " is null or undefined"); + } + return value.toVariant(); +} + +template<> QJsonValue ensureIsType(const QJsonValue &value, const Requirement, const QString &what) +{ + if (value.isNull() || value.isUndefined()) + { + throw JsonException(what + " is null or undefined"); + } + return value; +} + +} diff --git a/logic/Json.h b/logic/Json.h new file mode 100644 index 00000000..d22aa606 --- /dev/null +++ b/logic/Json.h @@ -0,0 +1,239 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "Exception.h" + +namespace Json +{ +DECLARE_EXCEPTION(Json); + +enum Requirement +{ + Required +}; + +void write(const QJsonDocument &doc, const QString &filename); +void write(const QJsonObject &object, const QString &filename); +void write(const QJsonArray &array, const QString &filename); +QByteArray toBinary(const QJsonObject &obj); +QByteArray toBinary(const QJsonArray &array); +QByteArray toText(const QJsonObject &obj); +QByteArray toText(const QJsonArray &array); + +QJsonDocument ensureDocument(const QByteArray &data, const QString &what = "Document"); +QJsonDocument ensureDocument(const QString &filename, const QString &what = "Document"); +QJsonObject ensureObject(const QJsonDocument &doc, const QString &what = "Document"); +QJsonArray ensureArray(const QJsonDocument &doc, const QString &what = "Document"); + +/////////////////// WRITING //////////////////// + +void writeString(QJsonObject & to, const QString &key, const QString &value); +void writeStringList(QJsonObject & to, const QString &key, const QStringList &values); + +template +void writeObjectList(QJsonObject & to, QString key, QList> values) +{ + if (!values.isEmpty()) + { + QJsonArray array; + for (auto value: values) + { + array.append(value->toJson()); + } + to.insert(key, array); + } +} +template +void writeObjectList(QJsonObject & to, QString key, QList values) +{ + if (!values.isEmpty()) + { + QJsonArray array; + for (auto value: values) + { + array.append(value.toJson()); + } + to.insert(key, array); + } +} + +template +QJsonValue toJson(const T &t) +{ + return QJsonValue(t); +} +template<> +QJsonValue toJson(const QUrl &url); +template<> +QJsonValue toJson(const QByteArray &data); +template<> +QJsonValue toJson(const QDateTime &datetime); +template<> +QJsonValue toJson(const QDir &dir); +template<> +QJsonValue toJson(const QUuid &uuid); +template<> +QJsonValue toJson(const QVariant &variant); + +template +QJsonArray toJsonArray(const QList &container) +{ + QJsonArray array; + for (const T item : container) + { + array.append(toJson(item)); + } + return array; +} + +////////////////// READING //////////////////// + +template +T ensureIsType(const QJsonValue &value, const Requirement requirement = Required, const QString &what = "Value"); + +template<> double ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> bool ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> int ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QJsonObject ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QJsonArray ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QJsonValue ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QByteArray ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QDateTime ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QVariant ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QString ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QUuid ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QDir ensureIsType(const QJsonValue &value, const Requirement, const QString &what); +template<> QUrl ensureIsType(const QJsonValue &value, const Requirement, const QString &what); + +// the following functions are higher level functions, that make use of the above functions for +// type conversion +template +T ensureIsType(const QJsonValue &value, const T default_, const QString &what = "Value") +{ + if (value.isUndefined()) + { + return default_; + } + return ensureIsType(value, Required, what); +} +template +T ensureIsType(const QJsonObject &parent, const QString &key, + const Requirement requirement = Required, + const QString &what = "__placeholder__") +{ + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) + { + throw JsonException(localWhat + "s parent does not contain " + localWhat); + } + return ensureIsType(parent.value(key), requirement, localWhat); +} +template +T ensureIsType(const QJsonObject &parent, const QString &key, const T default_, + const QString &what = "__placeholder__") +{ + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) + { + return default_; + } + return ensureIsType(parent.value(key), default_, localWhat); +} + +template +QList ensureIsArrayOf(const QJsonDocument &doc) +{ + const QJsonArray array = ensureArray(doc); + QList out; + for (const QJsonValue val : array) + { + out.append(ensureIsType(val, Required, "Document")); + } + return out; +} +template +QList ensureIsArrayOf(const QJsonValue &value, const Requirement = Required, + const QString &what = "Value") +{ + const QJsonArray array = ensureIsType(value, Required, what); + QList out; + for (const QJsonValue val : array) + { + out.append(ensureIsType(val, Required, what)); + } + return out; +} +template +QList ensureIsArrayOf(const QJsonValue &value, const QList default_, + const QString &what = "Value") +{ + if (value.isUndefined()) + { + return default_; + } + return ensureIsArrayOf(value, Required, what); +} +template +QList ensureIsArrayOf(const QJsonObject &parent, const QString &key, + const Requirement requirement = Required, + const QString &what = "__placeholder__") +{ + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) + { + throw JsonException(localWhat + "s parent does not contain " + localWhat); + } + return ensureIsArrayOf(parent.value(key), requirement, localWhat); +} +template +QList ensureIsArrayOf(const QJsonObject &parent, const QString &key, + const QList &default_, const QString &what = "__placeholder__") +{ + const QString localWhat = QString(what).replace("__placeholder__", '\'' + key + '\''); + if (!parent.contains(key)) + { + return default_; + } + return ensureIsArrayOf(parent.value(key), default_, localWhat); +} + +// this macro part could be replaced by variadic functions that just pass on their arguments, but that wouldn't work well with IDE helpers +#define JSON_HELPERFUNCTIONS(NAME, TYPE) \ + inline TYPE ensure##NAME(const QJsonValue &value, const Requirement requirement = Required, const QString &what = "Value") \ +{ return ensureIsType(value, requirement, what); } \ + inline TYPE ensure##NAME(const QJsonValue &value, const TYPE default_, const QString &what = "Value") \ +{ return ensureIsType(value, default_, what); } \ + inline TYPE ensure##NAME(const QJsonObject &parent, const QString &key, const Requirement requirement = Required, const QString &what = "__placeholder__") \ +{ return ensureIsType(parent, key, requirement, what); } \ + inline TYPE ensure##NAME(const QJsonObject &parent, const QString &key, const TYPE default_, const QString &what = "__placeholder") \ +{ return ensureIsType(parent, key, default_, what); } + +JSON_HELPERFUNCTIONS(Array, QJsonArray) +JSON_HELPERFUNCTIONS(Object, QJsonObject) +JSON_HELPERFUNCTIONS(JsonValue, QJsonValue) +JSON_HELPERFUNCTIONS(String, QString) +JSON_HELPERFUNCTIONS(Boolean, bool) +JSON_HELPERFUNCTIONS(Double, double) +JSON_HELPERFUNCTIONS(Integer, int) +JSON_HELPERFUNCTIONS(DateTime, QDateTime) +JSON_HELPERFUNCTIONS(Url, QUrl) +JSON_HELPERFUNCTIONS(ByteArray, QByteArray) +JSON_HELPERFUNCTIONS(Dir, QDir) +JSON_HELPERFUNCTIONS(Uuid, QUuid) +JSON_HELPERFUNCTIONS(Variant, QVariant) + +#undef JSON_HELPERFUNCTIONS + +} +using JSONValidationError = Json::JsonException; diff --git a/logic/MMCError.h b/logic/MMCError.h deleted file mode 100644 index e81054a6..00000000 --- a/logic/MMCError.h +++ /dev/null @@ -1,25 +0,0 @@ -#pragma once -#include -#include -#include - -class MMCError : public std::exception -{ -public: - MMCError(QString cause) - { - exceptionCause = cause; - qCritical() << "Exception: " + cause; - }; - virtual ~MMCError() noexcept {} - virtual const char *what() const noexcept - { - return exceptionCause.toLocal8Bit(); - }; - virtual QString cause() const - { - return exceptionCause; - } -private: - QString exceptionCause; -}; diff --git a/logic/MMCJson.cpp b/logic/MMCJson.cpp deleted file mode 100644 index 23af4fff..00000000 --- a/logic/MMCJson.cpp +++ /dev/null @@ -1,142 +0,0 @@ -#include "MMCJson.h" - -#include -#include -#include -#include - -QJsonDocument MMCJson::parseDocument(const QByteArray &data, const QString &what) -{ - QJsonParseError error; - QJsonDocument doc = QJsonDocument::fromJson(data, &error); - if (error.error != QJsonParseError::NoError) - { - throw JSONValidationError(what + " is not valid JSON: " + error.errorString() + " at " + error.offset); - } - return doc; -} - -bool MMCJson::ensureBoolean(const QJsonValue val, const QString what) -{ - if (!val.isBool()) - throw JSONValidationError(what + " is not boolean"); - return val.toBool(); -} - -QJsonValue MMCJson::ensureExists(QJsonValue val, const QString what) -{ - if(val.isUndefined() || val.isUndefined()) - throw JSONValidationError(what + " does not exist"); - return val; -} - -QJsonArray MMCJson::ensureArray(const QJsonValue val, const QString what) -{ - if (!val.isArray()) - throw JSONValidationError(what + " is not an array"); - return val.toArray(); -} - -QJsonArray MMCJson::ensureArray(const QJsonDocument &val, const QString &what) -{ - if (!val.isArray()) - { - throw JSONValidationError(what + " is not an array"); - } - return val.array(); -} - -double MMCJson::ensureDouble(const QJsonValue val, const QString what) -{ - if (!val.isDouble()) - throw JSONValidationError(what + " is not a number"); - return val.toDouble(); -} - -int MMCJson::ensureInteger(const QJsonValue val, const QString what) -{ - double ret = ensureDouble(val, what); - if (fmod(ret, 1) != 0) - throw JSONValidationError(what + " is not an integer"); - return ret; -} - -QJsonObject MMCJson::ensureObject(const QJsonValue val, const QString what) -{ - if (!val.isObject()) - throw JSONValidationError(what + " is not an object"); - return val.toObject(); -} - -QJsonObject MMCJson::ensureObject(const QJsonDocument val, const QString what) -{ - if (!val.isObject()) - throw JSONValidationError(what + " is not an object"); - return val.object(); -} - -QString MMCJson::ensureString(const QJsonValue val, const QString what) -{ - if (!val.isString()) - throw JSONValidationError(what + " is not a string"); - return val.toString(); -} - -QUrl MMCJson::ensureUrl(const QJsonValue &val, const QString &what) -{ - const QUrl url = QUrl(ensureString(val, what)); - if (!url.isValid()) - { - throw JSONValidationError(what + " is not an url"); - } - return url; -} - -QJsonDocument MMCJson::parseFile(const QString &filename, const QString &what) -{ - QFile f(filename); - if (!f.open(QFile::ReadOnly)) - { - throw FileOpenError(f); - } - return parseDocument(f.readAll(), what); -} - -int MMCJson::ensureInteger(const QJsonValue val, QString what, const int def) -{ - if (val.isUndefined()) - return def; - return ensureInteger(val, what); -} - -void MMCJson::writeString(QJsonObject &to, QString key, QString value) -{ - if (!value.isEmpty()) - { - to.insert(key, value); - } -} - -void MMCJson::writeStringList(QJsonObject &to, QString key, QStringList values) -{ - if (!values.isEmpty()) - { - QJsonArray array; - for(auto value: values) - { - array.append(value); - } - to.insert(key, array); - } -} - -QStringList MMCJson::ensureStringList(const QJsonValue val, QString what) -{ - const QJsonArray array = ensureArray(val, what); - QStringList out; - for (const auto value : array) - { - out.append(ensureString(value)); - } - return out; -} diff --git a/logic/MMCJson.h b/logic/MMCJson.h deleted file mode 100644 index dc0b4224..00000000 --- a/logic/MMCJson.h +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Some de-bullshitting for Qt JSON failures. - * - * Simple exception-throwing - */ - -#pragma once -#include -#include -#include -#include -#include -#include -#include "MMCError.h" - -class JSONValidationError : public MMCError -{ -public: - JSONValidationError(QString cause) : MMCError(cause) {} -}; -class FileOpenError : public MMCError -{ -public: - FileOpenError(const QFile &file) : MMCError(QObject::tr("Error opening %1: %2").arg(file.fileName(), file.errorString())) {} -}; - -namespace MMCJson -{ -/// parses the data into a json document. throws if there's a parse error -QJsonDocument parseDocument(const QByteArray &data, const QString &what); - -/// tries to open and then parses the specified file. throws if there's an error -QJsonDocument parseFile(const QString &filename, const QString &what); - -/// make sure the value exists. throw otherwise. -QJsonValue ensureExists(QJsonValue val, const QString what = "value"); - -/// make sure the value is converted into an object. throw otherwise. -QJsonObject ensureObject(const QJsonValue val, const QString what = "value"); - -/// make sure the document is converted into an object. throw otherwise. -QJsonObject ensureObject(const QJsonDocument val, const QString what = "document"); - -/// make sure the value is converted into an array. throw otherwise. -QJsonArray ensureArray(const QJsonValue val, QString what = "value"); - -/// make sure the document is converted into an array. throw otherwise. -QJsonArray ensureArray(const QJsonDocument &val, const QString &what = "document"); - -/// make sure the value is converted into a string. throw otherwise. -QString ensureString(const QJsonValue val, QString what = "value"); - -/// make sure the value is converted into a string that's parseable as an url. throw otherwise. -QUrl ensureUrl(const QJsonValue &val, const QString &what = "value"); - -/// make sure the value is converted into a boolean. throw otherwise. -bool ensureBoolean(const QJsonValue val, QString what = "value"); - -/// make sure the value is converted into an integer. throw otherwise. -int ensureInteger(const QJsonValue val, QString what = "value"); - -/// make sure the value is converted into an integer. throw otherwise. this version will return the default value if the field is undefined. -int ensureInteger(const QJsonValue val, QString what, const int def); - -/// make sure the value is converted into a double precision floating number. throw otherwise. -double ensureDouble(const QJsonValue val, QString what = "value"); - -QStringList ensureStringList(const QJsonValue val, QString what); - -void writeString(QJsonObject & to, QString key, QString value); - -void writeStringList(QJsonObject & to, QString key, QStringList values); - -template -void writeObjectList(QJsonObject & to, QString key, QList> values) -{ - if (!values.isEmpty()) - { - QJsonArray array; - for (auto value: values) - { - array.append(value->toJson()); - } - to.insert(key, array); - } -} -template -void writeObjectList(QJsonObject & to, QString key, QList values) -{ - if (!values.isEmpty()) - { - QJsonArray array; - for (auto value: values) - { - array.append(value.toJson()); - } - to.insert(key, array); - } -} -} - diff --git a/logic/QObjectPtr.h b/logic/QObjectPtr.h index 2bde1bd8..32e59bd9 100644 --- a/logic/QObjectPtr.h +++ b/logic/QObjectPtr.h @@ -19,6 +19,11 @@ public: { m_ptr = other.m_ptr; } + template + QObjectPtr(const QObjectPtr &other) + { + m_ptr = other.unwrap(); + } public: void reset(T * wrap) diff --git a/logic/forge/ForgeInstaller.cpp b/logic/forge/ForgeInstaller.cpp index 18527c49..32ce5788 100644 --- a/logic/forge/ForgeInstaller.cpp +++ b/logic/forge/ForgeInstaller.cpp @@ -22,6 +22,7 @@ #include "forge/ForgeVersionList.h" #include "minecraft/VersionFilterData.h" #include "Env.h" +#include "Exception.h" #include #include @@ -412,7 +413,7 @@ protected: m_instance->reloadProfile(); emitSucceeded(); } - catch (MMCError &e) + catch (Exception &e) { emitFailed(e.cause()); } diff --git a/logic/liteloader/LiteLoaderInstaller.cpp b/logic/liteloader/LiteLoaderInstaller.cpp index 9a06d620..e255921f 100644 --- a/logic/liteloader/LiteLoaderInstaller.cpp +++ b/logic/liteloader/LiteLoaderInstaller.cpp @@ -24,6 +24,7 @@ #include "minecraft/OneSixLibrary.h" #include "minecraft/OneSixInstance.h" #include "liteloader/LiteLoaderVersionList.h" +#include "Exception.h" LiteLoaderInstaller::LiteLoaderInstaller() : BaseInstaller() { @@ -118,7 +119,7 @@ protected: m_instance->reloadProfile(); emitSucceeded(); } - catch (MMCError &e) + catch (Exception &e) { emitFailed(e.cause()); } diff --git a/logic/liteloader/LiteLoaderVersionList.cpp b/logic/liteloader/LiteLoaderVersionList.cpp index 8b3c13e0..21bb4f50 100644 --- a/logic/liteloader/LiteLoaderVersionList.cpp +++ b/logic/liteloader/LiteLoaderVersionList.cpp @@ -16,7 +16,7 @@ #include "LiteLoaderVersionList.h" #include "Env.h" #include "net/URLConstants.h" -#include "MMCError.h" +#include "Exception.h" #include @@ -254,7 +254,7 @@ void LLListLoadTask::listDownloaded() } version->libraries.append(lib); } - catch (MMCError &e) + catch (Exception &e) { qCritical() << "Couldn't read JSON object:"; continue; diff --git a/logic/minecraft/JarMod.cpp b/logic/minecraft/JarMod.cpp index bf711c1f..bf985707 100644 --- a/logic/minecraft/JarMod.cpp +++ b/logic/minecraft/JarMod.cpp @@ -1,6 +1,6 @@ #include "JarMod.h" -#include "MMCJson.h" -using namespace MMCJson; +#include "Json.h" +using namespace Json; JarmodPtr Jarmod::fromJson(const QJsonObject &libObj, const QString &filename, const QString &originalName) { diff --git a/logic/minecraft/MinecraftProfile.cpp b/logic/minecraft/MinecraftProfile.cpp index 0661aec1..1baf008e 100644 --- a/logic/minecraft/MinecraftProfile.cpp +++ b/logic/minecraft/MinecraftProfile.cpp @@ -17,12 +17,13 @@ #include #include #include +#include #include #include "minecraft/MinecraftProfile.h" #include "ProfileUtils.h" #include "NullProfileStrategy.h" -#include "VersionBuildError.h" +#include "Exception.h" MinecraftProfile::MinecraftProfile(ProfileStrategy *strategy) : QAbstractListModel() @@ -277,7 +278,7 @@ std::shared_ptr MinecraftProfile::fromJson(const QJsonObject & file->applyTo(version.get()); version->appendPatch(file); } - catch(MMCError & err) + catch(Exception &err) { return 0; } @@ -424,7 +425,7 @@ bool MinecraftProfile::reapplySafe() { reapply(); } - catch(MMCError & error) + catch (Exception & error) { clear(); qWarning() << "Couldn't apply profile patches because: " << error.cause(); diff --git a/logic/minecraft/MinecraftVersionList.cpp b/logic/minecraft/MinecraftVersionList.cpp index c20534e9..44be281b 100644 --- a/logic/minecraft/MinecraftVersionList.cpp +++ b/logic/minecraft/MinecraftVersionList.cpp @@ -14,12 +14,12 @@ */ #include -#include "MMCJson.h" +#include "Json.h" #include #include #include "Env.h" -#include "MMCError.h" +#include "Exception.h" #include "MinecraftVersionList.h" #include "net/URLConstants.h" @@ -71,10 +71,10 @@ protected: MinecraftVersionList *m_list; }; -class ListLoadError : public MMCError +class ListLoadError : public Exception { public: - ListLoadError(QString cause) : MMCError(cause) {}; + ListLoadError(QString cause) : Exception(cause) {}; virtual ~ListLoadError() noexcept { } @@ -142,7 +142,7 @@ void MinecraftVersionList::loadCachedList() } loadMojangList(jsonDoc, Local); } - catch (MMCError &e) + catch (Exception &e) { // the cache has gone bad for some reason... flush it. qCritical() << "The minecraft version cache is corrupted. Flushing cache."; @@ -157,12 +157,11 @@ void MinecraftVersionList::loadBuiltinList() qDebug() << "Loading builtin version list."; // grab the version list data from internal resources. const QJsonDocument doc = - MMCJson::parseFile(":/versions/minecraft.json", - "builtin version list"); + Json::ensureDocument(QString(":/versions/minecraft.json"), "builtin version list"); const QJsonObject root = doc.object(); // parse all the versions - for (const auto version : MMCJson::ensureArray(root.value("versions"))) + for (const auto version : Json::ensureArray(root.value("versions"))) { QJsonObject versionObj = version.toObject(); QString versionID = versionObj.value("id").toString(""); @@ -204,9 +203,9 @@ void MinecraftVersionList::loadBuiltinList() mcVersion->m_processArguments = versionObj.value("processArguments").toString("legacy"); if (versionObj.contains("+traits")) { - for (auto traitVal : MMCJson::ensureArray(versionObj.value("+traits"))) + for (auto traitVal : Json::ensureArray(versionObj.value("+traits"))) { - mcVersion->m_traits.insert(MMCJson::ensureString(traitVal)); + mcVersion->m_traits.insert(Json::ensureString(traitVal)); } } m_lookup[versionID] = mcVersion; @@ -227,11 +226,11 @@ void MinecraftVersionList::loadMojangList(QJsonDocument jsonDoc, VersionSource s try { - QJsonObject latest = MMCJson::ensureObject(root.value("latest")); - m_latestReleaseID = MMCJson::ensureString(latest.value("release")); - m_latestSnapshotID = MMCJson::ensureString(latest.value("snapshot")); + QJsonObject latest = Json::ensureObject(root.value("latest")); + m_latestReleaseID = Json::ensureString(latest.value("release")); + m_latestSnapshotID = Json::ensureString(latest.value("snapshot")); } - catch (MMCError &err) + catch (Exception &err) { qCritical() << tr("Error parsing version list JSON: couldn't determine latest versions"); @@ -481,7 +480,7 @@ void MCVListLoadTask::list_downloaded() } m_list->loadMojangList(jsonDoc, Remote); } - catch (MMCError &e) + catch (Exception &e) { emitFailed(e.cause()); return; @@ -532,7 +531,7 @@ void MCVListVersionUpdateTask::json_downloaded() { file = VersionFile::fromJson(jsonDoc, "net.minecraft.json", false); } - catch (MMCError &e) + catch (Exception &e) { emitFailed(tr("Couldn't process version file: %1").arg(e.cause())); return; diff --git a/logic/minecraft/OneSixInstance.cpp b/logic/minecraft/OneSixInstance.cpp index ffccc259..b7937e31 100644 --- a/logic/minecraft/OneSixInstance.cpp +++ b/logic/minecraft/OneSixInstance.cpp @@ -16,7 +16,6 @@ #include #include #include -#include "MMCError.h" #include "minecraft/OneSixInstance.h" @@ -338,7 +337,7 @@ void OneSixInstance::reloadProfile() catch (VersionIncomplete &error) { } - catch (MMCError &error) + catch (Exception &error) { m_version->clear(); setFlag(VersionBrokenFlag); diff --git a/logic/minecraft/OneSixProfileStrategy.cpp b/logic/minecraft/OneSixProfileStrategy.cpp index 173cd4d6..f5de690b 100644 --- a/logic/minecraft/OneSixProfileStrategy.cpp +++ b/logic/minecraft/OneSixProfileStrategy.cpp @@ -294,7 +294,7 @@ bool OneSixProfileStrategy::customizePatch(ProfilePatchPtr patch) { qDebug() << "Version was incomplete:" << error.cause(); } - catch (MMCError &error) + catch (Exception &error) { qWarning() << "Version could not be loaded:" << error.cause(); } @@ -324,7 +324,7 @@ bool OneSixProfileStrategy::revertPatch(ProfilePatchPtr patch) { qDebug() << "Version was incomplete:" << error.cause(); } - catch (MMCError &error) + catch (Exception &error) { qWarning() << "Version could not be loaded:" << error.cause(); } diff --git a/logic/minecraft/OneSixUpdate.cpp b/logic/minecraft/OneSixUpdate.cpp index 485727ec..8463ead6 100644 --- a/logic/minecraft/OneSixUpdate.cpp +++ b/logic/minecraft/OneSixUpdate.cpp @@ -33,6 +33,7 @@ #include "forge/ForgeMirrors.h" #include "net/URLConstants.h" #include "minecraft/AssetsUtils.h" +#include "Exception.h" #include "MMCZip.h" OneSixUpdate::OneSixUpdate(OneSixInstance *inst, QObject *parent) : Task(parent), m_inst(inst) @@ -182,7 +183,7 @@ void OneSixUpdate::jarlibStart() { inst->reloadProfile(); } - catch (MMCError &e) + catch (Exception &e) { emitFailed(e.cause()); return; diff --git a/logic/minecraft/ParseUtils.cpp b/logic/minecraft/ParseUtils.cpp index 49e0e0ca..8fccf403 100644 --- a/logic/minecraft/ParseUtils.cpp +++ b/logic/minecraft/ParseUtils.cpp @@ -1,7 +1,6 @@ #include #include #include "ParseUtils.h" -#include QDateTime timeFromS3Time(QString str) { diff --git a/logic/minecraft/ProfileUtils.cpp b/logic/minecraft/ProfileUtils.cpp index 3eaca920..68fe0f14 100644 --- a/logic/minecraft/ProfileUtils.cpp +++ b/logic/minecraft/ProfileUtils.cpp @@ -1,6 +1,6 @@ #include "ProfileUtils.h" #include "minecraft/VersionFilterData.h" -#include "MMCJson.h" +#include "Json.h" #include #include @@ -74,18 +74,18 @@ bool readOverrideOrders(QString path, PatchOrder &order) // and then read it and process it if all above is true. try { - auto obj = MMCJson::ensureObject(doc); + auto obj = Json::ensureObject(doc); // check order file version. - auto version = MMCJson::ensureInteger(obj.value("version"), "version"); + auto version = Json::ensureInteger(obj.value("version")); if (version != currentOrderFileVersion) { throw JSONValidationError(QObject::tr("Invalid order file version, expected %1") .arg(currentOrderFileVersion)); } - auto orderArray = MMCJson::ensureArray(obj.value("order")); + auto orderArray = Json::ensureArray(obj.value("order")); for(auto item: orderArray) { - order.append(MMCJson::ensureString(item)); + order.append(Json::ensureString(item)); } } catch (JSONValidationError &err) diff --git a/logic/minecraft/RawLibrary.cpp b/logic/minecraft/RawLibrary.cpp index c4cd97a1..90883312 100644 --- a/logic/minecraft/RawLibrary.cpp +++ b/logic/minecraft/RawLibrary.cpp @@ -1,5 +1,5 @@ -#include "MMCJson.h" -using namespace MMCJson; +#include "Json.h" +using namespace Json; #include "RawLibrary.h" #include @@ -74,7 +74,7 @@ RawLibraryPtr RawLibrary::fromJsonPlus(const QJsonObject &libObj, const QString auto lib = RawLibrary::fromJson(libObj, filename); if (libObj.contains("insert")) { - QJsonValue insertVal = ensureExists(libObj.value("insert"), "library insert rule"); + QJsonValue insertVal = ensureJsonValue(libObj.value("insert"), "library insert rule"); if (insertVal.isString()) { // it's just a simple string rule. OK. diff --git a/logic/minecraft/VersionBuildError.h b/logic/minecraft/VersionBuildError.h index ae479851..fda453e5 100644 --- a/logic/minecraft/VersionBuildError.h +++ b/logic/minecraft/VersionBuildError.h @@ -1,9 +1,9 @@ -#include "MMCError.h" +#include "Exception.h" -class VersionBuildError : public MMCError +class VersionBuildError : public Exception { public: - VersionBuildError(QString cause) : MMCError(cause) {}; + explicit VersionBuildError(QString cause) : Exception(cause) {} virtual ~VersionBuildError() noexcept { } @@ -55,4 +55,4 @@ public: virtual ~VersionIncomplete() noexcept { } -}; \ No newline at end of file +}; diff --git a/logic/minecraft/VersionFile.cpp b/logic/minecraft/VersionFile.cpp index 227ba8be..426cba8c 100644 --- a/logic/minecraft/VersionFile.cpp +++ b/logic/minecraft/VersionFile.cpp @@ -10,8 +10,8 @@ #include "minecraft/JarMod.h" #include "ParseUtils.h" -#include "MMCJson.h" -using namespace MMCJson; +#include "Json.h" +using namespace Json; #include "VersionBuildError.h" diff --git a/logic/minecraft/VersionFile.h b/logic/minecraft/VersionFile.h index dd5c962f..e5ce4026 100644 --- a/logic/minecraft/VersionFile.h +++ b/logic/minecraft/VersionFile.h @@ -3,11 +3,12 @@ #include #include #include +#include + #include #include "minecraft/OpSys.h" #include "minecraft/OneSixRule.h" #include "ProfilePatch.h" -#include "MMCError.h" #include "OneSixLibrary.h" #include "JarMod.h" diff --git a/logic/net/CacheDownload.h b/logic/net/CacheDownload.h index 49d2d99f..7f95a69d 100644 --- a/logic/net/CacheDownload.h +++ b/logic/net/CacheDownload.h @@ -20,6 +20,29 @@ #include #include +class INetworkValidator +{ +public: + virtual ~INetworkValidator() {} + + virtual void validate(const QByteArray &data) = 0; +}; +class JsonValidator : public INetworkValidator +{ +public: + void validate(const QByteArray &data) override; +}; +class MD5HashValidator : public INetworkValidator +{ +public: + explicit MD5HashValidator(const QByteArray &expected) + : m_expected(expected) {} + void validate(const QByteArray &data) override; + +private: + QByteArray m_expected; +}; + typedef std::shared_ptr CacheDownloadPtr; class CacheDownload : public NetAction { @@ -33,6 +56,8 @@ private: /// the hash-as-you-download QCryptographicHash md5sum; + INetworkValidator *m_validator = nullptr; + bool wroteAnyData = false; public: @@ -46,6 +71,10 @@ public: { return m_target_path; } + void setValidator(INetworkValidator *validator) + { + m_validator = validator; + } protected slots: virtual void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); diff --git a/logic/resources/IconResourceHandler.cpp b/logic/resources/IconResourceHandler.cpp new file mode 100644 index 00000000..d47dcc3d --- /dev/null +++ b/logic/resources/IconResourceHandler.cpp @@ -0,0 +1,60 @@ +#include "IconResourceHandler.h" + +#include +#include + +QString IconResourceHandler::m_theme = "multimc"; +QList> IconResourceHandler::m_iconHandlers; + +IconResourceHandler::IconResourceHandler(const QString &key) + : m_key(key) +{ +} + +void IconResourceHandler::setTheme(const QString &theme) +{ + m_theme = theme; + + for (auto handler : m_iconHandlers) + { + std::shared_ptr ptr = handler.lock(); + if (ptr) + { + ptr->setResult(ptr->get()); + } + } +} + +void IconResourceHandler::init(std::shared_ptr &ptr) +{ + m_iconHandlers.append(std::dynamic_pointer_cast(ptr)); + setResult(get()); +} + +QVariant IconResourceHandler::get() const +{ + const QDir iconsDir = QDir(":/icons/" + m_theme); + + QVariantMap out; + for (const QFileInfo &sizeInfo : iconsDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot)) + { + const QDir dir = QDir(sizeInfo.absoluteFilePath()); + const QString dirName = sizeInfo.fileName(); + const int size = dirName.left(dirName.indexOf('x')).toInt(); + if (dir.exists(m_key + ".png") && dirName != "scalable") + { + out.insert(dir.absoluteFilePath(m_key + ".png"), size); + } + else if (dir.exists(m_key + ".svg") && dirName == "scalable") + { + out.insert(dir.absoluteFilePath(m_key + ".svg"), size); + } + } + + if (out.isEmpty()) + { + qWarning() << "Couldn't find any icons for" << m_key; + } + + return out; +} diff --git a/logic/resources/IconResourceHandler.h b/logic/resources/IconResourceHandler.h new file mode 100644 index 00000000..dedfecb2 --- /dev/null +++ b/logic/resources/IconResourceHandler.h @@ -0,0 +1,22 @@ +#pragma once + +#include + +#include "ResourceHandler.h" + +class IconResourceHandler : public ResourceHandler +{ +public: + explicit IconResourceHandler(const QString &key); + + static void setTheme(const QString &theme); + +private: + void init(std::shared_ptr &ptr) override; + + QString m_key; + static QString m_theme; + static QList> m_iconHandlers; + + QVariant get() const; +}; diff --git a/logic/resources/Resource.cpp b/logic/resources/Resource.cpp new file mode 100644 index 00000000..16ed3d2d --- /dev/null +++ b/logic/resources/Resource.cpp @@ -0,0 +1,121 @@ +#include "Resource.h" + +#include + +#include "WebResourceHandler.h" +#include "IconResourceHandler.h" +#include "ResourceObserver.h" + +QMap(const QString &)>> Resource::m_handlers; +QMap, std::function> Resource::m_transfomers; +QMap> Resource::m_resources; + +Resource::Resource(const QString &resource) +{ + if (!m_handlers.contains("web")) + { + registerHandler("web"); + } + if (!m_handlers.contains("icon")) + { + registerHandler("icon"); + } + + Q_ASSERT(resource.contains(':')); + const QString resourceId = resource.left(resource.indexOf(':')); + Q_ASSERT(m_handlers.contains(resourceId)); + m_handler = m_handlers.value(resourceId)(resource.mid(resource.indexOf(':') + 1)); + m_handler->init(m_handler); + m_handler->setResource(this); + Q_ASSERT(m_handler); +} +Resource::~Resource() +{ + qDeleteAll(m_observers); +} + +Resource::Ptr Resource::create(const QString &resource) +{ + Resource::Ptr ptr = m_resources.contains(resource) + ? m_resources.value(resource).lock() + : nullptr; + if (!ptr) + { + struct ConstructableResource : public Resource + { + explicit ConstructableResource(const QString &resource) + : Resource(resource) {} + }; + ptr = std::make_shared(resource); + m_resources.insert(resource, ptr); + } + return ptr; +} + +Resource::Ptr Resource::applyTo(ResourceObserver *observer) +{ + m_observers.append(observer); + observer->setSource(shared_from_this()); // give the observer a shared_ptr for us so we don't get deleted + observer->resourceUpdated(); + return shared_from_this(); +} +Resource::Ptr Resource::applyTo(QObject *target, const char *property) +{ + // the cast to ResourceObserver* is required to ensure the right overload gets choosen + return applyTo(static_cast(new QObjectResourceObserver(target, property))); +} + +Resource::Ptr Resource::placeholder(Resource::Ptr other) +{ + m_placeholder = other; + for (ResourceObserver *observer : m_observers) + { + observer->resourceUpdated(); + } + return shared_from_this(); +} + +QVariant Resource::getResourceInternal(const int typeId) const +{ + if (m_handler->result().isNull() && m_placeholder) + { + return m_placeholder->getResourceInternal(typeId); + } + const QVariant variant = m_handler->result(); + const auto typePair = qMakePair(int(variant.type()), typeId); + if (m_transfomers.contains(typePair)) + { + return m_transfomers.value(typePair)(variant); + } + else + { + return variant; + } +} + +void Resource::reportResult() +{ + for (ResourceObserver *observer : m_observers) + { + observer->resourceUpdated(); + } +} +void Resource::reportFailure(const QString &reason) +{ + for (ResourceObserver *observer : m_observers) + { + observer->setFailure(reason); + } +} +void Resource::reportProgress(const int progress) +{ + for (ResourceObserver *observer : m_observers) + { + observer->setProgress(progress); + } +} + +void Resource::notifyObserverDeleted(ResourceObserver *observer) +{ + m_observers.removeAll(observer); +} diff --git a/logic/resources/Resource.h b/logic/resources/Resource.h new file mode 100644 index 00000000..d566b2a2 --- /dev/null +++ b/logic/resources/Resource.h @@ -0,0 +1,116 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "ResourceObserver.h" + +class ResourceHandler; + +namespace Detail +{ +template struct Function : public Function {}; +template struct Function : public Function {}; +template struct Function +{ + using ReturnType = Ret; + using Argument = Arg; +}; +template struct Function : public Function {}; +template struct Function : public Function {}; +template struct Function : public Function {}; +template struct Function : public Function {}; +} + +/** Frontend class for resources + * + * Usage: + * Resource::create("icon:noaccount")->applyTo(accountsAction); + * Resource::create("web:http://asdf.com/image.png")->applyTo(imageLbl)->placeholder(Resource::create("icon:loading")); + * + * Memory management: + * Resource caches ResourcePtrs using weak pointers, so while a resource is still existing + * when a new resource is created the resources will be the same (including the same handler). + * + * ResourceObservers keep a shared pointer to the resource, as does the Resource itself to it's + * placeholder (if present). This means a resource stays valid while it's still used ("applied to" etc.) + * by something. When nothing uses it anymore it gets deleted. + * + * \note Always pass resource around using ResourcePtr! Copy and move constructors are disabled for a reason. + */ +class Resource : public std::enable_shared_from_this +{ + explicit Resource(const QString &resource); + Resource(const Resource &) = delete; + Resource(Resource &&) = delete; +public: + using Ptr = std::shared_ptr; + + ~Resource(); + + /// The returned pointer needs to be stored until either Resource::then is called, or it is used as the argument to Resource::placeholder. + static Ptr create(const QString &resource); + + /// This can e.g. be used to set a local icon as the placeholder while a slow (remote) icon is fetched + Ptr placeholder(Ptr other); + + /// Use these functions to specify what should happen when e.g. the resource changes + Ptr applyTo(ResourceObserver *observer); + Ptr applyTo(QObject *target, const char *property = nullptr); + template + Ptr then(Func &&func) + { + using Arg = typename std::remove_cv< + typename std::remove_reference::Argument>::type + >::type; + return applyTo(new FunctionResourceObserver< + typename Detail::Function::ReturnType, + Arg, Func + >(std::forward(func))); + } + + /// Retrieve the currently active resource. If it's type is different from T a conversion will be attempted. + template + T getResource() const { return getResourceInternal(qMetaTypeId()).template value(); } + QVariant getResourceInternal(const int typeId) const; + + template + static void registerHandler(const QString &id) + { + m_handlers.insert(id, [](const QString &res) { return std::make_shared(res); }); + } + template + static void registerTransformer(Func &&func) + { + using Out = typename Detail::Function::ReturnType; + using In = typename std::remove_cv::Argument>::type>::type; + static_assert(!std::is_same::value, "It does not make sense to transform a value to itself"); + m_transfomers.insert(qMakePair(qMetaTypeId(), qMetaTypeId()), [func](const QVariant &in) + { + return QVariant::fromValue(func(in.value())); + }); + } + +private: + friend class ResourceHandler; + void reportResult(); + void reportFailure(const QString &reason); + void reportProgress(const int progress); + + friend class ResourceObserver; + void notifyObserverDeleted(ResourceObserver *observer); + +private: + QList m_observers; + std::shared_ptr m_handler = nullptr; + Ptr m_placeholder = nullptr; + + // a list of resource handler factories, registered using registerHandler + static QMap(const QString &)>> m_handlers; + // a list of resource transformers, registered using registerTransformer + static QMap, std::function> m_transfomers; + static QMap> m_resources; +}; diff --git a/logic/resources/ResourceHandler.cpp b/logic/resources/ResourceHandler.cpp new file mode 100644 index 00000000..46a4422c --- /dev/null +++ b/logic/resources/ResourceHandler.cpp @@ -0,0 +1,28 @@ +#include "ResourceHandler.h" + +#include "Resource.h" + +void ResourceHandler::setResult(const QVariant &result) +{ + m_result = result; + if (m_resource) + { + m_resource->reportResult(); + } +} + +void ResourceHandler::setFailure(const QString &reason) +{ + if (m_resource) + { + m_resource->reportFailure(reason); + } +} + +void ResourceHandler::setProgress(const int progress) +{ + if (m_resource) + { + m_resource->reportProgress(progress); + } +} diff --git a/logic/resources/ResourceHandler.h b/logic/resources/ResourceHandler.h new file mode 100644 index 00000000..c1105efc --- /dev/null +++ b/logic/resources/ResourceHandler.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include + +class Resource; + +/** Base class for things that can retrieve a resource. + * + * Subclass, provide a constructor that takes a single QString as argument, and + * call Resource::registerHandler(""), where is the + * prefix of the resource ("web", "icon", etc.) + */ +class ResourceHandler +{ +public: + virtual ~ResourceHandler() {} + + void setResource(Resource *resource) { m_resource = resource; } + // reimplement this if you need to do something after you have been put in a shared pointer + virtual void init(std::shared_ptr&) {} + + QVariant result() const { return m_result; } + +protected: // use these methods to notify the resource of changes + void setResult(const QVariant &result); + void setFailure(const QString &reason); + void setProgress(const int progress); + +private: + QVariant m_result; + Resource *m_resource = nullptr; +}; diff --git a/logic/resources/ResourceObserver.cpp b/logic/resources/ResourceObserver.cpp new file mode 100644 index 00000000..4f168fd2 --- /dev/null +++ b/logic/resources/ResourceObserver.cpp @@ -0,0 +1,55 @@ +#include "ResourceObserver.h" + +#include + +#include "Resource.h" + +static const char *defaultPropertyForTarget(QObject *target) +{ + if (target->inherits("QLabel")) + { + return "pixmap"; + } + else if (target->inherits("QAction") || + target->inherits("QMenu") || + target->inherits("QAbstractButton")) + { + return "icon"; + } + // for unit tests + else if (target->inherits("DummyObserverObject")) + { + return "property"; + } + else + { + Q_ASSERT_X(false, "ResourceObserver.cpp: defaultPropertyForTarget", "Unrecognized QObject subclass"); + return nullptr; + } +} + +QObjectResourceObserver::QObjectResourceObserver(QObject *target, const char *property) + : QObject(target), m_target(target) +{ + const QMetaObject *mo = m_target->metaObject(); + m_property = mo->property(mo->indexOfProperty( + property ? + property + : defaultPropertyForTarget(target))); +} +void QObjectResourceObserver::resourceUpdated() +{ + m_property.write(m_target, getInternal(m_property.type())); +} + + +ResourceObserver::~ResourceObserver() +{ + m_resource->notifyObserverDeleted(this); +} + +QVariant ResourceObserver::getInternal(const int typeId) const +{ + Q_ASSERT(m_resource); + return m_resource->getResourceInternal(typeId); +} diff --git a/logic/resources/ResourceObserver.h b/logic/resources/ResourceObserver.h new file mode 100644 index 00000000..27430d42 --- /dev/null +++ b/logic/resources/ResourceObserver.h @@ -0,0 +1,67 @@ +#pragma once + +#include +#include + +#include +#include + +class QVariant; +class Resource; + +/// Base class for things that can use a resource +class ResourceObserver +{ +public: + virtual ~ResourceObserver(); + +protected: // these methods are called by the Resource when something changes + virtual void resourceUpdated() = 0; + virtual void setFailure(const QString &) {} + virtual void setProgress(const int) {} + +private: + friend class Resource; + void setSource(std::shared_ptr resource) { m_resource = resource; } + +protected: + template + T get() const { return getInternal(qMetaTypeId()).template value(); } + QVariant getInternal(const int typeId) const; + +private: + std::shared_ptr m_resource; +}; + +/** Observer for QObject properties + * + * Give it a target and the name of a property, and that property will be set when the resource changes. + * + * If no name is given an attempt to find a default property for some common classes is done. + */ +class QObjectResourceObserver : public QObject, public ResourceObserver +{ +public: + explicit QObjectResourceObserver(QObject *target, const char *property = nullptr); + + void resourceUpdated() override; + +private: + QObject *m_target; + QMetaProperty m_property; +}; + +template +class FunctionResourceObserver : public ResourceObserver +{ + std::function m_function; +public: + template + explicit FunctionResourceObserver(T &&func) + : m_function(std::forward(func)) {} + + void resourceUpdated() override + { + m_function(get()); + } +}; diff --git a/logic/resources/ResourceProxyModel.cpp b/logic/resources/ResourceProxyModel.cpp new file mode 100644 index 00000000..6ff11367 --- /dev/null +++ b/logic/resources/ResourceProxyModel.cpp @@ -0,0 +1,103 @@ +#include "ResourceProxyModel.h" + +#include + +#include "Resource.h" +#include "ResourceObserver.h" + +//Q_DECLARE_METATYPE(QVector) + +class ModelResourceObserver : public ResourceObserver +{ +public: + explicit ModelResourceObserver(const QModelIndex &index, const int role) + : m_index(index), m_role(role) + { + qRegisterMetaType>("QVector"); + } + + void resourceUpdated() override + { + if (m_index.isValid()) + { + QMetaObject::invokeMethod(const_cast(m_index.model()), + "dataChanged", Qt::QueuedConnection, + Q_ARG(QModelIndex, m_index), Q_ARG(QModelIndex, m_index), Q_ARG(QVector, QVector() << m_role)); + } + } + +private: + QPersistentModelIndex m_index; + int m_role; +}; + +ResourceProxyModel::ResourceProxyModel(const int resultTypeId, QObject *parent) + : QIdentityProxyModel(parent), m_resultTypeId(resultTypeId) +{ +} + +QVariant ResourceProxyModel::data(const QModelIndex &proxyIndex, int role) const +{ + const QModelIndex mapped = mapToSource(proxyIndex); + if (mapped.isValid() && role == Qt::DecorationRole && !mapToSource(proxyIndex).data(role).toString().isEmpty()) + { + if (!m_resources.contains(mapped)) + { + Resource::Ptr res = Resource::create(mapToSource(proxyIndex).data(role).toString()) + ->applyTo(new ModelResourceObserver(proxyIndex, role)); + + const QVariant placeholder = mapped.data(PlaceholderRole); + if (!placeholder.isNull() && placeholder.type() == QVariant::String) + { + res->placeholder(Resource::create(placeholder.toString())); + } + + m_resources.insert(mapped, res); + } + + return m_resources.value(mapped)->getResourceInternal(m_resultTypeId); + } + return mapped.data(role); +} + +void ResourceProxyModel::setSourceModel(QAbstractItemModel *model) +{ + if (sourceModel()) + { + disconnect(sourceModel(), 0, this, 0); + } + if (model) + { + connect(model, &QAbstractItemModel::dataChanged, this, [this](const QModelIndex &tl, const QModelIndex &br, const QVector &roles) + { + if (roles.contains(Qt::DecorationRole) || roles.isEmpty()) + { + const QItemSelectionRange range(tl, br); + for (const QModelIndex &index : range.indexes()) + { + m_resources.remove(index); + } + } + else if (roles.contains(PlaceholderRole)) + { + const QItemSelectionRange range(tl, br); + for (const QModelIndex &index : range.indexes()) + { + if (m_resources.contains(index)) + { + const QVariant placeholder = index.data(PlaceholderRole); + if (!placeholder.isNull() && placeholder.type() == QVariant::String) + { + m_resources.value(index)->placeholder(Resource::create(placeholder.toString())); + } + else + { + m_resources.value(index)->placeholder(nullptr); + } + } + } + } + }); + } + QIdentityProxyModel::setSourceModel(model); +} diff --git a/logic/resources/ResourceProxyModel.h b/logic/resources/ResourceProxyModel.h new file mode 100644 index 00000000..9db09545 --- /dev/null +++ b/logic/resources/ResourceProxyModel.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include + +/// Convenience proxy model that transforms resource identifiers (strings) for Qt::DecorationRole into other types. +class ResourceProxyModel : public QIdentityProxyModel +{ + Q_OBJECT +public: + // resultTypeId is found using qMetaTypeId() + explicit ResourceProxyModel(const int resultTypeId, QObject *parent = nullptr); + + enum + { + // provide this role from your model if you want to show a placeholder + PlaceholderRole = Qt::UserRole + 0xabc // some random offset to not collide with other stuff + }; + + QVariant data(const QModelIndex &proxyIndex, int role) const override; + void setSourceModel(QAbstractItemModel *model) override; + + template + static QAbstractItemModel *mixin(QAbstractItemModel *model) + { + ResourceProxyModel *proxy = new ResourceProxyModel(qMetaTypeId(), model); + proxy->setSourceModel(model); + return proxy; + } + +private: + // mutable because it needs to be available from the const data() + mutable QMap> m_resources; + + const int m_resultTypeId; +}; diff --git a/logic/resources/WebResourceHandler.cpp b/logic/resources/WebResourceHandler.cpp new file mode 100644 index 00000000..7ced5bc6 --- /dev/null +++ b/logic/resources/WebResourceHandler.cpp @@ -0,0 +1,67 @@ +#include "WebResourceHandler.h" + +#include "net/CacheDownload.h" +#include "net/HttpMetaCache.h" +#include "net/NetJob.h" +#include "FileSystem.h" +#include "Env.h" + +QMap WebResourceHandler::m_activeDownloads; + +WebResourceHandler::WebResourceHandler(const QString &url) + : QObject(), m_url(url) +{ + MetaEntryPtr entry = ENV.metacache()->resolveEntry("icons", url); + if (!entry->stale) + { + setResultFromFile(entry->getFullPath()); + } + else if (m_activeDownloads.contains(url)) + { + NetJob *job = m_activeDownloads.value(url); + connect(job, &NetJob::succeeded, this, &WebResourceHandler::succeeded); + connect(job, &NetJob::failed, this, [job, this]() {setFailure(job->failReason());}); + connect(job, &NetJob::progress, this, &WebResourceHandler::progress); + } + else + { + NetJob *job = new NetJob("Icon download"); + job->addNetAction(CacheDownload::make(QUrl(url), entry)); + connect(job, &NetJob::succeeded, this, &WebResourceHandler::succeeded); + connect(job, &NetJob::failed, this, [job, this]() {setFailure(job->failReason());}); + connect(job, &NetJob::progress, this, &WebResourceHandler::progress); + connect(job, &NetJob::finished, job, [job](){m_activeDownloads.remove(m_activeDownloads.key(job));job->deleteLater();}); + m_activeDownloads.insert(url, job); + job->start(); + } +} + +void WebResourceHandler::succeeded() +{ + MetaEntryPtr entry = ENV.metacache()->resolveEntry("icons", m_url); + setResultFromFile(entry->getFullPath()); + m_activeDownloads.remove(m_activeDownloads.key(qobject_cast(sender()))); +} +void WebResourceHandler::progress(qint64 current, qint64 total) +{ + if (total == 0) + { + setProgress(101); + } + else + { + setProgress(current / total); + } +} + +void WebResourceHandler::setResultFromFile(const QString &file) +{ + try + { + setResult(FS::read(file)); + } + catch (Exception &e) + { + setFailure(e.cause()); + } +} diff --git a/logic/resources/WebResourceHandler.h b/logic/resources/WebResourceHandler.h new file mode 100644 index 00000000..88804af3 --- /dev/null +++ b/logic/resources/WebResourceHandler.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include "ResourceHandler.h" + +class NetJob; + +class WebResourceHandler : public QObject, public ResourceHandler +{ +public: + explicit WebResourceHandler(const QString &url); + +private slots: + void succeeded(); + void progress(qint64 current, qint64 total); + +private: + static QMap m_activeDownloads; + + QString m_url; + + void setResultFromFile(const QString &file); +}; diff --git a/logic/tasks/StandardTask.cpp b/logic/tasks/StandardTask.cpp new file mode 100644 index 00000000..3201d674 --- /dev/null +++ b/logic/tasks/StandardTask.cpp @@ -0,0 +1,120 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#include "StandardTask.h" + +#include +#include + +#include "net/CacheDownload.h" +#include "net/ByteArrayDownload.h" +#include "net/NetJob.h" +#include "FileSystem.h" +#include "Exception.h" +#include "Env.h" + +StandardTask::StandardTask(QObject *parent) + : Task(parent) +{ + m_loop = new QEventLoop(this); +} + +void StandardTask::runTask(QObjectPtr other) +{ + connect(other.get(), &Task::succeeded, m_loop, &QEventLoop::quit); + connect(other.get(), &Task::failed, m_loop, &QEventLoop::quit); + connect(other.get(), &Task::progress, this, [this](qint64 current, qint64 total){setProgress(current / total);}); + connect(other.get(), &Task::status, this, &StandardTask::setStatus); + if (!other->isRunning()) + { + other->start(); + } + if (other->isRunning()) + { + m_loop->exec(); + } + disconnect(other.get(), 0, m_loop, 0); + disconnect(other.get(), 0, this, 0); + other->deleteLater(); + if (!other->successful()) + { + throw Exception(other->failReason()); + } +} +void StandardTask::runTaskNonBlocking(QObjectPtr other) +{ + if (!other) + { + return; + } + m_pendingTasks.append(other.get()); + m_pendingTaskPtrs.append(other); + other->start(); +} +QByteArray StandardTask::networkGet(const QUrl &url) +{ + ByteArrayDownloadPtr task = ByteArrayDownload::make(url); + runTask(wrapDownload("", task)); + return task->m_data; +} +QByteArray StandardTask::networkGetCached(const QString &name, const QString &base, const QString &path, const QUrl &url, const bool alwaysRefetch, + INetworkValidator *validator) +{ + MetaEntryPtr entry = ENV.metacache()->resolveEntry(base, path); + if (!alwaysRefetch && !entry->stale) + { + if (validator) { delete validator; } + return FS::read(entry->getFullPath()); + } + else if (alwaysRefetch) + { + entry->stale = true; + } + CacheDownloadPtr task = CacheDownload::make(url, entry); + task->setValidator(validator); + runTask(wrapDownload(name, task)); + return FS::read(entry->getFullPath()); +} +QByteArray StandardTask::networkGetCached(const QString &name, const QString &base, const QString &path, const QUrl &url, const QMap &headers, + INetworkValidator *validator) +{ + MetaEntryPtr entry = ENV.metacache()->resolveEntry(base, path); + if (!entry->stale) + { + if (validator) { delete validator; } + return FS::read(entry->getFullPath()); + } + CacheDownloadPtr task = CacheDownload::make(url, entry); + //task->setHeaders(headers); + task->setValidator(validator); + runTask(wrapDownload(name, task)); + return FS::read(entry->getFullPath()); +} +void StandardTask::networkGetCachedNonBlocking(const QString &name, const QString &base, const QString &path, const QUrl &url, const bool alwaysRefetch, + INetworkValidator *validator) +{ + MetaEntryPtr entry = ENV.metacache()->resolveEntry(base, path); + if (!alwaysRefetch && !entry->stale) + { + return; + } + CacheDownloadPtr dl = CacheDownload::make(url, entry); + dl->setValidator(validator); + runTaskNonBlocking(wrapDownload(name, dl)); +} +void StandardTask::waitOnPending() +{ + for (int i = 0; i < m_pendingTasks.size(); ++i) + { + if (m_pendingTasks.at(i) && m_pendingTasks.at(i)->isRunning()) + { + runTask(m_pendingTaskPtrs.at(i)); + } + } +} + +QObjectPtr StandardTask::wrapDownload(const QString &name, std::shared_ptr action) +{ + NetJobPtr task = NetJobPtr(new NetJob(name)); + task->addNetAction(action); + return task; +} diff --git a/logic/tasks/StandardTask.h b/logic/tasks/StandardTask.h new file mode 100644 index 00000000..6f283dcd --- /dev/null +++ b/logic/tasks/StandardTask.h @@ -0,0 +1,43 @@ +// Licensed under the Apache-2.0 license. See README.md for details. + +#pragma once + +#include "Task.h" + +#include +#include + +#include "QObjectPtr.h" + +class QEventLoop; +class QDir; +class NetAction; +class NetJob; +class INetworkValidator; + +class StandardTask : public Task +{ + Q_OBJECT +public: + explicit StandardTask(QObject *parent = nullptr); + +protected: + // TODO: switch to a future-based system + void runTask(QObjectPtr other); + void runTaskNonBlocking(QObjectPtr other); + QByteArray networkGet(const QUrl &url); + QByteArray networkGetCached(const QString &name, const QString &base, const QString &path, const QUrl &url, const bool alwaysRefetch = false, + INetworkValidator *validator = nullptr); + QByteArray networkGetCached(const QString &name, const QString &base, const QString &path, const QUrl &url, const QMap &headers, + INetworkValidator *validator = nullptr); + void networkGetCachedNonBlocking(const QString &name, const QString &base, const QString &path, const QUrl &url, const bool alwaysRefetch = false, + INetworkValidator *validator = nullptr); + void waitOnPending(); + +private: + QEventLoop *m_loop; + QList> m_pendingTasks; // only used to check if the object was deleted + QList> m_pendingTaskPtrs; + + QObjectPtr wrapDownload(const QString &name, std::shared_ptr action); +}; diff --git a/logic/tasks/Task.cpp b/logic/tasks/Task.cpp index 8fed810b..eaeff4c2 100644 --- a/logic/tasks/Task.cpp +++ b/logic/tasks/Task.cpp @@ -14,6 +14,7 @@ */ #include "Task.h" + #include Task::Task(QObject *parent) : QObject(parent) diff --git a/logic/tasks/Task.h b/logic/tasks/Task.h index 3ab85d7d..93ca620d 100644 --- a/logic/tasks/Task.h +++ b/logic/tasks/Task.h @@ -39,6 +39,8 @@ public: */ virtual QString failReason() const; + virtual bool canAbort() const { return false; } + signals: void started(); void progress(qint64 current, qint64 total); diff --git a/tests/tst_Resource.cpp b/tests/tst_Resource.cpp new file mode 100644 index 00000000..ba6f0509 --- /dev/null +++ b/tests/tst_Resource.cpp @@ -0,0 +1,101 @@ +#include +#include +#include "TestUtil.h" + +#include "resources/Resource.h" +#include "resources/ResourceHandler.h" +#include "resources/ResourceObserver.h" + +class DummyStringResourceHandler : public ResourceHandler +{ +public: + explicit DummyStringResourceHandler(const QString &key) + : m_key(key) {} + + void init(std::shared_ptr &) override + { + setResult(m_key); + } + + QString m_key; +}; +class DummyObserver : public ResourceObserver +{ +public: + void resourceUpdated() override + { + values += get(); + } + + QStringList values; +}; +class DummyObserverObject : public QObject +{ + Q_OBJECT + Q_PROPERTY(QString property MEMBER property) + +public: + explicit DummyObserverObject(QObject *parent = nullptr) : QObject(parent) {} + + QString property; +}; + +class ResourceTest : public QObject +{ + Q_OBJECT +private +slots: + void initTestCase() + { + Resource::registerHandler("dummy"); + } + void cleanupTestCase() + { + } + + void test_Then() + { + QString val; + Resource::create("dummy:test_Then") + ->then([&val](const QString &key) { val = key; }); + QCOMPARE(val, QStringLiteral("test_Then")); + } + void test_Object() + { + DummyObserver *observer = new DummyObserver; + Resource::create("dummy:test_Object")->applyTo(observer); + QCOMPARE(observer->values, QStringList() << "test_Object"); + } + void test_QObjectProperty() + { + DummyObserverObject *object = new DummyObserverObject; + Resource::create("dummy:test_QObjectProperty")->applyTo(object); + QCOMPARE(object->property, QStringLiteral("test_QObjectProperty")); + } + + void test_DontRequestPlaceholder() + { + auto resource = Resource::create("dummy:asdf") + ->then([](const QString &key) { QCOMPARE(key, QStringLiteral("asdf")); }); + // the following call should not notify the observer. if it does the above QCOMPARE would fail. + resource->placeholder(Resource::create("dummy:fdsa")); + } + + void test_MergedResources() + { + auto r1 = Resource::create("dummy:asdf"); + auto r2 = Resource::create("dummy:asdf"); + auto r3 = Resource::create("dummy:fdsa"); + auto r4 = Resource::create("dummy:asdf"); + + QCOMPARE(r1, r2); + QCOMPARE(r1, r4); + QVERIFY(r1 != r3); + QVERIFY(r2 != r3); + QVERIFY(r4 != r3); + } +}; + +QTEST_GUILESS_MAIN(ResourceTest) + +#include "tst_Resource.moc"