diff options
Diffstat (limited to 'src')
76 files changed, 4321 insertions, 2340 deletions
diff --git a/src/ModOrganizer.pro b/src/ModOrganizer.pro new file mode 100644 index 00000000..4624ffec --- /dev/null +++ b/src/ModOrganizer.pro @@ -0,0 +1,27 @@ +TEMPLATE = subdirs
+
+
+SUBDIRS = bsatk \
+ shared \
+ uibase \
+ organizer \
+ hookdll \
+ archive \
+ helper \
+ plugins \
+ proxydll \
+ nxmhandler \
+ BossDummy \
+ pythonRunner \
+ esptk \
+ loot_cli
+
+plugins.depends = pythonRunner
+hookdll.depends = shared
+organizer.depends = shared uibase plugins loot_cli
+
+CONFIG(debug, debug|release) {
+ DESTDIR = outputd
+} else {
+ DESTDIR = output
+}
diff --git a/src/bbcode.cpp b/src/bbcode.cpp index fe7decfd..455f4767 100644 --- a/src/bbcode.cpp +++ b/src/bbcode.cpp @@ -22,6 +22,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QRegExp> #include <map> #include <algorithm> +#include <boost/assign.hpp> namespace BBCode { @@ -43,7 +44,6 @@ public: // extract the tag name m_TagNameExp.indexIn(input, 1, QRegExp::CaretAtOffset); QString tagName = m_TagNameExp.cap(0).toLower(); - //qDebug("tag name %s", tagName.toUtf8().constData()); TagMap::iterator tagIter = m_TagMap.find(tagName); if (tagIter != m_TagMap.end()) { // recognized tag @@ -60,7 +60,21 @@ public: length = closeTagPos + closeTag.length(); QString temp = input.mid(0, length); if (tagIter->second.first.indexIn(temp) == 0) { - return temp.replace(tagIter->second.first, tagIter->second.second); + if (tagIter->second.second.isEmpty()) { + if (tagName == "color") { + QString color = tagIter->second.first.cap(1); + QString content = tagIter->second.first.cap(2); + auto colIter = m_ColorMap.find(color.toLower()); + if (colIter != m_ColorMap.end()) { + color = colIter->second; + } + return temp.replace(tagIter->second.first, QString("<font style=\"color: #%1;\">%2</font>").arg(color, content)); + } else { + qWarning("don't know how to deal with tag %s", qPrintable(tagName)); + } + } else { + return temp.replace(tagIter->second.first, tagIter->second.second); + } } else { // expression doesn't match. either the input string is invalid // or the expression is @@ -96,7 +110,7 @@ private: m_TagMap["size="] = std::make_pair(QRegExp("\\[size=([^\\]]*)\\](.*)\\[/size\\]"), "<font size=\"\\1\">\\2</font>"); m_TagMap["color="] = std::make_pair(QRegExp("\\[color=([^\\]]*)\\](.*)\\[/color\\]"), - "<font style=\"color: #\\1;\">\\2</font>"); + ""); m_TagMap["font="] = std::make_pair(QRegExp("\\[font=([^\\]]*)\\](.*)\\[/font\\]"), "<font face=\\1>\\2</font>"); m_TagMap["center"] = std::make_pair(QRegExp("\\[center\\](.*)\\[/center\\]"), @@ -139,17 +153,18 @@ private: "<a href=\"\\1\">\\1</a>"); m_TagMap["url="] = std::make_pair(QRegExp("\\[url=([^\\]]*)\\](.*)\\[/url\\]"), "<a href=\"\\1\">\\2</a>"); -/* m_TagMap["img"] = std::make_pair(QRegExp("\\[img\\](.*)\\[/img\\]"), - "<img src=\"\\1\"/>"); - m_TagMap["img="] = std::make_pair(QRegExp("\\[img=([^\\]]*)\\](.*)\\[/img\\]"), - "<img src=\"\\2\" align=\"\\1\" />");*/ - m_TagMap["img"] = std::make_pair(QRegExp("\\[img\\](.*)\\[/img\\]"), ""); - m_TagMap["img="] = std::make_pair(QRegExp("\\[img=([^\\]]*)\\](.*)\\[/img\\]"), ""); + m_TagMap["img"] = std::make_pair(QRegExp("\\[img\\](.*)\\[/img\\]"), " "); + m_TagMap["img="] = std::make_pair(QRegExp("\\[img=([^\\]]*)\\](.*)\\[/img\\]"), " "); m_TagMap["email="] = std::make_pair(QRegExp("\\[email=\"?([^\\]]*)\"?\\](.*)\\[/email\\]"), "<a href=\"mailto:\\1\">\\2</a>"); m_TagMap["youtube"] = std::make_pair(QRegExp("\\[youtube\\](.*)\\[/youtube\\]"), "<a href=\"http://www.youtube.com/v/\\1\">http://www.youtube.com/v/\\1</a>"); + m_ColorMap = boost::assign::map_list_of("red", "FF0000")("green", "00FF00")("blue", "0000FF") + ("black", "000000")("gray", "7F7F7F")("white", "FFFFFF") + ("yellow", "FFFF00")("cyan", "00FFFF")("magenta", "FF00FF") + ("brown", "A52A2A")("orange", "FFCC00"); + // make all patterns non-greedy and case-insensitive for (TagMap::iterator iter = m_TagMap.begin(); iter != m_TagMap.end(); ++iter) { iter->second.first.setCaseSensitivity(Qt::CaseInsensitive); @@ -157,10 +172,11 @@ private: } } - private: + QRegExp m_TagNameExp; TagMap m_TagMap; + std::map<QString, QString> m_ColorMap; }; diff --git a/src/browserdialog.cpp b/src/browserdialog.cpp new file mode 100644 index 00000000..a832f1a3 --- /dev/null +++ b/src/browserdialog.cpp @@ -0,0 +1,270 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "browserdialog.h" +#include "ui_browserdialog.h" + +#include "messagedialog.h" +#include "report.h" +#include "json.h" +#include "persistentcookiejar.h" + +#include <gameinfo.h> + +#include <utility.h> +#include <gameinfo.h> +#include <QNetworkCookieJar> +#include <QNetworkCookie> +#include <QMenu> +#include <QInputDialog> +#include <QWebHistory> +#include <QDir> +#include <QWebFrame> +#include <QDesktopWidget> + + + +BrowserDialog::BrowserDialog(QWidget *parent) + : QDialog(parent) + , ui(new Ui::BrowserDialog) + , m_AccessManager(new QNetworkAccessManager) +{ + ui->setupUi(this); + + m_AccessManager->setCookieJar(new PersistentCookieJar( + QDir::fromNativeSeparators(MOBase::ToQString(MOShared::GameInfo::instance().getCacheDir())) + "/cookies.dat", this)); + + Qt::WindowFlags flags = windowFlags() | Qt::WindowMaximizeButtonHint | Qt::WindowMinimizeButtonHint; + Qt::WindowFlags helpFlag = Qt::WindowContextHelpButtonHint; + flags = flags & (~helpFlag); + setWindowFlags(flags); + + m_Tabs = this->findChild<QTabWidget*>("browserTabWidget"); + + connect(m_Tabs, SIGNAL(tabCloseRequested(int)), this, SLOT(tabCloseRequested(int))); +} + + +BrowserDialog::~BrowserDialog() +{ + delete ui; +} + +void BrowserDialog::closeEvent(QCloseEvent *event) +{ +// m_AccessManager->showCookies(); + QDialog::closeEvent(event); +} + +void BrowserDialog::initTab(BrowserView *newView) +{ + newView->page()->setNetworkAccessManager(m_AccessManager); + newView->page()->setForwardUnsupportedContent(true); + + connect(newView, SIGNAL(loadProgress(int)), this, SLOT(progress(int))); + connect(newView, SIGNAL(titleChanged(QString)), this, SLOT(titleChanged(QString))); + connect(newView, SIGNAL(initTab(BrowserView*)), this, SLOT(initTab(BrowserView*))); + connect(newView, SIGNAL(startFind()), this, SLOT(startSearch())); + connect(newView, SIGNAL(urlChanged(QUrl)), this, SLOT(urlChanged(QUrl))); + connect(newView, SIGNAL(openUrlInNewTab(QUrl)), this, SLOT(openInNewTab(QUrl))); + connect(newView->page(), SIGNAL(downloadRequested(QNetworkRequest)), this, SLOT(downloadRequested(QNetworkRequest))); + connect(newView->page(), SIGNAL(unsupportedContent(QNetworkReply*)), this, SLOT(unsupportedContent(QNetworkReply*))); + + ui->backBtn->setEnabled(false); + ui->fwdBtn->setEnabled(false); + m_Tabs->addTab(newView, tr("new")); + newView->settings()->setAttribute(QWebSettings::PluginsEnabled, true); + newView->settings()->setAttribute(QWebSettings::AutoLoadImages, true); +} + + +void BrowserDialog::openInNewTab(const QUrl &url) +{ + BrowserView *newView = new BrowserView(this); + initTab(newView); + newView->setUrl(url); +} + + +BrowserView *BrowserDialog::getCurrentView() +{ + return qobject_cast<BrowserView*>(m_Tabs->currentWidget()); +} + + +void BrowserDialog::urlChanged(const QUrl&) +{ + BrowserView *currentView = getCurrentView(); + if (currentView != NULL) { + ui->backBtn->setEnabled(currentView->history()->canGoBack()); + ui->fwdBtn->setEnabled(currentView->history()->canGoForward()); + } +} + + +void BrowserDialog::openUrl(const QUrl &url) +{ + if (isHidden()) { + show(); + } + openInNewTab(url); +} + + +void BrowserDialog::maximizeWidth() +{ + int viewportWidth = getCurrentView()->page()->viewportSize().width(); + int frameWidth = width() - viewportWidth; + + int contentWidth = getCurrentView()->page()->mainFrame()->contentsSize().width(); + + QDesktopWidget screen; + int currentScreen = screen.screenNumber(this); + int screenWidth = screen.screenGeometry(currentScreen).size().width(); + + int targetWidth = std::min<int>(std::max<int>(viewportWidth, contentWidth) + frameWidth, screenWidth); + this->resize(targetWidth, height()); +} + + +void BrowserDialog::progress(int value) +{ + ui->loadProgress->setValue(value); + if (value == 100) { + maximizeWidth(); + ui->loadProgress->setVisible(false); + } else { + ui->loadProgress->setVisible(true); + } +} + + +void BrowserDialog::titleChanged(const QString &title) +{ + BrowserView *view = qobject_cast<BrowserView*>(sender()); + for (int i = 0; i < m_Tabs->count(); ++i) { + if (m_Tabs->widget(i) == view) { + m_Tabs->setTabText(i, title.mid(0, 15)); + m_Tabs->setTabToolTip(i, title); + } + } +} + + +QString BrowserDialog::guessFileName(const QString &url) +{ + QRegExp uploadsExp(QString("http://.+/uploads/([^/]+)$")); + if (uploadsExp.indexIn(url) != -1) { + // these seem to be premium downloads + return uploadsExp.cap(1); + } + + QRegExp filesExp(QString("http://.+\\?file=([^&]+)")); + if (filesExp.indexIn(url) != -1) { + // a regular manual download? + return filesExp.cap(1); + } + return "unknown"; +} + +void BrowserDialog::unsupportedContent(QNetworkReply *reply) +{ + try { + QWebPage *page = qobject_cast<QWebPage*>(sender()); + if (page == NULL) { + qCritical("sender not a page"); + return; + } + BrowserView *view = qobject_cast<BrowserView*>(page->view()); + if (view == NULL) { + qCritical("no view?"); + return; + } + + qDebug("unsupported: %s - %s", view->url().toString().toUtf8().constData(), reply->url().toString().toUtf8().constData()); + emit requestDownload(view->url(), reply); + } catch (const std::exception &e) { + if (isVisible()) { + MessageDialog::showMessage(tr("failed to start download"), this); + } + qCritical("exception downloading unsupported content: %s", e.what()); + } +} + + +void BrowserDialog::downloadRequested(const QNetworkRequest &request) +{ + qCritical("download request %s ignored", request.url().toString().toUtf8().constData()); +} + + +void BrowserDialog::tabCloseRequested(int index) +{ + if (m_Tabs->count() == 1) { + this->close(); + } else { + m_Tabs->widget(index)->deleteLater(); + m_Tabs->removeTab(index); + } +} + +void BrowserDialog::on_backBtn_clicked() +{ + BrowserView *currentView = getCurrentView(); + if (currentView != NULL) { + currentView->back(); + } +} + +void BrowserDialog::on_fwdBtn_clicked() +{ + BrowserView *currentView = getCurrentView(); + if (currentView != NULL) { + currentView->forward(); + } +} + + +void BrowserDialog::startSearch() +{ + ui->searchEdit->setFocus(); +} + + +void BrowserDialog::on_searchEdit_returnPressed() +{ + BrowserView *currentView = getCurrentView(); + if (currentView != NULL) { + currentView->findText(ui->searchEdit->text(), QWebPage::FindWrapsAroundDocument); + } +} + +void BrowserDialog::on_browserTabWidget_currentChanged(QWidget *current) +{ + BrowserView *currentView = qobject_cast<BrowserView*>(current); + if (currentView != NULL) { + ui->backBtn->setEnabled(currentView->history()->canGoBack()); + ui->fwdBtn->setEnabled(currentView->history()->canGoForward()); + } +} + +void BrowserDialog::on_refreshBtn_clicked() +{ + getCurrentView()->reload(); +} diff --git a/src/browserdialog.h b/src/browserdialog.h new file mode 100644 index 00000000..060d596c --- /dev/null +++ b/src/browserdialog.h @@ -0,0 +1,124 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef BROWSERDIALOG_H +#define BROWSERDIALOG_H + +#include "browserview.h" +#include "tutorialcontrol.h" +#include <QDialog> +#include <QProgressBar> +#include <QNetworkRequest> +#include <QNetworkReply> +#include <QTimer> +#include <QWebView> +#include <QQueue> +#include <QTabWidget> +#include <QAtomicInt> + + +namespace Ui { + class BrowserDialog; +} + + +/** + * @brief a dialog containing a webbrowser that is intended to browse the nexus network + **/ +class BrowserDialog : public QDialog +{ + Q_OBJECT + +public: + + /** + * @brief constructor + * + * @param accessManager the access manager to use for network requests + * @param parent parent widget + **/ + explicit BrowserDialog(QWidget *parent = 0); + ~BrowserDialog(); + + /** + * @brief set the url to open. If automatic login is enabled, the url is opened after login + * + * @param url the url to open + **/ + void openUrl(const QUrl &url); + +signals: + + /** + * @brief emitted when the user starts a download + * @param pageUrl url of the current web site from which the download was started + * @param reply network reply of the started download + */ + void requestDownload(const QUrl &pageUrl, QNetworkReply *reply); + +protected: + + virtual void closeEvent(QCloseEvent *); + +private slots: + + void initTab(BrowserView *newView); + void openInNewTab(const QUrl &url); + + void progress(int value); + + void titleChanged(const QString &title); + void unsupportedContent(QNetworkReply *reply); + void downloadRequested(const QNetworkRequest &request); + + void tabCloseRequested(int index); + + void urlChanged(const QUrl &url); + + void on_backBtn_clicked(); + + void on_fwdBtn_clicked(); + + void on_searchEdit_returnPressed(); + + void startSearch(); + + void on_browserTabWidget_currentChanged(QWidget *arg1); + + void on_refreshBtn_clicked(); + +private: + + QString guessFileName(const QString &url); + + BrowserView *getCurrentView(); + + void maximizeWidth(); + +private: + + Ui::BrowserDialog *ui; + + QNetworkAccessManager *m_AccessManager; + + QTabWidget *m_Tabs; + +}; + +#endif // BROWSERDIALOG_H diff --git a/src/browserdialog.ui b/src/browserdialog.ui new file mode 100644 index 00000000..7d154fbb --- /dev/null +++ b/src/browserdialog.ui @@ -0,0 +1,289 @@ +<?xml version="1.0" encoding="UTF-8"?>
+<ui version="4.0">
+ <class>BrowserDialog</class>
+ <widget class="QDialog" name="BrowserDialog">
+ <property name="geometry">
+ <rect>
+ <x>0</x>
+ <y>0</y>
+ <width>1008</width>
+ <height>750</height>
+ </rect>
+ </property>
+ <property name="windowTitle">
+ <string>Some Page</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="spacing">
+ <number>0</number>
+ </property>
+ <property name="margin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QWidget" name="toolBar" native="true">
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>22</height>
+ </size>
+ </property>
+ <property name="palette">
+ <palette>
+ <active>
+ <colorrole role="WindowText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>226</red>
+ <green>226</green>
+ <blue>226</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Button">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>106</red>
+ <green>106</green>
+ <blue>106</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="ButtonText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>199</red>
+ <green>199</green>
+ <blue>199</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Base">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>255</red>
+ <green>255</green>
+ <blue>255</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Window">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>81</red>
+ <green>81</green>
+ <blue>81</blue>
+ </color>
+ </brush>
+ </colorrole>
+ </active>
+ <inactive>
+ <colorrole role="WindowText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>226</red>
+ <green>226</green>
+ <blue>226</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Button">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>106</red>
+ <green>106</green>
+ <blue>106</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="ButtonText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>199</red>
+ <green>199</green>
+ <blue>199</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Base">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>255</red>
+ <green>255</green>
+ <blue>255</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Window">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>81</red>
+ <green>81</green>
+ <blue>81</blue>
+ </color>
+ </brush>
+ </colorrole>
+ </inactive>
+ <disabled>
+ <colorrole role="WindowText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>120</red>
+ <green>120</green>
+ <blue>120</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Button">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>106</red>
+ <green>106</green>
+ <blue>106</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="ButtonText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>120</red>
+ <green>120</green>
+ <blue>120</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Base">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>81</red>
+ <green>81</green>
+ <blue>81</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="Window">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>81</red>
+ <green>81</green>
+ <blue>81</blue>
+ </color>
+ </brush>
+ </colorrole>
+ </disabled>
+ </palette>
+ </property>
+ <property name="autoFillBackground">
+ <bool>true</bool>
+ </property>
+ <layout class="QHBoxLayout" name="horizontalLayout">
+ <property name="spacing">
+ <number>6</number>
+ </property>
+ <property name="margin">
+ <number>0</number>
+ </property>
+ <item>
+ <widget class="QPushButton" name="backBtn">
+ <property name="autoFillBackground">
+ <bool>false</bool>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/previous</normaloff>:/MO/gui/previous</iconset>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="fwdBtn">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/next</normaloff>:/MO/gui/next</iconset>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="refreshBtn">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/refresh</normaloff>:/MO/gui/refresh</iconset>
+ </property>
+ <property name="autoDefault">
+ <bool>false</bool>
+ </property>
+ <property name="flat">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QLabel" name="label_2">
+ <property name="text">
+ <string>Search</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLineEdit" name="searchEdit"/>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
+ <widget class="QTabWidget" name="browserTabWidget">
+ <property name="contextMenuPolicy">
+ <enum>Qt::DefaultContextMenu</enum>
+ </property>
+ <property name="tabsClosable">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QProgressBar" name="loadProgress">
+ <property name="value">
+ <number>0</number>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <resources>
+ <include location="resources.qrc"/>
+ </resources>
+ <connections/>
+</ui>
diff --git a/src/browserview.cpp b/src/browserview.cpp new file mode 100644 index 00000000..f1b61e66 --- /dev/null +++ b/src/browserview.cpp @@ -0,0 +1,76 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#include "browserview.h" + +#include <QEvent> +#include <QKeyEvent> +#include <QWebFrame> +#include <QWebElement> +#include <QNetworkDiskCache> +#include <QMenu> +#include <Shlwapi.h> +#include "utility.h" + +BrowserView::BrowserView(QWidget *parent) + : QWebView(parent) +{ + installEventFilter(this); + + page()->settings()->setMaximumPagesInCache(10); +} + + +QWebView *BrowserView::createWindow(QWebPage::WebWindowType) +{ + BrowserView *newView = new BrowserView(parentWidget()); + emit initTab(newView); + return newView; +} + + +bool BrowserView::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::ShortcutOverride) { + QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event); + if (keyEvent->matches(QKeySequence::Find)) { + emit startFind(); + } else if (keyEvent->matches(QKeySequence::FindNext)) { + emit findAgain(); + } + } else if (event->type() == QEvent::MouseButtonPress) { + QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event); + if (mouseEvent->button() == Qt::MidButton) { + mouseEvent->ignore(); + return true; + } + } else if (event->type() == QEvent::MouseButtonRelease) { + QMouseEvent *mouseEvent = static_cast<QMouseEvent*>(event); + if (mouseEvent->button() == Qt::MidButton) { + QWebHitTestResult hitTest = page()->frameAt(mouseEvent->pos())->hitTestContent(mouseEvent->pos()); + if (hitTest.linkUrl().isValid()) { + emit openUrlInNewTab(hitTest.linkUrl()); + } + mouseEvent->ignore(); + + return true; + } + } + return QWebView::eventFilter(obj, event); +} diff --git a/src/browserview.h b/src/browserview.h new file mode 100644 index 00000000..3468276b --- /dev/null +++ b/src/browserview.h @@ -0,0 +1,80 @@ +/* +Copyright (C) 2012 Sebastian Herbord. All rights reserved. + +This file is part of Mod Organizer. + +Mod Organizer is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +Mod Organizer is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. +*/ + +#ifndef NEXUSVIEW_H +#define NEXUSVIEW_H + +#include "finddialog.h" + +#include <QWebView> +#include <QWebPage> +#include <QTabWidget> + +/** + * @brief web view used to display a nexus page + **/ +class BrowserView : public QWebView +{ + Q_OBJECT + +public: + + explicit BrowserView(QWidget *parent = 0); + +signals: + + /** + * @brief emitted when the user opens a new window to be displayed in another tab + * + * @param newView the view for the newly opened window + **/ + void initTab(BrowserView *newView); + + /** + * @brief emitted when the user requests a link to be opened in a new tab by middle-clicking + * + * @param url the url to open + */ + void openUrlInNewTab(const QUrl &url); + + /** + * @brief Ctrl-f was clicked. The containing dialog should activate its find-facility + */ + void startFind(); + + /** + * @brief F3 was pressed. The containing dialog should search again + */ + void findAgain(); + +protected: + + virtual QWebView *createWindow(QWebPage::WebWindowType type); + + virtual bool eventFilter(QObject *obj, QEvent *event); + + +private: + + QString m_FindPattern; + bool m_MiddleClick; + +}; + +#endif // NEXUSVIEW_H diff --git a/src/directoryrefresher.cpp b/src/directoryrefresher.cpp index db68601b..21d1f811 100644 --- a/src/directoryrefresher.cpp +++ b/src/directoryrefresher.cpp @@ -46,10 +46,12 @@ DirectoryEntry *DirectoryRefresher::getDirectoryStructure() return result; } -void DirectoryRefresher::setMods(const std::vector<std::tuple<QString, QString, int> > &mods) +void DirectoryRefresher::setMods(const std::vector<std::tuple<QString, QString, int> > &mods + , const std::set<QString> &managedArchives) { QMutexLocker locker(&m_RefreshLock); m_Mods = mods; + m_ManagedArchives = managedArchives; } @@ -74,8 +76,10 @@ void DirectoryRefresher::addModToStructure(DirectoryEntry *directoryStructure, c QDir dir(directory); QFileInfoList bsaFiles = dir.entryInfoList(QStringList("*.bsa"), QDir::Files); foreach (QFileInfo file, bsaFiles) { - directoryStructure->addFromBSA(ToWString(modName), directoryW, - ToWString(QDir::toNativeSeparators(file.absoluteFilePath())), priority); + if (m_ManagedArchives.find(file.fileName()) != m_ManagedArchives.end()) { + directoryStructure->addFromBSA(ToWString(modName), directoryW, + ToWString(QDir::toNativeSeparators(file.absoluteFilePath())), priority); + } } } diff --git a/src/directoryrefresher.h b/src/directoryrefresher.h index a6c8f00b..691448fa 100644 --- a/src/directoryrefresher.h +++ b/src/directoryrefresher.h @@ -20,11 +20,12 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #ifndef DIRECTORYREFRESHER_H #define DIRECTORYREFRESHER_H +#include <directoryentry.h> #include <QObject> -#include <vector> #include <QMutex> +#include <vector> +#include <set> #include <tuple> -#include <directoryentry.h> /** @@ -60,7 +61,7 @@ public: * * @param mods list of the mods to include **/ - void setMods(const std::vector<std::tuple<QString, QString, int> > &mods); + void setMods(const std::vector<std::tuple<QString, QString, int> > &mods, const std::set<QString> &managedArchives); /** * @brief sets up the directory where mods are stored @@ -82,7 +83,7 @@ public: * @param directory * @param priorityDir */ - static void addModToStructure(MOShared::DirectoryEntry *directoryStructure, const QString &modName, int priority, const QString &directory); + void addModToStructure(MOShared::DirectoryEntry *directoryStructure, const QString &modName, int priority, const QString &directory); public slots: @@ -100,6 +101,7 @@ signals: private: std::vector<std::tuple<QString, QString, int> > m_Mods; + std::set<QString> m_ManagedArchives; MOShared::DirectoryEntry *m_DirectoryStructure; QMutex m_RefreshLock; diff --git a/src/downloadlist.cpp b/src/downloadlist.cpp index d280cdb6..e19e2e7b 100644 --- a/src/downloadlist.cpp +++ b/src/downloadlist.cpp @@ -81,8 +81,8 @@ QVariant DownloadList::data(const QModelIndex &index, int role) const if (m_Manager->isInfoIncomplete(index.row())) { text += tr("Information missing, please select \"Query Info\" from the context menu to re-retrieve."); } else { - NexusInfo info = m_Manager->getNexusInfo(index.row()); - text += QString("%1 (ID %2) %3").arg(info.m_ModName).arg(m_Manager->getModID(index.row())).arg(info.m_Version); + const MOBase::ModRepositoryFileInfo *info = m_Manager->getFileInfo(index.row()); + return QString("%1 (ID %2) %3<br><span>%4</span>").arg(info->modName).arg(m_Manager->getModID(index.row())).arg(info->version.canonicalString()).arg(info->description); } return text; } else { diff --git a/src/downloadlistwidget.cpp b/src/downloadlistwidget.cpp index 68dd2adf..953cdacd 100644 --- a/src/downloadlistwidget.cpp +++ b/src/downloadlistwidget.cpp @@ -40,8 +40,13 @@ DownloadListWidget::~DownloadListWidget() } -DownloadListWidgetDelegate::DownloadListWidgetDelegate(DownloadManager *manager, QTreeView *view, QObject *parent) - : QItemDelegate(parent), m_Manager(manager), m_ItemWidget(new DownloadListWidget), m_ContextRow(0), m_View(view) +DownloadListWidgetDelegate::DownloadListWidgetDelegate(DownloadManager *manager, bool metaDisplay, QTreeView *view, QObject *parent) + : QItemDelegate(parent) + , m_Manager(manager) + , m_MetaDisplay(metaDisplay) + , m_ItemWidget(new DownloadListWidget) + , m_ContextRow(0) + , m_View(view) { m_NameLabel = m_ItemWidget->findChild<QLabel*>("nameLabel"); m_SizeLabel = m_ItemWidget->findChild<QLabel*>("sizeLabel"); @@ -95,7 +100,7 @@ void DownloadListWidgetDelegate::paintPendingDownload(int downloadIndex) const void DownloadListWidgetDelegate::paintRegularDownload(int downloadIndex) const { - QString name = m_Manager->getFileName(downloadIndex); + QString name = m_MetaDisplay ? m_Manager->getDisplayName(downloadIndex) : m_Manager->getFileName(downloadIndex); if (name.length() > 53) { name.truncate(50); name.append("..."); diff --git a/src/downloadlistwidget.h b/src/downloadlistwidget.h index 80c4430a..fa7cc845 100644 --- a/src/downloadlistwidget.h +++ b/src/downloadlistwidget.h @@ -54,7 +54,7 @@ class DownloadListWidgetDelegate : public QItemDelegate public: - DownloadListWidgetDelegate(DownloadManager *manager, QTreeView *view, QObject *parent = 0); + DownloadListWidgetDelegate(DownloadManager *manager, bool metaDisplay, QTreeView *view, QObject *parent = 0); ~DownloadListWidgetDelegate(); virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; @@ -105,6 +105,8 @@ private: DownloadListWidget *m_ItemWidget; DownloadManager *m_Manager; + bool m_MetaDisplay; + QLabel *m_NameLabel; QLabel *m_SizeLabel; QProgressBar *m_Progress; diff --git a/src/downloadlistwidgetcompact.cpp b/src/downloadlistwidgetcompact.cpp index e2fbcd24..818d339b 100644 --- a/src/downloadlistwidgetcompact.cpp +++ b/src/downloadlistwidgetcompact.cpp @@ -40,8 +40,12 @@ DownloadListWidgetCompact::~DownloadListWidgetCompact() } -DownloadListWidgetCompactDelegate::DownloadListWidgetCompactDelegate(DownloadManager *manager, QTreeView *view, QObject *parent) - : QItemDelegate(parent), m_Manager(manager), m_ItemWidget(new DownloadListWidgetCompact), m_View(view) +DownloadListWidgetCompactDelegate::DownloadListWidgetCompactDelegate(DownloadManager *manager, bool metaDisplay, QTreeView *view, QObject *parent) + : QItemDelegate(parent) + , m_Manager(manager) + , m_MetaDisplay(metaDisplay) + , m_ItemWidget(new DownloadListWidgetCompact) + , m_View(view) { m_NameLabel = m_ItemWidget->findChild<QLabel*>("nameLabel"); m_SizeLabel = m_ItemWidget->findChild<QLabel*>("sizeLabel"); @@ -97,7 +101,7 @@ void DownloadListWidgetCompactDelegate::paintPendingDownload(int downloadIndex) void DownloadListWidgetCompactDelegate::paintRegularDownload(int downloadIndex) const { - QString name = m_Manager->getFileName(downloadIndex); + QString name = m_MetaDisplay ? m_Manager->getDisplayName(downloadIndex) : m_Manager->getFileName(downloadIndex); if (name.length() > 53) { name.truncate(50); name.append("..."); diff --git a/src/downloadlistwidgetcompact.h b/src/downloadlistwidgetcompact.h index 4d7f40de..c3cd5c11 100644 --- a/src/downloadlistwidgetcompact.h +++ b/src/downloadlistwidgetcompact.h @@ -54,7 +54,7 @@ class DownloadListWidgetCompactDelegate : public QItemDelegate public: - DownloadListWidgetCompactDelegate(DownloadManager *manager, QTreeView *view, QObject *parent = 0); + DownloadListWidgetCompactDelegate(DownloadManager *manager, bool metaDisplay, QTreeView *view, QObject *parent = 0); ~DownloadListWidgetCompactDelegate(); virtual void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const; @@ -103,6 +103,8 @@ private: DownloadListWidgetCompact *m_ItemWidget; DownloadManager *m_Manager; + bool m_MetaDisplay; + QLabel *m_NameLabel; QLabel *m_SizeLabel; QProgressBar *m_Progress; diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index 31d8bcec..5200bb4f 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -27,6 +27,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "json.h" #include "selectiondialog.h" #include <utility.h> +#include <bbcode.h> #include <QTimer> #include <QFileInfo> #include <QRegExp> @@ -49,7 +50,7 @@ static const char UNFINISHED[] = ".unfinished"; unsigned int DownloadManager::DownloadInfo::s_NextDownloadID = 1U; -DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createNew(const NexusInfo &nexusInfo, int modID, int fileID, const QStringList &URLs) +DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createNew(const ModRepositoryFileInfo *fileInfo, const QStringList &URLs) { DownloadInfo *info = new DownloadInfo; info->m_DownloadID = s_NextDownloadID++; @@ -57,9 +58,7 @@ DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createNew(const Ne info->m_PreResumeSize = 0LL; info->m_Progress = 0; info->m_ResumePos = 0; - info->m_ModID = modID; - info->m_FileID = fileID; - info->m_NexusInfo = nexusInfo; + info->m_FileInfo = new ModRepositoryFileInfo(*fileInfo); info->m_Urls = URLs; info->m_CurrentUrl = 0; info->m_Tries = AUTOMATIC_RETRIES; @@ -104,17 +103,28 @@ DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createFromMeta(con info->m_Output.setFileName(filePath); info->m_TotalSize = QFileInfo(filePath).size(); info->m_PreResumeSize = info->m_TotalSize; - info->m_ModID = metaFile.value("modID", 0).toInt(); - info->m_FileID = metaFile.value("fileID", 0).toInt(); info->m_CurrentUrl = 0; info->m_Urls = metaFile.value("url", "").toString().split(";"); info->m_Tries = 0; info->m_TaskProgressId = TaskProgressManager::instance().getId(); - info->m_NexusInfo.m_Name = metaFile.value("name", 0).toString(); - info->m_NexusInfo.m_ModName = metaFile.value("modName", "").toString(); - info->m_NexusInfo.m_Version = metaFile.value("version", 0).toString(); - info->m_NexusInfo.m_NewestVersion = metaFile.value("newestVersion", "").toString(); - info->m_NexusInfo.m_Category = metaFile.value("category", 0).toInt(); + int modID = metaFile.value("modID", 0).toInt(); + int fileID = metaFile.value("fileID", 0).toInt(); + info->m_FileInfo = new ModRepositoryFileInfo(modID, fileID); + info->m_FileInfo->name = metaFile.value("name", "").toString(); + if (info->m_FileInfo->name == "0") { + // bug in earlier version + info->m_FileInfo->name = ""; + } + info->m_FileInfo->modName = metaFile.value("modName", "").toString(); + info->m_FileInfo->modID = modID; + info->m_FileInfo->fileID = fileID; + info->m_FileInfo->description = metaFile.value("description").toString(); + info->m_FileInfo->version.parse(metaFile.value("version", "0").toString()); + info->m_FileInfo->newestVersion.parse(metaFile.value("newestVersion", "0").toString()); + info->m_FileInfo->categoryID = metaFile.value("category", 0).toInt(); + info->m_FileInfo->fileCategory = metaFile.value("fileCategory", 0).toInt(); + info->m_FileInfo->repository = metaFile.value("repository", "Nexus").toString(); + info->m_FileInfo->userData = metaFile.value("userData").toMap(); return info; } @@ -138,7 +148,10 @@ void DownloadManager::DownloadInfo::setName(QString newName, bool renameFile) metaFile.rename(newName.mid(0).append(".meta")); } } - m_Output.setFileName(newName); + if (!m_Output.isOpen()) { + // can't set file name if it's open + m_Output.setFileName(newName); + } } bool DownloadManager::DownloadInfo::isPausedState() @@ -243,72 +256,75 @@ void DownloadManager::setShowHidden(bool showHidden) void DownloadManager::refreshList() { - int downloadsBefore = m_ActiveDownloads.size(); + try { + int downloadsBefore = m_ActiveDownloads.size(); - // remove finished downloads - for (QVector<DownloadInfo*>::iterator Iter = m_ActiveDownloads.begin(); Iter != m_ActiveDownloads.end();) { - if (((*Iter)->m_State == STATE_READY) || ((*Iter)->m_State == STATE_INSTALLED) || ((*Iter)->m_State == STATE_UNINSTALLED)) { - delete *Iter; - Iter = m_ActiveDownloads.erase(Iter); - } else { - ++Iter; + // remove finished downloads + for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) { + if (((*iter)->m_State == STATE_READY) || ((*iter)->m_State == STATE_INSTALLED) || ((*iter)->m_State == STATE_UNINSTALLED)) { + delete *iter; + iter = m_ActiveDownloads.erase(iter); + } else { + ++iter; + } } - } - QStringList nameFilters(m_SupportedExtensions); - foreach (const QString &extension, m_SupportedExtensions) { - nameFilters.append("*." + extension); - } - - nameFilters.append(QString("*").append(UNFINISHED)); - QDir dir(QDir::fromNativeSeparators(m_OutputDirectory)); - - // find orphaned meta files and delete them (sounds cruel but it's better for everyone) - QStringList orphans; - QStringList metaFiles = dir.entryList(QStringList() << "*.meta"); - foreach (const QString &metaFile, metaFiles) { - QString baseFile = metaFile.left(metaFile.length() - 5); - if (!QFile::exists(dir.absoluteFilePath(baseFile))) { - orphans.append(dir.absoluteFilePath(metaFile)); + QStringList nameFilters(m_SupportedExtensions); + foreach (const QString &extension, m_SupportedExtensions) { + nameFilters.append("*." + extension); } - } - if (orphans.size() > 0) { - qDebug("%d orphaned meta files will be deleted", orphans.size()); - shellDelete(orphans, true); - } - // add existing downloads to list - foreach (QString file, dir.entryList(nameFilters, QDir::Files, QDir::Time)) { - bool Exists = false; - for (QVector<DownloadInfo*>::const_iterator Iter = m_ActiveDownloads.begin(); Iter != m_ActiveDownloads.end() && !Exists; ++Iter) { - if (QString::compare((*Iter)->m_FileName, file, Qt::CaseInsensitive) == 0) { - Exists = true; - } else if (QString::compare(QFileInfo((*Iter)->m_Output.fileName()).fileName(), file, Qt::CaseInsensitive) == 0) { - Exists = true; + nameFilters.append(QString("*").append(UNFINISHED)); + QDir dir(QDir::fromNativeSeparators(m_OutputDirectory)); + + // find orphaned meta files and delete them (sounds cruel but it's better for everyone) + QStringList orphans; + QStringList metaFiles = dir.entryList(QStringList() << "*.meta"); + foreach (const QString &metaFile, metaFiles) { + QString baseFile = metaFile.left(metaFile.length() - 5); + if (!QFile::exists(dir.absoluteFilePath(baseFile))) { + orphans.append(dir.absoluteFilePath(metaFile)); } } - if (Exists) { - qDebug("%s exists", qPrintable(file)); - continue; + if (orphans.size() > 0) { + qDebug("%d orphaned meta files will be deleted", orphans.size()); + shellDelete(orphans, true); } - QString fileName = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + file; + // add existing downloads to list + foreach (QString file, dir.entryList(nameFilters, QDir::Files, QDir::Time)) { + bool Exists = false; + for (QVector<DownloadInfo*>::const_iterator Iter = m_ActiveDownloads.begin(); Iter != m_ActiveDownloads.end() && !Exists; ++Iter) { + if (QString::compare((*Iter)->m_FileName, file, Qt::CaseInsensitive) == 0) { + Exists = true; + } else if (QString::compare(QFileInfo((*Iter)->m_Output.fileName()).fileName(), file, Qt::CaseInsensitive) == 0) { + Exists = true; + } + } + if (Exists) { + continue; + } - DownloadInfo *info = DownloadInfo::createFromMeta(fileName, m_ShowHidden); - if (info != NULL) { - m_ActiveDownloads.push_front(info); + QString fileName = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + file; + + DownloadInfo *info = DownloadInfo::createFromMeta(fileName, m_ShowHidden); + if (info != NULL) { + m_ActiveDownloads.push_front(info); + } } - } - if (m_ActiveDownloads.size() != downloadsBefore) { - qDebug("downloads after refresh: %d", m_ActiveDownloads.size()); + if (m_ActiveDownloads.size() != downloadsBefore) { + qDebug("downloads after refresh: %d", m_ActiveDownloads.size()); + } + emit update(-1); + } catch (const std::bad_alloc&) { + reportError(tr("Memory allocation error (in refreshing directory).")); } - emit update(-1); } bool DownloadManager::addDownload(const QStringList &URLs, - int modID, int fileID, const NexusInfo &nexusInfo) + int modID, int fileID, const ModRepositoryFileInfo *fileInfo) { QString fileName = QFileInfo(URLs.first()).fileName(); if (fileName.isEmpty()) { @@ -316,20 +332,34 @@ bool DownloadManager::addDownload(const QStringList &URLs, } QNetworkRequest request(URLs.first()); - return addDownload(m_NexusInterface->getAccessManager()->get(request), URLs, fileName, modID, fileID, nexusInfo); + return addDownload(m_NexusInterface->getAccessManager()->get(request), URLs, fileName, modID, fileID, fileInfo); +} + + +bool DownloadManager::addDownload(QNetworkReply *reply, const ModRepositoryFileInfo *fileInfo) +{ + QString fileName = getFileNameFromNetworkReply(reply); + if (fileName.isEmpty()) { + fileName = "unknown"; + } + + return addDownload(reply, QStringList(reply->url().toString()), fileName, fileInfo->modID, fileInfo->fileID, fileInfo); } bool DownloadManager::addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName, - int modID, int fileID, const NexusInfo &nexusInfo) + int modID, int fileID, const ModRepositoryFileInfo *fileInfo) { + if (!reply->isRunning()) { + qDebug("this is not a running download! %d", reply->isFinished()); + } // download invoked from an already open network reply (i.e. download link in the browser) - DownloadInfo *newDownload = DownloadInfo::createNew(nexusInfo, modID, fileID, URLs); + DownloadInfo *newDownload = DownloadInfo::createNew(fileInfo, URLs); QString baseName = fileName; - if (!nexusInfo.m_FileName.isEmpty()) { - baseName = nexusInfo.m_FileName; + if (!fileInfo->fileName.isEmpty()) { + baseName = fileInfo->fileName; } else { QString dispoName = getFileNameFromNetworkReply(reply); @@ -383,6 +413,7 @@ void DownloadManager::startDownload(QNetworkReply *reply, DownloadInfo *newDownl } newDownload->m_StartTime.start(); + createMetaFile(newDownload); if (!newDownload->m_Output.open(mode)) { reportError(tr("failed to download %1: could not open output file: %2") @@ -392,20 +423,24 @@ void DownloadManager::startDownload(QNetworkReply *reply, DownloadInfo *newDownl connect(newDownload->m_Reply, SIGNAL(downloadProgress(qint64, qint64)), this, SLOT(downloadProgress(qint64, qint64))); connect(newDownload->m_Reply, SIGNAL(finished()), this, SLOT(downloadFinished())); + connect(newDownload->m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(downloadError(QNetworkReply::NetworkError))); connect(newDownload->m_Reply, SIGNAL(readyRead()), this, SLOT(downloadReadyRead())); connect(newDownload->m_Reply, SIGNAL(metaDataChanged()), this, SLOT(metaDataChanged())); if (!resume) { newDownload->m_PreResumeSize = newDownload->m_Output.size(); - - removePending(newDownload->m_ModID, newDownload->m_FileID); + removePending(newDownload->m_FileInfo->modID, newDownload->m_FileInfo->fileID); emit aboutToUpdate(); - m_ActiveDownloads.append(newDownload); emit update(-1); emit downloadAdded(); + + if (reply->isFinished()) { + // it's possible the download has already finished before this function ran + downloadFinished(); + } } } @@ -424,12 +459,11 @@ void DownloadManager::addNXMDownload(const QString &url) } emit aboutToUpdate(); - m_PendingDownloads.append(std::make_pair(nxmInfo.modId(), nxmInfo.fileId())); emit update(-1); emit downloadAdded(); - m_RequestIDs.insert(m_NexusInterface->requestFileInfo(nxmInfo.modId(), nxmInfo.fileId(), this, nxmInfo.fileId())); + m_RequestIDs.insert(m_NexusInterface->requestFileInfo(nxmInfo.modId(), nxmInfo.fileId(), this, nxmInfo.fileId(), "")); } @@ -574,8 +608,11 @@ void DownloadManager::pauseDownload(int index) DownloadInfo *info = m_ActiveDownloads.at(index); if (info->m_State == STATE_DOWNLOADING) { - setState(info, STATE_PAUSING); - qDebug("pausing %d - %s", index, info->m_FileName.toUtf8().constData()); + if (info->m_Reply->isRunning()) { + setState(info, STATE_PAUSING); + } else { + setState(info, STATE_PAUSED); + } } else if ((info->m_State == STATE_FETCHINGMODINFO) || (info->m_State == STATE_FETCHINGFILEINFO)) { setState(info, STATE_READY); } @@ -600,6 +637,11 @@ void DownloadManager::resumeDownloadInt(int index) } DownloadInfo *info = m_ActiveDownloads[index]; if (info->isPausedState()) { + if ((info->m_Urls.size() == 0) + || ((info->m_Urls.size() == 1) && (info->m_Urls[0].size() == 0))) { + emit showMessage(tr("No known download urls. Sorry, this download can't be resumed.")); + return; + } if (info->m_State == STATE_ERROR) { info->m_CurrentUrl = (info->m_CurrentUrl + 1) % info->m_Urls.count(); } @@ -635,16 +677,21 @@ void DownloadManager::queryInfo(int index) } DownloadInfo *info = m_ActiveDownloads[index]; + if (info->m_FileInfo->repository != "Nexus") { + qWarning("re-querying file info is currently only possible with Nexus"); + return; + } + if (info->m_State < DownloadManager::STATE_READY) { // UI shouldn't allow this return; } - if (info->m_ModID == 0UL) { + if (info->m_FileInfo->modID == 0UL) { QString fileName = getFileName(index); QString ignore; - NexusInterface::interpretNexusFileName(fileName, ignore, info->m_ModID, true); - if (info->m_ModID < 0) { + NexusInterface::interpretNexusFileName(fileName, ignore, info->m_FileInfo->modID, true); + if (info->m_FileInfo->modID < 0) { QString modIDString; while (modIDString.isEmpty()) { modIDString = QInputDialog::getText(NULL, tr("Please enter the nexus mod id"), tr("Mod ID:"), QLineEdit::Normal, @@ -657,12 +704,11 @@ void DownloadManager::queryInfo(int index) modIDString.clear(); } } - info->m_ModID = modIDString.toInt(NULL, 10); + info->m_FileInfo->modID = modIDString.toInt(NULL, 10); } } info->m_ReQueried = true; setState(info, STATE_FETCHINGMODINFO); -// m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_ModID, this, qVariantFromValue(static_cast<void*>(info)))); } @@ -694,6 +740,34 @@ QString DownloadManager::getFilePath(int index) const return m_OutputDirectory + "/" + m_ActiveDownloads.at(index)->m_FileName; } +QString DownloadManager::getFileTypeString(int fileType) +{ + switch (fileType) { + case 1: return tr("Main"); + case 2: return tr("Update"); + case 3: return tr("Optional"); + case 4: return tr("Old"); + case 5: return tr("Misc"); + default: return tr("Unknown"); + } +} + +QString DownloadManager::getDisplayName(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("invalid index")); + } + + DownloadInfo *info = m_ActiveDownloads.at(index); + + if (!info->m_FileInfo->name.isEmpty()) { + return QString("%1 (%2, v%3)").arg(info->m_FileInfo->name) + .arg(getFileTypeString(info->m_FileInfo->fileCategory)) + .arg(info->m_FileInfo->version.displayString()); + } else { + return info->m_FileName; + } +} QString DownloadManager::getFileName(int index) const { @@ -755,7 +829,11 @@ bool DownloadManager::isInfoIncomplete(int index) const } DownloadInfo *info = m_ActiveDownloads.at(index); - return (info->m_FileID == 0) || (info->m_ModID == 0) || info->m_NexusInfo.m_Version.isEmpty(); + if (info->m_FileInfo->repository != "Nexus") { + // other repositories currently don't support re-querying info anyway + return false; + } + return (info->m_FileInfo->fileID == 0) || (info->m_FileInfo->modID == 0) || !info->m_FileInfo->version.isValid(); } @@ -764,7 +842,7 @@ int DownloadManager::getModID(int index) const if ((index < 0) || (index >= m_ActiveDownloads.size())) { throw MyException(tr("invalid index")); } - return m_ActiveDownloads.at(index)->m_ModID; + return m_ActiveDownloads.at(index)->m_FileInfo->modID; } bool DownloadManager::isHidden(int index) const @@ -776,13 +854,13 @@ bool DownloadManager::isHidden(int index) const } -NexusInfo DownloadManager::getNexusInfo(int index) const +const ModRepositoryFileInfo *DownloadManager::getFileInfo(int index) const { if ((index < 0) || (index >= m_ActiveDownloads.size())) { throw MyException(tr("invalid index")); } - return m_ActiveDownloads.at(index)->m_NexusInfo; + return m_ActiveDownloads.at(index)->m_FileInfo; } @@ -864,10 +942,10 @@ void DownloadManager::setState(DownloadManager::DownloadInfo *info, DownloadMana info->m_Reply->abort(); } break; case STATE_FETCHINGMODINFO: { - m_RequestIDs.insert(m_NexusInterface->requestDescription(info->m_ModID, this, info->m_DownloadID)); + m_RequestIDs.insert(m_NexusInterface->requestDescription(info->m_FileInfo->modID, this, info->m_DownloadID, QString())); } break; case STATE_FETCHINGFILEINFO: { - m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_ModID, this, info->m_DownloadID)); + m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_FileInfo->modID, this, info->m_DownloadID, QString())); } break; case STATE_READY: { createMetaFile(info); @@ -900,32 +978,40 @@ void DownloadManager::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) return; } int index = 0; - DownloadInfo *info = findDownload(this->sender(), &index); - if (info != NULL) { - if (info->m_State == STATE_CANCELING) { - setState(info, STATE_CANCELED); - } else if (info->m_State == STATE_PAUSING) { - setState(info, STATE_PAUSED); - } else { - if (bytesTotal > info->m_TotalSize) { - info->m_TotalSize = bytesTotal; - } - int oldProgress = info->m_Progress; - info->m_Progress = ((info->m_ResumePos + bytesReceived) * 100) / (info->m_ResumePos + bytesTotal); - TaskProgressManager::instance().updateProgress(info->m_TaskProgressId, bytesReceived, bytesTotal); - if (oldProgress != info->m_Progress) { - emit update(index); + try { + DownloadInfo *info = findDownload(this->sender(), &index); + if (info != NULL) { + if (info->m_State == STATE_CANCELING) { + setState(info, STATE_CANCELED); + } else if (info->m_State == STATE_PAUSING) { + setState(info, STATE_PAUSED); + } else { + if (bytesTotal > info->m_TotalSize) { + info->m_TotalSize = bytesTotal; + } + int oldProgress = info->m_Progress; + info->m_Progress = ((info->m_ResumePos + bytesReceived) * 100) / (info->m_ResumePos + bytesTotal); + TaskProgressManager::instance().updateProgress(info->m_TaskProgressId, bytesReceived, bytesTotal); + if (oldProgress != info->m_Progress) { + emit update(index); + } } } + } catch (const std::bad_alloc&) { + reportError(tr("Memory allocation error (in processing progress event).")); } } void DownloadManager::downloadReadyRead() { - DownloadInfo *info = findDownload(this->sender()); - if (info != NULL) { - info->m_Output.write(info->m_Reply->readAll()); + try { + DownloadInfo *info = findDownload(this->sender()); + if (info != NULL) { + info->m_Output.write(info->m_Reply->readAll()); + } + } catch (const std::bad_alloc&) { + reportError(tr("Memory allocation error (in processing downloaded data).")); } } @@ -933,16 +1019,19 @@ void DownloadManager::downloadReadyRead() void DownloadManager::createMetaFile(DownloadInfo *info) { QSettings metaFile(QString("%1.meta").arg(info->m_Output.fileName()), QSettings::IniFormat); - metaFile.setValue("modID", info->m_ModID); - metaFile.setValue("fileID", info->m_FileID); + metaFile.setValue("modID", info->m_FileInfo->modID); + metaFile.setValue("fileID", info->m_FileInfo->fileID); metaFile.setValue("url", info->m_Urls.join(";")); - metaFile.setValue("name", info->m_NexusInfo.m_Name); - metaFile.setValue("modName", info->m_NexusInfo.m_ModName); - metaFile.setValue("version", info->m_NexusInfo.m_Version); - metaFile.setValue("fileTime", info->m_NexusInfo.m_FileTime); - metaFile.setValue("fileCategory", info->m_NexusInfo.m_FileCategory); - metaFile.setValue("newestVersion", info->m_NexusInfo.m_NewestVersion); - metaFile.setValue("category", info->m_NexusInfo.m_Category); + metaFile.setValue("name", info->m_FileInfo->name); + metaFile.setValue("description", info->m_FileInfo->description); + metaFile.setValue("modName", info->m_FileInfo->modName); + metaFile.setValue("version", info->m_FileInfo->version.canonicalString()); + metaFile.setValue("newestVersion", info->m_FileInfo->newestVersion.canonicalString()); + metaFile.setValue("fileTime", info->m_FileInfo->fileTime); + metaFile.setValue("fileCategory", info->m_FileInfo->fileCategory); + metaFile.setValue("category", info->m_FileInfo->categoryID); + metaFile.setValue("repository", info->m_FileInfo->repository); + metaFile.setValue("userData", info->m_FileInfo->userData); metaFile.setValue("installed", info->m_State == DownloadManager::STATE_INSTALLED); metaFile.setValue("uninstalled", info->m_State == DownloadManager::STATE_UNINSTALLED); metaFile.setValue("paused", (info->m_State == DownloadManager::STATE_PAUSED) || @@ -971,10 +1060,10 @@ void DownloadManager::nxmDescriptionAvailable(int, QVariant userData, QVariant r DownloadInfo *info = downloadInfoByID(userData.toInt()); if (info == NULL) return; - info->m_NexusInfo.m_Category = result["category_id"].toInt(); - info->m_NexusInfo.m_ModName = result["name"].toString().trimmed(); - info->m_NexusInfo.m_NewestVersion = result["version"].toString(); - if (info->m_FileID != 0) { + info->m_FileInfo->categoryID = result["category_id"].toInt(); + info->m_FileInfo->modName = result["name"].toString().trimmed(); + info->m_FileInfo->newestVersion.parse(result["version"].toString()); + if (info->m_FileInfo->fileID != 0) { setState(info, STATE_READY); } else { setState(info, STATE_FETCHINGFILEINFO); @@ -993,6 +1082,18 @@ QDateTime DownloadManager::matchDate(const QString &timeString) } +EFileCategory convertFileCategory(int id) +{ + // TODO: need to handle file categories in the mod page plugin + switch (id) { + case 0: return TYPE_MAIN; + case 1: return TYPE_UPDATE; + case 2: return TYPE_OPTION; + default: return TYPE_MAIN; + } +} + + void DownloadManager::nxmFilesAvailable(int, QVariant userData, QVariant resultData, int requestID) { std::set<int>::iterator idIter = m_RequestIDs.find(requestID); @@ -1024,14 +1125,14 @@ void DownloadManager::nxmFilesAvailable(int, QVariant userData, QVariant resultD QString fileNameVariant = fileName.mid(0).replace(' ', '_'); if ((fileName == info->m_FileName) || (fileName == alternativeLocalName) || (fileNameVariant == info->m_FileName) || (fileNameVariant == alternativeLocalName)) { - info->m_NexusInfo.m_Name = fileInfo["name"].toString(); - info->m_NexusInfo.m_Version = fileInfo["version"].toString(); - if (info->m_NexusInfo.m_Version.isEmpty()) { - info->m_NexusInfo.m_Version = info->m_NexusInfo.m_NewestVersion; + info->m_FileInfo->name = fileInfo["name"].toString(); + info->m_FileInfo->version.parse(fileInfo["version"].toString()); + if (!info->m_FileInfo->version.isValid()) { + info->m_FileInfo->version = info->m_FileInfo->newestVersion; } - info->m_NexusInfo.m_FileCategory = fileInfo["category_id"].toInt(); - info->m_NexusInfo.m_FileTime = matchDate(fileInfo["date"].toString()); - info->m_FileID = fileInfo["id"].toInt(); + info->m_FileInfo->fileCategory = convertFileCategory(fileInfo["category_id"].toInt()); + info->m_FileInfo->fileTime = matchDate(fileInfo["date"].toString()); + info->m_FileInfo->fileID = fileInfo["id"].toInt(); found = true; break; } @@ -1050,16 +1151,16 @@ void DownloadManager::nxmFilesAvailable(int, QVariant userData, QVariant resultD } if (selection.exec() == QDialog::Accepted) { QVariantMap fileInfo = selection.getChoiceData().toMap(); - info->m_NexusInfo.m_Name = fileInfo["name"].toString(); - info->m_NexusInfo.m_Version = fileInfo["version"].toString(); - info->m_NexusInfo.m_FileCategory = fileInfo["category_id"].toInt(); - info->m_FileID = fileInfo["id"].toInt(); + info->m_FileInfo->name = fileInfo["name"].toString(); + info->m_FileInfo->version.parse(fileInfo["version"].toString()); + info->m_FileInfo->fileCategory = convertFileCategory(fileInfo["category_id"].toInt()); + info->m_FileInfo->fileID = fileInfo["id"].toInt(); } else { emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?")); } } } else { - if (info->m_FileID == 0) { + if (info->m_FileInfo->fileID == 0) { qWarning("could not determine file id for %s (state %d)", info->m_FileName.toUtf8().constData(), info->m_State); } @@ -1078,19 +1179,26 @@ void DownloadManager::nxmFileInfoAvailable(int modID, int fileID, QVariant userD m_RequestIDs.erase(idIter); } - NexusInfo info; + ModRepositoryFileInfo *info = new ModRepositoryFileInfo(); QVariantMap result = resultData.toMap(); - info.m_Name = result["name"].toString(); - qDebug("file info received for %s", qPrintable(info.m_Name)); - info.m_Version = result["version"].toString(); - if (info.m_Version.isEmpty()) { - info.m_Version = info.m_NewestVersion; + info->name = result["name"].toString(); + qDebug("file info received for %s", qPrintable(info->name)); + info->version.parse(result["version"].toString()); + if (!info->version.isValid()) { + info->version = info->newestVersion; } - info.m_FileName = result["uri"].toString(); - info.m_FileTime = matchDate(result["date"].toString()); + info->fileName = result["uri"].toString(); + info->fileCategory = result["category_id"].toInt(); + info->fileTime = matchDate(result["date"].toString()); + info->description = BBCode::convertToHTML(result["description"].toString()); + + info->repository = "Nexus"; + info->modID = modID; + info->fileID = fileID; - m_RequestIDs.insert(m_NexusInterface->requestDownloadURL(modID, fileID, this, QVariant::fromValue(info))); + QObject *test = info; + m_RequestIDs.insert(m_NexusInterface->requestDownloadURL(modID, fileID, this, qVariantFromValue(test), QString())); } @@ -1138,7 +1246,7 @@ bool DownloadManager::ServerByPreference(const std::map<QString, int> &preferred int DownloadManager::startDownloadURLs(const QStringList &urls) { - addDownload(urls, -1); + addDownload(urls, -1, -1, nullptr); return m_ActiveDownloads.size() - 1; } @@ -1173,7 +1281,7 @@ void DownloadManager::nxmDownloadURLsAvailable(int modID, int fileID, QVariant u m_RequestIDs.erase(idIter); } - NexusInfo info = userData.value<NexusInfo>(); + ModRepositoryFileInfo *info = qobject_cast<ModRepositoryFileInfo*>(qvariant_cast<QObject*>(userData)); QVariantList resultList = resultData.toList(); if (resultList.length() == 0) { removePending(modID, fileID); @@ -1183,14 +1291,13 @@ void DownloadManager::nxmDownloadURLsAvailable(int modID, int fileID, QVariant u std::sort(resultList.begin(), resultList.end(), boost::bind(&DownloadManager::ServerByPreference, m_PreferredServers, _1, _2)); - info.m_DownloadMap = resultList; + info->userData["downloadMap"] = resultList; QStringList URLs; foreach (const QVariant &server, resultList) { URLs.append(server.toMap()["URI"].toString()); } - addDownload(URLs, modID, fileID, info); } @@ -1208,7 +1315,7 @@ void DownloadManager::nxmRequestFailed(int modID, int fileID, QVariant userData, for (QVector<DownloadInfo*>::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++index) { DownloadInfo *info = *iter; - if (info->m_ModID == modID) { + if (info->m_FileInfo->modID == modID) { if (info->m_State < STATE_FETCHINGMODINFO) { m_ActiveDownloads.erase(iter); delete info; @@ -1246,7 +1353,7 @@ void DownloadManager::downloadFinished() textData) { if (info->m_Tries == 0) { if (textData && (reply->error() == QNetworkReply::NoError)) { - emit showMessage(tr("Download failed. Server reported: %1").arg(readFileText(info->m_Output.fileName()))); + emit showMessage(tr("Download failed. Server reported: %1").arg(QString(data))); } else { emit showMessage(tr("Download failed: %1 (%2)").arg(reply->errorString()).arg(reply->error())); } @@ -1279,20 +1386,27 @@ void DownloadManager::downloadFinished() createMetaFile(info); emit update(index); } else { - QString url = info->m_Urls[info->m_CurrentUrl]; - foreach (const QVariant &server, info->m_NexusInfo.m_DownloadMap) { - QVariantMap serverMap = server.toMap(); - if (serverMap["URI"].toString() == url) { - int deltaTime = info->m_StartTime.secsTo(QTime::currentTime()); - if (deltaTime > 5) { - emit downloadSpeed(serverMap["Name"].toString(), (info->m_TotalSize - info->m_PreResumeSize) / deltaTime); - } // no division by zero please! Also, if the download is shorter than a few seconds, the result is way to inprecise - break; + if (info->m_FileInfo->userData.contains("downloadMap")) { + foreach (const QVariant &server, info->m_FileInfo->userData["downloadMap"].toList()) { + QVariantMap serverMap = server.toMap(); + if (serverMap["URI"].toString() == url) { + int deltaTime = info->m_StartTime.secsTo(QTime::currentTime()); + if (deltaTime > 5) { + emit downloadSpeed(serverMap["Name"].toString(), (info->m_TotalSize - info->m_PreResumeSize) / deltaTime); + } // no division by zero please! Also, if the download is shorter than a few seconds, the result is way to inprecise + break; + } } } - setState(info, STATE_FETCHINGMODINFO); // need to set this state before changing the file name, otherwise .unfinished is appended + bool isNexus = info->m_FileInfo->repository == "Nexus"; + // need to change state before changing the file name, otherwise .unfinished is appended + if (isNexus) { + setState(info, STATE_FETCHINGMODINFO); + } else { + setState(info, STATE_NOFETCH); + } QString newName = getFileNameFromNetworkReply(reply); QString oldName = QFileInfo(info->m_Output).fileName(); @@ -1302,6 +1416,10 @@ void DownloadManager::downloadFinished() info->setName(m_OutputDirectory + "/" + info->m_FileName, true); // don't rename but remove the ".unfinished" extension } + if (!isNexus) { + setState(info, STATE_READY); + } + emit update(index); } reply->close(); @@ -1317,6 +1435,14 @@ void DownloadManager::downloadFinished() } +void DownloadManager::downloadError(QNetworkReply::NetworkError error) +{ + if (error != QNetworkReply::OperationCanceledError) { + qWarning("Download error occured: %d", error); + } +} + + void DownloadManager::metaDataChanged() { int index = 0; @@ -1327,7 +1453,7 @@ void DownloadManager::metaDataChanged() if (!newName.isEmpty() && (newName != info->m_FileName)) { info->setName(getDownloadFileName(newName), true); refreshAlphabeticalTranslation(); - if (!info->m_Output.open(QIODevice::WriteOnly | QIODevice::Append)) { + if (!info->m_Output.isOpen() && !info->m_Output.open(QIODevice::WriteOnly | QIODevice::Append)) { reportError(tr("failed to re-open %1").arg(info->m_FileName)); setState(info, STATE_CANCELING); } diff --git a/src/downloadmanager.h b/src/downloadmanager.h index 80b99ad2..0d8d314a 100644 --- a/src/downloadmanager.h +++ b/src/downloadmanager.h @@ -36,22 +36,6 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QSettings> -struct NexusInfo { - NexusInfo() : m_Category(0), m_FileCategory(0), m_Set(false) {} - int m_Category; - int m_FileCategory; - QString m_Name; - QString m_ModName; - QString m_Version; - QString m_NewestVersion; - QString m_FileName; - QVariantList m_DownloadMap; - QDateTime m_FileTime; - bool m_Set; -}; -Q_DECLARE_METATYPE(NexusInfo) - - /*! * \brief manages downloading of files and provides progress information for gui elements **/ @@ -71,6 +55,7 @@ public: STATE_ERROR, STATE_FETCHINGMODINFO, STATE_FETCHINGFILEINFO, + STATE_NOFETCH, STATE_READY, STATE_INSTALLED, STATE_UNINSTALLED @@ -79,6 +64,7 @@ public: private: struct DownloadInfo { + ~DownloadInfo() { delete m_FileInfo; } unsigned int m_DownloadID; QString m_FileName; QFile m_Output; @@ -86,14 +72,11 @@ private: QTime m_StartTime; qint64 m_PreResumeSize; int m_Progress; - int m_ModID; - int m_FileID; DownloadState m_State; int m_CurrentUrl; QStringList m_Urls; qint64 m_ResumePos; qint64 m_TotalSize; - QDateTime m_Created; // used as a cache in DownloadManager::getFileTime, may not be valid elsewhere int m_Tries; @@ -101,11 +84,11 @@ private: quint32 m_TaskProgressId; - NexusInfo m_NexusInfo; + MOBase::ModRepositoryFileInfo *m_FileInfo; bool m_Hidden; - static DownloadInfo *createNew(const NexusInfo &nexusInfo, int modID, int fileID, const QStringList &URLs); + static DownloadInfo *createNew(const MOBase::ModRepositoryFileInfo *fileInfo, const QStringList &URLs); static DownloadInfo *createFromMeta(const QString &filePath, bool showHidden); /** @@ -180,13 +163,20 @@ public: * @brief download from an already open network connection * * @param reply the network reply to download from - * @param fileName the name to use for the file. This may be overridden by the name in the nexusInfo-structure or if the http stream specifies a name - * @param modID the nexus mod id this download belongs to - * @param fileID the nexus file id this download belongs to, if known. Defaults to 0. - * @param nexusInfo information previously retrieved from the nexus network + * @param fileInfo information about the file, like mod id, file id, version, ... * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again **/ - bool addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName, int modID, int fileID = 0, const NexusInfo &nexusInfo = NexusInfo()); + bool addDownload(QNetworkReply *reply, const MOBase::ModRepositoryFileInfo *fileInfo); + + /** + * @brief download from an already open network connection + * + * @param reply the network reply to download from + * @param fileName the name to use for the file. This may be overridden by the name in the fileInfo-structure or if the http stream specifies a name + * @param fileInfo information previously retrieved from the nexus network + * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again + **/ + bool addDownload(QNetworkReply *reply, const QStringList &URLs, const QString &fileName, int modID, int fileID = 0, const MOBase::ModRepositoryFileInfo *fileInfo = new MOBase::ModRepositoryFileInfo()); /** * @brief start a download using a nxm-link @@ -227,6 +217,14 @@ public: QString getFilePath(int index) const; /** + * @brief retrieve a descriptive name of the download specified by index + * + * @param index index of the file to look up + * @return display name of the file + **/ + QString getDisplayName(int index) const; + + /** * @brief retrieve the filename of the download specified by index * * @param index index of the file to look up @@ -297,7 +295,7 @@ public: * @param index index of the file to look up * @return the nexus mod information **/ - NexusInfo getNexusInfo(int index) const; + const MOBase::ModRepositoryFileInfo *getFileInfo(int index) const; /** * @brief mark a download as installed @@ -419,6 +417,7 @@ private slots: void downloadProgress(qint64 bytesReceived, qint64 bytesTotal); void downloadReadyRead(); void downloadFinished(); + void downloadError(QNetworkReply::NetworkError error); void metaDataChanged(); void directoryChanged(const QString &dirctory); @@ -434,12 +433,10 @@ private: * @brief start a download from a url * * @param url the url to download from - * @param modID the nexus mod id this download belongs to - * @param fileID the nexus file id this download belongs to, if known. Defaults to 0. - * @param nexusInfo information previously retrieved from the nexus network + * @param fileInfo information previously retrieved from the mod page * @return true if the download was started, false if it wasn't. The latter currently only happens if there is a duplicate and the user decides not to download again **/ - bool addDownload(const QStringList &URLs, int modID, int fileID = 0, const NexusInfo &nexusInfo = NexusInfo()); + bool addDownload(const QStringList &URLs, int modID, int fileID, const MOBase::ModRepositoryFileInfo *fileInfo); // important: the caller has to lock the list-mutex, otherwise the DownloadInfo-pointer might get invalidated at any time DownloadInfo *findDownload(QObject *reply, int *index = NULL) const; @@ -460,6 +457,8 @@ private: void removePending(int modID, int fileID); + static QString getFileTypeString(int fileType); + private: static const int AUTOMATIC_RETRIES = 3; diff --git a/src/executableslist.cpp b/src/executableslist.cpp index 8f2da051..57341323 100644 --- a/src/executableslist.cpp +++ b/src/executableslist.cpp @@ -68,7 +68,6 @@ void ExecutablesList::init() { std::vector<ExecutableInfo> executables = GameInfo::instance().getExecutables(); for (std::vector<ExecutableInfo>::const_iterator iter = executables.begin(); iter != executables.end(); ++iter) { - ExecutableInfo test = *iter; addExecutableInternal(ToQString(iter->title), QDir::fromNativeSeparators(ToQString(GameInfo::instance().getGameDirectory())).append("/").append(ToQString(iter->binary)), ToQString(iter->arguments), ToQString(iter->workingDirectory), diff --git a/src/installationmanager.cpp b/src/installationmanager.cpp index e6e660e6..c6ddabce 100644 --- a/src/installationmanager.cpp +++ b/src/installationmanager.cpp @@ -69,7 +69,7 @@ template <typename T> T resolveFunction(QLibrary &lib, const char *name) InstallationManager::InstallationManager(QWidget *parent) : QObject(parent), m_ParentWidget(parent), - m_InstallationProgress(parent), m_SupportedExtensions(boost::assign::list_of("zip")("rar")("7z")("fomod")) + m_InstallationProgress(parent), m_SupportedExtensions(boost::assign::list_of("zip")("rar")("7z")("fomod")("001")) { QLibrary archiveLib("dlls\\archive.dll"); if (!archiveLib.load()) { @@ -522,9 +522,10 @@ bool InstallationManager::doInstall(GuessedValue<QString> &modName, int modID, return false; } - QString targetDirectory = QDir::fromNativeSeparators(m_ModsDirectory.mid(0).append("\\").append(modName)); + QString targetDirectoryNative = m_ModsDirectory.mid(0).append("\\").append(modName); + QString targetDirectory = QDir::fromNativeSeparators(targetDirectoryNative); - qDebug("installing to \"%s\"", targetDirectory.toUtf8().constData()); + qDebug("installing to \"%s\"", targetDirectoryNative.toUtf8().constData()); m_InstallationProgress.setWindowTitle(tr("Extracting files")); m_InstallationProgress.setLabelText(QString()); @@ -659,7 +660,9 @@ bool InstallationManager::install(const QString &fileName, GuessedValue<QString> // open the archive and construct the directory tree the installers work on bool archiveOpen = m_CurrentArchive->open(ToWString(QDir::toNativeSeparators(fileName)).c_str(), new MethodCallback<InstallationManager, void, LPSTR>(this, &InstallationManager::queryPassword)); - + if (!archiveOpen) { + qDebug("integrated archiver can't open %s. errorcode %d", qPrintable(fileName), m_CurrentArchive->getLastError()); + } ON_BLOCK_EXIT(std::bind(&InstallationManager::postInstallCleanup, this)); QScopedPointer<DirectoryTree> filesTree(archiveOpen ? createFilesTree() : NULL); @@ -676,8 +679,12 @@ bool InstallationManager::install(const QString &fileName, GuessedValue<QString> } // try only manual installers if that was requested - if ((installResult == IPluginInstaller::RESULT_MANUALREQUESTED) && !installer->isManualInstaller()) { - continue; + if (installResult == IPluginInstaller::RESULT_MANUALREQUESTED) { + if (!installer->isManualInstaller()) { + continue; + } + } else if (installResult != IPluginInstaller::RESULT_NOTATTEMPTED) { + break; } try { @@ -719,7 +726,8 @@ bool InstallationManager::install(const QString &fileName, GuessedValue<QString> case IPluginInstaller::RESULT_FAILED: { return false; } break; - case IPluginInstaller::RESULT_SUCCESS: { + case IPluginInstaller::RESULT_SUCCESS: + case IPluginInstaller::RESULT_SUCCESSCANCEL: { if (filesTree != NULL) { DirectoryTree::node_iterator iniTweakNode = filesTree->nodeFind(DirectoryTreeInformation("INI Tweaks")); hasIniTweaks = (iniTweakNode != filesTree->nodesEnd()) && diff --git a/src/logbuffer.cpp b/src/logbuffer.cpp index 689d2b55..1bf9cd85 100644 --- a/src/logbuffer.cpp +++ b/src/logbuffer.cpp @@ -29,7 +29,7 @@ QMutex LogBuffer::s_Mutex; LogBuffer::LogBuffer(int messageCount, QtMsgType minMsgType, const QString &outputFileName) - : QObject(NULL), m_OutFileName(outputFileName), m_ShutDown(false), + : QAbstractItemModel(NULL), m_OutFileName(outputFileName), m_ShutDown(false), m_MinMsgType(minMsgType), m_NumMessages(0) { m_Messages.resize(messageCount); @@ -51,7 +51,16 @@ LogBuffer::~LogBuffer() void LogBuffer::logMessage(QtMsgType type, const QString &message) { if (type >= m_MinMsgType) { - m_Messages.at(m_NumMessages % m_Messages.size()) = message; + Message msg = { type, QTime::currentTime(), message }; + if (m_NumMessages < m_Messages.size()) { + beginInsertRows(QModelIndex(), m_NumMessages, m_NumMessages + 1); + } + m_Messages.at(m_NumMessages % m_Messages.size()) = msg; + if (m_NumMessages < m_Messages.size()) { + endInsertRows(); + } else { + emit dataChanged(createIndex(0, 0), createIndex(m_Messages.size(), 0)); + } ++m_NumMessages; if (type >= QtCriticalMsg) { write(); @@ -77,7 +86,7 @@ void LogBuffer::write() const unsigned int i = (m_NumMessages > m_Messages.size()) ? m_NumMessages - m_Messages.size() : 0U; for (; i < m_NumMessages; ++i) { - file.write(m_Messages.at(i % m_Messages.size()).toUtf8()); + file.write(m_Messages.at(i % m_Messages.size()).toString().toUtf8()); file.write("\r\n"); } ::SetLastError(lastError); @@ -125,6 +134,72 @@ char LogBuffer::msgTypeID(QtMsgType type) } } +QModelIndex LogBuffer::index(int row, int column, const QModelIndex&) const +{ + return createIndex(row, column, row); +} + +QModelIndex LogBuffer::parent(const QModelIndex&) const +{ + return QModelIndex(); +} + +int LogBuffer::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) + return 0; + else + return std::min(m_NumMessages, m_Messages.size()); +} + +int LogBuffer::columnCount(const QModelIndex&) const +{ + return 2; +} + + +QVariant LogBuffer::data(const QModelIndex &index, int role) const +{ + unsigned offset = m_NumMessages < m_Messages.size() ? 0 + : m_NumMessages - m_Messages.size(); + unsigned int msgIndex = (offset + index.row()) % m_Messages.size(); + switch (role) { + case Qt::DisplayRole: { + if (index.column() == 0) { + return m_Messages.at(msgIndex).time; + } else if (index.column() == 1) { + const QString &msg = m_Messages.at(msgIndex).message; + if (msg.length() < 200) { + return msg; + } else { + return msg.mid(0, 200) + "..."; + } + } + } break; + case Qt::DecorationRole: { + if (index.column() == 1) { + switch (m_Messages.at(msgIndex).type) { + case QtDebugMsg: return QIcon(":/MO/gui/information"); + case QtWarningMsg: return QIcon(":/MO/gui/warning"); + case QtCriticalMsg: return QIcon(":/MO/gui/important"); + case QtFatalMsg: return QIcon(":/MO/gui/problem"); + } + } + } break; + case Qt::UserRole: { + if (index.column() == 1) { + switch (m_Messages.at(msgIndex).type) { + case QtDebugMsg: return "D"; + case QtWarningMsg: return "W"; + case QtCriticalMsg: return "C"; + case QtFatalMsg: return "F"; + } + } + } break; + } + return QVariant(); +} + void LogBuffer::log(QtMsgType type, const char *message) { QMutexLocker guard(&s_Mutex); @@ -171,3 +246,9 @@ void log(const char *format, ...) va_end(argList); } + + +QString LogBuffer::Message::toString() const +{ + return QString("%1 [%2] %3").arg(time.toString()).arg(msgTypeID(type)).arg(message); +} diff --git a/src/logbuffer.h b/src/logbuffer.h index 68753996..caada1d9 100644 --- a/src/logbuffer.h +++ b/src/logbuffer.h @@ -23,10 +23,12 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QObject> #include <QMutex> #include <QScopedPointer> +#include <QStringListModel> +#include <QTime> #include <vector> -class LogBuffer : public QObject +class LogBuffer : public QAbstractItemModel { Q_OBJECT @@ -42,12 +44,22 @@ public: static void writeNow(); static void cleanQuit(); + static LogBuffer *instance() { return s_Instance.data(); } + public: virtual ~LogBuffer(); void logMessage(QtMsgType type, const QString &message); + // QAbstractItemModel interface +public: + QModelIndex index(int row, int column, const QModelIndex &parent) const; + QModelIndex parent(const QModelIndex &child) const; + int rowCount(const QModelIndex &parent) const; + int columnCount(const QModelIndex &parent) const; + QVariant data(const QModelIndex &index, int role) const; + signals: public slots: @@ -64,6 +76,15 @@ private: private: + struct Message { + QtMsgType type; + QTime time; + QString message; + QString toString() const; + }; + +private: + static QScopedPointer<LogBuffer> s_Instance; static QMutex s_Mutex; @@ -71,7 +92,7 @@ private: bool m_ShutDown; QtMsgType m_MinMsgType; unsigned int m_NumMessages; - std::vector<QString> m_Messages; + std::vector<Message> m_Messages; }; diff --git a/src/main.cpp b/src/main.cpp index ac903615..05dc54d7 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -80,24 +80,6 @@ using namespace MOBase; using namespace MOShared; -void removeOldLogfiles() -{ - QFileInfoList files = QDir(ToQString(GameInfo::instance().getLogDir())).entryInfoList(QStringList("ModOrganizer*.log"), - QDir::Files, QDir::Name); - - if (files.count() > 5) { - QStringList deleteFiles; - for (int i = 0; i < files.count() - 5; ++i) { - deleteFiles.append(files.at(i).absoluteFilePath()); - } - - if (!shellDelete(deleteFiles)) { - qWarning("failed to remove log files: %s", qPrintable(windowsErrorString(::GetLastError()))); - } - } -} - - // set up required folders (for a first install or after an update or to fix a broken installation) bool bootstrap() { @@ -111,7 +93,7 @@ bool bootstrap() } // cycle logfile - removeOldLogfiles(); + removeOldFiles(ToQString(GameInfo::instance().getLogDir()), "ModOrganizer*.log", 5, QDir::Name); // create organizer directories QString dirNames[] = { @@ -120,8 +102,7 @@ bool bootstrap() QDir::fromNativeSeparators(ToQString(gameInfo.getDownloadDir())), QDir::fromNativeSeparators(ToQString(gameInfo.getOverwriteDir())), QDir::fromNativeSeparators(ToQString(gameInfo.getLogDir())), - QDir::fromNativeSeparators(ToQString(gameInfo.getTutorialDir())), - QDir::fromNativeSeparators(ToQString(gameInfo.getOrganizerDirectory()) + "/boss") + QDir::fromNativeSeparators(ToQString(gameInfo.getTutorialDir())) }; static const int NUM_DIRECTORIES = sizeof(dirNames) / sizeof(QString); @@ -158,6 +139,10 @@ bool bootstrap() // verify the hook-dll exists QString dllName = qApp->applicationDirPath() + "/" + ToQString(AppConfig::hookDLLName()); + if (::GetModuleHandleW(ToWString(dllName).c_str()) != NULL) { + throw std::runtime_error("hook.dll already loaded! You can't start Mod Organizer from within itself (not even indirectly)"); + } + HMODULE dllMod = ::LoadLibraryW(ToWString(dllName).c_str()); if (dllMod == NULL) { throw windows_error("hook.dll is missing or invalid"); @@ -210,7 +195,6 @@ bool isNxmLink(const QString &link) return link.left(6).toLower() == "nxm://"; } - LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *exceptionPtrs) { typedef BOOL (WINAPI *FuncMiniDumpWriteDump)(HANDLE process, DWORD pid, HANDLE file, MINIDUMP_TYPE dumpType, @@ -272,14 +256,11 @@ LONG WINAPI MyUnhandledExceptionFilter(struct _EXCEPTION_POINTERS *exceptionPtrs return result; } - void registerMetaTypes() { registerExecutable(); } - - bool HaveWriteAccess(const std::wstring &path) { bool writable = false; @@ -330,7 +311,6 @@ bool HaveWriteAccess(const std::wstring &path) } - int main(int argc, char *argv[]) { MOApplication application(argc, argv); @@ -349,7 +329,7 @@ int main(int argc, char *argv[]) , ToWString(QDir::currentPath()).c_str(), SW_SHOWNORMAL); return 1; } - LogBuffer::init(200, QtDebugMsg, application.applicationDirPath() + "/logs/mo_interface.log"); + LogBuffer::init(100, QtDebugMsg, application.applicationDirPath() + "/logs/mo_interface.log"); qDebug("Working directory: %s", qPrintable(QDir::toNativeSeparators(QDir::currentPath()))); qDebug("MO at: %s", qPrintable(QDir::toNativeSeparators(application.applicationDirPath()))); @@ -545,7 +525,11 @@ int main(int argc, char *argv[]) arguments.removeAt(profileIndex); arguments.removeAt(profileIndex); } - qDebug("configured profile: %s", qPrintable(selectedProfileName)); + if (selectedProfileName.isEmpty()) { + qDebug("no configured profile"); + } else { + qDebug("configured profile: %s", qPrintable(selectedProfileName)); + } // if we have a command line parameter, it is either a nxm link or // a binary to start @@ -555,7 +539,12 @@ int main(int argc, char *argv[]) arguments.removeFirst(); // remove application name (ModOrganizer.exe) arguments.removeFirst(); // remove binary name // pass the remaining parameters to the binary - mainWindow.startApplication(exeName, arguments, QString(), selectedProfileName); + try { + mainWindow.startApplication(exeName, arguments, QString(), selectedProfileName); + } catch (const std::exception &e) { + reportError(QObject::tr("failed to start application: %1").arg(e.what())); + } + return 0; } diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 625b406d..1e8d512a 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -57,8 +57,10 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "savetextasdialog.h" #include "problemsdialog.h" #include "previewdialog.h" +#include "browserdialog.h" #include "aboutdialog.h" #include "safewritefile.h" +#include "organizerproxy.h" #include <gameinfo.h> #include <appconfig.h> #include <utility.h> @@ -94,6 +96,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QDesktopWidget> #include <QtPlugin> #include <QIdentityProxyModel> +#include <QClipboard> #include <boost/bind.hpp> #include <boost/foreach.hpp> #include <boost/assign.hpp> @@ -109,8 +112,10 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QtConcurrentRun> #endif #include <QCoreApplication> +#include <QProgressDialog> #include <scopeguard.h> #include <boost/thread.hpp> +#include <boost/algorithm/string.hpp> #ifdef TEST_MODELS @@ -157,12 +162,21 @@ MainWindow::MainWindow(const QString &exeName, QSettings &initSettings, QWidget m_PluginList(this), m_OldExecutableIndex(-1), m_GamePath(ToQString(GameInfo::instance().getGameDirectory())), m_DownloadManager(NexusInterface::instance(), this), m_InstallationManager(this), m_Updater(NexusInterface::instance(), this), m_CategoryFactory(CategoryFactory::instance()), - m_CurrentProfile(NULL), m_AskForNexusPW(false), m_LoginAttempted(false), + m_CurrentProfile(NULL), m_AskForNexusPW(false), m_ArchivesInit(false), m_ContextItem(NULL), m_ContextAction(NULL), m_CurrentSaveView(NULL), - m_GameInfo(new GameInfoImpl()), m_AboutToRun() + m_GameInfo(new GameInfoImpl()), m_AboutToRun(), m_ModInstalled() { ui->setupUi(this); this->setWindowTitle(ToQString(GameInfo::instance().getGameName()) + " Mod Organizer v" + m_Updater.getVersion().displayString()); + ui->logList->setModel(LogBuffer::instance()); + ui->logList->setColumnWidth(0, 100); + ui->logList->setAutoScroll(true); + ui->logList->scrollToBottom(); + ui->logList->addAction(ui->actionCopy_Log_to_Clipboard); + int splitterSize = this->size().height(); // actually total window size, but the splitter doesn't seem to return the true value + ui->topLevelSplitter->setSizes(QList<int>() << splitterSize - 100 << 100); + connect(ui->logList->model(), SIGNAL(rowsInserted(const QModelIndex &, int, int)), ui->logList, SLOT(scrollToBottom())); + connect(ui->logList->model(), SIGNAL(dataChanged(QModelIndex,QModelIndex)), ui->logList, SLOT(scrollToBottom())); m_RefreshProgress = new QProgressBar(statusBar()); m_RefreshProgress->setTextVisible(true); @@ -232,6 +246,8 @@ MainWindow::MainWindow(const QString &exeName, QSettings &initSettings, QWidget linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Start Menu"), this, SLOT(linkMenu())); ui->linkButton->setMenu(linkMenu); + ui->listOptionsBtn->setMenu(modListContextMenu()); + m_DownloadManager.setOutputDirectory(m_Settings.getDownloadDirectory()); m_DownloadManager.setPreferredServers(m_Settings.getPreferredServers()); @@ -269,7 +285,6 @@ MainWindow::MainWindow(const QString &exeName, QSettings &initSettings, QWidget connect(&m_PluginList, SIGNAL(saveTimer()), this, SLOT(savePluginList())); connect(ui->bsaList, SIGNAL(itemsMoved()), this, SLOT(bsaList_itemMoved())); - connect(ui->bsaWarning, SIGNAL(linkActivated(QString)), this, SLOT(linkClicked(QString))); connect(ui->dataTree, SIGNAL(itemExpanded(QTreeWidgetItem*)), this, SLOT(expandDataTreeItem(QTreeWidgetItem*))); @@ -292,11 +307,14 @@ MainWindow::MainWindow(const QString &exeName, QSettings &initSettings, QWidget connect(NexusInterface::instance()->getAccessManager(), SIGNAL(loginFailed(QString)), this, SLOT(loginFailed(QString))); connect(NexusInterface::instance(), SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); connect(NexusInterface::instance(), SIGNAL(nxmDownloadURLsAvailable(int,int,QVariant,QVariant,int)), this, SLOT(nxmDownloadURLs(int,int,QVariant,QVariant,int))); + connect(NexusInterface::instance(), SIGNAL(needLogin()), this, SLOT(nexusLogin())); connect(&TutorialManager::instance(), SIGNAL(windowTutorialFinished(QString)), this, SLOT(windowTutorialFinished(QString))); connect(ui->toolBar, SIGNAL(customContextMenuRequested(QPoint)), this, SLOT(toolBar_customContextMenuRequested(QPoint))); + connect(&m_IntegratedBrowser, SIGNAL(requestDownload(QUrl,QNetworkReply*)), this, SLOT(requestDownload(QUrl,QNetworkReply*))); + connect(this, SIGNAL(styleChanged(QString)), this, SLOT(updateStyle(QString))); m_CheckBSATimer.setSingleShot(true); @@ -309,7 +327,6 @@ MainWindow::MainWindow(const QString &exeName, QSettings &initSettings, QWidget m_DirectoryRefresher.moveToThread(&m_RefresherThread); m_RefresherThread.start(); - setCompactDownloads(initSettings.value("compact_downloads", false).toBool()); m_AskForNexusPW = initSettings.value("ask_for_nexuspw", true).toBool(); setCategoryListVisible(initSettings.value("categorylist_visible", true).toBool()); FileDialogMemory::restore(initSettings); @@ -337,6 +354,7 @@ MainWindow::~MainWindow() { m_RefresherThread.exit(); m_RefresherThread.wait(); + m_IntegratedBrowser.close(); delete ui; delete m_GameInfo; delete m_DirectoryStructure; @@ -514,14 +532,26 @@ void MainWindow::updateToolBar() void MainWindow::updateProblemsButton() { - if (checkForProblems()) { + int numProblems = checkForProblems(); + if (numProblems > 0) { ui->actionProblems->setEnabled(true); ui->actionProblems->setIconText(tr("Problems")); ui->actionProblems->setToolTip(tr("There are potential problems with your setup")); + +// QPixmap mergedIcon(64, 64); + QPixmap mergedIcon = QPixmap(":/MO/gui/warning").scaled(64, 64); + { + QPainter painter(&mergedIcon); +// painter.setBrush(QBrush(Qt::transparent)); + std::string badgeName = std::string(":/MO/gui/badge_") + (numProblems < 10 ? std::to_string(static_cast<long long>(numProblems)) : "more"); + painter.drawPixmap(32, 32, 32, 32, QPixmap(badgeName.c_str())); + } + ui->actionProblems->setIcon(QIcon(mergedIcon)); } else { ui->actionProblems->setEnabled(false); ui->actionProblems->setIconText(tr("No Problems")); ui->actionProblems->setToolTip(tr("Everything seems to be in order")); + ui->actionProblems->setIcon(QIcon(":/MO/gui/warning")); } } @@ -556,15 +586,13 @@ bool MainWindow::errorReported(QString &logFile) } -bool MainWindow::checkForProblems() +int MainWindow::checkForProblems() { foreach (IPluginDiagnose *diagnose, m_DiagnosisPlugins) { std::vector<unsigned int> activeProblems = diagnose->activeProblems(); - if (activeProblems.size() > 0) { - return true; - } + return activeProblems.size(); } - return false; + return 0; } void MainWindow::about() @@ -648,8 +676,9 @@ void MainWindow::saveArchiveList() } } } - archiveFile.commit(); - qDebug("%s saved", qPrintable(QDir::toNativeSeparators(m_CurrentProfile->getArchivesFileName()))); + if (archiveFile.commitIfDifferent(m_ArchiveListHash)) { + qDebug("%s saved", qPrintable(QDir::toNativeSeparators(m_CurrentProfile->getArchivesFileName()))); + } } else { qWarning("archive list not initialised"); } @@ -665,10 +694,12 @@ void MainWindow::savePluginList() m_PluginList.saveLoadOrder(*m_DirectoryStructure); } -void MainWindow::modFilterActive(bool active) +void MainWindow::modFilterActive(bool filterActive) { - if (active) { + if (filterActive) { ui->modList->setStyleSheet("QTreeView { border: 2px ridge #f00; }"); + } else if (ui->groupCombo->currentIndex() != 0) { + ui->modList->setStyleSheet("QTreeView { border: 2px ridge #337733; }"); } else { ui->modList->setStyleSheet(""); } @@ -824,6 +855,8 @@ void MainWindow::closeEvent(QCloseEvent* event) setCursor(Qt::WaitCursor); + m_IntegratedBrowser.close(); + storeSettings(); // profile has to be cleaned up before the modinfo-buffer is cleared @@ -846,6 +879,12 @@ void MainWindow::createFirstProfile() } +void MainWindow::setBrowserGeometry(const QByteArray &geometry) +{ + m_IntegratedBrowser.restoreGeometry(geometry); +} + + SaveGameGamebryo *MainWindow::getSaveGame(const QString &name) { return new SaveGameGamebryo(this, name); @@ -968,7 +1007,7 @@ bool MainWindow::verifyPlugin(IPlugin *plugin) { if (plugin == NULL) { return false; - } else if (!plugin->init(this)) { + } else if (!plugin->init(new OrganizerProxy(this, plugin->name()))) { qWarning("plugin failed to initialize"); return false; } @@ -990,6 +1029,50 @@ void MainWindow::toolPluginInvoke() } +void MainWindow::requestDownload(const QUrl &url, QNetworkReply *reply) +{ + QToolButton *browserBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionNexus)); + + QList<QAction*> browserActions = browserBtn->menu()->actions(); + foreach (QAction *action, browserActions) { + // the nexus action doesn't have a plugin connected currently + if (action->data().isValid()) { + IPluginModPage *plugin = qobject_cast<IPluginModPage*>(qvariant_cast<QObject*>(action->data())); + if (plugin == NULL) { + qCritical("invalid mod page. This is a bug"); + continue; + } + ModRepositoryFileInfo *fileInfo = new ModRepositoryFileInfo(); + if (plugin->handlesDownload(url, reply->url(), *fileInfo)) { + fileInfo->repository = plugin->name(); + m_DownloadManager.addDownload(reply, fileInfo); + return; + } + } + } + + if (QMessageBox::question(this, tr("Download?"), + tr("A download has been started but no installed page plugin recognizes it.\n" + "If you download anyway no information (i.e. version) will be associated with the download.\n" + "Continue?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_DownloadManager.addDownload(reply, new ModRepositoryFileInfo()); + } +} + + +void MainWindow::modPagePluginInvoke() +{ + QAction *triggeredAction = qobject_cast<QAction*>(sender()); + IPluginModPage *plugin = qobject_cast<IPluginModPage*>(triggeredAction->data().value<QObject*>()); + if (plugin->useIntegratedBrowser()) { + m_IntegratedBrowser.setWindowTitle(plugin->displayName()); + m_IntegratedBrowser.openUrl(plugin->pageURL()); + } else { + ::ShellExecuteW(NULL, L"open", ToWString(plugin->pageURL().toString()).c_str(), NULL, NULL, SW_SHOWNORMAL); + } +} + void MainWindow::registerPluginTool(IPluginTool *tool) { QAction *action = new QAction(tool->icon(), tool->displayName(), ui->toolBar); @@ -1002,6 +1085,34 @@ void MainWindow::registerPluginTool(IPluginTool *tool) } +void MainWindow::registerModPage(IPluginModPage *modPage) +{ + QToolButton *browserBtn = NULL; + // turn the browser action into a drop-down menu if necessary + if (ui->actionNexus->menu() == NULL) { + QAction *nexusAction = ui->actionNexus; + // TODO: use a different icon for nexus! + ui->actionNexus = new QAction(nexusAction->icon(), tr("Browse Mod Page"), ui->toolBar); + ui->toolBar->insertAction(nexusAction, ui->actionNexus); + ui->toolBar->removeAction(nexusAction); + actionToToolButton(ui->actionNexus); + + browserBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionNexus)); + browserBtn->menu()->addAction(nexusAction); + } else { + browserBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionNexus)); + } + + QAction *action = new QAction(modPage->icon(), modPage->displayName(), ui->toolBar); + modPage->setParentWidget(this); + action->setData(qVariantFromValue(reinterpret_cast<QObject*>(modPage))); + + connect(action, SIGNAL(triggered()), this, SLOT(modPagePluginInvoke()), Qt::QueuedConnection); + QToolButton *toolBtn = qobject_cast<QToolButton*>(ui->toolBar->widgetForAction(ui->actionNexus)); + toolBtn->menu()->addAction(action); +} + + bool MainWindow::registerPlugin(QObject *plugin, const QString &fileName) { { // generic treatment for all plugins @@ -1021,6 +1132,13 @@ bool MainWindow::registerPlugin(QObject *plugin, const QString &fileName) diagnose->onInvalidated([&] () { this->updateProblemsButton(); }); } } + { // mod page plugin + IPluginModPage *modPage = qobject_cast<IPluginModPage*>(plugin); + if (verifyPlugin(modPage)) { + registerModPage(modPage); + return true; + } + } { // tool plugins IPluginTool *tool = qobject_cast<IPluginTool*>(plugin); if (verifyPlugin(tool)) { @@ -1147,131 +1265,6 @@ void MainWindow::loadPlugins() m_DiagnosisPlugins.push_back(this); } -IGameInfo &MainWindow::gameInfo() const -{ - return *m_GameInfo; -} - - -IModRepositoryBridge *MainWindow::createNexusBridge() const -{ - return new NexusBridge(); -} - - -QString MainWindow::profileName() const -{ - if (m_CurrentProfile != NULL) { - return m_CurrentProfile->getName(); - } else { - return ""; - } -} - -QString MainWindow::profilePath() const -{ - if (m_CurrentProfile != NULL) { - return m_CurrentProfile->getPath(); - } else { - return ""; - } -} - -QString MainWindow::downloadsPath() const -{ - return QDir::fromNativeSeparators(m_Settings.getDownloadDirectory()); -} - -VersionInfo MainWindow::appVersion() const -{ - return m_Updater.getVersion(); -} - - -IModInterface *MainWindow::getMod(const QString &name) -{ - unsigned int index = ModInfo::getIndex(name); - if (index == UINT_MAX) { - return NULL; - } else { - return ModInfo::getByIndex(index).data(); - } -} - - -IModInterface *MainWindow::createMod(GuessedValue<QString> &name) -{ - if (!m_InstallationManager.testOverwrite(name)) { - return NULL; - } - - m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); - -/* QString fixedName = name; - fixDirectoryName(fixedName); - unsigned int index = ModInfo::getIndex(fixedName); - if (index != UINT_MAX) { - ModInfo::Ptr result = ModInfo::getByIndex(index); - if (!result->isEmpty()) { - throw MyException(tr("The mod \"%1\" already exists!").arg(fixedName)); - } - return result.data(); - } else {*/ - QString targetDirectory = QDir::fromNativeSeparators(m_Settings.getModDirectory()).append("/").append(name); - - QSettings settingsFile(targetDirectory.mid(0).append("/meta.ini"), QSettings::IniFormat); - - settingsFile.setValue("modid", 0); - settingsFile.setValue("version", ""); - settingsFile.setValue("newestVersion", ""); - settingsFile.setValue("category", 0); - settingsFile.setValue("installationFile", ""); - return ModInfo::createFrom(QDir(targetDirectory), &m_DirectoryStructure).data(); -// } -} - -bool MainWindow::removeMod(IModInterface *mod) -{ - unsigned int index = ModInfo::getIndex(mod->name()); - if (index == UINT_MAX) { - return mod->remove(); - } else { - return ModInfo::removeMod(index); - } -} - - -void MainWindow::modDataChanged(IModInterface*) -{ - refreshModList(); -} - -QVariant MainWindow::pluginSetting(const QString &pluginName, const QString &key) const -{ - return m_Settings.pluginSetting(pluginName, key); -} - -void MainWindow::setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value) -{ - m_Settings.setPluginSetting(pluginName, key, value); -} - -QVariant MainWindow::persistent(const QString &pluginName, const QString &key, const QVariant &def) const -{ - return m_Settings.pluginPersistent(pluginName, key, def); -} - -void MainWindow::setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync) -{ - m_Settings.setPluginPersistent(pluginName, key, value, sync); -} - -QString MainWindow::pluginDataPath() const -{ - QString pluginPath = QDir::fromNativeSeparators(ToQString(GameInfo::instance().getOrganizerDirectory())) + "/" + ToQString(AppConfig::pluginPath()); - return pluginPath + "/data"; -} - void MainWindow::startSteam() { @@ -1318,11 +1311,12 @@ HANDLE MainWindow::spawnBinaryDirect(const QFileInfo &binary, const QString &arg } } - while (m_RefreshProgress->isVisible()) { + while (m_DirectoryUpdate) { ::Sleep(100); QCoreApplication::processEvents(); } + // need to make sure all data is saved before we start the application if (m_CurrentProfile != nullptr) { m_CurrentProfile->writeModlistNow(true); } @@ -1336,28 +1330,6 @@ HANDLE MainWindow::spawnBinaryDirect(const QFileInfo &binary, const QString &arg } } -/* -void MainWindow::spawnProgram(const QString &fileName, const QString &argumentsArg, - const QString &profileName, const QDir ¤tDirectory) -{ - QFileInfo binary; - QString arguments = argumentsArg; - QString steamAppID; - try { - const Executable &exe = m_ExecutablesList.find(fileName); - steamAppID = exe.m_SteamAppID; - if (arguments == "") { - arguments = exe.m_Arguments; - } - binary = exe.m_BinaryInfo; - } catch (const std::runtime_error&) { - qWarning("\"%s\" not set up as executable", fileName.toUtf8().constData()); - binary = QFileInfo(fileName); - } - spawnBinaryDirect(binary, arguments, profileName, currentDirectory, steamAppID); -} -*/ - void MainWindow::spawnBinary(const QFileInfo &binary, const QString &arguments, const QDir ¤tDirectory, bool closeAfterStart, const QString &steamAppID) { @@ -1411,21 +1383,6 @@ void MainWindow::startExeAction() } -void MainWindow::refreshModList(bool saveChanges) -{ - // don't lose changes! - if (saveChanges) { - m_CurrentProfile->writeModlistNow(true); - } - ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure); - m_CurrentProfile->refreshModStatus(); - - m_ModList.notifyChange(-1); - - refreshDirectoryStructure(); -} - - void MainWindow::setExecutablesList(const ExecutablesList &executablesList) { m_ExecutablesList = executablesList; @@ -1668,11 +1625,27 @@ bool MainWindow::refreshProfiles(bool selectProfile) } +std::set<QString> MainWindow::managedArchives() +{ + std::set<QString> result; + + QFile archiveFile(m_CurrentProfile->getArchivesFileName()); + if (archiveFile.open(QIODevice::ReadOnly)) { + while (!archiveFile.atEnd()) { + result.insert(QString::fromUtf8(archiveFile.readLine()).trimmed()); + } + archiveFile.close(); + } + return result; +} + + void MainWindow::refreshDirectoryStructure() { m_DirectoryUpdate = true; std::vector<std::tuple<QString, QString, int> > activeModList = m_CurrentProfile->getActiveMods(); - m_DirectoryRefresher.setMods(activeModList); + + m_DirectoryRefresher.setMods(activeModList, managedArchives()); statusBar()->show(); m_RefreshProgress->setRange(0, 100); @@ -1807,6 +1780,20 @@ void MainWindow::refreshESPList() } } +void MainWindow::refreshModList(bool saveChanges) +{ + // don't lose changes! + if (saveChanges) { + m_CurrentProfile->writeModlistNow(true); + } + ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure); + m_CurrentProfile->refreshModStatus(); + + m_ModList.notifyChange(-1); + + refreshDirectoryStructure(); +} + static bool BySortValue(const std::pair<UINT32, QTreeWidgetItem*> &LHS, const std::pair<UINT32, QTreeWidgetItem*> &RHS) { @@ -1814,6 +1801,17 @@ static bool BySortValue(const std::pair<UINT32, QTreeWidgetItem*> &LHS, const st } +template <typename InputIterator> +QStringList toStringList(InputIterator current, InputIterator end) +{ + QStringList result; + for (; current != end; ++current) { + result.append(*current); + } + return result; +} + + void MainWindow::refreshBSAList() { m_ArchivesInit = false; @@ -1849,17 +1847,9 @@ void MainWindow::refreshBSAList() m_ActiveArchives.clear(); - QFile archiveFile(m_CurrentProfile->getArchivesFileName()); - if (archiveFile.open(QIODevice::ReadOnly)) { - while (!archiveFile.atEnd()) { - m_ActiveArchives.append(QString::fromUtf8(archiveFile.readLine())); - } - archiveFile.close(); - - for (int i = 0; i < m_ActiveArchives.count(); ++i) { - m_ActiveArchives[i] = m_ActiveArchives[i].trimmed(); - } - } else { + auto iter = managedArchives(); + m_ActiveArchives = toStringList(iter.begin(), iter.end()); + if (m_ActiveArchives.isEmpty()) { m_ActiveArchives = m_DefaultArchives; } @@ -1878,6 +1868,9 @@ void MainWindow::refreshBSAList() continue; } int index = m_ActiveArchives.indexOf(filename); + if (index == -1) { + index = 0xFFFF; + } QStringList strings(filename); bool isArchive = false; int origin = current->getOrigin(isArchive); @@ -1886,13 +1879,12 @@ void MainWindow::refreshBSAList() newItem->setData(0, Qt::UserRole, index); newItem->setData(1, Qt::UserRole, origin); newItem->setFlags(newItem->flags() & ~Qt::ItemIsDropEnabled | Qt::ItemIsUserCheckable); -// newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); newItem->setCheckState(0, (index != -1) ? Qt::Checked : Qt::Unchecked); if (m_Settings.forceEnableCoreFiles() && m_DefaultArchives.contains(filename)) { newItem->setCheckState(0, Qt::Checked); newItem->setDisabled(true); } else { - newItem->setCheckState(0, (index != -1) ? Qt::Checked : Qt::Unchecked); + newItem->setCheckState(0, (index != 0xFFFF) ? Qt::Checked : Qt::Unchecked); } if (index < 0) index = 0; @@ -1917,7 +1909,6 @@ void MainWindow::refreshBSAList() ui->bsaList->addTopLevelItem(subItem); } subItem->addChild(iter->second); - //ui->bsaList->addTopLevelItem(iter->second); } checkBSAList(); @@ -1945,7 +1936,7 @@ void MainWindow::checkBSAList() item->setIcon(0, QIcon(":/MO/gui/warning")); item->setToolTip(0, tr("This bsa is enabled in the ini file so it may be required!")); modWarning = true; - } else { +/* } else { QString espName = filename.mid(0, filename.length() - 3).append("esp").toLower(); QString esmName = filename.mid(0, filename.length() - 3).append("esm").toLower(); if (m_PluginList.isEnabled(espName) || m_PluginList.isEnabled(esmName)) { @@ -1953,7 +1944,7 @@ void MainWindow::checkBSAList() item->setToolTip(0, tr("This archive will still be loaded since there is a plugin of the same name but " "its files will not follow installation order!")); modWarning = true; - } + }*/ } } } @@ -2061,51 +2052,74 @@ void MainWindow::storeSettings() m_CurrentProfile->createTweakedIniFile(); saveCurrentLists(); m_Settings.setupLoadMechanism(); - QSettings settings(ToQString(GameInfo::instance().getIniFilename()), QSettings::IniFormat); - if (m_CurrentProfile != NULL) { - settings.setValue("selected_profile", m_CurrentProfile->getName().toUtf8().constData()); - } else { - settings.remove("selected_profile"); - } - settings.setValue("mod_list_state", ui->modList->header()->saveState()); - settings.setValue("plugin_list_state", ui->espList->header()->saveState()); + QString iniFile = ToQString(GameInfo::instance().getIniFilename()); + shellCopy(iniFile, iniFile + ".new", true, this); - settings.setValue("group_state", ui->groupCombo->currentIndex()); + QSettings::Status result = QSettings::NoError; + { + QSettings settings(iniFile + ".new", QSettings::IniFormat); + if (m_CurrentProfile != NULL) { + settings.setValue("selected_profile", m_CurrentProfile->getName().toUtf8().constData()); + } else { + settings.remove("selected_profile"); + } - settings.setValue("compact_downloads", ui->compactBox->isChecked()); - settings.setValue("ask_for_nexuspw", m_AskForNexusPW); + settings.setValue("mod_list_state", ui->modList->header()->saveState()); + settings.setValue("plugin_list_state", ui->espList->header()->saveState()); - settings.setValue("window_geometry", saveGeometry()); - settings.setValue("window_split", ui->splitter->saveState()); + settings.setValue("group_state", ui->groupCombo->currentIndex()); - settings.setValue("filters_visible", ui->displayCategoriesBtn->isChecked()); + settings.setValue("ask_for_nexuspw", m_AskForNexusPW); - settings.remove("customExecutables"); - settings.beginWriteArray("customExecutables"); - std::vector<Executable>::const_iterator current, end; - m_ExecutablesList.getExecutables(current, end); - int count = 0; - for (; current != end; ++current) { - const Executable &item = *current; - if (item.m_Custom || item.m_Toolbar) { - settings.setArrayIndex(count++); - settings.setValue("binary", item.m_BinaryInfo.absoluteFilePath()); - settings.setValue("title", item.m_Title); - settings.setValue("arguments", item.m_Arguments); - settings.setValue("workingDirectory", item.m_WorkingDirectory); - settings.setValue("closeOnStart", item.m_CloseMO == DEFAULT_CLOSE); - settings.setValue("steamAppID", item.m_SteamAppID); - settings.setValue("custom", item.m_Custom); - settings.setValue("toolbar", item.m_Toolbar); + settings.setValue("window_geometry", saveGeometry()); + settings.setValue("window_split", ui->splitter->saveState()); + + settings.setValue("browser_geometry", m_IntegratedBrowser.saveGeometry()); + + settings.setValue("filters_visible", ui->displayCategoriesBtn->isChecked()); + + settings.remove("customExecutables"); + settings.beginWriteArray("customExecutables"); + std::vector<Executable>::const_iterator current, end; + m_ExecutablesList.getExecutables(current, end); + int count = 0; + for (; current != end; ++current) { + const Executable &item = *current; + if (item.m_Custom || item.m_Toolbar) { + settings.setArrayIndex(count++); + settings.setValue("binary", item.m_BinaryInfo.absoluteFilePath()); + settings.setValue("title", item.m_Title); + settings.setValue("arguments", item.m_Arguments); + settings.setValue("workingDirectory", item.m_WorkingDirectory); + settings.setValue("closeOnStart", item.m_CloseMO == DEFAULT_CLOSE); + settings.setValue("steamAppID", item.m_SteamAppID); + settings.setValue("custom", item.m_Custom); + settings.setValue("toolbar", item.m_Toolbar); + } } - } - settings.endArray(); + settings.endArray(); - QComboBox *executableBox = findChild<QComboBox*>("executablesListBox"); - settings.setValue("selected_executable", executableBox->currentIndex()); + QComboBox *executableBox = findChild<QComboBox*>("executablesListBox"); + settings.setValue("selected_executable", executableBox->currentIndex()); - FileDialogMemory::save(m_Settings.directInterface()); + FileDialogMemory::save(settings); + + settings.sync(); + result = settings.status(); + } + if (result == QSettings::NoError) { + if (!shellRename(iniFile + ".new", iniFile, true, this)) { + QMessageBox::critical(this, tr("Failed to write settings"), + tr("An error occured trying to write back MO settings: %1").arg(windowsErrorString(::GetLastError()))); + } + } else { + QString reason = result == QSettings::AccessError ? tr("File is write protected") + : result == QSettings::FormatError ? tr("Invalid file format (probably a bug)") + : tr("Unknown error %1").arg(result); + QMessageBox::critical(this, tr("Failed to write settings"), + tr("An error occured trying to write back MO settings: %1").arg(reason)); + } } @@ -2133,172 +2147,6 @@ void MainWindow::on_tabWidget_currentChanged(int index) } } - -void MainWindow::installMod(const QString &fileName) -{ - if (m_CurrentProfile == NULL) { - return; - } - - bool hasIniTweaks = false; - GuessedValue<QString> modName; - m_CurrentProfile->writeModlistNow(); - m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); - if (m_InstallationManager.install(fileName, modName, hasIniTweaks)) { - MessageDialog::showMessage(tr("Installation successful"), this); - refreshModList(); - - QModelIndexList posList = m_ModList.match(m_ModList.index(0, 0), Qt::DisplayRole, static_cast<const QString&>(modName)); - if (posList.count() == 1) { - ui->modList->scrollTo(posList.at(0)); - } - int modIndex = ModInfo::getIndex(modName); - if (modIndex != UINT_MAX) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - if (hasIniTweaks && - (QMessageBox::question(this, tr("Configure Mod"), - tr("This mod contains ini tweaks. Do you want to configure them now?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { - displayModInformation(modInfo, modIndex, ModInfoDialog::TAB_INIFILES); - } - testExtractBSA(modIndex); - } else { - reportError(tr("mod \"%1\" not found").arg(modName)); - } - } else if (m_InstallationManager.wasCancelled()) { - QMessageBox::information(this, tr("Installation cancelled"), tr("The mod was not installed completely."), QMessageBox::Ok); - } -} - -QString MainWindow::resolvePath(const QString &fileName) const -{ - if (m_DirectoryStructure == NULL) { - return QString(); - } - const FileEntry::Ptr file = m_DirectoryStructure->searchFile(ToWString(fileName), NULL); - if (file.get() != NULL) { - return ToQString(file->getFullPath()); - } else { - return QString(); - } -} - -QStringList MainWindow::listDirectories(const QString &directoryName) const -{ - QStringList result; - DirectoryEntry *dir = m_DirectoryStructure->findSubDirectoryRecursive(ToWString(directoryName)); - if (dir != NULL) { - std::vector<DirectoryEntry*>::iterator current, end; - dir->getSubDirectories(current, end); - for (; current != end; ++current) { - result.append(ToQString((*current)->getName())); - } - } - return result; -} - -QStringList MainWindow::findFiles(const QString &path, const std::function<bool(const QString&)> &filter) const -{ - QStringList result; - DirectoryEntry *dir = m_DirectoryStructure->findSubDirectoryRecursive(ToWString(path)); - if (dir != NULL) { - std::vector<FileEntry::Ptr> files = dir->getFiles(); - foreach (FileEntry::Ptr file, files) { - if (filter(ToQString(file->getFullPath()))) { - result.append(ToQString(file->getFullPath())); - } - } - } else { - qWarning("directory %s not found", qPrintable(path)); - } - return result; -} - -QList<IOrganizer::FileInfo> MainWindow::findFileInfos(const QString &path, const std::function<bool (const IOrganizer::FileInfo &)> &filter) const -{ - QList<IOrganizer::FileInfo> result; - DirectoryEntry *dir = m_DirectoryStructure->findSubDirectoryRecursive(ToWString(path)); - if (dir != NULL) { - std::vector<FileEntry::Ptr> files = dir->getFiles(); - foreach (FileEntry::Ptr file, files) { - FileInfo info; - info.filePath = ToQString(file->getFullPath()); - bool fromArchive = false; - info.origins.append(ToQString(m_DirectoryStructure->getOriginByID(file->getOrigin(fromArchive)).getName())); - info.archive = fromArchive ? ToQString(file->getArchive()) : ""; - foreach (int idx, file->getAlternatives()) { - info.origins.append(ToQString(m_DirectoryStructure->getOriginByID(idx).getName())); - } - - if (filter(info)) { - result.append(info); - } - } - } else { - qWarning("directory %s not found", qPrintable(path)); - } - return result; -} - -IDownloadManager *MainWindow::downloadManager() -{ - return &m_DownloadManager; -} - -IPluginList *MainWindow::pluginList() -{ - return &m_PluginList; -} - -IModList *MainWindow::modList() -{ - return &m_ModList; -} - -HANDLE MainWindow::startApplication(const QString &executable, const QStringList &args, const QString &cwd, const QString &profile) -{ - QFileInfo binary; - QString arguments = args.join(" "); - QString currentDirectory = cwd; - QString profileName = (profile.length() > 0) ? profile : m_CurrentProfile->getName(); - QString steamAppID; - if (executable.contains('\\') || executable.contains('/')) { - // file path - binary = QFileInfo(executable); - if (binary.isRelative()) { - // relative path, should be relative to game directory - binary = QFileInfo(QDir::fromNativeSeparators(ToQString(GameInfo::instance().getGameDirectory())) + "/" + executable); - } - if (cwd.length() == 0) { - currentDirectory = binary.absolutePath(); - } - } else { - // only a file name, search executables list - try { - const Executable &exe = m_ExecutablesList.find(executable); - steamAppID = exe.m_SteamAppID; - if (arguments == "") { - arguments = exe.m_Arguments; - } - binary = exe.m_BinaryInfo; - if (cwd.length() == 0) { - currentDirectory = exe.m_WorkingDirectory; - } - } catch (const std::runtime_error&) { - qWarning("\"%s\" not set up as executable", executable.toUtf8().constData()); - binary = QFileInfo(executable); - } - } - - return spawnBinaryDirect(binary, arguments, profileName, currentDirectory, steamAppID); -} - -bool MainWindow::onAboutToRun(const std::function<bool (const QString &)> &func) -{ - auto conn = m_AboutToRun.connect(func); - return conn.connected(); -} - std::vector<unsigned int> MainWindow::activeProblems() const { std::vector<unsigned int> problems; @@ -2377,6 +2225,109 @@ void MainWindow::installMod() } } +IModInterface *MainWindow::installMod(const QString &fileName) +{ + if (m_CurrentProfile == NULL) { + return NULL; + } + + bool hasIniTweaks = false; + GuessedValue<QString> modName; + m_CurrentProfile->writeModlistNow(); + m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); + if (m_InstallationManager.install(fileName, modName, hasIniTweaks)) { + MessageDialog::showMessage(tr("Installation successful"), this); + refreshModList(); + + QModelIndexList posList = m_ModList.match(m_ModList.index(0, 0), Qt::DisplayRole, static_cast<const QString&>(modName)); + if (posList.count() == 1) { + ui->modList->scrollTo(posList.at(0)); + } + int modIndex = ModInfo::getIndex(modName); + if (modIndex != UINT_MAX) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + if (hasIniTweaks && + (QMessageBox::question(this, tr("Configure Mod"), + tr("This mod contains ini tweaks. Do you want to configure them now?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { + displayModInformation(modInfo, modIndex, ModInfoDialog::TAB_INIFILES); + } + m_ModInstalled(modName); + return modInfo.data(); + } else { + reportError(tr("mod \"%1\" not found").arg(modName)); + } + } else if (m_InstallationManager.wasCancelled()) { + QMessageBox::information(this, tr("Installation cancelled"), tr("The mod was not installed completely."), QMessageBox::Ok); + } + return NULL; +} + +IModInterface *MainWindow::getMod(const QString &name) +{ + unsigned int index = ModInfo::getIndex(name); + if (index == UINT_MAX) { + return NULL; + } else { + return ModInfo::getByIndex(index).data(); + } +} + +IModInterface *MainWindow::createMod(GuessedValue<QString> &name) +{ + if (!m_InstallationManager.testOverwrite(name)) { + return NULL; + } + + m_InstallationManager.setModsDirectory(m_Settings.getModDirectory()); + + QString targetDirectory = QDir::fromNativeSeparators(m_Settings.getModDirectory()).append("/").append(name); + + QSettings settingsFile(targetDirectory.mid(0).append("/meta.ini"), QSettings::IniFormat); + + settingsFile.setValue("modid", 0); + settingsFile.setValue("version", ""); + settingsFile.setValue("newestVersion", ""); + settingsFile.setValue("category", 0); + settingsFile.setValue("installationFile", ""); + return ModInfo::createFrom(QDir(targetDirectory), &m_DirectoryStructure).data(); +} + +bool MainWindow::removeMod(IModInterface *mod) +{ + unsigned int index = ModInfo::getIndex(mod->name()); + if (index == UINT_MAX) { + return mod->remove(); + } else { + return ModInfo::removeMod(index); + } +} + +QList<IOrganizer::FileInfo> MainWindow::findFileInfos(const QString &path, const std::function<bool (const IOrganizer::FileInfo &)> &filter) const +{ + QList<IOrganizer::FileInfo> result; + DirectoryEntry *dir = m_DirectoryStructure->findSubDirectoryRecursive(ToWString(path)); + if (dir != NULL) { + std::vector<FileEntry::Ptr> files = dir->getFiles(); + foreach (FileEntry::Ptr file, files) { + IOrganizer::FileInfo info; + info.filePath = ToQString(file->getFullPath()); + bool fromArchive = false; + info.origins.append(ToQString(m_DirectoryStructure->getOriginByID(file->getOrigin(fromArchive)).getName())); + info.archive = fromArchive ? ToQString(file->getArchive()) : ""; + foreach (int idx, file->getAlternatives()) { + info.origins.append(ToQString(m_DirectoryStructure->getOriginByID(idx).getName())); + } + + if (filter(info)) { + result.append(info); + } + } + } else { + qDebug("directory %s not found", qPrintable(path)); + } + return result; +} void MainWindow::on_startButton_clicked() { @@ -2580,12 +2531,6 @@ void MainWindow::setESPListSorting(int index) } -void MainWindow::setCompactDownloads(bool compact) -{ - ui->compactBox->setChecked(compact); -} - - bool MainWindow::queryLogin(QString &username, QString &password) { CredentialsDialog dialog(this); @@ -2640,16 +2585,12 @@ void MainWindow::refresher_progress(int percent) void MainWindow::directory_refreshed() { - statusBar()->hide(); - DirectoryEntry *newStructure = m_DirectoryRefresher.getDirectoryStructure(); + Q_ASSERT(newStructure != m_DirectoryStructure); if (newStructure != NULL) { - DirectoryEntry *oldStructure = m_DirectoryStructure; - m_DirectoryStructure = newStructure; - delete oldStructure; - + std::swap(m_DirectoryStructure, newStructure); + delete newStructure; refreshDataTree(); - refreshLists(); } else { // TODO: don't know why this happens, this slot seems to get called twice with only one emit return; @@ -2668,6 +2609,7 @@ void MainWindow::directory_refreshed() ModInfo::Ptr modInfo = ModInfo::getByIndex(i); modInfo->clearCaches(); } + statusBar()->hide(); } @@ -2685,7 +2627,10 @@ void MainWindow::modStatusChanged(unsigned int index) try { ModInfo::Ptr modInfo = ModInfo::getByIndex(index); if (m_CurrentProfile->modEnabled(index)) { - DirectoryRefresher::addModToStructure(m_DirectoryStructure, modInfo->name(), m_CurrentProfile->getModPriority(index), modInfo->absolutePath()); + m_DirectoryRefresher.addModToStructure(m_DirectoryStructure + , modInfo->name() + , m_CurrentProfile->getModPriority(index) + , modInfo->absolutePath()); DirectoryRefresher::cleanStructure(m_DirectoryStructure); } else { if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { @@ -2744,11 +2689,10 @@ void MainWindow::procFinished(int, QProcess::ExitStatus) } -void MainWindow::on_profileRefreshBtn_clicked() +void MainWindow::profileRefresh() { m_CurrentProfile->writeModlist(); -// m_ModList.updateModCollection(); refreshModList(); } @@ -3103,6 +3047,22 @@ void MainWindow::reinstallMod_clicked() } +void MainWindow::resumeDownload(int downloadIndex) +{ + if (NexusInterface::instance()->getAccessManager()->loggedIn()) { + m_DownloadManager.resumeDownload(downloadIndex); + } else { + QString username, password; + if (m_Settings.getNexusLogin(username, password)) { + m_PostLoginTasks.push_back(boost::bind(&MainWindow::resumeDownload, _1, downloadIndex)); + NexusInterface::instance()->getAccessManager()->login(username, password); + } else { + MessageDialog::showMessage(tr("You need to be logged in with Nexus to resume a download"), this); + } + } +} + + void MainWindow::endorseMod(ModInfo::Ptr mod) { if (NexusInterface::instance()->getAccessManager()->loggedIn()) { @@ -3199,9 +3159,9 @@ void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int index, FilesOrigin& origin = m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())); origin.enable(false); - DirectoryRefresher::addModToStructure(m_DirectoryStructure, - modInfo->name(), m_CurrentProfile->getModPriority(index), - modInfo->absolutePath()); + m_DirectoryRefresher.addModToStructure(m_DirectoryStructure, + modInfo->name(), m_CurrentProfile->getModPriority(index), + modInfo->absolutePath()); DirectoryRefresher::cleanStructure(m_DirectoryStructure); refreshLists(); } @@ -3266,54 +3226,6 @@ void MainWindow::displayModInformation(int row, int tab) } -void MainWindow::testExtractBSA(int modIndex) -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - QDir dir(modInfo->absolutePath()); - - QFileInfoList archives = dir.entryInfoList(QStringList("*.bsa")); - if (archives.length() != 0 && - (QuestionBoxMemory::query(this, "unpackBSA", tr("Extract BSA"), - tr("This mod contains at least one BSA. Do you want to unpack it?\n" - "(This removes the BSA after completion. If you don't know about BSAs, just select no)"), - QDialogButtonBox::Yes | QDialogButtonBox::No, QDialogButtonBox::No) == QMessageBox::Yes)) { - - - foreach (QFileInfo archiveInfo, archives) { - BSA::Archive archive; - BSA::EErrorCode result = archive.read(archiveInfo.absoluteFilePath().toLocal8Bit().constData()); - if ((result != BSA::ERROR_NONE) && (result != BSA::ERROR_INVALIDHASHES)) { - reportError(tr("failed to read %1: %2").arg(archiveInfo.fileName()).arg(result)); - return; - } - - QProgressDialog progress(this); - progress.setMaximum(100); - progress.setValue(0); - progress.show(); - - archive.extractAll(modInfo->absolutePath().toLocal8Bit().constData(), - boost::bind(&MainWindow::extractProgress, this, boost::ref(progress), _1, _2), - false); - - if (result == BSA::ERROR_INVALIDHASHES) { - reportError(tr("This archive contains invalid hashes. Some files may be broken.")); - } - - archive.close(); - - if (!QFile::remove(archiveInfo.absoluteFilePath())) { - qCritical("failed to remove archive %s", archiveInfo.absoluteFilePath().toUtf8().constData()); - } else { - m_DirectoryStructure->removeFile(ToWString(archiveInfo.fileName())); - } - } - - refreshBSAList(); - } -} - - void MainWindow::ignoreMissingData_clicked() { ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); @@ -3478,13 +3390,13 @@ void MainWindow::replaceCategoriesFromMenu(QMenu *menu, int modRow) } } -void MainWindow::addRemoveCategoriesFromMenu(QMenu *menu, int modRow) +void MainWindow::addRemoveCategoriesFromMenu(QMenu *menu, int modRow, int referenceRow) { - if (m_ContextRow != -1 && m_ContextRow != modRow) { - ModInfo::Ptr editedModInfo = ModInfo::getByIndex(m_ContextRow); + if (referenceRow != -1 && referenceRow != modRow) { + ModInfo::Ptr editedModInfo = ModInfo::getByIndex(referenceRow); foreach (QAction* action, menu->actions()) { if (action->menu() != NULL) { - addRemoveCategoriesFromMenu(action->menu(), modRow); + addRemoveCategoriesFromMenu(action->menu(), modRow, referenceRow); } else { QWidgetAction *widgetAction = qobject_cast<QWidgetAction*>(action); if (widgetAction != NULL) { @@ -3501,7 +3413,6 @@ void MainWindow::addRemoveCategoriesFromMenu(QMenu *menu, int modRow) } } } else { - //This block shouldn't be reached, but if it is then fall back to replace (context row is invalid or replacing edited mod) replaceCategoriesFromMenu(menu, modRow); } } @@ -3513,37 +3424,26 @@ void MainWindow::addRemoveCategories_MenuHandler() { return; } - QModelIndexList selected = ui->modList->selectionModel()->selectedRows(); + QModelIndexList selectedTemp = ui->modList->selectionModel()->selectedRows(); + QList<QPersistentModelIndex> selected; + foreach (const QModelIndex &idx, selectedTemp) { + selected.append(QPersistentModelIndex(idx)); + } if (selected.size() > 0) { - int min = INT_MAX; - int max = INT_MIN; - - QStringList selectedMods; - for (int i = 0; i < selected.size(); ++i) { - QModelIndex temp = mapToModel(&m_ModList, selected.at(i)); - selectedMods.append(temp.data().toString()); - if (temp.row() < min) min = temp.row(); - if (temp.row() > max) max = temp.row(); - // save the currently selected mod for last... then we can use it as a pattern for what is changing... - int modRow = m_ModListSortProxy->mapToSource(selected.at(i)).row(); - if (modRow != m_ContextRow) { - addRemoveCategoriesFromMenu(menu,modRow); + foreach (const QPersistentModelIndex &idx, selected) { + qDebug("change categories on: %s (ref: %s)", qPrintable(idx.data().toString()), qPrintable(m_ContextIdx.data().toString())); + QModelIndex modIdx = mapToModel(&m_ModList, idx); + if (modIdx.row() != m_ContextIdx.row()) { + addRemoveCategoriesFromMenu(menu, modIdx.row(), m_ContextIdx.row()); } } - //come back to the currently selected mod, after the others have been set - replaceCategoriesFromMenu(menu, m_ContextRow); + replaceCategoriesFromMenu(menu, m_ContextIdx.row()); m_ModList.notifyChange(-1); - // find mods by their name because indices are invalidated - QAbstractItemModel *model = ui->modList->model(); - Q_FOREACH(const QString &mod, selectedMods) { - QModelIndexList matches = model->match(model->index(0, 0), Qt::DisplayRole, mod, 1, - Qt::MatchFixedString | Qt::MatchCaseSensitive | Qt::MatchRecursive); - if (matches.size() > 0) { - ui->modList->selectionModel()->select(matches.at(0), QItemSelectionModel::Select | QItemSelectionModel::Rows); - } + foreach (const QPersistentModelIndex &idx, selected) { + ui->modList->selectionModel()->select(idx, QItemSelectionModel::Select | QItemSelectionModel::Rows); } } else { //For single mod selections, just do a replace @@ -3564,15 +3464,10 @@ void MainWindow::replaceCategories_MenuHandler() { QModelIndexList selected = ui->modList->selectionModel()->selectedRows(); if (selected.size() > 0) { - int min = INT_MAX; - int max = INT_MIN; - QStringList selectedMods; for (int i = 0; i < selected.size(); ++i) { QModelIndex temp = mapToModel(&m_ModList, selected.at(i)); selectedMods.append(temp.data().toString()); - if (temp.row() < min) min = temp.row(); - if (temp.row() > max) max = temp.row(); replaceCategoriesFromMenu(menu, mapToModel(&m_ModList, selected.at(i)).row()); } @@ -3789,102 +3684,113 @@ void addMenuAsPushButton(QMenu *menu, QMenu *subMenu) menu->addAction(action); } -void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) +QMenu *MainWindow::modListContextMenu() { - try { - QTreeView *modList = findChild<QTreeView*>("modList"); + QMenu *menu = new QMenu(); + menu->addAction(tr("Install Mod..."), this, SLOT(installMod_clicked())); - QModelIndex index = mapToModel(&m_ModList, modList->indexAt(pos)); - m_ContextRow = index.row(); + menu->addAction(tr("Enable all visible"), this, SLOT(enableVisibleMods())); + menu->addAction(tr("Disable all visible"), this, SLOT(disableVisibleMods())); - QMenu menu; + menu->addAction(tr("Check all for update"), this, SLOT(checkModsForUpdates())); - menu.addAction(tr("Install Mod..."), this, SLOT(installMod_clicked())); + menu->addAction(tr("Refresh"), this, SLOT(profileRefresh())); - menu.addAction(tr("Enable all visible"), this, SLOT(enableVisibleMods())); - menu.addAction(tr("Disable all visible"), this, SLOT(disableVisibleMods())); - - menu.addAction(tr("Check all for update"), this, SLOT(checkModsForUpdates())); + menu->addAction(tr("Export to csv..."), this, SLOT(exportModListCSV())); + return menu; +} - menu.addAction(tr("Refresh"), this, SLOT(on_profileRefreshBtn_clicked())); +void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) +{ + try { + QTreeView *modList = findChild<QTreeView*>("modList"); - menu.addAction(tr("Export to csv..."), this, SLOT(exportModListCSV())); + m_ContextIdx = mapToModel(&m_ModList, modList->indexAt(pos)); + m_ContextRow = m_ContextIdx.row(); - if (m_ContextRow != -1) { - menu.addSeparator(); + QMenu *menu = NULL; + QMenu *allMods = modListContextMenu(); + if (m_ContextRow == -1) { + // no selection + menu = allMods; + menu->setParent(this); + } else { + menu = new QMenu(this); + allMods->setTitle(tr("All Mods")); + menu->addMenu(allMods); ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); std::vector<ModInfo::EFlag> flags = info->getFlags(); if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) { if (QDir(info->absolutePath()).count() > 2) { - menu.addAction(tr("Sync to Mods..."), this, SLOT(syncOverwrite())); - menu.addAction(tr("Create Mod..."), this, SLOT(createModFromOverwrite())); + menu->addAction(tr("Sync to Mods..."), this, SLOT(syncOverwrite())); + menu->addAction(tr("Create Mod..."), this, SLOT(createModFromOverwrite())); } } else if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) != flags.end()) { - menu.addAction(tr("Restore Backup"), this, SLOT(restoreBackup_clicked())); - menu.addAction(tr("Remove Backup..."), this, SLOT(removeMod_clicked())); + menu->addAction(tr("Restore Backup"), this, SLOT(restoreBackup_clicked())); + menu->addAction(tr("Remove Backup..."), this, SLOT(removeMod_clicked())); } else { QMenu *addRemoveCategoriesMenu = new QMenu(tr("Add/Remove Categories")); populateMenuCategories(addRemoveCategoriesMenu, 0); connect(addRemoveCategoriesMenu, SIGNAL(aboutToHide()), this, SLOT(addRemoveCategories_MenuHandler())); - addMenuAsPushButton(&menu, addRemoveCategoriesMenu); + addMenuAsPushButton(menu, addRemoveCategoriesMenu); QMenu *replaceCategoriesMenu = new QMenu(tr("Replace Categories")); populateMenuCategories(replaceCategoriesMenu, 0); connect(replaceCategoriesMenu, SIGNAL(aboutToHide()), this, SLOT(replaceCategories_MenuHandler())); - addMenuAsPushButton(&menu, replaceCategoriesMenu); + addMenuAsPushButton(menu, replaceCategoriesMenu); QMenu *primaryCategoryMenu = new QMenu(tr("Primary Category")); connect(primaryCategoryMenu, SIGNAL(aboutToShow()), this, SLOT(addPrimaryCategoryCandidates())); connect(primaryCategoryMenu, SIGNAL(aboutToHide()), this, SLOT(savePrimaryCategory())); - addMenuAsPushButton(&menu, primaryCategoryMenu); + addMenuAsPushButton(menu, primaryCategoryMenu); - menu.addSeparator(); + menu->addSeparator(); if (info->downgradeAvailable()) { - menu.addAction(tr("Change versioning scheme"), this, SLOT(changeVersioningScheme())); + menu->addAction(tr("Change versioning scheme"), this, SLOT(changeVersioningScheme())); } if (info->updateAvailable() || info->downgradeAvailable()) { if (info->updateIgnored()) { - menu.addAction(tr("Un-ignore update"), this, SLOT(unignoreUpdate())); + menu->addAction(tr("Un-ignore update"), this, SLOT(unignoreUpdate())); } else { - menu.addAction(tr("Ignore update"), this, SLOT(ignoreUpdate())); + menu->addAction(tr("Ignore update"), this, SLOT(ignoreUpdate())); } } - menu.addSeparator(); + menu->addSeparator(); - menu.addAction(tr("Rename Mod..."), this, SLOT(renameMod_clicked())); - menu.addAction(tr("Remove Mod..."), this, SLOT(removeMod_clicked())); - menu.addAction(tr("Reinstall Mod"), this, SLOT(reinstallMod_clicked())); + menu->addAction(tr("Rename Mod..."), this, SLOT(renameMod_clicked())); + menu->addAction(tr("Remove Mod..."), this, SLOT(removeMod_clicked())); + menu->addAction(tr("Reinstall Mod"), this, SLOT(reinstallMod_clicked())); switch (info->endorsedState()) { case ModInfo::ENDORSED_TRUE: { - menu.addAction(tr("Un-Endorse"), this, SLOT(unendorse_clicked())); + menu->addAction(tr("Un-Endorse"), this, SLOT(unendorse_clicked())); } break; case ModInfo::ENDORSED_FALSE: { - menu.addAction(tr("Endorse"), this, SLOT(endorse_clicked())); - menu.addAction(tr("Won't endorse"), this, SLOT(dontendorse_clicked())); + menu->addAction(tr("Endorse"), this, SLOT(endorse_clicked())); + menu->addAction(tr("Won't endorse"), this, SLOT(dontendorse_clicked())); } break; case ModInfo::ENDORSED_NEVER: { - menu.addAction(tr("Endorse"), this, SLOT(endorse_clicked())); + menu->addAction(tr("Endorse"), this, SLOT(endorse_clicked())); } break; default: { - QAction *action = new QAction(tr("Endorsement state unknown"), &menu); + QAction *action = new QAction(tr("Endorsement state unknown"), menu); action->setEnabled(false); - menu.addAction(action); + menu->addAction(action); } break; } std::vector<ModInfo::EFlag> flags = info->getFlags(); if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_INVALID) != flags.end()) { - menu.addAction(tr("Ignore missing data"), this, SLOT(ignoreMissingData_clicked())); + menu->addAction(tr("Ignore missing data"), this, SLOT(ignoreMissingData_clicked())); } - menu.addAction(tr("Visit on Nexus"), this, SLOT(visitOnNexus_clicked())); - menu.addAction(tr("Open in explorer"), this, SLOT(openExplorer_clicked())); + menu->addAction(tr("Visit on Nexus"), this, SLOT(visitOnNexus_clicked())); + menu->addAction(tr("Open in explorer"), this, SLOT(openExplorer_clicked())); } - QAction *infoAction = menu.addAction(tr("Information..."), this, SLOT(information_clicked())); - menu.setDefaultAction(infoAction); + QAction *infoAction = menu->addAction(tr("Information..."), this, SLOT(information_clicked())); + menu->setDefaultAction(infoAction); } - menu.exec(modList->mapToGlobal(pos)); + menu->exec(modList->mapToGlobal(pos)); } catch (const std::exception &e) { reportError(tr("Exception: ").arg(e.what())); } catch (...) { @@ -3895,7 +3801,6 @@ void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) void MainWindow::on_categoriesList_itemSelectionChanged() { -// int filter = current->data(0, Qt::UserRole).toInt(); QModelIndexList indices = ui->categoriesList->selectionModel()->selectedRows(); std::vector<int> categories; foreach (const QModelIndex &index, indices) { @@ -3906,7 +3811,7 @@ void MainWindow::on_categoriesList_itemSelectionChanged() } m_ModListSortProxy->setCategoryFilter(categories); -// ui->currentCategoryLabel->setText(QString("(%1)").arg(current->text(0))); + ui->clickBlankLabel->setEnabled(categories.size() > 0); if (indices.count() == 0) { ui->currentCategoryLabel->setText(QString("(%1)").arg(tr("<All>"))); } else if (indices.count() > 1) { @@ -4066,16 +3971,18 @@ void MainWindow::linkDesktop() reportError(tr("failed to remove %1").arg(linkName)); } } else { - QFileInfo exeInfo(m_ExeName); + QFileInfo exeInfo(qApp->arguments().at(0)); // create link std::wstring targetFile = ToWString(exeInfo.absoluteFilePath()); - std::wstring parameter = ToWString(QString("\"") + selectedExecutable.m_BinaryInfo.absoluteFilePath() + "\" " + selectedExecutable.m_Arguments); + std::wstring parameter = ToWString(QString("\"%1\" %2").arg(QDir::toNativeSeparators(selectedExecutable.m_BinaryInfo.absoluteFilePath())) + .arg(selectedExecutable.m_Arguments)); std::wstring description = ToWString(selectedExecutable.m_BinaryInfo.fileName()); - std::wstring currentDirectory = ToWString(selectedExecutable.m_BinaryInfo.absolutePath()); - - if (CreateShortcut(targetFile.c_str(), parameter.c_str(), - linkName.toUtf8().constData(), - description.c_str(), currentDirectory.c_str()) != E_INVALIDARG) { + std::wstring currentDirectory = ToWString(QDir::toNativeSeparators(exeInfo.absolutePath())); + if (CreateShortcut(targetFile.c_str() + , parameter.c_str() + , linkName.toUtf8().constData() + , description.c_str() + , currentDirectory.c_str()) != E_INVALIDARG) { ui->linkButton->menu()->actions().at(0)->setIcon(QIcon(":/MO/gui/remove")); } else { reportError(tr("failed to create %1").arg(linkName)); @@ -4097,12 +4004,13 @@ void MainWindow::linkMenu() reportError(tr("failed to remove %1").arg(linkName)); } } else { - QFileInfo exeInfo(m_ExeName); + QFileInfo exeInfo(qApp->arguments().at(0)); // create link std::wstring targetFile = ToWString(exeInfo.absoluteFilePath()); - std::wstring parameter = ToWString(QString("\"") + selectedExecutable.m_BinaryInfo.absoluteFilePath() + "\" " + selectedExecutable.m_Arguments); + std::wstring parameter = ToWString(QString("\"%1\" %2").arg(QDir::toNativeSeparators(selectedExecutable.m_BinaryInfo.absoluteFilePath())) + .arg(selectedExecutable.m_Arguments)); std::wstring description = ToWString(selectedExecutable.m_BinaryInfo.fileName()); - std::wstring currentDirectory = ToWString(selectedExecutable.m_BinaryInfo.absolutePath()); + std::wstring currentDirectory = ToWString(QDir::toNativeSeparators(exeInfo.absolutePath())); if (CreateShortcut(targetFile.c_str(), parameter.c_str(), linkName.toUtf8().constData(), @@ -4153,6 +4061,8 @@ void MainWindow::on_actionSettings_triggered() } NexusInterface::instance()->setNMMVersion(m_Settings.getNMMVersion()); + + updateDownloadListDelegate(); } @@ -4175,17 +4085,30 @@ void MainWindow::linkClicked(const QString &url) } -void MainWindow::downloadRequestedNXM(const QString &url) +bool MainWindow::nexusLogin() { QString username, password; - qDebug("download requested: %s", qPrintable(url)); - if (!m_LoginAttempted && !NexusInterface::instance()->getAccessManager()->loggedIn() && - (m_Settings.getNexusLogin(username, password) || - (m_AskForNexusPW && queryLogin(username, password)))) { + NXMAccessManager *accessManager = NexusInterface::instance()->getAccessManager(); + + if (!accessManager->loginAttempted() + && !accessManager->loggedIn() + && (m_Settings.getNexusLogin(username, password) + || (m_AskForNexusPW + && queryLogin(username, password)))) { + accessManager->login(username, password); + return true; + } else { + return false; + } +} + + +void MainWindow::downloadRequestedNXM(const QString &url) +{ + qDebug("download requested: %s", qPrintable(url)); + if (nexusLogin()) { m_PendingDownloads.append(url); - NexusInterface::instance()->getAccessManager()->login(username, password); - m_LoginAttempted = true; } else { m_DownloadManager.addNXMDownload(url); } @@ -4195,7 +4118,7 @@ void MainWindow::downloadRequestedNXM(const QString &url) void MainWindow::downloadRequested(QNetworkReply *reply, int modID, const QString &fileName) { try { - if (m_DownloadManager.addDownload(reply, QStringList(), fileName, modID)) { + if (m_DownloadManager.addDownload(reply, QStringList(), fileName, modID, 0, new ModRepositoryFileInfo(modID))) { MessageDialog::showMessage(tr("Download started"), this); } } catch (const std::exception &e) { @@ -4210,7 +4133,9 @@ void MainWindow::installTranslator(const QString &name) QTranslator *translator = new QTranslator(this); QString fileName = name + "_" + m_CurrentLanguage; if (!translator->load(fileName, qApp->applicationDirPath() + "/translations")) { - qWarning("localization file %s not found", qPrintable(fileName)); + if (m_CurrentLanguage != "en-US") { + qWarning("localization file %s not found", qPrintable(fileName)); + } // we don't actually expect localization files for english } qApp->installTranslator(translator); m_Translators.push_back(translator); @@ -4291,7 +4216,8 @@ void MainWindow::installDownload(int index) QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { displayModInformation(modInfo, modIndex, ModInfoDialog::TAB_INIFILES); } - testExtractBSA(modIndex); + + m_ModInstalled(modName); } else { reportError(tr("mod \"%1\" not found").arg(modName)); } @@ -4642,13 +4568,7 @@ void MainWindow::on_conflictsCheckBox_toggled(bool) void MainWindow::on_actionUpdate_triggered() { - QString username, password; - - if (!m_LoginAttempted && !NexusInterface::instance()->getAccessManager()->loggedIn() && - (m_Settings.getNexusLogin(username, password) || - (m_AskForNexusPW && queryLogin(username, password)))) { - NexusInterface::instance()->getAccessManager()->login(username, password); - m_LoginAttempted = true; + if (nexusLogin()) { m_PostLoginTasks.push_back([&](MainWindow*) { m_Updater.startUpdate(); }); } else { m_Updater.startUpdate(); @@ -4661,17 +4581,19 @@ void MainWindow::on_actionEndorseMO_triggered() if (QMessageBox::question(this, tr("Endorse Mod Organizer"), tr("Do you want to endorse Mod Organizer on %1 now?").arg(ToQString(GameInfo::instance().getNexusPage())), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - NexusInterface::instance()->requestToggleEndorsement(GameInfo::instance().getNexusModID(), true, this, QVariant()); + NexusInterface::instance()->requestToggleEndorsement(GameInfo::instance().getNexusModID(), true, this, QVariant(), QString()); } } void MainWindow::updateDownloadListDelegate() { - if (ui->compactBox->isChecked()) { - ui->downloadView->setItemDelegate(new DownloadListWidgetCompactDelegate(&m_DownloadManager, ui->downloadView, ui->downloadView)); + if (m_Settings.compactDownloads()) { + ui->downloadView->setItemDelegate(new DownloadListWidgetCompactDelegate(&m_DownloadManager, m_Settings.metaDownloads(), + ui->downloadView, ui->downloadView)); } else { - ui->downloadView->setItemDelegate(new DownloadListWidgetDelegate(&m_DownloadManager, ui->downloadView, ui->downloadView)); + ui->downloadView->setItemDelegate(new DownloadListWidgetDelegate(&m_DownloadManager, m_Settings.metaDownloads(), + ui->downloadView, ui->downloadView)); } DownloadListSortProxy *sortProxy = new DownloadListSortProxy(&m_DownloadManager, ui->downloadView); @@ -4689,13 +4611,7 @@ void MainWindow::updateDownloadListDelegate() connect(ui->downloadView->itemDelegate(), SIGNAL(restoreDownload(int)), &m_DownloadManager, SLOT(restoreDownload(int))); connect(ui->downloadView->itemDelegate(), SIGNAL(cancelDownload(int)), &m_DownloadManager, SLOT(cancelDownload(int))); connect(ui->downloadView->itemDelegate(), SIGNAL(pauseDownload(int)), &m_DownloadManager, SLOT(pauseDownload(int))); - connect(ui->downloadView->itemDelegate(), SIGNAL(resumeDownload(int)), &m_DownloadManager, SLOT(resumeDownload(int))); -} - - -void MainWindow::on_compactBox_toggled(bool) -{ - updateDownloadListDelegate(); + connect(ui->downloadView->itemDelegate(), SIGNAL(resumeDownload(int)), this, SLOT(resumeDownload(int))); } @@ -4755,19 +4671,6 @@ void MainWindow::nxmUpdatesAvailable(const std::vector<int> &modIDs, QVariant us } } -/* -void MainWindow::nxmEndorsementToggled(int, QVariant, QVariant resultData, int) -{ - if (resultData.toBool()) { - ui->actionEndorseMO->setVisible(false); - QMessageBox::question(this, tr("Thank you!"), tr("Thank you for your endorsement!")); - } - - if (!disconnect(sender(), SIGNAL(nxmEndorsementToggled(int, QVariant, QVariant, int)), - this, SLOT(nxmEndorsementToggled(int, QVariant, QVariant, int)))) { - qCritical("failed to disconnect endorsement slot"); - } -}*/ void MainWindow::nxmDownloadURLs(int, int, QVariant, QVariant resultData, int) { @@ -4813,6 +4716,7 @@ void MainWindow::loginSuccessful(bool necessary) } m_PostLoginTasks.clear(); + NexusInterface::instance()->loginCompleted(); } @@ -4838,6 +4742,7 @@ void MainWindow::loginFailed(const QString &message) m_PostLoginTasks.clear(); statusBar()->hide(); } + NexusInterface::instance()->loginCompleted(); } @@ -5024,10 +4929,16 @@ void MainWindow::editCategories() } } +void MainWindow::deselectFilters() +{ + ui->categoriesList->clearSelection(); +} + void MainWindow::on_categoriesList_customContextMenuRequested(const QPoint &pos) { QMenu menu; menu.addAction(tr("Edit Categories..."), this, SLOT(editCategories())); + menu.addAction(tr("Deselect filter"), this, SLOT(deselectFilters())); menu.exec(ui->categoriesList->mapToGlobal(pos)); } @@ -5060,8 +4971,13 @@ void MainWindow::unlockESPIndex() void MainWindow::removeFromToolbar() { - Executable &exe = m_ExecutablesList.find(m_ContextAction->text()); - exe.m_Toolbar = false; + try { + Executable &exe = m_ExecutablesList.find(m_ContextAction->text()); + exe.m_Toolbar = false; + } catch (const std::runtime_error&) { + qDebug("executable doesn't exist any more"); + } + updateToolBar(); } @@ -5069,11 +4985,13 @@ void MainWindow::removeFromToolbar() void MainWindow::toolBar_customContextMenuRequested(const QPoint &point) { QAction *action = ui->toolBar->actionAt(point); - if (action->objectName().startsWith("custom_")) { - m_ContextAction = action; - QMenu menu; - menu.addAction(tr("Remove"), this, SLOT(removeFromToolbar())); - menu.exec(ui->toolBar->mapToGlobal(point)); + if (action != NULL) { + if (action->objectName().startsWith("custom_")) { + m_ContextAction = action; + QMenu menu; + menu.addAction(tr("Remove"), this, SLOT(removeFromToolbar())); + menu.exec(ui->toolBar->mapToGlobal(point)); + } } } @@ -5143,10 +5061,10 @@ void MainWindow::on_groupCombo_currentIndexChanged(int index) connect(ui->modList, SIGNAL(expanded(QModelIndex)),newModel, SLOT(expanded(QModelIndex))); connect(ui->modList, SIGNAL(collapsed(QModelIndex)), newModel, SLOT(collapsed(QModelIndex))); connect(newModel, SIGNAL(expandItem(QModelIndex)), this, SLOT(expandModList(QModelIndex))); - } else { m_ModListSortProxy->setSourceModel(&m_ModList); } + modFilterActive(m_ModListSortProxy->isFilterActive()); } void MainWindow::on_linkButton_pressed() @@ -5169,20 +5087,326 @@ void MainWindow::on_showHiddenBox_toggled(bool checked) m_DownloadManager.setShowHidden(checked); } + +void MainWindow::createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite) +{ + SECURITY_ATTRIBUTES secAttributes; + secAttributes.nLength = sizeof(SECURITY_ATTRIBUTES); + secAttributes.bInheritHandle = TRUE; + secAttributes.lpSecurityDescriptor = NULL; + + if (!::CreatePipe(stdOutRead, stdOutWrite, &secAttributes, 0)) { + qCritical("failed to create stdout reroute"); + } + + if (!::SetHandleInformation(*stdOutRead, HANDLE_FLAG_INHERIT, 0)) { + qCritical("failed to correctly set up the stdout reroute"); + *stdOutWrite = *stdOutRead = INVALID_HANDLE_VALUE; + } +} + +std::string MainWindow::readFromPipe(HANDLE stdOutRead) +{ + static const int chunkSize = 128; + std::string result; + + char buffer[chunkSize + 1]; + buffer[chunkSize] = '\0'; + + DWORD read = 1; + while (read > 0) { + if (!::ReadFile(stdOutRead, buffer, chunkSize, &read, NULL)) { + break; + } + if (read > 0) { + result.append(buffer, read); + if (read < chunkSize) { + break; + } + } + } + return result; +} + +void MainWindow::processLOOTOut(const std::string &lootOut, std::string &reportURL, std::string &errorMessages, QProgressDialog &dialog) +{ + std::vector<std::string> lines; + boost::split(lines, lootOut, boost::is_any_of("\r\n")); + foreach (const std::string &line, lines) { + if (line.length() > 0) { + size_t progidx = line.find("[progress]"); + size_t reportidx = line.find("[report]"); + size_t erroridx = line.find("[error]"); + if (progidx != std::string::npos) { + dialog.setLabelText(line.substr(progidx + 11).c_str()); + } else if (reportidx != std::string::npos) { + reportURL = line.substr(reportidx + 9); + } else if (erroridx != std::string::npos) { + qWarning("%s", line.c_str()); + errorMessages.append(boost::algorithm::trim_copy(line.substr(erroridx + 8)) + "\n"); + } else { + qDebug("%s", line.c_str()); + } + } + } +} + + +HANDLE MainWindow::startApplication(const QString &executable, const QStringList &args, const QString &cwd, const QString &profile) +{ + QFileInfo binary; + QString arguments = args.join(" "); + QString currentDirectory = cwd; + QString profileName = profile; + if (profile.length() == 0) { + if (m_CurrentProfile != NULL) { + profileName = m_CurrentProfile->getName(); + } else { + throw MyException(tr("No profile set")); + } + } + QString steamAppID; + if (executable.contains('\\') || executable.contains('/')) { + // file path + binary = QFileInfo(executable); + if (binary.isRelative()) { + // relative path, should be relative to game directory + binary = QFileInfo(QDir::fromNativeSeparators(ToQString(GameInfo::instance().getGameDirectory())) + "/" + executable); + } + if (cwd.length() == 0) { + currentDirectory = binary.absolutePath(); + } + } else { + // only a file name, search executables list + try { + const Executable &exe = m_ExecutablesList.find(executable); + steamAppID = exe.m_SteamAppID; + if (arguments == "") { + arguments = exe.m_Arguments; + } + binary = exe.m_BinaryInfo; + if (cwd.length() == 0) { + currentDirectory = exe.m_WorkingDirectory; + } + } catch (const std::runtime_error&) { + qWarning("\"%s\" not set up as executable", executable.toUtf8().constData()); + binary = QFileInfo(executable); + } + } + + return spawnBinaryDirect(binary, arguments, profileName, currentDirectory, steamAppID); +} + void MainWindow::on_bossButton_clicked() { + std::string reportURL; + std::string errorMessages; + + m_CurrentProfile->writeModlistNow(); + + bool success = false; + try { this->setEnabled(false); ON_BLOCK_EXIT([&] () { this->setEnabled(true); }); - LockedDialog dialog(this, tr("BOSS working"), false); + QProgressDialog dialog(this); + dialog.setLabelText(tr("LOOT working")); + dialog.setMaximum(0); dialog.show(); - qApp->processEvents(); - m_PluginList.bossSort(); - savePluginList(); - dialog.hide(); + QStringList parameters; + parameters << "--game" << ToQString(GameInfo::instance().getGameShortName()) + << "--gamePath" << QString("\"%1\"").arg(ToQString(GameInfo::instance().getGameDirectory())); + + HANDLE stdOutWrite = INVALID_HANDLE_VALUE; + HANDLE stdOutRead = INVALID_HANDLE_VALUE; + createStdoutPipe(&stdOutRead, &stdOutWrite); + HANDLE loot = startBinary(QFileInfo(qApp->applicationDirPath() + "/loot/lootcli.exe"), + parameters.join(" "), + m_CurrentProfile->getName(), + m_Settings.logLevel(), + qApp->applicationDirPath() + "/loot", + true, + stdOutWrite); + + // we don't use the write end + ::CloseHandle(stdOutWrite); + + if (loot != INVALID_HANDLE_VALUE) { + while (::WaitForSingleObject(loot, 100) == WAIT_TIMEOUT) { + // keep processing events so the app doesn't appear dead + QCoreApplication::processEvents(); + if (dialog.wasCanceled()) { + ::TerminateProcess(loot, 1); + } + std::string lootOut = readFromPipe(stdOutRead); + processLOOTOut(lootOut, reportURL, errorMessages, dialog); + } + std::string remainder = readFromPipe(stdOutRead).c_str(); + if (remainder.length() > 0) { + processLOOTOut(remainder, reportURL, errorMessages, dialog); + } + DWORD exitCode = 0UL; + ::GetExitCodeProcess(loot, &exitCode); + if (exitCode != 0UL) { + reportError(tr("loot failed. Exit code was: %1").arg(exitCode)); + return; + } else { + success = true; + } + } } catch (const std::exception &e) { - reportError(tr("failed to run boss: %1").arg(e.what())); - ui->bossButton->setEnabled(false); + reportError(tr("failed to run loot: %1").arg(e.what())); + } + if (errorMessages.length() > 0) { + QMessageBox *warn = new QMessageBox(QMessageBox::Warning, tr("Errors occured"), errorMessages.c_str(), QMessageBox::Ok, this); + warn->setModal(false); + warn->show(); + } + + if (success) { + if (reportURL.length() > 0) { + m_IntegratedBrowser.setWindowTitle("LOOT Report"); + QString report(reportURL.c_str()); + if (QFile::exists(report)) { + m_IntegratedBrowser.openUrl(QUrl::fromLocalFile(report)); + } else { + qWarning("report file missing"); + } + } + refreshESPList(); + + if (GameInfo::instance().getLoadOrderMechanism() == GameInfo::TYPE_FILETIME) { + QFile::remove(m_CurrentProfile->getLoadOrderFileName()); + } + } +} + + +const char *MainWindow::PATTERN_BACKUP_GLOB = ".????_??_??_??_??_??"; +const char *MainWindow::PATTERN_BACKUP_REGEX = "\\.(\\d\\d\\d\\d_\\d\\d_\\d\\d_\\d\\d_\\d\\d_\\d\\d)"; +const char *MainWindow::PATTERN_BACKUP_DATE = "yyyy_MM_dd_hh_mm_ss"; + + +bool MainWindow::createBackup(const QString &filePath, const QDateTime &time) +{ + QString outPath = filePath + "." + time.toString(PATTERN_BACKUP_DATE); + if (shellCopy(QStringList(filePath), QStringList(outPath), this)) { + QFileInfo fileInfo(filePath); + removeOldFiles(fileInfo.absolutePath(), fileInfo.fileName() + PATTERN_BACKUP_GLOB, 3, QDir::Name); + return true; + } else { + return false; + } +} + +void MainWindow::on_saveButton_clicked() +{ + savePluginList(); + QDateTime now = QDateTime::currentDateTime(); + if (createBackup(m_CurrentProfile->getPluginsFileName(), now) + && createBackup(m_CurrentProfile->getLoadOrderFileName(), now) + && createBackup(m_CurrentProfile->getLockedOrderFileName(), now)) { + MessageDialog::showMessage(tr("Backup of load order created"), this); } } + +QString MainWindow::queryRestore(const QString &filePath) +{ + QFileInfo pluginFileInfo(filePath); + QString pattern = pluginFileInfo.fileName() + ".*"; + QFileInfoList files = pluginFileInfo.absoluteDir().entryInfoList(QStringList(pattern), QDir::Files, QDir::Name); + + SelectionDialog dialog(tr("Choose backup to restore"), this); + QRegExp exp(pluginFileInfo.fileName() + PATTERN_BACKUP_REGEX); + QRegExp exp2(pluginFileInfo.fileName() + "\\.(.*)"); + foreach(const QFileInfo &info, files) { + if (exp.exactMatch(info.fileName())) { + QDateTime time = QDateTime::fromString(exp.cap(1), PATTERN_BACKUP_DATE); + dialog.addChoice(time.toString(), "", exp.cap(1)); + } else if (exp2.exactMatch(info.fileName())) { + dialog.addChoice(exp2.cap(1), "", exp2.cap(1)); + } + } + + if (dialog.numChoices() == 0) { + QMessageBox::information(this, tr("No Backups"), tr("There are no backups to restore")); + return QString(); + } + + if (dialog.exec() == QDialog::Accepted) { + return dialog.getChoiceData().toString(); + } else { + return QString(); + } +} + +void MainWindow::on_restoreButton_clicked() +{ + QString pluginName = m_CurrentProfile->getPluginsFileName(); + QString choice = queryRestore(pluginName); + if (!choice.isEmpty()) { + QString loadOrderName = m_CurrentProfile->getLoadOrderFileName(); + QString lockedName = m_CurrentProfile->getLockedOrderFileName(); + if (!shellCopy(pluginName + "." + choice, pluginName, true, this) || + !shellCopy(loadOrderName + "." + choice, loadOrderName, true, this) || + !shellCopy(lockedName + "." + choice, lockedName, true, this)) { + QMessageBox::critical(this, tr("Restore failed"), + tr("Failed to restore the backup. Errorcode: %1").arg(windowsErrorString(::GetLastError()))); + } + refreshESPList(); + } +} + +void MainWindow::on_saveModsButton_clicked() +{ + m_CurrentProfile->writeModlistNow(true); + QDateTime now = QDateTime::currentDateTime(); + if (createBackup(m_CurrentProfile->getModlistFileName(), now)) { + MessageDialog::showMessage(tr("Backup of modlist created"), this); + } +} +void MainWindow::on_restoreModsButton_clicked() +{ + QString modlistName = m_CurrentProfile->getModlistFileName(); + QString choice = queryRestore(modlistName); + if (!choice.isEmpty()) { + if (!shellCopy(modlistName + "." + choice, modlistName, true, this)) { + QMessageBox::critical(this, tr("Restore failed"), + tr("Failed to restore the backup. Errorcode: %1").arg(windowsErrorString(::GetLastError()))); + } + refreshModList(false); + } +} + +void MainWindow::on_actionCopy_Log_to_Clipboard_triggered() +{ + QStringList lines; + QAbstractItemModel *model = ui->logList->model(); + for (int i = 0; i < model->rowCount(); ++i) { + lines.append(QString("%1 [%2] %3").arg(model->index(i, 0).data().toString()) + .arg(model->index(i, 1).data(Qt::UserRole).toString()) + .arg(model->index(i, 1).data().toString())); + } + QApplication::clipboard()->setText(lines.join("\n")); +} + +void MainWindow::on_categoriesAndBtn_toggled(bool checked) +{ + if (checked) { + m_ModListSortProxy->setFilterMode(ModListSortProxy::FILTER_AND); + } +} + +void MainWindow::on_categoriesOrBtn_toggled(bool checked) +{ + if (checked) { + m_ModListSortProxy->setFilterMode(ModListSortProxy::FILTER_OR); + } +} + +void MainWindow::on_managedArchiveLabel_linkHovered(const QString&) +{ + QToolTip::showText(QCursor::pos(), + ui->managedArchiveLabel->toolTip()); +} diff --git a/src/mainwindow.h b/src/mainwindow.h index 250f2a48..bbbbea2f 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -39,6 +39,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <imoinfo.h> #include <iplugintool.h> #include <iplugindiagnose.h> +#include <ipluginmodpage.h> #include "settings.h" #include "downloadmanager.h" #include "installationmanager.h" @@ -49,6 +50,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "tutorialcontrol.h" #include "savegameinfowidgetgamebryo.h" #include "previewgenerator.h" +#include "browserdialog.h" #include <guessedvalue.h> #include <directoryentry.h> #include <boost/signals2.hpp> @@ -62,11 +64,13 @@ class ModListSortProxy; class ModListGroupCategoriesProxy; -class MainWindow : public QMainWindow, public MOBase::IOrganizer, public MOBase::IPluginDiagnose +class MainWindow : public QMainWindow, public MOBase::IPluginDiagnose { Q_OBJECT Q_INTERFACES(MOBase::IPluginDiagnose) + friend class OrganizerProxy; + private: struct SignalCombinerAnd @@ -86,6 +90,7 @@ private: }; typedef boost::signals2::signal<bool (const QString&), SignalCombinerAnd> SignalAboutToRunApplication; + typedef boost::signals2::signal<void (const QString&)> SignalModInstalled; public: explicit MainWindow(const QString &exeName, QSettings &initSettings, QWidget *parent = 0); @@ -95,7 +100,6 @@ public: bool addProfile(); void refreshLists(); - void refreshESPList(); void refreshBSAList(); void refreshDataTree(); void refreshSaveList(); @@ -104,7 +108,6 @@ public: void setModListSorting(int index); void setESPListSorting(int index); - void setCompactDownloads(bool compact); bool setCurrentProfile(int index); bool setCurrentProfile(const QString &name); @@ -116,34 +119,6 @@ public: void loadPlugins(); - virtual MOBase::IGameInfo &gameInfo() const; - virtual MOBase::IModRepositoryBridge *createNexusBridge() const; - virtual QString profileName() const; - virtual QString profilePath() const; - virtual QString downloadsPath() const; - virtual MOBase::VersionInfo appVersion() const; - virtual MOBase::IModInterface *getMod(const QString &name); - virtual MOBase::IModInterface *createMod(MOBase::GuessedValue<QString> &name); - virtual bool removeMod(MOBase::IModInterface *mod); - virtual void modDataChanged(MOBase::IModInterface *mod); - virtual QVariant pluginSetting(const QString &pluginName, const QString &key) const; - virtual void setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value); - virtual QVariant persistent(const QString &pluginName, const QString &key, const QVariant &def = QVariant()) const; - virtual void setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync = true); - virtual QString pluginDataPath() const; - virtual void installMod(const QString &fileName); - virtual QString resolvePath(const QString &fileName) const; - virtual QStringList listDirectories(const QString &directoryName) const; - virtual QStringList findFiles(const QString &path, const std::function<bool(const QString &)> &filter) const; - virtual QList<FileInfo> findFileInfos(const QString &path, const std::function<bool(const FileInfo&)> &filter) const; - - virtual MOBase::IDownloadManager *downloadManager(); - virtual MOBase::IPluginList *pluginList(); - virtual MOBase::IModList *modList(); - virtual HANDLE startApplication(const QString &executable, const QStringList &args = QStringList(), const QString &cwd = "", const QString &profile = ""); - virtual bool onAboutToRun(const std::function<bool(const QString&)> &func); - virtual void refreshModList(bool saveChanges = true); - virtual std::vector<unsigned int> activeProblems() const; virtual QString shortDescription(unsigned int key) const; virtual QString fullDescription(unsigned int key) const; @@ -154,6 +129,12 @@ public: void saveArchiveList(); + void createStdoutPipe(HANDLE *stdOutRead, HANDLE *stdOutWrite); + std::string readFromPipe(HANDLE stdOutRead); + void processLOOTOut(const std::string &lootOut, std::string &reportURL, std::string &errorMessages, QProgressDialog &dialog); + + HANDLE startApplication(const QString &executable, const QStringList &args = QStringList(), const QString &cwd = "", const QString &profile = ""); + public slots: void displayColumnSelection(const QPoint &pos); @@ -164,6 +145,7 @@ public slots: void directory_refreshed(); void toolPluginInvoke(); + void modPagePluginInvoke(); signals: @@ -195,9 +177,13 @@ protected: private: + void refreshESPList(); + void refreshModList(bool saveChanges = true); + void actionToToolButton(QAction *&sourceAction); bool verifyPlugin(MOBase::IPlugin *plugin); void registerPluginTool(MOBase::IPluginTool *tool); + void registerModPage(MOBase::IPluginModPage *modPage); bool registerPlugin(QObject *pluginObj, const QString &fileName); void updateToolBar(); @@ -205,10 +191,6 @@ private: void setExecutableIndex(int index); - bool nexusLogin(); - - void saveCurrentESPList(); - bool testForSteam(); void startSteam(); @@ -220,6 +202,13 @@ private: bool refreshProfiles(bool selectProfile = true); void refreshExecutablesList(); void installMod(); + MOBase::IModInterface *installMod(const QString &fileName); + MOBase::IModInterface *getMod(const QString &name); + MOBase::IModInterface *createMod(MOBase::GuessedValue<QString> &name); + bool removeMod(MOBase::IModInterface *mod); + + QList<MOBase::IOrganizer::FileInfo> findFileInfos(const QString &path, const std::function<bool (const MOBase::IOrganizer::FileInfo &)> &filter) const; + bool modifyExecutablesDialog(); void displayModInformation(ModInfo::Ptr modInfo, unsigned int index, int tab); void displayModInformation(int row, int tab = 0); @@ -236,8 +225,9 @@ private: * the changes made in the menu (which is the delta between the current menu selection and the reference mod) * @param menu the menu after editing by the user * @param modRow index of the mod to edit + * @param referenceRow row of the reference mod */ - void addRemoveCategoriesFromMenu(QMenu *menu, int modRow); + void addRemoveCategoriesFromMenu(QMenu *menu, int modRow, int referenceRow); /** * Sets category selections from menu; for multiple mods, this will completely @@ -262,7 +252,7 @@ private: bool extractProgress(QProgressDialog &extractProgress, int percentage, std::string fileName); - bool checkForProblems(); + int checkForProblems(); int getBinaryExecuteInfo(const QFileInfo &targetInfo, QFileInfo &binaryInfo, QString &arguments); QTreeWidgetItem *addFilterItem(QTreeWidgetItem *root, const QString &name, int categoryID); @@ -288,12 +278,24 @@ private: static void setupNetworkProxy(bool activate); void activateProxy(bool activate); void installTranslator(const QString &name); + void setBrowserGeometry(const QByteArray &geometry); + + bool createBackup(const QString &filePath, const QDateTime &time); + QString queryRestore(const QString &filePath); + + QMenu *modListContextMenu(); + + std::set<QString> managedArchives(); private: static const unsigned int PROBLEM_PLUGINSNOTLOADED = 1; static const unsigned int PROBLEM_TOOMANYPLUGINS = 2; + static const char *PATTERN_BACKUP_GLOB; + static const char *PATTERN_BACKUP_REGEX; + static const char *PATTERN_BACKUP_DATE; + private: Ui::MainWindow *ui; @@ -324,6 +326,7 @@ private: QString m_GamePath; int m_ContextRow; + QPersistentModelIndex m_ContextIdx; QTreeWidgetItem *m_ContextItem; QAction *m_ContextAction; @@ -360,21 +363,26 @@ private: MOBase::IGameInfo *m_GameInfo; std::vector<MOBase::IPluginDiagnose*> m_DiagnosisPlugins; + std::vector<MOBase::IPluginModPage*> m_ModPages; std::vector<QString> m_UnloadedPlugins; QFile m_PluginsCheck; SignalAboutToRunApplication m_AboutToRun; + SignalModInstalled m_ModInstalled; QString m_CurrentLanguage; std::vector<QTranslator*> m_Translators; PreviewGenerator m_PreviewGenerator; + BrowserDialog m_IntegratedBrowser; QFileSystemWatcher m_SavesWatcher; std::vector<QTreeWidgetItem*> m_RemoveWidget; + uint m_ArchiveListHash; + private slots: void showMessage(const QString &message); @@ -440,6 +448,8 @@ private slots: void linkClicked(const QString &url); + bool nexusLogin(); + void loginSuccessful(bool necessary); void loginSuccessfulUpdate(bool necessary); void loginFailed(const QString &message); @@ -471,6 +481,7 @@ private slots: void nxmRequestFailed(int modID, int fileID, QVariant userData, int requestID, const QString &errorString); void editCategories(); + void deselectFilters(); void displayModInformation(const QString &modName, int tab); void modOpenNext(); @@ -483,6 +494,7 @@ private slots: void hookUpWindowTutorials(); + void resumeDownload(int downloadIndex); void endorseMod(ModInfo::Ptr mod); void cancelModListEditor(); @@ -536,6 +548,9 @@ private slots: void about(); void delayedRemove(); + void requestDownload(const QUrl &url, QNetworkReply *reply); + void profileRefresh(); + private slots: // ui slots // actions void on_actionAdd_Profile_triggered(); @@ -551,14 +566,12 @@ private slots: // ui slots void bsaList_itemMoved(); void on_btnRefreshData_clicked(); void on_categoriesList_customContextMenuRequested(const QPoint &pos); - void on_compactBox_toggled(bool checked); void on_conflictsCheckBox_toggled(bool checked); void on_dataTree_customContextMenuRequested(const QPoint &pos); void on_executablesListBox_currentIndexChanged(int index); void on_modList_customContextMenuRequested(const QPoint &pos); void on_modList_doubleClicked(const QModelIndex &index); void on_profileBox_currentIndexChanged(int index); - void on_profileRefreshBtn_clicked(); void on_savegameList_customContextMenuRequested(const QPoint &pos); void on_startButton_clicked(); void on_tabWidget_currentChanged(int index); @@ -571,6 +584,17 @@ private slots: // ui slots void on_showHiddenBox_toggled(bool checked); void on_bsaList_itemChanged(QTreeWidgetItem *item, int column); void on_bossButton_clicked(); + + void on_saveButton_clicked(); + void on_restoreButton_clicked(); + void on_restoreModsButton_clicked(); + void on_saveModsButton_clicked(); + void on_actionCopy_Log_to_Clipboard_triggered(); + void on_categoriesAndBtn_toggled(bool checked); + void on_categoriesOrBtn_toggled(bool checked); + void on_managedArchiveLabel_linkHovered(const QString &link); }; + + #endif // MAINWINDOW_H diff --git a/src/mainwindow.ui b/src/mainwindow.ui index 5d02a6f5..1bb70d64 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -27,623 +27,247 @@ <verstretch>0</verstretch>
</sizepolicy>
</property>
- <layout class="QHBoxLayout" name="horizontalLayout_8" stretch="0,1">
- <property name="spacing">
- <number>4</number>
- </property>
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>6</number>
- </property>
+ <layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
- <layout class="QVBoxLayout" name="verticalLayout_8">
- <item>
- <widget class="QGroupBox" name="categoriesGroup">
- <property name="title">
- <string>Categories</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout_10">
- <property name="leftMargin">
- <number>3</number>
- </property>
- <property name="topMargin">
- <number>7</number>
- </property>
- <property name="rightMargin">
- <number>3</number>
- </property>
- <property name="bottomMargin">
- <number>1</number>
- </property>
- <item>
- <widget class="QTreeWidget" name="categoriesList">
- <property name="minimumSize">
- <size>
- <width>100</width>
- <height>0</height>
- </size>
- </property>
- <property name="maximumSize">
- <size>
- <width>161</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::ExtendedSelection</enum>
- </property>
- <attribute name="headerVisible">
- <bool>false</bool>
- </attribute>
- <column>
- <property name="text">
- <string notr="true">1</string>
- </property>
- </column>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QSplitter" name="splitter">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
+ <widget class="QSplitter" name="topLevelSplitter">
<property name="orientation">
- <enum>Qt::Horizontal</enum>
+ <enum>Qt::Vertical</enum>
</property>
- <widget class="QWidget" name="layoutWidget">
- <layout class="QVBoxLayout" name="verticalLayout">
- <property name="spacing">
- <number>2</number>
- </property>
+ <widget class="QWidget" name="horizontalLayoutWidget_2">
+ <layout class="QHBoxLayout" name="horizontalLayout_10" stretch="0,2">
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_6">
- <item>
- <widget class="QLabel" name="label_3">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Profile</string>
- </property>
- <property name="buddy">
- <cstring>profileBox</cstring>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="profileBox">
- <property name="toolTip">
- <string>Pick a module collection</string>
- </property>
- <property name="whatsThis">
- <string><!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:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Create profiles here. Each profile contains its own list of active mods and esps. This way you can quickly switch between setups for different play throughs.</span></p>
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Please note that right now your esp load order is not kept seperate for different profiles.</span></p></body></html></string>
- </property>
- </widget>
- </item>
+ <layout class="QVBoxLayout" name="verticalLayout_8">
<item>
- <widget class="QPushButton" name="profileRefreshBtn">
- <property name="maximumSize">
- <size>
- <width>24</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="toolTip">
- <string>Refresh list</string>
- </property>
- <property name="whatsThis">
- <string>Refresh list. This is usually not necessary unless you modified data outside the program.</string>
- </property>
- <property name="text">
- <string/>
+ <widget class="QGroupBox" name="categoriesGroup">
+ <property name="title">
+ <string>Categories</string>
</property>
- <property name="icon">
- <iconset>
- <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <widget class="QGroupBox" name="groupBox">
- <property name="title">
- <string>Mods</string>
- </property>
- <layout class="QVBoxLayout" name="verticalLayout_6">
- <property name="leftMargin">
- <number>4</number>
- </property>
- <property name="topMargin">
- <number>4</number>
- </property>
- <property name="rightMargin">
- <number>4</number>
- </property>
- <property name="bottomMargin">
- <number>4</number>
- </property>
- <item>
- <widget class="ModListView" name="modList">
- <property name="minimumSize">
- <size>
- <width>330</width>
- <height>400</height>
- </size>
- </property>
- <property name="palette">
- <palette>
- <active>
- <colorrole role="ButtonText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>64</red>
- <green>64</green>
- <blue>64</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Link">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>255</red>
- <green>0</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="LinkVisited">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>0</red>
- <green>170</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- </active>
- <inactive>
- <colorrole role="ButtonText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>64</red>
- <green>64</green>
- <blue>64</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Link">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>255</red>
- <green>0</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="LinkVisited">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>0</red>
- <green>170</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- </inactive>
- <disabled>
- <colorrole role="ButtonText">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>120</red>
- <green>120</green>
- <blue>120</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="Link">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>255</red>
- <green>0</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- <colorrole role="LinkVisited">
- <brush brushstyle="SolidPattern">
- <color alpha="255">
- <red>0</red>
- <green>170</green>
- <blue>0</blue>
- </color>
- </brush>
- </colorrole>
- </disabled>
- </palette>
+ <layout class="QVBoxLayout" name="verticalLayout_10">
+ <property name="leftMargin">
+ <number>3</number>
</property>
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
+ <property name="topMargin">
+ <number>7</number>
</property>
- <property name="toolTip">
- <string>List of available mods.</string>
+ <property name="rightMargin">
+ <number>3</number>
</property>
- <property name="whatsThis">
- <string>This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag & drop mods to change their "installation" orders.</string>
+ <property name="bottomMargin">
+ <number>1</number>
</property>
- <property name="styleSheet">
- <string notr="true"/>
- </property>
- <property name="editTriggers">
- <set>QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
- </property>
- <property name="showDropIndicator" stdset="0">
- <bool>true</bool>
- </property>
- <property name="dragEnabled">
- <bool>true</bool>
- </property>
- <property name="dragDropMode">
- <enum>QAbstractItemView::DragDrop</enum>
- </property>
- <property name="defaultDropAction">
- <enum>Qt::MoveAction</enum>
- </property>
- <property name="alternatingRowColors">
- <bool>true</bool>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::ExtendedSelection</enum>
- </property>
- <property name="selectionBehavior">
- <enum>QAbstractItemView::SelectRows</enum>
- </property>
- <property name="indentation">
- <number>20</number>
- </property>
- <property name="itemsExpandable">
- <bool>true</bool>
- </property>
- <property name="sortingEnabled">
- <bool>true</bool>
- </property>
- <property name="expandsOnDoubleClick">
- <bool>false</bool>
- </property>
- <attribute name="headerDefaultSectionSize">
- <number>20</number>
- </attribute>
- <attribute name="headerShowSortIndicator" stdset="0">
- <bool>true</bool>
- </attribute>
- <attribute name="headerStretchLastSection">
- <bool>false</bool>
- </attribute>
- </widget>
- </item>
- </layout>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_4" stretch="0,0,1,1,1">
- <item>
- <widget class="QPushButton" name="displayCategoriesBtn">
- <property name="maximumSize">
- <size>
- <width>20</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="text">
- <string notr="true">x</string>
- </property>
- <property name="iconSize">
- <size>
- <width>20</width>
- <height>20</height>
- </size>
- </property>
- <property name="checkable">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="label_2">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="text">
- <string>Filter</string>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="currentCategoryLabel">
- <property name="font">
- <font>
- <pointsize>8</pointsize>
- <italic>true</italic>
- </font>
- </property>
- <property name="text">
- <string/>
- </property>
- </widget>
- </item>
- <item>
- <widget class="QComboBox" name="groupCombo">
- <item>
- <property name="text">
- <string>No groups</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Categories</string>
- </property>
- </item>
- <item>
- <property name="text">
- <string>Nexus IDs</string>
- </property>
- </item>
- </widget>
- </item>
- <item>
- <widget class="MOBase::LineEditClear" name="modFilterEdit">
- <property name="placeholderText">
- <string>Namefilter</string>
- </property>
- </widget>
- </item>
- </layout>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="layoutWidget_2">
- <layout class="QVBoxLayout" name="verticalLayout_2">
- <item>
- <widget class="QFrame" name="startGroup">
- <layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0">
- <item>
- <widget class="QComboBox" name="executablesListBox">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>0</width>
- <height>40</height>
- </size>
- </property>
- <property name="font">
- <font>
- <pointsize>9</pointsize>
- <weight>75</weight>
- <bold>true</bold>
- </font>
- </property>
- <property name="toolTip">
- <string>Pick a program to run.</string>
- </property>
- <property name="whatsThis">
- <string><!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:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Choose the program to run. Once you start using ModOrganizer, you should always run your game and tools from here or through shortcuts created here, otherwise mods installed through MO will not be visible.</span></p>
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">You can add new Tools to this list, but I can't promise tools I haven't tested will work.</span></p></body></html></string>
- </property>
- <property name="iconSize">
- <size>
- <width>32</width>
- <height>32</height>
- </size>
- </property>
- <property name="frame">
- <bool>false</bool>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QVBoxLayout" name="verticalLayout_12" stretch="0,0">
<item>
- <widget class="QPushButton" name="startButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
+ <widget class="QTreeWidget" name="categoriesList">
<property name="minimumSize">
<size>
- <width>120</width>
+ <width>100</width>
<height>0</height>
</size>
</property>
<property name="maximumSize">
<size>
- <width>16777215</width>
+ <width>161</width>
<height>16777215</height>
</size>
</property>
- <property name="font">
- <font>
- <pointsize>10</pointsize>
- <weight>75</weight>
- <bold>true</bold>
- </font>
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
</property>
- <property name="toolTip">
- <string>Run program</string>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
</property>
- <property name="whatsThis">
- <string><!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:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Run the selected program with ModOrganizer enabled.</span></p></body></html></string>
+ <property name="indentation">
+ <number>0</number>
</property>
- <property name="locale">
- <locale language="English" country="UnitedStates"/>
+ <attribute name="headerVisible">
+ <bool>false</bool>
+ </attribute>
+ <column>
+ <property name="text">
+ <string notr="true">1</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ <item>
+ <widget class="QLabel" name="clickBlankLabel">
+ <property name="enabled">
+ <bool>false</bool>
</property>
<property name="text">
- <string>Run</string>
- </property>
- <property name="icon">
- <iconset>
- <normaloff>:/MO/gui/run</normaloff>:/MO/gui/run</iconset>
- </property>
- <property name="iconSize">
- <size>
- <width>36</width>
- <height>36</height>
- </size>
+ <string>Click blank area to deselect</string>
</property>
</widget>
</item>
<item>
- <widget class="QPushButton" name="linkButton">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="minimumSize">
- <size>
- <width>140</width>
- <height>0</height>
- </size>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <property name="baseSize">
- <size>
- <width>0</width>
- <height>0</height>
- </size>
- </property>
- <property name="toolTip">
- <string>Create a shortcut in your start menu or on the desktop to the specified program</string>
- </property>
- <property name="whatsThis">
- <string><!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:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This creates a start menu shortcut that directly starts the selected program with the MO active.</span></p></body></html></string>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string/>
</property>
- <property name="text">
- <string>Shortcut</string>
+ <property name="flat">
+ <bool>false</bool>
</property>
- <property name="icon">
- <iconset>
- <normaloff>:/MO/gui/link</normaloff>:/MO/gui/link</iconset>
+ <property name="checkable">
+ <bool>false</bool>
</property>
+ <layout class="QHBoxLayout" name="horizontalLayout_11">
+ <item>
+ <widget class="QRadioButton" name="categoriesAndBtn">
+ <property name="toolTip">
+ <string>If checked, only mods that match all selected categories are displayed.</string>
+ </property>
+ <property name="text">
+ <string>And</string>
+ </property>
+ <property name="checked">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QRadioButton" name="categoriesOrBtn">
+ <property name="toolTip">
+ <string>If checked, all mods that match one of the selected categories are displayed.</string>
+ </property>
+ <property name="text">
+ <string>Or</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
</widget>
</item>
</layout>
- </item>
- </layout>
- </widget>
+ </widget>
+ </item>
+ </layout>
</item>
<item>
- <widget class="QTabWidget" name="tabWidget">
- <property name="minimumSize">
- <size>
- <width>340</width>
- <height>250</height>
- </size>
+ <widget class="QSplitter" name="splitter">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
</property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
</property>
- <property name="contextMenuPolicy">
- <enum>Qt::NoContextMenu</enum>
- </property>
- <property name="tabShape">
- <enum>QTabWidget::Rounded</enum>
- </property>
- <property name="currentIndex">
- <number>0</number>
- </property>
- <widget class="QWidget" name="espTab">
- <property name="sizePolicy">
- <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
- <horstretch>0</horstretch>
- <verstretch>0</verstretch>
- </sizepolicy>
- </property>
- <property name="maximumSize">
- <size>
- <width>16777215</width>
- <height>16777215</height>
- </size>
- </property>
- <attribute name="title">
- <string>Plugins</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_4">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>0</number>
+ <widget class="QWidget" name="layoutWidget">
+ <layout class="QVBoxLayout" name="verticalLayout">
+ <property name="spacing">
+ <number>2</number>
</property>
<item>
- <widget class="QTreeView" name="espList">
+ <layout class="QHBoxLayout" name="horizontalLayout_6" stretch="0,1,0,0,0,0">
+ <item>
+ <widget class="QLabel" name="label_3">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="text">
+ <string>Profile</string>
+ </property>
+ <property name="buddy">
+ <cstring>profileBox</cstring>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="profileBox">
+ <property name="toolTip">
+ <string>Pick a module collection</string>
+ </property>
+ <property name="whatsThis">
+ <string><!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:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Create profiles here. Each profile contains its own list of active mods and esps. This way you can quickly switch between setups for different play throughs.</span></p>
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Please note that right now your esp load order is not kept seperate for different profiles.</span></p></body></html></string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="listOptionsBtn">
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="toolTip">
+ <string>Refresh list</string>
+ </property>
+ <property name="whatsThis">
+ <string>Refresh list. This is usually not necessary unless you modified data outside the program.</string>
+ </property>
+ <property name="text">
+ <string/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/settings</normaloff>:/MO/gui/settings</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>16</width>
+ <height>16</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="restoreModsButton">
+ <property name="toolTip">
+ <string>Restore Backup...</string>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/restore</normaloff>:/MO/gui/restore</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="saveModsButton">
+ <property name="toolTip">
+ <string>Create Backup</string>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/backup</normaloff>:/MO/gui/backup</iconset>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="ModListView" name="modList">
<property name="minimumSize">
<size>
- <width>250</width>
- <height>250</height>
+ <width>330</width>
+ <height>400</height>
</size>
</property>
<property name="palette">
@@ -658,6 +282,24 @@ p, li { white-space: pre-wrap; } </color>
</brush>
</colorrole>
+ <colorrole role="Link">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>255</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="LinkVisited">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>170</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
</active>
<inactive>
<colorrole role="ButtonText">
@@ -669,6 +311,24 @@ p, li { white-space: pre-wrap; } </color>
</brush>
</colorrole>
+ <colorrole role="Link">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>255</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="LinkVisited">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>170</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
</inactive>
<disabled>
<colorrole role="ButtonText">
@@ -680,6 +340,24 @@ p, li { white-space: pre-wrap; } </color>
</brush>
</colorrole>
+ <colorrole role="Link">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>255</red>
+ <green>0</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
+ <colorrole role="LinkVisited">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>0</red>
+ <green>170</green>
+ <blue>0</blue>
+ </color>
+ </brush>
+ </colorrole>
</disabled>
</palette>
</property>
@@ -687,23 +365,25 @@ p, li { white-space: pre-wrap; } <enum>Qt::CustomContextMenu</enum>
</property>
<property name="toolTip">
- <string>List of available esp/esm files</string>
+ <string>List of available mods.</string>
</property>
<property name="whatsThis">
- <string><!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:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This list contains the esps and esms contained in the active mods. These require their own load order. Use drag&amp;drop to modify this load order. Please note that MO will only save the load order for mods that are active/checked.<br />There is a great tool named &quot;BOSS&quot; to automatically sort these files.</span></p></body></html></string>
+ <string>This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag & drop mods to change their "installation" orders.</string>
</property>
- <property name="dragEnabled">
+ <property name="styleSheet">
+ <string notr="true"/>
+ </property>
+ <property name="editTriggers">
+ <set>QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked</set>
+ </property>
+ <property name="showDropIndicator" stdset="0">
<bool>true</bool>
</property>
- <property name="dragDropOverwriteMode">
- <bool>false</bool>
+ <property name="dragEnabled">
+ <bool>true</bool>
</property>
<property name="dragDropMode">
- <enum>QAbstractItemView::InternalMove</enum>
+ <enum>QAbstractItemView::DragDrop</enum>
</property>
<property name="defaultDropAction">
<enum>Qt::MoveAction</enum>
@@ -718,357 +398,814 @@ p, li { white-space: pre-wrap; } <enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="indentation">
- <number>0</number>
- </property>
- <property name="uniformRowHeights">
- <bool>true</bool>
+ <number>20</number>
</property>
<property name="itemsExpandable">
- <bool>false</bool>
+ <bool>true</bool>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
+ <property name="expandsOnDoubleClick">
+ <bool>false</bool>
+ </property>
+ <attribute name="headerDefaultSectionSize">
+ <number>20</number>
+ </attribute>
+ <attribute name="headerShowSortIndicator" stdset="0">
+ <bool>true</bool>
+ </attribute>
<attribute name="headerStretchLastSection">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
- <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <layout class="QHBoxLayout" name="horizontalLayout_4" stretch="0,0,1,1,1">
<item>
- <widget class="MOBase::LineEditClear" name="espFilterEdit">
+ <widget class="QPushButton" name="displayCategoriesBtn">
+ <property name="maximumSize">
+ <size>
+ <width>20</width>
+ <height>16777215</height>
+ </size>
+ </property>
<property name="text">
- <string/>
+ <string notr="true">x</string>
</property>
- <property name="placeholderText">
- <string>Namefilter</string>
+ <property name="iconSize">
+ <size>
+ <width>20</width>
+ <height>20</height>
+ </size>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
</property>
</widget>
</item>
<item>
- <widget class="QPushButton" name="bossButton">
+ <widget class="QLabel" name="label_2">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Minimum" vsizetype="Preferred">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
<property name="text">
- <string>Sort</string>
+ <string>Filter</string>
</property>
</widget>
</item>
- </layout>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="bsaTab">
- <attribute name="title">
- <string>Archives</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_9">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>6</number>
- </property>
- <item>
- <widget class="MOBase::SortableTreeWidget" name="bsaList">
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="toolTip">
- <string>List of available BS Archives. Archives not checked here are not managed by MO and ignore installation order.</string>
- </property>
- <property name="whatsThis">
- <string>BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they "compete" with loose files in your data directory over which is loaded.
-By default, BSAs that share their base name with an enabled ESP (i.e. plugin.esp and plugin.bsa) are automatically loaded and will have precedence over all loose files, the installation order you set up to the left is then ignored!
-
-BSAs checked here are loaded in such a way that your installation order is obeyed properly.</string>
- </property>
- <property name="editTriggers">
- <set>QAbstractItemView::NoEditTriggers</set>
- </property>
- <property name="showDropIndicator" stdset="0">
- <bool>true</bool>
- </property>
- <property name="dragEnabled">
- <bool>false</bool>
- </property>
- <property name="dragDropOverwriteMode">
- <bool>false</bool>
- </property>
- <property name="dragDropMode">
- <enum>QAbstractItemView::DragDrop</enum>
- </property>
- <property name="defaultDropAction">
- <enum>Qt::MoveAction</enum>
- </property>
- <property name="selectionMode">
- <enum>QAbstractItemView::SingleSelection</enum>
- </property>
- <property name="selectionBehavior">
- <enum>QAbstractItemView::SelectRows</enum>
- </property>
- <property name="indentation">
- <number>20</number>
- </property>
- <property name="itemsExpandable">
- <bool>true</bool>
- </property>
- <property name="columnCount">
- <number>1</number>
- </property>
- <attribute name="headerVisible">
- <bool>false</bool>
- </attribute>
- <attribute name="headerDefaultSectionSize">
- <number>200</number>
- </attribute>
- <column>
- <property name="text">
- <string>File</string>
- </property>
- </column>
- </widget>
- </item>
- <item>
- <widget class="QLabel" name="bsaWarning">
- <property name="text">
- <string><html><head/><body><p>Marked Archives (<img src=":/MO/gui/warning_16"/>) are still loaded on Skyrim but the <a href="http://forums.bethsoft.com/topic/1354395-update-bsas-and-you/"><span style=" text-decoration: underline; color:#0000ff;">regular file override</span></a> mechanism will apply: Loose files override BSAs, no matter the mod/plugin priority.</p></body></html></string>
- </property>
- <property name="wordWrap">
- <bool>true</bool>
- </property>
- </widget>
- </item>
- </layout>
- </widget>
- <widget class="QWidget" name="dataTab">
- <attribute name="title">
- <string>Data</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_5">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>6</number>
- </property>
- <item>
- <widget class="QPushButton" name="btnRefreshData">
- <property name="toolTip">
- <string>refresh data-directory overview</string>
- </property>
- <property name="whatsThis">
- <string>Refresh the overview. This may take a moment.</string>
- </property>
- <property name="text">
- <string>Refresh</string>
- </property>
- <property name="icon">
- <iconset>
- <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
- </property>
- </widget>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
- <widget class="QTreeWidget" name="dataTree">
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="whatsThis">
- <string>This is an overview of your data directory as visible to the game (and tools). </string>
+ <widget class="QLabel" name="currentCategoryLabel">
+ <property name="font">
+ <font>
+ <pointsize>8</pointsize>
+ <italic>true</italic>
+ </font>
</property>
- <property name="animated">
- <bool>true</bool>
+ <property name="text">
+ <string/>
</property>
- <attribute name="headerDefaultSectionSize">
- <number>400</number>
- </attribute>
- <column>
+ </widget>
+ </item>
+ <item>
+ <widget class="QComboBox" name="groupCombo">
+ <item>
+ <property name="text">
+ <string>No groups</string>
+ </property>
+ </item>
+ <item>
<property name="text">
- <string>File</string>
+ <string>Categories</string>
</property>
- </column>
- <column>
+ </item>
+ <item>
<property name="text">
- <string>Mod</string>
+ <string>Nexus IDs</string>
</property>
- </column>
+ </item>
+ </widget>
+ </item>
+ <item>
+ <widget class="MOBase::LineEditClear" name="modFilterEdit">
+ <property name="placeholderText">
+ <string>Namefilter</string>
+ </property>
</widget>
</item>
</layout>
</item>
- <item>
- <widget class="QCheckBox" name="conflictsCheckBox">
- <property name="toolTip">
- <string>Filter the above list so that only conflicts are displayed.</string>
- </property>
- <property name="whatsThis">
- <string>Filter the above list so that only conflicts are displayed.</string>
- </property>
- <property name="text">
- <string>Show only conflicts</string>
- </property>
- </widget>
- </item>
</layout>
</widget>
- <widget class="QWidget" name="savesTab">
- <attribute name="title">
- <string>Saves</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_3">
- <property name="leftMargin">
- <number>6</number>
- </property>
- <property name="topMargin">
- <number>6</number>
- </property>
- <property name="rightMargin">
- <number>6</number>
- </property>
- <property name="bottomMargin">
- <number>6</number>
- </property>
+ <widget class="QWidget" name="layoutWidget_2">
+ <layout class="QVBoxLayout" name="verticalLayout_2">
<item>
- <widget class="QListWidget" name="savegameList">
- <property name="contextMenuPolicy">
- <enum>Qt::CustomContextMenu</enum>
- </property>
- <property name="toolTip">
- <string notr="true"/>
- </property>
- <property name="whatsThis">
- <string><!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+ <widget class="QFrame" name="startGroup">
+ <layout class="QHBoxLayout" name="horizontalLayout_5" stretch="1,0">
+ <item>
+ <widget class="QComboBox" name="executablesListBox">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>0</width>
+ <height>40</height>
+ </size>
+ </property>
+ <property name="font">
+ <font>
+ <pointsize>9</pointsize>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="toolTip">
+ <string>Pick a program to run.</string>
+ </property>
+ <property name="whatsThis">
+ <string><!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:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This is a list of all savegames for this game. Hover over a list entry to get detailed information about the save including a list of esps/esms that were used at the time this save was created but aren't active now.</span></p>
-<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; font-size:8pt;"></p>
-<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">If you click &quot;Fix Mods...&quot; in the context menu, MO will try to activate all mods and esps to fix those missing esps. It will not disable anything!</span></p></body></html></string>
- </property>
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Choose the program to run. Once you start using ModOrganizer, you should always run your game and tools from here or through shortcuts created here, otherwise mods installed through MO will not be visible.</span></p>
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">You can add new Tools to this list, but I can't promise tools I haven't tested will work.</span></p></body></html></string>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>32</width>
+ <height>32</height>
+ </size>
+ </property>
+ <property name="frame">
+ <bool>false</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QVBoxLayout" name="verticalLayout_12" stretch="0,0">
+ <item>
+ <widget class="QPushButton" name="startButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>120</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="font">
+ <font>
+ <pointsize>10</pointsize>
+ <weight>75</weight>
+ <bold>true</bold>
+ </font>
+ </property>
+ <property name="toolTip">
+ <string>Run program</string>
+ </property>
+ <property name="whatsThis">
+ <string><!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:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">Run the selected program with ModOrganizer enabled.</span></p></body></html></string>
+ </property>
+ <property name="locale">
+ <locale language="English" country="UnitedStates"/>
+ </property>
+ <property name="text">
+ <string>Run</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/run</normaloff>:/MO/gui/run</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>36</width>
+ <height>36</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="linkButton">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Fixed" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="minimumSize">
+ <size>
+ <width>140</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="baseSize">
+ <size>
+ <width>0</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="toolTip">
+ <string>Create a shortcut in your start menu or on the desktop to the specified program</string>
+ </property>
+ <property name="whatsThis">
+ <string><!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:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This creates a start menu shortcut that directly starts the selected program with the MO active.</span></p></body></html></string>
+ </property>
+ <property name="text">
+ <string>Shortcut</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/link</normaloff>:/MO/gui/link</iconset>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
</widget>
</item>
- </layout>
- </widget>
- <widget class="QWidget" name="downloadTab">
- <attribute name="title">
- <string>Downloads</string>
- </attribute>
- <layout class="QVBoxLayout" name="verticalLayout_7">
- <property name="leftMargin">
- <number>2</number>
- </property>
- <property name="topMargin">
- <number>2</number>
- </property>
- <property name="rightMargin">
- <number>2</number>
- </property>
- <property name="bottomMargin">
- <number>2</number>
- </property>
<item>
- <layout class="QVBoxLayout" name="downloadLayout">
- <item>
- <widget class="QTreeView" name="downloadView">
- <property name="minimumSize">
- <size>
- <width>320</width>
- <height>0</height>
- </size>
+ <widget class="QTabWidget" name="tabWidget">
+ <property name="minimumSize">
+ <size>
+ <width>340</width>
+ <height>250</height>
+ </size>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <property name="contextMenuPolicy">
+ <enum>Qt::NoContextMenu</enum>
+ </property>
+ <property name="tabShape">
+ <enum>QTabWidget::Rounded</enum>
+ </property>
+ <property name="currentIndex">
+ <number>0</number>
+ </property>
+ <widget class="QWidget" name="espTab">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Expanding">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </sizepolicy>
+ </property>
+ <property name="maximumSize">
+ <size>
+ <width>16777215</width>
+ <height>16777215</height>
+ </size>
+ </property>
+ <attribute name="title">
+ <string>Plugins</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_4">
+ <property name="leftMargin">
+ <number>6</number>
</property>
- <property name="contextMenuPolicy">
- <enum>Qt::PreventContextMenu</enum>
+ <property name="topMargin">
+ <number>6</number>
</property>
- <property name="toolTip">
- <string/>
+ <property name="rightMargin">
+ <number>6</number>
</property>
- <property name="whatsThis">
- <string>This is a list of mods you downloaded from Nexus. Double click one to install it.</string>
+ <property name="bottomMargin">
+ <number>0</number>
</property>
- <property name="verticalScrollBarPolicy">
- <enum>Qt::ScrollBarAlwaysOn</enum>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_7">
+ <item>
+ <widget class="QPushButton" name="bossButton">
+ <property name="text">
+ <string>Sort</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/sort</normaloff>:/MO/gui/sort</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_2">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="QPushButton" name="restoreButton">
+ <property name="toolTip">
+ <string>Restore Backup...</string>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/restore</normaloff>:/MO/gui/restore</iconset>
+ </property>
+ <property name="iconSize">
+ <size>
+ <width>16</width>
+ <height>16</height>
+ </size>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QPushButton" name="saveButton">
+ <property name="toolTip">
+ <string>Create Backup</string>
+ </property>
+ <property name="text">
+ <string notr="true"/>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/backup</normaloff>:/MO/gui/backup</iconset>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QTreeView" name="espList">
+ <property name="minimumSize">
+ <size>
+ <width>250</width>
+ <height>250</height>
+ </size>
+ </property>
+ <property name="palette">
+ <palette>
+ <active>
+ <colorrole role="ButtonText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>64</red>
+ <green>64</green>
+ <blue>64</blue>
+ </color>
+ </brush>
+ </colorrole>
+ </active>
+ <inactive>
+ <colorrole role="ButtonText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>64</red>
+ <green>64</green>
+ <blue>64</blue>
+ </color>
+ </brush>
+ </colorrole>
+ </inactive>
+ <disabled>
+ <colorrole role="ButtonText">
+ <brush brushstyle="SolidPattern">
+ <color alpha="255">
+ <red>120</red>
+ <green>120</green>
+ <blue>120</blue>
+ </color>
+ </brush>
+ </colorrole>
+ </disabled>
+ </palette>
+ </property>
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string>List of available esp/esm files</string>
+ </property>
+ <property name="whatsThis">
+ <string><!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:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This list contains the esps and esms contained in the active mods. These require their own load order. Use drag&amp;drop to modify this load order. Please note that MO will only save the load order for mods that are active/checked.<br />There is a great tool named &quot;BOSS&quot; to automatically sort these files.</span></p></body></html></string>
+ </property>
+ <property name="dragEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropOverwriteMode">
+ <bool>false</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::InternalMove</enum>
+ </property>
+ <property name="defaultDropAction">
+ <enum>Qt::MoveAction</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::ExtendedSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="indentation">
+ <number>0</number>
+ </property>
+ <property name="uniformRowHeights">
+ <bool>true</bool>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerStretchLastSection">
+ <bool>false</bool>
+ </attribute>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_3">
+ <item>
+ <widget class="MOBase::LineEditClear" name="espFilterEdit">
+ <property name="text">
+ <string/>
+ </property>
+ <property name="placeholderText">
+ <string>Namefilter</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="bsaTab">
+ <attribute name="title">
+ <string>Archives</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_9">
+ <property name="leftMargin">
+ <number>6</number>
</property>
- <property name="horizontalScrollBarPolicy">
- <enum>Qt::ScrollBarAlwaysOff</enum>
+ <property name="topMargin">
+ <number>6</number>
</property>
- <property name="dragEnabled">
- <bool>true</bool>
+ <property name="rightMargin">
+ <number>6</number>
</property>
- <property name="dragDropMode">
- <enum>QAbstractItemView::DragDrop</enum>
+ <property name="bottomMargin">
+ <number>6</number>
</property>
- <property name="defaultDropAction">
- <enum>Qt::MoveAction</enum>
+ <item>
+ <widget class="QLabel" name="managedArchiveLabel">
+ <property name="toolTip">
+ <string><html><head/><body><p><span style=" font-weight:600;">Managed</span> Archives are always loaded and the priority of their mod (left pane) applies to them. MO will also provide conflict information for managed archives.<br/><span style=" font-weight:600;">Unmanaged</span> Archives are only loaded if there is a plugin of the same name (&quot;Plugins&quot; tab) with the same priority as that plugin!</p><p><span style=" font-style:italic;">If you don't understand this it's safest to leave all archives unchecked except the grayed out ones.</span></p></body></html></string>
+ </property>
+ <property name="text">
+ <string><html><head/><body><p>Check an archive to have MO manage it. (<a href="#"><span style=" text-decoration: underline; color:#0000ff;">read more</span></a>)</p></body></html></string>
+ </property>
+ <property name="wordWrap">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="MOBase::SortableTreeWidget" name="bsaList">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string>List of available BS Archives. Archives not checked here are not managed by MO and ignore installation order.</string>
+ </property>
+ <property name="whatsThis">
+ <string>BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they "compete" with loose files in your data directory over which is loaded.
+By default, BSAs that share their base name with an enabled ESP (i.e. plugin.esp and plugin.bsa) are automatically loaded and will have precedence over all loose files, the installation order you set up to the left is then ignored!
+
+BSAs checked here are loaded in such a way that your installation order is obeyed properly.</string>
+ </property>
+ <property name="editTriggers">
+ <set>QAbstractItemView::NoEditTriggers</set>
+ </property>
+ <property name="showDropIndicator" stdset="0">
+ <bool>true</bool>
+ </property>
+ <property name="dragEnabled">
+ <bool>false</bool>
+ </property>
+ <property name="dragDropOverwriteMode">
+ <bool>false</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::DragDrop</enum>
+ </property>
+ <property name="defaultDropAction">
+ <enum>Qt::MoveAction</enum>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::SingleSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="indentation">
+ <number>20</number>
+ </property>
+ <property name="itemsExpandable">
+ <bool>true</bool>
+ </property>
+ <property name="columnCount">
+ <number>1</number>
+ </property>
+ <attribute name="headerVisible">
+ <bool>false</bool>
+ </attribute>
+ <attribute name="headerDefaultSectionSize">
+ <number>200</number>
+ </attribute>
+ <column>
+ <property name="text">
+ <string>File</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="dataTab">
+ <attribute name="title">
+ <string>Data</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_5">
+ <property name="leftMargin">
+ <number>6</number>
</property>
- <property name="alternatingRowColors">
- <bool>true</bool>
+ <property name="topMargin">
+ <number>6</number>
</property>
- <property name="selectionMode">
- <enum>QAbstractItemView::SingleSelection</enum>
+ <property name="rightMargin">
+ <number>6</number>
</property>
- <property name="selectionBehavior">
- <enum>QAbstractItemView::SelectRows</enum>
+ <property name="bottomMargin">
+ <number>6</number>
</property>
- <property name="verticalScrollMode">
- <enum>QAbstractItemView::ScrollPerPixel</enum>
+ <item>
+ <widget class="QPushButton" name="btnRefreshData">
+ <property name="toolTip">
+ <string>refresh data-directory overview</string>
+ </property>
+ <property name="whatsThis">
+ <string>Refresh the overview. This may take a moment.</string>
+ </property>
+ <property name="text">
+ <string>Refresh</string>
+ </property>
+ <property name="icon">
+ <iconset resource="resources.qrc">
+ <normaloff>:/MO/gui/resources/view-refresh.png</normaloff>:/MO/gui/resources/view-refresh.png</iconset>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout_2">
+ <item>
+ <widget class="QTreeWidget" name="dataTree">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="whatsThis">
+ <string>This is an overview of your data directory as visible to the game (and tools). </string>
+ </property>
+ <property name="animated">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerDefaultSectionSize">
+ <number>400</number>
+ </attribute>
+ <column>
+ <property name="text">
+ <string>File</string>
+ </property>
+ </column>
+ <column>
+ <property name="text">
+ <string>Mod</string>
+ </property>
+ </column>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="conflictsCheckBox">
+ <property name="toolTip">
+ <string>Filter the above list so that only conflicts are displayed.</string>
+ </property>
+ <property name="whatsThis">
+ <string>Filter the above list so that only conflicts are displayed.</string>
+ </property>
+ <property name="text">
+ <string>Show only conflicts</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="savesTab">
+ <attribute name="title">
+ <string>Saves</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_3">
+ <property name="leftMargin">
+ <number>6</number>
</property>
- <property name="indentation">
- <number>0</number>
+ <property name="topMargin">
+ <number>6</number>
</property>
- <property name="itemsExpandable">
- <bool>false</bool>
+ <property name="rightMargin">
+ <number>6</number>
</property>
- <property name="sortingEnabled">
- <bool>true</bool>
+ <property name="bottomMargin">
+ <number>6</number>
</property>
- <attribute name="headerDefaultSectionSize">
- <number>100</number>
- </attribute>
- <attribute name="headerStretchLastSection">
- <bool>true</bool>
- </attribute>
- </widget>
- </item>
- </layout>
- </item>
- <item>
- <layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0,2">
- <item>
- <widget class="QCheckBox" name="compactBox">
- <property name="text">
- <string>Compact</string>
+ <item>
+ <widget class="QListWidget" name="savegameList">
+ <property name="contextMenuPolicy">
+ <enum>Qt::CustomContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string notr="true"/>
+ </property>
+ <property name="whatsThis">
+ <string><!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:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;">
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This is a list of all savegames for this game. Hover over a list entry to get detailed information about the save including a list of esps/esms that were used at the time this save was created but aren't active now.</span></p>
+<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; font-size:8pt;"></p>
+<p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">If you click &quot;Fix Mods...&quot; in the context menu, MO will try to activate all mods and esps to fix those missing esps. It will not disable anything!</span></p></body></html></string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ <widget class="QWidget" name="downloadTab">
+ <attribute name="title">
+ <string>Downloads</string>
+ </attribute>
+ <layout class="QVBoxLayout" name="verticalLayout_7">
+ <property name="leftMargin">
+ <number>2</number>
</property>
- </widget>
- </item>
- <item>
- <widget class="QCheckBox" name="showHiddenBox">
- <property name="text">
- <string>Show Hidden</string>
+ <property name="topMargin">
+ <number>2</number>
</property>
- </widget>
- </item>
- <item>
- <widget class="MOBase::LineEditClear" name="downloadFilterEdit">
- <property name="placeholderText">
- <string>Namefilter</string>
+ <property name="rightMargin">
+ <number>2</number>
</property>
- </widget>
- </item>
- </layout>
+ <property name="bottomMargin">
+ <number>2</number>
+ </property>
+ <item>
+ <layout class="QVBoxLayout" name="downloadLayout">
+ <item>
+ <widget class="QTreeView" name="downloadView">
+ <property name="minimumSize">
+ <size>
+ <width>320</width>
+ <height>0</height>
+ </size>
+ </property>
+ <property name="contextMenuPolicy">
+ <enum>Qt::PreventContextMenu</enum>
+ </property>
+ <property name="toolTip">
+ <string/>
+ </property>
+ <property name="whatsThis">
+ <string>This is a list of mods you downloaded from Nexus. Double click one to install it.</string>
+ </property>
+ <property name="verticalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOn</enum>
+ </property>
+ <property name="horizontalScrollBarPolicy">
+ <enum>Qt::ScrollBarAlwaysOff</enum>
+ </property>
+ <property name="dragEnabled">
+ <bool>true</bool>
+ </property>
+ <property name="dragDropMode">
+ <enum>QAbstractItemView::DragDrop</enum>
+ </property>
+ <property name="defaultDropAction">
+ <enum>Qt::MoveAction</enum>
+ </property>
+ <property name="alternatingRowColors">
+ <bool>true</bool>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::SingleSelection</enum>
+ </property>
+ <property name="selectionBehavior">
+ <enum>QAbstractItemView::SelectRows</enum>
+ </property>
+ <property name="verticalScrollMode">
+ <enum>QAbstractItemView::ScrollPerPixel</enum>
+ </property>
+ <property name="indentation">
+ <number>0</number>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="sortingEnabled">
+ <bool>true</bool>
+ </property>
+ <attribute name="headerDefaultSectionSize">
+ <number>100</number>
+ </attribute>
+ <attribute name="headerStretchLastSection">
+ <bool>true</bool>
+ </attribute>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ <item>
+ <layout class="QHBoxLayout" name="horizontalLayout" stretch="0,0,2">
+ <item>
+ <widget class="QCheckBox" name="showHiddenBox">
+ <property name="text">
+ <string>Show Hidden</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <spacer name="horizontalSpacer_3">
+ <property name="orientation">
+ <enum>Qt::Horizontal</enum>
+ </property>
+ <property name="sizeHint" stdset="0">
+ <size>
+ <width>40</width>
+ <height>20</height>
+ </size>
+ </property>
+ </spacer>
+ </item>
+ <item>
+ <widget class="MOBase::LineEditClear" name="downloadFilterEdit">
+ <property name="placeholderText">
+ <string>Namefilter</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </item>
+ </layout>
+ </widget>
+ </widget>
</item>
</layout>
</widget>
@@ -1076,6 +1213,20 @@ p, li { white-space: pre-wrap; } </item>
</layout>
</widget>
+ <widget class="QTreeView" name="logList">
+ <property name="contextMenuPolicy">
+ <enum>Qt::ActionsContextMenu</enum>
+ </property>
+ <property name="selectionMode">
+ <enum>QAbstractItemView::NoSelection</enum>
+ </property>
+ <property name="itemsExpandable">
+ <bool>false</bool>
+ </property>
+ <property name="headerHidden">
+ <bool>true</bool>
+ </property>
+ </widget>
</widget>
</item>
</layout>
@@ -1126,7 +1277,7 @@ p, li { white-space: pre-wrap; } <widget class="QStatusBar" name="statusBar"/>
<action name="actionInstallMod">
<property name="icon">
- <iconset>
+ <iconset resource="resources.qrc">
<normaloff>:/MO/gui/resources/system-installer.png</normaloff>:/MO/gui/resources/system-installer.png</iconset>
</property>
<property name="text">
@@ -1144,7 +1295,7 @@ p, li { white-space: pre-wrap; } </action>
<action name="actionAdd_Profile">
<property name="icon">
- <iconset>
+ <iconset resource="resources.qrc">
<normaloff>:/MO/gui/profiles</normaloff>:/MO/gui/profiles</iconset>
</property>
<property name="text">
@@ -1162,7 +1313,7 @@ p, li { white-space: pre-wrap; } </action>
<action name="actionModify_Executables">
<property name="icon">
- <iconset>
+ <iconset resource="resources.qrc">
<normaloff>:/MO/gui/icon_executable</normaloff>:/MO/gui/icon_executable</iconset>
</property>
<property name="text">
@@ -1180,7 +1331,7 @@ p, li { white-space: pre-wrap; } </action>
<action name="actionTool">
<property name="icon">
- <iconset>
+ <iconset resource="resources.qrc">
<normaloff>:/MO/gui/plugins</normaloff>:/MO/gui/plugins</iconset>
</property>
<property name="text">
@@ -1198,7 +1349,7 @@ p, li { white-space: pre-wrap; } </action>
<action name="actionSettings">
<property name="icon">
- <iconset>
+ <iconset resource="resources.qrc">
<normaloff>:/MO/gui/settings</normaloff>:/MO/gui/settings</iconset>
</property>
<property name="text">
@@ -1216,7 +1367,7 @@ p, li { white-space: pre-wrap; } </action>
<action name="actionNexus">
<property name="icon">
- <iconset>
+ <iconset resource="resources.qrc">
<normaloff>:/MO/gui/resources/internet-web-browser.png</normaloff>:/MO/gui/resources/internet-web-browser.png</iconset>
</property>
<property name="text">
@@ -1234,7 +1385,7 @@ p, li { white-space: pre-wrap; } <bool>false</bool>
</property>
<property name="icon">
- <iconset>
+ <iconset resource="resources.qrc">
<normaloff>:/MO/gui/update</normaloff>:/MO/gui/update</iconset>
</property>
<property name="text">
@@ -1249,7 +1400,7 @@ p, li { white-space: pre-wrap; } <bool>false</bool>
</property>
<property name="icon">
- <iconset>
+ <iconset resource="resources.qrc">
<normaloff>:/MO/gui/warning</normaloff>:/MO/gui/warning</iconset>
</property>
<property name="text">
@@ -1264,7 +1415,7 @@ Right now this has very limited functionality</string> </action>
<action name="actionHelp">
<property name="icon">
- <iconset>
+ <iconset resource="resources.qrc">
<normaloff>:/MO/gui/help</normaloff>:/MO/gui/help</iconset>
</property>
<property name="text">
@@ -1279,7 +1430,7 @@ Right now this has very limited functionality</string> </action>
<action name="actionEndorseMO">
<property name="icon">
- <iconset>
+ <iconset resource="resources.qrc">
<normaloff>:/MO/gui/icon_favorite</normaloff>:/MO/gui/icon_favorite</iconset>
</property>
<property name="text">
@@ -1289,6 +1440,14 @@ Right now this has very limited functionality</string> <string>Endorse Mod Organizer</string>
</property>
</action>
+ <action name="actionCopy_Log_to_Clipboard">
+ <property name="text">
+ <string>Copy Log to Clipboard</string>
+ </property>
+ <property name="shortcut">
+ <string>Ctrl+C</string>
+ </property>
+ </action>
</widget>
<layoutdefault spacing="6" margin="11"/>
<customwidgets>
@@ -1308,6 +1467,8 @@ Right now this has very limited functionality</string> <header>sortabletreewidget.h</header>
</customwidget>
</customwidgets>
- <resources/>
+ <resources>
+ <include location="resources.qrc"/>
+ </resources>
<connections/>
</ui>
diff --git a/src/modinfo.cpp b/src/modinfo.cpp index bd244852..12463559 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -219,7 +219,7 @@ ModInfo::ModInfo() void ModInfo::checkChunkForUpdate(const std::vector<int> &modIDs, QObject *receiver) { if (modIDs.size() != 0) { - NexusInterface::instance()->requestUpdates(modIDs, receiver, QVariant()); + NexusInterface::instance()->requestUpdates(modIDs, receiver, QVariant(), QString()); } } diff --git a/src/modinfodialog.ui b/src/modinfodialog.ui index 1d357f70..d3862b58 100644 --- a/src/modinfodialog.ui +++ b/src/modinfodialog.ui @@ -78,7 +78,7 @@ </attribute>
<layout class="QHBoxLayout" name="horizontalLayout_4">
<item>
- <layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,0">
+ <layout class="QVBoxLayout" name="verticalLayout_5" stretch="0,0,0,0">
<property name="spacing">
<number>6</number>
</property>
@@ -86,6 +86,13 @@ <enum>QLayout::SetMinimumSize</enum>
</property>
<item>
+ <widget class="QLabel" name="label_6">
+ <property name="text">
+ <string>Ini Files</string>
+ </property>
+ </widget>
+ </item>
+ <item>
<widget class="QListWidget" name="iniFileList">
<property name="maximumSize">
<size>
@@ -102,6 +109,13 @@ </widget>
</item>
<item>
+ <widget class="QLabel" name="label_5">
+ <property name="text">
+ <string>Ini Tweaks</string>
+ </property>
+ </widget>
+ </item>
+ <item>
<widget class="QListWidget" name="iniTweaksList">
<property name="maximumSize">
<size>
@@ -112,6 +126,12 @@ <property name="contextMenuPolicy">
<enum>Qt::CustomContextMenu</enum>
</property>
+ <property name="toolTip">
+ <string>This is a list of ini tweaks (ini modifications that can be toggled).</string>
+ </property>
+ <property name="whatsThis">
+ <string>This is a list of ini tweaks. Ini Tweaks are (usually small) fragments of ini files that are applied over existing settings in skyrim.ini/skyrimprefs.ini. Each tweak can be toggled individually. You should check the description of the mod wether the tweaks are really optional.</string>
+ </property>
</widget>
</item>
</layout>
diff --git a/src/modlist.cpp b/src/modlist.cpp index e2cb7cf0..25b0eb52 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -375,20 +375,23 @@ bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) int modID = index.row(); + ModInfo::Ptr info = ModInfo::getByIndex(modID); + IModList::ModStates oldState = state(modID); + + bool result = false; + if (role == Qt::CheckStateRole) { bool enabled = value.toInt() == Qt::Checked; if (m_Profile->modEnabled(modID) != enabled) { m_Profile->setModEnabled(modID, enabled); m_Modified = true; - emit modlist_changed(index, role); } - return true; + result = true; } else if (role == Qt::EditRole) { - bool res = false; switch (index.column()) { case COL_NAME: { - res = renameMod(modID, value.toString()); + result = renameMod(modID, value.toString()); } break; case COL_PRIORITY: { bool ok = false; @@ -397,47 +400,55 @@ bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) m_Profile->setModPriority(modID, newPriority); emit modlist_changed(index, role); - res = true; + result = true; } else { - res = false; + result = false; } } break; case COL_MODID: { - ModInfo::Ptr info = ModInfo::getByIndex(modID); bool ok = false; int newID = value.toInt(&ok); if (ok) { info->setNexusID(newID); emit modlist_changed(index, role); - res = true; + result = true; } else { - res = false; + result = false; } } break; case COL_VERSION: { - ModInfo::Ptr info = ModInfo::getByIndex(modID); VersionInfo::VersionScheme scheme = info->getVersion().scheme(); VersionInfo version(value.toString(), scheme); if (version.isValid()) { info->setVersion(version); - res = true; + result = true; } else { - res = false; + result = false; } } break; default: { qWarning("edit on column \"%s\" not supported", getColumnName(index.column()).toUtf8().constData()); - res = false; + result = false; } break; } - if (res) { + if (result) { emit dataChanged(index, index); } - return res; - } else { - return false; } + + IModList::ModStates newState = state(modID); + if (oldState != newState) { + try { + m_ModStateChanged(info->name(), newState); + } catch (const std::exception &e) { + qCritical("failed to invoke state changed notification: %s", e.what()); + } catch (...) { + qCritical("failed to invoke state changed notification: unknown exception"); + } + } + + return result; } @@ -578,10 +589,9 @@ void ModList::modInfoChanged(ModInfo::Ptr info) } } -IModList::ModStates ModList::state(const QString &name) const +IModList::ModStates ModList::state(unsigned int modIndex) const { - ModStates result; - unsigned int modIndex = ModInfo::getIndex(name); + IModList::ModStates result; if (modIndex != UINT_MAX) { result |= IModList::STATE_EXISTS; ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); @@ -605,6 +615,13 @@ IModList::ModStates ModList::state(const QString &name) const return result; } +IModList::ModStates ModList::state(const QString &name) const +{ + unsigned int modIndex = ModInfo::getIndex(name); + + return state(modIndex); +} + int ModList::priority(const QString &name) const { unsigned int modIndex = ModInfo::getIndex(name); @@ -669,7 +686,7 @@ bool ModList::dropURLs(const QMimeData *mimeData, int row, const QModelIndex &pa } if (source.count() != 0) { - shellMove(source, target, NULL); + shellMove(source, target); } return true; @@ -744,6 +761,8 @@ void ModList::removeRowForce(int row) } if (m_Profile == NULL) return; + m_Profile->setModEnabled(row, false); + ModInfo::Ptr modInfo = ModInfo::getByIndex(row); bool wasEnabled = m_Profile->modEnabled(row); @@ -770,6 +789,8 @@ void ModList::removeRow(int row, const QModelIndex&) } if (m_Profile == NULL) return; + m_Profile->setModEnabled(row, false); + ModInfo::Ptr modInfo = ModInfo::getByIndex(row); if (!modInfo->isRegular()) return; diff --git a/src/modlist.h b/src/modlist.h index 054c8cca..e0bc9df9 100644 --- a/src/modlist.h +++ b/src/modlist.h @@ -239,6 +239,8 @@ private: bool dropMod(const QMimeData *mimeData, int row, const QModelIndex &parent); + ModStates state(unsigned int modIndex) const; + private slots: private: diff --git a/src/modlistsortproxy.cpp b/src/modlistsortproxy.cpp index 4d767230..052e65b2 100644 --- a/src/modlistsortproxy.cpp +++ b/src/modlistsortproxy.cpp @@ -28,8 +28,12 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. ModListSortProxy::ModListSortProxy(Profile* profile, QObject *parent) - : QSortFilterProxyModel(parent), m_Profile(profile), - m_CategoryFilter(), m_CurrentFilter() + : QSortFilterProxyModel(parent) + , m_Profile(profile) + , m_CategoryFilter() + , m_CurrentFilter() + , m_FilterActive(false) + , m_FilterMode(FILTER_AND) { m_EnabledColumns.set(ModList::COL_FLAGS); m_EnabledColumns.set(ModList::COL_NAME); @@ -47,7 +51,8 @@ void ModListSortProxy::setProfile(Profile *profile) void ModListSortProxy::updateFilterActive() { - emit filterActive((m_CategoryFilter.size() > 0) || !m_CurrentFilter.isEmpty()); + m_FilterActive = (m_CategoryFilter.size() > 0) || !m_CurrentFilter.isEmpty(); + emit filterActive(m_FilterActive); } void ModListSortProxy::setCategoryFilter(const std::vector<int> &categories) @@ -220,13 +225,8 @@ bool ModListSortProxy::hasConflictFlag(const std::vector<ModInfo::EFlag> &flags) } -bool ModListSortProxy::filterMatchesMod(ModInfo::Ptr info, bool enabled) const +bool ModListSortProxy::filterMatchesModAnd(ModInfo::Ptr info, bool enabled) const { - if (!m_CurrentFilter.isEmpty() && - !info->name().contains(m_CurrentFilter, Qt::CaseInsensitive)) { - return false; - } - for (auto iter = m_CategoryFilter.begin(); iter != m_CategoryFilter.end(); ++iter) { switch (*iter) { case CategoryFactory::CATEGORY_SPECIAL_CHECKED: { @@ -246,17 +246,71 @@ bool ModListSortProxy::filterMatchesMod(ModInfo::Ptr info, bool enabled) const } break; case CategoryFactory::CATEGORY_SPECIAL_NOTENDORSED: { ModInfo::EEndorsedState state = info->endorsedState(); - return (state == ModInfo::ENDORSED_FALSE) || (state == ModInfo::ENDORSED_NEVER); + if (state != ModInfo::ENDORSED_FALSE) return false; } break; default: { if (!info->categorySet(*iter)) return false; } break; } } - return true; } +bool ModListSortProxy::filterMatchesModOr(ModInfo::Ptr info, bool enabled) const +{ + for (auto iter = m_CategoryFilter.begin(); iter != m_CategoryFilter.end(); ++iter) { + switch (*iter) { + case CategoryFactory::CATEGORY_SPECIAL_CHECKED: { + if (enabled) return true; + } break; + case CategoryFactory::CATEGORY_SPECIAL_UNCHECKED: { + if (!enabled) return true; + } break; + case CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE: { + if (info->updateAvailable() || info->downgradeAvailable()) return true; + } break; + case CategoryFactory::CATEGORY_SPECIAL_NOCATEGORY: { + if (info->getCategories().size() == 0) return true; + } break; + case CategoryFactory::CATEGORY_SPECIAL_CONFLICT: { + if (hasConflictFlag(info->getFlags())) return true; + } break; + case CategoryFactory::CATEGORY_SPECIAL_NOTENDORSED: { + ModInfo::EEndorsedState state = info->endorsedState(); + if ((state == ModInfo::ENDORSED_FALSE) && (state != ModInfo::ENDORSED_NEVER)) return true; + } break; + default: { + if (info->categorySet(*iter)) return true; + } break; + } + } + return false; +} + + + +bool ModListSortProxy::filterMatchesMod(ModInfo::Ptr info, bool enabled) const +{ + if (!m_CurrentFilter.isEmpty() && + !info->name().contains(m_CurrentFilter, Qt::CaseInsensitive)) { + return false; + } + + if (m_FilterMode == FILTER_AND) { + return filterMatchesModAnd(info, enabled); + } else { + return (m_CategoryFilter.size() == 0) || filterMatchesModOr(info, enabled); + } +} + +void ModListSortProxy::setFilterMode(ModListSortProxy::FilterMode mode) +{ + if (m_FilterMode != mode) { + m_FilterMode = mode; + this->invalidate(); + } +} + bool ModListSortProxy::filterAcceptsRow(int row, const QModelIndex &parent) const { diff --git a/src/modlistsortproxy.h b/src/modlistsortproxy.h index 3e18ea4e..c04ba28d 100644 --- a/src/modlistsortproxy.h +++ b/src/modlistsortproxy.h @@ -32,6 +32,13 @@ class ModListSortProxy : public QSortFilterProxyModel public: + enum FilterMode { + FILTER_AND, + FILTER_OR + }; + +public: + explicit ModListSortProxy(Profile *profile, QObject *parent = 0); void setProfile(Profile *profile); @@ -52,8 +59,26 @@ public: **/ void disableAllVisible(); + /** + * @brief tests if a filtere matches for a mod + * @param info mod information + * @param enabled true if the mod is currently active + * @return true if current active filters match for the specified mod + */ bool filterMatchesMod(ModInfo::Ptr info, bool enabled) const; + /** + * @return true if a filter is currently active + */ + bool isFilterActive() const { return m_FilterActive; } + + void setFilterMode(FilterMode mode); + + /** + * @brief tests if the specified index has child nodes + * @param parent the node to test + * @return true if there are child nodes + */ virtual bool hasChildren ( const QModelIndex & parent = QModelIndex() ) const { return rowCount(parent) > 0; } @@ -77,6 +102,8 @@ private: bool hasConflictFlag(const std::vector<ModInfo::EFlag> &flags) const; void updateFilterActive(); + bool filterMatchesModAnd(ModInfo::Ptr info, bool enabled) const; + bool filterMatchesModOr(ModInfo::Ptr info, bool enabled) const; private: @@ -86,6 +113,9 @@ private: std::bitset<ModList::COL_LASTCOLUMN + 1> m_EnabledColumns; QString m_CurrentFilter; + bool m_FilterActive; + FilterMode m_FilterMode; + }; #endif // MODLISTSORTPROXY_H diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index 64dd6272..1f95fcd7 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -32,36 +32,37 @@ using namespace MOBase; using namespace MOShared; -NexusBridge::NexusBridge() +NexusBridge::NexusBridge(const QString &subModule) : m_Interface(NexusInterface::instance()) , m_Url(MOBase::ToQString(MOShared::GameInfo::instance().getNexusInfoUrl())) + , m_SubModule(subModule) { } void NexusBridge::requestDescription(int modID, QVariant userData) { - m_RequestIDs.insert(m_Interface->requestDescription(modID, this, userData, m_Url)); + m_RequestIDs.insert(m_Interface->requestDescription(modID, this, userData, m_SubModule, m_Url)); } void NexusBridge::requestFiles(int modID, QVariant userData) { - m_RequestIDs.insert(m_Interface->requestFiles(modID, this, userData, m_Url)); + m_RequestIDs.insert(m_Interface->requestFiles(modID, this, userData, m_SubModule, m_Url)); } void NexusBridge::requestFileInfo(int modID, int fileID, QVariant userData) { - m_RequestIDs.insert(m_Interface->requestFileInfo(modID, fileID, this, userData, m_Url)); + m_RequestIDs.insert(m_Interface->requestFileInfo(modID, fileID, this, userData, m_SubModule, m_Url)); } void NexusBridge::requestDownloadURL(int modID, int fileID, QVariant userData) { - m_RequestIDs.insert(m_Interface->requestDownloadURL(modID, fileID, this, userData, m_Url)); + m_RequestIDs.insert(m_Interface->requestDownloadURL(modID, fileID, this, userData, m_SubModule, m_Url)); } void NexusBridge::requestToggleEndorsement(int modID, bool endorse, QVariant userData) { - m_RequestIDs.insert(m_Interface->requestToggleEndorsement(modID, endorse, this, userData, m_Url)); + m_RequestIDs.insert(m_Interface->requestToggleEndorsement(modID, endorse, this, userData, m_SubModule, m_Url)); } void NexusBridge::nxmDescriptionAvailable(int modID, QVariant userData, QVariant resultData, int requestID) @@ -161,8 +162,6 @@ NexusInterface::NexusInterface() void NexusInterface::cleanup() { - delete NexusInterface::s_Instance; - NexusInterface::s_Instance = NULL; } @@ -172,15 +171,10 @@ NXMAccessManager *NexusInterface::getAccessManager() } -NexusInterface *NexusInterface::s_Instance = NULL; - - NexusInterface *NexusInterface::instance() { - if (s_Instance == NULL) { - s_Instance = new NexusInterface; - } - return s_Instance; + static NexusInterface s_Instance; + return &s_Instance; } @@ -196,13 +190,17 @@ void NexusInterface::setNMMVersion(const QString &nmmVersion) m_NMMVersion = nmmVersion; } +void NexusInterface::loginCompleted() +{ + nextRequest(); +} + void NexusInterface::interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query) { static std::tr1::regex exp("^([a-zA-Z0-9_\\- ]*?)([-_ ][VvRr]?[0-9_]+)?-([1-9][0-9]+).*"); static std::tr1::regex simpleexp("^([a-zA-Z0-9_]+)"); -// std::tr1::match_results<std::string::const_iterator> result; QByteArray fileNameUTF8 = fileName.toUtf8(); std::tr1::cmatch result; if (std::tr1::regex_search(fileNameUTF8.constData(), result, exp)) { @@ -249,9 +247,10 @@ void NexusInterface::interpretNexusFileName(const QString &fileName, QString &mo } -int NexusInterface::requestDescription(int modID, QObject *receiver, QVariant userData, const QString &url) +int NexusInterface::requestDescription(int modID, QObject *receiver, QVariant userData, + const QString &subModule, const QString &url) { - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_DESCRIPTION, userData, url); + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_DESCRIPTION, userData, subModule, url); m_RequestQueue.enqueue(requestInfo); connect(this, SIGNAL(nxmDescriptionAvailable(int,QVariant,QVariant,int)), @@ -265,9 +264,10 @@ int NexusInterface::requestDescription(int modID, QObject *receiver, QVariant us } -int NexusInterface::requestUpdates(const std::vector<int> &modIDs, QObject *receiver, QVariant userData, const QString &url) +int NexusInterface::requestUpdates(const std::vector<int> &modIDs, QObject *receiver, QVariant userData, + const QString &subModule, const QString &url) { - NXMRequestInfo requestInfo(modIDs, NXMRequestInfo::TYPE_GETUPDATES, userData, url); + NXMRequestInfo requestInfo(modIDs, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, url); m_RequestQueue.enqueue(requestInfo); connect(this, SIGNAL(nxmUpdatesAvailable(std::vector<int>,QVariant,QVariant,int)), @@ -300,9 +300,10 @@ void NexusInterface::fakeFiles() } -int NexusInterface::requestFiles(int modID, QObject *receiver, QVariant userData, const QString &url) +int NexusInterface::requestFiles(int modID, QObject *receiver, QVariant userData, + const QString &subModule, const QString &url) { - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_FILES, userData, url); + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_FILES, userData, subModule, url); m_RequestQueue.enqueue(requestInfo); connect(this, SIGNAL(nxmFilesAvailable(int,QVariant,QVariant,int)), receiver, SLOT(nxmFilesAvailable(int,QVariant,QVariant,int)), Qt::UniqueConnection); @@ -319,9 +320,10 @@ int NexusInterface::requestFiles(int modID, QObject *receiver, QVariant userData } -int NexusInterface::requestFileInfo(int modID, int fileID, QObject *receiver, QVariant userData, const QString &url) +int NexusInterface::requestFileInfo(int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule, + const QString &url) { - NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_FILEINFO, userData, url); + NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_FILEINFO, userData, subModule, url); m_RequestQueue.enqueue(requestInfo); connect(this, SIGNAL(nxmFileInfoAvailable(int,int,QVariant,QVariant,int)), @@ -335,9 +337,10 @@ int NexusInterface::requestFileInfo(int modID, int fileID, QObject *receiver, QV } -int NexusInterface::requestDownloadURL(int modID, int fileID, QObject *receiver, QVariant userData, const QString &url) +int NexusInterface::requestDownloadURL(int modID, int fileID, QObject *receiver, QVariant userData, + const QString &subModule, const QString &url) { - NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_DOWNLOADURL, userData, url); + NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_DOWNLOADURL, userData, subModule, url); m_RequestQueue.enqueue(requestInfo); connect(this, SIGNAL(nxmDownloadURLsAvailable(int,int,QVariant,QVariant,int)), @@ -351,9 +354,10 @@ int NexusInterface::requestDownloadURL(int modID, int fileID, QObject *receiver, } -int NexusInterface::requestToggleEndorsement(int modID, bool endorse, QObject *receiver, QVariant userData, const QString &url) +int NexusInterface::requestToggleEndorsement(int modID, bool endorse, QObject *receiver, QVariant userData, + const QString &subModule, const QString &url) { - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, url); + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, url); requestInfo.m_Endorse = endorse; m_RequestQueue.enqueue(requestInfo); @@ -367,13 +371,29 @@ int NexusInterface::requestToggleEndorsement(int modID, bool endorse, QObject *r return requestInfo.m_ID; } +bool NexusInterface::requiresLogin(const NXMRequestInfo &info) +{ + return (info.m_Type == NXMRequestInfo::TYPE_TOGGLEENDORSEMENT) + || (info.m_Type == NXMRequestInfo::TYPE_DOWNLOADURL); +} void NexusInterface::nextRequest() { - if ((m_ActiveRequest.size() >= MAX_ACTIVE_DOWNLOADS) || (m_RequestQueue.isEmpty())) { + if ((m_ActiveRequest.size() >= MAX_ACTIVE_DOWNLOADS) + || m_RequestQueue.isEmpty()) { return; } + if (!getAccessManager()->loggedIn() + && requiresLogin(m_RequestQueue.head())) { + if (!getAccessManager()->loginAttempted()) { + emit needLogin(); + return; + } else if (getAccessManager()->loginWaiting()) { + return; + } + } + NXMRequestInfo info = m_RequestQueue.dequeue(); info.m_Timeout = new QTimer(this); info.m_Timeout->setInterval(60000); @@ -411,10 +431,16 @@ void NexusInterface::nextRequest() } QNetworkRequest request(url); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml"); - request.setRawHeader("User-Agent", - QString("Mod Organizer v%1 (compatible to Nexus Client v%2)") - .arg(m_MOVersion.displayString()) - .arg(m_NMMVersion).toUtf8()); + QStringList comments; + comments << "compatible to Nexus Client v" + m_NMMVersion; + if (!info.m_SubModule.isEmpty()) { + comments << "module: " + info.m_SubModule; + } + + QString userAgent = QString("Mod Organizer v%1 (%2)") + .arg(m_MOVersion.displayString()) + .arg(comments.join("; ")); + request.setRawHeader("User-Agent", userAgent.toUtf8()); info.m_Reply = m_AccessManager->get(request); @@ -454,6 +480,7 @@ void NexusInterface::requestFinished(std::list<NXMRequestInfo>::iterator iter) if (nexusError.length() == 0) { nexusError = tr("empty response"); } + qDebug("nexus error: %s", qPrintable(nexusError)); emit nxmRequestFailed(iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, nexusError); } else { bool ok; @@ -512,7 +539,8 @@ void NexusInterface::requestError(QNetworkReply::NetworkError) return; } - qCritical("request error: %s", reply->errorString().toUtf8().constData()); + qCritical("request (%s) error: %s", + qPrintable(reply->url().toString()), qPrintable(reply->errorString())); } @@ -531,3 +559,57 @@ void NexusInterface::requestTimeout() } } } + + +NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID + , NexusInterface::NXMRequestInfo::Type type + , QVariant userData + , const QString &subModule + , const QString &url) + : m_ModID(modID) + , m_FileID(0) + , m_Reply(NULL) + , m_Type(type) + , m_UserData(userData) + , m_Timeout(NULL) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(url) + , m_SubModule(subModule) +{} + +NexusInterface::NXMRequestInfo::NXMRequestInfo(std::vector<int> modIDList + , NexusInterface::NXMRequestInfo::Type type + , QVariant userData + , const QString &subModule + , const QString &url) + : m_ModID(-1) + , m_ModIDList(modIDList) + , m_FileID(0) + , m_Reply(NULL) + , m_Type(type) + , m_UserData(userData) + , m_Timeout(NULL) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(url) + , m_SubModule(subModule) +{} + +NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID + , int fileID + , NexusInterface::NXMRequestInfo::Type type + , QVariant userData + , const QString &subModule + , const QString &url) + : m_ModID(modID) + , m_FileID(fileID) + , m_Reply(NULL) + , m_Type(type) + , m_UserData(userData) + , m_Timeout(NULL) + , m_Reroute(false) + , m_ID(s_NextID.fetchAndAddAcquire(1)) + , m_URL(url) + , m_SubModule(subModule) +{} diff --git a/src/nexusinterface.h b/src/nexusinterface.h index 7b709e1c..c4427474 100644 --- a/src/nexusinterface.h +++ b/src/nexusinterface.h @@ -54,7 +54,7 @@ class NexusBridge : public MOBase::IModRepositoryBridge public: - NexusBridge(); + NexusBridge(const QString &subModule = ""); /** * @brief request description for a mod @@ -111,6 +111,7 @@ private: NexusInterface *m_Interface; QString m_Url; + QString m_SubModule; std::set<int> m_RequestIDs; }; @@ -145,7 +146,7 @@ public: * @param url the url to request from * @return int an id to identify the request **/ - int requestDescription(int modID, QObject *receiver, QVariant userData, + int requestDescription(int modID, QObject *receiver, QVariant userData, const QString &subModule, const QString &url = MOBase::ToQString(MOShared::GameInfo::instance().getNexusInfoUrl())); /** @@ -156,7 +157,7 @@ public: * @param url the url to request from * @return int an id to identify the request */ - int requestUpdates(const std::vector<int> &modIDs, QObject *receiver, QVariant userData, + int requestUpdates(const std::vector<int> &modIDs, QObject *receiver, QVariant userData, const QString &subModule, const QString &url = MOBase::ToQString(MOShared::GameInfo::instance().getNexusInfoUrl())); /** @@ -168,7 +169,7 @@ public: * @param url the url to request from * @return int an id to identify the request **/ - int requestFiles(int modID, QObject *receiver, QVariant userData, + int requestFiles(int modID, QObject *receiver, QVariant userData, const QString &subModule, const QString &url = MOBase::ToQString(MOShared::GameInfo::instance().getNexusInfoUrl())); /** @@ -181,7 +182,7 @@ public: * @param url the url to request from * @return int an id to identify the request **/ - int requestFileInfo(int modID, int fileID, QObject *receiver, QVariant userData, + int requestFileInfo(int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule, const QString &url = MOBase::ToQString(MOShared::GameInfo::instance().getNexusInfoUrl())); /** @@ -194,7 +195,7 @@ public: * @param url the url to request from * @return int an id to identify the request **/ - int requestDownloadURL(int modID, int fileID, QObject *receiver, QVariant userData, + int requestDownloadURL(int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule, const QString &url = MOBase::ToQString(MOShared::GameInfo::instance().getNexusInfoUrl())); /** @@ -206,7 +207,7 @@ public: * @param url the url to request from * @return int an id to identify the request */ - int requestToggleEndorsement(int modID, bool endorse, QObject *receiver, QVariant userData, + int requestToggleEndorsement(int modID, bool endorse, QObject *receiver, QVariant userData, const QString &subModule, const QString &url = MOBase::ToQString(MOShared::GameInfo::instance().getNexusInfoUrl())); /** @@ -220,6 +221,11 @@ public: **/ void setNMMVersion(const QString &nmmVersion); + /** + * @brief called when the log-in completes. This was, requests waiting for the log-in can be run + */ + void loginCompleted(); + public: /** @@ -234,6 +240,8 @@ signals: void requestNXMDownload(const QString &url); + void needLogin(); + void nxmDescriptionAvailable(int modID, QVariant userData, QVariant resultData, int requestID); void nxmUpdatesAvailable(const std::vector<int> &modIDs, QVariant userData, QVariant resultData, int requestID); void nxmFilesAvailable(int modID, QVariant userData, QVariant resultData, int requestID); @@ -270,19 +278,14 @@ private: QVariant m_UserData; QTimer *m_Timeout; QString m_URL; + QString m_SubModule; bool m_Reroute; int m_ID; int m_Endorse; - NXMRequestInfo(int modID, Type type, QVariant userData, const QString &url) - : m_ModID(modID), m_FileID(0), m_Reply(NULL), m_Type(type), m_UserData(userData), - m_Timeout(NULL), m_Reroute(false), m_ID(s_NextID.fetchAndAddAcquire(1)), m_URL(url) {} - NXMRequestInfo(std::vector<int> modIDList, Type type, QVariant userData, const QString &url) - : m_ModID(-1), m_ModIDList(modIDList), m_FileID(0), m_Reply(NULL), m_Type(type), m_UserData(userData), - m_Timeout(NULL), m_Reroute(false), m_ID(s_NextID.fetchAndAddAcquire(1)), m_URL(url) {} - NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString &url) - : m_ModID(modID), m_FileID(fileID), m_Reply(NULL), m_Type(type), m_UserData(userData), - m_Timeout(NULL), m_Reroute(false), m_ID(s_NextID.fetchAndAddAcquire(1)), m_URL(url) {} + NXMRequestInfo(int modID, Type type, QVariant userData, const QString &subModule, const QString &url); + NXMRequestInfo(std::vector<int> modIDList, Type type, QVariant userData, const QString &subModule, const QString &url); + NXMRequestInfo(int modID, int fileID, Type type, QVariant userData, const QString &subModule, const QString &url); private: static QAtomicInt s_NextID; @@ -295,11 +298,12 @@ private: NexusInterface(); void nextRequest(); void requestFinished(std::list<NXMRequestInfo>::iterator iter); + bool requiresLogin(const NXMRequestInfo &info); static void cleanup(); private: - static NexusInterface *s_Instance; +// static NexusInterface *s_Instance; QNetworkDiskCache *m_DiskCache; diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index a05b8a6c..b65ff082 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -22,6 +22,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include "report.h" #include "utility.h" #include "selfupdater.h" +#include "persistentcookiejar.h" #include <QMessageBox> #include <QPushButton> #include <QNetworkProxy> @@ -29,6 +30,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <QNetworkCookie> #include <QNetworkCookieJar> #include <QCoreApplication> +#include <QDir> #include <gameinfo.h> #if QT_VERSION >= QT_VERSION_CHECK(5,0,0) @@ -40,8 +42,13 @@ using namespace MOShared; NXMAccessManager::NXMAccessManager(QObject *parent) - : QNetworkAccessManager(parent), m_LoginReply(NULL), m_ProgressDialog() + : QNetworkAccessManager(parent) + , m_LoginReply(NULL) + , m_ProgressDialog() + , m_LoginAttempted(false) { + setCookieJar(new PersistentCookieJar( + QDir::fromNativeSeparators(MOBase::ToQString(MOShared::GameInfo::instance().getCacheDir())) + "/nexus_cookies.dat", this)); } NXMAccessManager::~NXMAccessManager() @@ -88,6 +95,11 @@ bool NXMAccessManager::loggedIn() const return hasLoginCookies(); } +bool NXMAccessManager::loginWaiting() const +{ + return m_LoginReply != NULL; +} + void NXMAccessManager::login(const QString &username, const QString &password) { @@ -100,6 +112,8 @@ void NXMAccessManager::login(const QString &username, const QString &password) return; } + m_LoginAttempted = true; + m_Username = username; m_Password = password; pageLogin(); @@ -145,6 +159,7 @@ void NXMAccessManager::loginTimeout() emit loginFailed(tr("timeout")); m_LoginReply->deleteLater(); m_LoginReply = NULL; + m_LoginAttempted = false; // this usually means we might have usccess later m_LoginTimeout.stop(); m_Username.clear(); m_Password.clear(); diff --git a/src/nxmaccessmanager.h b/src/nxmaccessmanager.h index 1d8d36ab..6c0dedb9 100644 --- a/src/nxmaccessmanager.h +++ b/src/nxmaccessmanager.h @@ -41,6 +41,9 @@ public: bool loggedIn() const; + bool loginAttempted() const { return m_LoginAttempted; } + bool loginWaiting() const; + void login(const QString &username, const QString &password); void showCookies(); @@ -93,6 +96,8 @@ private: QString m_Username; QString m_Password; + bool m_LoginAttempted; + }; #endif // NXMACCESSMANAGER_H diff --git a/src/organizer.pro b/src/organizer.pro index 073d2906..c16196d7 100644 --- a/src/organizer.pro +++ b/src/organizer.pro @@ -6,9 +6,9 @@ contains(QT_VERSION, "^5.*") {
- QT += core gui widgets network xml sql xmlpatterns qml quick script
+ QT += core gui widgets network xml sql xmlpatterns qml quick script webkit
} else {
- QT += core gui network xml declarative script sql xmlpatterns
+ QT += core gui network xml declarative script sql xmlpatterns webkit
}
TARGET = ModOrganizer
@@ -78,6 +78,9 @@ SOURCES += \ ../esptk/record.cpp \
../esptk/espfile.cpp \
../esptk/subrecord.cpp \
+ browserview.cpp \
+ browserdialog.cpp \
+ persistentcookiejar.cpp \
noeditdelegate.cpp \
previewgenerator.cpp \
previewdialog.cpp \
@@ -85,7 +88,8 @@ SOURCES += \ json.cpp \
safewritefile.cpp \
modflagicondelegate.cpp \
- pluginflagicondelegate.cpp
+ pluginflagicondelegate.cpp \
+ organizerproxy.cpp
HEADERS += \
@@ -152,6 +156,9 @@ HEADERS += \ ../esptk/espfile.h \
../esptk/subrecord.h \
../esptk/espexceptions.h \
+ browserview.h \
+ browserdialog.h \
+ persistentcookiejar.h \
noeditdelegate.h \
previewgenerator.h \
previewdialog.h \
@@ -160,7 +167,8 @@ HEADERS += \ safewritefile.h\
pdll.h \
modflagicondelegate.h \
- pluginflagicondelegate.h
+ pluginflagicondelegate.h \
+ organizerproxy.h
FORMS += \
transfersavesdialog.ui \
@@ -191,6 +199,7 @@ FORMS += \ savetextasdialog.ui \
problemsdialog.ui \
previewdialog.ui \
+ browserdialog.ui \
aboutdialog.ui
INCLUDEPATH += ../shared ../archive ../uibase ../bsatk ../esptk ../boss_modified/boss-api "$(BOOSTPATH)"
diff --git a/src/organizerproxy.cpp b/src/organizerproxy.cpp new file mode 100644 index 00000000..c48bc765 --- /dev/null +++ b/src/organizerproxy.cpp @@ -0,0 +1,191 @@ +#include "organizerproxy.h"
+#include <gameinfo.h>
+#include <appconfig.h>
+
+
+using namespace MOBase;
+using namespace MOShared;
+
+
+OrganizerProxy::OrganizerProxy(MainWindow *window, const QString &pluginName)
+ : m_Proxied(window)
+ , m_PluginName(pluginName)
+{
+}
+
+IGameInfo &OrganizerProxy::gameInfo() const
+{
+ return *m_Proxied->m_GameInfo;
+}
+
+
+IModRepositoryBridge *OrganizerProxy::createNexusBridge() const
+{
+ return new NexusBridge(m_PluginName);
+}
+
+
+QString OrganizerProxy::profileName() const
+{
+ if (m_Proxied->m_CurrentProfile != NULL) {
+ return m_Proxied->m_CurrentProfile->getName();
+ } else {
+ return "";
+ }
+}
+
+QString OrganizerProxy::profilePath() const
+{
+ if (m_Proxied->m_CurrentProfile != NULL) {
+ return m_Proxied->m_CurrentProfile->getPath();
+ } else {
+ return "";
+ }
+}
+
+QString OrganizerProxy::downloadsPath() const
+{
+ return QDir::fromNativeSeparators(m_Proxied->m_Settings.getDownloadDirectory());
+}
+
+VersionInfo OrganizerProxy::appVersion() const
+{
+ return m_Proxied->m_Updater.getVersion();
+}
+
+IModInterface *OrganizerProxy::getMod(const QString &name)
+{
+ return m_Proxied->getMod(name);
+}
+
+IModInterface *OrganizerProxy::createMod(MOBase::GuessedValue<QString> &name)
+{
+ return m_Proxied->createMod(name);
+}
+
+bool OrganizerProxy::removeMod(IModInterface *mod)
+{
+ return m_Proxied->removeMod(mod);
+}
+
+void OrganizerProxy::modDataChanged(IModInterface*)
+{
+ m_Proxied->refreshModList();
+}
+
+QVariant OrganizerProxy::pluginSetting(const QString &pluginName, const QString &key) const
+{
+ return m_Proxied->m_Settings.pluginSetting(pluginName, key);
+}
+
+void OrganizerProxy::setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value)
+{
+ m_Proxied->m_Settings.setPluginSetting(pluginName, key, value);
+}
+
+QVariant OrganizerProxy::persistent(const QString &pluginName, const QString &key, const QVariant &def) const
+{
+ return m_Proxied->m_Settings.pluginPersistent(pluginName, key, def);
+}
+
+void OrganizerProxy::setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync)
+{
+ m_Proxied->m_Settings.setPluginPersistent(pluginName, key, value, sync);
+}
+
+QString OrganizerProxy::pluginDataPath() const
+{
+ QString pluginPath = QDir::fromNativeSeparators(ToQString(GameInfo::instance().getOrganizerDirectory())) + "/" + ToQString(AppConfig::pluginPath());
+ return pluginPath + "/data";
+}
+
+HANDLE OrganizerProxy::startApplication(const QString &executable, const QStringList &args, const QString &cwd, const QString &profile)
+{
+ return m_Proxied->startApplication(executable, args, cwd, profile);
+}
+
+bool OrganizerProxy::onAboutToRun(const std::function<bool (const QString &)> &func)
+{
+ auto conn = m_Proxied->m_AboutToRun.connect(func);
+ return conn.connected();
+}
+
+bool OrganizerProxy::onModInstalled(const std::function<void (const QString &)> &func)
+{
+ auto conn = m_Proxied->m_ModInstalled.connect(func);
+ return conn.connected();
+}
+
+void OrganizerProxy::refreshModList(bool saveChanges)
+{
+ m_Proxied->refreshModList(saveChanges);
+}
+
+IModInterface *OrganizerProxy::installMod(const QString &fileName)
+{
+ return m_Proxied->installMod(fileName);
+}
+
+QString OrganizerProxy::resolvePath(const QString &fileName) const
+{
+ if (m_Proxied->m_DirectoryStructure == NULL) {
+ return QString();
+ }
+ const FileEntry::Ptr file = m_Proxied->m_DirectoryStructure->searchFile(ToWString(fileName), NULL);
+ if (file.get() != NULL) {
+ return ToQString(file->getFullPath());
+ } else {
+ return QString();
+ }
+}
+
+QStringList OrganizerProxy::listDirectories(const QString &directoryName) const
+{
+ QStringList result;
+ DirectoryEntry *dir = m_Proxied->m_DirectoryStructure->findSubDirectoryRecursive(ToWString(directoryName));
+ if (dir != NULL) {
+ std::vector<DirectoryEntry*>::iterator current, end;
+ dir->getSubDirectories(current, end);
+ for (; current != end; ++current) {
+ result.append(ToQString((*current)->getName()));
+ }
+ }
+ return result;
+}
+
+QStringList OrganizerProxy::findFiles(const QString &path, const std::function<bool(const QString&)> &filter) const
+{
+ QStringList result;
+ DirectoryEntry *dir = m_Proxied->m_DirectoryStructure->findSubDirectoryRecursive(ToWString(path));
+ if (dir != NULL) {
+ std::vector<FileEntry::Ptr> files = dir->getFiles();
+ foreach (FileEntry::Ptr file, files) {
+ if (filter(ToQString(file->getFullPath()))) {
+ result.append(ToQString(file->getFullPath()));
+ }
+ }
+ } else {
+ qWarning("directory %s not found", qPrintable(path));
+ }
+ return result;
+}
+
+QList<MOBase::IOrganizer::FileInfo> OrganizerProxy::findFileInfos(const QString &path, const std::function<bool (const MOBase::IOrganizer::FileInfo &)> &filter) const
+{
+ return m_Proxied->findFileInfos(path, filter);
+}
+
+MOBase::IDownloadManager *OrganizerProxy::downloadManager()
+{
+ return &m_Proxied->m_DownloadManager;
+}
+
+MOBase::IPluginList *OrganizerProxy::pluginList()
+{
+ return &m_Proxied->m_PluginList;
+}
+
+MOBase::IModList *OrganizerProxy::modList()
+{
+ return &m_Proxied->m_ModList;
+}
diff --git a/src/organizerproxy.h b/src/organizerproxy.h new file mode 100644 index 00000000..017908e5 --- /dev/null +++ b/src/organizerproxy.h @@ -0,0 +1,50 @@ +#ifndef ORGANIZERPROXY_H
+#define ORGANIZERPROXY_H
+
+
+#include <imoinfo.h>
+#include "mainwindow.h"
+
+class OrganizerProxy : public MOBase::IOrganizer
+{
+public:
+ OrganizerProxy(MainWindow *window, const QString &pluginName);
+
+ virtual MOBase::IGameInfo &gameInfo() const;
+ virtual MOBase::IModRepositoryBridge *createNexusBridge() const;
+ virtual QString profileName() const;
+ virtual QString profilePath() const;
+ virtual QString downloadsPath() const;
+ virtual MOBase::VersionInfo appVersion() const;
+ virtual MOBase::IModInterface *getMod(const QString &name);
+ virtual MOBase::IModInterface *createMod(MOBase::GuessedValue<QString> &name);
+ virtual bool removeMod(MOBase::IModInterface *mod);
+ virtual void modDataChanged(MOBase::IModInterface *mod);
+ virtual QVariant pluginSetting(const QString &pluginName, const QString &key) const;
+ virtual void setPluginSetting(const QString &pluginName, const QString &key, const QVariant &value);
+ virtual QVariant persistent(const QString &pluginName, const QString &key, const QVariant &def = QVariant()) const;
+ virtual void setPersistent(const QString &pluginName, const QString &key, const QVariant &value, bool sync = true);
+ virtual QString pluginDataPath() const;
+ virtual MOBase::IModInterface *installMod(const QString &fileName);
+ virtual QString resolvePath(const QString &fileName) const;
+ virtual QStringList listDirectories(const QString &directoryName) const;
+ virtual QStringList findFiles(const QString &path, const std::function<bool(const QString &)> &filter) const;
+ virtual QList<FileInfo> findFileInfos(const QString &path, const std::function<bool(const FileInfo&)> &filter) const;
+
+ virtual MOBase::IDownloadManager *downloadManager();
+ virtual MOBase::IPluginList *pluginList();
+ virtual MOBase::IModList *modList();
+ virtual HANDLE startApplication(const QString &executable, const QStringList &args = QStringList(), const QString &cwd = "", const QString &profile = "");
+ virtual void refreshModList(bool saveChanges);
+
+ virtual bool onAboutToRun(const std::function<bool(const QString&)> &func);
+ virtual bool onModInstalled(const std::function<void (const QString &)> &func);
+
+private:
+ MainWindow *m_Proxied;
+ const QString &m_PluginName;
+
+};
+
+
+#endif // ORGANIZERPROXY_H
diff --git a/src/persistentcookiejar.cpp b/src/persistentcookiejar.cpp new file mode 100644 index 00000000..7b9694a2 --- /dev/null +++ b/src/persistentcookiejar.cpp @@ -0,0 +1,64 @@ +#include "persistentcookiejar.h"
+#include <QTemporaryFile>
+
+
+PersistentCookieJar::PersistentCookieJar(const QString &fileName, QObject *parent)
+: QNetworkCookieJar(parent), m_FileName(fileName)
+{
+ restore();
+}
+
+PersistentCookieJar::~PersistentCookieJar() {
+ qDebug("save %s", qPrintable(m_FileName));
+ save();
+}
+
+void PersistentCookieJar::save() {
+ QTemporaryFile file;
+ if (!file.open()) {
+ qCritical("failed to save cookies: couldn't create temporary file");
+ return;
+ }
+ QDataStream data(&file);
+
+ QList<QNetworkCookie> cookies = allCookies();
+ data << static_cast<quint32>(cookies.size());
+
+ foreach (const QNetworkCookie &cookie, allCookies()) {
+ data << cookie.toRawForm();
+ }
+
+ {
+ QFile oldCookies(m_FileName);
+ if (oldCookies.exists()) {
+ if (!oldCookies.remove()) {
+ qCritical("failed to save cookies: failed to remove %s", qPrintable(m_FileName));
+ return;
+ }
+ } // if it doesn't exists that's fine
+ }
+
+ if (!file.copy(m_FileName)) {
+ qCritical("failed to save cookies: failed to write %s", qPrintable(m_FileName));
+ }
+}
+
+void PersistentCookieJar::restore() {
+ QFile file(m_FileName);
+ if (!file.open(QIODevice::ReadOnly)) {
+ // not necessarily a problem, the file may just not exist (yet)
+ return;
+ }
+
+ QList<QNetworkCookie> allCookies;
+
+ QDataStream data(&file);
+ quint32 count;
+ data >> count;
+ for (quint32 i = 0; i < count; ++i) {
+ QByteArray cookieRaw;
+ data >> cookieRaw;
+ allCookies.append(QNetworkCookie::parseCookies(cookieRaw));
+ }
+ setAllCookies(allCookies);
+}
diff --git a/src/persistentcookiejar.h b/src/persistentcookiejar.h new file mode 100644 index 00000000..812b785c --- /dev/null +++ b/src/persistentcookiejar.h @@ -0,0 +1,24 @@ +#ifndef PERSISTENTCOOKIEJAR_H
+#define PERSISTENTCOOKIEJAR_H
+
+#include <QNetworkCookieJar>
+
+
+class PersistentCookieJar : public QNetworkCookieJar {
+public:
+ PersistentCookieJar(const QString &fileName, QObject *parent = 0);
+ virtual ~PersistentCookieJar();
+private:
+
+ void save();
+
+ void restore();
+
+private:
+
+ QString m_FileName;
+
+};
+
+
+#endif // PERSISTENTCOOKIEJAR_H
diff --git a/src/pluginflagicondelegate.cpp b/src/pluginflagicondelegate.cpp index 6c0bb29e..761555d7 100644 --- a/src/pluginflagicondelegate.cpp +++ b/src/pluginflagicondelegate.cpp @@ -11,8 +11,10 @@ PluginFlagIconDelegate::PluginFlagIconDelegate(QObject *parent) QList<QIcon> PluginFlagIconDelegate::getIcons(const QModelIndex &index) const
{
QList<QIcon> result;
- foreach (const QVariant &var, index.data(Qt::UserRole + 1).toList()) {
- result.append(var.value<QIcon>());
+ if (index.isValid()) {
+ foreach (const QVariant &var, index.data(Qt::UserRole + 1).toList()) {
+ result.append(var.value<QIcon>());
+ }
}
return result;
}
diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp index ec063994..24504f92 100644 --- a/src/pluginlist.cpp +++ b/src/pluginlist.cpp @@ -75,10 +75,9 @@ bool ByDate(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RHS) { } PluginList::PluginList(QObject *parent) - : QAbstractTableModel(parent) + : QAbstractItemModel(parent) , m_FontMetrics(QFont()) , m_SaveTimer(this) - , m_BOSS(NULL) { m_SaveTimer.setSingleShot(true); connect(&m_SaveTimer, SIGNAL(timeout()), this, SIGNAL(saveTimer())); @@ -95,11 +94,6 @@ PluginList::PluginList(QObject *parent) PluginList::~PluginList() { - if (m_BOSS != NULL) { - m_BOSS->CleanUpAPI(); - delete m_BOSS; - m_BOSS = NULL; - } } @@ -397,16 +391,19 @@ void PluginList::writePlugins(const QString &fileName, bool writeUnchecked) cons file->write(textCodec->fromUnicode("# This file was automatically generated by Mod Organizer.\r\n")); + QStringList saveList; + bool invalidFileNames = false; int writtenCount = 0; for (size_t i = 0; i < m_ESPs.size(); ++i) { int priority = m_ESPsByPriority[i]; - if ((m_ESPs[priority].m_Enabled || writeUnchecked) && !m_ESPs[priority].m_Removed) { + if (m_ESPs[priority].m_Enabled || writeUnchecked) { //file.write(m_ESPs[priority].m_Name.toUtf8()); if (!textCodec->canEncode(m_ESPs[priority].m_Name)) { invalidFileNames = true; qCritical("invalid plugin name %s", m_ESPs[priority].m_Name.toUtf8().constData()); } else { + saveList << m_ESPs[priority].m_Name; file->write(textCodec->fromUnicode(m_ESPs[priority].m_Name)); } file->write("\r\n"); @@ -419,9 +416,9 @@ void PluginList::writePlugins(const QString &fileName, bool writeUnchecked) cons "Please see mo_interface.log for a list of affected plugins and rename them.")); } - file.commit(); - - qDebug("%s saved", QDir::toNativeSeparators(fileName).toUtf8().constData()); + if (file.commitIfDifferent(m_LastSaveHash[fileName])) { + qDebug("%s saved", QDir::toNativeSeparators(fileName).toUtf8().constData()); + } } @@ -438,7 +435,11 @@ void PluginList::writeLockedOrder(const QString &fileName) const } -void PluginList::saveTo(const QString &pluginFileName, const QString &loadOrderFileName, const QString &lockedOrderFileName, const QString& deleterFileName, bool hideUnchecked) const +void PluginList::saveTo(const QString &pluginFileName + , const QString &loadOrderFileName + , const QString &lockedOrderFileName + , const QString& deleterFileName + , bool hideUnchecked) const { writePlugins(pluginFileName, false); writePlugins(loadOrderFileName, true); @@ -452,7 +453,7 @@ void PluginList::saveTo(const QString &pluginFileName, const QString &loadOrderF for (size_t i = 0; i < m_ESPs.size(); ++i) { int priority = m_ESPsByPriority[i]; - if (m_ESPs[priority].m_Removed) { + if (!m_ESPs[priority].m_Enabled) { deleterFile.write(m_ESPs[priority].m_Name.toUtf8()); deleterFile.write("\r\n"); } @@ -595,226 +596,7 @@ void PluginList::refreshLoadOrder() } -class boss_exception : public std::runtime_error { -public: - boss_exception(const std::string &message) : std::runtime_error(message) {} -}; - -#define THROW_BOSS_ERROR(obj) \ - uint8_t *message; \ - obj->GetLastErrorDetails(&message); \ - throw boss_exception(std::string(reinterpret_cast<char*>(message))); - -#define U8(text) reinterpret_cast<const uint8_t*>(text) - - -void outputBossLog(const QString &filename) -{ - QFile file(filename); - if (file.open(QIODevice::ReadOnly)) { - while (!file.atEnd()) { - QByteArray line = file.readLine().trimmed(); - if (line.length() < 1) - continue; - - int endFirstWord = line.indexOf(':'); - if (endFirstWord < 0) { - endFirstWord = 0; - } - QString firstWord = line.mid(0, endFirstWord); - if (firstWord == "DEBUG") { - qDebug("(boss) %s", line.mid(endFirstWord + 2).constData()); - } else { - qWarning("(boss) %s", line.mid(endFirstWord + 2).constData()); - } - } - } - file.resize(0); -} - - -boss_db PluginList::initBoss() -{ - boss_db result; - bool firstRun = (m_BOSS == nullptr); - if (firstRun) { - m_BOSS = new BossDLL(TEXT("dlls\\boss.dll")); - - if (!m_BOSS->IsCompatibleVersion(2,1,1)) { - throw MyException(tr("BOSS dll incompatible")); - } - uint8_t *versionString; - if (m_BOSS->GetVersionString(&versionString) != BossDLL::RESULT_OK) { - THROW_BOSS_ERROR(m_BOSS) - } - qDebug("using boss version %s", versionString); - - m_TempFile.open(); m_TempFile.close(); // yeah, stupid, but open is required to generate the name - m_BOSS->SetLoggerOutput(m_TempFile.fileName().toLocal8Bit().constData(), 4); - } - - if (m_BOSS->CreateBossDb(&result, BossDLL::SKYRIM, NULL) != BossDLL::RESULT_OK) { - uint8_t *message; - m_BOSS->GetLastErrorDetails(&message); - std::string messageCopy(reinterpret_cast<const char*>(message)); - delete m_BOSS; - m_BOSS = NULL; - throw boss_exception(messageCopy); - } - qApp->processEvents(); - - QString masterlistName = QDir::toNativeSeparators(qApp->applicationDirPath() + "/boss/masterlist.txt"); - - if (firstRun) { - uint32_t res = m_BOSS->UpdateMasterlist(result, U8(masterlistName.toUtf8().constData())); - qApp->processEvents(); - if (res == BossDLL::RESULT_OK) { - qDebug("boss masterlist updated"); - } else if (res == BossDLL::RESULT_NO_UPDATE_NECESSARY) { - qDebug("boss masterlist already up-to-date"); - } else { - THROW_BOSS_ERROR(m_BOSS) - } - } - if (m_BOSS->Load(result, - U8(masterlistName.toUtf8().constData()), - U8(QDir::toNativeSeparators(qApp->applicationDirPath() + "/boss/userlist.txt").toUtf8().constData())) != BossDLL::RESULT_OK) { - THROW_BOSS_ERROR(m_BOSS) - } - return result; -} - -void PluginList::convertPluginListForBoss(boss_db db, boost::ptr_vector<uint8_t> &inputPlugins, std::vector<uint8_t*> &activePlugins) -{ - foreach (int idx, m_ESPsByPriority) { - QString fileName = m_ESPs[idx].m_Name; - QByteArray name = fileName.toUtf8(); - - uint8_t *nameU8 = new uint8_t[name.length() + 1]; - memcpy(nameU8, name.constData(), name.length() + 1); - if (m_ESPs[idx].m_Enabled) { - activePlugins.push_back(nameU8); - } - inputPlugins.push_back(nameU8); - } - if (m_BOSS->SetActivePluginsDumb(db, &activePlugins[0], activePlugins.size()) != BossDLL::RESULT_OK) { - THROW_BOSS_ERROR(m_BOSS) - } -} - -void PluginList::applyBOSSSorting(boss_db db, std::map<int, QString> &lockedLoadOrder, uint8_t **pluginList, size_t size, - int &priority, int &loadOrder, bool recognized, const char *extension) -{ - for (size_t i = 0; i < size; ++i) { - QString name = QString::fromUtf8(reinterpret_cast<const char*>(pluginList[i])).toLower(); - if (name.endsWith(extension)) { - auto iter = m_ESPsByName.find(name); - if (iter == m_ESPsByName.end()) { - // boss seems to report plugins from userlist as sorted that aren't even installed - continue; - } - - BossMessage *message; - size_t numMessages = 0; - m_BOSS->GetPluginMessages(db, pluginList[i], &message, &numMessages); - BossInfo newInfo; - for (size_t im = 0; im < numMessages; ++im) { - newInfo.m_BOSSMessages.append(QString::fromUtf8(reinterpret_cast<const char*>(message[im].message))); - } - newInfo.m_BOSSUnrecognized = !recognized; - m_BossInfo[name] = newInfo; - // locked order plugins are not inserted by boss sorting ... - if (m_LockedOrder.find(name) != m_LockedOrder.end()) { - continue; - } - - // ... but by their enforced priority - while (lockedLoadOrder.find(loadOrder) != lockedLoadOrder.end()) { - auto lloIter = lockedLoadOrder.find(loadOrder); - auto nameIter = m_ESPsByName.find(lloIter->second); - if (nameIter != m_ESPsByName.end()) { - m_ESPs[nameIter->second].m_Priority = priority++; - if (m_ESPs[nameIter->second].m_Enabled) { - m_ESPs[nameIter->second].m_LoadOrder = loadOrder++; - } else { - m_ESPs[nameIter->second].m_LoadOrder = -1; - } - } - lockedLoadOrder.erase(lloIter); - } - - m_ESPs[iter->second].m_Priority = priority++; - if (m_ESPs[iter->second].m_Enabled) { - m_ESPs[iter->second].m_LoadOrder = loadOrder++; - } else { - m_ESPs[iter->second].m_LoadOrder = -1; - } - } - } -} - -void PluginList::bossSort() -{ - boss_db db = initBoss(); - ON_BLOCK_EXIT([&] { m_BOSS->DestroyBossDb(db); }); - - // create a boss-compatible representation of our current mod list. - boost::ptr_vector<uint8_t> inputPlugins; - std::vector<uint8_t*> activePlugins; - convertPluginListForBoss(db, inputPlugins, activePlugins); - - // sort mods in-memory - uint8_t **sortedPlugins; - uint8_t **unrecognizedPlugins; - size_t sizeSorted, sizeUnrecognized; - if (m_BOSS->SortCustomMods(db, - inputPlugins.c_array(), inputPlugins.size(), - &sortedPlugins, &sizeSorted, - &unrecognizedPlugins, &sizeUnrecognized) != BossDLL::RESULT_OK) { - THROW_BOSS_ERROR(m_BOSS) - } - - // output the log from boss to our own log to make it visible - outputBossLog(m_TempFile.fileName()); - - qDebug("%d sorted, %d unrecognized", sizeSorted, sizeUnrecognized); - - ChangeBracket<PluginList> layoutChange(this); - - std::map<int, QString> lockedLoadOrder; - std::for_each(m_LockedOrder.begin(), m_LockedOrder.end(), - [&lockedLoadOrder] (const std::pair<QString, int> &ele) { lockedLoadOrder[ele.second] = ele.first; }); - - int priority = 0; - int loadOrder = 0; - applyBOSSSorting(db, lockedLoadOrder, sortedPlugins, sizeSorted, priority, loadOrder, true, "esm"); - applyBOSSSorting(db, lockedLoadOrder, unrecognizedPlugins, sizeUnrecognized, priority, loadOrder, false, "esm"); - applyBOSSSorting(db, lockedLoadOrder, sortedPlugins, sizeSorted, priority, loadOrder, true, "esp"); - applyBOSSSorting(db, lockedLoadOrder, unrecognizedPlugins, sizeUnrecognized, priority, loadOrder, false, "esp"); - - // applyBOSSSorting removed entries from lockedLoadOrder when they were inserted so everything that's left is plugins - // locked to the end of the list. Now this inserts the rest in the ascending priority order. If the list is too short - // to place the plugins in their intended position then this guarantees plugins are kept in the correct relative order - // but it doesn't minimize the number of misplaced plugins. - for (auto iter = lockedLoadOrder.begin(); iter != lockedLoadOrder.end(); ++iter) { - auto nameIter = m_ESPsByName.find(iter->second); - if (nameIter != m_ESPsByName.end()) { - m_ESPs[nameIter->second].m_Priority = priority++; - if (m_ESPs[nameIter->second].m_Enabled) { - m_ESPs[nameIter->second].m_LoadOrder = loadOrder++; - } else { - m_ESPs[nameIter->second].m_LoadOrder = -1; - } - } - } - - // inform view of the changed data - updateIndices(); - layoutChange.finish(); - emit dataChanged(this->index(0, 0), this->index(m_ESPs.size(), columnCount())); - m_Refreshed(); -} IPluginList::PluginState PluginList::state(const QString &name) const { @@ -951,7 +733,16 @@ QVariant PluginList::data(const QModelIndex &modelIndex, int role) const } break; } } else if ((role == Qt::CheckStateRole) && (modelIndex.column() == 0)) { - return m_ESPs[index].m_Enabled ? Qt::Checked : Qt::Unchecked; + if (m_ESPs[index].m_ForceEnabled) { + return QVariant(); + } else { + return m_ESPs[index].m_Enabled ? Qt::Checked : Qt::Unchecked; + } + } else if (role == Qt::ForegroundRole) { + if ((modelIndex.column() == COL_NAME) && + m_ESPs[index].m_ForceEnabled) { + return QBrush(Qt::gray); + } } else if (role == Qt::FontRole) { QFont result; if (m_ESPs[index].m_IsMaster) { @@ -1026,9 +817,8 @@ QVariant PluginList::data(const QModelIndex &modelIndex, int role) const result.append(QIcon(":/MO/gui/edit_clear")); } return result; - } else { - return QVariant(); } + return QVariant(); } @@ -1070,13 +860,12 @@ QVariant PluginList::headerData(int section, Qt::Orientation orientation, Qt::ItemFlags PluginList::flags(const QModelIndex &modelIndex) const { int index = modelIndex.row(); - Qt::ItemFlags result = QAbstractTableModel::flags(modelIndex); + Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex); if (modelIndex.isValid()) { - if ((m_ESPs[index].m_ForceEnabled)) { - result &= ~Qt::ItemIsEnabled; + if (!m_ESPs[index].m_ForceEnabled) { + result |= Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled; } - result |= Qt::ItemIsUserCheckable | Qt::ItemIsDragEnabled; } else { result |= Qt::ItemIsDropEnabled; } @@ -1216,6 +1005,19 @@ bool PluginList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, return false; } +QModelIndex PluginList::index(int row, int column, const QModelIndex&) const +{ + if ((row < 0) || (row >= rowCount()) || (column < 0) || (column >= columnCount())) { + return QModelIndex(); + } + return createIndex(row, column, row); +} + +QModelIndex PluginList::parent(const QModelIndex&) const +{ + return QModelIndex(); +} + bool PluginList::eventFilter(QObject *obj, QEvent *event) { @@ -1227,7 +1029,6 @@ bool PluginList::eventFilter(QObject *obj, QEvent *event) } QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event); - // ctrl+up and ctrl+down -> increase or decrease priority of selected plugins if ((keyEvent->modifiers() == Qt::ControlModifier) && ((keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down))) { @@ -1285,7 +1086,7 @@ bool PluginList::eventFilter(QObject *obj, QEvent *event) PluginList::ESPInfo::ESPInfo(const QString &name, bool enabled, FILETIME time, const QString &originName, const QString &fullPath, bool hasIni) - : m_Name(name), m_Enabled(enabled), m_ForceEnabled(enabled), m_Removed(false), + : m_Name(name), m_Enabled(enabled), m_ForceEnabled(enabled), m_Priority(0), m_LoadOrder(-1), m_Time(time), m_OriginName(originName), m_HasIni(hasIni) { try { diff --git a/src/pluginlist.h b/src/pluginlist.h index 3d7f0926..ecd9c654 100644 --- a/src/pluginlist.h +++ b/src/pluginlist.h @@ -29,6 +29,7 @@ along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. #include <boost/signals2.hpp> #include <boost/ptr_container/ptr_vector.hpp> #include <vector> +#include <map> #include "pdll.h" #include <BOSS-API.h> @@ -69,7 +70,7 @@ private: /** * @brief model representing the plugins (.esp/.esm) in the current virtual data folder **/ -class PluginList : public QAbstractTableModel, public MOBase::IPluginList +class PluginList : public QAbstractItemModel, public MOBase::IPluginList { Q_OBJECT friend class ChangeBracket<PluginList>; @@ -181,9 +182,8 @@ public: void refreshLoadOrder(); - void bossSort(); - public: + virtual PluginState state(const QString &name) const; virtual int priority(const QString &name) const; virtual int loadOrder(const QString &name) const; @@ -201,8 +201,9 @@ public: // implementation of the QAbstractTableModel interface virtual Qt::ItemFlags flags(const QModelIndex &index) const; virtual Qt::DropActions supportedDropActions() const { return Qt::MoveAction; } virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent); + virtual QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const; + virtual QModelIndex parent(const QModelIndex &child) const; - void applyBOSSSorting(boss_db db, std::map<int, QString> &lockedLoadOrder, uint8_t **pluginList, size_t size, int &priority, int &loadOrder, bool recognized, const char *extension); public slots: /** @@ -237,7 +238,6 @@ private: QString m_Name; bool m_Enabled; bool m_ForceEnabled; - bool m_Removed; int m_Priority; int m_LoadOrder; FILETIME m_Time; @@ -258,82 +258,6 @@ private: friend bool ByDate(const ESPInfo& LHS, const ESPInfo& RHS); friend bool ByPriority(const ESPInfo& LHS, const ESPInfo& RHS); - class BossDLL : public PDLL { - DECLARE_CLASS(BossDLL) - - DECLARE_FUNCTION3(__cdecl, uint32_t, CreateBossDb, boss_db*, const uint32_t, const uint8_t*) - DECLARE_FUNCTION1(__cdecl, void, DestroyBossDb, boss_db) - DECLARE_FUNCTION0(__cdecl, void, CleanUpAPI) - - DECLARE_FUNCTION7(__cdecl, uint32_t, SortCustomMods, boss_db, uint8_t**, size_t, uint8_t***, size_t*, uint8_t***, size_t*) - DECLARE_FUNCTION3(__cdecl, uint32_t, SetActivePluginsDumb, boss_db, uint8_t**, const size_t) - - DECLARE_FUNCTION3(__cdecl, uint32_t, GetActivePluginsDumb , boss_db, uint8_t***, size_t*) - - DECLARE_FUNCTION2(__cdecl, uint32_t, UpdateMasterlist, boss_db, const uint8_t*) - DECLARE_FUNCTION3(__cdecl, uint32_t, Load, boss_db, const uint8_t*, const uint8_t*) - - DECLARE_FUNCTION2(__cdecl, void, SetLoggerOutput, const char*, uint8_t) - DECLARE_FUNCTION1(__cdecl, uint32_t, GetLastErrorDetails, uint8_t**) - - DECLARE_FUNCTION1(__cdecl, uint32_t, GetVersionString, uint8_t**) - DECLARE_FUNCTION3(__cdecl, bool, IsCompatibleVersion, const uint32_t, const uint32_t, const uint32_t) - - DECLARE_FUNCTION4(__cdecl, uint32_t, GetPluginMessages, boss_db, const uint8_t*, BossMessage**, size_t*) - - enum ResultCode { - RESULT_OK = 0, - RESULT_NO_MASTER_FILE = 1, - RESULT_FILE_READ_FAIL = 2, - RESULT_FILE_WRITE_FAIL = 3, - RESULT_FILE_NOT_UTF8 = 4, - RESULT_FILE_NOT_FOUND = 5, - RESULT_FILE_PARSE_FAIL = 6, - RESULT_CONDITION_EVAL_FAIL = 7, - RESULT_REGEX_EVAL_FAIL = 8, - RESULT_NO_GAME_DETECTED = 9, - RESULT_ENCODING_CONVERSION_FAIL = 10, - RESULT_FIND_ONLINE_MASTERLIST_REVISION_FAIL = 11, - RESULT_FIND_ONLINE_MASTERLIST_DATE_FAIL = 12, - RESULT_READ_UPDATE_FILE_LIST_FAIL = 13, - RESULT_FILE_CRC_MISMATCH = 14, - RESULT_FS_FILE_MOD_TIME_READ_FAIL = 15, - RESULT_FS_FILE_MOD_TIME_WRITE_FAIL = 16, - RESULT_FS_FILE_RENAME_FAIL = 17, - RESULT_FS_FILE_DELETE_FAIL = 18, - RESULT_FS_CREATE_DIRECTORY_FAIL = 19, - RESULT_FS_ITER_DIRECTORY_FAIL = 20, - RESULT_CURL_INIT_FAIL = 21, - RESULT_CURL_SET_ERRBUFF_FAIL = 22, - RESULT_CURL_SET_OPTION_FAIL = 23, - RESULT_CURL_SET_PROXY_FAIL = 24, - RESULT_CURL_SET_PROXY_TYPE_FAIL = 25, - RESULT_CURL_SET_PROXY_AUTH_FAIL = 26, - RESULT_CURL_SET_PROXY_AUTH_TYPE_FAIL = 27, - RESULT_CURL_PERFORM_FAIL = 28, - RESULT_CURL_USER_CANCEL = 29, - RESULT_GUI_WINDOW_INIT_FAIL = 30, - RESULT_NO_UPDATE_NECESSARY = 31, - RESULT_LO_MISMATCH = 32, - RESULT_NO_MEM = 33, - RESULT_INVALID_ARGS = 34, - RESULT_NETWORK_FAIL = 35, - RESULT_NO_INTERNET_CONNECTION = 36, - RESULT_NO_TAG_MAP = 37, - RESULT_PLUGINS_FULL = 38, - RESULT_PLUGIN_BEFORE_MASTER = 39, - RESULT_INVALID_SYNTAX = 40 - }; - - enum GameIDs { - AUTODETECT = 0, - OBLIVION = 1, - SKYRIM = 3, - FALLOUT3 = 4, - FALLOUTNV = 5 - }; - }; - private: void syncLoadOrder(); @@ -351,12 +275,10 @@ private: void testMasters(); - boss_db initBoss(); - void convertPluginListForBoss(boss_db db, boost::ptr_vector<uint8_t> &inputPlugins, std::vector<uint8_t*> &activePlugins); - private: std::vector<ESPInfo> m_ESPs; + mutable std::map<QString, uint> m_LastSaveHash; std::map<QString, int> m_ESPsByName; std::vector<int> m_ESPsByPriority; @@ -379,7 +301,6 @@ private: SignalRefreshed m_Refreshed; - BossDLL *m_BOSS; QTemporaryFile m_TempFile; }; diff --git a/src/profile.cpp b/src/profile.cpp index 41a867c5..8d02c047 100644 --- a/src/profile.cpp +++ b/src/profile.cpp @@ -93,7 +93,7 @@ Profile::Profile(const QDir& directory) GameInfo::instance().repairProfile(ToWString(m_Directory.absolutePath())); if (!QFile::exists(getIniFileName())) { - reportError(QObject::tr("\"%1\" is missing").arg(getIniFileName())); + reportError(QObject::tr("\"%1\" is missing or inaccessible").arg(getIniFileName())); } refreshModStatus(); } @@ -147,7 +147,6 @@ void Profile::writeModlistNow(bool onlyOnTimer) const m_SaveTimer->stop(); if (!m_Directory.exists()) return; -#pragma message("right now, this is doing unnecessary saves. Need a flag that says that mod priority, enabled-state or name of a mod has changed") try { QString fileName = getModlistFileName(); @@ -175,9 +174,9 @@ void Profile::writeModlistNow(bool onlyOnTimer) const } } - file.commit(); - - qDebug("%s saved", QDir::toNativeSeparators(fileName).toUtf8().constData()); + if (file.commitIfDifferent(m_LastModlistHash)) { + qDebug("%s saved", QDir::toNativeSeparators(fileName).toUtf8().constData()); + } } catch (const std::exception &e) { reportError(tr("failed to write mod list: %1").arg(e.what())); return; @@ -231,7 +230,7 @@ void Profile::refreshModStatus() { QFile file(getModlistFileName()); if (!file.exists()) { - throw MyException(QObject::tr("failed to find \"%1\"").arg(getModlistFileName())); + throw MyException(tr("\"%1\" is missing or inaccessible").arg(getModlistFileName())); } bool modStatusModified = false; diff --git a/src/profile.h b/src/profile.h index 4960671a..df27bb37 100644 --- a/src/profile.h +++ b/src/profile.h @@ -160,6 +160,11 @@ public: QString getLockedOrderFileName() const; /** + * @return the path of the modlist file in this profile + */ + QString getModlistFileName() const; + + /** * @return path of the archives file in this profile */ QString getArchivesFileName() const; @@ -301,7 +306,6 @@ private: void updateIndices(); - QString getModlistFileName() const; void copyFilesTo(QString &target) const; std::vector<std::wstring> splitDZString(const wchar_t *buffer) const; @@ -312,6 +316,7 @@ private: QDir m_Directory; + mutable uint m_LastModlistHash; std::vector<ModStatus> m_ModStatus; std::vector<unsigned int> m_ModIndexByPriority; unsigned int m_NumRegularMods; diff --git a/src/resources.qrc b/src/resources.qrc index 73921f64..3287ad5d 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -24,13 +24,13 @@ <file alias="previous">resources/go-previous_16.png</file> <file alias="refresh">resources/view-refresh_16.png</file> <file alias="update_available">resources/software-update-available.png</file> - <file>resources/emblem-important.png</file> + <file alias="important">resources/emblem-important.png</file> <file>resources/check.png</file> <file>mo_icon.ico</file> <file alias="warning">resources/dialog-warning.png</file> <file alias="emblem_backup">resources/symbol-backup.png</file> <file alias="icon_tools">resources/applications-accessories.png</file> - <file alias="emblem_problem">resources/emblem-unreadable.png</file> + <file alias="problem">resources/emblem-unreadable.png</file> <file>resources/internet-web-browser.png</file> <file alias="update">resources/system-software-update.png</file> <file alias="help">resources/help-browser_32.png</file> @@ -52,5 +52,18 @@ <file alias="version_date">resources/x-office-calendar.png</file> <file alias="warning_16">resources/dialog-warning_16.png</file> <file alias="attachment">resources/mail-attachment.png</file> + <file alias="backup">resources/document-save_32.png</file> + <file alias="restore">resources/edit-undo.png</file> + <file alias="sort">resources/arrange-boxes.png</file> + <file alias="badge_1">resources/badge_1.png</file> + <file alias="badge_2">resources/badge_2.png</file> + <file alias="badge_3">resources/badge_3.png</file> + <file alias="badge_4">resources/badge_4.png</file> + <file alias="badge_5">resources/badge_5.png</file> + <file alias="badge_6">resources/badge_6.png</file> + <file alias="badge_7">resources/badge_7.png</file> + <file alias="badge_8">resources/badge_8.png</file> + <file alias="badge_9">resources/badge_9.png</file> + <file alias="badge_more">resources/badge_more.png</file> </qresource> </RCC> diff --git a/src/resources/arrange-boxes.png b/src/resources/arrange-boxes.png Binary files differnew file mode 100644 index 00000000..b1ab67cf --- /dev/null +++ b/src/resources/arrange-boxes.png diff --git a/src/resources/badge_1.png b/src/resources/badge_1.png Binary files differnew file mode 100644 index 00000000..5a525b50 --- /dev/null +++ b/src/resources/badge_1.png diff --git a/src/resources/badge_2.png b/src/resources/badge_2.png Binary files differnew file mode 100644 index 00000000..93ac67e2 --- /dev/null +++ b/src/resources/badge_2.png diff --git a/src/resources/badge_3.png b/src/resources/badge_3.png Binary files differnew file mode 100644 index 00000000..f21a7bef --- /dev/null +++ b/src/resources/badge_3.png diff --git a/src/resources/badge_4.png b/src/resources/badge_4.png Binary files differnew file mode 100644 index 00000000..becfaf46 --- /dev/null +++ b/src/resources/badge_4.png diff --git a/src/resources/badge_5.png b/src/resources/badge_5.png Binary files differnew file mode 100644 index 00000000..08ef8b16 --- /dev/null +++ b/src/resources/badge_5.png diff --git a/src/resources/badge_6.png b/src/resources/badge_6.png Binary files differnew file mode 100644 index 00000000..1a08d784 --- /dev/null +++ b/src/resources/badge_6.png diff --git a/src/resources/badge_7.png b/src/resources/badge_7.png Binary files differnew file mode 100644 index 00000000..c173a347 --- /dev/null +++ b/src/resources/badge_7.png diff --git a/src/resources/badge_8.png b/src/resources/badge_8.png Binary files differnew file mode 100644 index 00000000..32c7b675 --- /dev/null +++ b/src/resources/badge_8.png diff --git a/src/resources/badge_9.png b/src/resources/badge_9.png Binary files differnew file mode 100644 index 00000000..bf5f2cc8 --- /dev/null +++ b/src/resources/badge_9.png diff --git a/src/resources/badge_more.png b/src/resources/badge_more.png Binary files differnew file mode 100644 index 00000000..e0f27f24 --- /dev/null +++ b/src/resources/badge_more.png diff --git a/src/resources/document-save_32.png b/src/resources/document-save_32.png Binary files differnew file mode 100644 index 00000000..db5c52b7 --- /dev/null +++ b/src/resources/document-save_32.png diff --git a/src/resources/edit-undo.png b/src/resources/edit-undo.png Binary files differnew file mode 100644 index 00000000..61b2ce9a --- /dev/null +++ b/src/resources/edit-undo.png diff --git a/src/safewritefile.cpp b/src/safewritefile.cpp index 6df8c2b8..626413dd 100644 --- a/src/safewritefile.cpp +++ b/src/safewritefile.cpp @@ -46,3 +46,23 @@ void SafeWriteFile::commit() { m_TempFile.setAutoRemove(false);
m_TempFile.close();
}
+
+bool SafeWriteFile::commitIfDifferent(uint &inHash) {
+ uint newHash = hash();
+ if (newHash != inHash) {
+ commit();
+ inHash = newHash;
+ return true;
+ } else {
+ return false;
+ }
+}
+
+uint SafeWriteFile::hash()
+{
+ qint64 pos = m_TempFile.pos();
+ m_TempFile.seek(0);
+ QByteArray data = m_TempFile.readAll();
+ m_TempFile.seek(pos);
+ return qHash(data);
+}
diff --git a/src/safewritefile.h b/src/safewritefile.h index 56bd7744..06c22acc 100644 --- a/src/safewritefile.h +++ b/src/safewritefile.h @@ -37,6 +37,10 @@ public: void commit();
+ bool commitIfDifferent(uint &hash);
+
+ uint hash();
+
private:
QString m_FileName;
QTemporaryFile m_TempFile;
diff --git a/src/selectiondialog.cpp b/src/selectiondialog.cpp index dbc10791..902b4d9c 100644 --- a/src/selectiondialog.cpp +++ b/src/selectiondialog.cpp @@ -17,62 +17,67 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. */ -#include "selectiondialog.h"
-#include "ui_selectiondialog.h"
-
-#include <QCommandLinkButton>
-
-SelectionDialog::SelectionDialog(const QString &description, QWidget *parent)
- : QDialog(parent), ui(new Ui::SelectionDialog), m_Choice(NULL), m_ValidateByData(false)
-{
- ui->setupUi(this);
-
- ui->descriptionLabel->setText(description);
-}
-
-SelectionDialog::~SelectionDialog()
-{
- delete ui;
-}
-
-
-void SelectionDialog::addChoice(const QString &buttonText, const QString &description, const QVariant &data)
-{
- QCommandLinkButton *button = new QCommandLinkButton(buttonText, description, ui->buttonBox);
- button->setProperty("data", data);
- ui->buttonBox->addButton(button, QDialogButtonBox::AcceptRole);
- if (data.isValid()) m_ValidateByData = true;
-}
-
-
-QVariant SelectionDialog::getChoiceData()
-{
- return m_Choice->property("data");
-}
-
-
-QString SelectionDialog::getChoiceString()
-{
- if ((m_Choice == NULL) ||
- (m_ValidateByData && !m_Choice->property("data").isValid())) {
- return QString();
- } else {
- return m_Choice->text();
- }
-}
-
-
-void SelectionDialog::on_buttonBox_clicked(QAbstractButton *button)
-{
- m_Choice = button;
- if (!m_ValidateByData || m_Choice->property("data").isValid()) {
- this->accept();
- } else {
- this->reject();
- }
-}
-
-void SelectionDialog::on_cancelButton_clicked()
-{
- this->reject();
-}
+#include "selectiondialog.h" +#include "ui_selectiondialog.h" + +#include <QCommandLinkButton> + +SelectionDialog::SelectionDialog(const QString &description, QWidget *parent) + : QDialog(parent), ui(new Ui::SelectionDialog), m_Choice(NULL), m_ValidateByData(false) +{ + ui->setupUi(this); + + ui->descriptionLabel->setText(description); +} + +SelectionDialog::~SelectionDialog() +{ + delete ui; +} + + +void SelectionDialog::addChoice(const QString &buttonText, const QString &description, const QVariant &data) +{ + QCommandLinkButton *button = new QCommandLinkButton(buttonText, description, ui->buttonBox); + button->setProperty("data", data); + ui->buttonBox->addButton(button, QDialogButtonBox::AcceptRole); + if (data.isValid()) m_ValidateByData = true; +} + +int SelectionDialog::numChoices() const +{ + return ui->buttonBox->findChildren<QCommandLinkButton*>(QString()).count(); +} + + +QVariant SelectionDialog::getChoiceData() +{ + return m_Choice->property("data"); +} + + +QString SelectionDialog::getChoiceString() +{ + if ((m_Choice == NULL) || + (m_ValidateByData && !m_Choice->property("data").isValid())) { + return QString(); + } else { + return m_Choice->text(); + } +} + + +void SelectionDialog::on_buttonBox_clicked(QAbstractButton *button) +{ + m_Choice = button; + if (!m_ValidateByData || m_Choice->property("data").isValid()) { + this->accept(); + } else { + this->reject(); + } +} + +void SelectionDialog::on_cancelButton_clicked() +{ + this->reject(); +} diff --git a/src/selectiondialog.h b/src/selectiondialog.h index a2844e97..fc04291e 100644 --- a/src/selectiondialog.h +++ b/src/selectiondialog.h @@ -17,50 +17,52 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see <http://www.gnu.org/licenses/>. */ -#ifndef SELECTIONDIALOG_H
-#define SELECTIONDIALOG_H
-
-#include <QDialog>
-#include <QAbstractButton>
-
-namespace Ui {
-class SelectionDialog;
-}
-
-class SelectionDialog : public QDialog
-{
- Q_OBJECT
-
-public:
-
- explicit SelectionDialog(const QString &description, QWidget *parent = 0);
-
- ~SelectionDialog();
-
- /**
- * @brief add a choice to the dialog
- * @param buttonText the text to be displayed on the button
- * @param description the description that shows up under in small letters inside the button
- * @param data data to be stored with the button. Please note that as soon as one choice has data associated with it (non-invalid QVariant)
- * all buttons that contain no data will be treated as "cancel" buttons
- */
- void addChoice(const QString &buttonText, const QString &description, const QVariant &data);
-
- QVariant getChoiceData();
- QString getChoiceString();
-
-private slots:
-
- void on_buttonBox_clicked(QAbstractButton *button);
-
- void on_cancelButton_clicked();
-
-private:
-
- Ui::SelectionDialog *ui;
- QAbstractButton *m_Choice;
- bool m_ValidateByData;
-
-};
-
-#endif // SELECTIONDIALOG_H
+#ifndef SELECTIONDIALOG_H +#define SELECTIONDIALOG_H + +#include <QDialog> +#include <QAbstractButton> + +namespace Ui { +class SelectionDialog; +} + +class SelectionDialog : public QDialog +{ + Q_OBJECT + +public: + + explicit SelectionDialog(const QString &description, QWidget *parent = 0); + + ~SelectionDialog(); + + /** + * @brief add a choice to the dialog + * @param buttonText the text to be displayed on the button + * @param description the description that shows up under in small letters inside the button + * @param data data to be stored with the button. Please note that as soon as one choice has data associated with it (non-invalid QVariant) + * all buttons that contain no data will be treated as "cancel" buttons + */ + void addChoice(const QString &buttonText, const QString &description, const QVariant &data); + + int numChoices() const; + + QVariant getChoiceData(); + QString getChoiceString(); + +private slots: + + void on_buttonBox_clicked(QAbstractButton *button); + + void on_cancelButton_clicked(); + +private: + + Ui::SelectionDialog *ui; + QAbstractButton *m_Choice; + bool m_ValidateByData; + +}; + +#endif // SELECTIONDIALOG_H diff --git a/src/selfupdater.cpp b/src/selfupdater.cpp index 14df8566..0895c5ab 100644 --- a/src/selfupdater.cpp +++ b/src/selfupdater.cpp @@ -99,7 +99,7 @@ void SelfUpdater::testForUpdate() if (m_UpdateRequestID == -1) { m_UpdateRequestID = m_Interface->requestDescription( SkyrimInfo::getNexusModIDStatic(), this, QVariant(), - ToQString(SkyrimInfo::getNexusInfoUrlStatic())); + QString(), ToQString(SkyrimInfo::getNexusInfoUrlStatic())); } } diff --git a/src/settings.cpp b/src/settings.cpp index d9a7e799..d63aabe4 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -242,6 +242,16 @@ bool Settings::getNexusLogin(QString &username, QString &password) const } } +bool Settings::compactDownloads() const +{ + return m_Settings.value("Settings/compact_downloads", false).toBool(); +} + +bool Settings::metaDownloads() const +{ + return m_Settings.value("Settings/meta_downloads", false).toBool(); +} + bool Settings::offlineMode() const { return m_Settings.value("Settings/offline_mode", false).toBool(); @@ -508,7 +518,8 @@ void Settings::query(QWidget *parent) QComboBox *languageBox = dialog.findChild<QComboBox*>("languageBox"); QComboBox *styleBox = dialog.findChild<QComboBox*>("styleBox"); QComboBox *logLevelBox = dialog.findChild<QComboBox*>("logLevelBox"); -// QCheckBox *handleNXMBox = dialog.findChild<QCheckBox*>("handleNXMBox"); + QCheckBox *compactBox = dialog.findChild<QCheckBox*>("compactBox"); + QCheckBox *showMetaBox = dialog.findChild<QCheckBox*>("showMetaBox"); QLineEdit *downloadDirEdit = dialog.findChild<QLineEdit*>("downloadDirEdit"); QLineEdit *modDirEdit = dialog.findChild<QLineEdit*>("modDirEdit"); @@ -587,6 +598,9 @@ void Settings::query(QWidget *parent) } } + compactBox->setChecked(compactDownloads()); + showMetaBox->setChecked(metaDownloads()); + hideUncheckedBox->setChecked(hideUncheckedPlugins()); forceEnableBox->setChecked(forceEnableCoreFiles()); @@ -651,6 +665,8 @@ void Settings::query(QWidget *parent) m_Settings.setValue("Settings/hide_unchecked_plugins", hideUncheckedBox->checkState() ? true : false); m_Settings.setValue("Settings/force_enable_core_files", forceEnableBox->checkState() ? true : false); + m_Settings.setValue("Settings/compact_downloads", compactBox->isChecked()); + m_Settings.setValue("Settings/meta_downloads", showMetaBox->isChecked()); m_Settings.setValue("Settings/load_mechanism", mechanismBox->itemData(mechanismBox->currentIndex()).toInt()); if (QDir(downloadDirEdit->text()).exists()) { m_Settings.setValue("Settings/download_directory", QDir::toNativeSeparators(downloadDirEdit->text())); diff --git a/src/settings.h b/src/settings.h index 81174440..ccb70d8a 100644 --- a/src/settings.h +++ b/src/settings.h @@ -143,6 +143,16 @@ public: bool offlineMode() const; /** + * @return true if the user chose compact downloads + */ + bool compactDownloads() const; + + /** + * @return true if the user chose meta downloads + */ + bool metaDownloads() const; + + /** * @return the configured log level */ int logLevel() const; diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index bda6726c..2e0ffae1 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -194,6 +194,35 @@ p, li { white-space: pre-wrap; } </widget>
</item>
<item>
+ <widget class="QGroupBox" name="groupBox">
+ <property name="title">
+ <string>User interface</string>
+ </property>
+ <layout class="QVBoxLayout" name="verticalLayout_11">
+ <item>
+ <widget class="QCheckBox" name="compactBox">
+ <property name="toolTip">
+ <string>If checked, the download interface will be more compact.</string>
+ </property>
+ <property name="text">
+ <string>Compact Download Interface</string>
+ </property>
+ </widget>
+ </item>
+ <item>
+ <widget class="QCheckBox" name="showMetaBox">
+ <property name="toolTip">
+ <string>If checked, the download list will display meta information instead of file names.</string>
+ </property>
+ <property name="text">
+ <string>Download Meta Information</string>
+ </property>
+ </widget>
+ </item>
+ </layout>
+ </widget>
+ </item>
+ <item>
<widget class="QPushButton" name="resetDialogsButton">
<property name="maximumSize">
<size>
diff --git a/src/shared/directoryentry.cpp b/src/shared/directoryentry.cpp index a232ea19..6311ba25 100644 --- a/src/shared/directoryentry.cpp +++ b/src/shared/directoryentry.cpp @@ -235,6 +235,9 @@ std::vector<FileEntry::Ptr> FilesOrigin::getFiles() const void FileEntry::addOrigin(int origin, FILETIME fileTime, const std::wstring &archive)
{
+ if (m_Parent != NULL) {
+ m_Parent->propagateOrigin(origin);
+ }
if (m_Origin == -1) {
m_Origin = origin;
m_FileTime = fileTime;
@@ -385,29 +388,25 @@ std::wstring FileEntry::getRelativePath() const //
DirectoryEntry::DirectoryEntry(const std::wstring &name, DirectoryEntry *parent, int originID)
: m_OriginConnection(new OriginConnection),
- m_Name(name), m_Parent(parent), m_Populated(false), m_Origin(originID), m_TopLevel(true)
+ m_Name(name), m_Parent(parent), m_Populated(false), m_TopLevel(true)
{
m_FileRegister.reset(new FileRegister(m_OriginConnection));
+ m_Origins.insert(originID);
LEAK_TRACE;
}
DirectoryEntry::DirectoryEntry(const std::wstring &name, DirectoryEntry *parent, int originID,
boost::shared_ptr<FileRegister> fileRegister, boost::shared_ptr<OriginConnection> originConnection)
: m_FileRegister(fileRegister), m_OriginConnection(originConnection),
- m_Name(name), m_Parent(parent), m_Populated(false), m_Origin(originID), m_TopLevel(false)
+ m_Name(name), m_Parent(parent), m_Populated(false), m_TopLevel(false)
{
LEAK_TRACE;
+ m_Origins.insert(originID);
}
DirectoryEntry::~DirectoryEntry()
{
-/* if (m_TopLevel) {
- if (m_FileRegister.use_count() > 1) {
-log("this should not happen");
- delete m_FileRegister.get();
- }
- }*/
LEAK_UNTRACE;
clear();
}
@@ -479,6 +478,14 @@ void DirectoryEntry::addFromBSA(const std::wstring &originName, std::wstring &di m_Populated = true;
}
+void DirectoryEntry::propagateOrigin(int origin)
+{
+ m_Origins.insert(origin);
+ if (m_Parent != NULL) {
+ m_Parent->propagateOrigin(origin);
+ }
+}
+
static bool SupportOptimizedFind()
{
@@ -603,6 +610,10 @@ void DirectoryEntry::removeDir(const std::wstring &path) }
}
+bool DirectoryEntry::hasContentsFromOrigin(int originID) const
+{
+ return m_Origins.find(originID) != m_Origins.end();
+}
void DirectoryEntry::insertFile(const std::wstring &filePath, FilesOrigin &origin, FILETIME fileTime)
{
@@ -656,7 +667,7 @@ int DirectoryEntry::anyOrigin() const return res;
}
}
- return m_Origin;
+ return *(m_Origins.begin());
}
diff --git a/src/shared/directoryentry.h b/src/shared/directoryentry.h index 15af5e9e..f691603f 100644 --- a/src/shared/directoryentry.h +++ b/src/shared/directoryentry.h @@ -213,6 +213,8 @@ public: void addFromOrigin(const std::wstring &originName, const std::wstring &directory, int priority);
void addFromBSA(const std::wstring &originName, std::wstring &directory, const std::wstring &fileName, int priority);
+ void propagateOrigin(int origin);
+
const std::wstring &getName() const;
boost::shared_ptr<FileRegister> getFileRegister() { return m_FileRegister; }
@@ -269,6 +271,8 @@ public: }
}
+ bool hasContentsFromOrigin(int originID) const;
+
private:
DirectoryEntry(const DirectoryEntry &reference);
@@ -281,6 +285,7 @@ private: file = m_FileRegister->getFile(iter->second);
} else {
file = m_FileRegister->createFile(fileName, this);
+ // TODO this has been observed to cause a crash, no clue why
m_Files[fileName] = file->getIndex();
}
file->addOrigin(origin.getID(), fileTime, archive);
@@ -318,7 +323,7 @@ private: std::vector<DirectoryEntry*> m_SubDirectories;
DirectoryEntry *m_Parent;
- int m_Origin;
+ std::set<int> m_Origins;
bool m_Populated;
diff --git a/src/shared/error_report.cpp b/src/shared/error_report.cpp index c1b25229..02ff2d56 100644 --- a/src/shared/error_report.cpp +++ b/src/shared/error_report.cpp @@ -36,7 +36,6 @@ void reportError(LPCSTR format, ...) va_end(argList);
MessageBoxA(NULL, buffer, "Error", MB_OK | MB_ICONERROR);
- LocalFree(buffer);
}
void reportError(LPCWSTR format, ...)
@@ -52,7 +51,6 @@ void reportError(LPCWSTR format, ...) va_end(argList);
MessageBoxW(NULL, buffer, L"Error", MB_OK | MB_ICONERROR);
- LocalFree(buffer);
}
@@ -99,4 +97,4 @@ std::wstring getCurrentErrorStringW() return result;
}
}
-} // namespace MOShared +} // namespace MOShared
diff --git a/src/shared/gameinfo.cpp b/src/shared/gameinfo.cpp index 00bb42fd..b580a226 100644 --- a/src/shared/gameinfo.cpp +++ b/src/shared/gameinfo.cpp @@ -172,6 +172,14 @@ std::wstring GameInfo::getLogDir() const }
+std::wstring GameInfo::getLootDir() const
+{
+ std::wostringstream temp;
+ temp << m_OrganizerDirectory << "\\loot";
+ return temp.str();
+}
+
+
std::wstring GameInfo::getTutorialDir() const
{
std::wostringstream temp;
diff --git a/src/shared/gameinfo.h b/src/shared/gameinfo.h index 10775e6c..89c9402d 100644 --- a/src/shared/gameinfo.h +++ b/src/shared/gameinfo.h @@ -113,6 +113,7 @@ public: virtual std::wstring getCacheDir() const;
virtual std::wstring getOverwriteDir() const;
virtual std::wstring getLogDir() const;
+ virtual std::wstring getLootDir() const;
virtual std::wstring getTutorialDir() const;
virtual bool requiresBSAInvalidation() const { return true; }
diff --git a/src/shared/skyriminfo.cpp b/src/shared/skyriminfo.cpp index 1620bcc3..bf7500e6 100644 --- a/src/shared/skyriminfo.cpp +++ b/src/shared/skyriminfo.cpp @@ -106,7 +106,7 @@ GameInfo::LoadOrderMechanism SkyrimInfo::getLoadOrderMechanism() const return TYPE_FILETIME;
}
} catch (const std::exception &e) {
- reportError("TESV.exe is invalid: %s", e.what());
+ log("TESV.exe is invalid: %s", e.what());
return TYPE_FILETIME;
}
}
diff --git a/src/singleinstance.cpp b/src/singleinstance.cpp index 029e8366..2befab70 100644 --- a/src/singleinstance.cpp +++ b/src/singleinstance.cpp @@ -85,7 +85,7 @@ void SingleInstance::sendMessage(const QString &message) socket.write(message.toUtf8()); if (!socket.waitForBytesWritten(s_Timeout)) { - reportError(tr("failed to connect to running instance: %1").arg(socket.errorString())); + reportError(tr("failed to communicate with running instance: %1").arg(socket.errorString())); return; } diff --git a/src/spawn.cpp b/src/spawn.cpp index b1c5e963..6adafba0 100644 --- a/src/spawn.cpp +++ b/src/spawn.cpp @@ -36,10 +36,23 @@ using namespace MOShared; static const int BUFSIZE = 4096; -bool spawn(LPCWSTR binary, LPCWSTR arguments, LPCWSTR currentDirectory, bool suspended, HANDLE& processHandle, HANDLE& threadHandle) +bool spawn(LPCWSTR binary, LPCWSTR arguments, LPCWSTR currentDirectory, bool suspended, + HANDLE stdOut, HANDLE stdErr, + HANDLE& processHandle, HANDLE& threadHandle) { + BOOL inheritHandles = FALSE; STARTUPINFO si; ::ZeroMemory(&si, sizeof(si)); + if (stdOut != INVALID_HANDLE_VALUE) { + si.hStdOutput = stdOut; + inheritHandles = TRUE; + si.dwFlags |= STARTF_USESTDHANDLES; + } + if (stdErr != INVALID_HANDLE_VALUE) { + si.hStdError = stdErr; + inheritHandles = TRUE; + si.dwFlags |= STARTF_USESTDHANDLES; + } si.cb = sizeof(si); int length = wcslen(binary) + wcslen(arguments) + 4; wchar_t *commandLine = NULL; @@ -74,7 +87,7 @@ bool spawn(LPCWSTR binary, LPCWSTR arguments, LPCWSTR currentDirectory, bool sus BOOL success = ::CreateProcess(NULL, commandLine, NULL, NULL, // no special process or thread attributes - FALSE, // don't inherit handle + inheritHandles, // inherit handles if we plan to use stdout or stderr reroute suspended ? CREATE_SUSPENDED : 0, // create suspended so I have time to inject the DLL NULL, // same environment as parent currentDirectory, // current directory @@ -95,14 +108,22 @@ bool spawn(LPCWSTR binary, LPCWSTR arguments, LPCWSTR currentDirectory, bool sus } -HANDLE startBinary(const QFileInfo &binary, const QString &arguments, const QString& profileName, int logLevel, const QDir ¤tDirectory, bool hooked) +HANDLE startBinary(const QFileInfo &binary, + const QString &arguments, + const QString& profileName, + int logLevel, + const QDir ¤tDirectory, + bool hooked, + HANDLE stdOut, + HANDLE stdErr) { HANDLE processHandle, threadHandle; std::wstring binaryName = ToWString(QDir::toNativeSeparators(binary.absoluteFilePath())); std::wstring currentDirectoryName = ToWString(QDir::toNativeSeparators(currentDirectory.absolutePath())); try { - if (!spawn(binaryName.c_str(), ToWString(arguments).c_str(), currentDirectoryName.c_str(), hooked, processHandle, threadHandle)) { + if (!spawn(binaryName.c_str(), ToWString(arguments).c_str(), currentDirectoryName.c_str(), hooked, + stdOut, stdErr, processHandle, threadHandle)) { reportError(QObject::tr("failed to spawn \"%1\"").arg(binary.fileName())); return INVALID_HANDLE_VALUE; } diff --git a/src/spawn.h b/src/spawn.h index 48320fea..3f037119 100644 --- a/src/spawn.h +++ b/src/spawn.h @@ -52,11 +52,17 @@ private: * @param arguments arguments to pass to the binary * @param profileName name of the active profile * @param currentDirectory the directory to use as the working directory to run in + * @param logLevel log level to be used by the hook library. Ignored if hooked is false * @param hooked if set, the binary is started with mo injected + * @param stdout if not equal to INVALID_HANDLE_VALUE, this is used as stdout for the process + * @param stderr if not equal to INVALID_HANDLE_VALUE, this is used as stderr for the process * @return the process handle * @todo is the profile name even used any more? * @todo is the hooked parameter used? **/ -HANDLE startBinary(const QFileInfo &binary, const QString &arguments, const QString &profileName, int logLevel, const QDir ¤tDirectory, bool hooked); +HANDLE startBinary(const QFileInfo &binary, const QString &arguments, const QString &profileName, int logLevel, + const QDir ¤tDirectory, bool hooked, + HANDLE stdOut = INVALID_HANDLE_VALUE, HANDLE stdErr = INVALID_HANDLE_VALUE); #endif // SPAWN_H + diff --git a/src/version.rc b/src/version.rc index 33c25b1c..5e9b62c5 100644 --- a/src/version.rc +++ b/src/version.rc @@ -1,7 +1,7 @@ #include "Winver.h"
-#define VER_FILEVERSION 1,1,3,0
-#define VER_FILEVERSION_STR "1,1,3,0\0"
+#define VER_FILEVERSION 1,2,2,0
+#define VER_FILEVERSION_STR "1,2,2,0\0"
VS_VERSION_INFO VERSIONINFO
FILEVERSION VER_FILEVERSION
|
