From 7cbfdd509c14429e1462b96bcd5fc4e35bdac593 Mon Sep 17 00:00:00 2001
From: Ivan Savenko
Date: Thu, 22 Aug 2013 14:22:49 +0000
Subject: [PATCH] VCMI Launcher/Mod manager. See forum post for details.
---
config/schemas/settings.json | 8 +-
launcher/CMakeLists.txt | 58 +++
launcher/StdInc.cpp | 1 +
launcher/StdInc.h | 11 +
launcher/icons/menu-game.png | Bin 0 -> 5482 bytes
launcher/icons/menu-mods.png | Bin 0 -> 2986 bytes
launcher/icons/menu-settings.png | Bin 0 -> 4474 bytes
launcher/icons/mod-delete.png | Bin 0 -> 1449 bytes
launcher/icons/mod-disabled.png | Bin 0 -> 1604 bytes
launcher/icons/mod-download.png | Bin 0 -> 895 bytes
launcher/icons/mod-enabled.png | Bin 0 -> 1104 bytes
launcher/icons/mod-update.png | Bin 0 -> 1341 bytes
launcher/launcherdirs.cpp | 26 ++
launcher/launcherdirs.h | 13 +
launcher/main.cpp | 11 +
launcher/mainwindow.cpp | 77 ++++
launcher/mainwindow.h | 25 ++
launcher/mainwindow.ui | 206 +++++++++
launcher/modManager/cdownloadmanager.cpp | 114 +++++
launcher/modManager/cdownloadmanager.h | 56 +++
launcher/modManager/cmodlist.cpp | 204 +++++++++
launcher/modManager/cmodlist.h | 77 ++++
launcher/modManager/cmodlistmodel.cpp | 193 +++++++++
launcher/modManager/cmodlistmodel.h | 68 +++
launcher/modManager/cmodlistview.cpp | 518 +++++++++++++++++++++++
launcher/modManager/cmodlistview.h | 84 ++++
launcher/modManager/cmodlistview.ui | 453 ++++++++++++++++++++
launcher/modManager/cmodmanager.cpp | 256 +++++++++++
launcher/modManager/cmodmanager.h | 38 ++
launcher/settingsView/csettingsview.cpp | 112 +++++
launcher/settingsView/csettingsview.h | 34 ++
launcher/settingsView/csettingsview.ui | 367 ++++++++++++++++
32 files changed, 3008 insertions(+), 2 deletions(-)
create mode 100644 launcher/CMakeLists.txt
create mode 100644 launcher/StdInc.cpp
create mode 100644 launcher/StdInc.h
create mode 100644 launcher/icons/menu-game.png
create mode 100644 launcher/icons/menu-mods.png
create mode 100644 launcher/icons/menu-settings.png
create mode 100644 launcher/icons/mod-delete.png
create mode 100644 launcher/icons/mod-disabled.png
create mode 100644 launcher/icons/mod-download.png
create mode 100644 launcher/icons/mod-enabled.png
create mode 100644 launcher/icons/mod-update.png
create mode 100644 launcher/launcherdirs.cpp
create mode 100644 launcher/launcherdirs.h
create mode 100644 launcher/main.cpp
create mode 100644 launcher/mainwindow.cpp
create mode 100644 launcher/mainwindow.h
create mode 100644 launcher/mainwindow.ui
create mode 100644 launcher/modManager/cdownloadmanager.cpp
create mode 100644 launcher/modManager/cdownloadmanager.h
create mode 100644 launcher/modManager/cmodlist.cpp
create mode 100644 launcher/modManager/cmodlist.h
create mode 100644 launcher/modManager/cmodlistmodel.cpp
create mode 100644 launcher/modManager/cmodlistmodel.h
create mode 100644 launcher/modManager/cmodlistview.cpp
create mode 100644 launcher/modManager/cmodlistview.h
create mode 100644 launcher/modManager/cmodlistview.ui
create mode 100644 launcher/modManager/cmodmanager.cpp
create mode 100644 launcher/modManager/cmodmanager.h
create mode 100644 launcher/settingsView/csettingsview.cpp
create mode 100644 launcher/settingsView/csettingsview.h
create mode 100644 launcher/settingsView/csettingsview.ui
diff --git a/config/schemas/settings.json b/config/schemas/settings.json
index 412ec9263..fc41d1b4a 100644
--- a/config/schemas/settings.json
+++ b/config/schemas/settings.json
@@ -229,14 +229,18 @@
"type" : "object",
"default": {},
"additionalProperties" : false,
- "required" : [ "repositoryURL" ],
+ "required" : [ "repositoryURL", "enableInstalledMods" ],
"properties" : {
"repositoryURL" : {
"type" : "array",
"default" : [ ],
"items" : {
"type" : "string"
- }
+ },
+ },
+ "enableInstalledMods" : {
+ "type" : "boolean",
+ "default" : true
}
}
}
diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt
new file mode 100644
index 000000000..b7fd754eb
--- /dev/null
+++ b/launcher/CMakeLists.txt
@@ -0,0 +1,58 @@
+project(vcmilauncher)
+cmake_minimum_required(VERSION 2.8.7)
+
+include_directories(${CMAKE_HOME_DIRECTORY} ${CMAKE_CURRENT_SOURCE_DIR})
+include_directories(${Qt5Widgets_INCLUDE_DIRS} ${Qt5Network_INCLUDE_DIRS})
+
+set(launcher_modmanager_SRCS
+ modManager/cdownloadmanager.cpp
+ modManager/cmodlist.cpp
+ modManager/cmodlistmodel.cpp
+ modManager/cmodlistview.cpp
+ modManager/cmodmanager.cpp
+)
+
+set(launcher_settingsview_SRCS
+ settingsView/csettingsview.cpp
+)
+
+set(launcher_SRCS
+ ${launcher_modmanager_SRCS}
+ ${launcher_settingsview_SRCS}
+ main.cpp
+ mainwindow.cpp
+ launcherdirs.cpp
+)
+
+set(launcher_FORMS
+ modManager/cmodlistview.ui
+ settingsView/csettingsview.ui
+ mainwindow.ui
+)
+
+# Tell CMake to run moc when necessary:
+set(CMAKE_AUTOMOC ON)
+
+# As moc files are generated in the binary dir, tell CMake
+# to always look for includes there:
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+# We need add -DQT_WIDGETS_LIB when using QtWidgets in Qt 5.
+add_definitions(${Qt5Widgets_DEFINITIONS})
+add_definitions(${Qt5Network_DEFINITIONS})
+
+# Executables fail to build with Qt 5 in the default configuration
+# without -fPIE. We add that here.
+set(CMAKE_CXX_FLAGS "${Qt5Widgets_EXECUTABLE_COMPILE_FLAGS} ${CMAKE_CXX_FLAGS}")
+
+qt5_wrap_ui(launcher_UI_HEADERS ${launcher_FORMS})
+
+add_executable(vcmilauncher ${launcher_SRCS} ${launcher_UI_HEADERS})
+
+# The Qt5Widgets_LIBRARIES variable also includes QtGui and QtCore
+target_link_libraries(vcmilauncher vcmi ${Qt5Widgets_LIBRARIES} ${Qt5Network_LIBRARIES})
+
+if (NOT APPLE) # Already inside bundle
+ install(TARGETS vcmilauncher DESTINATION ${BIN_DIR})
+endif()
+
diff --git a/launcher/StdInc.cpp b/launcher/StdInc.cpp
new file mode 100644
index 000000000..b64b59be5
--- /dev/null
+++ b/launcher/StdInc.cpp
@@ -0,0 +1 @@
+#include "StdInc.h"
diff --git a/launcher/StdInc.h b/launcher/StdInc.h
new file mode 100644
index 000000000..751c21f85
--- /dev/null
+++ b/launcher/StdInc.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include "../Global.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
\ No newline at end of file
diff --git a/launcher/icons/menu-game.png b/launcher/icons/menu-game.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f632e2b71bab48cd3c01d300a8146267c7c193a
GIT binary patch
literal 5482
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
literal 0
HcmV?d00001
diff --git a/launcher/icons/menu-settings.png b/launcher/icons/menu-settings.png
new file mode 100644
index 0000000000000000000000000000000000000000..4cce5d91ea81b0b051dfd7736f7beeda24380e0d
GIT binary patch
literal 4474
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
M07*qoM6N<$g8#!*Hvj+t
literal 0
HcmV?d00001
diff --git a/launcher/icons/mod-delete.png b/launcher/icons/mod-delete.png
new file mode 100644
index 0000000000000000000000000000000000000000..fa0c04d95bd286e97de68da3b40bc1cfd0dbcfb9
GIT binary patch
literal 1449
zcmV;a1y=frP)zs}v|HDFrc-hzT#gh&~y7Ly3t7qk>Rx(HIkm
z62M4;FC<_hFD4igee!`sF(?wTkwWh@P+IA3cei_X&wXb6XZCbygJOK}B>&DiGv9pQ
zf0_BS0RQtt9<(^HbzRz^{&U1IUck=|!g#%$7aA>qfMMbLUm3(W*LLxb{C^U-($kYj
z8ume<(|xJ6oiw?jGahMf1}za+>&&vipZXKZ{R4LS#vlX6kEBHLlh#X<_P+{Tf4sAe
zaq@kly`!aJ`?feW5K_*8aLNGOuYhO-NGu7EOapg0+tX!FBUO}B31Hfz#op)huItSLA1jj5p_bmB5c(T@3-(57e*ejM+nj5I7#Do58fu;L*O1V9KZg
zO9^<&87|hGlO8R
zHb|Q}aSZ&~EX2}{u=UtU$Tl@A+j{gQMAK=tHgn>56)(-p%s{-cF~K<9CAoF^<1?~e
zk47Pw%3|Rd5Iije3+mpw4U_L4gw{hxAkxqPTMi$s)T^cjy)D3<_xcn@$WtW?FUNcl
z-cI6N8tY*Ys=gtNK%RW&xQg>$1RQdXY2*DN
zRZpZ?+4Ui_WGOsx@(YM!isS*17Ok&`Z8(>0TBb03VHC*Wlq?7YnXL*4K?cl%iQ-xX
z
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
literal 0
HcmV?d00001
diff --git a/launcher/icons/mod-enabled.png b/launcher/icons/mod-enabled.png
new file mode 100644
index 0000000000000000000000000000000000000000..40204a56f4f2a67fae6c199ac8ac30be8dea745f
GIT binary patch
literal 1104
zcmV-W1h4yvP)0tFiqx|hd}<3erfn=qt&NE_Y22jAp4`vw+1+!_%=off)+BAv_0d24
zABLIve$4m(7~ua}r%1U1FMM=|PJOY{0I3sPygpr3``M8vkMQC?Ef*lw=u2Cw>l&VW
zxTCVZx~98kX>z=)r)}4N0@!zG=LSZ|Tf3WEjewCsNBeHQqkT^eW%#CeMkxoN5I51%
z(4_cv0o(Z#0AOV;p;Gd5*)MqhaNA~8)t=eW*kt6~2~gsLkYW5Va5nEaqO)v(kJ1m?
zo0@zeOW?wW#^N|LJmh$u^JUM0Gb3dIx_Vl6=!&+hv96A#b4duHW7(GI`}LA1^ZD0G
z&;E0O1W{Gi`@r^X{=A!l<7UA~6@I)hXiLrycE5Pu{M($?(nusgQ?b@pDJ6Pz#&|c8
zIN@DOPH*cY!BFt_+GrKYWG_QuLCh`NxRji5xHR5hskP<{X5;t19j&NH)W$ZxF?UnG
z@7WLA;sCJstG!I2^y8h4n}c?K2CnCUkqAx={gUAlZywrzBE2eUtNm^3&=lpBJ@;%g
zG?ijF8Qgwucx&Me}#_(mnD-x)f2VdR(x?lsrn8cb)W;Rz0%`O!CgG0i>sa_RSQLyIUSL%$^_
zb3&>Rl48kDA{21RL(L6F+t!+(u5;MVA_(zgEM-c|&W`pR=>PV5nOFt5x}o;LDRbe|
z;{(GP$MJ)}hji8im$TSZ87Ap$3Lcl>o{Dq7CvDF8)Box>-QU}?Poc`emMzu(3O|r@
z7eNSxlpMMm!FVd?j84uS>+bA-;zj_r29N?!&wf8Oc-Q@#PS04@V+x~6BoLyGmxYuR
zTI#u+$ncFl~X9VsORK!gxPN{LV?L~PqeDd3tlkw}C{
zDT$Pll+I^%BAuNY%za&bckt#}+pS-oci%br`IVmtA%YOXDW!r^DilTGnx+ZMvIGDQ
zK#wvqIBYEXFwJR0u(f&;l5Q5Ls+WsbGu=(=<7NqA)McgRGOV(q0*9Q4lHZRrMEM
WTe#;$n{L(s00005I1-UB5ex=H
zbavvp!=0tNw7!5kT-MzQH|#`Tp^I8t&0DORVK8b1
zu7^DBVd?WY;e$_(BteJgh7_ez?q^WoIrlR9`3&C^$%r9C6-1Fsv#58{R5Sm_&14G9
z7bKw2=kCFAY;Y(MOC@7<8ESe=wePlRK!zT}r=BH21uTGo+;6D~2VRRM2f8EKapZdf
z3T$`Si-SDnry4u$Pk=3D+0=?0Dm0z;<9rWkL*~b$fCv@Xy4h<%!ob-8Li{Px8J?=b
zB#`kws9l((s=;3POW{NS`sFGW5aHM>CyALCRId-NQ7dz)huK6$Q*jg9QlJeR?B=<&zfvxfspCv`YKVaU(d7}&$OA$w3$jfc1k$R8lI=*?~`nKOgKIkVO{6RhxARh{z8xETr37;Q}oF6;4
zFrKt9Ay|@JUy|f4&z7&s>lF)sDHeMai+#$Km&%p#5AWY@Y$!K2K5TAo;*a?D!6$Vl
z%H72SgH==4z~QvCw6qZfPau&|ltQ6U=?s>hp1!_;vB@^G?K^i{^LRl){NM{0V=rHc
zPt47$t*x(bXl!lk9~>GP9eXuC^?G_nDwEI6%`ZW``xwhBA7d3_ZB3z2u0wn`;-nIuo0j;$-S-j9Nx{S_(*u
zYEF6kh{Pjy?5uvn4DWPF0G&rwUJ6sz*I6X%|JXoYBRAXL?m4p
zyJ5Z5E7lx_YGX#?j%)G>1Kct~xsiLr8*#BHrv{
+
+int main(int argc, char *argv[])
+{
+ QApplication a(argc, argv);
+ MainWindow w;
+ w.show();
+
+ return a.exec();
+}
diff --git a/launcher/mainwindow.cpp b/launcher/mainwindow.cpp
new file mode 100644
index 000000000..d02393877
--- /dev/null
+++ b/launcher/mainwindow.cpp
@@ -0,0 +1,77 @@
+#include "StdInc.h"
+#include "mainwindow.h"
+#include "ui_mainwindow.h"
+
+#include
+#include
+
+#include "../lib/CConfigHandler.h"
+#include "../lib/VCMIDirs.h"
+#include "../lib/filesystem/Filesystem.h"
+#include "../lib/logging/CBasicLogConfigurator.h"
+
+void MainWindow::load()
+{
+ console = new CConsoleHandler;
+ CBasicLogConfigurator logConfig(VCMIDirs::get().userCachePath() + "/VCMI_Launcher_log.txt", console);
+ logConfig.configureDefault();
+
+ CResourceHandler::initialize();
+ CResourceHandler::loadMainFileSystem("config/filesystem.json");
+
+ for (auto & string : VCMIDirs::get().dataPaths())
+ QDir::addSearchPath("icons", QString::fromUtf8(string.c_str()) + "/launcher/icons");
+ QDir::addSearchPath("icons", QString::fromUtf8(VCMIDirs::get().userDataPath().c_str()) + "/launcher/icons");
+
+ settings.init();
+}
+
+MainWindow::MainWindow(QWidget *parent) :
+ QMainWindow(parent),
+ ui(new Ui::MainWindow)
+{
+ load(); // load FS before UI
+
+ ui->setupUi(this);
+ ui->tabListWidget->setCurrentIndex(0);
+
+ connect(ui->tabSelectList, SIGNAL(currentRowChanged(int)),
+ ui->tabListWidget, SLOT(setCurrentIndex(int)));
+}
+
+MainWindow::~MainWindow()
+{
+ delete ui;
+}
+
+void MainWindow::on_startGameButon_clicked()
+{
+#if defined(Q_OS_WIN)
+ QString clientName = "VCMI_Client.exe";
+#else
+ // TODO: Right now launcher will only start vcmi from system-default locations
+ QString clientName = "vcmiclient";
+#endif
+ startExecutable(clientName);
+}
+
+void MainWindow::startExecutable(QString name)
+{
+ QProcess process;
+
+ // Start the executable
+ if (process.startDetached(name))
+ {
+ close(); // exit launcher
+ }
+ else
+ {
+ QMessageBox::critical(this,
+ "Error starting executable",
+ "Failed to start " + name + ": " + process.errorString(),
+ QMessageBox::Ok,
+ QMessageBox::Ok);
+ return;
+ }
+
+}
diff --git a/launcher/mainwindow.h b/launcher/mainwindow.h
new file mode 100644
index 000000000..e1c994c99
--- /dev/null
+++ b/launcher/mainwindow.h
@@ -0,0 +1,25 @@
+#pragma once
+#include
+
+namespace Ui {
+ class MainWindow;
+}
+
+class QTableWidgetItem;
+
+class MainWindow : public QMainWindow
+{
+ Q_OBJECT
+
+ void load();
+ void startExecutable(QString name);
+public:
+ explicit MainWindow(QWidget *parent = 0);
+ ~MainWindow();
+
+private slots:
+ void on_startGameButon_clicked();
+
+private:
+ Ui::MainWindow *ui;
+};
diff --git a/launcher/mainwindow.ui b/launcher/mainwindow.ui
new file mode 100644
index 000000000..f2d65c8c9
--- /dev/null
+++ b/launcher/mainwindow.ui
@@ -0,0 +1,206 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 800
+ 480
+
+
+
+
+ 0
+ 0
+
+
+
+ VCMI Launcher
+
+
+
+ icons:menu-game.pngicons:menu-game.png
+
+
+
+ 64
+ 64
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 65
+ 16777215
+
+
+
+ Qt::ScrollBarAlwaysOff
+
+
+ Qt::ScrollBarAlwaysOff
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ false
+
+
+ QAbstractItemView::NoDragDrop
+
+
+ QAbstractItemView::SelectRows
+
+
+
+ 48
+ 64
+
+
+
+ QListView::Static
+
+
+ QListView::Fixed
+
+
+ 0
+
+
+
+ 64
+ 64
+
+
+
+ QListView::IconMode
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+ -1
+
+
-
+
+ Mods
+
+
+
+ icons:menu-mods.pngicons:menu-mods.png
+
+
+ -
+
+ Settings
+
+
+
+ icons:menu-settings.pngicons:menu-settings.png
+
+
+
+
+ -
+
+
+ Play
+
+
+
+ icons:menu-game.pngicons:menu-game.png
+
+
+
+ 60
+ 60
+
+
+
+ false
+
+
+ false
+
+
+ Qt::ToolButtonIconOnly
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ Start game
+
+
+ Qt::AlignCenter
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
+
+
+ CModListView
+ QWidget
+ modManager/cmodlistview.h
+ 1
+
+
+ CSettingsView
+ QWidget
+ settingsView/csettingsview.h
+ 1
+
+
+
+ tabSelectList
+ startGameButon
+
+
+
+
diff --git a/launcher/modManager/cdownloadmanager.cpp b/launcher/modManager/cdownloadmanager.cpp
new file mode 100644
index 000000000..0d98dfc46
--- /dev/null
+++ b/launcher/modManager/cdownloadmanager.cpp
@@ -0,0 +1,114 @@
+#include "StdInc.h"
+#include "cdownloadmanager.h"
+
+#include "launcherdirs.h"
+
+CDownloadManager::CDownloadManager()
+{
+ connect(&manager, SIGNAL(finished(QNetworkReply*)),
+ SLOT(downloadFinished(QNetworkReply*)));
+}
+
+void CDownloadManager::downloadFile(const QUrl &url, const QString &file)
+{
+ QNetworkRequest request(url);
+ FileEntry entry;
+ entry.file.reset(new QFile(CLauncherDirs::get().downloadsPath() + '/' + file));
+ entry.bytesReceived = 0;
+ entry.totalSize = 0;
+
+ if (entry.file->open(QIODevice::WriteOnly | QIODevice::Truncate))
+ {
+ entry.status = FileEntry::IN_PROGRESS;
+ entry.reply = manager.get(request);
+
+ connect(entry.reply, SIGNAL(downloadProgress(qint64, qint64)),
+ SLOT(downloadProgressChanged(qint64, qint64)));
+ }
+ else
+ {
+ entry.status = FileEntry::FAILED;
+ entry.reply = nullptr;
+ encounteredErrors += entry.file->errorString();
+ }
+
+ // even if failed - add it into list to report it in finished() call
+ currentDownloads.push_back(entry);
+}
+
+CDownloadManager::FileEntry & CDownloadManager::getEntry(QNetworkReply * reply)
+{
+ assert(reply);
+ for (auto & entry : currentDownloads)
+ {
+ if (entry.reply == reply)
+ return entry;
+ }
+ assert(0);
+ static FileEntry errorValue;
+ return errorValue;
+}
+
+void CDownloadManager::downloadFinished(QNetworkReply *reply)
+{
+ FileEntry & file = getEntry(reply);
+
+ if (file.reply->error())
+ {
+ encounteredErrors += file.reply->errorString();
+ file.file->remove();
+ file.status = FileEntry::FAILED;
+ }
+ else
+ {
+ file.file->write(file.reply->readAll());
+ file.file->close();
+ file.status = FileEntry::FINISHED;
+ }
+
+ file.reply->deleteLater();
+
+ bool downloadComplete = true;
+ for (auto & entry : currentDownloads)
+ {
+ if (entry.status == FileEntry::IN_PROGRESS)
+ {
+ downloadComplete = false;
+ break;
+ }
+ }
+
+ QStringList successful;
+ QStringList failed;
+
+ for (auto & entry : currentDownloads)
+ {
+ if (entry.status == FileEntry::FINISHED)
+ successful += entry.file->fileName();
+ else
+ failed += entry.file->fileName();
+ }
+
+ if (downloadComplete)
+ emit finished(successful, failed, encounteredErrors);
+}
+
+void CDownloadManager::downloadProgressChanged(qint64 bytesReceived, qint64 bytesTotal)
+{
+ auto reply = dynamic_cast(sender());
+ FileEntry & entry = getEntry(reply);
+
+ entry.file->write(entry.reply->readAll());
+ entry.bytesReceived = bytesReceived;
+ entry.totalSize = bytesTotal;
+
+ quint64 total = 0;
+ for (auto & entry : currentDownloads)
+ total += entry.totalSize > 0 ? entry.totalSize : 0;
+
+ quint64 received = 0;
+ for (auto & entry : currentDownloads)
+ received += entry.bytesReceived > 0 ? entry.bytesReceived : 0;
+
+ emit downloadProgress(received, total);
+}
diff --git a/launcher/modManager/cdownloadmanager.h b/launcher/modManager/cdownloadmanager.h
new file mode 100644
index 000000000..1f2cc4f7c
--- /dev/null
+++ b/launcher/modManager/cdownloadmanager.h
@@ -0,0 +1,56 @@
+#pragma once
+
+#include
+#include
+
+class QFile;
+
+class CDownloadManager: public QObject
+{
+ Q_OBJECT
+
+ struct FileEntry
+ {
+ enum Status
+ {
+ IN_PROGRESS,
+ FINISHED,
+ FAILED
+ };
+
+ QNetworkReply * reply;
+ QSharedPointer file;
+ Status status;
+ qint64 bytesReceived;
+ qint64 totalSize;
+ };
+
+ QStringList encounteredErrors;
+
+ QNetworkAccessManager manager;
+
+ QList currentDownloads;
+
+ FileEntry & getEntry(QNetworkReply * reply);
+public:
+ CDownloadManager();
+
+ // returns true if download with such URL is in progress/queued
+ // FIXME: not sure what's right place for "mod download in progress" check
+ bool downloadInProgress(const QUrl &url);
+
+ // returns network reply so caller can connect to required signals
+ void downloadFile(const QUrl &url, const QString &file);
+
+public slots:
+ void downloadFinished(QNetworkReply *reply);
+ void downloadProgressChanged(qint64 bytesReceived, qint64 bytesTotal);
+
+signals:
+ // for status bar updates. Merges all queued downloads into one
+ void downloadProgress(qint64 currentAmount, qint64 maxAmount);
+
+ // called when all files were downloaded and manager goes to idle state
+ // Lists contains files that were successfully downloaded / failed to download
+ void finished(QStringList savedFiles, QStringList failedFiles, QStringList errors);
+};
diff --git a/launcher/modManager/cmodlist.cpp b/launcher/modManager/cmodlist.cpp
new file mode 100644
index 000000000..3c503fe8b
--- /dev/null
+++ b/launcher/modManager/cmodlist.cpp
@@ -0,0 +1,204 @@
+#include "StdInc.h"
+#include "cmodlist.h"
+
+bool CModEntry::compareVersions(QString lesser, QString greater)
+{
+ static const int maxSections = 3; // versions consist from up to 3 sections, major.minor.patch
+
+ QStringList lesserList = lesser.split(".");
+ QStringList greaterList = greater.split(".");
+
+ assert(lesserList.size() <= maxSections);
+ assert(greaterList.size() <= maxSections);
+
+ for (int i=0; i< maxSections; i++)
+ {
+ if (greaterList.size() <= i) // 1.1.1 > 1.1
+ return false;
+
+ if (lesserList.size() <= i) // 1.1 < 1.1.1
+ return true;
+
+ if (lesserList[i].toInt() != greaterList[i].toInt())
+ return lesserList[i].toInt() < greaterList[i].toInt(); // 1.1 < 1.2
+ }
+ return false;
+}
+
+CModEntry::CModEntry(QJsonObject repository, QJsonObject localData, QJsonValue modSettings, QString modname):
+ repository(repository),
+ localData(localData),
+ modSettings(modSettings),
+ modname(modname)
+{
+}
+
+bool CModEntry::isEnabled() const
+{
+ if (!isInstalled())
+ return false;
+
+ return modSettings.toBool(false);
+}
+
+bool CModEntry::isDisabled() const
+{
+ if (!isInstalled())
+ return false;
+ return !isEnabled();
+}
+
+bool CModEntry::isAvailable() const
+{
+ if (isInstalled())
+ return false;
+ return !repository.isEmpty();
+}
+
+bool CModEntry::isUpdateable() const
+{
+ if (!isInstalled())
+ return false;
+
+ QString installedVer = localData["installedVersion"].toString();
+ QString availableVer = repository["latestVersion"].toString();
+
+ if (compareVersions(installedVer, availableVer))
+ return true;
+ return false;
+}
+
+bool CModEntry::isInstalled() const
+{
+ return !localData.isEmpty();
+}
+
+int CModEntry::getModStatus() const
+{
+ return
+ (isEnabled() ? ModStatus::ENABLED : 0) |
+ (isInstalled() ? ModStatus::INSTALLED : 0) |
+ (isUpdateable()? ModStatus::UPDATEABLE : 0);
+}
+
+QString CModEntry::getName() const
+{
+ return modname;
+}
+
+QVariant CModEntry::getValue(QString value) const
+{
+ if (repository.contains(value))
+ return repository[value].toVariant();
+
+ if (localData.contains(value))
+ return localData[value].toVariant();
+
+ return QVariant();
+}
+
+QJsonObject CModList::copyField(QJsonObject data, QString from, QString to)
+{
+ QJsonObject renamed;
+
+ for (auto it = data.begin(); it != data.end(); it++)
+ {
+ QJsonObject object = it.value().toObject();
+
+ object.insert(to, object.value(from));
+ renamed.insert(it.key(), QJsonValue(object));
+ }
+ return renamed;
+}
+
+void CModList::addRepository(QJsonObject data)
+{
+ repositores.push_back(copyField(data, "version", "latestVersion"));
+}
+
+void CModList::setLocalModList(QJsonObject data)
+{
+ localModList = copyField(data, "version", "installedVersion");
+}
+
+void CModList::setModSettings(QJsonObject data)
+{
+ modSettings = data;
+}
+
+CModEntry CModList::getMod(QString modname) const
+{
+ assert(hasMod(modname));
+
+ QJsonObject repo;
+ QJsonObject local = localModList[modname].toObject();
+ QJsonValue settings = modSettings[modname];
+
+ for (auto entry : repositores)
+ {
+ if (entry.contains(modname))
+ {
+ if (repo.empty())
+ repo = entry[modname].toObject();
+ else
+ {
+ if (CModEntry::compareVersions(repo["version"].toString(),
+ entry[modname].toObject()["version"].toString()))
+ repo = entry[modname].toObject();
+ }
+ }
+ }
+
+ return CModEntry(repo, local, settings, modname);
+}
+
+bool CModList::hasMod(QString modname) const
+{
+ if (localModList.contains(modname))
+ return true;
+
+ for (auto entry : repositores)
+ if (entry.contains(modname))
+ return true;
+
+ return false;
+}
+
+QStringList CModList::getRequirements(QString modname)
+{
+ QStringList ret;
+
+ if (hasMod(modname))
+ {
+ auto mod = getMod(modname);
+
+ for (auto entry : mod.getValue("depends").toStringList())
+ ret += getRequirements(entry);
+ }
+ ret += modname;
+
+ return ret;
+}
+
+QVector CModList::getModList() const
+{
+ QSet knownMods;
+ QVector modList;
+ for (auto repo : repositores)
+ {
+ for (auto it = repo.begin(); it != repo.end(); it++)
+ {
+ knownMods.insert(it.key());
+ }
+ }
+ for (auto it = localModList.begin(); it != localModList.end(); it++)
+ {
+ knownMods.insert(it.key());
+ }
+
+ for (auto entry : knownMods)
+ {
+ modList.push_back(entry);
+ }
+ return modList;
+}
diff --git a/launcher/modManager/cmodlist.h b/launcher/modManager/cmodlist.h
new file mode 100644
index 000000000..6e2bdf12c
--- /dev/null
+++ b/launcher/modManager/cmodlist.h
@@ -0,0 +1,77 @@
+#pragma once
+
+#include
+#include
+#include
+
+namespace ModStatus
+{
+ enum EModStatus
+ {
+ MASK_NONE = 0,
+ ENABLED = 1,
+ INSTALLED = 2,
+ UPDATEABLE = 4,
+ MASK_ALL = 255
+ };
+}
+
+class CModEntry
+{
+ // repository contains newest version only (if multiple are available)
+ QJsonObject repository;
+ QJsonObject localData;
+ QJsonValue modSettings;
+
+ QString modname;
+public:
+ CModEntry(QJsonObject repository, QJsonObject localData, QJsonValue modSettings, QString modname);
+
+ // installed and enabled
+ bool isEnabled() const;
+ // installed but disabled
+ bool isDisabled() const;
+ // available in any of repositories but not installed
+ bool isAvailable() const;
+ // installed and greater version exists in repository
+ bool isUpdateable() const;
+ // installed
+ bool isInstalled() const;
+
+ // see ModStatus enum
+ int getModStatus() const;
+
+ QString getName() const;
+
+ // get value of some field in mod structure. Returns empty optional if value is not present
+ QVariant getValue(QString value) const;
+
+ // returns true if less < greater comparing versions section by section
+ static bool compareVersions(QString lesser, QString greater);
+};
+
+class CModList
+{
+ QVector repositores;
+ QJsonObject localModList;
+ QJsonObject modSettings;
+
+ QJsonObject copyField(QJsonObject data, QString from, QString to);
+public:
+ virtual void addRepository(QJsonObject data);
+ virtual void setLocalModList(QJsonObject data);
+ virtual void setModSettings(QJsonObject data);
+
+ // returns mod by name. Note: mod MUST exist
+ CModEntry getMod(QString modname) const;
+
+ // returns list of all mods necessary to run selected one, including mod itself
+ // order is: first mods in list don't have any dependencies, last mod is modname
+ // note: may include mods not present in list
+ QStringList getRequirements(QString modname);
+
+ bool hasMod(QString modname) const;
+
+ // returns list of all available mods
+ QVector getModList() const;
+};
\ No newline at end of file
diff --git a/launcher/modManager/cmodlistmodel.cpp b/launcher/modManager/cmodlistmodel.cpp
new file mode 100644
index 000000000..141f42c26
--- /dev/null
+++ b/launcher/modManager/cmodlistmodel.cpp
@@ -0,0 +1,193 @@
+#include "StdInc.h"
+#include "cmodlistmodel.h"
+
+#include
+
+namespace ModFields
+{
+ static const QString names [ModFields::COUNT] =
+ {
+ "",
+ "",
+ "modType",
+ "name",
+ "version",
+ "size",
+ "author"
+ };
+
+ static const QString header [ModFields::COUNT] =
+ {
+ "", // status icon
+ "", // status icon
+ "Type",
+ "Name",
+ "Version",
+ "Size (KB)",
+ "Author"
+ };
+}
+
+namespace ModStatus
+{
+ static const QString iconDelete = "icons:mod-delete.png";
+ static const QString iconDisabled = "icons:mod-disabled.png";
+ static const QString iconDownload = "icons:mod-download.png";
+ static const QString iconEnabled = "icons:mod-enabled.png";
+ static const QString iconUpdate = "icons:mod-update.png";
+}
+
+CModListModel::CModListModel(QObject *parent) :
+ QAbstractTableModel(parent)
+{
+}
+
+QString CModListModel::modIndexToName(int index) const
+{
+ return indexToName[index];
+}
+
+QVariant CModListModel::data(const QModelIndex &index, int role) const
+{
+ if (index.isValid())
+ {
+ auto mod = getMod(modIndexToName(index.row()));
+
+ if (index.column() == ModFields::STATUS_ENABLED)
+ {
+ if (role == Qt::DecorationRole)
+ {
+ if (mod.isEnabled())
+ return QIcon(ModStatus::iconEnabled);
+
+ if (mod.isDisabled())
+ return QIcon(ModStatus::iconDisabled);
+
+ return QVariant();
+ }
+ }
+ if (index.column() == ModFields::STATUS_UPDATE)
+ {
+ if (role == Qt::DecorationRole)
+ {
+ if (mod.isUpdateable())
+ return QIcon(ModStatus::iconUpdate);
+
+ if (!mod.isInstalled())
+ return QIcon(ModStatus::iconDownload);
+
+ return QVariant();
+ }
+ }
+
+ if (role == Qt::DisplayRole)
+ {
+ return mod.getValue(ModFields::names[index.column()]);
+ }
+ }
+ return QVariant();
+}
+
+int CModListModel::rowCount(const QModelIndex &) const
+{
+ return indexToName.size();
+}
+
+int CModListModel::columnCount(const QModelIndex &) const
+{
+ return ModFields::COUNT;
+}
+
+Qt::ItemFlags CModListModel::flags(const QModelIndex &) const
+{
+ return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
+}
+
+QVariant CModListModel::headerData(int section, Qt::Orientation orientation, int role) const
+{
+ if (role == Qt::DisplayRole && orientation == Qt::Horizontal)
+ return ModFields::header[section];
+ return QVariant();
+}
+
+void CModListModel::addRepository(QJsonObject data)
+{
+ beginResetModel();
+ CModList::addRepository(data);
+ endResetModel();
+}
+
+void CModListModel::setLocalModList(QJsonObject data)
+{
+ beginResetModel();
+ CModList::setLocalModList(data);
+ endResetModel();
+}
+
+void CModListModel::setModSettings(QJsonObject data)
+{
+ beginResetModel();
+ CModList::setModSettings(data);
+ endResetModel();
+}
+
+void CModListModel::endResetModel()
+{
+ indexToName = getModList();
+ QAbstractItemModel::endResetModel();
+}
+
+void CModFilterModel::setTypeFilter(int filteredType, int filterMask)
+{
+ this->filterMask = filterMask;
+ this->filteredType = filteredType;
+ invalidateFilter();
+}
+
+bool CModFilterModel::filterMatches(int modIndex) const
+{
+ CModEntry mod = base->getMod(base->modIndexToName(modIndex));
+
+ return (mod.getModStatus() & filterMask) == filteredType;
+}
+
+bool CModFilterModel::filterAcceptsRow(int source_row, const QModelIndex &source_parent) const
+{
+ if (filterMatches(source_row))
+ return QSortFilterProxyModel::filterAcceptsRow(source_row, source_parent);
+ return false;
+}
+
+bool CModFilterModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
+{
+ assert(left.column() == right.column());
+
+ CModEntry mod = base->getMod(base->modIndexToName(left.row()));
+
+ switch (left.column())
+ {
+ case ModFields::STATUS_ENABLED:
+ {
+ return (mod.getModStatus() & (ModStatus::ENABLED | ModStatus::INSTALLED))
+ < (mod.getModStatus() & (ModStatus::ENABLED | ModStatus::INSTALLED));
+ }
+ case ModFields::STATUS_UPDATE:
+ {
+ return (mod.getModStatus() & (ModStatus::UPDATEABLE | ModStatus::INSTALLED))
+ < (mod.getModStatus() & (ModStatus::UPDATEABLE | ModStatus::INSTALLED));
+ }
+ default:
+ {
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+ }
+}
+
+CModFilterModel::CModFilterModel(CModListModel * model, QObject * parent):
+ QSortFilterProxyModel(parent),
+ base(model),
+ filteredType(ModStatus::MASK_NONE),
+ filterMask(ModStatus::MASK_NONE)
+{
+ setSourceModel(model);
+}
diff --git a/launcher/modManager/cmodlistmodel.h b/launcher/modManager/cmodlistmodel.h
new file mode 100644
index 000000000..5dc705806
--- /dev/null
+++ b/launcher/modManager/cmodlistmodel.h
@@ -0,0 +1,68 @@
+#pragma once
+
+#include "cmodlist.h"
+
+#include
+#include
+
+namespace ModFields
+{
+ enum EModFields
+ {
+ STATUS_ENABLED,
+ STATUS_UPDATE,
+ TYPE,
+ NAME,
+ VERSION,
+ SIZE,
+ AUTHOR,
+ COUNT
+ };
+}
+
+class CModListModel : public QAbstractTableModel, public CModList
+{
+ Q_OBJECT
+
+ QVector indexToName;
+
+ void endResetModel();
+public:
+ /// CModListContainer overrides
+ void addRepository(QJsonObject data);
+ void setLocalModList(QJsonObject data);
+ void setModSettings(QJsonObject data);
+
+ QString modIndexToName(int index) const;
+
+ explicit CModListModel(QObject *parent = 0);
+
+ QVariant data(const QModelIndex &index, int role) const;
+ QVariant headerData(int section, Qt::Orientation orientation, int role) const;
+
+ int rowCount(const QModelIndex &parent) const;
+ int columnCount(const QModelIndex &parent) const;
+
+ Qt::ItemFlags flags(const QModelIndex &index) const;
+signals:
+
+public slots:
+
+};
+
+class CModFilterModel : public QSortFilterProxyModel
+{
+ CModListModel * base;
+ int filteredType;
+ int filterMask;
+
+ bool filterMatches(int modIndex) const;
+
+ bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const;
+
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const;
+public:
+ void setTypeFilter(int filteredType, int filterMask);
+
+ CModFilterModel(CModListModel * model, QObject *parent = 0);
+};
diff --git a/launcher/modManager/cmodlistview.cpp b/launcher/modManager/cmodlistview.cpp
new file mode 100644
index 000000000..9495ff322
--- /dev/null
+++ b/launcher/modManager/cmodlistview.cpp
@@ -0,0 +1,518 @@
+#include "StdInc.h"
+#include "cmodlistview.h"
+#include "ui_cmodlistview.h"
+
+#include
+#include
+
+#include "cmodlistmodel.h"
+#include "cmodmanager.h"
+#include "cdownloadmanager.h"
+#include "launcherdirs.h"
+
+#include "../lib/CConfigHandler.h"
+
+void CModListView::setupModModel()
+{
+ modModel = new CModListModel();
+ manager = new CModManager(modModel);
+}
+
+void CModListView::setupFilterModel()
+{
+ filterModel = new CModFilterModel(modModel);
+
+ filterModel->setFilterKeyColumn(-1); // filter across all columns
+ filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); // to make it more user-friendly
+}
+
+void CModListView::setupModsView()
+{
+ ui->allModsView->setModel(filterModel);
+ // input data is not sorted - sort it before display
+ ui->allModsView->sortByColumn(ModFields::TYPE, Qt::AscendingOrder);
+ ui->allModsView->setColumnWidth(ModFields::STATUS_ENABLED, 30);
+ ui->allModsView->setColumnWidth(ModFields::STATUS_UPDATE, 30);
+ ui->allModsView->setColumnWidth(ModFields::NAME, 120);
+ ui->allModsView->setColumnWidth(ModFields::SIZE, 60);
+ ui->allModsView->setColumnWidth(ModFields::VERSION, 60);
+
+ connect( ui->allModsView->selectionModel(), SIGNAL( currentRowChanged( const QModelIndex &, const QModelIndex & )),
+ this, SLOT( modSelected( const QModelIndex &, const QModelIndex & )));
+
+ connect( filterModel, SIGNAL( modelReset()),
+ this, SLOT( modelReset()));
+}
+
+CModListView::CModListView(QWidget *parent) :
+ QWidget(parent),
+ ui(new Ui::CModListView)
+{
+ ui->setupUi(this);
+
+ setupModModel();
+ setupFilterModel();
+ setupModsView();
+
+ ui->progressWidget->setVisible(false);
+ dlManager = nullptr;
+
+ // hide mod description on start. looks better this way
+ hideModInfo();
+
+ for (auto entry : settings["launcher"]["repositoryURL"].Vector())
+ {
+ QString str = QString::fromUtf8(entry.String().c_str());
+
+ // URL must be encoded to something else to get rid of symbols illegal in file names
+ auto hashed = QCryptographicHash::hash(str.toUtf8(), QCryptographicHash::Md5);
+ auto hashedStr = QString::fromUtf8(hashed.toHex());
+
+ downloadFile(hashedStr + ".json", str, "repository index");
+ }
+}
+
+CModListView::~CModListView()
+{
+ delete ui;
+}
+
+void CModListView::showModInfo()
+{
+ ui->modInfoWidget->show();
+ ui->hideModInfoButton->setArrowType(Qt::RightArrow);
+}
+
+void CModListView::hideModInfo()
+{
+ ui->modInfoWidget->hide();
+ ui->hideModInfoButton->setArrowType(Qt::LeftArrow);
+}
+
+static QString replaceIfNotEmpty(QVariant value, QString pattern)
+{
+ if (value.canConvert())
+ return pattern.arg(value.toStringList().join(", "));
+
+ if (value.canConvert())
+ return pattern.arg(value.toString());
+
+ // all valid types of data should have been filtered by code above
+ assert(!value.isValid());
+
+ return "";
+}
+
+static QVariant sizeToString(QVariant value)
+{
+ if (value.canConvert())
+ {
+ static QString symbols = "kMGTPE";
+ auto number = value.toUInt();
+ size_t i=0;
+
+ while (number >= 1000)
+ {
+ number /= 1000;
+ i++;
+ }
+ return QVariant(QString("%1 %2B").arg(number).arg(symbols.at(i)));
+ }
+ return value;
+}
+
+static QString replaceIfNotEmpty(QStringList value, QString pattern)
+{
+ if (!value.empty())
+ return pattern.arg(value.join(", "));
+ return "";
+}
+
+QString CModListView::genModInfoText(CModEntry &mod)
+{
+ QString prefix = "%1: "; // shared prefix
+ QString lineTemplate = prefix + "%2
";
+ QString urlTemplate = prefix + "%2
";
+ QString textTemplate = prefix + "
%2
";
+ QString noteTemplate = "%1: %2
";
+
+ QString result;
+
+ result += "";
+ result += replaceIfNotEmpty(mod.getValue("name"), lineTemplate.arg("Mod name"));
+ result += replaceIfNotEmpty(mod.getValue("installedVersion"), lineTemplate.arg("Installed version"));
+ result += replaceIfNotEmpty(mod.getValue("latestVersion"), lineTemplate.arg("Latest version"));
+ result += replaceIfNotEmpty(sizeToString(mod.getValue("size")), lineTemplate.arg("Download size"));
+ result += replaceIfNotEmpty(mod.getValue("author"), lineTemplate.arg("Authors"));
+ result += replaceIfNotEmpty(mod.getValue("contact"), urlTemplate.arg("Home"));
+ result += replaceIfNotEmpty(mod.getValue("depends"), lineTemplate.arg("Required mods"));
+ result += replaceIfNotEmpty(mod.getValue("conflicts"), lineTemplate.arg("Conflicting mods"));
+ result += replaceIfNotEmpty(mod.getValue("description"), textTemplate.arg("Description"));
+
+ result += ""; // to get some empty space
+
+ QString unknownDeps = "This mod can not be installed or enabled because following dependencies are not present";
+ QString blockingMods = "This mod can not be enabled because following mods are incompatible with this mod";
+ QString hasActiveDependentMods = "This mod can not be disabled because it is required to run following mods";
+ QString hasDependentMods = "This mod can not be uninstalled or updated because it is required to run following mods";
+
+ QString notes;
+
+ notes += replaceIfNotEmpty(findInvalidDependencies(mod.getName()), noteTemplate.arg(unknownDeps));
+ notes += replaceIfNotEmpty(findBlockingMods(mod.getName()), noteTemplate.arg(blockingMods));
+ if (mod.isEnabled())
+ notes += replaceIfNotEmpty(findDependentMods(mod.getName(), true), noteTemplate.arg(hasActiveDependentMods));
+ if (mod.isInstalled())
+ notes += replaceIfNotEmpty(findDependentMods(mod.getName(), false), noteTemplate.arg(hasDependentMods));
+
+ if (notes.size())
+ result += textTemplate.arg("Notes").arg(notes);
+
+ result += "";
+ return result;
+}
+
+void CModListView::enableModInfo()
+{
+ ui->hideModInfoButton->setEnabled(true);
+}
+
+void CModListView::disableModInfo()
+{
+ hideModInfo();
+ ui->hideModInfoButton->setEnabled(false);
+}
+
+void CModListView::selectMod(int index)
+{
+ if (index < 0)
+ {
+ disableModInfo();
+ }
+ else
+ {
+ enableModInfo();
+
+ auto mod = modModel->getMod(modModel->modIndexToName(index));
+
+ ui->textBrowser->setHtml(genModInfoText(mod));
+
+ bool hasInvalidDeps = !findInvalidDependencies(modModel->modIndexToName(index)).empty();
+ bool hasBlockingMods = !findBlockingMods(modModel->modIndexToName(index)).empty();
+ bool hasDependentMods = !findDependentMods(modModel->modIndexToName(index), true).empty();
+
+ ui->disableButton->setVisible(mod.isEnabled());
+ ui->enableButton->setVisible(mod.isDisabled());
+ ui->installButton->setVisible(mod.isAvailable());
+ ui->uninstallButton->setVisible(mod.isInstalled());
+ ui->updateButton->setVisible(mod.isUpdateable());
+
+ // Block buttons if action is not allowed at this time
+ // TODO: automate handling of some of these cases instead of forcing player
+ // to resolve all conflicts manually.
+ ui->disableButton->setEnabled(!hasDependentMods);
+ ui->enableButton->setEnabled(!hasBlockingMods && !hasInvalidDeps);
+ ui->installButton->setEnabled(!hasInvalidDeps);
+ ui->uninstallButton->setEnabled(!hasDependentMods);
+ ui->updateButton->setEnabled(!hasInvalidDeps && !hasDependentMods);
+ }
+}
+
+void CModListView::keyPressEvent(QKeyEvent * event)
+{
+ if (event->key() == Qt::Key_Escape && ui->modInfoWidget->isVisible() )
+ {
+ ui->modInfoWidget->hide();
+ }
+ else
+ {
+ return QWidget::keyPressEvent(event);
+ }
+}
+
+void CModListView::modSelected(const QModelIndex & current, const QModelIndex & )
+{
+ selectMod(filterModel->mapToSource(current).row());
+}
+
+void CModListView::on_hideModInfoButton_clicked()
+{
+ if (ui->modInfoWidget->isVisible())
+ hideModInfo();
+ else
+ showModInfo();
+}
+
+void CModListView::on_allModsView_doubleClicked(const QModelIndex &index)
+{
+ showModInfo();
+ selectMod(filterModel->mapToSource(index).row());
+}
+
+void CModListView::on_lineEdit_textChanged(const QString &arg1)
+{
+ QRegExp regExp(arg1, Qt::CaseInsensitive, QRegExp::Wildcard);
+ filterModel->setFilterRegExp(regExp);
+}
+
+void CModListView::on_comboBox_currentIndexChanged(int index)
+{
+ switch (index)
+ {
+ break; case 0: filterModel->setTypeFilter(ModStatus::MASK_NONE, ModStatus::MASK_NONE);
+ break; case 1: filterModel->setTypeFilter(ModStatus::MASK_NONE, ModStatus::INSTALLED);
+ break; case 2: filterModel->setTypeFilter(ModStatus::INSTALLED, ModStatus::INSTALLED);
+ break; case 3: filterModel->setTypeFilter(ModStatus::UPDATEABLE, ModStatus::UPDATEABLE);
+ break; case 4: filterModel->setTypeFilter(ModStatus::ENABLED | ModStatus::INSTALLED, ModStatus::ENABLED | ModStatus::INSTALLED);
+ break; case 5: filterModel->setTypeFilter(ModStatus::INSTALLED, ModStatus::ENABLED | ModStatus::INSTALLED);
+ }
+}
+
+QStringList CModListView::findInvalidDependencies(QString mod)
+{
+ QStringList ret;
+ for (QString requrement : modModel->getRequirements(mod))
+ {
+ if (!modModel->hasMod(requrement))
+ ret += requrement;
+ }
+ return ret;
+}
+
+QStringList CModListView::findBlockingMods(QString mod)
+{
+ QStringList ret;
+ auto required = modModel->getRequirements(mod);
+
+ for (QString name : modModel->getModList())
+ {
+ auto mod = modModel->getMod(name);
+
+ if (mod.isEnabled())
+ {
+ // one of enabled mods have requirement (or this mod) marked as conflict
+ for (auto conflict : mod.getValue("conflicts").toStringList())
+ if (required.contains(conflict))
+ ret.push_back(name);
+ }
+ }
+
+ return ret;
+}
+
+QStringList CModListView::findDependentMods(QString mod, bool excludeDisabled)
+{
+ QStringList ret;
+ for (QString modName : modModel->getModList())
+ {
+ auto current = modModel->getMod(modName);
+
+ if (current.getValue("depends").toStringList().contains(mod) &&
+ !(current.isDisabled() && excludeDisabled))
+ ret += modName;
+ }
+ return ret;
+}
+
+void CModListView::on_enableButton_clicked()
+{
+ QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
+
+ assert(findBlockingMods(modName).empty());
+ assert(findInvalidDependencies(modName).empty());
+
+ for (auto & name : modModel->getRequirements(modName))
+ if (modModel->getMod(name).isDisabled())
+ manager->enableMod(name);
+}
+
+void CModListView::on_disableButton_clicked()
+{
+ QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
+
+ for (auto & name : modModel->getRequirements(modName))
+ if (modModel->hasMod(name) &&
+ modModel->getMod(name).isEnabled())
+ manager->disableMod(name);
+}
+
+void CModListView::on_updateButton_clicked()
+{
+ QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
+
+ assert(findInvalidDependencies(modName).empty());
+
+ for (auto & name : modModel->getRequirements(modName))
+ {
+ auto mod = modModel->getMod(name);
+ // update required mod, install missing (can be new dependency)
+ if (mod.isUpdateable() || !mod.isInstalled())
+ downloadFile(name + ".zip", mod.getValue("download").toString(), "mods");
+ }
+}
+
+void CModListView::on_uninstallButton_clicked()
+{
+ QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
+ // NOTE: perhaps add "manually installed" flag and uninstall those dependencies that don't have it?
+
+ if (modModel->hasMod(modName) &&
+ modModel->getMod(modName).isInstalled())
+ {
+ manager->disableMod(modName);
+ manager->uninstallMod(modName);
+ }
+}
+
+void CModListView::on_installButton_clicked()
+{
+ QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
+
+ assert(findInvalidDependencies(modName).empty());
+
+ for (auto & name : modModel->getRequirements(modName))
+ {
+ auto mod = modModel->getMod(name);
+ if (!mod.isInstalled())
+ downloadFile(name + ".zip", mod.getValue("download").toString(), "mods");
+ }
+}
+
+void CModListView::downloadFile(QString file, QString url, QString description)
+{
+ if (!dlManager)
+ {
+ dlManager = new CDownloadManager();
+ ui->progressWidget->setVisible(true);
+ connect(dlManager, SIGNAL(downloadProgress(qint64,qint64)),
+ this, SLOT(downloadProgress(qint64,qint64)));
+
+ connect(dlManager, SIGNAL(finished(QStringList,QStringList,QStringList)),
+ this, SLOT(downloadFinished(QStringList,QStringList,QStringList)));
+
+
+ QString progressBarFormat = "Downloading %s%. %p% (%v KB out of %m KB) finished";
+
+ progressBarFormat.replace("%s%", description);
+ ui->progressBar->setFormat(progressBarFormat);
+ }
+
+ dlManager->downloadFile(QUrl(url), file);
+}
+
+void CModListView::downloadProgress(qint64 current, qint64 max)
+{
+ // display progress, in kilobytes
+ ui->progressBar->setValue(current/1024);
+ ui->progressBar->setMaximum(max/1024);
+}
+
+void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFiles, QStringList errors)
+{
+ QString title = "Download failed";
+ QString firstLine = "Unable to download all files.\n\nEncountered errors:\n\n";
+ QString lastLine = "\n\nInstall successfully downloaded?";
+
+ // if all files were d/loaded there should be no errors. And on failure there must be an error
+ assert(failedFiles.empty() == errors.empty());
+
+ if (savedFiles.empty())
+ {
+ // no successfully downloaded mods
+ QMessageBox::warning(this, title, firstLine + errors.join("\n"), QMessageBox::Ok, QMessageBox::Ok );
+ }
+ else if (!failedFiles.empty())
+ {
+ // some mods were not downloaded
+ int result = QMessageBox::warning (this, title, firstLine + errors.join("\n") + lastLine,
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No );
+
+ if (result == QMessageBox::Yes)
+ installFiles(savedFiles);
+ }
+ else
+ {
+ // everything OK
+ installFiles(savedFiles);
+ }
+
+ // remove progress bar after some delay so user can see that download was complete and not interrupted.
+ QTimer::singleShot(1000, this, SLOT(hideProgressBar()));
+
+ dlManager->deleteLater();
+ dlManager = nullptr;
+}
+
+void CModListView::hideProgressBar()
+{
+ if (dlManager == nullptr) // it was not recreated meanwhile
+ {
+ ui->progressWidget->setVisible(false);
+ ui->progressBar->setMaximum(0);
+ ui->progressBar->setValue(0);
+ }
+}
+
+void CModListView::installFiles(QStringList files)
+{
+ QStringList mods;
+
+ // TODO: some better way to separate zip's with mods and downloaded repository files
+ for (QString filename : files)
+ {
+ if (filename.contains(".zip"))
+ mods.push_back(filename);
+ if (filename.contains(".json"))
+ manager->loadRepository(filename);
+ }
+ if (!mods.empty())
+ installMods(mods);
+}
+
+void CModListView::installMods(QStringList archives)
+{
+ //TODO: check return status of all calls to manager!!!
+
+ QStringList modNames;
+
+ for (QString archive : archives)
+ {
+ // get basename out of full file name
+ // remove path remove extension
+ QString modName = archive.section('/', -1, -1).section('.', 0, 0);
+
+ modNames.push_back(modName);
+ }
+
+ // disable mod(s), to properly recalculate dependencies, if changed
+ for (QString mod : boost::adaptors::reverse(modNames))
+ manager->disableMod(mod);
+
+ // uninstall old version of mod, if installed
+ for (QString mod : boost::adaptors::reverse(modNames))
+ manager->uninstallMod(mod);
+
+ for (int i=0; iinstallMod(modNames[i], archives[i]);
+
+ if (settings["launcher"]["enableInstalledMods"].Bool())
+ {
+ for (QString mod : modNames)
+ manager->enableMod(mod);
+ }
+
+ for (QString archive : archives)
+ QFile::remove(archive);
+}
+
+void CModListView::on_pushButton_clicked()
+{
+ delete dlManager;
+ dlManager = nullptr;
+ hideProgressBar();
+}
+
+void CModListView::modelReset()
+{
+ selectMod(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
+}
diff --git a/launcher/modManager/cmodlistview.h b/launcher/modManager/cmodlistview.h
new file mode 100644
index 000000000..1fab2f4d4
--- /dev/null
+++ b/launcher/modManager/cmodlistview.h
@@ -0,0 +1,84 @@
+#pragma once
+
+namespace Ui {
+ class CModListView;
+}
+
+class CModManager;
+class CModListModel;
+class CModFilterModel;
+class CDownloadManager;
+class QTableWidgetItem;
+
+class CModEntry;
+
+class CModListView : public QWidget
+{
+ Q_OBJECT
+
+ CModManager * manager;
+ CModListModel * modModel;
+ CModFilterModel * filterModel;
+ CDownloadManager * dlManager;
+
+ void keyPressEvent(QKeyEvent * event);
+
+ void setupModModel();
+ void setupFilterModel();
+ void setupModsView();
+
+ // find mods unknown to mod list (not present in repo and not installed)
+ QStringList findInvalidDependencies(QString mod);
+ // find mods that block enabling of this mod: conflicting with this mod or one of required mods
+ QStringList findBlockingMods(QString mod);
+ // find mods that depend on this one
+ QStringList findDependentMods(QString mod, bool excludeDisabled);
+
+ void downloadFile(QString file, QString url, QString description);
+
+ void installMods(QStringList archives);
+ void installFiles(QStringList mods);
+
+ QString genModInfoText(CModEntry & mod);
+public:
+ explicit CModListView(QWidget *parent = 0);
+ ~CModListView();
+
+ void showModInfo();
+ void hideModInfo();
+
+ void enableModInfo();
+ void disableModInfo();
+
+ void selectMod(int index);
+
+private slots:
+ void modSelected(const QModelIndex & current, const QModelIndex & previous);
+ void downloadProgress(qint64 current, qint64 max);
+ void downloadFinished(QStringList savedFiles, QStringList failedFiles, QStringList errors);
+ void modelReset ();
+ void hideProgressBar();
+
+ void on_hideModInfoButton_clicked();
+
+ void on_allModsView_doubleClicked(const QModelIndex &index);
+
+ void on_lineEdit_textChanged(const QString &arg1);
+
+ void on_comboBox_currentIndexChanged(int index);
+
+ void on_enableButton_clicked();
+
+ void on_disableButton_clicked();
+
+ void on_updateButton_clicked();
+
+ void on_uninstallButton_clicked();
+
+ void on_installButton_clicked();
+
+ void on_pushButton_clicked();
+
+private:
+ Ui::CModListView *ui;
+};
diff --git a/launcher/modManager/cmodlistview.ui b/launcher/modManager/cmodlistview.ui
new file mode 100644
index 000000000..53d64b969
--- /dev/null
+++ b/launcher/modManager/cmodlistview.ui
@@ -0,0 +1,453 @@
+
+
+ CModListView
+
+
+
+ 0
+ 0
+ 596
+ 342
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ 6
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+
+
+
+ Filter
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ 0
+
+
-
+
+ All mods
+
+
+ -
+
+ Downloadable
+
+
+ -
+
+ Installed
+
+
+ -
+
+ Updatable
+
+
+ -
+
+ Active
+
+
+ -
+
+ Inactive
+
+
+
+
+ -
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::SelectRows
+
+
+
+ 32
+ 20
+
+
+
+ QAbstractItemView::ScrollPerItem
+
+
+ QAbstractItemView::ScrollPerPixel
+
+
+ true
+
+
+ true
+
+
+ false
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 16
+ 100
+
+
+
+
+
+
+ true
+
+
+ Qt::RightArrow
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 16777215
+ 16777215
+
+
+
+ Qt::LeftToRight
+
+
+ false
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ true
+
+
+ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<html><head><meta name="qrichtext" content="1" /><style type="text/css">
+p, li { white-space: pre-wrap; }
+</style></head><body style=" font-family:'Ubuntu'; font-size:11pt; font-weight:400; font-style:normal;">
+<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><br /></p></body></html>
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::MinimumExpanding
+
+
+
+ 0
+ 20
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 51
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Enable
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 51
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Disable
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 51
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Update
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 51
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Uninstall
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 51
+ 0
+
+
+
+
+ 100
+ 16777215
+
+
+
+ Install
+
+
+
+
+
+
+ -
+
+
+ true
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+ 0
+
+
+ 0
+
+
+ true
+
+
+ false
+
+
+ %p% (%v KB out of %m KB)
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+ Abort
+
+
+
+
+
+
+
+
+
+ lineEdit
+ comboBox
+ allModsView
+ textBrowser
+ hideModInfoButton
+ enableButton
+ disableButton
+ updateButton
+ uninstallButton
+ installButton
+
+
+
+
diff --git a/launcher/modManager/cmodmanager.cpp b/launcher/modManager/cmodmanager.cpp
new file mode 100644
index 000000000..7c4d18fb5
--- /dev/null
+++ b/launcher/modManager/cmodmanager.cpp
@@ -0,0 +1,256 @@
+#include "StdInc.h"
+#include "cmodmanager.h"
+
+#include "../lib/VCMIDirs.h"
+#include "../lib/filesystem/Filesystem.h"
+#include "../lib/filesystem/CZipLoader.h"
+
+#include "../launcherdirs.h"
+
+static QJsonObject JsonFromFile(QString filename)
+{
+ QFile file(filename);
+ file.open(QFile::ReadOnly);
+
+ return QJsonDocument::fromJson(file.readAll()).object();
+}
+
+static void JsonToFile(QString filename, QJsonObject object)
+{
+ QFile file(filename);
+ file.open(QFile::WriteOnly);
+ file.write(QJsonDocument(object).toJson());
+}
+
+static QString detectModArchive(QString path, QString modName)
+{
+ auto files = ZipArchive::listFiles(path.toUtf8().data());
+
+ QString modDirName;
+
+ for (auto file : files)
+ {
+ QString filename = QString::fromUtf8(file.c_str());
+ if (filename.toLower().startsWith(modName))
+ {
+ // archive must contain mod.json file
+ if (filename.toLower() == modName + "/mod.json")
+ modDirName = filename.section('/', 0, 0);
+ }
+ else // all files must be in directory
+ return "";
+ }
+ return modDirName;
+}
+
+CModManager::CModManager(CModList * modList):
+ modList(modList)
+{
+ loadMods();
+ loadModSettings();
+}
+
+QString CModManager::settingsPath()
+{
+ return QString::fromUtf8(VCMIDirs::get().userConfigPath().c_str()) + "/modSettings.json";
+}
+
+void CModManager::loadModSettings()
+{
+ modSettings = JsonFromFile(settingsPath());
+ modList->setModSettings(modSettings["activeMods"].toObject());
+}
+
+void CModManager::loadRepository(QString file)
+{
+ modList->addRepository(JsonFromFile(file));
+}
+
+void CModManager::loadMods()
+{
+ auto installedMods = CResourceHandler::getAvailableMods();
+
+ for (auto modname : installedMods)
+ {
+ ResourceID resID("Mods/" + modname + "/mod.json");
+
+ if (CResourceHandler::get()->existsResource(resID))
+ {
+ auto data = CResourceHandler::get()->load(resID)->readAll();
+ auto array = QByteArray(reinterpret_cast(data.first.get()), data.second);
+
+ auto mod = QJsonDocument::fromJson(array);
+ assert (mod.isObject()); // TODO: use JsonNode from vcmi code here - QJsonNode parser is just too pedantic
+
+ localMods.insert(QString::fromUtf8(modname.c_str()).toLower(), QJsonValue(mod.object()));
+ }
+ }
+ modList->setLocalModList(localMods);
+}
+
+bool CModManager::installMod(QString modname, QString archivePath)
+{
+ return canInstallMod(modname) && doInstallMod(modname, archivePath);
+}
+
+bool CModManager::uninstallMod(QString modname)
+{
+ return canUninstallMod(modname) && doUninstallMod(modname);
+}
+
+bool CModManager::enableMod(QString modname)
+{
+ return canEnableMod(modname) && doEnableMod(modname, true);
+}
+
+bool CModManager::disableMod(QString modname)
+{
+ return canDisableMod(modname) && doEnableMod(modname, false);
+}
+
+bool CModManager::canInstallMod(QString modname)
+{
+ auto mod = modList->getMod(modname);
+
+ if (mod.isInstalled())
+ return false;
+
+ if (!mod.isAvailable())
+ return false;
+ return true;
+}
+
+bool CModManager::canUninstallMod(QString modname)
+{
+ auto mod = modList->getMod(modname);
+
+ if (!mod.isInstalled())
+ return false;
+
+ if (mod.isEnabled())
+ return false;
+ return true;
+}
+
+bool CModManager::canEnableMod(QString modname)
+{
+ auto mod = modList->getMod(modname);
+
+ if (mod.isEnabled())
+ return false;
+
+ if (!mod.isInstalled())
+ return false;
+
+ for (auto modEntry : mod.getValue("depends").toStringList())
+ {
+ if (!modList->hasMod(modEntry)) // required mod is not available
+ return false;
+ if (!modList->getMod(modEntry).isEnabled())
+ return false;
+ }
+
+ for (QString name : modList->getModList())
+ {
+ auto mod = modList->getMod(name);
+
+ if (mod.isEnabled() && mod.getValue("conflicts").toStringList().contains(modname))
+ return false; // "reverse conflict" - enabled mod has this one as conflict
+ }
+
+ for (auto modEntry : mod.getValue("conflicts").toStringList())
+ {
+ if (modList->hasMod(modEntry) &&
+ modList->getMod(modEntry).isEnabled()) // conflicting mod installed and enabled
+ return false;
+ }
+ return true;
+}
+
+bool CModManager::canDisableMod(QString modname)
+{
+ auto mod = modList->getMod(modname);
+
+ if (mod.isDisabled())
+ return false;
+
+ if (!mod.isInstalled())
+ return false;
+
+ for (QString modEntry : modList->getModList())
+ {
+ auto current = modList->getMod(modEntry);
+
+ if (current.getValue("depends").toStringList().contains(modname) &&
+ !current.isDisabled())
+ return false; // this mod must be disabled first
+ }
+ return true;
+}
+
+bool CModManager::doEnableMod(QString mod, bool on)
+{
+ QJsonValue value(on);
+ QJsonObject list = modSettings["activeMods"].toObject();
+
+ list.insert(mod, value);
+ modSettings.insert("activeMods", list);
+
+ modList->setModSettings(modSettings["activeMods"].toObject());
+
+ JsonToFile(settingsPath(), modSettings);
+
+ return true;
+}
+
+bool CModManager::doInstallMod(QString modname, QString archivePath)
+{
+ QString destDir = CLauncherDirs::get().modsPath() + "/";
+
+ if (!QFile(archivePath).exists())
+ return false; // archive with mod data exists
+
+ if (QDir(destDir + modname).exists()) // FIXME: recheck wog/vcmi data behavior - they have bits of data in our trunk
+ return false; // no mod with such name installed
+
+ if (localMods.contains(modname))
+ return false; // no installed data known
+
+ QString modDirName = detectModArchive(archivePath, modname);
+ if (!modDirName.size())
+ return false; // archive content looks like mod FS
+
+ if (!ZipArchive::extract(archivePath.toUtf8().data(), destDir.toUtf8().data()))
+ {
+ QDir(destDir + modDirName).removeRecursively();
+ return false; // extraction failed
+ }
+
+ QJsonObject json = JsonFromFile(destDir + modDirName + "/mod.json");
+
+ localMods.insert(modname, json);
+ modList->setLocalModList(localMods);
+
+ return true;
+}
+
+bool CModManager::doUninstallMod(QString modname)
+{
+ ResourceID resID(std::string("Mods/") + modname.toUtf8().data(), EResType::DIRECTORY);
+ // Get location of the mod, in case-insensitive way
+ QString modDir = QString::fromUtf8(CResourceHandler::get()->getResourceName(resID)->c_str());
+
+ if (!QDir(modDir).exists())
+ return false;
+
+ if (!localMods.contains(modname))
+ return false;
+
+ if (!QDir(modDir).removeRecursively())
+ return false;
+
+ localMods.remove(modname);
+ modList->setLocalModList(localMods);
+
+ return true;
+}
diff --git a/launcher/modManager/cmodmanager.h b/launcher/modManager/cmodmanager.h
new file mode 100644
index 000000000..bb787a84d
--- /dev/null
+++ b/launcher/modManager/cmodmanager.h
@@ -0,0 +1,38 @@
+#pragma once
+
+#include "cmodlist.h"
+
+class CModManager
+{
+ CModList * modList;
+
+ QString settingsPath();
+
+ // check-free version of public method
+ bool doEnableMod(QString mod, bool on);
+ bool doInstallMod(QString mod, QString archivePath);
+ bool doUninstallMod(QString mod);
+
+ QJsonObject modSettings;
+ QJsonObject localMods;
+
+public:
+ CModManager(CModList * modList);
+
+ void loadRepository(QString filename);
+ void loadModSettings();
+ void loadMods();
+
+ /// mod management functions. Return true if operation was successful
+
+ /// installs mod from zip archive located at archivePath
+ bool installMod(QString mod, QString archivePath);
+ bool uninstallMod(QString mod);
+ bool enableMod(QString mod);
+ bool disableMod(QString mod);
+
+ bool canInstallMod(QString mod);
+ bool canUninstallMod(QString mod);
+ bool canEnableMod(QString mod);
+ bool canDisableMod(QString mod);
+};
diff --git a/launcher/settingsView/csettingsview.cpp b/launcher/settingsView/csettingsview.cpp
new file mode 100644
index 000000000..1dce1f976
--- /dev/null
+++ b/launcher/settingsView/csettingsview.cpp
@@ -0,0 +1,112 @@
+#include "StdInc.h"
+#include "csettingsview.h"
+#include "ui_csettingsview.h"
+
+#include "../lib/CConfigHandler.h"
+#include "../lib/VCMIDirs.h"
+
+void CSettingsView::loadSettings()
+{
+ int resX = settings["video"]["screenRes"]["width"].Float();
+ int resY = settings["video"]["screenRes"]["height"].Float();
+
+ int resIndex = ui->comboBoxResolution->findText(QString("%1x%2").arg(resX).arg(resY));
+
+ ui->comboBoxResolution->setCurrentIndex(resIndex);
+ ui->comboBoxFullScreen->setCurrentIndex(settings["video"]["fullscreen"].Bool());
+
+ int neutralAIIndex = ui->comboBoxNeutralAI->findText(QString::fromUtf8(settings["server"]["neutralAI"].String().c_str()));
+ int playerAIIndex = ui->comboBoxPlayerAI->findText(QString::fromUtf8(settings["server"]["playerAI"].String().c_str()));
+
+ ui->comboBoxNeutralAI->setCurrentIndex(neutralAIIndex);
+ ui->comboBoxPlayerAI->setCurrentIndex(playerAIIndex);
+
+ ui->spinBoxNetworkPort->setValue(settings["server"]["port"].Float());
+
+ ui->comboBoxEnableMods->setCurrentIndex(settings["launcher"]["enableInstalledMods"].Bool());
+
+ // all calls to plainText will trigger textChanged() signal overwriting config. Create backup before editing widget
+ JsonNode urls = settings["launcher"]["repositoryURL"];
+
+ ui->plainTextEditRepos->clear();
+ for (auto entry : urls.Vector())
+ ui->plainTextEditRepos->appendPlainText(QString::fromUtf8(entry.String().c_str()));
+
+ ui->lineEditUserDataDir->setText(QString::fromUtf8(VCMIDirs::get().userDataPath().c_str()));
+ QStringList dataDirs;
+ for (auto string : VCMIDirs::get().dataPaths())
+ dataDirs += QString::fromUtf8(string.c_str());
+ ui->lineEditGameDir->setText(dataDirs.join(':'));
+}
+
+CSettingsView::CSettingsView(QWidget *parent) :
+ QWidget(parent),
+ ui(new Ui::CSettingsView)
+{
+ ui->setupUi(this);
+
+ loadSettings();
+}
+
+CSettingsView::~CSettingsView()
+{
+ delete ui;
+}
+
+void CSettingsView::on_comboBoxResolution_currentIndexChanged(const QString &arg1)
+{
+ QStringList list = arg1.split("x");
+
+ Settings node = settings.write["video"]["screenRes"];
+ node["width"].Float() = list[0].toInt();
+ node["height"].Float() = list[1].toInt();
+}
+
+void CSettingsView::on_comboBoxFullScreen_currentIndexChanged(int index)
+{
+ Settings node = settings.write["video"]["fullscreen"];
+ node->Bool() = index;
+}
+
+void CSettingsView::on_comboBoxPlayerAI_currentIndexChanged(const QString &arg1)
+{
+ Settings node = settings.write["server"]["playerAI"];
+ node->String() = arg1.toUtf8().data();
+}
+
+void CSettingsView::on_comboBoxNeutralAI_currentIndexChanged(const QString &arg1)
+{
+ Settings node = settings.write["server"]["neutralAI"];
+ node->String() = arg1.toUtf8().data();
+}
+
+void CSettingsView::on_comboBoxEnableMods_currentIndexChanged(int index)
+{
+ Settings node = settings.write["launcher"]["enableInstalledMods"];
+ node->Bool() = index;
+}
+
+void CSettingsView::on_spinBoxNetworkPort_valueChanged(int arg1)
+{
+ Settings node = settings.write["server"]["port"];
+ node->Float() = arg1;
+}
+
+void CSettingsView::on_plainTextEditRepos_textChanged()
+{
+ Settings node = settings.write["launcher"]["repositoryURL"];
+
+ QStringList list = ui->plainTextEditRepos->toPlainText().split('\n');
+
+ node->Vector().clear();
+ for (QString line : list)
+ {
+ if (line.trimmed().size() > 0)
+ {
+ JsonNode entry;
+ entry.String() = line.trimmed().toUtf8().data();
+ node->Vector().push_back(entry);
+ }
+ }
+
+}
diff --git a/launcher/settingsView/csettingsview.h b/launcher/settingsView/csettingsview.h
new file mode 100644
index 000000000..419b91302
--- /dev/null
+++ b/launcher/settingsView/csettingsview.h
@@ -0,0 +1,34 @@
+#pragma once
+
+namespace Ui {
+ class CSettingsView;
+}
+
+class CSettingsView : public QWidget
+{
+ Q_OBJECT
+
+public:
+ explicit CSettingsView(QWidget *parent = 0);
+ ~CSettingsView();
+
+ void loadSettings();
+
+private slots:
+ void on_comboBoxResolution_currentIndexChanged(const QString &arg1);
+
+ void on_comboBoxFullScreen_currentIndexChanged(int index);
+
+ void on_comboBoxPlayerAI_currentIndexChanged(const QString &arg1);
+
+ void on_comboBoxNeutralAI_currentIndexChanged(const QString &arg1);
+
+ void on_comboBoxEnableMods_currentIndexChanged(int index);
+
+ void on_spinBoxNetworkPort_valueChanged(int arg1);
+
+ void on_plainTextEditRepos_textChanged();
+
+private:
+ Ui::CSettingsView *ui;
+};
diff --git a/launcher/settingsView/csettingsview.ui b/launcher/settingsView/csettingsview.ui
new file mode 100644
index 000000000..d28ce195c
--- /dev/null
+++ b/launcher/settingsView/csettingsview.ui
@@ -0,0 +1,367 @@
+
+
+ CSettingsView
+
+
+
+ 0
+ 0
+ 700
+ 303
+
+
+
+ Form
+
+
+ -
+
+
+ false
+
+
+
+ 150
+ 0
+
+
+
+ /usr/share/vcmi
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Fixed
+
+
+
+ 20
+ 8
+
+
+
+
+ -
+
+
+ Resolution
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+ QSizePolicy::Fixed
+
+
+
+ 56
+ 8
+
+
+
+
+ -
+
+
+ Fullscreen
+
+
+
+ -
+
+
+ 1024
+
+
+ 65535
+
+
+ 3030
+
+
+
+ -
+
+
-
+
+ VCAI
+
+
+
+
+ -
+
+
+ Network port
+
+
+
+ -
+
+
-
+
+ 800x600
+
+
+ -
+
+ 1024x600
+
+
+ -
+
+ 1024x768
+
+
+ -
+
+ 1280x800
+
+
+ -
+
+ 1280x960
+
+
+ -
+
+ 1280x1024
+
+
+ -
+
+ 1366x768
+
+
+ -
+
+ 1440x900
+
+
+ -
+
+ 1600x1200
+
+
+ -
+
+ 1680x1050
+
+
+ -
+
+ 1920x1080
+
+
+
+
+ -
+
+
+ User data directory
+
+
+
+ -
+
+
+ 1
+
+
-
+
+ Off
+
+
+ -
+
+ On
+
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ Repositories
+
+
+
+ -
+
+
+ QPlainTextEdit::NoWrap
+
+
+ http://downloads.vcmi.eu/Mods/repository.json
+
+
+
+ -
+
+
+ Player AI
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+ QSizePolicy::Fixed
+
+
+
+ 8
+ 20
+
+
+
+
+ -
+
+
+ 0
+
+
-
+
+ Off
+
+
+ -
+
+ On
+
+
+
+
+ -
+
+
+ Enable mods on install
+
+
+
+ -
+
+
+ false
+
+
+
+ 150
+ 0
+
+
+
+ /home/user/.vcmi
+
+
+ true
+
+
+
+ -
+
+
+ Neutral AI
+
+
+
+ -
+
+
-
+
+ StupidAI
+
+
+ -
+
+ BattleAI
+
+
+
+
+ -
+
+
+ Game directory
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ AI Settings
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ Video
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ Data Directories (unchangeable)
+
+
+
+ -
+
+
+
+ 75
+ true
+
+
+
+ General
+
+
+
+
+
+
+
+