cmodlistview_moc.cpp 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553
  1. #include "StdInc.h"
  2. #include "cmodlistview_moc.h"
  3. #include "ui_cmodlistview_moc.h"
  4. #include <QJsonArray>
  5. #include <QCryptographicHash>
  6. #include "cmodlistmodel_moc.h"
  7. #include "cmodmanager.h"
  8. #include "cdownloadmanager_moc.h"
  9. #include "../launcherdirs.h"
  10. #include "../../lib/CConfigHandler.h"
  11. void CModListView::setupModModel()
  12. {
  13. modModel = new CModListModel();
  14. manager = new CModManager(modModel);
  15. }
  16. void CModListView::setupFilterModel()
  17. {
  18. filterModel = new CModFilterModel(modModel);
  19. filterModel->setFilterKeyColumn(-1); // filter across all columns
  20. filterModel->setSortCaseSensitivity(Qt::CaseInsensitive); // to make it more user-friendly
  21. }
  22. void CModListView::setupModsView()
  23. {
  24. ui->allModsView->setModel(filterModel);
  25. // input data is not sorted - sort it before display
  26. ui->allModsView->sortByColumn(ModFields::TYPE, Qt::AscendingOrder);
  27. ui->allModsView->setColumnWidth(ModFields::STATUS_ENABLED, 30);
  28. ui->allModsView->setColumnWidth(ModFields::STATUS_UPDATE, 30);
  29. ui->allModsView->setColumnWidth(ModFields::TYPE, 80);
  30. ui->allModsView->setColumnWidth(ModFields::NAME, 180);
  31. ui->allModsView->setColumnWidth(ModFields::SIZE, 80);
  32. ui->allModsView->setColumnWidth(ModFields::VERSION, 60);
  33. connect( ui->allModsView->selectionModel(), SIGNAL( currentRowChanged( const QModelIndex &, const QModelIndex & )),
  34. this, SLOT( modSelected( const QModelIndex &, const QModelIndex & )));
  35. connect( filterModel, SIGNAL( modelReset()),
  36. this, SLOT( modelReset()));
  37. selectMod(filterModel->rowCount() > 0 ? 0 : -1);
  38. }
  39. CModListView::CModListView(QWidget *parent) :
  40. QWidget(parent),
  41. settingsListener(settings.listen["launcher"]["repositoryURL"]),
  42. ui(new Ui::CModListView)
  43. {
  44. settingsListener([&](const JsonNode &){ repositoriesChanged = true; });
  45. ui->setupUi(this);
  46. setupModModel();
  47. setupFilterModel();
  48. setupModsView();
  49. ui->progressWidget->setVisible(false);
  50. dlManager = nullptr;
  51. loadRepositories();
  52. }
  53. void CModListView::loadRepositories()
  54. {
  55. manager->resetRepositories();
  56. for (auto entry : settings["launcher"]["repositoryURL"].Vector())
  57. {
  58. QString str = QString::fromUtf8(entry.String().c_str());
  59. // URL must be encoded to something else to get rid of symbols illegal in file names
  60. auto hashed = QCryptographicHash::hash(str.toUtf8(), QCryptographicHash::Md5);
  61. auto hashedStr = QString::fromUtf8(hashed.toHex());
  62. downloadFile(hashedStr + ".json", str, "repository index");
  63. }
  64. }
  65. CModListView::~CModListView()
  66. {
  67. delete ui;
  68. }
  69. void CModListView::showEvent(QShowEvent * event)
  70. {
  71. QWidget::showEvent(event);
  72. if (repositoriesChanged)
  73. {
  74. repositoriesChanged = false;
  75. loadRepositories();
  76. }
  77. }
  78. void CModListView::showModInfo()
  79. {
  80. ui->modInfoWidget->show();
  81. ui->hideModInfoButton->setArrowType(Qt::RightArrow);
  82. }
  83. void CModListView::hideModInfo()
  84. {
  85. ui->modInfoWidget->hide();
  86. ui->hideModInfoButton->setArrowType(Qt::LeftArrow);
  87. }
  88. static QString replaceIfNotEmpty(QVariant value, QString pattern)
  89. {
  90. if (value.canConvert<QStringList>())
  91. return pattern.arg(value.toStringList().join(", "));
  92. if (value.canConvert<QString>())
  93. return pattern.arg(value.toString());
  94. // all valid types of data should have been filtered by code above
  95. assert(!value.isValid());
  96. return "";
  97. }
  98. static QString replaceIfNotEmpty(QStringList value, QString pattern)
  99. {
  100. if (!value.empty())
  101. return pattern.arg(value.join(", "));
  102. return "";
  103. }
  104. QString CModListView::genModInfoText(CModEntry &mod)
  105. {
  106. QString prefix = "<p><span style=\" font-weight:600;\">%1: </span>"; // shared prefix
  107. QString lineTemplate = prefix + "%2</p>";
  108. QString urlTemplate = prefix + "<a href=\"%2\"><span style=\" text-decoration: underline; color:#0000ff;\">%2</span></a></p>";
  109. QString textTemplate = prefix + "</p><p align=\"justify\">%2</p>";
  110. QString noteTemplate = "<p align=\"justify\">%1: %2</p>";
  111. QString result;
  112. result += "<html><body>";
  113. result += replaceIfNotEmpty(mod.getValue("name"), lineTemplate.arg("Mod name"));
  114. result += replaceIfNotEmpty(mod.getValue("installedVersion"), lineTemplate.arg("Installed version"));
  115. result += replaceIfNotEmpty(mod.getValue("latestVersion"), lineTemplate.arg("Latest version"));
  116. result += replaceIfNotEmpty(CModEntry::sizeToString(mod.getValue("size").toDouble()), lineTemplate.arg("Download size"));
  117. result += replaceIfNotEmpty(mod.getValue("author"), lineTemplate.arg("Authors"));
  118. result += replaceIfNotEmpty(mod.getValue("contact"), urlTemplate.arg("Home"));
  119. result += replaceIfNotEmpty(mod.getValue("depends"), lineTemplate.arg("Required mods"));
  120. result += replaceIfNotEmpty(mod.getValue("conflicts"), lineTemplate.arg("Conflicting mods"));
  121. result += replaceIfNotEmpty(mod.getValue("description"), textTemplate.arg("Description"));
  122. result += "<p></p>"; // to get some empty space
  123. QString unknownDeps = "This mod can not be installed or enabled because following dependencies are not present";
  124. QString blockingMods = "This mod can not be enabled because following mods are incompatible with this mod";
  125. QString hasActiveDependentMods = "This mod can not be disabled because it is required to run following mods";
  126. QString hasDependentMods = "This mod can not be uninstalled or updated because it is required to run following mods";
  127. QString notes;
  128. notes += replaceIfNotEmpty(findInvalidDependencies(mod.getName()), noteTemplate.arg(unknownDeps));
  129. notes += replaceIfNotEmpty(findBlockingMods(mod.getName()), noteTemplate.arg(blockingMods));
  130. if (mod.isEnabled())
  131. notes += replaceIfNotEmpty(findDependentMods(mod.getName(), true), noteTemplate.arg(hasActiveDependentMods));
  132. if (mod.isInstalled())
  133. notes += replaceIfNotEmpty(findDependentMods(mod.getName(), false), noteTemplate.arg(hasDependentMods));
  134. if (notes.size())
  135. result += textTemplate.arg("Notes").arg(notes);
  136. result += "</body></html>";
  137. return result;
  138. }
  139. void CModListView::enableModInfo()
  140. {
  141. showModInfo();
  142. ui->hideModInfoButton->setEnabled(true);
  143. }
  144. void CModListView::disableModInfo()
  145. {
  146. hideModInfo();
  147. ui->hideModInfoButton->setEnabled(false);
  148. }
  149. void CModListView::selectMod(int index)
  150. {
  151. if (index < 0)
  152. {
  153. disableModInfo();
  154. }
  155. else
  156. {
  157. enableModInfo();
  158. auto mod = modModel->getMod(modModel->modIndexToName(index));
  159. ui->textBrowser->setHtml(genModInfoText(mod));
  160. bool hasInvalidDeps = !findInvalidDependencies(modModel->modIndexToName(index)).empty();
  161. bool hasBlockingMods = !findBlockingMods(modModel->modIndexToName(index)).empty();
  162. bool hasDependentMods = !findDependentMods(modModel->modIndexToName(index), true).empty();
  163. ui->disableButton->setVisible(mod.isEnabled());
  164. ui->enableButton->setVisible(mod.isDisabled());
  165. ui->installButton->setVisible(mod.isAvailable());
  166. ui->uninstallButton->setVisible(mod.isInstalled());
  167. ui->updateButton->setVisible(mod.isUpdateable());
  168. // Block buttons if action is not allowed at this time
  169. // TODO: automate handling of some of these cases instead of forcing player
  170. // to resolve all conflicts manually.
  171. ui->disableButton->setEnabled(!hasDependentMods);
  172. ui->enableButton->setEnabled(!hasBlockingMods && !hasInvalidDeps);
  173. ui->installButton->setEnabled(!hasInvalidDeps);
  174. ui->uninstallButton->setEnabled(!hasDependentMods);
  175. ui->updateButton->setEnabled(!hasInvalidDeps && !hasDependentMods);
  176. }
  177. }
  178. void CModListView::keyPressEvent(QKeyEvent * event)
  179. {
  180. if (event->key() == Qt::Key_Escape && ui->modInfoWidget->isVisible() )
  181. {
  182. ui->modInfoWidget->hide();
  183. }
  184. else
  185. {
  186. return QWidget::keyPressEvent(event);
  187. }
  188. }
  189. void CModListView::modSelected(const QModelIndex & current, const QModelIndex & )
  190. {
  191. selectMod(filterModel->mapToSource(current).row());
  192. }
  193. void CModListView::on_hideModInfoButton_clicked()
  194. {
  195. if (ui->modInfoWidget->isVisible())
  196. hideModInfo();
  197. else
  198. showModInfo();
  199. }
  200. void CModListView::on_allModsView_doubleClicked(const QModelIndex &index)
  201. {
  202. showModInfo();
  203. selectMod(filterModel->mapToSource(index).row());
  204. }
  205. void CModListView::on_lineEdit_textChanged(const QString &arg1)
  206. {
  207. QRegExp regExp(arg1, Qt::CaseInsensitive, QRegExp::Wildcard);
  208. filterModel->setFilterRegExp(regExp);
  209. }
  210. void CModListView::on_comboBox_currentIndexChanged(int index)
  211. {
  212. switch (index)
  213. {
  214. break; case 0: filterModel->setTypeFilter(ModStatus::MASK_NONE, ModStatus::MASK_NONE);
  215. break; case 1: filterModel->setTypeFilter(ModStatus::MASK_NONE, ModStatus::INSTALLED);
  216. break; case 2: filterModel->setTypeFilter(ModStatus::INSTALLED, ModStatus::INSTALLED);
  217. break; case 3: filterModel->setTypeFilter(ModStatus::UPDATEABLE, ModStatus::UPDATEABLE);
  218. break; case 4: filterModel->setTypeFilter(ModStatus::ENABLED | ModStatus::INSTALLED, ModStatus::ENABLED | ModStatus::INSTALLED);
  219. break; case 5: filterModel->setTypeFilter(ModStatus::INSTALLED, ModStatus::ENABLED | ModStatus::INSTALLED);
  220. }
  221. }
  222. QStringList CModListView::findInvalidDependencies(QString mod)
  223. {
  224. QStringList ret;
  225. for (QString requrement : modModel->getRequirements(mod))
  226. {
  227. if (!modModel->hasMod(requrement))
  228. ret += requrement;
  229. }
  230. return ret;
  231. }
  232. QStringList CModListView::findBlockingMods(QString mod)
  233. {
  234. QStringList ret;
  235. auto required = modModel->getRequirements(mod);
  236. for (QString name : modModel->getModList())
  237. {
  238. auto mod = modModel->getMod(name);
  239. if (mod.isEnabled())
  240. {
  241. // one of enabled mods have requirement (or this mod) marked as conflict
  242. for (auto conflict : mod.getValue("conflicts").toStringList())
  243. if (required.contains(conflict))
  244. ret.push_back(name);
  245. }
  246. }
  247. return ret;
  248. }
  249. QStringList CModListView::findDependentMods(QString mod, bool excludeDisabled)
  250. {
  251. QStringList ret;
  252. for (QString modName : modModel->getModList())
  253. {
  254. auto current = modModel->getMod(modName);
  255. if (!current.isInstalled())
  256. continue;
  257. if (current.getValue("depends").toStringList().contains(mod) &&
  258. !(current.isDisabled() && excludeDisabled))
  259. ret += modName;
  260. }
  261. return ret;
  262. }
  263. void CModListView::on_enableButton_clicked()
  264. {
  265. QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
  266. assert(findBlockingMods(modName).empty());
  267. assert(findInvalidDependencies(modName).empty());
  268. for (auto & name : modModel->getRequirements(modName))
  269. if (modModel->getMod(name).isDisabled())
  270. manager->enableMod(name);
  271. checkManagerErrors();
  272. }
  273. void CModListView::on_disableButton_clicked()
  274. {
  275. QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
  276. if (modModel->hasMod(modName) &&
  277. modModel->getMod(modName).isEnabled())
  278. manager->disableMod(modName);
  279. checkManagerErrors();
  280. }
  281. void CModListView::on_updateButton_clicked()
  282. {
  283. QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
  284. assert(findInvalidDependencies(modName).empty());
  285. for (auto & name : modModel->getRequirements(modName))
  286. {
  287. auto mod = modModel->getMod(name);
  288. // update required mod, install missing (can be new dependency)
  289. if (mod.isUpdateable() || !mod.isInstalled())
  290. downloadFile(name + ".zip", mod.getValue("download").toString(), "mods");
  291. }
  292. }
  293. void CModListView::on_uninstallButton_clicked()
  294. {
  295. QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
  296. // NOTE: perhaps add "manually installed" flag and uninstall those dependencies that don't have it?
  297. if (modModel->hasMod(modName) &&
  298. modModel->getMod(modName).isInstalled())
  299. {
  300. if (modModel->getMod(modName).isEnabled())
  301. manager->disableMod(modName);
  302. manager->uninstallMod(modName);
  303. }
  304. checkManagerErrors();
  305. }
  306. void CModListView::on_installButton_clicked()
  307. {
  308. QString modName = modModel->modIndexToName(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
  309. assert(findInvalidDependencies(modName).empty());
  310. for (auto & name : modModel->getRequirements(modName))
  311. {
  312. auto mod = modModel->getMod(name);
  313. if (!mod.isInstalled())
  314. downloadFile(name + ".zip", mod.getValue("download").toString(), "mods");
  315. }
  316. }
  317. void CModListView::downloadFile(QString file, QString url, QString description)
  318. {
  319. if (!dlManager)
  320. {
  321. dlManager = new CDownloadManager();
  322. ui->progressWidget->setVisible(true);
  323. connect(dlManager, SIGNAL(downloadProgress(qint64,qint64)),
  324. this, SLOT(downloadProgress(qint64,qint64)));
  325. connect(dlManager, SIGNAL(finished(QStringList,QStringList,QStringList)),
  326. this, SLOT(downloadFinished(QStringList,QStringList,QStringList)));
  327. QString progressBarFormat = "Downloading %s%. %p% (%v KB out of %m KB) finished";
  328. progressBarFormat.replace("%s%", description);
  329. ui->progressBar->setFormat(progressBarFormat);
  330. }
  331. dlManager->downloadFile(QUrl(url), file);
  332. }
  333. void CModListView::downloadProgress(qint64 current, qint64 max)
  334. {
  335. // display progress, in kilobytes
  336. ui->progressBar->setValue(current/1024);
  337. ui->progressBar->setMaximum(max/1024);
  338. }
  339. void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFiles, QStringList errors)
  340. {
  341. QString title = "Download failed";
  342. QString firstLine = "Unable to download all files.\n\nEncountered errors:\n\n";
  343. QString lastLine = "\n\nInstall successfully downloaded?";
  344. // if all files were d/loaded there should be no errors. And on failure there must be an error
  345. assert(failedFiles.empty() == errors.empty());
  346. if (savedFiles.empty())
  347. {
  348. // no successfully downloaded mods
  349. QMessageBox::warning(this, title, firstLine + errors.join("\n"), QMessageBox::Ok, QMessageBox::Ok );
  350. }
  351. else if (!failedFiles.empty())
  352. {
  353. // some mods were not downloaded
  354. int result = QMessageBox::warning (this, title, firstLine + errors.join("\n") + lastLine,
  355. QMessageBox::Yes | QMessageBox::No, QMessageBox::No );
  356. if (result == QMessageBox::Yes)
  357. installFiles(savedFiles);
  358. }
  359. else
  360. {
  361. // everything OK
  362. installFiles(savedFiles);
  363. }
  364. // remove progress bar after some delay so user can see that download was complete and not interrupted.
  365. QTimer::singleShot(1000, this, SLOT(hideProgressBar()));
  366. dlManager->deleteLater();
  367. dlManager = nullptr;
  368. }
  369. void CModListView::hideProgressBar()
  370. {
  371. if (dlManager == nullptr) // it was not recreated meanwhile
  372. {
  373. ui->progressWidget->setVisible(false);
  374. ui->progressBar->setMaximum(0);
  375. ui->progressBar->setValue(0);
  376. }
  377. }
  378. void CModListView::installFiles(QStringList files)
  379. {
  380. QStringList mods;
  381. // TODO: some better way to separate zip's with mods and downloaded repository files
  382. for (QString filename : files)
  383. {
  384. if (filename.contains(".zip"))
  385. mods.push_back(filename);
  386. if (filename.contains(".json"))
  387. manager->loadRepository(filename);
  388. }
  389. if (!mods.empty())
  390. installMods(mods);
  391. }
  392. void CModListView::installMods(QStringList archives)
  393. {
  394. QStringList modNames;
  395. for (QString archive : archives)
  396. {
  397. // get basename out of full file name
  398. // remove path remove extension
  399. QString modName = archive.section('/', -1, -1).section('.', 0, 0);
  400. modNames.push_back(modName);
  401. }
  402. QStringList modsToEnable;
  403. // disable mod(s), to properly recalculate dependencies, if changed
  404. for (QString mod : boost::adaptors::reverse(modNames))
  405. {
  406. CModEntry entry = modModel->getMod(mod);
  407. if (entry.isInstalled())
  408. {
  409. // enable mod if installed and enabled
  410. if (entry.isEnabled())
  411. modsToEnable.push_back(mod);
  412. }
  413. else
  414. {
  415. // enable mod if m
  416. if (settings["launcher"]["enableInstalledMods"].Bool())
  417. modsToEnable.push_back(mod);
  418. }
  419. }
  420. // uninstall old version of mod, if installed
  421. for (QString mod : boost::adaptors::reverse(modNames))
  422. {
  423. if (modModel->getMod(mod).isInstalled())
  424. manager->uninstallMod(mod);
  425. }
  426. for (int i=0; i<modNames.size(); i++)
  427. manager->installMod(modNames[i], archives[i]);
  428. for (QString mod : modsToEnable)
  429. manager->enableMod(mod);
  430. for (QString archive : archives)
  431. QFile::remove(archive);
  432. checkManagerErrors();
  433. }
  434. void CModListView::on_pushButton_clicked()
  435. {
  436. delete dlManager;
  437. dlManager = nullptr;
  438. hideProgressBar();
  439. }
  440. void CModListView::modelReset()
  441. {
  442. //selectMod(filterModel->mapToSource(ui->allModsView->currentIndex()).row());
  443. selectMod(filterModel->rowCount() > 0 ? 0 : -1);
  444. }
  445. void CModListView::checkManagerErrors()
  446. {
  447. QString errors = manager->getErrors().join('\n');
  448. if (errors.size() != 0)
  449. {
  450. QString title = "Operation failed";
  451. QString description = "Encountered errors:\n" + errors;
  452. QMessageBox::warning(this, title, description, QMessageBox::Ok, QMessageBox::Ok );
  453. }
  454. }