diff --git a/CMakeLists.txt b/CMakeLists.txt index c026de4d..7ac13fa0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,6 +144,8 @@ SET(MultiMC_CHANLIST_URL "" CACHE STRING "URL for the channel list.") # Updater enabled? SET(MultiMC_UPDATER false CACHE BOOL "Whether or not the update system is enabled. If this is enabled, you must also set MultiMC_CHANLIST_URL and MultiMC_VERSION_CHANNEL in order for it to work properly.") +# Notification URL +SET(MultiMC_NOTIFICATION_URL "" CACHE STRING "URL for checking for notifications.") # Build a version string to display in the configure logs. SET(MultiMC_VERSION_STRING "${MultiMC_VERSION_MAJOR}.${MultiMC_VERSION_MINOR}") @@ -337,6 +339,8 @@ logic/updater/UpdateChecker.h logic/updater/UpdateChecker.cpp logic/updater/DownloadUpdateTask.h logic/updater/DownloadUpdateTask.cpp +logic/updater/NotificationChecker.h +logic/updater/NotificationChecker.cpp # News System logic/news/NewsChecker.h diff --git a/MultiMC.cpp b/MultiMC.cpp index 4e06f558..b105fd66 100644 --- a/MultiMC.cpp +++ b/MultiMC.cpp @@ -26,6 +26,7 @@ #include "logic/JavaUtils.h" #include "logic/updater/UpdateChecker.h" +#include "logic/updater/NotificationChecker.h" #include "pathutils.h" #include "cmdutils.h" @@ -47,7 +48,7 @@ using namespace Util::Commandline; -MultiMC::MultiMC(int &argc, char **argv, const QString &root) +MultiMC::MultiMC(int &argc, char **argv, const QString &data_dir_override) : QApplication(argc, argv), m_version{VERSION_MAJOR, VERSION_MINOR, VERSION_BUILD, VERSION_CHANNEL, VERSION_BUILD_TYPE} { @@ -60,10 +61,6 @@ MultiMC::MultiMC(int &argc, char **argv, const QString &root) // Don't quit on hiding the last window this->setQuitOnLastWindowClosed(false); - // Print app header - std::cout << "MultiMC 5" << std::endl; - std::cout << "(c) 2013 MultiMC Contributors" << std::endl << std::endl; - // Commandline parsing QHash args; { @@ -82,16 +79,6 @@ MultiMC::MultiMC(int &argc, char **argv, const QString &root) parser.addShortOpt("dir", 'd'); parser.addDocumentation("dir", "use the supplied directory as MultiMC root instead of " "the binary location (use '.' for current)"); - // --update - parser.addOption("update"); - parser.addShortOpt("update", 'u'); - parser.addDocumentation("update", "replaces the given file with the running executable", - ""); - // --quietupdate - parser.addSwitch("quietupdate"); - parser.addShortOpt("quietupdate", 'U'); - parser.addDocumentation("quietupdate", - "doesn't restart MultiMC after installing updates"); // WARNING: disabled until further notice /* // --launch @@ -129,41 +116,76 @@ MultiMC::MultiMC(int &argc, char **argv, const QString &root) m_status = MultiMC::Succeeded; return; } - - // update - // Note: cwd is always the current executable path! - if (!args["update"].isNull()) - { - std::cout << "Performing MultiMC update: " << qPrintable(args["update"].toString()) - << std::endl; - QString cwd = QDir::currentPath(); - QDir::setCurrent(applicationDirPath()); - QFile file(applicationFilePath()); - file.copy(args["update"].toString()); - if (args["quietupdate"].toBool()) - { - m_status = MultiMC::Succeeded; - return; - } - QDir::setCurrent(cwd); - } + } + origcwdPath = QDir::currentPath(); + binPath = applicationDirPath(); + QString adjustedBy; + // change directory + QString dirParam = args["dir"].toString(); + if (!data_dir_override.isEmpty()) + { + // the override is used for tests (although dirparam would be enough...) + // TODO: remove the need for this extra logic + adjustedBy += "Test override " + data_dir_override; + dataPath = data_dir_override; + } + else if (!dirParam.isEmpty()) + { + // the dir param. it makes multimc data path point to whatever the user specified + // on command line + adjustedBy += "Command line " + dirParam; + dataPath = dirParam; + } + if(!ensureFolderPathExists(dataPath) || !QDir::setCurrent(dataPath)) + { + // BAD STUFF. WHAT DO? + initLogger(); + QLOG_ERROR() << "Failed to set work path. Will exit. NOW."; + m_status = MultiMC::Failed; + return; } - // change directory - QDir::setCurrent( - args["dir"].toString().isEmpty() - ? (root.isEmpty() ? QDir::currentPath() : QDir::current().absoluteFilePath(root)) - : args["dir"].toString()); + { + #ifdef Q_OS_LINUX + QDir foo(PathCombine(binPath, "..")); + rootPath = foo.absolutePath(); + #elif defined(Q_OS_WIN32) + QDir foo(PathCombine(binPath, "..")); + rootPath = foo.absolutePath(); + #elif defined(Q_OS_MAC) + QDir foo(PathCombine(binPath, "../..")); + rootPath = foo.absolutePath(); + #endif + } // init the logger initLogger(); + QLOG_INFO() << "MultiMC 5, (c) 2013 MultiMC Contributors"; + QLOG_INFO() << "Version : " << VERSION_STR; + QLOG_INFO() << "Git commit : " << GIT_COMMIT; + if (adjustedBy.size()) + { + QLOG_INFO() << "Work dir before adjustment : " << origcwdPath; + QLOG_INFO() << "Work dir after adjustment : " << QDir::currentPath(); + QLOG_INFO() << "Adjusted by : " << adjustedBy; + } + else + { + QLOG_INFO() << "Work dir : " << QDir::currentPath(); + } + QLOG_INFO() << "Binary path : " << binPath; + QLOG_INFO() << "Application root path : " << rootPath; + // load settings initGlobalSettings(); // initialize the updater m_updateChecker.reset(new UpdateChecker()); + // initialize the notification checker + m_notificationChecker.reset(new NotificationChecker()); + // initialize the news checker m_newsChecker.reset(new NewsChecker(NEWS_RSS_URL)); @@ -319,7 +341,7 @@ void MultiMC::initLogger() QsLogging::Logger &logger = QsLogging::Logger::instance(); logger.setLoggingLevel(QsLogging::TraceLevel); m_fileDestination = QsLogging::DestinationFactory::MakeFileDestination(logBase.arg(0)); - m_debugDestination = QsLogging::DestinationFactory::MakeQDebugDestination(); + m_debugDestination = QsLogging::DestinationFactory::MakeDebugOutputDestination(); logger.addDestination(m_fileDestination.get()); logger.addDestination(m_debugDestination.get()); // log all the things @@ -332,6 +354,7 @@ void MultiMC::initGlobalSettings() // Updates m_settings->registerSetting("UseDevBuilds", false); m_settings->registerSetting("AutoUpdate", true); + m_settings->registerSetting("ShownNotifications", QString()); // FTB m_settings->registerSetting("TrackFTBInstances", false); diff --git a/MultiMC.h b/MultiMC.h index 9ad276ff..3a25aa5e 100644 --- a/MultiMC.h +++ b/MultiMC.h @@ -17,6 +17,7 @@ class QNetworkAccessManager; class ForgeVersionList; class JavaVersionList; class UpdateChecker; +class NotificationChecker; class NewsChecker; #if defined(MMC) @@ -90,6 +91,11 @@ public: return m_updateChecker; } + std::shared_ptr notificationChecker() + { + return m_notificationChecker; + } + std::shared_ptr newsChecker() { return m_newsChecker; @@ -125,6 +131,29 @@ public: */ bool openJsonEditor(const QString &filename); + /// this is the root of the 'installation'. Used for automatic updates + const QString &root() + { + return rootPath; + } + /// this is the where the binary files reside + const QString &bin() + { + return binPath; + } + /// this is the work/data path. All user data is here. + const QString &data() + { + return dataPath; + } + /** + * this is the original work path before it was changed by the adjustment mechanism + */ + const QString &origcwd() + { + return origcwdPath; + } + private: void initLogger(); @@ -143,6 +172,7 @@ private: std::shared_ptr m_settings; std::shared_ptr m_instances; std::shared_ptr m_updateChecker; + std::shared_ptr m_notificationChecker; std::shared_ptr m_newsChecker; std::shared_ptr m_accounts; std::shared_ptr m_icons; @@ -157,6 +187,11 @@ private: QString m_updateOnExitPath; + QString rootPath; + QString binPath; + QString dataPath; + QString origcwdPath; + Status m_status = MultiMC::Failed; MultiMCVersion m_version; }; diff --git a/config.h.in b/config.h.in index aa604056..9681b825 100644 --- a/config.h.in +++ b/config.h.in @@ -10,6 +10,12 @@ // URL for the updater's channel #define CHANLIST_URL "@MultiMC_CHANLIST_URL@" +// URL for notifications +#define NOTIFICATION_URL "@MultiMC_NOTIFICATION_URL@" + +// Used for matching notifications +#define FULL_VERSION_STR "@MultiMC_VERSION_MAJOR@.@MultiMC_VERSION_MINOR@.@MultiMC_VERSION_BUILD@" + // The commit hash of this build #define GIT_COMMIT "@MultiMC_GIT_COMMIT@" diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp index 75ebefe4..d1119028 100644 --- a/gui/MainWindow.cpp +++ b/gui/MainWindow.cpp @@ -92,6 +92,7 @@ #include "logic/assets/AssetsUtils.h" #include "logic/assets/AssetsMigrateTask.h" #include +#include #include MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWindow) @@ -283,6 +284,9 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi // if automatic update checks are allowed, start one. if (MMC->settings()->get("AutoUpdate").toBool()) on_actionCheckUpdate_triggered(); + + connect(MMC->notificationChecker().get(), &NotificationChecker::notificationCheckFinished, + this, &MainWindow::notificationsChanged); } const QString currentInstanceId = MMC->settings()->get("SelectedInstance").toString(); @@ -522,6 +526,63 @@ void MainWindow::updateAvailable(QString repo, QString versionName, int versionI } } +QList stringToIntList(const QString &string) +{ + QStringList split = string.split(',', QString::SkipEmptyParts); + QList out; + for (int i = 0; i < split.size(); ++i) + { + out.append(split.at(i).toInt()); + } + return out; +} +QString intListToString(const QList &list) +{ + QStringList slist; + for (int i = 0; i < list.size(); ++i) + { + slist.append(QString::number(list.at(i))); + } + return slist.join(','); +} +void MainWindow::notificationsChanged() +{ + QList entries = + MMC->notificationChecker()->notificationEntries(); + QList shownNotifications = + stringToIntList(MMC->settings()->get("ShownNotifications").toString()); + for (auto it = entries.begin(); it != entries.end(); ++it) + { + NotificationChecker::NotificationEntry entry = *it; + if (!shownNotifications.contains(entry.id) && entry.applies()) + { + QMessageBox::Icon icon; + switch (entry.type) + { + case NotificationChecker::NotificationEntry::Critical: + icon = QMessageBox::Critical; + break; + case NotificationChecker::NotificationEntry::Warning: + icon = QMessageBox::Warning; + break; + case NotificationChecker::NotificationEntry::Information: + icon = QMessageBox::Information; + break; + } + + QMessageBox box(icon, tr("Notification"), entry.message, QMessageBox::Close, this); + QPushButton *dontShowAgainButton = box.addButton(tr("Don't show again"), QMessageBox::AcceptRole); + box.setDefaultButton(QMessageBox::Close); + box.exec(); + if (box.clickedButton() == dontShowAgainButton) + { + shownNotifications.append(entry.id); + } + } + } + MMC->settings()->set("ShownNotifications", intListToString(shownNotifications)); +} + void MainWindow::downloadUpdates(QString repo, int versionId, bool installOnExit) { QLOG_INFO() << "Downloading updates."; diff --git a/gui/MainWindow.h b/gui/MainWindow.h index ca11b380..af2f1dca 100644 --- a/gui/MainWindow.h +++ b/gui/MainWindow.h @@ -159,6 +159,8 @@ slots: void updateAvailable(QString repo, QString versionName, int versionId); + void notificationsChanged(); + void activeAccountChanged(); void changeActiveAccount(); diff --git a/gui/widgets/ModListView.cpp b/gui/widgets/ModListView.cpp index 9d5950c3..358e6331 100644 --- a/gui/widgets/ModListView.cpp +++ b/gui/widgets/ModListView.cpp @@ -44,9 +44,19 @@ void ModListView::setModel ( QAbstractItemModel* model ) QTreeView::setModel ( model ); auto head = header(); head->setStretchLastSection(false); - head->setSectionResizeMode(0, QHeaderView::ResizeToContents); - head->setSectionResizeMode(1, QHeaderView::Stretch); - for(int i = 2; i < head->count(); i++) - head->setSectionResizeMode(i, QHeaderView::ResizeToContents); - dropIndicatorPosition(); + // HACK: this is true for the checkbox column of mod lists + auto string = model->headerData(0,head->orientation()).toString(); + if(!string.size()) + { + head->setSectionResizeMode(0, QHeaderView::ResizeToContents); + head->setSectionResizeMode(1, QHeaderView::Stretch); + for(int i = 2; i < head->count(); i++) + head->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + else + { + head->setSectionResizeMode(0, QHeaderView::Stretch); + for(int i = 1; i < head->count(); i++) + head->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } } diff --git a/logic/ModList.cpp b/logic/ModList.cpp index fd41bcf7..499623bf 100644 --- a/logic/ModList.cpp +++ b/logic/ModList.cpp @@ -416,7 +416,7 @@ QVariant ModList::data(const QModelIndex &index, int role) const switch (index.column()) { case ActiveColumn: - return mods[row].enabled(); + return mods[row].enabled() ? Qt::Checked: Qt::Unchecked; default: return QVariant(); } diff --git a/logic/lists/InstanceList.cpp b/logic/lists/InstanceList.cpp index 48a2865a..bfd183d9 100644 --- a/logic/lists/InstanceList.cpp +++ b/logic/lists/InstanceList.cpp @@ -307,9 +307,10 @@ void InstanceList::loadForgeInstances(QMap groupMap) QLOG_INFO() << "The FTB directory specified does not exist. Please check your settings"; return; } - dir.cd("ModPacks"); - QFile f(dir.absoluteFilePath("modpacks.xml")); + auto fpath = dir.absoluteFilePath("modpacks.xml"); + QFile f(fpath); + QLOG_INFO() << "Discovering FTB instances -- " << fpath; if (!f.open(QFile::ReadOnly)) return; @@ -326,6 +327,9 @@ void InstanceList::loadForgeInstances(QMap groupMap) QXmlStreamAttributes attrs = reader.attributes(); FTBRecord record; record.dir = attrs.value("dir").toString(); + QDir test(dataDir.absoluteFilePath(record.dir)); + if(!test.exists()) + continue; record.name = attrs.value("name").toString(); record.logo = attrs.value("logo").toString(); record.mcVersion = attrs.value("mcVersion").toString(); @@ -343,11 +347,17 @@ void InstanceList::loadForgeInstances(QMap groupMap) } } f.close(); - + if(!records.size()) + { + QLOG_INFO() << "No FTB instances to load."; + return; + } + QLOG_INFO() << "Loading FTB instances! -- got " << records.size(); // process the records we acquired. for (auto record : records) { auto instanceDir = dataDir.absoluteFilePath(record.dir); + QLOG_INFO() << "Loading FTB instance from " << instanceDir; auto templateDir = dir.absoluteFilePath(record.dir); if (!QFileInfo(instanceDir).exists()) { @@ -361,6 +371,7 @@ void InstanceList::loadForgeInstances(QMap groupMap) if (!QFileInfo(PathCombine(instanceDir, "instance.cfg")).exists()) { + QLOG_INFO() << "Converting " << record.name << " as new."; BaseInstance *instPtr = NULL; auto &factory = InstanceFactory::get(); auto version = MMC->minecraftlist()->findVersion(record.mcVersion); @@ -386,6 +397,7 @@ void InstanceList::loadForgeInstances(QMap groupMap) } else { + QLOG_INFO() << "Loading existing " << record.name; BaseInstance *instPtr = NULL; auto error = InstanceFactory::get().loadInstance(instPtr, instanceDir); if (!instPtr || error != InstanceFactory::NoCreateError) @@ -419,7 +431,7 @@ InstanceList::InstListError InstanceList::loadList() QString subDir = iter.next(); if (!QFileInfo(PathCombine(subDir, "instance.cfg")).exists()) continue; - + QLOG_INFO() << "Loading MultiMC instance from " << subDir; BaseInstance *instPtr = NULL; auto error = InstanceFactory::get().loadInstance(instPtr, subDir); continueProcessInstance(instPtr, error, subDir, groupMap); @@ -534,7 +546,7 @@ void InstanceList::continueProcessInstance(BaseInstance *instPtr, const int erro { instPtr->setGroupInitial((*iter)); } - QLOG_INFO() << "Loaded instance " << instPtr->name(); + QLOG_INFO() << "Loaded instance " << instPtr->name() << " from " << dir.absolutePath(); instPtr->setParent(this); m_instances.append(std::shared_ptr(instPtr)); connect(instPtr, SIGNAL(propertiesChanged(BaseInstance *)), this, diff --git a/logic/updater/DownloadUpdateTask.cpp b/logic/updater/DownloadUpdateTask.cpp index 6e0a92f0..029286dd 100644 --- a/logic/updater/DownloadUpdateTask.cpp +++ b/logic/updater/DownloadUpdateTask.cpp @@ -404,11 +404,10 @@ DownloadUpdateTask::processFileLists(NetJob *job, { auto cache_entry = MMC->metacache()->resolveEntry("root", entry.path); QLOG_DEBUG() << "Updater will be in " << cache_entry->getFullPath(); - if(cache_entry->stale) - { - auto download = CacheDownload::make(QUrl(source.url), cache_entry); - job->addNetAction(download); - } + // force check. + cache_entry->stale = true; + auto download = CacheDownload::make(QUrl(source.url), cache_entry); + job->addNetAction(download); } else { diff --git a/logic/updater/NotificationChecker.cpp b/logic/updater/NotificationChecker.cpp new file mode 100644 index 00000000..40367eac --- /dev/null +++ b/logic/updater/NotificationChecker.cpp @@ -0,0 +1,120 @@ +#include "NotificationChecker.h" + +#include +#include +#include + +#include "MultiMC.h" +#include "logic/net/CacheDownload.h" +#include "config.h" + +NotificationChecker::NotificationChecker(QObject *parent) + : QObject(parent), m_notificationsUrl(QUrl(NOTIFICATION_URL)) +{ + // this will call checkForNotifications once the event loop is running + QMetaObject::invokeMethod(this, "checkForNotifications", Qt::QueuedConnection); +} + +QUrl NotificationChecker::notificationsUrl() const +{ + return m_notificationsUrl; +} +void NotificationChecker::setNotificationsUrl(const QUrl ¬ificationsUrl) +{ + m_notificationsUrl = notificationsUrl; +} + +QList NotificationChecker::notificationEntries() const +{ + return m_entries; +} + +void NotificationChecker::checkForNotifications() +{ + if (!m_notificationsUrl.isValid()) + { + QLOG_ERROR() << "Failed to check for notifications. No notifications URL set." + << "If you'd like to use MultiMC's notification system, please pass the " + "URL to CMake at compile time."; + return; + } + if (m_checkJob) + { + return; + } + m_checkJob.reset(new NetJob("Checking for notifications")); + auto entry = MMC->metacache()->resolveEntry("root", "notifications.json"); + entry->stale = true; + m_checkJob->addNetAction(m_download = CacheDownload::make(m_notificationsUrl, entry)); + connect(m_download.get(), &CacheDownload::succeeded, this, + &NotificationChecker::downloadSucceeded); + m_checkJob->start(); +} + +void NotificationChecker::downloadSucceeded(int) +{ + m_entries.clear(); + + QFile file(m_download->m_output_file.fileName()); + if (file.open(QFile::ReadOnly)) + { + QJsonArray root = QJsonDocument::fromJson(file.readAll()).array(); + for (auto it = root.begin(); it != root.end(); ++it) + { + QJsonObject obj = (*it).toObject(); + NotificationEntry entry; + entry.id = obj.value("id").toDouble(); + entry.message = obj.value("message").toString(); + entry.channel = obj.value("channel").toString(); + entry.buildtype = obj.value("buildtype").toString(); + entry.from = obj.value("from").toString(); + entry.to = obj.value("to").toString(); + const QString type = obj.value("type").toString("critical"); + if (type == "critical") + { + entry.type = NotificationEntry::Critical; + } + else if (type == "warning") + { + entry.type = NotificationEntry::Warning; + } + else if (type == "information") + { + entry.type = NotificationEntry::Information; + } + m_entries.append(entry); + } + } + + m_checkJob.reset(); + + emit notificationCheckFinished(); +} + +bool NotificationChecker::NotificationEntry::applies() const +{ + bool channelApplies = channel.isEmpty() || channel == VERSION_CHANNEL; + bool buildtypeApplies = buildtype.isEmpty() || buildtype == VERSION_BUILD_TYPE; + bool fromApplies = + from.isEmpty() || from == FULL_VERSION_STR || !versionLessThan(FULL_VERSION_STR, from); + bool toApplies = + to.isEmpty() || to == FULL_VERSION_STR || !versionLessThan(to, FULL_VERSION_STR); + return channelApplies && buildtypeApplies && fromApplies && toApplies; +} + +bool NotificationChecker::NotificationEntry::versionLessThan(const QString &v1, + const QString &v2) +{ + QStringList l1 = v1.split('.'); + QStringList l2 = v2.split('.'); + while (!l1.isEmpty() && !l2.isEmpty()) + { + int one = l1.isEmpty() ? 0 : l1.takeFirst().toInt(); + int two = l2.isEmpty() ? 0 : l2.takeFirst().toInt(); + if (one != two) + { + return one < two; + } + } + return false; +} diff --git a/logic/updater/NotificationChecker.h b/logic/updater/NotificationChecker.h new file mode 100644 index 00000000..20541757 --- /dev/null +++ b/logic/updater/NotificationChecker.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include "logic/net/NetJob.h" +#include "logic/net/CacheDownload.h" + +class NotificationChecker : public QObject +{ + Q_OBJECT + +public: + explicit NotificationChecker(QObject *parent = 0); + + QUrl notificationsUrl() const; + void setNotificationsUrl(const QUrl ¬ificationsUrl); + + struct NotificationEntry + { + int id; + QString message; + enum + { + Critical, + Warning, + Information + } type; + QString channel; + QString buildtype; + QString from; + QString to; + bool applies() const; + static bool versionLessThan(const QString &v1, const QString &v2); + }; + + QList notificationEntries() const; + +public +slots: + void checkForNotifications(); + +private +slots: + void downloadSucceeded(int); + +signals: + void notificationCheckFinished(); + +private: + QList m_entries; + QUrl m_notificationsUrl; + NetJobPtr m_checkJob; + CacheDownloadPtr m_download; +}; diff --git a/tests/TestUtil.h b/tests/TestUtil.h index fd25d24f..231ce7f6 100644 --- a/tests/TestUtil.h +++ b/tests/TestUtil.h @@ -39,7 +39,7 @@ int main(int argc, char *argv[]) \ { \ char *argv_[] = { argv[0] _MMC_EXTRA_ARGV }; \ int argc_ = 1 + _MMC_EXTRA_ARGC; \ - MultiMC app(argc_, argv_, QDir::temp().absoluteFilePath("MultiMC_Test")); \ + MultiMC app(argc_, argv_/*, QDir::temp().absoluteFilePath("MultiMC_Test")*/); \ app.setAttribute(Qt::AA_Use96Dpi, true); \ TestObject tc; \ return QTest::qExec(&tc, argc, argv); \