cmodlistview_moc.cpp 17 KB

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