From 5d1154c24e2475ed2b7ac248de27ab7b501a1e5e Mon Sep 17 00:00:00 2001 From: Tannin Date: Sat, 16 Feb 2013 19:09:57 +0100 Subject: - hooks for CreateHardLink - createprocess hook will now reroute the cwd (fixes SUM, may break other tools?) - profile code moved to separate file - executables can now be linked to toolbar - category filters are now represented as a tree - csv export of mod list - ModInfoDialog is now "window modal" instead of "application modal" - nexus dialog now updates the mod-id field while browsing - right-click in nexus browser allows to open in external browser - ini viewer is no longer modal - bugfix: another attempt to fix processing of invalid header lines in fomod xmls (bom now handled) - bugfix: integrated fomod installer will no longer overwrite detected mod name by the one from the xml - bugfix: "hide file" from conflicted files list now updates that list - bugfix: conflicted files list no longer offers to hide files in BSAs - bugfix: pressing delete on the mod list with multiplie files selected offered to delete the wrong files - regression: mod name guessing with the old regular expression was less likely to lead to an empty mod name. Now both are tried --- src/baincomplexinstallerdialog.cpp | 202 +- src/csvbuilder.cpp | 272 ++ src/csvbuilder.h | 93 + src/downloadmanager.cpp | 1817 +++++---- src/editexecutablesdialog.cpp | 3 +- src/executableslist.cpp | 366 +- src/executableslist.h | 262 +- src/fomodinstallerdialog.cpp | 1857 ++++----- src/fomodinstallerdialog.h | 386 +- src/installationmanager.cpp | 94 +- src/installationmanager.h | 382 +- src/main.cpp | 844 ++-- src/mainwindow.cpp | 7616 ++++++++++++++++++------------------ src/mainwindow.h | 815 ++-- src/mainwindow.ui | 10 +- src/modinfo.cpp | 1651 ++++---- src/modinfo.h | 1612 ++++---- src/modinfodialog.cpp | 2397 ++++++------ src/modinfodialog.h | 397 +- src/modlist.cpp | 1301 +++--- src/modlistsortproxy.cpp | 425 +- src/modlistsortproxy.h | 124 +- src/nexusdialog.cpp | 618 +-- src/nexusdialog.h | 283 +- src/nexusinterface.cpp | 903 ++--- src/nexusview.cpp | 167 +- src/nexusview.h | 146 +- src/organizer.pro | 30 +- src/savetextasdialog.cpp | 46 + src/savetextasdialog.h | 31 + src/savetextasdialog.ui | 69 + src/shared/directoryentry.cpp | 1560 ++++---- src/textviewer.h | 93 - src/version.rc | 4 +- 34 files changed, 13790 insertions(+), 13086 deletions(-) create mode 100644 src/csvbuilder.cpp create mode 100644 src/csvbuilder.h create mode 100644 src/savetextasdialog.cpp create mode 100644 src/savetextasdialog.h create mode 100644 src/savetextasdialog.ui delete mode 100644 src/textviewer.h (limited to 'src') diff --git a/src/baincomplexinstallerdialog.cpp b/src/baincomplexinstallerdialog.cpp index c21fa5ad..9b282c82 100644 --- a/src/baincomplexinstallerdialog.cpp +++ b/src/baincomplexinstallerdialog.cpp @@ -17,104 +17,104 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include "baincomplexinstallerdialog.h" -#include "textviewer.h" -#include "ui_baincomplexinstallerdialog.h" - -#include - -BainComplexInstallerDialog::BainComplexInstallerDialog(DirectoryTree *tree, const QString &modName, bool hasPackageTXT, QWidget *parent) - : TutorableDialog("BainInstaller", parent), ui(new Ui::BainComplexInstallerDialog), m_Manual(false) -{ - ui->setupUi(this); - - ui->nameEdit->setText(modName); - - for (DirectoryTree::const_node_iterator iter = tree->nodesBegin(); iter != tree->nodesEnd(); ++iter) { - const QString &dirName = (*iter)->getData().name; - if ((dirName.compare("fomod", Qt::CaseInsensitive) == 0) || - (dirName.startsWith("--"))) { - continue; - } - - QListWidgetItem *item = new QListWidgetItem(ui->optionsList); - item->setText((*iter)->getData().name); - item->setFlags(item->flags() | Qt::ItemIsUserCheckable); - item->setCheckState(item->text().mid(0, 2) == "00" ? Qt::Checked : Qt::Unchecked); - item->setData(Qt::UserRole, qVariantFromValue((void*)(*iter))); - ui->optionsList->addItem(item); - } - - ui->packageBtn->setEnabled(hasPackageTXT); -} - - -BainComplexInstallerDialog::~BainComplexInstallerDialog() -{ - delete ui; -} - - -QString BainComplexInstallerDialog::getName() const -{ - return ui->nameEdit->text(); -} - - -void BainComplexInstallerDialog::moveTreeUp(DirectoryTree *target, DirectoryTree::Node *child) -{ - for (DirectoryTree::const_node_iterator iter = child->nodesBegin(); - iter != child->nodesEnd();) { - target->addNode(*iter, true); - iter = child->detach(iter); - } - - for (DirectoryTree::const_leaf_reverse_iterator iter = child->leafsRBegin(); - iter != child->leafsREnd(); ++iter) { - target->addLeaf(*iter); - } -} - - -DirectoryTree *BainComplexInstallerDialog::updateTree(DirectoryTree *tree) -{ - DirectoryTree *newTree = new DirectoryTree; - - for (DirectoryTree::const_node_reverse_iterator iter = tree->nodesRBegin(); - iter != tree->nodesREnd();) { - QList items = ui->optionsList->findItems((*iter)->getData().name, Qt::MatchFixedString); - if ((items.count() == 1) && (items.at(0)->checkState() == Qt::Checked)) { - moveTreeUp(newTree, *iter); - } - iter = tree->erase(iter); - } - - return newTree; -} - - -void BainComplexInstallerDialog::on_okBtn_clicked() -{ - this->accept(); -} - - -void BainComplexInstallerDialog::on_cancelBtn_clicked() -{ - this->reject(); -} - - -void BainComplexInstallerDialog::on_manualBtn_clicked() -{ - m_Manual = true; - this->reject(); -} - -void BainComplexInstallerDialog::on_packageBtn_clicked() -{ - TextViewer viewer(this); - viewer.setDescription(""); - viewer.addFile(QDir::tempPath().append("/package.txt"), false); - viewer.exec(); -} +#include "baincomplexinstallerdialog.h" +#include "textviewer.h" +#include "ui_baincomplexinstallerdialog.h" + +#include + +BainComplexInstallerDialog::BainComplexInstallerDialog(DirectoryTree *tree, const QString &modName, bool hasPackageTXT, QWidget *parent) + : TutorableDialog("BainInstaller", parent), ui(new Ui::BainComplexInstallerDialog), m_Manual(false) +{ + ui->setupUi(this); + + ui->nameEdit->setText(modName); + + for (DirectoryTree::const_node_iterator iter = tree->nodesBegin(); iter != tree->nodesEnd(); ++iter) { + const QString &dirName = (*iter)->getData().name; + if ((dirName.compare("fomod", Qt::CaseInsensitive) == 0) || + (dirName.startsWith("--"))) { + continue; + } + + QListWidgetItem *item = new QListWidgetItem(ui->optionsList); + item->setText((*iter)->getData().name); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(item->text().mid(0, 2) == "00" ? Qt::Checked : Qt::Unchecked); + item->setData(Qt::UserRole, qVariantFromValue((void*)(*iter))); + ui->optionsList->addItem(item); + } + + ui->packageBtn->setEnabled(hasPackageTXT); +} + + +BainComplexInstallerDialog::~BainComplexInstallerDialog() +{ + delete ui; +} + + +QString BainComplexInstallerDialog::getName() const +{ + return ui->nameEdit->text(); +} + + +void BainComplexInstallerDialog::moveTreeUp(DirectoryTree *target, DirectoryTree::Node *child) +{ + for (DirectoryTree::const_node_iterator iter = child->nodesBegin(); + iter != child->nodesEnd();) { + target->addNode(*iter, true); + iter = child->detach(iter); + } + + for (DirectoryTree::const_leaf_reverse_iterator iter = child->leafsRBegin(); + iter != child->leafsREnd(); ++iter) { + target->addLeaf(*iter); + } +} + + +DirectoryTree *BainComplexInstallerDialog::updateTree(DirectoryTree *tree) +{ + DirectoryTree *newTree = new DirectoryTree; + + for (DirectoryTree::const_node_reverse_iterator iter = tree->nodesRBegin(); + iter != tree->nodesREnd();) { + QList items = ui->optionsList->findItems((*iter)->getData().name, Qt::MatchFixedString); + if ((items.count() == 1) && (items.at(0)->checkState() == Qt::Checked)) { + moveTreeUp(newTree, *iter); + } + iter = tree->erase(iter); + } + + return newTree; +} + + +void BainComplexInstallerDialog::on_okBtn_clicked() +{ + this->accept(); +} + + +void BainComplexInstallerDialog::on_cancelBtn_clicked() +{ + this->reject(); +} + + +void BainComplexInstallerDialog::on_manualBtn_clicked() +{ + m_Manual = true; + this->reject(); +} + +void BainComplexInstallerDialog::on_packageBtn_clicked() +{ + TextViewer viewer("package.txt", this); + viewer.setDescription(""); + viewer.addFile(QDir::tempPath().append("/package.txt"), false); + viewer.exec(); +} diff --git a/src/csvbuilder.cpp b/src/csvbuilder.cpp new file mode 100644 index 00000000..fa4218a2 --- /dev/null +++ b/src/csvbuilder.cpp @@ -0,0 +1,272 @@ +#include "csvbuilder.h" + + +CSVBuilder::CSVBuilder(QIODevice *target) + : m_Out(target), m_Separator(','), m_LineBreak(BREAK_CRLF) +{ + m_Out.setCodec("UTF-8"); + + m_QuoteMode[TYPE_INTEGER] = QUOTE_NEVER; + m_QuoteMode[TYPE_FLOAT] = QUOTE_NEVER; + m_QuoteMode[TYPE_STRING] = QUOTE_ONDEMAND; +} + + +CSVBuilder::~CSVBuilder() +{ + +} + + +void CSVBuilder::setFieldSeparator(char sep) +{ + char oldSeparator = m_Separator; + m_Separator = sep; + try { + checkFields(m_Fields); + } catch (const CSVException&) { + m_Separator = oldSeparator; + throw; + } +} + + +void CSVBuilder::setLineBreak(CSVBuilder::ELineBreak lineBreak) +{ + m_LineBreak = lineBreak; +} + + +void CSVBuilder::setEscapeMode(CSVBuilder::EFieldType type, CSVBuilder::EQuoteMode mode) +{ + m_QuoteMode[type] = mode; +} + + +void CSVBuilder::setFields(const std::vector > &fields) +{ + std::vector fieldNames; + std::map fieldTypes; + + for (auto iter = fields.begin(); iter != fields.end(); ++iter) { + fieldNames.push_back(iter->first); + fieldTypes[iter->first] = iter->second; + } + + checkFields(fieldNames); + + m_Fields = fieldNames; + m_FieldTypes = fieldTypes; + m_Defaults.clear(); + m_RowBuffer.clear(); + +} + + +void CSVBuilder::checkValue(const QString &field, const QVariant &value) +{ + auto typeIter = m_FieldTypes.find(field); + if (typeIter == m_FieldTypes.end()) { + throw CSVException(QObject::tr("invalid field name \"%1\"").arg(field)); + } + + switch (typeIter->second) { + case TYPE_INTEGER: { + if (!value.canConvert()) { + throw CSVException(QObject::tr("invalid type for \"%1\" (should be integer)").arg(field)); + } + } break; + case TYPE_STRING: { + if (!value.canConvert()) { + throw CSVException(QObject::tr("invalid type for \"%1\" (should be string)").arg(field)); + } + } break; + case TYPE_FLOAT: { + if (!value.canConvert()) { + throw CSVException(QObject::tr("invalid type for \"%1\" (should be float)").arg(field)); + } + } break; + } +} + + +void CSVBuilder::setDefault(const QString &field, const QVariant &value) +{ + checkValue(field, value); + m_Defaults[field] = value; +} + + +void CSVBuilder::writeHeader() +{ + if (m_Fields.size() == 0) { + throw CSVException(QObject::tr("no fields set up yet!")); + } + + for (auto iter = m_Fields.begin(); iter != m_Fields.end(); ++iter) { + if (iter != m_Fields.begin()) { + m_Out << m_Separator; + } + m_Out << *iter; + } + m_Out << lineBreak(); + m_Out.flush(); +} + + +void CSVBuilder::setRowField(const QString &field, const QVariant &value) +{ + checkValue(field, value); + m_RowBuffer[field] = value; +} + + +void CSVBuilder::writeData(const std::map &data, bool check) +{ + QString line; + QTextStream temp(&line); + + for (auto iter = m_Fields.begin(); iter != m_Fields.end(); ++iter) { + if (iter != m_Fields.begin()) { + temp << separator(); + } + + QVariant val; + + auto valIter = data.find(*iter); + if (valIter == data.end()) { + auto defaultIter = m_Defaults.find(*iter); + if (defaultIter == m_Defaults.end()) { + throw CSVException(QObject::tr("field not set \"%1\"").arg(*iter)); + } else { + val = defaultIter->second; + } + } else { + val = valIter->second; + } + + if (check) { + checkValue(*iter, val); + } + + switch (m_FieldTypes[*iter]) { + case TYPE_INTEGER: { + quoteInsert(temp, val.toInt()); + } break; + case TYPE_FLOAT: { + quoteInsert(temp, val.toFloat()); + } break; + case TYPE_STRING: { + quoteInsert(temp, val.toString()); + } break; + } + } + m_Out << line << lineBreak(); + m_Out.flush(); +} + + +void CSVBuilder::quoteInsert(QTextStream &stream, int value) +{ + switch (m_QuoteMode[TYPE_INTEGER]) { + case QUOTE_NEVER: + case QUOTE_ONDEMAND: { + stream << value; + } break; + case QUOTE_ALWAYS: { + stream << "\"" << value << "\""; + } break; + } +} + + +void CSVBuilder::quoteInsert(QTextStream &stream, float value) +{ + switch (m_QuoteMode[TYPE_FLOAT]) { + case QUOTE_NEVER: + case QUOTE_ONDEMAND: { + stream << value; + } break; + case QUOTE_ALWAYS: { + stream << "\"" << value << "\""; + } break; + } +} + + +void CSVBuilder::quoteInsert(QTextStream &stream, const QString &value) +{ + switch (m_QuoteMode[TYPE_STRING]) { + case QUOTE_NEVER: { + stream << value; + } break; + case QUOTE_ONDEMAND: { + if (value.contains("[,\r\n]")) { + stream << "\"" << value.mid(0).replace("\"", "\"\"") << "\""; + } else { + stream << value; + } + } break; + case QUOTE_ALWAYS: { + stream << "\"" << value.mid(0).replace("\"", "\"\"") << "\""; + } break; + } +} + + +void CSVBuilder::writeRow() +{ + writeData(m_RowBuffer, false); // data was tested on input + m_RowBuffer.clear(); +} + + +void CSVBuilder::addRow(const std::map &data) +{ + writeData(data, true); +} + + +void CSVBuilder::checkFields(const std::vector &fields) +{ + for (auto iter = fields.begin(); iter != fields.end(); ++iter) { + if (iter->contains(m_Separator) || + iter->contains('\r') || + iter->contains('\n') || + iter->contains('"')) { + throw CSVException(QObject::tr("invalid character in field \"%1\"").arg(*iter)); + } + if (iter->length() == 0) { + throw CSVException(QObject::tr("empty field name")); + } + } +} + + +/* + +if(cell.contains(KDefaultEscapeChar) || cell.contains(KDefaultNewLine) + || cell.contains(KDefaultDelimiter)) { + m_currentLine->append(cell.replace(KDefaultNewLine, + QString(KDefaultEscapeChar) + KDefaultEscapeChar) + .prepend(KDefaultEscapeChar) + .append(KDefaultEscapeChar)); +} else { +m_currentLine->append(cell); +}*/ + + +const char *CSVBuilder::lineBreak() +{ + switch (m_LineBreak) { + case BREAK_CR: return "\r"; + case BREAK_CRLF: return "\r\n"; + case BREAK_LF: return "\n"; + default: return "\n"; // default shouldn't be necessary + } +} + +const char CSVBuilder::separator() +{ + return m_Separator; +} diff --git a/src/csvbuilder.h b/src/csvbuilder.h new file mode 100644 index 00000000..f2e0e987 --- /dev/null +++ b/src/csvbuilder.h @@ -0,0 +1,93 @@ +#ifndef CSVBUILDER_H +#define CSVBUILDER_H + + +#include +#include +#include +#include + + +class CSVException : public std::exception { + +public: + CSVException(const QString &text) + : std::exception(), m_Message(text.toLocal8Bit()) {} + + virtual const char* what() const throw() + { return m_Message.constData(); } +private: + QByteArray m_Message; + +}; + + +class CSVBuilder +{ + +public: + + enum EFieldType { + TYPE_INTEGER, + TYPE_STRING, + TYPE_FLOAT + }; + + enum EQuoteMode { + QUOTE_NEVER, + QUOTE_ONDEMAND, + QUOTE_ALWAYS + }; + + enum ELineBreak { + BREAK_LF, + BREAK_CRLF, + BREAK_CR + }; + +public: + + CSVBuilder(QIODevice *target); + ~CSVBuilder(); + + void setFieldSeparator(char sep); + void setLineBreak(ELineBreak lineBreak); + void setEscapeMode(EFieldType type, EQuoteMode mode); + void setFields(const std::vector > &fields); + void setDefault(const QString &field, const QVariant &value); + + void writeHeader(); + + void setRowField(const QString &field, const QVariant &value); + void writeRow(); + + void addRow(const std::map &data); + +private: + + const char *lineBreak(); + const char separator(); + + void fieldValid(); + void checkFields(const std::vector &fields); + void checkValue(const QString &field, const QVariant &value); + void writeData(const std::map &data, bool check); + + void quoteInsert(QTextStream &stream, int value); + void quoteInsert(QTextStream &stream, float value); + void quoteInsert(QTextStream &stream, const QString &value); + +private: + + QTextStream m_Out; + char m_Separator; + ELineBreak m_LineBreak; + std::map m_QuoteMode; + std::vector m_Fields; + std::map m_FieldTypes; + std::map m_Defaults; + std::map m_RowBuffer; + +}; + +#endif // CSVBUILDER_H diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index a08490e6..2e756e5d 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -17,912 +17,911 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include "downloadmanager.h" -#include "report.h" -#include "nxmurl.h" -#include -#include "utility.h" -#include "json.h" -#include "selectiondialog.h" -#include -#include -#include -#include -#include -#include -#include -#include - - -using QtJson::Json; - - -// TODO limit number of downloads, also display download during nxm requests, store modid/fileid with downloads - - -static const char UNFINISHED[] = ".unfinished"; - - -void DownloadManager::DownloadInfo::setName(QString newName, bool renameFile) -{ - QString oldMetaFileName = QString("%1.meta").arg(m_FileName); - m_FileName = QFileInfo(newName).fileName(); - if ((m_State == DownloadManager::STATE_STARTED) || - (m_State == DownloadManager::STATE_DOWNLOADING)) { - newName.append(UNFINISHED); - } - if (renameFile) { - if ((newName != m_Output.fileName()) && !m_Output.rename(newName)) { - reportError(tr("failed to rename \"%1\" to \"%2\"").arg(m_Output.fileName()).arg(newName)); - return; - } - - QFile metaFile(oldMetaFileName); - if (metaFile.exists()) { - metaFile.rename(newName.mid(0).append(".meta")); - } - } - m_Output.setFileName(newName); -} - - -DownloadManager::DownloadManager(NexusInterface *nexusInterface, QObject *parent) - : QObject(parent), m_NexusInterface(nexusInterface), m_DirWatcher() -{ - connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); -} - - -DownloadManager::~DownloadManager() -{ - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { - delete *iter; - } -} - - -bool DownloadManager::downloadsInProgress() -{ - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { - if ((*iter)->m_State < STATE_READY) { - return true; - } - } - return false; -} - - -void DownloadManager::setOutputDirectory(const QString &outputDirectory) -{ - QStringList directories = m_DirWatcher.directories(); - if (directories.length() != 0) { - m_DirWatcher.removePaths(directories); - } - m_OutputDirectory = QDir::fromNativeSeparators(outputDirectory); - m_DirWatcher.addPath(m_OutputDirectory); - refreshList(); -} - -void DownloadManager::refreshList() -{ - // remove finished downloads - for (QVector::iterator Iter = m_ActiveDownloads.begin(); Iter != m_ActiveDownloads.end();) { - if (((*Iter)->m_State == STATE_READY) || ((*Iter)->m_State == STATE_INSTALLED)) { - delete *Iter; - Iter = m_ActiveDownloads.erase(Iter); - } else { -//qDebug("leaving %s / %s in the download list since it's still being processed", -// qPrintable((*Iter)->m_FileName), qPrintable((*Iter)->m_Output.fileName())); - ++Iter; - } - } - - QStringList nameFilters; - nameFilters.append("*.rar"); - nameFilters.append("*.zip"); - nameFilters.append("*.7z"); - nameFilters.append("*.fomod"); - nameFilters.append(QString("*").append(UNFINISHED)); - - QDir dir(QDir::fromNativeSeparators(m_OutputDirectory)); - - // add existing downloads to list - foreach (QString file, dir.entryList(nameFilters, QDir::Files, QDir::Time)) { - bool Exists = false; - for (QVector::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) { -//qDebug("%s found in m_Output: %s", qPrintable(file), qPrintable((*Iter)->m_Output.fileName())); - Exists = true; - } - } - if (Exists) { - continue; - } - DownloadInfo *info = new DownloadInfo; - info->m_State = STATE_READY; - if (file.endsWith(UNFINISHED)) { - info->m_FileName = file.mid(0, file.length() - strlen(UNFINISHED)); - info->m_State = STATE_PAUSED; - } else { - info->m_FileName = file; - } - info->m_Output.setFileName(QString("%1/%2").arg(QDir::fromNativeSeparators(m_OutputDirectory)).arg(file)); - - QString metaFileName = QString("%1.meta").arg(info->m_Output.fileName()); - QSettings metaFile(metaFileName, QSettings::IniFormat); - if (metaFile.value("removed", false).toBool()) { - continue; - } - info->m_ModID = metaFile.value("modID", 0).toInt(); - info->m_FileID = metaFile.value("fileID", 0).toInt(); - info->m_Url = metaFile.value("url", "").toString(); - 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(); - if (metaFile.value("installed", false).toBool()) { - info->m_State = STATE_INSTALLED; - } - if (metaFile.value("paused", false).toBool()) { - info->m_State = STATE_PAUSED; - } - m_ActiveDownloads.push_front(info); - } - qDebug("downloads after refresh: %d", m_ActiveDownloads.size()); - emit update(-1); -} - - -bool DownloadManager::addDownload(const QUrl &url, int modID, int fileID, const NexusInfo &nexusInfo) -{ - QString fileName = QFileInfo(url.path()).fileName(); - if (fileName.isEmpty()) { - fileName = "unknown"; - } - - QNetworkRequest request(url); - return addDownload(m_NexusInterface->getAccessManager()->get(request), fileName, modID, fileID, nexusInfo); -} - -bool DownloadManager::addDownload(QNetworkReply *reply, const QString &fileName, int modID, - int fileID, const NexusInfo &nexusInfo) -{ - // download invoked from an already open network reply (i.e. download link in the browser) - DownloadInfo *newDownload = new DownloadInfo; - newDownload->m_StartTime.start(); - newDownload->m_Progress = 0; - newDownload->m_ResumePos = 0; - newDownload->m_ModID = modID; - newDownload->m_FileID = fileID; - newDownload->m_NexusInfo = nexusInfo; - - QString baseName = fileName; - - if (!nexusInfo.m_FileName.isEmpty()) { - baseName = nexusInfo.m_FileName; - } else { - QString dispoName = getFileNameFromNetworkReply(reply); - - if (!dispoName.isEmpty()) { - baseName = dispoName; - } - } - - if (QFile::exists(m_OutputDirectory + "/" + baseName) && - (QMessageBox::question(NULL, tr("Download again?"), tr("A file with the same name has already been downloaded. " - "Do you want to download it again? The new file will receive a different name."), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::No)) { - delete newDownload; - return false; - } - - newDownload->setName(getDownloadFileName(baseName), false); - - addDownload(reply, newDownload, false); - - emit update(-1); - return true; -} - - -void DownloadManager::addDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume) -{ - newDownload->m_Reply = reply; - newDownload->m_State = STATE_DOWNLOADING; - newDownload->m_Url = reply->url().toString(); - - QIODevice::OpenMode mode = QIODevice::WriteOnly; - if (resume) { - mode |= QIODevice::Append; - } - - if (!newDownload->m_Output.open(mode)) { - reportError(tr("failed to download %1: could not open output file: %2") - .arg(reply->url().toString()).arg(newDownload->m_Output.fileName())); - return; - } - - 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(readyRead()), this, SLOT(downloadReadyRead())); - connect(newDownload->m_Reply, SIGNAL(metaDataChanged()), this, SLOT(metaDataChanged())); - - if (!resume) { - emit aboutToUpdate(); - - m_ActiveDownloads.append(newDownload); -// qDebug("downloads after add: %d", m_ActiveDownloads.size()); - - emit update(-1); - } -} - - -void DownloadManager::addNXMDownload(const QString &url) -{ - NXMUrl nxmInfo(url); - m_RequestIDs.insert(m_NexusInterface->requestFileInfo(nxmInfo.getModId(), nxmInfo.getFileId(), this, QVariant())); -} - - -void DownloadManager::removeFile(int index, bool deleteFile) -{ - if (index >= m_ActiveDownloads.size()) { - throw MyException(tr("invalid index")); - } - - DownloadInfo *download = m_ActiveDownloads.at(index); - QString filePath = m_OutputDirectory + "/" + download->m_FileName; - if ((download->m_State == STATE_STARTED) || - (download->m_State == STATE_DOWNLOADING)) { - // shouldn't have been possible - qCritical("tried to remove active download"); - return; - } - - if (download->m_State == STATE_PAUSED) { - filePath.append(UNFINISHED); - } - - if (deleteFile) { - if (!QFile(filePath).remove()) { - reportError(tr("failed to delete %1").arg(filePath)); - return; - } - - QFile metaFile(filePath.append(".meta")); - if (metaFile.exists() && !metaFile.remove()) { - reportError(tr("failed to delete meta file for %1").arg(filePath)); - } - } else { - QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat); - metaSettings.setValue("removed", true); - } -} - -class LessThanWrapper -{ -public: - LessThanWrapper(DownloadManager *manager) : m_Manager(manager) {} - bool operator()(int LHS, int RHS) { - return m_Manager->getFileName(LHS).compare(m_Manager->getFileName(RHS), Qt::CaseInsensitive) < 0; - - } - -private: - DownloadManager *m_Manager; -}; - -bool DownloadManager::ByName(int LHS, int RHS) -{ - return m_ActiveDownloads[LHS]->m_FileName < m_ActiveDownloads[RHS]->m_FileName; -} - - -void DownloadManager::refreshAlphabeticalTranslation() -{ - m_AlphabeticalTranslation.clear(); - int pos = 0; - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++pos) { - m_AlphabeticalTranslation.push_back(pos); - } - - qSort(m_AlphabeticalTranslation.begin(), m_AlphabeticalTranslation.end(), LessThanWrapper(this)); -} - - -void DownloadManager::removeDownload(int index, bool deleteFile) -{ - try { - emit aboutToUpdate(); - - if (index < 0) { - DownloadState minState = index < -1 ? STATE_INSTALLED : STATE_READY; - index = 0; - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) { - if ((*iter)->m_State >= minState) { - removeFile(index, deleteFile); - delete *iter; - iter = m_ActiveDownloads.erase(iter); - } else { - ++iter; - ++index; - } - } - } else { - if (index >= m_ActiveDownloads.size()) { - reportError(tr("invalid index %1").arg(index)); - return; - } - - removeFile(index, deleteFile); - delete m_ActiveDownloads[index]; - m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); - } - emit update(-1); - } catch (const std::exception &e) { - qCritical("failed to remove download: %s", e.what()); - } -} - - -void DownloadManager::cancelDownload(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("invalid index %1").arg(index)); - return; - } - - if (m_ActiveDownloads[index]->m_State == STATE_DOWNLOADING) { - m_ActiveDownloads[index]->m_State = STATE_CANCELING; - qDebug("canceled %d - %s", index, m_ActiveDownloads[index]->m_FileName.toUtf8().constData()); - } -} - - -void DownloadManager::pauseDownload(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("invalid index %1").arg(index)); - return; - } - - if (m_ActiveDownloads[index]->m_State == STATE_DOWNLOADING) { - m_ActiveDownloads[index]->m_State = STATE_PAUSING; - qDebug("pausing %d - %s", index, m_ActiveDownloads[index]->m_FileName.toUtf8().constData()); - } -} - - -void DownloadManager::resumeDownload(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("invalid index %1").arg(index)); - return; - } - if (m_ActiveDownloads[index]->m_State == STATE_PAUSED) { - DownloadInfo *info = m_ActiveDownloads[index]; - qDebug("request resume from url %s", info->m_Url.toUtf8().constData()); - QNetworkRequest request(info->m_Url); - info->m_ResumePos = info->m_Output.size(); - QByteArray rangeHeader = "bytes=" + QByteArray::number(info->m_ResumePos) + "-"; - request.setRawHeader("Range", rangeHeader); - addDownload(m_NexusInterface->getAccessManager()->get(request), info, true); - } - emit update(index); -} - - -void DownloadManager::queryInfo(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - reportError(tr("invalid index %1").arg(index)); - return; - } - DownloadInfo *info = m_ActiveDownloads[index]; - - if (info->m_ModID == 0UL) { - QString fileName = getFileName(index); - QString ignore; - NexusInterface::interpretNexusFileName(fileName, ignore, info->m_ModID, true); - if (info->m_ModID < 0) { - QString modIDString; - while (modIDString.isEmpty()) { - modIDString = QInputDialog::getText(NULL, tr("Please enter the nexus mod id"), tr("Mod ID:"), QLineEdit::Normal, - QString(), NULL, 0, Qt::ImhFormattedNumbersOnly); - if (modIDString.isNull()) { - // canceled - return; - } else if (modIDString.contains(QRegExp("[^0-9]"))) { - qDebug("illegal character in mod-id"); - modIDString.clear(); - } - } - info->m_ModID = modIDString.toInt(NULL, 10); - } - } - m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_ModID, this, qVariantFromValue(static_cast(info)))); -} - - -int DownloadManager::numTotalDownloads() const -{ - return m_ActiveDownloads.size(); -} - - -QString DownloadManager::getFilePath(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("invalid index")); - } - - return m_OutputDirectory + "/" + m_ActiveDownloads.at(index)->m_FileName; -} - - -QString DownloadManager::getFileName(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("invalid index")); - } - - return m_ActiveDownloads.at(index)->m_FileName; -} - - -int DownloadManager::getProgress(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("invalid index")); - } - - return m_ActiveDownloads.at(index)->m_Progress; -} - - -DownloadManager::DownloadState DownloadManager::getState(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("invalid index")); - } - - return m_ActiveDownloads.at(index)->m_State; -} - - -bool DownloadManager::isInfoIncomplete(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("invalid index")); - } - - DownloadInfo *info = m_ActiveDownloads.at(index); - return (info->m_FileID == 0) || (info->m_ModID == 0) || info->m_NexusInfo.m_Version.isEmpty(); -} - - -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; -} - - -NexusInfo DownloadManager::getNexusInfo(int index) const -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("invalid index")); - } - - return m_ActiveDownloads.at(index)->m_NexusInfo; -} - - -void DownloadManager::markInstalled(int index) -{ - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("invalid index")); - } - QSettings metaFile(QString("%1.meta").arg(m_ActiveDownloads.at(index)->m_Output.fileName()), QSettings::IniFormat); - metaFile.setValue("installed", true); - - m_ActiveDownloads.at(index)->m_State = STATE_INSTALLED; -} - - -QString DownloadManager::getDownloadFileName(const QString &baseName) const -{ - QString fullPath = m_OutputDirectory + "/" + baseName; - if (QFile::exists(fullPath)) { - int i = 1; - while (QFile::exists(QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName))) { - ++i; - } - - fullPath = QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName); - } - return fullPath; -} - - -QString DownloadManager::getFileNameFromNetworkReply(QNetworkReply *reply) -{ - if (reply->hasRawHeader("Content-Disposition")) { - std::tr1::regex exp("filename=\"(.*)\""); - - std::tr1::cmatch result; - if (std::tr1::regex_search(reply->rawHeader("Content-Disposition").constData(), result, exp)) { - return QString::fromUtf8(result.str(1).c_str()); - } - } - - return QString(); -} - - -DownloadManager::DownloadInfo *DownloadManager::findDownload(QObject *reply, int *index) const -{ - for (int i = 0; i < m_ActiveDownloads.size(); ++i) { - if (m_ActiveDownloads[i]->m_Reply == reply) { - if (index != NULL) { - *index = i; - } - return m_ActiveDownloads[i]; - } - } - return NULL; -} - - -void DownloadManager::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) -{ - if (bytesTotal == 0) { - return; - } - int index = 0; - - DownloadInfo *info = findDownload(this->sender(), &index); - if (info != NULL) { - if (info->m_State == STATE_CANCELING) { - info->m_Reply->abort(); - info->m_State = STATE_CANCELED; - } else if (info->m_State == STATE_PAUSING) { - info->m_Reply->abort(); - info->m_State = STATE_PAUSED; - } else { - info->m_Progress = ((info->m_ResumePos + bytesReceived) * 100) / (info->m_ResumePos + bytesTotal); - emit update(index); - } - } -} - - -void DownloadManager::downloadReadyRead() -{ - DownloadInfo *info = findDownload(this->sender()); - if (info != NULL) { - info->m_Output.write(info->m_Reply->readAll()); - } -} - - -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("url", info->m_Url); - 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("newestVersion", info->m_NexusInfo.m_NewestVersion); - metaFile.setValue("category", info->m_NexusInfo.m_Category); - metaFile.setValue("installed", info->m_State == DownloadManager::STATE_INSTALLED); - metaFile.setValue("paused", info->m_State == DownloadManager::STATE_PAUSED); - - // slightly hackish... - for (int i = 0; i < m_ActiveDownloads.size(); ++i) { - if (m_ActiveDownloads[i] == info) { - emit update(i); - } - } -} - - -void DownloadManager::nxmDescriptionAvailable(int, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - QVariantMap result = resultData.toMap(); - - DownloadInfo *info = static_cast(userData.value()); - 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) { - createMetaFile(info); - info->m_State = STATE_READY; - } else { - info->m_State = STATE_FETCHINGFILEINFO; - m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_ModID, this, qVariantFromValue(static_cast(info)))); - } -} - - -void DownloadManager::nxmFilesAvailable(int, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - DownloadInfo *info = static_cast(userData.value()); - - QVariantList result = resultData.toList(); - - // MO sometimes prepends _ to the filename in case of duplicate downloads. - // this may muck up the file name comparison - QString alternativeLocalName = info->m_FileName; - - QRegExp expression("^\\d_(.*)$"); - if (expression.indexIn(alternativeLocalName) == 0) { - alternativeLocalName = expression.cap(1); - } - - bool found = false; - - foreach(QVariant file, result) { - QVariantMap fileInfo = file.toMap(); - QString fileName = fileInfo["uri"].toString(); - 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(); - info->m_NexusInfo.m_FileCategory = fileInfo["category_id"].toInt(); - info->m_FileID = fileInfo["id"].toInt(); - found = true; - break; - } - } - - if ((info->m_State == STATE_READY) || - (info->m_State == STATE_INSTALLED)) { - if (found) { - emit showMessage(tr("Information updated")); - } else if (result.count() == 0) { - emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?")); - } else { - SelectionDialog selection(tr("No file on Nexus matches the selected file by name. Please manually choose the correct one.")); - foreach(QVariant file, result) { - QVariantMap fileInfo = file.toMap(); - selection.addChoice(fileInfo["uri"].toString(), "", file); - } - 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(); - } 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) { - qWarning("could not determine file id for %s", info->m_FileName.toUtf8().constData()); - } - - info->m_State = STATE_READY; - } - createMetaFile(info); -} - - -void DownloadManager::nxmFileInfoAvailable(int modID, int fileID, QVariant, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - NexusInfo info; - - QVariantMap result = resultData.toMap(); - - info.m_Name = result["name"].toString(); - info.m_Version = result["version"].toString(); - info.m_FileName = result["uri"].toString(); - - m_RequestIDs.insert(m_NexusInterface->requestDownloadURL(modID, fileID, this, qVariantFromValue(info))); -} - - -// sort function to sort by best download server -bool DownloadManager::ServerByPreference(const QVariant &LHS, const QVariant &RHS) -{ - QVariantMap LHSMap = LHS.toMap(); - QVariantMap RHSMap = RHS.toMap(); - int LHSUsers = LHSMap["ConnectedUsers"].toInt(); - int RHSUsers = RHSMap["ConnectedUsers"].toInt(); - bool LHSPremium = LHSMap["IsPremium"].toBool(); - bool RHSPremium = RHSMap["IsPremium"].toBool(); - - // 0 users is probably a sign that the server is offline. Since there is currently no - // mechanism to try a different server, we avoid those without users - if ((LHSUsers == 0) && (RHSUsers != 0)) return false; - if ((LHSUsers != 0) && (RHSUsers == 0)) return true; - - - if (LHSPremium && !RHSPremium) { - return true; - } else if (!LHSPremium && RHSPremium) { - return false; - } - - // TODO implement country preference - - return LHSUsers < RHSUsers; -} - - -void DownloadManager::nxmDownloadURLsAvailable(int modID, int fileID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - NexusInfo info = userData.value(); - QVariantList resultList = resultData.toList(); - if (resultList.length() == 0) { - emit showMessage(tr("No download server available. Please try again later.")); - return; - } - qSort(resultList.begin(), resultList.end(), ServerByPreference); - - QVariantMap dlServer = resultList.first().toMap(); - qDebug("picked url: %s", dlServer["URI"].toString().toUtf8().constData()); - - - addDownload(dlServer["URI"].toString(), modID, fileID, info); -} - - -void DownloadManager::nxmRequestFailed(int modID, QVariant, int requestID, const QString &errorString) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - int index = 0; - - for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++index) { - DownloadInfo *info = *iter; - if (info->m_ModID == modID) { - if (info->m_State == STATE_STARTED) { - m_ActiveDownloads.erase(iter); - delete info; - } else if ((info->m_State == STATE_FETCHINGFILEINFO) || - (info->m_State == STATE_FETCHINGMODINFO)) { - info->m_State = STATE_READY; - } - emit update(index); - break; - } - } - emit showMessage(tr("Failed to request file info from nexus: %1").arg(errorString)); -} - - -void DownloadManager::downloadFinished() -{ - int index = 0; - - DownloadInfo *info = findDownload(this->sender(), &index); - if (info != NULL) { - QNetworkReply *reply = info->m_Reply; - QByteArray data = info->m_Reply->readAll(); - qDebug("finished %s (%d) (%d)", info->m_FileName.toUtf8().constData(), reply->error(), info->m_State); - info->m_Output.write(data); - info->m_Output.close(); - - if ((info->m_State != STATE_CANCELING) && - (info->m_State != STATE_PAUSING)) { - if ((info->m_Output.size() == 0) || - (reply->error() != QNetworkReply::NoError) || - reply->header(QNetworkRequest::ContentTypeHeader).toString().startsWith("text", Qt::CaseInsensitive)) { - emit showMessage(tr("Download failed: %1 (%2)").arg(reply->errorString()).arg(reply->error())); - info->m_State = STATE_CANCELING; - } - } - - if (info->m_State == STATE_CANCELING) { - info->m_Reply->abort(); - info->m_State = STATE_CANCELED; - } else if (info->m_State == STATE_PAUSING) { - info->m_Output.write(info->m_Reply->readAll()); - info->m_Reply->abort(); - - info->m_State = STATE_PAUSED; - } - - if (info->m_State == STATE_CANCELED) { - emit aboutToUpdate(); - info->m_Output.remove(); - delete info; - m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); - emit update(-1); - } else if (info->m_State == STATE_PAUSED) { - info->m_Output.close(); - createMetaFile(info); - emit update(index); - } else { - info->m_State = STATE_FETCHINGMODINFO; - QString newName = getFileNameFromNetworkReply(reply); - QString oldName = QFileInfo(info->m_Output).fileName(); - if (!newName.isEmpty() && (newName != oldName)) { - info->setName(getDownloadFileName(newName), true); - } else { - info->setName(m_OutputDirectory + "/" + info->m_FileName, true); // don't rename but remove the ".unfinished" extension - } - - m_RequestIDs.insert(m_NexusInterface->requestDescription(info->m_ModID, this, qVariantFromValue(static_cast(info)))); - - emit update(index); - } - reply->close(); - reply->deleteLater(); - } else { - qWarning("no download index %d", index); - } -} - - -void DownloadManager::metaDataChanged() -{ - int index = 0; - - DownloadInfo *info = findDownload(this->sender(), &index); - if (info != NULL) { - QString newName = getFileNameFromNetworkReply(info->m_Reply); - if (!newName.isEmpty() && (newName != info->m_FileName)) { - info->setName(getDownloadFileName(newName), true); - refreshAlphabeticalTranslation(); - if (!info->m_Output.open(QIODevice::WriteOnly | QIODevice::Append)) { - reportError(tr("failed to re-open %1").arg(info->m_FileName)); - info->m_State = STATE_CANCELING; - info->m_Reply->abort(); - } - } - } else { - qWarning("meta data event for unknown download"); - } -} - -void DownloadManager::directoryChanged(const QString&) -{ - refreshList(); -} +#include "downloadmanager.h" +#include "report.h" +#include "nxmurl.h" +#include +#include "utility.h" +#include "json.h" +#include "selectiondialog.h" +#include +#include +#include +#include +#include +#include +#include +#include + + +using QtJson::Json; + + +// TODO limit number of downloads, also display download during nxm requests, store modid/fileid with downloads + + +static const char UNFINISHED[] = ".unfinished"; + + +void DownloadManager::DownloadInfo::setName(QString newName, bool renameFile) +{ + QString oldMetaFileName = QString("%1.meta").arg(m_FileName); + m_FileName = QFileInfo(newName).fileName(); + if ((m_State == DownloadManager::STATE_STARTED) || + (m_State == DownloadManager::STATE_DOWNLOADING)) { + newName.append(UNFINISHED); + } + if (renameFile) { + if ((newName != m_Output.fileName()) && !m_Output.rename(newName)) { + reportError(tr("failed to rename \"%1\" to \"%2\"").arg(m_Output.fileName()).arg(newName)); + return; + } + + QFile metaFile(oldMetaFileName); + if (metaFile.exists()) { + metaFile.rename(newName.mid(0).append(".meta")); + } + } + m_Output.setFileName(newName); +} + + +DownloadManager::DownloadManager(NexusInterface *nexusInterface, QObject *parent) + : QObject(parent), m_NexusInterface(nexusInterface), m_DirWatcher() +{ + connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); +} + + +DownloadManager::~DownloadManager() +{ + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { + delete *iter; + } +} + + +bool DownloadManager::downloadsInProgress() +{ + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { + if ((*iter)->m_State < STATE_READY) { + return true; + } + } + return false; +} + + +void DownloadManager::setOutputDirectory(const QString &outputDirectory) +{ + QStringList directories = m_DirWatcher.directories(); + if (directories.length() != 0) { + m_DirWatcher.removePaths(directories); + } + m_OutputDirectory = QDir::fromNativeSeparators(outputDirectory); + m_DirWatcher.addPath(m_OutputDirectory); + refreshList(); +} + +void DownloadManager::refreshList() +{ + // remove finished downloads + for (QVector::iterator Iter = m_ActiveDownloads.begin(); Iter != m_ActiveDownloads.end();) { + if (((*Iter)->m_State == STATE_READY) || ((*Iter)->m_State == STATE_INSTALLED)) { + delete *Iter; + Iter = m_ActiveDownloads.erase(Iter); + } else { +//qDebug("leaving %s / %s in the download list since it's still being processed", +// qPrintable((*Iter)->m_FileName), qPrintable((*Iter)->m_Output.fileName())); + ++Iter; + } + } + + QStringList nameFilters; + nameFilters.append("*.rar"); + nameFilters.append("*.zip"); + nameFilters.append("*.7z"); + nameFilters.append("*.fomod"); + nameFilters.append(QString("*").append(UNFINISHED)); + + QDir dir(QDir::fromNativeSeparators(m_OutputDirectory)); + + // add existing downloads to list + foreach (QString file, dir.entryList(nameFilters, QDir::Files, QDir::Time)) { + bool Exists = false; + for (QVector::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) { +//qDebug("%s found in m_Output: %s", qPrintable(file), qPrintable((*Iter)->m_Output.fileName())); + Exists = true; + } + } + if (Exists) { + continue; + } + DownloadInfo *info = new DownloadInfo; + info->m_State = STATE_READY; + if (file.endsWith(UNFINISHED)) { + info->m_FileName = file.mid(0, file.length() - strlen(UNFINISHED)); + info->m_State = STATE_PAUSED; + } else { + info->m_FileName = file; + } + info->m_Output.setFileName(QString("%1/%2").arg(QDir::fromNativeSeparators(m_OutputDirectory)).arg(file)); + + QString metaFileName = QString("%1.meta").arg(info->m_Output.fileName()); + QSettings metaFile(metaFileName, QSettings::IniFormat); + if (metaFile.value("removed", false).toBool()) { + continue; + } + info->m_ModID = metaFile.value("modID", 0).toInt(); + info->m_FileID = metaFile.value("fileID", 0).toInt(); + info->m_Url = metaFile.value("url", "").toString(); + 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(); + if (metaFile.value("installed", false).toBool()) { + info->m_State = STATE_INSTALLED; + } + if (metaFile.value("paused", false).toBool()) { + info->m_State = STATE_PAUSED; + } + m_ActiveDownloads.push_front(info); + } + qDebug("downloads after refresh: %d", m_ActiveDownloads.size()); + emit update(-1); +} + + +bool DownloadManager::addDownload(const QUrl &url, int modID, int fileID, const NexusInfo &nexusInfo) +{ + QString fileName = QFileInfo(url.path()).fileName(); + if (fileName.isEmpty()) { + fileName = "unknown"; + } + + QNetworkRequest request(url); + return addDownload(m_NexusInterface->getAccessManager()->get(request), fileName, modID, fileID, nexusInfo); +} + +bool DownloadManager::addDownload(QNetworkReply *reply, const QString &fileName, int modID, + int fileID, const NexusInfo &nexusInfo) +{ + // download invoked from an already open network reply (i.e. download link in the browser) + DownloadInfo *newDownload = new DownloadInfo; + newDownload->m_StartTime.start(); + newDownload->m_Progress = 0; + newDownload->m_ResumePos = 0; + newDownload->m_ModID = modID; + newDownload->m_FileID = fileID; + newDownload->m_NexusInfo = nexusInfo; + + QString baseName = fileName; + + if (!nexusInfo.m_FileName.isEmpty()) { + baseName = nexusInfo.m_FileName; + } else { + QString dispoName = getFileNameFromNetworkReply(reply); + + if (!dispoName.isEmpty()) { + baseName = dispoName; + } + } + + if (QFile::exists(m_OutputDirectory + "/" + baseName) && + (QMessageBox::question(NULL, tr("Download again?"), tr("A file with the same name has already been downloaded. " + "Do you want to download it again? The new file will receive a different name."), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No)) { + delete newDownload; + return false; + } + + newDownload->setName(getDownloadFileName(baseName), false); + + addDownload(reply, newDownload, false); + + emit update(-1); + return true; +} + + +void DownloadManager::addDownload(QNetworkReply *reply, DownloadInfo *newDownload, bool resume) +{ + newDownload->m_Reply = reply; + newDownload->m_State = STATE_DOWNLOADING; + newDownload->m_Url = reply->url().toString(); + + QIODevice::OpenMode mode = QIODevice::WriteOnly; + if (resume) { + mode |= QIODevice::Append; + } + + if (!newDownload->m_Output.open(mode)) { + reportError(tr("failed to download %1: could not open output file: %2") + .arg(reply->url().toString()).arg(newDownload->m_Output.fileName())); + return; + } + + 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(readyRead()), this, SLOT(downloadReadyRead())); + connect(newDownload->m_Reply, SIGNAL(metaDataChanged()), this, SLOT(metaDataChanged())); + + if (!resume) { + emit aboutToUpdate(); + + m_ActiveDownloads.append(newDownload); +// qDebug("downloads after add: %d", m_ActiveDownloads.size()); + + emit update(-1); + } +} + + +void DownloadManager::addNXMDownload(const QString &url) +{ + NXMUrl nxmInfo(url); + m_RequestIDs.insert(m_NexusInterface->requestFileInfo(nxmInfo.getModId(), nxmInfo.getFileId(), this, QVariant())); +} + + +void DownloadManager::removeFile(int index, bool deleteFile) +{ + if (index >= m_ActiveDownloads.size()) { + throw MyException(tr("invalid index")); + } + + DownloadInfo *download = m_ActiveDownloads.at(index); + QString filePath = m_OutputDirectory + "/" + download->m_FileName; + if ((download->m_State == STATE_STARTED) || + (download->m_State == STATE_DOWNLOADING)) { + // shouldn't have been possible + qCritical("tried to remove active download"); + return; + } + + if (download->m_State == STATE_PAUSED) { + filePath.append(UNFINISHED); + } + + if (deleteFile) { + if (!QFile(filePath).remove()) { + reportError(tr("failed to delete %1").arg(filePath)); + return; + } + + QFile metaFile(filePath.append(".meta")); + if (metaFile.exists() && !metaFile.remove()) { + reportError(tr("failed to delete meta file for %1").arg(filePath)); + } + } else { + QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat); + metaSettings.setValue("removed", true); + } +} + +class LessThanWrapper +{ +public: + LessThanWrapper(DownloadManager *manager) : m_Manager(manager) {} + bool operator()(int LHS, int RHS) { + return m_Manager->getFileName(LHS).compare(m_Manager->getFileName(RHS), Qt::CaseInsensitive) < 0; + + } + +private: + DownloadManager *m_Manager; +}; + +bool DownloadManager::ByName(int LHS, int RHS) +{ + return m_ActiveDownloads[LHS]->m_FileName < m_ActiveDownloads[RHS]->m_FileName; +} + + +void DownloadManager::refreshAlphabeticalTranslation() +{ + m_AlphabeticalTranslation.clear(); + int pos = 0; + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++pos) { + m_AlphabeticalTranslation.push_back(pos); + } + + qSort(m_AlphabeticalTranslation.begin(), m_AlphabeticalTranslation.end(), LessThanWrapper(this)); +} + + +void DownloadManager::removeDownload(int index, bool deleteFile) +{ + try { + emit aboutToUpdate(); + + if (index < 0) { + DownloadState minState = index < -1 ? STATE_INSTALLED : STATE_READY; + index = 0; + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end();) { + if ((*iter)->m_State >= minState) { + removeFile(index, deleteFile); + delete *iter; + iter = m_ActiveDownloads.erase(iter); + } else { + ++iter; + ++index; + } + } + } else { + if (index >= m_ActiveDownloads.size()) { + reportError(tr("invalid index %1").arg(index)); + return; + } + + removeFile(index, deleteFile); + delete m_ActiveDownloads[index]; + m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); + } + emit update(-1); + } catch (const std::exception &e) { + qCritical("failed to remove download: %s", e.what()); + } +} + + +void DownloadManager::cancelDownload(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("invalid index %1").arg(index)); + return; + } + + if (m_ActiveDownloads[index]->m_State == STATE_DOWNLOADING) { + m_ActiveDownloads[index]->m_State = STATE_CANCELING; + qDebug("canceled %d - %s", index, m_ActiveDownloads[index]->m_FileName.toUtf8().constData()); + } +} + + +void DownloadManager::pauseDownload(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("invalid index %1").arg(index)); + return; + } + + if (m_ActiveDownloads[index]->m_State == STATE_DOWNLOADING) { + m_ActiveDownloads[index]->m_State = STATE_PAUSING; + qDebug("pausing %d - %s", index, m_ActiveDownloads[index]->m_FileName.toUtf8().constData()); + } +} + + +void DownloadManager::resumeDownload(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("invalid index %1").arg(index)); + return; + } + if (m_ActiveDownloads[index]->m_State == STATE_PAUSED) { + DownloadInfo *info = m_ActiveDownloads[index]; + qDebug("request resume from url %s", info->m_Url.toUtf8().constData()); + QNetworkRequest request(info->m_Url); + info->m_ResumePos = info->m_Output.size(); + QByteArray rangeHeader = "bytes=" + QByteArray::number(info->m_ResumePos) + "-"; + request.setRawHeader("Range", rangeHeader); + addDownload(m_NexusInterface->getAccessManager()->get(request), info, true); + } + emit update(index); +} + + +void DownloadManager::queryInfo(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("invalid index %1").arg(index)); + return; + } + DownloadInfo *info = m_ActiveDownloads[index]; + + if (info->m_ModID == 0UL) { + QString fileName = getFileName(index); + QString ignore; + NexusInterface::interpretNexusFileName(fileName, ignore, info->m_ModID, true); + if (info->m_ModID < 0) { + QString modIDString; + while (modIDString.isEmpty()) { + modIDString = QInputDialog::getText(NULL, tr("Please enter the nexus mod id"), tr("Mod ID:"), QLineEdit::Normal, + QString(), NULL, 0, Qt::ImhFormattedNumbersOnly); + if (modIDString.isNull()) { + // canceled + return; + } else if (modIDString.contains(QRegExp("[^0-9]"))) { + qDebug("illegal character in mod-id"); + modIDString.clear(); + } + } + info->m_ModID = modIDString.toInt(NULL, 10); + } + } + m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_ModID, this, qVariantFromValue(static_cast(info)))); +} + + +int DownloadManager::numTotalDownloads() const +{ + return m_ActiveDownloads.size(); +} + + +QString DownloadManager::getFilePath(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("invalid index")); + } + + return m_OutputDirectory + "/" + m_ActiveDownloads.at(index)->m_FileName; +} + + +QString DownloadManager::getFileName(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("invalid index")); + } + + return m_ActiveDownloads.at(index)->m_FileName; +} + + +int DownloadManager::getProgress(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("invalid index")); + } + + return m_ActiveDownloads.at(index)->m_Progress; +} + + +DownloadManager::DownloadState DownloadManager::getState(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("invalid index")); + } + + return m_ActiveDownloads.at(index)->m_State; +} + + +bool DownloadManager::isInfoIncomplete(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("invalid index")); + } + + DownloadInfo *info = m_ActiveDownloads.at(index); + return (info->m_FileID == 0) || (info->m_ModID == 0) || info->m_NexusInfo.m_Version.isEmpty(); +} + + +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; +} + + +NexusInfo DownloadManager::getNexusInfo(int index) const +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("invalid index")); + } + + return m_ActiveDownloads.at(index)->m_NexusInfo; +} + + +void DownloadManager::markInstalled(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + throw MyException(tr("invalid index")); + } + QSettings metaFile(QString("%1.meta").arg(m_ActiveDownloads.at(index)->m_Output.fileName()), QSettings::IniFormat); + metaFile.setValue("installed", true); + + m_ActiveDownloads.at(index)->m_State = STATE_INSTALLED; +} + + +QString DownloadManager::getDownloadFileName(const QString &baseName) const +{ + QString fullPath = m_OutputDirectory + "/" + baseName; + if (QFile::exists(fullPath)) { + int i = 1; + while (QFile::exists(QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName))) { + ++i; + } + + fullPath = QString("%1/%2_%3").arg(m_OutputDirectory).arg(i).arg(baseName); + } + return fullPath; +} + + +QString DownloadManager::getFileNameFromNetworkReply(QNetworkReply *reply) +{ + if (reply->hasRawHeader("Content-Disposition")) { + std::tr1::regex exp("filename=\"(.*)\""); + + std::tr1::cmatch result; + if (std::tr1::regex_search(reply->rawHeader("Content-Disposition").constData(), result, exp)) { + return QString::fromUtf8(result.str(1).c_str()); + } + } + + return QString(); +} + + +DownloadManager::DownloadInfo *DownloadManager::findDownload(QObject *reply, int *index) const +{ + for (int i = 0; i < m_ActiveDownloads.size(); ++i) { + if (m_ActiveDownloads[i]->m_Reply == reply) { + if (index != NULL) { + *index = i; + } + return m_ActiveDownloads[i]; + } + } + return NULL; +} + + +void DownloadManager::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) +{ + if (bytesTotal == 0) { + return; + } + int index = 0; + + DownloadInfo *info = findDownload(this->sender(), &index); + if (info != NULL) { + if (info->m_State == STATE_CANCELING) { + info->m_Reply->abort(); + info->m_State = STATE_CANCELED; + } else if (info->m_State == STATE_PAUSING) { + info->m_Reply->abort(); + info->m_State = STATE_PAUSED; + } else { + info->m_Progress = ((info->m_ResumePos + bytesReceived) * 100) / (info->m_ResumePos + bytesTotal); + emit update(index); + } + } +} + + +void DownloadManager::downloadReadyRead() +{ + DownloadInfo *info = findDownload(this->sender()); + if (info != NULL) { + info->m_Output.write(info->m_Reply->readAll()); + } +} + + +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("url", info->m_Url); + 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("newestVersion", info->m_NexusInfo.m_NewestVersion); + metaFile.setValue("category", info->m_NexusInfo.m_Category); + metaFile.setValue("installed", info->m_State == DownloadManager::STATE_INSTALLED); + metaFile.setValue("paused", info->m_State == DownloadManager::STATE_PAUSED); + + // slightly hackish... + for (int i = 0; i < m_ActiveDownloads.size(); ++i) { + if (m_ActiveDownloads[i] == info) { + emit update(i); + } + } +} + + +void DownloadManager::nxmDescriptionAvailable(int, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + QVariantMap result = resultData.toMap(); + + DownloadInfo *info = static_cast(userData.value()); + 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) { + createMetaFile(info); + info->m_State = STATE_READY; + } else { + info->m_State = STATE_FETCHINGFILEINFO; + m_RequestIDs.insert(m_NexusInterface->requestFiles(info->m_ModID, this, qVariantFromValue(static_cast(info)))); + } +} + + +void DownloadManager::nxmFilesAvailable(int, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + DownloadInfo *info = static_cast(userData.value()); + + QVariantList result = resultData.toList(); + + // MO sometimes prepends _ to the filename in case of duplicate downloads. + // this may muck up the file name comparison + QString alternativeLocalName = info->m_FileName; + + QRegExp expression("^\\d_(.*)$"); + if (expression.indexIn(alternativeLocalName) == 0) { + alternativeLocalName = expression.cap(1); + } + + bool found = false; + + foreach(QVariant file, result) { + QVariantMap fileInfo = file.toMap(); + QString fileName = fileInfo["uri"].toString(); + 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(); + info->m_NexusInfo.m_FileCategory = fileInfo["category_id"].toInt(); + info->m_FileID = fileInfo["id"].toInt(); + found = true; + break; + } + } + + if ((info->m_State == STATE_READY) || + (info->m_State == STATE_INSTALLED)) { + if (found) { + emit showMessage(tr("Information updated")); + } else if (result.count() == 0) { + emit showMessage(tr("No matching file found on Nexus! Maybe this file is no longer available or it was renamed?")); + } else { + SelectionDialog selection(tr("No file on Nexus matches the selected file by name. Please manually choose the correct one.")); + foreach(QVariant file, result) { + QVariantMap fileInfo = file.toMap(); + selection.addChoice(fileInfo["uri"].toString(), "", file); + } + 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(); + } 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) { + qWarning("could not determine file id for %s", info->m_FileName.toUtf8().constData()); + } + + info->m_State = STATE_READY; + } + createMetaFile(info); +} + + +void DownloadManager::nxmFileInfoAvailable(int modID, int fileID, QVariant, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + NexusInfo info; + + QVariantMap result = resultData.toMap(); + + info.m_Name = result["name"].toString(); + info.m_Version = result["version"].toString(); + info.m_FileName = result["uri"].toString(); + + m_RequestIDs.insert(m_NexusInterface->requestDownloadURL(modID, fileID, this, qVariantFromValue(info))); +} + + +// sort function to sort by best download server +bool DownloadManager::ServerByPreference(const QVariant &LHS, const QVariant &RHS) +{ + QVariantMap LHSMap = LHS.toMap(); + QVariantMap RHSMap = RHS.toMap(); + int LHSUsers = LHSMap["ConnectedUsers"].toInt(); + int RHSUsers = RHSMap["ConnectedUsers"].toInt(); + bool LHSPremium = LHSMap["IsPremium"].toBool(); + bool RHSPremium = RHSMap["IsPremium"].toBool(); + + // 0 users is probably a sign that the server is offline. Since there is currently no + // mechanism to try a different server, we avoid those without users + if ((LHSUsers == 0) && (RHSUsers != 0)) return false; + if ((LHSUsers != 0) && (RHSUsers == 0)) return true; + + + if (LHSPremium && !RHSPremium) { + return true; + } else if (!LHSPremium && RHSPremium) { + return false; + } + + // TODO implement country preference + + return LHSUsers < RHSUsers; +} + + +void DownloadManager::nxmDownloadURLsAvailable(int modID, int fileID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + NexusInfo info = userData.value(); + QVariantList resultList = resultData.toList(); + if (resultList.length() == 0) { + emit showMessage(tr("No download server available. Please try again later.")); + return; + } + qSort(resultList.begin(), resultList.end(), ServerByPreference); + + QVariantMap dlServer = resultList.first().toMap(); + qDebug("picked url: %s", dlServer["URI"].toString().toUtf8().constData()); + + + addDownload(dlServer["URI"].toString(), modID, fileID, info); +} + + +void DownloadManager::nxmRequestFailed(int modID, QVariant, int requestID, const QString &errorString) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + int index = 0; + + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter, ++index) { + DownloadInfo *info = *iter; + if (info->m_ModID == modID) { + if (info->m_State == STATE_STARTED) { + m_ActiveDownloads.erase(iter); + delete info; + } else if ((info->m_State == STATE_FETCHINGFILEINFO) || + (info->m_State == STATE_FETCHINGMODINFO)) { + info->m_State = STATE_READY; + } + emit update(index); + break; + } + } + emit showMessage(tr("Failed to request file info from nexus: %1").arg(errorString)); +} + + +void DownloadManager::downloadFinished() +{ + int index = 0; + + DownloadInfo *info = findDownload(this->sender(), &index); + if (info != NULL) { + QNetworkReply *reply = info->m_Reply; + QByteArray data = info->m_Reply->readAll(); + qDebug("finished %s (%d) (%d)", info->m_FileName.toUtf8().constData(), reply->error(), info->m_State); + info->m_Output.write(data); + info->m_Output.close(); + + if ((info->m_State != STATE_CANCELING) && + (info->m_State != STATE_PAUSING)) { + if ((info->m_Output.size() == 0) || + (reply->error() != QNetworkReply::NoError) || + reply->header(QNetworkRequest::ContentTypeHeader).toString().startsWith("text", Qt::CaseInsensitive)) { + emit showMessage(tr("Download failed: %1 (%2)").arg(reply->errorString()).arg(reply->error())); + info->m_State = STATE_CANCELING; + } + } + + if (info->m_State == STATE_CANCELING) { + info->m_Reply->abort(); + info->m_State = STATE_CANCELED; + } else if (info->m_State == STATE_PAUSING) { + info->m_Output.write(info->m_Reply->readAll()); + info->m_Reply->abort(); + + info->m_State = STATE_PAUSED; + } + + if (info->m_State == STATE_CANCELED) { + emit aboutToUpdate(); + info->m_Output.remove(); + delete info; + m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); + emit update(-1); + } else if (info->m_State == STATE_PAUSED) { + info->m_Output.close(); + createMetaFile(info); + emit update(index); + } else { + info->m_State = STATE_FETCHINGMODINFO; + QString newName = getFileNameFromNetworkReply(reply); + QString oldName = QFileInfo(info->m_Output).fileName(); + if (!newName.isEmpty() && (newName != oldName)) { + info->setName(getDownloadFileName(newName), true); + } else { + info->setName(m_OutputDirectory + "/" + info->m_FileName, true); // don't rename but remove the ".unfinished" extension + } + + m_RequestIDs.insert(m_NexusInterface->requestDescription(info->m_ModID, this, qVariantFromValue(static_cast(info)))); + + emit update(index); + } + reply->close(); + reply->deleteLater(); + } else { + qWarning("no download index %d", index); + } +} + + +void DownloadManager::metaDataChanged() +{ + int index = 0; + + DownloadInfo *info = findDownload(this->sender(), &index); + if (info != NULL) { + QString newName = getFileNameFromNetworkReply(info->m_Reply); + if (!newName.isEmpty() && (newName != info->m_FileName)) { + info->setName(getDownloadFileName(newName), true); + refreshAlphabeticalTranslation(); + if (!info->m_Output.open(QIODevice::WriteOnly | QIODevice::Append)) { + reportError(tr("failed to re-open %1").arg(info->m_FileName)); + info->m_State = STATE_CANCELING; + info->m_Reply->abort(); + } + } + } else { + qWarning("meta data event for unknown download"); + } +} + +void DownloadManager::directoryChanged(const QString&) +{ + refreshList(); +} diff --git a/src/editexecutablesdialog.cpp b/src/editexecutablesdialog.cpp index a364c65d..419d1395 100644 --- a/src/editexecutablesdialog.cpp +++ b/src/editexecutablesdialog.cpp @@ -100,7 +100,8 @@ void EditExecutablesDialog::on_addButton_clicked() m_ExecutablesList.addExecutable(titleEdit->text(), QDir::fromNativeSeparators(binaryEdit->text()), argumentsEdit->text(), QDir::fromNativeSeparators(workingDirEdit->text()), (closeCheckBox->checkState() == Qt::Checked) ? DEFAULT_CLOSE : DEFAULT_STAY, - ui->overwriteAppIDBox->isChecked() ? ui->appIDOverwriteEdit->text() : ""); + ui->overwriteAppIDBox->isChecked() ? ui->appIDOverwriteEdit->text() : "", + true, false); resetInput(); refreshExecutablesWidget(); diff --git a/src/executableslist.cpp b/src/executableslist.cpp index ed66d8f4..036671f0 100644 --- a/src/executableslist.cpp +++ b/src/executableslist.cpp @@ -17,178 +17,194 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include "executableslist.h" -#include -#include -#include -#include "utility.h" -#include - - -QDataStream &operator<<(QDataStream &out, const Executable &obj) -{ - out << obj.m_Title << obj.m_BinaryInfo.absoluteFilePath() << obj.m_Arguments << obj.m_CloseMO - << obj.m_SteamAppID << obj.m_WorkingDirectory << obj.m_Custom; - return out; -} - -QDataStream &operator>>(QDataStream &in, Executable &obj) -{ - QString binaryTemp; - int closeStyleTemp; - in >> obj.m_Title >> binaryTemp >> obj.m_Arguments >> closeStyleTemp - >> obj.m_SteamAppID >> obj.m_WorkingDirectory >> obj.m_Custom; - obj.m_CloseMO = (CloseMOStyle)closeStyleTemp; - obj.m_BinaryInfo.setFile(binaryTemp); - return in; -} - - -void registerExecutable() -{ - qRegisterMetaType("Executable"); - qRegisterMetaTypeStreamOperators("Executable"); -} - - -ExecutablesList::ExecutablesList() -{ -} - -ExecutablesList::~ExecutablesList() -{ -} - -void ExecutablesList::init() -{ - std::vector executables = GameInfo::instance().getExecutables(); - for (std::vector::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), - iter->closeMO, ToQString(iter->steamAppID)); - } -} - - -void ExecutablesList::getExecutables(std::vector::iterator &begin, std::vector::iterator &end) -{ - begin = m_Executables.begin(); - end = m_Executables.end(); -} - - -void ExecutablesList::getExecutables(std::vector::const_iterator &begin, - std::vector::const_iterator &end) const -{ - begin = m_Executables.begin(); - end = m_Executables.end(); -} - - -const Executable &ExecutablesList::find(const QString &title) const -{ - for (std::vector::const_iterator iter = m_Executables.begin(); iter != m_Executables.end(); ++iter) { - if (iter->m_Title == title) { - return *iter; - } - } - throw std::runtime_error("invalid name"); -} - - -Executable *ExecutablesList::findExe(const QString &title) -{ - for (std::vector::iterator iter = m_Executables.begin(); iter != m_Executables.end(); ++iter) { - if (iter->m_Title == title) { - return &*iter; - } - } - return NULL; -} - - -bool ExecutablesList::titleExists(const QString &title) const -{ - auto test = [&] (const Executable &exe) { return exe.m_Title == title; }; - return std::find_if(m_Executables.begin(), m_Executables.end(), test) != m_Executables.end(); -} - - -void ExecutablesList::addExecutable(const Executable &executable) -{ - Executable *existingExe = findExe(executable.m_Title); - if (existingExe != NULL) { - *existingExe = executable; - } else { - m_Executables.push_back(executable); - } -} - - -void ExecutablesList::addExecutable(const QString &title, const QString &executableName, const QString &arguments, - const QString &workingDirectory, CloseMOStyle closeMO, const QString &steamAppID) -{ - QFileInfo file(executableName); - Executable *existingExe = findExe(title); - if (existingExe != NULL) { - existingExe->m_Title = title; - existingExe->m_CloseMO = closeMO; - existingExe->m_BinaryInfo = file; - existingExe->m_Arguments = arguments; - existingExe->m_WorkingDirectory = workingDirectory; - existingExe->m_SteamAppID = steamAppID; - existingExe->m_Custom = true; - } else { - Executable newExe; - newExe.m_Title = title; - newExe.m_CloseMO = closeMO; - newExe.m_BinaryInfo = file; - newExe.m_Arguments = arguments; - newExe.m_WorkingDirectory = workingDirectory; - newExe.m_SteamAppID = steamAppID; - newExe.m_Custom = true; - m_Executables.push_back(newExe); - } -} - -/*void ExecutablesList::remove(const QString &executableName) -{ - for (std::vector::iterator iter = m_Executables.begin(); iter != m_Executables.end(); ++iter) { - if (iter->m_Custom && (iter->m_BinaryInfo.absoluteFilePath() == executableName)) { - m_Executables.erase(iter); - break; - } - } -}*/ - - -void ExecutablesList::remove(const QString &title) -{ - for (std::vector::iterator iter = m_Executables.begin(); iter != m_Executables.end(); ++iter) { - if (iter->m_Custom && (iter->m_Title == title)) { - m_Executables.erase(iter); - break; - } - } -} - - -void ExecutablesList::addExecutableInternal(const QString &title, const QString &executableName, - const QString &arguments, const QString &workingDirectory, - CloseMOStyle closeMO, const QString &steamAppID) -{ - QFileInfo file(executableName); - if (file.exists()) { - Executable newExe; - newExe.m_CloseMO = closeMO; - newExe.m_BinaryInfo = file; - newExe.m_Title = title; - newExe.m_Arguments = arguments; - newExe.m_WorkingDirectory = workingDirectory; - newExe.m_SteamAppID = steamAppID; - newExe.m_Custom = false; - m_Executables.push_back(newExe); - } -} +#include "executableslist.h" +#include +#include +#include +#include "utility.h" +#include + + +QDataStream &operator<<(QDataStream &out, const Executable &obj) +{ + out << obj.m_Title << obj.m_BinaryInfo.absoluteFilePath() << obj.m_Arguments << obj.m_CloseMO + << obj.m_SteamAppID << obj.m_WorkingDirectory << obj.m_Custom << obj.m_Toolbar; + return out; +} + +QDataStream &operator>>(QDataStream &in, Executable &obj) +{ + QString binaryTemp; + int closeStyleTemp; + in >> obj.m_Title >> binaryTemp >> obj.m_Arguments >> closeStyleTemp + >> obj.m_SteamAppID >> obj.m_WorkingDirectory >> obj.m_Custom >> obj.m_Toolbar; + + obj.m_CloseMO = (CloseMOStyle)closeStyleTemp; + obj.m_BinaryInfo.setFile(binaryTemp); + return in; +} + + +void registerExecutable() +{ + qRegisterMetaType("Executable"); + qRegisterMetaTypeStreamOperators("Executable"); +} + + +ExecutablesList::ExecutablesList() +{ +} + +ExecutablesList::~ExecutablesList() +{ +} + +void ExecutablesList::init() +{ + std::vector executables = GameInfo::instance().getExecutables(); + for (std::vector::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), + iter->closeMO, ToQString(iter->steamAppID)); + } +} + + +void ExecutablesList::getExecutables(std::vector::iterator &begin, std::vector::iterator &end) +{ + begin = m_Executables.begin(); + end = m_Executables.end(); +} + + +void ExecutablesList::getExecutables(std::vector::const_iterator &begin, + std::vector::const_iterator &end) const +{ + begin = m_Executables.begin(); + end = m_Executables.end(); +} + + +const Executable &ExecutablesList::find(const QString &title) const +{ + for (std::vector::const_iterator iter = m_Executables.begin(); iter != m_Executables.end(); ++iter) { + if (iter->m_Title == title) { + return *iter; + } + } + throw std::runtime_error("invalid name"); +} + + +Executable &ExecutablesList::find(const QString &title) +{ + for (std::vector::iterator iter = m_Executables.begin(); iter != m_Executables.end(); ++iter) { + if (iter->m_Title == title) { + return *iter; + } + } + throw std::runtime_error("invalid name"); +} + + +Executable *ExecutablesList::findExe(const QString &title) +{ + for (std::vector::iterator iter = m_Executables.begin(); iter != m_Executables.end(); ++iter) { + if (iter->m_Title == title) { + return &*iter; + } + } + return NULL; +} + + +bool ExecutablesList::titleExists(const QString &title) const +{ + auto test = [&] (const Executable &exe) { return exe.m_Title == title; }; + return std::find_if(m_Executables.begin(), m_Executables.end(), test) != m_Executables.end(); +} + + +void ExecutablesList::addExecutable(const Executable &executable) +{ + Executable *existingExe = findExe(executable.m_Title); + if (existingExe != NULL) { + *existingExe = executable; + } else { + m_Executables.push_back(executable); + } +} + + +void ExecutablesList::addExecutable(const QString &title, const QString &executableName, const QString &arguments, + const QString &workingDirectory, CloseMOStyle closeMO, const QString &steamAppID, + bool custom, bool toolbar) +{ + QFileInfo file(executableName); + Executable *existingExe = findExe(title); + if (existingExe != NULL) { + existingExe->m_Title = title; + existingExe->m_CloseMO = closeMO; + existingExe->m_BinaryInfo = file; + existingExe->m_Arguments = arguments; + existingExe->m_WorkingDirectory = workingDirectory; + existingExe->m_SteamAppID = steamAppID; + existingExe->m_Custom = custom; + existingExe->m_Toolbar = toolbar; + } else { + Executable newExe; + newExe.m_Title = title; + newExe.m_CloseMO = closeMO; + newExe.m_BinaryInfo = file; + newExe.m_Arguments = arguments; + newExe.m_WorkingDirectory = workingDirectory; + newExe.m_SteamAppID = steamAppID; + newExe.m_Custom = true; + newExe.m_Toolbar = toolbar; + m_Executables.push_back(newExe); + } +} + +/*void ExecutablesList::remove(const QString &executableName) +{ + for (std::vector::iterator iter = m_Executables.begin(); iter != m_Executables.end(); ++iter) { + if (iter->m_Custom && (iter->m_BinaryInfo.absoluteFilePath() == executableName)) { + m_Executables.erase(iter); + break; + } + } +}*/ + + +void ExecutablesList::remove(const QString &title) +{ + for (std::vector::iterator iter = m_Executables.begin(); iter != m_Executables.end(); ++iter) { + if (iter->m_Custom && (iter->m_Title == title)) { + m_Executables.erase(iter); + break; + } + } +} + + +void ExecutablesList::addExecutableInternal(const QString &title, const QString &executableName, + const QString &arguments, const QString &workingDirectory, + CloseMOStyle closeMO, const QString &steamAppID) +{ + QFileInfo file(executableName); + if (file.exists()) { + Executable newExe; + newExe.m_CloseMO = closeMO; + newExe.m_BinaryInfo = file; + newExe.m_Title = title; + newExe.m_Arguments = arguments; + newExe.m_WorkingDirectory = workingDirectory; + newExe.m_SteamAppID = steamAppID; + newExe.m_Custom = false; + newExe.m_Toolbar = false; + m_Executables.push_back(newExe); + } +} diff --git a/src/executableslist.h b/src/executableslist.h index 6a7d36eb..7016c560 100644 --- a/src/executableslist.h +++ b/src/executableslist.h @@ -17,129 +17,139 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#ifndef EXECUTABLESLIST_H -#define EXECUTABLESLIST_H - - -#include -#include -#include -#include - - -/*! - * @brief Information about an executable - **/ -struct Executable { - QString m_Title; - QFileInfo m_BinaryInfo; - QString m_Arguments; - CloseMOStyle m_CloseMO; - QString m_SteamAppID; - QString m_WorkingDirectory; - - bool m_Custom; -}; -Q_DECLARE_METATYPE(Executable) - - -void registerExecutable(); - - -/*! - * @brief List of executables configured to by started from MO - **/ -class ExecutablesList { -public: - - /** - * @brief constructor - * - **/ - ExecutablesList(); - - ~ExecutablesList(); - - /** - * @brief initialise the list with the executables preconfigured for this game - **/ - void init(); - - /** - * @brief retrieve an executable by index - * - * @param index index of the executable to look up - * @return reference to the executable - * @exception out_of_range will throw an exception if the index is invalid - **/ - const Executable &get(int index) const { return m_Executables.at(index); } - - /** - * @brief find an executable by its name - * - * @param title the title of the executable to look up - * @return the executable - * @exception runtime_error will throw an exception if the name is not correct - **/ - const Executable &find(const QString &tilte) const; - - /** - * @brief determine if an executable exists - * @param title the title of the executable to look up - * @return true if the executable exists, false otherwise - **/ - bool titleExists(const QString &title) const; - - /** - * @brief add a new executable to the list - * @param executable - */ - void addExecutable(const Executable &executable); - - /** - * @brief add a new executable to the list - * - * @param title name displayed in the UI - * @param executableName the actual filename to execute - * @param arguments arguments to pass to the executable - * @param closeMO if true, MO will be closed when the binary is started - **/ - void addExecutable(const QString &title, const QString &executableName, const QString &arguments, const QString &workingDirectory, CloseMOStyle closeMO, const QString &steamAppID); - - /** - * @brief remove the executable with the specified file name. This needs to be an absolute file path - * - * @param title title of the executable to remove - * @note if the executable name is invalid, nothing happens. There is no way to determine if this was successful - **/ - void remove(const QString &title); - - /** - * @brief retrieve begin and end iterators of the configured executables - * - * @param begin iterator to the first executable - * @param end iterator one past the last executable - **/ - void getExecutables(std::vector::const_iterator &begin, std::vector::const_iterator &end) const; - - /** - * @brief retrieve begin and end iterators of the configured executables - * - * @param begin iterator to the first executable - * @param end iterator one past the last executable - **/ - void getExecutables(std::vector::iterator &begin, std::vector::iterator &end); - -private: - - Executable *findExe(const QString &title); - - void addExecutableInternal(const QString &title, const QString &executableName, const QString &arguments, const QString &workingDirectory, CloseMOStyle closeMO, const QString &steamAppID); - -private: - - std::vector m_Executables; - -}; - -#endif // EXECUTABLESLIST_H +#ifndef EXECUTABLESLIST_H +#define EXECUTABLESLIST_H + + +#include +#include +#include +#include + + +/*! + * @brief Information about an executable + **/ +struct Executable { + QString m_Title; + QFileInfo m_BinaryInfo; + QString m_Arguments; + CloseMOStyle m_CloseMO; + QString m_SteamAppID; + QString m_WorkingDirectory; + + bool m_Custom; + bool m_Toolbar; +}; +Q_DECLARE_METATYPE(Executable) + + +void registerExecutable(); + + +/*! + * @brief List of executables configured to by started from MO + **/ +class ExecutablesList { +public: + + /** + * @brief constructor + * + **/ + ExecutablesList(); + + ~ExecutablesList(); + + /** + * @brief initialise the list with the executables preconfigured for this game + **/ + void init(); + + /** + * @brief retrieve an executable by index + * + * @param index index of the executable to look up + * @return reference to the executable + * @exception out_of_range will throw an exception if the index is invalid + **/ + const Executable &get(int index) const { return m_Executables.at(index); } + + /** + * @brief find an executable by its name + * + * @param title the title of the executable to look up + * @return the executable + * @exception runtime_error will throw an exception if the name is not correct + **/ + const Executable &find(const QString &tilte) const; + + /** + * @brief find an executable by its name + * + * @param title the title of the executable to look up + * @return the executable + * @exception runtime_error will throw an exception if the name is not correct + **/ + Executable &find(const QString &tilte); + + /** + * @brief determine if an executable exists + * @param title the title of the executable to look up + * @return true if the executable exists, false otherwise + **/ + bool titleExists(const QString &title) const; + + /** + * @brief add a new executable to the list + * @param executable + */ + void addExecutable(const Executable &executable); + + /** + * @brief add a new executable to the list + * + * @param title name displayed in the UI + * @param executableName the actual filename to execute + * @param arguments arguments to pass to the executable + * @param closeMO if true, MO will be closed when the binary is started + **/ + void addExecutable(const QString &title, const QString &executableName, const QString &arguments, const QString &workingDirectory, CloseMOStyle closeMO, const QString &steamAppID, bool custom, bool toolbar); + + /** + * @brief remove the executable with the specified file name. This needs to be an absolute file path + * + * @param title title of the executable to remove + * @note if the executable name is invalid, nothing happens. There is no way to determine if this was successful + **/ + void remove(const QString &title); + + /** + * @brief retrieve begin and end iterators of the configured executables + * + * @param begin iterator to the first executable + * @param end iterator one past the last executable + **/ + void getExecutables(std::vector::const_iterator &begin, std::vector::const_iterator &end) const; + + /** + * @brief retrieve begin and end iterators of the configured executables + * + * @param begin iterator to the first executable + * @param end iterator one past the last executable + **/ + void getExecutables(std::vector::iterator &begin, std::vector::iterator &end); + +private: + + Executable *findExe(const QString &title); + + void addExecutableInternal(const QString &title, const QString &executableName, const QString &arguments, const QString &workingDirectory, CloseMOStyle closeMO, const QString &steamAppID); + +private: + + std::vector m_Executables; + +}; + +#endif // EXECUTABLESLIST_H diff --git a/src/fomodinstallerdialog.cpp b/src/fomodinstallerdialog.cpp index 16f6db60..bca05567 100644 --- a/src/fomodinstallerdialog.cpp +++ b/src/fomodinstallerdialog.cpp @@ -17,923 +17,940 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include "fomodinstallerdialog.h" -#include "report.h" -#include "utility.h" -#include "ui_fomodinstallerdialog.h" - -#include -#include -#include -#include -#include -#include -#include - - -bool ControlsAscending(QAbstractButton *LHS, QAbstractButton *RHS) -{ - return LHS->text() < RHS->text(); -} - - -bool ControlsDescending(QAbstractButton *LHS, QAbstractButton *RHS) -{ - return LHS->text() > RHS->text(); -} - - -bool PagesAscending(QGroupBox *LHS, QGroupBox *RHS) -{ - return LHS->title() < RHS->title(); -} - - -bool PagesDescending(QGroupBox *LHS, QGroupBox *RHS) -{ - return LHS->title() > RHS->title(); -} - - -FomodInstallerDialog::FomodInstallerDialog(const QString &modName, const QString &fomodPath, QWidget *parent) - : QDialog(parent), ui(new Ui::FomodInstallerDialog), m_FomodPath(fomodPath), m_Manual(false) -{ - ui->setupUi(this); - ui->nameEdit->setText(modName); -} - -FomodInstallerDialog::~FomodInstallerDialog() -{ - delete ui; -} - -#pragma message("implement module dependencies->file dependencies") - -void FomodInstallerDialog::initData() -{ - { // parse provided package information - QFile file(QDir::tempPath().append("/info.xml")); - if (file.open(QIODevice::ReadOnly)) { - // nmm allows files with wrong encoding and of course there are now files with broken - // so, let's do as nmm does and ignore the standard. yay - QByteArray header = file.readLine(); - if (strncmp(header.constData(), "screenshotLabel->width()); - ui->screenshotLabel->setPixmap(QPixmap::fromImage(screenshot)); - } - - { // parse xml installer file - QFile file(QDir::tempPath().append("/ModuleConfig.xml")); - if (!file.open(QIODevice::ReadOnly)) { - throw MyException(tr("ModuleConfig.xml missing")); - } - // nmm allows files with wrong encoding and of course there are now files that are broken - QByteArray header = file.readLine(); - if (strncmp(header.constData(), "nameEdit->text(); -} - - -void FomodInstallerDialog::moveTree(DirectoryTree::Node *target, DirectoryTree::Node *source) -{ - for (DirectoryTree::const_node_iterator iter = source->nodesBegin(); iter != source->nodesEnd();) { - target->addNode(*iter, true); - iter = source->detach(iter); - } - - for (DirectoryTree::const_leaf_reverse_iterator iter = source->leafsRBegin(); - iter != source->leafsREnd(); ++iter) { - target->addLeaf(*iter); - } -} - - -DirectoryTree::Node *FomodInstallerDialog::findNode(DirectoryTree::Node *node, const QString &path, bool create) -{ - if (path.length() == 0) { - return node; - } - -// static QRegExp pathSeparator("[/\\]"); - int pos = path.indexOf('\\'); - if (pos == -1) { - pos = path.indexOf('/'); - } - QString subPath = path; - if (pos > 0) { - subPath = path.mid(0, pos); - } - for (DirectoryTree::const_node_iterator iter = node->nodesBegin(); iter != node->nodesEnd(); ++iter) { - if ((*iter)->getData().name.compare(subPath, Qt::CaseInsensitive) == 0) { - if (pos <= 0) { - return *iter; - } else { - return findNode(*iter, path.mid(pos + 1), create); - } - } - } - if (create) { - DirectoryTree::Node *newNode = new DirectoryTree::Node; - newNode->setData(subPath); - node->addNode(newNode, false); - if (pos <= 0) { - return newNode; - } else { - return findNode(newNode, path.mid(pos + 1), create); - } - } else { - throw MyException(QString("%1 not found in archive").arg(path)); - } -} - -void FomodInstallerDialog::copyLeaf(DirectoryTree::Node *sourceTree, const QString &sourcePath, - DirectoryTree::Node *destinationTree, const QString &destinationPath) -{ - int sourceFileIndex = sourcePath.lastIndexOf('\\'); - if (sourceFileIndex == -1) { - sourceFileIndex = sourcePath.lastIndexOf('/'); - if (sourceFileIndex == -1) { - sourceFileIndex = 0; - } - } - DirectoryTree::Node *sourceNode = sourceFileIndex == 0 ? sourceTree : findNode(sourceTree, sourcePath.mid(0, sourceFileIndex), false); - - int destinationFileIndex = destinationPath.lastIndexOf('\\'); - if (destinationFileIndex == -1) { - destinationFileIndex = destinationPath.lastIndexOf('/'); - if (destinationFileIndex == -1) { - destinationFileIndex = 0; - } - } - - DirectoryTree::Node *destinationNode = - destinationFileIndex == 0 ? destinationTree - : findNode(destinationTree, destinationPath.mid(0, destinationFileIndex), true); - - QString sourceName = sourcePath.mid((sourceFileIndex != 0) ? sourceFileIndex + 1 : 0); - QString destinationName = (destinationFileIndex != 0) ? destinationPath.mid(destinationFileIndex + 1) : destinationPath; - if (destinationName.length() == 0) { - destinationName = sourceName; - } - - bool found = false; - for (DirectoryTree::const_leaf_reverse_iterator iter = sourceNode->leafsRBegin(); - iter != sourceNode->leafsREnd(); ++iter) { - if (iter->getName().compare(sourceName, Qt::CaseInsensitive) == 0) { - destinationNode->addLeaf(*iter); - found = true; - } - } - if (!found) { - qCritical("%s not found!", sourceName.toUtf8().constData()); - } -} - - -void dumpTree(DirectoryTree::Node *node, int indent) -{ - for (DirectoryTree::const_leaf_reverse_iterator iter = node->leafsRBegin(); - iter != node->leafsREnd(); ++iter) { - qDebug("%.*s%s", indent, " ", iter->getName().toUtf8().constData()); - } - - for (DirectoryTree::const_node_iterator iter = node->nodesBegin(); iter != node->nodesEnd(); ++iter) { - qDebug("%.*s-- %s", indent, " ", (*iter)->getData().name.toUtf8().constData()); - dumpTree(*iter, indent + 2); - } -} - - -bool FomodInstallerDialog::copyFileIterator(DirectoryTree *sourceTree, DirectoryTree *destinationTree, FileDescriptor *descriptor) -{ - QString source = (m_FomodPath.length() != 0) ? m_FomodPath.mid(0).append("\\").append(descriptor->m_Source) - : descriptor->m_Source; - QString destination = descriptor->m_Destination; - try { - if (descriptor->m_IsFolder) { - DirectoryTree::Node *sourceNode = findNode(sourceTree, source, false); - DirectoryTree::Node *targetNode = findNode(destinationTree, destination, true); - moveTree(targetNode, sourceNode); - } else { - copyLeaf(sourceTree, source, destinationTree, destination); - } - return true; - } catch (const MyException &e) { - qCritical("failed to extract %s to %s: %s", - source.toUtf8().constData(), destination.toUtf8().constData(), e.what()); - return false; - } -} - - -DirectoryTree *FomodInstallerDialog::updateTree(DirectoryTree *tree) -{ - DirectoryTree *newTree = new DirectoryTree; - - for (std::vector::iterator iter = m_RequiredFiles.begin(); iter != m_RequiredFiles.end(); ++iter) { - copyFileIterator(tree, newTree, *iter); - } - - for (std::vector::iterator installIter = m_ConditionalInstalls.begin(); - installIter != m_ConditionalInstalls.end(); ++installIter) { - bool match = installIter->m_Operator == ConditionalInstall::OP_AND; - for (std::vector::iterator conditionIter = installIter->m_Conditions.begin(); - conditionIter != installIter->m_Conditions.end(); ++conditionIter) { - bool conditionMatches = testCondition(ui->stepsStack->count(), conditionIter->m_Name, conditionIter->m_Value); - if (conditionMatches && (installIter->m_Operator == ConditionalInstall::OP_OR)) { - match = true; - break; - } else if (!conditionMatches && (installIter->m_Operator == ConditionalInstall::OP_AND)) { - match = false; - break; - } - } - if (match) { - for (std::vector::iterator fileIter = installIter->m_Files.begin(); - fileIter != installIter->m_Files.end(); ++fileIter) { - copyFileIterator(tree, newTree, *fileIter); - } - } - } - - QList choices = ui->stepsStack->findChildren("choice"); - foreach (QAbstractButton* choice, choices) { - if (choice->isChecked()) { - QVariantList fileList = choice->property("files").toList(); - foreach (QVariant fileVariant, fileList) { - copyFileIterator(tree, newTree, fileVariant.value()); - } - } - } - -// dumpTree(newTree, 0); - - return newTree; -} - - -void FomodInstallerDialog::highlightControl(QAbstractButton *button) -{ - QVariant screenshotName = button->property("screenshot"); - if (screenshotName.isValid()) { - QString screenshotFileName = screenshotName.toString(); - if (!screenshotFileName.isEmpty()) { - QString temp = QFileInfo(screenshotFileName).fileName(); - QImage screenshot(QDir::tempPath().append("/").append(temp)); - if (screenshot.isNull()) { - qWarning(">%s< is a null image", screenshotName.toString().toUtf8().constData()); - } - screenshot = screenshot.scaledToWidth(ui->screenshotLabel->width()); - ui->screenshotLabel->setPixmap(QPixmap::fromImage(screenshot)); - } else { - ui->screenshotLabel->setPixmap(QPixmap()); - } - } - ui->descriptionText->setText(button->property("description").toString()); -} - - -bool FomodInstallerDialog::eventFilter(QObject *object, QEvent *event) -{ - QAbstractButton *button = qobject_cast(object); - if ((button != NULL) && (event->type() == QEvent::HoverEnter)) { - highlightControl(button); - - } - return QDialog::eventFilter(object, event); -} - - -QString FomodInstallerDialog::readContent(QXmlStreamReader &reader) -{ - if (reader.readNext() == QXmlStreamReader::Characters) { - return reader.text().toString(); - } else { - return QString(); - } -} - - -void FomodInstallerDialog::parseInfo(const QByteArray &data) -{ - QXmlStreamReader reader(data); -/* while (reader.readNext() != QXmlStreamReader::StartDocument) {} - QTextDecoder *decoder = QTextCodec::codecForName(reader.documentEncoding().toLocal8Bit())->makeDecoder(QTextCodec::ConvertInvalidToNull); - QString test(decoder->toUnicode(data)); - qDebug("test: %d, %s", test.isNull(), test.toUtf8().constData()); - - qDebug(">%s<", reader.documentEncoding().toUtf8().constData());*/ - while (!reader.atEnd() && !reader.hasError()) { - switch (reader.readNext()) { - case QXmlStreamReader::StartElement: { - if (reader.name() == "Name") { - ui->nameEdit->setText(readContent(reader)); - } else if (reader.name() == "Author") { - ui->authorLabel->setText(readContent(reader)); - } else if (reader.name() == "Version") { - ui->versionLabel->setText(readContent(reader)); - } else if (reader.name() == "Website") { - QString url = readContent(reader); - ui->websiteLabel->setText(tr("Link").arg(url)); - ui->websiteLabel->setToolTip(url); - } - } break; - default: {} break; - } - } - if (reader.hasError()) { - throw MyException(tr("failed to parse info.xml: %1 (%2) (line %3, column %4)") - .arg(reader.errorString()) - .arg(reader.error()) - .arg(reader.lineNumber()) - .arg(reader.columnNumber())); - - } -} - - -FomodInstallerDialog::ItemOrder FomodInstallerDialog::getItemOrder(const QString &orderString) -{ - if (orderString == "Ascending") { - return ORDER_ASCENDING; - } else if (orderString == "Descending") { - return ORDER_DESCENDING; - } else if (orderString == "Explicit") { - return ORDER_EXPLICIT; - } else { - throw MyException(tr("unsupported order type %1").arg(orderString)); - } -} - - -FomodInstallerDialog::GroupType FomodInstallerDialog::getGroupType(const QString &typeString) -{ - if (typeString == "SelectAtLeastOne") { - return TYPE_SELECTATLEASTONE; - } else if (typeString == "SelectAtMostOne") { - return TYPE_SELECTATMOSTONE; - } else if (typeString == "SelectExactlyOne") { - return TYPE_SELECTEXACTLYONE; - } else if (typeString == "SelectAny") { - return TYPE_SELECTANY; - } else if (typeString == "SelectAll") { - return TYPE_SELECTALL; - } else { - throw MyException(tr("unsupported group type %1").arg(typeString)); - } -} - - -FomodInstallerDialog::PluginType FomodInstallerDialog::getPluginType(const QString &typeString) -{ - if (typeString == "Required") { - return FomodInstallerDialog::TYPE_REQUIRED; - } else if (typeString == "Optional") { - return FomodInstallerDialog::TYPE_OPTIONAL; - } else if (typeString == "Recommended") { - return FomodInstallerDialog::TYPE_RECOMMENDED; - } else if (typeString == "NotUsable") { - return FomodInstallerDialog::TYPE_NOTUSABLE; - } else if (typeString == "CouldBeUsable") { - return FomodInstallerDialog::TYPE_COULDBEUSABLE; - } else { - qCritical("invalid plugin type %s", typeString.toUtf8().constData()); - return FomodInstallerDialog::TYPE_OPTIONAL; - } -} - - -void FomodInstallerDialog::readFileList(QXmlStreamReader &reader, std::vector &fileList) -{ - QStringRef openTag = reader.name(); - while (!((reader.readNext() == QXmlStreamReader::EndElement) && - (reader.name() == openTag))) { - if (reader.tokenType() == QXmlStreamReader::StartElement) { - if ((reader.name() == "folder") || - (reader.name() == "file")) { - QXmlStreamAttributes attributes = reader.attributes(); - FileDescriptor *file = new FileDescriptor(this); - file->m_Source = attributes.value("source").toString(); - file->m_Destination = attributes.hasAttribute("destination") ? attributes.value("destination").toString() - : file->m_Source; - file->m_Priority = attributes.hasAttribute("priority") ? attributes.value("priority").string()->toInt() - : 0; - file->m_IsFolder = reader.name() == "folder"; - file->m_InstallIfUsable = attributes.hasAttribute("installIfUsable") ? (attributes.value("installIfUsable").compare("true") == 0) - : false; - file->m_AlwaysInstall = attributes.hasAttribute("alwaysInstall") ? (attributes.value("alwaysInstall").compare("true") == 0) - : false; - - fileList.push_back(file); - } - } - } -} - - -void FomodInstallerDialog::readPluginType(QXmlStreamReader &reader, Plugin &plugin) -{ - plugin.m_Type = TYPE_OPTIONAL; - while (!((reader.readNext() == QXmlStreamReader::EndElement) && - (reader.name() == "typeDescriptor"))) { - if (reader.tokenType() == QXmlStreamReader::StartElement) { - if (reader.name() == "type") { - plugin.m_Type = getPluginType(reader.attributes().value("name").toString()); - } - } - } -} - - -void FomodInstallerDialog::readConditionFlags(QXmlStreamReader &reader, Plugin &plugin) -{ - while (!((reader.readNext() == QXmlStreamReader::EndElement) && - (reader.name() == "conditionFlags"))) { - if (reader.tokenType() == QXmlStreamReader::StartElement) { - if (reader.name() == "flag") { - QString name = reader.attributes().value("name").toString(); - plugin.m_Conditions.push_back(Condition(name, readContent(reader))); - } - } - } -} - - -bool FomodInstallerDialog::byPriority(const FileDescriptor *LHS, const FileDescriptor *RHS) -{ - return LHS->m_Priority < RHS->m_Priority; -} - - -FomodInstallerDialog::Plugin FomodInstallerDialog::readPlugin(QXmlStreamReader &reader) -{ - Plugin result; - result.m_Name = reader.attributes().value("name").toString(); - - while (!((reader.readNext() == QXmlStreamReader::EndElement) && - (reader.name() == "plugin"))) { -// QXmlStreamReader::TokenType type = reader.tokenType(); -// QString name = reader.name().toUtf8(); - if (reader.tokenType() == QXmlStreamReader::StartElement) { - if (reader.name() == "description") { - result.m_Description = readContent(reader).trimmed(); - } else if (reader.name() == "image") { - result.m_ImagePath = reader.attributes().value("path").toString(); - } else if (reader.name() == "typeDescriptor") { - readPluginType(reader, result); - } else if (reader.name() == "conditionFlags") { - readConditionFlags(reader, result); - } else if (reader.name() == "files") { - readFileList(reader, result.m_Files); - } - } - } - - std::sort(result.m_Files.begin(), result.m_Files.end(), byPriority); - - return result; -} - - -void FomodInstallerDialog::readPlugins(QXmlStreamReader &reader, GroupType groupType, QLayout *layout) -{ - ItemOrder pluginOrder = reader.attributes().hasAttribute("order") ? getItemOrder(reader.attributes().value("order").toString()) - : ORDER_ASCENDING; - bool first = true; - bool maySelectMore = true; - - std::vector controls; - - while (!((reader.readNext() == QXmlStreamReader::EndElement) && - (reader.name() == "plugins"))) { - if (reader.tokenType() == QXmlStreamReader::StartElement) { - if (reader.name() == "plugin") { - Plugin plugin = readPlugin(reader); - QAbstractButton *newControl = NULL; - switch (groupType) { - case TYPE_SELECTATLEASTONE: - case TYPE_SELECTANY: { - newControl = new QCheckBox(plugin.m_Name); - } break; - case TYPE_SELECTATMOSTONE: { - newControl = new QRadioButton(plugin.m_Name); - } break; - case TYPE_SELECTEXACTLYONE: { - newControl = new QRadioButton(plugin.m_Name); - if (first) { - newControl->setChecked(true); - } - } break; - case TYPE_SELECTALL: { - newControl = new QCheckBox(plugin.m_Name); - newControl->setChecked(true); - newControl->setEnabled(false); - } break; - } - newControl->setObjectName("choice"); - switch (plugin.m_Type) { - case TYPE_REQUIRED: { - newControl->setChecked(true); - newControl->setEnabled(false); - newControl->setToolTip(tr("This component is required")); - } break; - case TYPE_RECOMMENDED: { - if (maySelectMore) { - newControl->setChecked(true); - } - newControl->setToolTip(tr("It is recommended you enable this component")); - if ((groupType == TYPE_SELECTATMOSTONE) || (groupType == TYPE_SELECTEXACTLYONE)) { - maySelectMore = false; - } - } break; - case TYPE_OPTIONAL: { - newControl->setToolTip(tr("Optional component")); - } break; - case TYPE_NOTUSABLE: { - newControl->setChecked(false); - newControl->setEnabled(false); - newControl->setToolTip(tr("This component is not usable in combination with other installed plugins")); - } break; - case TYPE_COULDBEUSABLE: { - newControl->setCheckable(true); - newControl->setIcon(QIcon(":/new/guiresources/resources/dialog-warning_16.png")); - newControl->setToolTip(tr("You may be experiencing instability in combination with other installed plugins")); - } break; - } - - newControl->setProperty("plugintype", plugin.m_Type); - newControl->setProperty("screenshot", plugin.m_ImagePath); - newControl->setProperty("description", plugin.m_Description); - QVariantList fileList; - for (std::vector::iterator iter = plugin.m_Files.begin(); iter != plugin.m_Files.end(); ++iter) { - fileList.append(qVariantFromValue(*iter)); - } - newControl->setProperty("files", fileList); - QVariantList conditionFlags; - for (std::vector::const_iterator iter = plugin.m_Conditions.begin(); iter != plugin.m_Conditions.end(); ++iter) { - if (iter->m_Name.length() != 0) { - conditionFlags.append(qVariantFromValue(Condition(iter->m_Name, iter->m_Value))); - } - } - newControl->setProperty("conditionFlags", conditionFlags); - newControl->installEventFilter(this); - controls.push_back(newControl); - first = false; - } - } - } - - if (pluginOrder == ORDER_ASCENDING) { - std::sort(controls.begin(), controls.end(), ControlsAscending); - } else if (pluginOrder == ORDER_DESCENDING) { - std::sort(controls.begin(), controls.end(), ControlsDescending); - } - - for (std::vector::const_iterator iter = controls.begin(); iter != controls.end(); ++iter) { - layout->addWidget(*iter); - } - - if (groupType == TYPE_SELECTATMOSTONE) { - QRadioButton *newButton = new QRadioButton(tr("None")); - if (maySelectMore) { - newButton->setChecked(true); - } - layout->addWidget(newButton); - } -} - - -void FomodInstallerDialog::readGroup(QXmlStreamReader &reader, QLayout *layout) -{ - //FileGroup result; - QString name = reader.attributes().value("name").toString(); - GroupType type = getGroupType(reader.attributes().value("type").toString()); - - if (type == TYPE_SELECTATLEASTONE) { - QLabel *label = new QLabel(tr("Select one or more of these options:")); - layout->addWidget(label); - } - - QGroupBox *groupBox = new QGroupBox(name); - - QVBoxLayout *groupLayout = new QVBoxLayout; - - while (!((reader.readNext() == QXmlStreamReader::EndElement) && - (reader.name() == "group"))) { - if (reader.tokenType() == QXmlStreamReader::StartElement) { - if (reader.name() == "plugins") { - readPlugins(reader, type, groupLayout); - } - } - } - - groupBox->setLayout(groupLayout); - layout->addWidget(groupBox); -} - - -void FomodInstallerDialog::readGroups(QXmlStreamReader &reader, QLayout *layout) -{ - while (!((reader.readNext() == QXmlStreamReader::EndElement) && - (reader.name() == "optionalFileGroups"))) { - if (reader.tokenType() == QXmlStreamReader::StartElement) { - if (reader.name() == "group") { - readGroup(reader, layout); - } - } - } -} - - -void FomodInstallerDialog::readVisible(QXmlStreamReader &reader, QVariantList &conditions) -{ - while (!((reader.readNext() == QXmlStreamReader::EndElement) && - (reader.name() == "visible"))) { - if (reader.tokenType() == QXmlStreamReader::StartElement) { - if (reader.name() == "flagDependency") { - Condition condition(reader.attributes().value("flag").toString(), - reader.attributes().value("value").toString()); - conditions.append(qVariantFromValue(condition)); - } - } - } -} - -QGroupBox *FomodInstallerDialog::readInstallerStep(QXmlStreamReader &reader) -{ - QString name = reader.attributes().value("name").toString(); - QGroupBox *page = new QGroupBox(name); - QVBoxLayout *pageLayout = new QVBoxLayout; - QScrollArea *scrollArea = new QScrollArea; - QFrame *scrolledArea = new QFrame; - QVBoxLayout *scrollLayout = new QVBoxLayout; - - QVariantList conditions; - - while (!((reader.readNext() == QXmlStreamReader::EndElement) && - (reader.name() == "installStep"))) { - if (reader.tokenType() == QXmlStreamReader::StartElement) { - if (reader.name() == "optionalFileGroups") { - readGroups(reader, scrollLayout); - } else if (reader.name() == "visible") { - readVisible(reader, conditions); - } - } - } - if (conditions.length() != 0) { - page->setProperty("conditions", conditions); - } - - scrolledArea->setLayout(scrollLayout); - scrollArea->setWidget(scrolledArea); - scrollArea->setWidgetResizable(true); - pageLayout->addWidget(scrollArea); - page->setLayout(pageLayout); - return page; -} - - -void FomodInstallerDialog::readInstallerSteps(QXmlStreamReader &reader) -{ - ItemOrder stepOrder = reader.attributes().hasAttribute("order") ? getItemOrder(reader.attributes().value("order").toString()) - : ORDER_ASCENDING; - - std::vector pages; - - while (!((reader.readNext() == QXmlStreamReader::EndElement) && - (reader.name() == "installSteps"))) { - if (reader.tokenType() == QXmlStreamReader::StartElement) { - if (reader.name() == "installStep") { - pages.push_back(readInstallerStep(reader)); - } - } - } - - if (stepOrder == ORDER_ASCENDING) { - std::sort(pages.begin(), pages.end(), PagesAscending); - } else if (stepOrder == ORDER_DESCENDING) { - std::sort(pages.begin(), pages.end(), PagesDescending); - } - - for (std::vector::const_iterator iter = pages.begin(); iter != pages.end(); ++iter) { - ui->stepsStack->addWidget(*iter); - } -} - - -FomodInstallerDialog::ConditionalInstall FomodInstallerDialog::readConditionalPattern(QXmlStreamReader &reader) -{ - ConditionalInstall result; - result.m_Operator = ConditionalInstall::OP_AND; - while (!((reader.readNext() == QXmlStreamReader::EndElement) && - (reader.name() == "pattern"))) { - if (reader.tokenType() == QXmlStreamReader::StartElement) { - if (reader.name() == "dependencies") { - QStringRef dependencyOperator = reader.attributes().value("operator"); - if (dependencyOperator == "And") { - result.m_Operator = ConditionalInstall::OP_AND; - } else if (dependencyOperator == "Or") { - result.m_Operator = ConditionalInstall::OP_OR; - } // otherwise operator is not set (which we can ignore) or invalid (which we should report actually) - - while (!((reader.readNext() == QXmlStreamReader::EndElement) && - (reader.name() == "dependencies"))) { - if (reader.tokenType() == QXmlStreamReader::StartElement) { - if (reader.name() == "flagDependency") { - result.m_Conditions.push_back(Condition(reader.attributes().value("flag").toString(), - reader.attributes().value("value").toString())); - } - } - } - } else if (reader.name() == "files") { - readFileList(reader, result.m_Files); - } - } - } - return result; -} - - -void FomodInstallerDialog::readConditionalFileInstalls(QXmlStreamReader &reader) -{ - while (!((reader.readNext() == QXmlStreamReader::EndElement) && - (reader.name() == "conditionalFileInstalls"))) { - if (reader.tokenType() == QXmlStreamReader::StartElement) { - if (reader.name() == "patterns") { - while (!((reader.readNext() == QXmlStreamReader::EndElement) && - (reader.name() == "patterns"))) { - if (reader.tokenType() == QXmlStreamReader::StartElement) { - if (reader.name() == "pattern") { - m_ConditionalInstalls.push_back(readConditionalPattern(reader)); - } - } - } - } - } - } -} - - -void FomodInstallerDialog::parseModuleConfig(const QByteArray &data) -{ - QXmlStreamReader reader(data); - while (!reader.atEnd() && !reader.hasError()) { - switch (reader.readNext()) { - case QXmlStreamReader::StartElement: { - if (reader.name() == "installSteps") { - readInstallerSteps(reader); - } else if (reader.name() == "requiredInstallFiles") { - readFileList(reader, m_RequiredFiles); - } else if (reader.name() == "conditionalFileInstalls") { - readConditionalFileInstalls(reader); - } - } break; - default: {} break; - } - } - if (reader.hasError()) { - reportError(tr("failed to parse ModuleConfig.xml: %1 - %2").arg(reader.errorString()).arg(reader.lineNumber())); - } - activateCurrentPage(); -} - - -void FomodInstallerDialog::on_manualBtn_clicked() -{ - m_Manual = true; - this->reject(); -} - -void FomodInstallerDialog::on_cancelBtn_clicked() -{ - this->reject(); -} - - -void FomodInstallerDialog::on_websiteLabel_linkActivated(const QString &link) -{ - ::ShellExecuteW(NULL, L"open", ToWString(link).c_str(), NULL, NULL, SW_SHOWNORMAL); -} - - -void FomodInstallerDialog::activateCurrentPage() -{ - QList choices = ui->stepsStack->currentWidget()->findChildren("choice"); - if (choices.count() > 0) { - highlightControl(choices.at(0)); - } -} - - -bool FomodInstallerDialog::testCondition(int maxIndex, const QString &flag, const QString &value) -{ - // iterate through all set condition flags on all activated controls on all visible pages if one of them matches the condition - for (int i = 0; i < maxIndex; ++i) { - if (testVisible(i)) { - QWidget *page = ui->stepsStack->widget(i); - QList choices = page->findChildren("choice"); - foreach (QAbstractButton* choice, choices) { - if (choice->isChecked()) { - QVariant temp = choice->property("conditionFlags"); - if (temp.isValid()) { - QVariantList conditionFlags = temp.toList(); - for (QVariantList::const_iterator iter = conditionFlags.begin(); iter != conditionFlags.end(); ++iter) { - Condition condition = iter->value(); - if ((condition.m_Name == flag) && (condition.m_Value == value)) { - return true; - } - } - } - } - } - } - } - return false; -} - - -bool FomodInstallerDialog::testVisible(int pageIndex) -{ - QWidget *page = ui->stepsStack->widget(pageIndex); - QVariant temp = page->property("conditions"); - if (temp.isValid()) { - QVariantList conditions = temp.toList(); - for (QVariantList::const_iterator iter = conditions.begin(); iter != conditions.end(); ++iter) { - Condition condition = iter->value(); - if (!testCondition(pageIndex, condition.m_Name, condition.m_Value)) { - return false; - } - } - return true; - } else { - return true; - } -} - - -bool FomodInstallerDialog::nextPage() -{ - int index = ui->stepsStack->currentIndex() + 1; - while (index < ui->stepsStack->count()) { - if (testVisible(index)) { - ui->stepsStack->setCurrentIndex(index); - return true; - } - ++index; - } - // no more visible pages -> install - return false; -} - - -void FomodInstallerDialog::on_nextBtn_clicked() -{ - if (ui->stepsStack->currentIndex() == ui->stepsStack->count() - 1) { - this->accept(); - } else { - if (nextPage()) { - if (ui->stepsStack->currentIndex() == ui->stepsStack->count() - 1) { - ui->nextBtn->setText(tr("Install")); - } - ui->prevBtn->setEnabled(true); - activateCurrentPage(); - } else { - this->accept(); - } - } -} - -void FomodInstallerDialog::on_prevBtn_clicked() -{ - if (ui->stepsStack->currentIndex() != 0) { - ui->stepsStack->setCurrentIndex(ui->stepsStack->currentIndex() - 1); - ui->nextBtn->setText(tr("Next")); - } - if (ui->stepsStack->currentIndex() == 0) { - ui->prevBtn->setEnabled(false); - } - activateCurrentPage(); -} +#include "fomodinstallerdialog.h" +#include "report.h" +#include "utility.h" +#include "ui_fomodinstallerdialog.h" + +#include +#include +#include +#include +#include +#include +#include + + +bool ControlsAscending(QAbstractButton *LHS, QAbstractButton *RHS) +{ + return LHS->text() < RHS->text(); +} + + +bool ControlsDescending(QAbstractButton *LHS, QAbstractButton *RHS) +{ + return LHS->text() > RHS->text(); +} + + +bool PagesAscending(QGroupBox *LHS, QGroupBox *RHS) +{ + return LHS->title() < RHS->title(); +} + + +bool PagesDescending(QGroupBox *LHS, QGroupBox *RHS) +{ + return LHS->title() > RHS->title(); +} + + +FomodInstallerDialog::FomodInstallerDialog(const QString &modName, bool nameWasGuessed, const QString &fomodPath, QWidget *parent) + : QDialog(parent), ui(new Ui::FomodInstallerDialog), m_NameWasGuessed(nameWasGuessed), m_FomodPath(fomodPath), m_Manual(false) +{ + ui->setupUi(this); + ui->nameEdit->setText(modName); +} + +FomodInstallerDialog::~FomodInstallerDialog() +{ + delete ui; +} + + +int FomodInstallerDialog::bomOffset(const QByteArray &buffer) +{ + static const unsigned char BOM_UTF8[] = { 0xEF, 0xBB, 0xBF }; + static const unsigned char BOM_UTF16BE[] = { 0xFE, 0xFF }; + static const unsigned char BOM_UTF16LE[] = { 0xFF, 0xFE }; + + if (buffer.startsWith(reinterpret_cast(BOM_UTF8))) return 3; + if (buffer.startsWith(reinterpret_cast(BOM_UTF16BE)) || + buffer.startsWith(reinterpret_cast(BOM_UTF16LE))) return 2; + + return 0; +} + +#pragma message("implement module dependencies->file dependencies") + +void FomodInstallerDialog::initData() +{ + { // parse provided package information + QFile file(QDir::tempPath().append("/info.xml")); + if (file.open(QIODevice::ReadOnly)) { + // nmm allows files with wrong encoding and of course there are now files with broken + // so, let's do as nmm does and ignore the standard. yay + QByteArray header = file.readLine(); + if (strncmp(header.constData() + bomOffset(header), "screenshotLabel->width()); + ui->screenshotLabel->setPixmap(QPixmap::fromImage(screenshot)); + } + + { // parse xml installer file + QFile file(QDir::tempPath().append("/ModuleConfig.xml")); + if (!file.open(QIODevice::ReadOnly)) { + throw MyException(tr("ModuleConfig.xml missing")); + } + // nmm allows files with wrong encoding and of course there are now files that are broken + QByteArray header = file.readLine(); + + if (strncmp(header.constData() + bomOffset(header), "nameEdit->text(); +} + + +void FomodInstallerDialog::moveTree(DirectoryTree::Node *target, DirectoryTree::Node *source) +{ + for (DirectoryTree::const_node_iterator iter = source->nodesBegin(); iter != source->nodesEnd();) { + target->addNode(*iter, true); + iter = source->detach(iter); + } + + for (DirectoryTree::const_leaf_reverse_iterator iter = source->leafsRBegin(); + iter != source->leafsREnd(); ++iter) { + target->addLeaf(*iter); + } +} + + +DirectoryTree::Node *FomodInstallerDialog::findNode(DirectoryTree::Node *node, const QString &path, bool create) +{ + if (path.length() == 0) { + return node; + } + +// static QRegExp pathSeparator("[/\\]"); + int pos = path.indexOf('\\'); + if (pos == -1) { + pos = path.indexOf('/'); + } + QString subPath = path; + if (pos > 0) { + subPath = path.mid(0, pos); + } + for (DirectoryTree::const_node_iterator iter = node->nodesBegin(); iter != node->nodesEnd(); ++iter) { + if ((*iter)->getData().name.compare(subPath, Qt::CaseInsensitive) == 0) { + if (pos <= 0) { + return *iter; + } else { + return findNode(*iter, path.mid(pos + 1), create); + } + } + } + if (create) { + DirectoryTree::Node *newNode = new DirectoryTree::Node; + newNode->setData(subPath); + node->addNode(newNode, false); + if (pos <= 0) { + return newNode; + } else { + return findNode(newNode, path.mid(pos + 1), create); + } + } else { + throw MyException(QString("%1 not found in archive").arg(path)); + } +} + +void FomodInstallerDialog::copyLeaf(DirectoryTree::Node *sourceTree, const QString &sourcePath, + DirectoryTree::Node *destinationTree, const QString &destinationPath) +{ + int sourceFileIndex = sourcePath.lastIndexOf('\\'); + if (sourceFileIndex == -1) { + sourceFileIndex = sourcePath.lastIndexOf('/'); + if (sourceFileIndex == -1) { + sourceFileIndex = 0; + } + } + DirectoryTree::Node *sourceNode = sourceFileIndex == 0 ? sourceTree : findNode(sourceTree, sourcePath.mid(0, sourceFileIndex), false); + + int destinationFileIndex = destinationPath.lastIndexOf('\\'); + if (destinationFileIndex == -1) { + destinationFileIndex = destinationPath.lastIndexOf('/'); + if (destinationFileIndex == -1) { + destinationFileIndex = 0; + } + } + + DirectoryTree::Node *destinationNode = + destinationFileIndex == 0 ? destinationTree + : findNode(destinationTree, destinationPath.mid(0, destinationFileIndex), true); + + QString sourceName = sourcePath.mid((sourceFileIndex != 0) ? sourceFileIndex + 1 : 0); + QString destinationName = (destinationFileIndex != 0) ? destinationPath.mid(destinationFileIndex + 1) : destinationPath; + if (destinationName.length() == 0) { + destinationName = sourceName; + } + + bool found = false; + for (DirectoryTree::const_leaf_reverse_iterator iter = sourceNode->leafsRBegin(); + iter != sourceNode->leafsREnd(); ++iter) { + if (iter->getName().compare(sourceName, Qt::CaseInsensitive) == 0) { + destinationNode->addLeaf(*iter); + found = true; + } + } + if (!found) { + qCritical("%s not found!", sourceName.toUtf8().constData()); + } +} + + +void dumpTree(DirectoryTree::Node *node, int indent) +{ + for (DirectoryTree::const_leaf_reverse_iterator iter = node->leafsRBegin(); + iter != node->leafsREnd(); ++iter) { + qDebug("%.*s%s", indent, " ", iter->getName().toUtf8().constData()); + } + + for (DirectoryTree::const_node_iterator iter = node->nodesBegin(); iter != node->nodesEnd(); ++iter) { + qDebug("%.*s-- %s", indent, " ", (*iter)->getData().name.toUtf8().constData()); + dumpTree(*iter, indent + 2); + } +} + + +bool FomodInstallerDialog::copyFileIterator(DirectoryTree *sourceTree, DirectoryTree *destinationTree, FileDescriptor *descriptor) +{ + QString source = (m_FomodPath.length() != 0) ? m_FomodPath.mid(0).append("\\").append(descriptor->m_Source) + : descriptor->m_Source; + QString destination = descriptor->m_Destination; + try { + if (descriptor->m_IsFolder) { + DirectoryTree::Node *sourceNode = findNode(sourceTree, source, false); + DirectoryTree::Node *targetNode = findNode(destinationTree, destination, true); + moveTree(targetNode, sourceNode); + } else { + copyLeaf(sourceTree, source, destinationTree, destination); + } + return true; + } catch (const MyException &e) { + qCritical("failed to extract %s to %s: %s", + source.toUtf8().constData(), destination.toUtf8().constData(), e.what()); + return false; + } +} + + +DirectoryTree *FomodInstallerDialog::updateTree(DirectoryTree *tree) +{ + DirectoryTree *newTree = new DirectoryTree; + + for (std::vector::iterator iter = m_RequiredFiles.begin(); iter != m_RequiredFiles.end(); ++iter) { + copyFileIterator(tree, newTree, *iter); + } + + for (std::vector::iterator installIter = m_ConditionalInstalls.begin(); + installIter != m_ConditionalInstalls.end(); ++installIter) { + bool match = installIter->m_Operator == ConditionalInstall::OP_AND; + for (std::vector::iterator conditionIter = installIter->m_Conditions.begin(); + conditionIter != installIter->m_Conditions.end(); ++conditionIter) { + bool conditionMatches = testCondition(ui->stepsStack->count(), conditionIter->m_Name, conditionIter->m_Value); + if (conditionMatches && (installIter->m_Operator == ConditionalInstall::OP_OR)) { + match = true; + break; + } else if (!conditionMatches && (installIter->m_Operator == ConditionalInstall::OP_AND)) { + match = false; + break; + } + } + if (match) { + for (std::vector::iterator fileIter = installIter->m_Files.begin(); + fileIter != installIter->m_Files.end(); ++fileIter) { + copyFileIterator(tree, newTree, *fileIter); + } + } + } + + QList choices = ui->stepsStack->findChildren("choice"); + foreach (QAbstractButton* choice, choices) { + if (choice->isChecked()) { + QVariantList fileList = choice->property("files").toList(); + foreach (QVariant fileVariant, fileList) { + copyFileIterator(tree, newTree, fileVariant.value()); + } + } + } + +// dumpTree(newTree, 0); + + return newTree; +} + + +void FomodInstallerDialog::highlightControl(QAbstractButton *button) +{ + QVariant screenshotName = button->property("screenshot"); + if (screenshotName.isValid()) { + QString screenshotFileName = screenshotName.toString(); + if (!screenshotFileName.isEmpty()) { + QString temp = QFileInfo(screenshotFileName).fileName(); + QImage screenshot(QDir::tempPath().append("/").append(temp)); + if (screenshot.isNull()) { + qWarning(">%s< is a null image", screenshotName.toString().toUtf8().constData()); + } + screenshot = screenshot.scaledToWidth(ui->screenshotLabel->width()); + ui->screenshotLabel->setPixmap(QPixmap::fromImage(screenshot)); + } else { + ui->screenshotLabel->setPixmap(QPixmap()); + } + } + ui->descriptionText->setText(button->property("description").toString()); +} + + +bool FomodInstallerDialog::eventFilter(QObject *object, QEvent *event) +{ + QAbstractButton *button = qobject_cast(object); + if ((button != NULL) && (event->type() == QEvent::HoverEnter)) { + highlightControl(button); + + } + return QDialog::eventFilter(object, event); +} + + +QString FomodInstallerDialog::readContent(QXmlStreamReader &reader) +{ + if (reader.readNext() == QXmlStreamReader::Characters) { + return reader.text().toString(); + } else { + return QString(); + } +} + + +void FomodInstallerDialog::parseInfo(const QByteArray &data) +{ + QXmlStreamReader reader(data); +/* while (reader.readNext() != QXmlStreamReader::StartDocument) {} + QTextDecoder *decoder = QTextCodec::codecForName(reader.documentEncoding().toLocal8Bit())->makeDecoder(QTextCodec::ConvertInvalidToNull); + QString test(decoder->toUnicode(data)); + qDebug("test: %d, %s", test.isNull(), test.toUtf8().constData()); + + qDebug(">%s<", reader.documentEncoding().toUtf8().constData());*/ + while (!reader.atEnd() && !reader.hasError()) { + switch (reader.readNext()) { + case QXmlStreamReader::StartElement: { + if (reader.name() == "Name") { + if (ui->nameEdit->text().isEmpty() || m_NameWasGuessed) { + ui->nameEdit->setText(readContent(reader)); + } + } else if (reader.name() == "Author") { + ui->authorLabel->setText(readContent(reader)); + } else if (reader.name() == "Version") { + ui->versionLabel->setText(readContent(reader)); + } else if (reader.name() == "Website") { + QString url = readContent(reader); + ui->websiteLabel->setText(tr("Link").arg(url)); + ui->websiteLabel->setToolTip(url); + } + } break; + default: {} break; + } + } + if (reader.hasError()) { + throw MyException(tr("failed to parse info.xml: %1 (%2) (line %3, column %4)") + .arg(reader.errorString()) + .arg(reader.error()) + .arg(reader.lineNumber()) + .arg(reader.columnNumber())); + + } +} + + +FomodInstallerDialog::ItemOrder FomodInstallerDialog::getItemOrder(const QString &orderString) +{ + if (orderString == "Ascending") { + return ORDER_ASCENDING; + } else if (orderString == "Descending") { + return ORDER_DESCENDING; + } else if (orderString == "Explicit") { + return ORDER_EXPLICIT; + } else { + throw MyException(tr("unsupported order type %1").arg(orderString)); + } +} + + +FomodInstallerDialog::GroupType FomodInstallerDialog::getGroupType(const QString &typeString) +{ + if (typeString == "SelectAtLeastOne") { + return TYPE_SELECTATLEASTONE; + } else if (typeString == "SelectAtMostOne") { + return TYPE_SELECTATMOSTONE; + } else if (typeString == "SelectExactlyOne") { + return TYPE_SELECTEXACTLYONE; + } else if (typeString == "SelectAny") { + return TYPE_SELECTANY; + } else if (typeString == "SelectAll") { + return TYPE_SELECTALL; + } else { + throw MyException(tr("unsupported group type %1").arg(typeString)); + } +} + + +FomodInstallerDialog::PluginType FomodInstallerDialog::getPluginType(const QString &typeString) +{ + if (typeString == "Required") { + return FomodInstallerDialog::TYPE_REQUIRED; + } else if (typeString == "Optional") { + return FomodInstallerDialog::TYPE_OPTIONAL; + } else if (typeString == "Recommended") { + return FomodInstallerDialog::TYPE_RECOMMENDED; + } else if (typeString == "NotUsable") { + return FomodInstallerDialog::TYPE_NOTUSABLE; + } else if (typeString == "CouldBeUsable") { + return FomodInstallerDialog::TYPE_COULDBEUSABLE; + } else { + qCritical("invalid plugin type %s", typeString.toUtf8().constData()); + return FomodInstallerDialog::TYPE_OPTIONAL; + } +} + + +void FomodInstallerDialog::readFileList(QXmlStreamReader &reader, std::vector &fileList) +{ + QStringRef openTag = reader.name(); + while (!((reader.readNext() == QXmlStreamReader::EndElement) && + (reader.name() == openTag))) { + if (reader.tokenType() == QXmlStreamReader::StartElement) { + if ((reader.name() == "folder") || + (reader.name() == "file")) { + QXmlStreamAttributes attributes = reader.attributes(); + FileDescriptor *file = new FileDescriptor(this); + file->m_Source = attributes.value("source").toString(); + file->m_Destination = attributes.hasAttribute("destination") ? attributes.value("destination").toString() + : file->m_Source; + file->m_Priority = attributes.hasAttribute("priority") ? attributes.value("priority").string()->toInt() + : 0; + file->m_IsFolder = reader.name() == "folder"; + file->m_InstallIfUsable = attributes.hasAttribute("installIfUsable") ? (attributes.value("installIfUsable").compare("true") == 0) + : false; + file->m_AlwaysInstall = attributes.hasAttribute("alwaysInstall") ? (attributes.value("alwaysInstall").compare("true") == 0) + : false; + + fileList.push_back(file); + } + } + } +} + + +void FomodInstallerDialog::readPluginType(QXmlStreamReader &reader, Plugin &plugin) +{ + plugin.m_Type = TYPE_OPTIONAL; + while (!((reader.readNext() == QXmlStreamReader::EndElement) && + (reader.name() == "typeDescriptor"))) { + if (reader.tokenType() == QXmlStreamReader::StartElement) { + if (reader.name() == "type") { + plugin.m_Type = getPluginType(reader.attributes().value("name").toString()); + } + } + } +} + + +void FomodInstallerDialog::readConditionFlags(QXmlStreamReader &reader, Plugin &plugin) +{ + while (!((reader.readNext() == QXmlStreamReader::EndElement) && + (reader.name() == "conditionFlags"))) { + if (reader.tokenType() == QXmlStreamReader::StartElement) { + if (reader.name() == "flag") { + QString name = reader.attributes().value("name").toString(); + plugin.m_Conditions.push_back(Condition(name, readContent(reader))); + } + } + } +} + + +bool FomodInstallerDialog::byPriority(const FileDescriptor *LHS, const FileDescriptor *RHS) +{ + return LHS->m_Priority < RHS->m_Priority; +} + + +FomodInstallerDialog::Plugin FomodInstallerDialog::readPlugin(QXmlStreamReader &reader) +{ + Plugin result; + result.m_Name = reader.attributes().value("name").toString(); + + while (!((reader.readNext() == QXmlStreamReader::EndElement) && + (reader.name() == "plugin"))) { +// QXmlStreamReader::TokenType type = reader.tokenType(); +// QString name = reader.name().toUtf8(); + if (reader.tokenType() == QXmlStreamReader::StartElement) { + if (reader.name() == "description") { + result.m_Description = readContent(reader).trimmed(); + } else if (reader.name() == "image") { + result.m_ImagePath = reader.attributes().value("path").toString(); + } else if (reader.name() == "typeDescriptor") { + readPluginType(reader, result); + } else if (reader.name() == "conditionFlags") { + readConditionFlags(reader, result); + } else if (reader.name() == "files") { + readFileList(reader, result.m_Files); + } + } + } + + std::sort(result.m_Files.begin(), result.m_Files.end(), byPriority); + + return result; +} + + +void FomodInstallerDialog::readPlugins(QXmlStreamReader &reader, GroupType groupType, QLayout *layout) +{ + ItemOrder pluginOrder = reader.attributes().hasAttribute("order") ? getItemOrder(reader.attributes().value("order").toString()) + : ORDER_ASCENDING; + bool first = true; + bool maySelectMore = true; + + std::vector controls; + + while (!((reader.readNext() == QXmlStreamReader::EndElement) && + (reader.name() == "plugins"))) { + if (reader.tokenType() == QXmlStreamReader::StartElement) { + if (reader.name() == "plugin") { + Plugin plugin = readPlugin(reader); + QAbstractButton *newControl = NULL; + switch (groupType) { + case TYPE_SELECTATLEASTONE: + case TYPE_SELECTANY: { + newControl = new QCheckBox(plugin.m_Name); + } break; + case TYPE_SELECTATMOSTONE: { + newControl = new QRadioButton(plugin.m_Name); + } break; + case TYPE_SELECTEXACTLYONE: { + newControl = new QRadioButton(plugin.m_Name); + if (first) { + newControl->setChecked(true); + } + } break; + case TYPE_SELECTALL: { + newControl = new QCheckBox(plugin.m_Name); + newControl->setChecked(true); + newControl->setEnabled(false); + } break; + } + newControl->setObjectName("choice"); + switch (plugin.m_Type) { + case TYPE_REQUIRED: { + newControl->setChecked(true); + newControl->setEnabled(false); + newControl->setToolTip(tr("This component is required")); + } break; + case TYPE_RECOMMENDED: { + if (maySelectMore) { + newControl->setChecked(true); + } + newControl->setToolTip(tr("It is recommended you enable this component")); + if ((groupType == TYPE_SELECTATMOSTONE) || (groupType == TYPE_SELECTEXACTLYONE)) { + maySelectMore = false; + } + } break; + case TYPE_OPTIONAL: { + newControl->setToolTip(tr("Optional component")); + } break; + case TYPE_NOTUSABLE: { + newControl->setChecked(false); + newControl->setEnabled(false); + newControl->setToolTip(tr("This component is not usable in combination with other installed plugins")); + } break; + case TYPE_COULDBEUSABLE: { + newControl->setCheckable(true); + newControl->setIcon(QIcon(":/new/guiresources/resources/dialog-warning_16.png")); + newControl->setToolTip(tr("You may be experiencing instability in combination with other installed plugins")); + } break; + } + + newControl->setProperty("plugintype", plugin.m_Type); + newControl->setProperty("screenshot", plugin.m_ImagePath); + newControl->setProperty("description", plugin.m_Description); + QVariantList fileList; + for (std::vector::iterator iter = plugin.m_Files.begin(); iter != plugin.m_Files.end(); ++iter) { + fileList.append(qVariantFromValue(*iter)); + } + newControl->setProperty("files", fileList); + QVariantList conditionFlags; + for (std::vector::const_iterator iter = plugin.m_Conditions.begin(); iter != plugin.m_Conditions.end(); ++iter) { + if (iter->m_Name.length() != 0) { + conditionFlags.append(qVariantFromValue(Condition(iter->m_Name, iter->m_Value))); + } + } + newControl->setProperty("conditionFlags", conditionFlags); + newControl->installEventFilter(this); + controls.push_back(newControl); + first = false; + } + } + } + + if (pluginOrder == ORDER_ASCENDING) { + std::sort(controls.begin(), controls.end(), ControlsAscending); + } else if (pluginOrder == ORDER_DESCENDING) { + std::sort(controls.begin(), controls.end(), ControlsDescending); + } + + for (std::vector::const_iterator iter = controls.begin(); iter != controls.end(); ++iter) { + layout->addWidget(*iter); + } + + if (groupType == TYPE_SELECTATMOSTONE) { + QRadioButton *newButton = new QRadioButton(tr("None")); + if (maySelectMore) { + newButton->setChecked(true); + } + layout->addWidget(newButton); + } +} + + +void FomodInstallerDialog::readGroup(QXmlStreamReader &reader, QLayout *layout) +{ + //FileGroup result; + QString name = reader.attributes().value("name").toString(); + GroupType type = getGroupType(reader.attributes().value("type").toString()); + + if (type == TYPE_SELECTATLEASTONE) { + QLabel *label = new QLabel(tr("Select one or more of these options:")); + layout->addWidget(label); + } + + QGroupBox *groupBox = new QGroupBox(name); + + QVBoxLayout *groupLayout = new QVBoxLayout; + + while (!((reader.readNext() == QXmlStreamReader::EndElement) && + (reader.name() == "group"))) { + if (reader.tokenType() == QXmlStreamReader::StartElement) { + if (reader.name() == "plugins") { + readPlugins(reader, type, groupLayout); + } + } + } + + groupBox->setLayout(groupLayout); + layout->addWidget(groupBox); +} + + +void FomodInstallerDialog::readGroups(QXmlStreamReader &reader, QLayout *layout) +{ + while (!((reader.readNext() == QXmlStreamReader::EndElement) && + (reader.name() == "optionalFileGroups"))) { + if (reader.tokenType() == QXmlStreamReader::StartElement) { + if (reader.name() == "group") { + readGroup(reader, layout); + } + } + } +} + + +void FomodInstallerDialog::readVisible(QXmlStreamReader &reader, QVariantList &conditions) +{ + while (!((reader.readNext() == QXmlStreamReader::EndElement) && + (reader.name() == "visible"))) { + if (reader.tokenType() == QXmlStreamReader::StartElement) { + if (reader.name() == "flagDependency") { + Condition condition(reader.attributes().value("flag").toString(), + reader.attributes().value("value").toString()); + conditions.append(qVariantFromValue(condition)); + } + } + } +} + +QGroupBox *FomodInstallerDialog::readInstallerStep(QXmlStreamReader &reader) +{ + QString name = reader.attributes().value("name").toString(); + QGroupBox *page = new QGroupBox(name); + QVBoxLayout *pageLayout = new QVBoxLayout; + QScrollArea *scrollArea = new QScrollArea; + QFrame *scrolledArea = new QFrame; + QVBoxLayout *scrollLayout = new QVBoxLayout; + + QVariantList conditions; + + while (!((reader.readNext() == QXmlStreamReader::EndElement) && + (reader.name() == "installStep"))) { + if (reader.tokenType() == QXmlStreamReader::StartElement) { + if (reader.name() == "optionalFileGroups") { + readGroups(reader, scrollLayout); + } else if (reader.name() == "visible") { + readVisible(reader, conditions); + } + } + } + if (conditions.length() != 0) { + page->setProperty("conditions", conditions); + } + + scrolledArea->setLayout(scrollLayout); + scrollArea->setWidget(scrolledArea); + scrollArea->setWidgetResizable(true); + pageLayout->addWidget(scrollArea); + page->setLayout(pageLayout); + return page; +} + + +void FomodInstallerDialog::readInstallerSteps(QXmlStreamReader &reader) +{ + ItemOrder stepOrder = reader.attributes().hasAttribute("order") ? getItemOrder(reader.attributes().value("order").toString()) + : ORDER_ASCENDING; + + std::vector pages; + + while (!((reader.readNext() == QXmlStreamReader::EndElement) && + (reader.name() == "installSteps"))) { + if (reader.tokenType() == QXmlStreamReader::StartElement) { + if (reader.name() == "installStep") { + pages.push_back(readInstallerStep(reader)); + } + } + } + + if (stepOrder == ORDER_ASCENDING) { + std::sort(pages.begin(), pages.end(), PagesAscending); + } else if (stepOrder == ORDER_DESCENDING) { + std::sort(pages.begin(), pages.end(), PagesDescending); + } + + for (std::vector::const_iterator iter = pages.begin(); iter != pages.end(); ++iter) { + ui->stepsStack->addWidget(*iter); + } +} + + +FomodInstallerDialog::ConditionalInstall FomodInstallerDialog::readConditionalPattern(QXmlStreamReader &reader) +{ + ConditionalInstall result; + result.m_Operator = ConditionalInstall::OP_AND; + while (!((reader.readNext() == QXmlStreamReader::EndElement) && + (reader.name() == "pattern"))) { + if (reader.tokenType() == QXmlStreamReader::StartElement) { + if (reader.name() == "dependencies") { + QStringRef dependencyOperator = reader.attributes().value("operator"); + if (dependencyOperator == "And") { + result.m_Operator = ConditionalInstall::OP_AND; + } else if (dependencyOperator == "Or") { + result.m_Operator = ConditionalInstall::OP_OR; + } // otherwise operator is not set (which we can ignore) or invalid (which we should report actually) + + while (!((reader.readNext() == QXmlStreamReader::EndElement) && + (reader.name() == "dependencies"))) { + if (reader.tokenType() == QXmlStreamReader::StartElement) { + if (reader.name() == "flagDependency") { + result.m_Conditions.push_back(Condition(reader.attributes().value("flag").toString(), + reader.attributes().value("value").toString())); + } + } + } + } else if (reader.name() == "files") { + readFileList(reader, result.m_Files); + } + } + } + return result; +} + + +void FomodInstallerDialog::readConditionalFileInstalls(QXmlStreamReader &reader) +{ + while (!((reader.readNext() == QXmlStreamReader::EndElement) && + (reader.name() == "conditionalFileInstalls"))) { + if (reader.tokenType() == QXmlStreamReader::StartElement) { + if (reader.name() == "patterns") { + while (!((reader.readNext() == QXmlStreamReader::EndElement) && + (reader.name() == "patterns"))) { + if (reader.tokenType() == QXmlStreamReader::StartElement) { + if (reader.name() == "pattern") { + m_ConditionalInstalls.push_back(readConditionalPattern(reader)); + } + } + } + } + } + } +} + + +void FomodInstallerDialog::parseModuleConfig(const QByteArray &data) +{ + QXmlStreamReader reader(data); + while (!reader.atEnd() && !reader.hasError()) { + switch (reader.readNext()) { + case QXmlStreamReader::StartElement: { + if (reader.name() == "installSteps") { + readInstallerSteps(reader); + } else if (reader.name() == "requiredInstallFiles") { + readFileList(reader, m_RequiredFiles); + } else if (reader.name() == "conditionalFileInstalls") { + readConditionalFileInstalls(reader); + } + } break; + default: {} break; + } + } + if (reader.hasError()) { + reportError(tr("failed to parse ModuleConfig.xml: %1 - %2").arg(reader.errorString()).arg(reader.lineNumber())); + } + activateCurrentPage(); +} + + +void FomodInstallerDialog::on_manualBtn_clicked() +{ + m_Manual = true; + this->reject(); +} + +void FomodInstallerDialog::on_cancelBtn_clicked() +{ + this->reject(); +} + + +void FomodInstallerDialog::on_websiteLabel_linkActivated(const QString &link) +{ + ::ShellExecuteW(NULL, L"open", ToWString(link).c_str(), NULL, NULL, SW_SHOWNORMAL); +} + + +void FomodInstallerDialog::activateCurrentPage() +{ + QList choices = ui->stepsStack->currentWidget()->findChildren("choice"); + if (choices.count() > 0) { + highlightControl(choices.at(0)); + } +} + + +bool FomodInstallerDialog::testCondition(int maxIndex, const QString &flag, const QString &value) +{ + // iterate through all set condition flags on all activated controls on all visible pages if one of them matches the condition + for (int i = 0; i < maxIndex; ++i) { + if (testVisible(i)) { + QWidget *page = ui->stepsStack->widget(i); + QList choices = page->findChildren("choice"); + foreach (QAbstractButton* choice, choices) { + if (choice->isChecked()) { + QVariant temp = choice->property("conditionFlags"); + if (temp.isValid()) { + QVariantList conditionFlags = temp.toList(); + for (QVariantList::const_iterator iter = conditionFlags.begin(); iter != conditionFlags.end(); ++iter) { + Condition condition = iter->value(); + if ((condition.m_Name == flag) && (condition.m_Value == value)) { + return true; + } + } + } + } + } + } + } + return false; +} + + +bool FomodInstallerDialog::testVisible(int pageIndex) +{ + QWidget *page = ui->stepsStack->widget(pageIndex); + QVariant temp = page->property("conditions"); + if (temp.isValid()) { + QVariantList conditions = temp.toList(); + for (QVariantList::const_iterator iter = conditions.begin(); iter != conditions.end(); ++iter) { + Condition condition = iter->value(); + if (!testCondition(pageIndex, condition.m_Name, condition.m_Value)) { + return false; + } + } + return true; + } else { + return true; + } +} + + +bool FomodInstallerDialog::nextPage() +{ + int index = ui->stepsStack->currentIndex() + 1; + while (index < ui->stepsStack->count()) { + if (testVisible(index)) { + ui->stepsStack->setCurrentIndex(index); + return true; + } + ++index; + } + // no more visible pages -> install + return false; +} + + +void FomodInstallerDialog::on_nextBtn_clicked() +{ + if (ui->stepsStack->currentIndex() == ui->stepsStack->count() - 1) { + this->accept(); + } else { + if (nextPage()) { + if (ui->stepsStack->currentIndex() == ui->stepsStack->count() - 1) { + ui->nextBtn->setText(tr("Install")); + } + ui->prevBtn->setEnabled(true); + activateCurrentPage(); + } else { + this->accept(); + } + } +} + +void FomodInstallerDialog::on_prevBtn_clicked() +{ + if (ui->stepsStack->currentIndex() != 0) { + ui->stepsStack->setCurrentIndex(ui->stepsStack->currentIndex() - 1); + ui->nextBtn->setText(tr("Next")); + } + if (ui->stepsStack->currentIndex() == 0) { + ui->prevBtn->setEnabled(false); + } + activateCurrentPage(); +} diff --git a/src/fomodinstallerdialog.h b/src/fomodinstallerdialog.h index 0fb5a5ec..875146a6 100644 --- a/src/fomodinstallerdialog.h +++ b/src/fomodinstallerdialog.h @@ -17,194 +17,198 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#ifndef FOMODINSTALLERDIALOG_H -#define FOMODINSTALLERDIALOG_H - -#include "installdialog.h" -#include -#include -#include -#include -#include - -namespace Ui { -class FomodInstallerDialog; -} - - -class Condition : public QObject { - Q_OBJECT -public: - Condition(QObject *parent = NULL) : QObject(parent) { } - Condition(const Condition &reference) : QObject(reference.parent()), m_Name(reference.m_Name), m_Value(reference.m_Value) { } - Condition(const QString &name, const QString &value) : QObject(), m_Name(name), m_Value(value) { } - QString m_Name; - QString m_Value; -private: - Condition &operator=(const Condition&); -}; - -Q_DECLARE_METATYPE(Condition) - - -class FileDescriptor : public QObject { - Q_OBJECT -public: - FileDescriptor(QObject *parent) - : QObject(parent), m_Source(), m_Destination(), m_Priority(0), m_IsFolder(false), m_AlwaysInstall(false), - m_InstallIfUsable(false) {} - FileDescriptor(const FileDescriptor &reference) - : QObject(reference.parent()), m_Source(reference.m_Source), m_Destination(reference.m_Destination), - m_Priority(reference.m_Priority), m_IsFolder(reference.m_IsFolder), m_AlwaysInstall(reference.m_AlwaysInstall), - m_InstallIfUsable(reference.m_InstallIfUsable) {} - QString m_Source; - QString m_Destination; - int m_Priority; - bool m_IsFolder; - bool m_AlwaysInstall; - bool m_InstallIfUsable; -private: - FileDescriptor &operator=(const FileDescriptor&); -}; - -Q_DECLARE_METATYPE(FileDescriptor*) - - -class FomodInstallerDialog : public QDialog -{ - Q_OBJECT - -public: - explicit FomodInstallerDialog(const QString &modName, const QString &fomodPath, QWidget *parent = 0); - ~FomodInstallerDialog(); - - void initData(); - - /** - * @return bool true if the user requested the manual dialog - **/ - bool manualRequested() const { return m_Manual; } - - /** - * @return QString the (user-modified) name to be used for the mod - **/ - QString getName() const; - - /** - * @brief retrieve the updated archive tree from the dialog. The caller is responsible to delete the returned tree. - * - * @note This call is destructive on the input tree! - * - * @param tree input tree. (TODO isn't this the same as the tree passed in the constructor?) - * @return DataTree* a new tree with only the selected options and directories arranged correctly. The caller takes custody of this pointer! - **/ - DirectoryTree *updateTree(DirectoryTree *tree); - -protected: - - virtual bool eventFilter(QObject *object, QEvent *event); - -private slots: - - void on_cancelBtn_clicked(); - - void on_manualBtn_clicked(); - - void on_websiteLabel_linkActivated(const QString &link); - - void on_nextBtn_clicked(); - - void on_prevBtn_clicked(); - -private: - - enum ItemOrder { - ORDER_ASCENDING, - ORDER_DESCENDING, - ORDER_EXPLICIT - }; - - enum GroupType { - TYPE_SELECTATLEASTONE, - TYPE_SELECTATMOSTONE, - TYPE_SELECTEXACTLYONE, - TYPE_SELECTANY, - TYPE_SELECTALL - }; - - enum PluginType { - TYPE_REQUIRED, - TYPE_RECOMMENDED, - TYPE_OPTIONAL, - TYPE_NOTUSABLE, - TYPE_COULDBEUSABLE - }; - - struct Plugin { - QString m_Name; - QString m_Description; - QString m_ImagePath; - PluginType m_Type; - std::vector m_Conditions; - std::vector m_Files; - }; - - struct ConditionalInstall { - enum { - OP_AND, - OP_OR - } m_Operator; - std::vector m_Conditions; - std::vector m_Files; - }; - -private: - - QString readContent(QXmlStreamReader &reader); - void parseInfo(const QByteArray &data); - - static ItemOrder getItemOrder(const QString &orderString); - static GroupType getGroupType(const QString &typeString); - static PluginType getPluginType(const QString &typeString); - static bool byPriority(const FileDescriptor *LHS, const FileDescriptor *RHS); - - bool copyFileIterator(DirectoryTree *sourceTree, DirectoryTree *destinationTree, FileDescriptor *descriptor); - void readFileList(QXmlStreamReader &reader, std::vector &fileList); - void readPluginType(QXmlStreamReader &reader, Plugin &plugin); - void readConditionFlags(QXmlStreamReader &reader, Plugin &plugin); - FomodInstallerDialog::Plugin readPlugin(QXmlStreamReader &reader); - void readPlugins(QXmlStreamReader &reader, GroupType groupType, QLayout *layout); - void readGroup(QXmlStreamReader &reader, QLayout *layout); - void readGroups(QXmlStreamReader &reader, QLayout *layout); - void readVisible(QXmlStreamReader &reader, QVariantList &conditions); - QGroupBox *readInstallerStep(QXmlStreamReader &reader); - ConditionalInstall readConditionalPattern(QXmlStreamReader &reader); - void readConditionalFileInstalls(QXmlStreamReader &reader); - void readInstallerSteps(QXmlStreamReader &reader); - void parseModuleConfig(const QByteArray &data); - void highlightControl(QAbstractButton *button); - - bool testCondition(int maxIndex, const QString &flag, const QString &value); - bool testVisible(int pageIndex); - bool nextPage(); - void activateCurrentPage(); - void moveTree(DirectoryTree::Node *target, DirectoryTree::Node *source); - DirectoryTree::Node *findNode(DirectoryTree::Node *node, const QString &path, bool create); - void copyLeaf(DirectoryTree::Node *sourceTree, const QString &sourcePath, - DirectoryTree::Node *destinationTree, const QString &destinationPath); - -private: - - Ui::FomodInstallerDialog *ui; - - QString m_FomodPath; - bool m_Manual; - -// ItemOrder m_StepOrder; -// std::vector m_Steps; - std::vector m_RequiredFiles; - std::vector m_ConditionalInstalls; - -}; - -#endif // FOMODINSTALLERDIALOG_H +#ifndef FOMODINSTALLERDIALOG_H +#define FOMODINSTALLERDIALOG_H + +#include "installdialog.h" +#include +#include +#include +#include +#include + +namespace Ui { +class FomodInstallerDialog; +} + + +class Condition : public QObject { + Q_OBJECT +public: + Condition(QObject *parent = NULL) : QObject(parent) { } + Condition(const Condition &reference) : QObject(reference.parent()), m_Name(reference.m_Name), m_Value(reference.m_Value) { } + Condition(const QString &name, const QString &value) : QObject(), m_Name(name), m_Value(value) { } + QString m_Name; + QString m_Value; +private: + Condition &operator=(const Condition&); +}; + +Q_DECLARE_METATYPE(Condition) + + +class FileDescriptor : public QObject { + Q_OBJECT +public: + FileDescriptor(QObject *parent) + : QObject(parent), m_Source(), m_Destination(), m_Priority(0), m_IsFolder(false), m_AlwaysInstall(false), + m_InstallIfUsable(false) {} + FileDescriptor(const FileDescriptor &reference) + : QObject(reference.parent()), m_Source(reference.m_Source), m_Destination(reference.m_Destination), + m_Priority(reference.m_Priority), m_IsFolder(reference.m_IsFolder), m_AlwaysInstall(reference.m_AlwaysInstall), + m_InstallIfUsable(reference.m_InstallIfUsable) {} + QString m_Source; + QString m_Destination; + int m_Priority; + bool m_IsFolder; + bool m_AlwaysInstall; + bool m_InstallIfUsable; +private: + FileDescriptor &operator=(const FileDescriptor&); +}; + +Q_DECLARE_METATYPE(FileDescriptor*) + + +class FomodInstallerDialog : public QDialog +{ + Q_OBJECT + +public: + explicit FomodInstallerDialog(const QString &modName, bool nameWasGuessed, const QString &fomodPath, QWidget *parent = 0); + ~FomodInstallerDialog(); + + void initData(); + + /** + * @return bool true if the user requested the manual dialog + **/ + bool manualRequested() const { return m_Manual; } + + /** + * @return QString the (user-modified) name to be used for the mod + **/ + QString getName() const; + + /** + * @brief retrieve the updated archive tree from the dialog. The caller is responsible to delete the returned tree. + * + * @note This call is destructive on the input tree! + * + * @param tree input tree. (TODO isn't this the same as the tree passed in the constructor?) + * @return DataTree* a new tree with only the selected options and directories arranged correctly. The caller takes custody of this pointer! + **/ + DirectoryTree *updateTree(DirectoryTree *tree); + +protected: + + virtual bool eventFilter(QObject *object, QEvent *event); + +private slots: + + void on_cancelBtn_clicked(); + + void on_manualBtn_clicked(); + + void on_websiteLabel_linkActivated(const QString &link); + + void on_nextBtn_clicked(); + + void on_prevBtn_clicked(); + +private: + + enum ItemOrder { + ORDER_ASCENDING, + ORDER_DESCENDING, + ORDER_EXPLICIT + }; + + enum GroupType { + TYPE_SELECTATLEASTONE, + TYPE_SELECTATMOSTONE, + TYPE_SELECTEXACTLYONE, + TYPE_SELECTANY, + TYPE_SELECTALL + }; + + enum PluginType { + TYPE_REQUIRED, + TYPE_RECOMMENDED, + TYPE_OPTIONAL, + TYPE_NOTUSABLE, + TYPE_COULDBEUSABLE + }; + + struct Plugin { + QString m_Name; + QString m_Description; + QString m_ImagePath; + PluginType m_Type; + std::vector m_Conditions; + std::vector m_Files; + }; + + struct ConditionalInstall { + enum { + OP_AND, + OP_OR + } m_Operator; + std::vector m_Conditions; + std::vector m_Files; + }; + +private: + + static int bomOffset(const QByteArray &buffer); + + QString readContent(QXmlStreamReader &reader); + void parseInfo(const QByteArray &data); + + static ItemOrder getItemOrder(const QString &orderString); + static GroupType getGroupType(const QString &typeString); + static PluginType getPluginType(const QString &typeString); + static bool byPriority(const FileDescriptor *LHS, const FileDescriptor *RHS); + + bool copyFileIterator(DirectoryTree *sourceTree, DirectoryTree *destinationTree, FileDescriptor *descriptor); + void readFileList(QXmlStreamReader &reader, std::vector &fileList); + void readPluginType(QXmlStreamReader &reader, Plugin &plugin); + void readConditionFlags(QXmlStreamReader &reader, Plugin &plugin); + FomodInstallerDialog::Plugin readPlugin(QXmlStreamReader &reader); + void readPlugins(QXmlStreamReader &reader, GroupType groupType, QLayout *layout); + void readGroup(QXmlStreamReader &reader, QLayout *layout); + void readGroups(QXmlStreamReader &reader, QLayout *layout); + void readVisible(QXmlStreamReader &reader, QVariantList &conditions); + QGroupBox *readInstallerStep(QXmlStreamReader &reader); + ConditionalInstall readConditionalPattern(QXmlStreamReader &reader); + void readConditionalFileInstalls(QXmlStreamReader &reader); + void readInstallerSteps(QXmlStreamReader &reader); + void parseModuleConfig(const QByteArray &data); + void highlightControl(QAbstractButton *button); + + bool testCondition(int maxIndex, const QString &flag, const QString &value); + bool testVisible(int pageIndex); + bool nextPage(); + void activateCurrentPage(); + void moveTree(DirectoryTree::Node *target, DirectoryTree::Node *source); + DirectoryTree::Node *findNode(DirectoryTree::Node *node, const QString &path, bool create); + void copyLeaf(DirectoryTree::Node *sourceTree, const QString &sourcePath, + DirectoryTree::Node *destinationTree, const QString &destinationPath); + +private: + + Ui::FomodInstallerDialog *ui; + + bool m_NameWasGuessed; + + QString m_FomodPath; + bool m_Manual; + +// ItemOrder m_StepOrder; +// std::vector m_Steps; + std::vector m_RequiredFiles; + std::vector m_ConditionalInstalls; + +}; + +#endif // FOMODINSTALLERDIALOG_H diff --git a/src/installationmanager.cpp b/src/installationmanager.cpp index e7784771..a6f062de 100644 --- a/src/installationmanager.cpp +++ b/src/installationmanager.cpp @@ -499,9 +499,9 @@ bool InstallationManager::testOverwrite(const QString &modsDirectory, QString &m QLineEdit::Normal, modName, &ok); if (ok && !name.isEmpty()) { modName = name; - if (!ensureValidModName(modName)) { - return false; - } + if (!ensureValidModName(modName)) { + return false; + } targetDirectory = QDir::fromNativeSeparators(modsDirectory.mid(0).append("\\").append(modName)); } } else if (overwriteDialog.action() == QueryOverwriteDialog::ACT_REPLACE) { @@ -547,46 +547,46 @@ void InstallationManager::fixModName(QString &name) { // name = name.remove("^[ ]*").trimmed(); name = name.simplified(); - while (name.endsWith('.')) name.chop(1); - - name.replace(QRegExp("[<>:\"/\\|?*]"), ""); - - static QString invalidNames[] = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", - "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" }; - for (int i = 0; i < sizeof(invalidNames) / sizeof(QString); ++i) { - if (name == invalidNames[i]) { - name = ""; - break; - } - } + while (name.endsWith('.')) name.chop(1); + + name.replace(QRegExp("[<>:\"/\\|?*]"), ""); + + static QString invalidNames[] = { "CON", "PRN", "AUX", "NUL", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9", + "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9" }; + for (int i = 0; i < sizeof(invalidNames) / sizeof(QString); ++i) { + if (name == invalidNames[i]) { + name = ""; + break; + } + } +} + + +bool InstallationManager::ensureValidModName(QString &name) +{ + fixModName(name); + + while (name.isEmpty()) { + bool ok; + name = QInputDialog::getText(m_ParentWidget, tr("Invalid name"), + tr("The name you entered is invalid, please enter a different one."), + QLineEdit::Normal, "", &ok); + if (!ok) { + return false; + } + fixModName(name); + } + return true; } -bool InstallationManager::ensureValidModName(QString &name) -{ - fixModName(name); - - while (name.isEmpty()) { - bool ok; - name = QInputDialog::getText(m_ParentWidget, tr("Invalid name"), - tr("The name you entered is invalid, please enter a different one."), - QLineEdit::Normal, "", &ok); - if (!ok) { - return false; - } - fixModName(name); - } - return true; -} - - -bool InstallationManager::doInstall(const QString &modsDirectory, QString &modName, int modID, +bool InstallationManager::doInstall(const QString &modsDirectory, QString &modName, int modID, const QString &version, const QString &newestVersion, int categoryID) { - if (!ensureValidModName(modName)) { - return false; - } - + if (!ensureValidModName(modName)) { + return false; + } + // determine target directory if (!testOverwrite(modsDirectory, modName)) { return false; @@ -679,7 +679,7 @@ bool EndsWith(LPCWSTR string, LPCWSTR subString) bool InstallationManager::installFomodInternal(DirectoryTree *&baseNode, const QString &fomodPath, const QString &modsDirectory, int modID, const QString &version, const QString &newestVersion, int categoryID, - QString &modName, bool &manualRequest) + QString &modName, bool nameGuessed, bool &manualRequest) { qDebug("treating as fomod archive"); @@ -740,7 +740,7 @@ bool InstallationManager::installFomodInternal(DirectoryTree *&baseNode, const Q bool success = false; try { - FomodInstallerDialog dialog(modName, fomodPath); + FomodInstallerDialog dialog(modName, nameGuessed, fomodPath); FileData* const *data; size_t size; @@ -924,6 +924,7 @@ bool InstallationManager::install(const QString &fileName, const QString &plugin QString version = ""; QString newestVersion = ""; int categoryID = 0; + bool nameGuessed = false; QString metaName = fileName.mid(0).append(".meta"); if (QFile(metaName).exists()) { @@ -945,9 +946,9 @@ bool InstallationManager::install(const QString &fileName, const QString &plugin { // guess the mod name and mod if from the file name if there was no meta information QString guessedModName; - int guessedModID = modID; - NexusInterface::interpretNexusFileName(QFileInfo(fileName).baseName(), guessedModName, guessedModID, false); - if ((modID == 0) && (guessedModID != -1)) { + int guessedModID = modID; + NexusInterface::interpretNexusFileName(QFileInfo(fileName).baseName(), guessedModName, guessedModID, false); + if ((modID == 0) && (guessedModID != -1)) { modID = guessedModID; } else if (modID != guessedModID) { qDebug("passed mod id: %d, guessed id: %d", modID, guessedModID); @@ -955,6 +956,7 @@ bool InstallationManager::install(const QString &fileName, const QString &plugin if (modName.isEmpty()) { modName = guessedModName; + nameGuessed = true; } } fixModName(modName); @@ -1090,8 +1092,8 @@ bool InstallationManager::install(const QString &fileName, const QString &plugin if (xmlInstaller || nmmInstaller) { if (!xmlInstaller || (nmmInstaller && !preferIntegrated)) { - if (!ensureValidModName(modName) || - !testOverwrite(modsDirectory, modName)) { + if (!ensureValidModName(modName) || + !testOverwrite(modsDirectory, modName)) { return false; } @@ -1120,7 +1122,7 @@ bool InstallationManager::install(const QString &fileName, const QString &plugin } else { if (installFomodInternal(baseNode, fomodPath, modsDirectory, modID, version, newestVersion, categoryID, - modName, manualRequest)) { + modName, nameGuessed, manualRequest)) { success = true; } } diff --git a/src/installationmanager.h b/src/installationmanager.h index 9ac97812..054f584a 100644 --- a/src/installationmanager.h +++ b/src/installationmanager.h @@ -17,194 +17,194 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#ifndef INSTALLATIONMANAGER_H -#define INSTALLATIONMANAGER_H - -#include "installdialog.h" - -#include -#include - -#include -#define WIN32_LEAN_AND_MEAN -#include -#include -#include -#include -#include - - -/** - * @brief manages the installation of mod archives - * This currently supports two special kind of archives: - * - "simple" archives: properly packaged archives without options, so they can be extracted to the (virtual) data directory directly - * - "complex" bain archives: archives with options for the bain system. - * All other archives are managed through the manual "InstallDialog" - * @todo this may be a good place to support plugins - **/ -class InstallationManager : public QObject, public IInstallationManager -{ - Q_OBJECT - -public: - - /** - * @brief constructor - * - * @param parent parent object. - **/ - explicit InstallationManager(QWidget *parent); - - ~InstallationManager(); - - /** - * @brief install a mod from an archive - * - * @param fileName absolute file name of the archive to install - * @param modName suggested name of the mod. If this is empty (the default), a name will be guessed based on the filename. The user will always have a chance to rename the mod - * @param preferIntegrated if true, integrated installers are chosen over external installers - * @return true if the archive was installed, false if installation failed or was refused - * @exception std::exception an exception may be thrown if the archive can't be opened (maybe the format is invalid or the file is damaged) - **/ - bool install(const QString &fileName, const QString &pluginsFileName, const QString &modsDirectory, bool preferIntegrated, bool enableQuickInstall, QString &modName, bool &hasIniTweaks); - - /** - * @return true if the installation was canceled - **/ - bool wasCancelled(); - - /** - * @brief retrieve a string describing the specified error code - * - * @param errorCode an error code as returned by the archiving function - * @return the error string - * @todo This function doesn't belong here, it is only public because the SelfUpdater class also uses "Archive" to get to the package.txt file - **/ - static QString getErrorString(Archive::Error errorCode); - - void installManual(DirectoryTree::Node *baseNode, DirectoryTree *filesTree, bool &hasIniTweaks, QString &modName, int categoryID, const QString &modsDirectory, QString newestVersion, QString version, int modID, bool success, bool manualRequest); - - /** - * @brief register an installer-plugin - * @param the installer to register - */ - void registerInstaller(IPluginInstaller *installer); - - /** - * @return list of file extensions we can install - */ - QStringList getSupportedExtensions() const; - - /** - * @brief extract the specified file from the currently open archive to a temporary location - * @param (relative) name of the file within the archive - * @return the absolute name of the temporary file - * @note the call will fail with an exception if no archive is open (plugins deriving - * from IPluginInstallerSimple can rely on that, custom installers shouldn't) - * @note the temporary file is automatically cleaned up after the installation - * @note This call can be very slow if the archive is large and "solid" - */ - virtual QString extractFile(const QString &fileName); - - /** - * @brief extract the specified files from the currently open archive to a temporary location - * @param (relative) names of files within the archive - * @return the absolute names of the temporary files - * @note the call will fail with an exception if no archive is open (plugins deriving - * from IPluginInstallerSimple can rely on that, custom installers shouldn't) - * @note the temporary file is automatically cleaned up after the installation - * @note This call can be very slow if the archive is large and "solid" - */ - virtual QStringList extractFiles(const QStringList &files); - - /** - * @brief installs an archive - * @param modName suggested name of the mod - * @param archiveFile path to the archive to install - * @return the installation result - */ - virtual IPluginInstaller::EInstallResult installArchive(const QString &modName, const QString &archiveName); - -private: - - void queryPassword(LPSTR password); - void updateProgress(float percentage); - void updateProgressFile(LPCWSTR fileName); - void report7ZipError(LPCWSTR errorMessage); - - void dummyProgressFile(LPCWSTR) {} - - DirectoryTree *createFilesTree(); - - // remap all files in the archive to the directory structure represented by baseNode - // files not present in baseNode are disabled - void mapToArchive(const DirectoryTree::Node *baseNode); - - // recursive worker function for mapToArchive - void mapToArchive(const DirectoryTree::Node *node, std::wstring path, FileData * const *data); - bool unpackPackageTXT(); - bool unpackSingleFile(const QString &fileName); - - - bool isSimpleArchiveTopLayer(const DirectoryTree::Node *node, bool bainStyle); - DirectoryTree::Node *getSimpleArchiveBase(DirectoryTree *dataTree); - bool checkBainPackage(DirectoryTree *dataTree); - bool checkFomodPackage(DirectoryTree *dataTree, QString &offset, bool &xmlInstaller); - bool checkNMMInstaller(); - - void fixModName(QString &name); - - bool testOverwrite(const QString &modsDirectory, QString &modName); - - bool doInstall(const QString &modsDirectory, QString &modName, - int modID, const QString &version, const QString &newestVersion, int categoryID); - - bool installFomodExternal(const QString &fileName, const QString &pluginsFileName, const QString &modDirectory); - bool installFomodInternal(DirectoryTree *&baseNode, const QString &fomodPath, const QString &modsDirectory, - int modID, const QString &version, const QString &newestVersion, - int categoryID, QString &modName, bool &manualRequest); - QString generateBackupName(const QString &directoryName); - - bool ensureValidModName(QString &name); - -private slots: - - void openFile(const QString &fileName); - -private: - - struct ByPriority { - bool operator()(IPluginInstaller *LHS, IPluginInstaller *RHS) const - { - return LHS->priority() > RHS->priority(); - } - }; - - struct CaseInsensitive { - bool operator() (const QString &LHS, const QString &RHS) const - { - return QString::compare(LHS, RHS, Qt::CaseInsensitive) < 0; - } - }; - -private: - - QWidget *m_ParentWidget; - - std::vector m_Installers; - std::set m_SupportedExtensions; - - QString m_NCCPath; - - Archive *m_CurrentArchive; - QString m_CurrentFile; - - QProgressDialog m_InstallationProgress; - - std::set m_FilesToDelete; - std::set m_TempFilesToDelete; - -}; - - -#endif // INSTALLATIONMANAGER_H +#ifndef INSTALLATIONMANAGER_H +#define INSTALLATIONMANAGER_H + +#include "installdialog.h" + +#include +#include + +#include +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include + + +/** + * @brief manages the installation of mod archives + * This currently supports two special kind of archives: + * - "simple" archives: properly packaged archives without options, so they can be extracted to the (virtual) data directory directly + * - "complex" bain archives: archives with options for the bain system. + * All other archives are managed through the manual "InstallDialog" + * @todo this may be a good place to support plugins + **/ +class InstallationManager : public QObject, public IInstallationManager +{ + Q_OBJECT + +public: + + /** + * @brief constructor + * + * @param parent parent object. + **/ + explicit InstallationManager(QWidget *parent); + + ~InstallationManager(); + + /** + * @brief install a mod from an archive + * + * @param fileName absolute file name of the archive to install + * @param modName suggested name of the mod. If this is empty (the default), a name will be guessed based on the filename. The user will always have a chance to rename the mod + * @param preferIntegrated if true, integrated installers are chosen over external installers + * @return true if the archive was installed, false if installation failed or was refused + * @exception std::exception an exception may be thrown if the archive can't be opened (maybe the format is invalid or the file is damaged) + **/ + bool install(const QString &fileName, const QString &pluginsFileName, const QString &modsDirectory, bool preferIntegrated, bool enableQuickInstall, QString &modName, bool &hasIniTweaks); + + /** + * @return true if the installation was canceled + **/ + bool wasCancelled(); + + /** + * @brief retrieve a string describing the specified error code + * + * @param errorCode an error code as returned by the archiving function + * @return the error string + * @todo This function doesn't belong here, it is only public because the SelfUpdater class also uses "Archive" to get to the package.txt file + **/ + static QString getErrorString(Archive::Error errorCode); + + void installManual(DirectoryTree::Node *baseNode, DirectoryTree *filesTree, bool &hasIniTweaks, QString &modName, int categoryID, const QString &modsDirectory, QString newestVersion, QString version, int modID, bool success, bool manualRequest); + + /** + * @brief register an installer-plugin + * @param the installer to register + */ + void registerInstaller(IPluginInstaller *installer); + + /** + * @return list of file extensions we can install + */ + QStringList getSupportedExtensions() const; + + /** + * @brief extract the specified file from the currently open archive to a temporary location + * @param (relative) name of the file within the archive + * @return the absolute name of the temporary file + * @note the call will fail with an exception if no archive is open (plugins deriving + * from IPluginInstallerSimple can rely on that, custom installers shouldn't) + * @note the temporary file is automatically cleaned up after the installation + * @note This call can be very slow if the archive is large and "solid" + */ + virtual QString extractFile(const QString &fileName); + + /** + * @brief extract the specified files from the currently open archive to a temporary location + * @param (relative) names of files within the archive + * @return the absolute names of the temporary files + * @note the call will fail with an exception if no archive is open (plugins deriving + * from IPluginInstallerSimple can rely on that, custom installers shouldn't) + * @note the temporary file is automatically cleaned up after the installation + * @note This call can be very slow if the archive is large and "solid" + */ + virtual QStringList extractFiles(const QStringList &files); + + /** + * @brief installs an archive + * @param modName suggested name of the mod + * @param archiveFile path to the archive to install + * @return the installation result + */ + virtual IPluginInstaller::EInstallResult installArchive(const QString &modName, const QString &archiveName); + +private: + + void queryPassword(LPSTR password); + void updateProgress(float percentage); + void updateProgressFile(LPCWSTR fileName); + void report7ZipError(LPCWSTR errorMessage); + + void dummyProgressFile(LPCWSTR) {} + + DirectoryTree *createFilesTree(); + + // remap all files in the archive to the directory structure represented by baseNode + // files not present in baseNode are disabled + void mapToArchive(const DirectoryTree::Node *baseNode); + + // recursive worker function for mapToArchive + void mapToArchive(const DirectoryTree::Node *node, std::wstring path, FileData * const *data); + bool unpackPackageTXT(); + bool unpackSingleFile(const QString &fileName); + + + bool isSimpleArchiveTopLayer(const DirectoryTree::Node *node, bool bainStyle); + DirectoryTree::Node *getSimpleArchiveBase(DirectoryTree *dataTree); + bool checkBainPackage(DirectoryTree *dataTree); + bool checkFomodPackage(DirectoryTree *dataTree, QString &offset, bool &xmlInstaller); + bool checkNMMInstaller(); + + void fixModName(QString &name); + + bool testOverwrite(const QString &modsDirectory, QString &modName); + + bool doInstall(const QString &modsDirectory, QString &modName, + int modID, const QString &version, const QString &newestVersion, int categoryID); + + bool installFomodExternal(const QString &fileName, const QString &pluginsFileName, const QString &modDirectory); + bool installFomodInternal(DirectoryTree *&baseNode, const QString &fomodPath, const QString &modsDirectory, + int modID, const QString &version, const QString &newestVersion, + int categoryID, QString &modName, bool nameGuessed, bool &manualRequest); + QString generateBackupName(const QString &directoryName); + + bool ensureValidModName(QString &name); + +private slots: + + void openFile(const QString &fileName); + +private: + + struct ByPriority { + bool operator()(IPluginInstaller *LHS, IPluginInstaller *RHS) const + { + return LHS->priority() > RHS->priority(); + } + }; + + struct CaseInsensitive { + bool operator() (const QString &LHS, const QString &RHS) const + { + return QString::compare(LHS, RHS, Qt::CaseInsensitive) < 0; + } + }; + +private: + + QWidget *m_ParentWidget; + + std::vector m_Installers; + std::set m_SupportedExtensions; + + QString m_NCCPath; + + Archive *m_CurrentArchive; + QString m_CurrentFile; + + QProgressDialog m_InstallationProgress; + + std::set m_FilesToDelete; + std::set m_TempFilesToDelete; + +}; + + +#endif // INSTALLATIONMANAGER_H diff --git a/src/main.cpp b/src/main.cpp index 0f1fb7c3..d452c3b8 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,424 +17,426 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#define WIN32_LEAN_AND_MEAN -#include -#include -#include -#include -#include -#include -#include "mainwindow.h" -#include "report.h" -#include "modlist.h" -#include "profile.h" -#include "gameinfo.h" -#include "fallout3info.h" -#include "falloutnvinfo.h" -#include "oblivioninfo.h" -#include "skyriminfo.h" -#include "spawn.h" -#include "executableslist.h" -#include "singleinstance.h" -#include "utility.h" -#include "helper.h" -#include "logbuffer.h" -#include "selectiondialog.h" -#include "moapplication.h" -#include "tutorialmanager.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#pragma comment(linker, "/manifestDependency:\"name='dlls' processorArchitecture='x86' version='1.0.0.0' type='win32' \"") - - -void removeOldLogfiles() -{ - QFileInfoList files = QDir(ToQString(GameInfo::instance().getLogDir())).entryInfoList(QStringList("ModOrganizer*.log"), - QDir::Files, QDir::Name); - - if (files.count() > 5) { - for (int i = 0; i < files.count() - 5; ++i) { - QFile::remove(files.at(i).absoluteFilePath()); - } - } -} - - -// set up required folders (for a first install or after an update or to fix a broken installation) -bool bootstrap() -{ - GameInfo &gameInfo = GameInfo::instance(); - - // remove the temporary backup directory in case we're restarting after an update - QString moDirectory = QDir::fromNativeSeparators(ToQString(gameInfo.getOrganizerDirectory())); - QString backupDirectory = moDirectory.mid(0).append("/update_backup"); - if (QDir(backupDirectory).exists()) { - removeDir(backupDirectory); - } - - // cycle logfile - removeOldLogfiles(); - - // create organizer directories - QString dirNames[] = { - QDir::fromNativeSeparators(ToQString(gameInfo.getProfilesDir())), - QDir::fromNativeSeparators(ToQString(gameInfo.getModsDir())), - QDir::fromNativeSeparators(ToQString(gameInfo.getDownloadDir())), - QDir::fromNativeSeparators(ToQString(gameInfo.getOverwriteDir())), - QDir::fromNativeSeparators(ToQString(gameInfo.getLogDir())), - QDir::fromNativeSeparators(ToQString(gameInfo.getTutorialDir())) - }; - static const int NUM_DIRECTORIES = sizeof(dirNames) / sizeof(QString); - - // optimistic run: try to simply create the directories: - for (int i = 0; i < NUM_DIRECTORIES; ++i) { - if (!QDir(dirNames[i]).exists()) { - QDir().mkdir(dirNames[i]); - } - } - - // verify all directories exist and are writable, - // otherwise invoke the helper to create them and make them writable - for (int i = 0; i < NUM_DIRECTORIES; ++i) { - QFileInfo fileInfo(dirNames[i]); - if (!fileInfo.exists() || !fileInfo.isWritable()) { - if (QMessageBox::question(NULL, QObject::tr("Permissions required"), - QObject::tr("The current user account doesn't have the required access rights to run " - "Mod Organizer. The neccessary changes can be made automatically (the MO directory " - "will be made writable for the current user account). You will be asked to run " - "\"helper.exe\" with administrative rights)."), - QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Yes) { - if (!Helper::init(GameInfo::instance().getOrganizerDirectory())) { - return false; - } - } else { - return false; - } - // no matter which directory didn't exist/wasn't writable, the helper - // should have created them all so we can break the loop - break; - } - } - - // verify the hook-dll exists - QString dllName = qApp->applicationDirPath() + "/" + ToQString(AppConfig::hookDLLName()); - HMODULE dllMod = ::LoadLibraryW(ToWString(dllName).c_str()); - if (dllMod == NULL) { - throw windows_error("hook.dll is missing or invalid"); - } - ::FreeLibrary(dllMod); - - return true; -} - - -void cleanupDir() -{ - // files from previous versions of MO that are no longer - // required (in that location) - QString fileNames[] = { - "ModOrganiser.exe", - "ModOrganizer.log", - "ModOrganizer.log.old", - "7z.dll", - "mo1.dll", - "mo_archive.dll", - "mo_helper.exe", - "msvcp90.dll", - "msvcr90.dll", - "phonon4.dll", - "QtCore4.dll", - "QtGui4.dll", - "QtNetwork4.dll", - "QtXml4.dll", - "QtWebKit4.dll", - "qjpeg4.dll" - }; - - static const int NUM_FILES = sizeof(fileNames) / sizeof(QString); - - for (int i = 0; i < NUM_FILES; ++i) { - if (QFile::remove(QApplication::applicationDirPath().append("/").append(fileNames[i]))) { - qDebug("%s removed in cleanup", - QApplication::applicationDirPath().append("/").append(fileNames[i]).toUtf8().constData()); - } - } -} - - -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, - const PMINIDUMP_EXCEPTION_INFORMATION exceptionParam, - const PMINIDUMP_USER_STREAM_INFORMATION userStreamParam, - const PMINIDUMP_CALLBACK_INFORMATION callbackParam); - LONG result = EXCEPTION_CONTINUE_SEARCH; - - HMODULE dbgDLL = ::LoadLibrary(L"dbghelp.dll"); - - static const int errorLen = 200; - char errorBuffer[errorLen + 1]; - memset(errorBuffer, '\0', errorLen + 1); - - if (dbgDLL) { - FuncMiniDumpWriteDump funcDump = (FuncMiniDumpWriteDump)::GetProcAddress(dbgDLL, "MiniDumpWriteDump"); - if (funcDump) { - - if (QMessageBox::question(NULL, QObject::tr("Woops"), - QObject::tr("ModOrganizer has crashed! Should a diagnostic file be created? If you send me this file " - "by email (sherb@gmx.net), the bug is a lot more likely to be fixed. " - "Please include a short description of what you were doing when the crash happened"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - - std::wstring dumpName = ToWString(qApp->applicationFilePath().append(".dmp")); - - HANDLE dumpFile = ::CreateFile(dumpName.c_str(), - GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); - if (dumpFile != INVALID_HANDLE_VALUE) { - _MINIDUMP_EXCEPTION_INFORMATION exceptionInfo; - exceptionInfo.ThreadId = ::GetCurrentThreadId(); - exceptionInfo.ExceptionPointers = exceptionPtrs; - exceptionInfo.ClientPointers = NULL; - - BOOL success = funcDump(::GetCurrentProcess(), ::GetCurrentProcessId(), dumpFile, MiniDumpNormal, &exceptionInfo, NULL, NULL); - - ::CloseHandle(dumpFile); - if (success) { - return EXCEPTION_EXECUTE_HANDLER; - } - _snprintf(errorBuffer, errorLen, "failed to save minidump to %ls (error %lu)", - dumpName.c_str(), ::GetLastError()); - } else { - _snprintf(errorBuffer, errorLen, "failed to create %ls (error %lu)", - dumpName.c_str(), ::GetLastError()); - } - } else { - return result; - } - } else { - _snprintf(errorBuffer, errorLen, "dbghelp.dll outdated"); - } - } else { - _snprintf(errorBuffer, errorLen, "dbghelp.dll not found"); - } - - QMessageBox::critical(NULL, QObject::tr("Woops"), - QObject::tr("ModOrganizer has crashed! Unfortunately I was not able to write a diagnostic file: %1").arg(errorBuffer)); - return result; -} - - -void registerMetaTypes() -{ - registerExecutable(); -} - - -int main(int argc, char *argv[]) -{ - MOApplication application(argc, argv); - SetUnhandledExceptionFilter(MyUnhandledExceptionFilter); - - LogBuffer::init(20, QtDebugMsg, application.applicationDirPath().append("/logs/mo_interface.log")); - QPixmap pixmap(":/MO/gui/splash"); - QSplashScreen splash(pixmap); - splash.show(); - - registerMetaTypes(); - - QStringList arguments = application.arguments(); - - bool update = false; - if (arguments.contains("update")) { - arguments.removeAll("update"); - update = true; - } - - try { - SingleInstance instance(update); - if (!instance.primaryInstance()) { - if ((arguments.size() == 2) && - isNxmLink(arguments.at(1))) { - instance.sendMessage(arguments.at(1)); - return 0; - } else if (arguments.size() == 1) { - QMessageBox::information(NULL, QObject::tr("Mod Organizer"), QObject::tr("An instance of Mod Organizer is already running")); - return 0; - } - } // we continue for the primary instance OR if MO has been called with parameters - - - // TODO: this should be MAX_PATH_UNICODE! - wchar_t omoPath[MAX_PATH]; - memset(omoPath, 0, sizeof(TCHAR) * MAX_PATH); - ::GetModuleFileNameW(NULL, omoPath, MAX_PATH); - wchar_t *lastBSlash = wcsrchr(omoPath, TEXT('\\')); - if (lastBSlash != NULL) { - *lastBSlash = TEXT('\0'); - } - QSettings settings(ToQString(std::wstring(omoPath).append(L"\\ModOrganizer.ini")), QSettings::IniFormat); - - QString gamePath = QString::fromUtf8(settings.value("gamePath", "").toByteArray()); - - bool done = false; - while (!done) { - if (!GameInfo::init(omoPath, ToWString(gamePath))) { - if (!gamePath.isEmpty()) { - reportError(QObject::tr("No game identified in \"%1\". The directory is required to contain " - "the game binary and its launcher.").arg(gamePath)); - } - SelectionDialog selection(QObject::tr("Please select the game to manage"), NULL); - - { // add options - QString skyrimPath = ToQString(SkyrimInfo::getRegPathStatic()); - QString falloutNVPath = ToQString(FalloutNVInfo::getRegPathStatic()); - QString fallout3Path = ToQString(Fallout3Info::getRegPathStatic()); - QString oblivionPath = ToQString(OblivionInfo::getRegPathStatic()); - if (skyrimPath.length() != 0) { - selection.addChoice(QString("Skyrim"), skyrimPath, skyrimPath); - } - if (falloutNVPath.length() != 0) { - selection.addChoice(QString("Fallout NV"), falloutNVPath, falloutNVPath); - } - if (fallout3Path.length() != 0) { - selection.addChoice(QString("Fallout 3"), fallout3Path, fallout3Path); - } - if (oblivionPath.length() != 0) { - selection.addChoice(QString("Oblivion"), oblivionPath, oblivionPath); - } - - selection.addChoice(QString("Browse..."), QString(), QString()); - } - - if (selection.exec() == QDialog::Rejected) { - gamePath = ""; - done = true; - } else { - gamePath = QDir::cleanPath(selection.getChoiceData().toString()); - if (gamePath.isEmpty()) { - gamePath = QFileDialog::getExistingDirectory(NULL, QObject::tr("Please select the game to manage"), QString(), - QFileDialog::ShowDirsOnly); - } - } - } else { - done = true; - gamePath = ToQString(GameInfo::instance().getGameDirectory()); - } - } - - if (gamePath.isEmpty()) { - // game not found and user canceled - return -1; - } else if (gamePath.length() != 0) { - // user selected a folder and game was initialised with it - settings.setValue("gamePath", gamePath.toUtf8().constData()); - } - ExecutablesList executablesList; - - executablesList.init(); - - if (!bootstrap()) { // requires gameinfo to be initialised! - return -1; - } - - cleanupDir(); - - int numCustomExecutables = settings.beginReadArray("customExecutables"); - for (int i = 0; i < numCustomExecutables; ++i) { - settings.setArrayIndex(i); - CloseMOStyle closeMO = settings.value("closeOnStart").toBool() ? DEFAULT_CLOSE : DEFAULT_STAY; - executablesList.addExecutable(settings.value("title").toString(), - settings.value("binary").toString(), - settings.value("arguments").toString(), - settings.value("workingDirectory", "").toString(), - closeMO, - settings.value("steamAppID", "").toString()); - } - - settings.endArray(); - - TutorialManager::init(QDir::fromNativeSeparators(ToQString(GameInfo::instance().getTutorialDir())).append("/")); - - application.setStyleFile(settings.value("Settings/style", "").toString()); - - // set up main window and its data structures - MainWindow mainWindow(argv[0], settings); - QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application, SLOT(setStyleFile(QString))); - - QObject::connect(&instance, SIGNAL(messageSent(QString)), &mainWindow, SLOT(externalMessage(QString))); - - mainWindow.setExecutablesList(executablesList); - mainWindow.readSettings(); - - QString selectedProfileName = QString::fromUtf8(settings.value("selected_profile", "").toByteArray()); - - { // see if there is a profile on the command line - int profileIndex = arguments.indexOf("-p", 1); - if ((profileIndex != -1) && (profileIndex < arguments.size() - 1)) { - selectedProfileName = arguments.at(profileIndex + 1); - } - arguments.removeAt(profileIndex); - arguments.removeAt(profileIndex); - } - - // if we have a command line parameter, it is either a nxm link or - // a binary to start - if ((arguments.size() > 1) && (!isNxmLink(arguments.at(1)))) { - QString exeName = arguments.at(1); - arguments.removeFirst(); // remove application name (ModOrganizer.exe) - arguments.removeFirst(); // remove binary name - // pass the remaining parameters to the binary - mainWindow.spawnProgram(exeName, arguments.join(" "), selectedProfileName, QDir()); - return 0; - } - - mainWindow.createFirstProfile(); - - if (selectedProfileName.length() != 0) { - if (!mainWindow.setCurrentProfile(selectedProfileName)) { - mainWindow.setCurrentProfile(1); - qWarning("failed to set profile: %s", - selectedProfileName.toUtf8().constData()); - } - } else { - mainWindow.setCurrentProfile(1); - } - - mainWindow.show(); - - if ((arguments.size() > 1) && - (isNxmLink(arguments.at(1)))) { - mainWindow.externalMessage(arguments.at(1)); - } - splash.finish(&mainWindow); - return application.exec(); - } catch (const std::exception &e) { - reportError(e.what()); - return 1; - } -} +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include +#include +#include "mainwindow.h" +#include "report.h" +#include "modlist.h" +#include "profile.h" +#include "gameinfo.h" +#include "fallout3info.h" +#include "falloutnvinfo.h" +#include "oblivioninfo.h" +#include "skyriminfo.h" +#include "spawn.h" +#include "executableslist.h" +#include "singleinstance.h" +#include "utility.h" +#include "helper.h" +#include "logbuffer.h" +#include "selectiondialog.h" +#include "moapplication.h" +#include "tutorialmanager.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#pragma comment(linker, "/manifestDependency:\"name='dlls' processorArchitecture='x86' version='1.0.0.0' type='win32' \"") + + +void removeOldLogfiles() +{ + QFileInfoList files = QDir(ToQString(GameInfo::instance().getLogDir())).entryInfoList(QStringList("ModOrganizer*.log"), + QDir::Files, QDir::Name); + + if (files.count() > 5) { + for (int i = 0; i < files.count() - 5; ++i) { + QFile::remove(files.at(i).absoluteFilePath()); + } + } +} + + +// set up required folders (for a first install or after an update or to fix a broken installation) +bool bootstrap() +{ + GameInfo &gameInfo = GameInfo::instance(); + + // remove the temporary backup directory in case we're restarting after an update + QString moDirectory = QDir::fromNativeSeparators(ToQString(gameInfo.getOrganizerDirectory())); + QString backupDirectory = moDirectory.mid(0).append("/update_backup"); + if (QDir(backupDirectory).exists()) { + removeDir(backupDirectory); + } + + // cycle logfile + removeOldLogfiles(); + + // create organizer directories + QString dirNames[] = { + QDir::fromNativeSeparators(ToQString(gameInfo.getProfilesDir())), + QDir::fromNativeSeparators(ToQString(gameInfo.getModsDir())), + QDir::fromNativeSeparators(ToQString(gameInfo.getDownloadDir())), + QDir::fromNativeSeparators(ToQString(gameInfo.getOverwriteDir())), + QDir::fromNativeSeparators(ToQString(gameInfo.getLogDir())), + QDir::fromNativeSeparators(ToQString(gameInfo.getTutorialDir())) + }; + static const int NUM_DIRECTORIES = sizeof(dirNames) / sizeof(QString); + + // optimistic run: try to simply create the directories: + for (int i = 0; i < NUM_DIRECTORIES; ++i) { + if (!QDir(dirNames[i]).exists()) { + QDir().mkdir(dirNames[i]); + } + } + + // verify all directories exist and are writable, + // otherwise invoke the helper to create them and make them writable + for (int i = 0; i < NUM_DIRECTORIES; ++i) { + QFileInfo fileInfo(dirNames[i]); + if (!fileInfo.exists() || !fileInfo.isWritable()) { + if (QMessageBox::question(NULL, QObject::tr("Permissions required"), + QObject::tr("The current user account doesn't have the required access rights to run " + "Mod Organizer. The neccessary changes can be made automatically (the MO directory " + "will be made writable for the current user account). You will be asked to run " + "\"helper.exe\" with administrative rights)."), + QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Yes) { + if (!Helper::init(GameInfo::instance().getOrganizerDirectory())) { + return false; + } + } else { + return false; + } + // no matter which directory didn't exist/wasn't writable, the helper + // should have created them all so we can break the loop + break; + } + } + + // verify the hook-dll exists + QString dllName = qApp->applicationDirPath() + "/" + ToQString(AppConfig::hookDLLName()); + HMODULE dllMod = ::LoadLibraryW(ToWString(dllName).c_str()); + if (dllMod == NULL) { + throw windows_error("hook.dll is missing or invalid"); + } + ::FreeLibrary(dllMod); + + return true; +} + + +void cleanupDir() +{ + // files from previous versions of MO that are no longer + // required (in that location) + QString fileNames[] = { + "ModOrganiser.exe", + "ModOrganizer.log", + "ModOrganizer.log.old", + "7z.dll", + "mo1.dll", + "mo_archive.dll", + "mo_helper.exe", + "msvcp90.dll", + "msvcr90.dll", + "phonon4.dll", + "QtCore4.dll", + "QtGui4.dll", + "QtNetwork4.dll", + "QtXml4.dll", + "QtWebKit4.dll", + "qjpeg4.dll" + }; + + static const int NUM_FILES = sizeof(fileNames) / sizeof(QString); + + for (int i = 0; i < NUM_FILES; ++i) { + if (QFile::remove(QApplication::applicationDirPath().append("/").append(fileNames[i]))) { + qDebug("%s removed in cleanup", + QApplication::applicationDirPath().append("/").append(fileNames[i]).toUtf8().constData()); + } + } +} + + +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, + const PMINIDUMP_EXCEPTION_INFORMATION exceptionParam, + const PMINIDUMP_USER_STREAM_INFORMATION userStreamParam, + const PMINIDUMP_CALLBACK_INFORMATION callbackParam); + LONG result = EXCEPTION_CONTINUE_SEARCH; + + HMODULE dbgDLL = ::LoadLibrary(L"dbghelp.dll"); + + static const int errorLen = 200; + char errorBuffer[errorLen + 1]; + memset(errorBuffer, '\0', errorLen + 1); + + if (dbgDLL) { + FuncMiniDumpWriteDump funcDump = (FuncMiniDumpWriteDump)::GetProcAddress(dbgDLL, "MiniDumpWriteDump"); + if (funcDump) { + + if (QMessageBox::question(NULL, QObject::tr("Woops"), + QObject::tr("ModOrganizer has crashed! Should a diagnostic file be created? If you send me this file " + "by email (sherb@gmx.net), the bug is a lot more likely to be fixed. " + "Please include a short description of what you were doing when the crash happened"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + + std::wstring dumpName = ToWString(qApp->applicationFilePath().append(".dmp")); + + HANDLE dumpFile = ::CreateFile(dumpName.c_str(), + GENERIC_WRITE, FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); + if (dumpFile != INVALID_HANDLE_VALUE) { + _MINIDUMP_EXCEPTION_INFORMATION exceptionInfo; + exceptionInfo.ThreadId = ::GetCurrentThreadId(); + exceptionInfo.ExceptionPointers = exceptionPtrs; + exceptionInfo.ClientPointers = NULL; + + BOOL success = funcDump(::GetCurrentProcess(), ::GetCurrentProcessId(), dumpFile, MiniDumpNormal, &exceptionInfo, NULL, NULL); + + ::CloseHandle(dumpFile); + if (success) { + return EXCEPTION_EXECUTE_HANDLER; + } + _snprintf(errorBuffer, errorLen, "failed to save minidump to %ls (error %lu)", + dumpName.c_str(), ::GetLastError()); + } else { + _snprintf(errorBuffer, errorLen, "failed to create %ls (error %lu)", + dumpName.c_str(), ::GetLastError()); + } + } else { + return result; + } + } else { + _snprintf(errorBuffer, errorLen, "dbghelp.dll outdated"); + } + } else { + _snprintf(errorBuffer, errorLen, "dbghelp.dll not found"); + } + + QMessageBox::critical(NULL, QObject::tr("Woops"), + QObject::tr("ModOrganizer has crashed! Unfortunately I was not able to write a diagnostic file: %1").arg(errorBuffer)); + return result; +} + + +void registerMetaTypes() +{ + registerExecutable(); +} + + +int main(int argc, char *argv[]) +{ + MOApplication application(argc, argv); + SetUnhandledExceptionFilter(MyUnhandledExceptionFilter); + + LogBuffer::init(20, QtDebugMsg, application.applicationDirPath().append("/logs/mo_interface.log")); + QPixmap pixmap(":/MO/gui/splash"); + QSplashScreen splash(pixmap); + splash.show(); + + registerMetaTypes(); + + QStringList arguments = application.arguments(); + + bool update = false; + if (arguments.contains("update")) { + arguments.removeAll("update"); + update = true; + } + + try { + SingleInstance instance(update); + if (!instance.primaryInstance()) { + if ((arguments.size() == 2) && + isNxmLink(arguments.at(1))) { + instance.sendMessage(arguments.at(1)); + return 0; + } else if (arguments.size() == 1) { + QMessageBox::information(NULL, QObject::tr("Mod Organizer"), QObject::tr("An instance of Mod Organizer is already running")); + return 0; + } + } // we continue for the primary instance OR if MO has been called with parameters + + + // TODO: this should be MAX_PATH_UNICODE! + wchar_t omoPath[MAX_PATH]; + memset(omoPath, 0, sizeof(TCHAR) * MAX_PATH); + ::GetModuleFileNameW(NULL, omoPath, MAX_PATH); + wchar_t *lastBSlash = wcsrchr(omoPath, TEXT('\\')); + if (lastBSlash != NULL) { + *lastBSlash = TEXT('\0'); + } + QSettings settings(ToQString(std::wstring(omoPath).append(L"\\ModOrganizer.ini")), QSettings::IniFormat); + + QString gamePath = QString::fromUtf8(settings.value("gamePath", "").toByteArray()); + + bool done = false; + while (!done) { + if (!GameInfo::init(omoPath, ToWString(gamePath))) { + if (!gamePath.isEmpty()) { + reportError(QObject::tr("No game identified in \"%1\". The directory is required to contain " + "the game binary and its launcher.").arg(gamePath)); + } + SelectionDialog selection(QObject::tr("Please select the game to manage"), NULL); + + { // add options + QString skyrimPath = ToQString(SkyrimInfo::getRegPathStatic()); + QString falloutNVPath = ToQString(FalloutNVInfo::getRegPathStatic()); + QString fallout3Path = ToQString(Fallout3Info::getRegPathStatic()); + QString oblivionPath = ToQString(OblivionInfo::getRegPathStatic()); + if (skyrimPath.length() != 0) { + selection.addChoice(QString("Skyrim"), skyrimPath, skyrimPath); + } + if (falloutNVPath.length() != 0) { + selection.addChoice(QString("Fallout NV"), falloutNVPath, falloutNVPath); + } + if (fallout3Path.length() != 0) { + selection.addChoice(QString("Fallout 3"), fallout3Path, fallout3Path); + } + if (oblivionPath.length() != 0) { + selection.addChoice(QString("Oblivion"), oblivionPath, oblivionPath); + } + + selection.addChoice(QString("Browse..."), QString(), QString()); + } + + if (selection.exec() == QDialog::Rejected) { + gamePath = ""; + done = true; + } else { + gamePath = QDir::cleanPath(selection.getChoiceData().toString()); + if (gamePath.isEmpty()) { + gamePath = QFileDialog::getExistingDirectory(NULL, QObject::tr("Please select the game to manage"), QString(), + QFileDialog::ShowDirsOnly); + } + } + } else { + done = true; + gamePath = ToQString(GameInfo::instance().getGameDirectory()); + } + } + + if (gamePath.isEmpty()) { + // game not found and user canceled + return -1; + } else if (gamePath.length() != 0) { + // user selected a folder and game was initialised with it + settings.setValue("gamePath", gamePath.toUtf8().constData()); + } + ExecutablesList executablesList; + + executablesList.init(); + + if (!bootstrap()) { // requires gameinfo to be initialised! + return -1; + } + + cleanupDir(); + + int numCustomExecutables = settings.beginReadArray("customExecutables"); + for (int i = 0; i < numCustomExecutables; ++i) { + settings.setArrayIndex(i); + CloseMOStyle closeMO = settings.value("closeOnStart").toBool() ? DEFAULT_CLOSE : DEFAULT_STAY; + executablesList.addExecutable(settings.value("title").toString(), + settings.value("binary").toString(), + settings.value("arguments").toString(), + settings.value("workingDirectory", "").toString(), + closeMO, + settings.value("steamAppID", "").toString(), + settings.value("custom", true).toBool(), + settings.value("toolbar", false).toBool()); + } + + settings.endArray(); + + TutorialManager::init(QDir::fromNativeSeparators(ToQString(GameInfo::instance().getTutorialDir())).append("/")); + + application.setStyleFile(settings.value("Settings/style", "").toString()); + + // set up main window and its data structures + MainWindow mainWindow(argv[0], settings); + QObject::connect(&mainWindow, SIGNAL(styleChanged(QString)), &application, SLOT(setStyleFile(QString))); + + QObject::connect(&instance, SIGNAL(messageSent(QString)), &mainWindow, SLOT(externalMessage(QString))); + + mainWindow.setExecutablesList(executablesList); + mainWindow.readSettings(); + + QString selectedProfileName = QString::fromUtf8(settings.value("selected_profile", "").toByteArray()); + + { // see if there is a profile on the command line + int profileIndex = arguments.indexOf("-p", 1); + if ((profileIndex != -1) && (profileIndex < arguments.size() - 1)) { + selectedProfileName = arguments.at(profileIndex + 1); + } + arguments.removeAt(profileIndex); + arguments.removeAt(profileIndex); + } + + // if we have a command line parameter, it is either a nxm link or + // a binary to start + if ((arguments.size() > 1) && (!isNxmLink(arguments.at(1)))) { + QString exeName = arguments.at(1); + arguments.removeFirst(); // remove application name (ModOrganizer.exe) + arguments.removeFirst(); // remove binary name + // pass the remaining parameters to the binary + mainWindow.spawnProgram(exeName, arguments.join(" "), selectedProfileName, QDir()); + return 0; + } + + mainWindow.createFirstProfile(); + + if (selectedProfileName.length() != 0) { + if (!mainWindow.setCurrentProfile(selectedProfileName)) { + mainWindow.setCurrentProfile(1); + qWarning("failed to set profile: %s", + selectedProfileName.toUtf8().constData()); + } + } else { + mainWindow.setCurrentProfile(1); + } + + mainWindow.show(); + + if ((arguments.size() > 1) && + (isNxmLink(arguments.at(1)))) { + mainWindow.externalMessage(arguments.at(1)); + } + splash.finish(&mainWindow); + return application.exec(); + } catch (const std::exception &e) { + reportError(e.what()); + return 1; + } +} diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 7493505c..dd217447 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -17,3735 +17,3887 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include "mainwindow.h" -#include "ui_mainwindow.h" -#include -#include "spawn.h" -#include "report.h" -#include "modlist.h" -#include "profile.h" -#include "pluginlist.h" -#include "installdialog.h" -#include "profilesdialog.h" -#include "editexecutablesdialog.h" -#include "categories.h" -#include "categoriesdialog.h" -#include "utility.h" -#include "modinfodialog.h" -#include "overwriteinfodialog.h" -#include "activatemodsdialog.h" -#include "downloadlist.h" -#include "downloadlistwidget.h" -#include "downloadlistwidgetcompact.h" -#include "messagedialog.h" -#include "installationmanager.h" -#include "textviewer.h" -#include "lockeddialog.h" -#include "syncoverwritedialog.h" -#include "logbuffer.h" -#include "downloadlistsortproxy.h" -#include "modlistsortproxy.h" -#include "motddialog.h" -#include "filedialogmemory.h" -#include "questionboxmemory.h" -#include "tutorialmanager.h" -#include "icondelegate.h" -#include "credentialsdialog.h" -#include "gameinfoimpl.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -//TODO factor out functionality and structure the rest! -//TODO name slots more consistently -//TODO turn operations on various trees/lists into actions for easier support in QMenus? -//TODO verify slots don't throw exceptions - -MainWindow::MainWindow(const QString &exeName, QSettings &initSettings, QWidget *parent) - : QMainWindow(parent), ui(new Ui::MainWindow), m_Tutorial(this, "MainWindow"), - m_ExeName(exeName), m_OldProfileIndex(-1), - m_DirectoryStructure(new DirectoryEntry(L"data", NULL, 0)), - m_ModList(NexusInterface::instance()), - m_OldExecutableIndex(-1), m_GamePath(ToQString(GameInfo::instance().getGameDirectory())), - m_NexusDialog(NexusInterface::instance()->getAccessManager(), NULL), - m_DownloadManager(NexusInterface::instance(), this), - m_InstallationManager(this), m_Translator(NULL), m_TranslatorQt(NULL), - m_Updater(NexusInterface::instance(), this), m_CategoryFactory(CategoryFactory::instance()), - m_CurrentProfile(NULL), m_AskForNexusPW(false), m_LoginAttempted(false), - m_ArchivesInit(false), m_ContextItem(NULL), m_CurrentSaveView(NULL), - m_GameInfo(new GameInfoImpl()) -{ - ui->setupUi(this); - - this->setWindowTitle(ToQString(GameInfo::instance().getGameName()).append(" Mod Organizer v").append(m_Updater.getVersion().canonicalString())); - - m_RefreshProgress = new QProgressBar(statusBar()); - m_RefreshProgress->setTextVisible(true); - m_RefreshProgress->setRange(0, 100); - m_RefreshProgress->setValue(0); - statusBar()->addWidget(m_RefreshProgress, 1000); - statusBar()->clearMessage(); - - ui->actionEndorseMO->setVisible(false); - - updateProblemsButton(); - - updateToolBar(); - ui->toolBar->blockSignals(true); - createHelpWidget(); - - ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure); - - // set up mod list - m_ModListSortProxy = new ModListSortProxy(m_CurrentProfile, this); - m_ModListSortProxy->setSourceModel(&m_ModList); - ui->modList->setModel(m_ModListSortProxy); - - ui->modList->sortByColumn(ModList::COL_PRIORITY, Qt::AscendingOrder); - ui->modList->setItemDelegateForColumn(ModList::COL_FLAGS, new IconDelegate(m_ModListSortProxy)); - ui->modList->header()->installEventFilter(&m_ModList); - ui->modList->header()->restoreState(initSettings.value("mod_list_state").toByteArray()); - ui->modList->installEventFilter(&m_ModList); - // restoreState also seems to restores the resize mode from previous session, - // I don't really like that -#if QT_VERSION >= 0x50000 - for (int i = 0; i < ui->modList->header()->count(); ++i) { - ui->modList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents); - } - ui->modList->header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch); -#else - for (int i = 0; i < ui->modList->header()->count(); ++i) { - ui->modList->header()->setResizeMode(i, QHeaderView::ResizeToContents); - } - ui->modList->header()->setResizeMode(ModList::COL_NAME, QHeaderView::Stretch); -#endif - - // set up plugin list - m_PluginListSortProxy = new PluginListSortProxy(this); - m_PluginListSortProxy->setSourceModel(&m_PluginList); - ui->espList->setModel(m_PluginListSortProxy); - ui->espList->sortByColumn(PluginList::COL_PRIORITY, Qt::AscendingOrder); - ui->espList->header()->restoreState(initSettings.value("plugin_list_state").toByteArray()); - ui->espList->installEventFilter(&m_PluginList); -#if QT_VERSION >= 0x50000 - for (int i = 0; i < ui->espList->header()->count(); ++i) { - ui->espList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents); - } - ui->espList->header()->setSectionResizeMode(0, QHeaderView::Stretch); -#else - for (int i = 0; i < ui->espList->header()->count(); ++i) { - ui->espList->header()->setResizeMode(i, QHeaderView::ResizeToContents); - } - ui->espList->header()->setResizeMode(0, QHeaderView::Stretch); -#endif - - QMenu *linkMenu = new QMenu(this); - linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Desktop"), this, SLOT(linkDesktop())); - linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Start Menu"), this, SLOT(linkMenu())); - ui->linkButton->setMenu(linkMenu); - - m_DownloadManager.setOutputDirectory(m_Settings.getDownloadDirectory()); - - NexusInterface::instance()->setCacheDirectory(m_Settings.getCacheDirectory()); - NexusInterface::instance()->setNMMVersion(m_Settings.getNMMVersion()); - - updateDownloadListDelegate(); - connect(&m_ModList, SIGNAL(requestColumnSelect(QPoint)), this, SLOT(displayColumnSelection(QPoint))); - - connect(&m_DownloadManager, SIGNAL(showMessage(QString)), this, SLOT(showMessage(QString))); - - connect(NexusInterface::instance(), SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); - connect(&m_NexusDialog, SIGNAL(requestDownload(QNetworkReply*, int, QString)), this, SLOT(downloadRequested(QNetworkReply*, int, QString))); - - ui->savegameList->installEventFilter(this); - ui->savegameList->setMouseTracking(true); - connect(ui->savegameList, SIGNAL(itemEntered(QListWidgetItem*)), - this, SLOT(saveSelectionChanged(QListWidgetItem*))); - - connect(&m_PluginList, SIGNAL(esplist_changed()), this, SLOT(esplist_changed())); - connect(&m_ModList, SIGNAL(modorder_changed()), this, SLOT(modorder_changed())); - connect(&m_ModList, SIGNAL(removeOrigin(QString)), this, SLOT(removeOrigin(QString))); - connect(&m_ModList, SIGNAL(showMessage(QString)), this, SLOT(showMessage(QString))); - connect(&m_ModList, SIGNAL(modRenamed(QString,QString)), this, SLOT(modRenamed(QString,QString))); - connect(ui->modFilterEdit, SIGNAL(textChanged(QString)), m_ModListSortProxy, SLOT(updateFilter(QString))); - connect(&m_ModList, SIGNAL(modlist_changed(int)), m_ModListSortProxy, SLOT(invalidate())); - connect(&m_ModList, SIGNAL(removeSelectedMods()), this, SLOT(removeMod_clicked())); - - connect(&m_DirectoryRefresher, SIGNAL(refreshed()), this, SLOT(directory_refreshed())); - connect(&m_DirectoryRefresher, SIGNAL(progress(int)), this, SLOT(refresher_progress(int))); - connect(&m_DirectoryRefresher, SIGNAL(error(QString)), this, SLOT(showError(QString))); - - connect(&m_Settings, SIGNAL(languageChanged(QString)), this, SLOT(languageChange(QString))); - connect(&m_Settings, SIGNAL(styleChanged(QString)), this, SIGNAL(styleChanged(QString))); - - connect(&m_Updater, SIGNAL(restart()), this, SLOT(close())); - connect(&m_Updater, SIGNAL(updateAvailable()), this, SLOT(updateAvailable())); - connect(&m_Updater, SIGNAL(motdAvailable(QString)), this, SLOT(motdReceived(QString))); - - connect(NexusInterface::instance()->getAccessManager(), SIGNAL(loginSuccessful(bool)), this, SLOT(loginSuccessful(bool))); - connect(NexusInterface::instance()->getAccessManager(), SIGNAL(loginFailed(QString)), this, SLOT(loginFailed(QString))); - - connect(&TutorialManager::instance(), SIGNAL(windowTutorialFinished(QString)), this, SLOT(windowTutorialFinished(QString))); - - 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); - - fixCategories(); - m_Updater.testForUpdate(); - m_StartTime = QTime::currentTime(); - - m_Tutorial.expose("modList", &m_ModList); - m_Tutorial.expose("espList", &m_PluginList); - loadPlugins(); -} - - -MainWindow::~MainWindow() -{ - m_RefresherThread.exit(); - m_RefresherThread.wait(); - m_NexusDialog.close(); - delete ui; - delete m_GameInfo; -} - - -void MainWindow::resizeEvent(QResizeEvent *event) -{ - m_Tutorial.resize(event->size()); - QMainWindow::resizeEvent(event); -} - - -void MainWindow::actionToToolButton(QAction *&sourceAction) -{ - QToolButton *button = new QToolButton(ui->toolBar); - button->setObjectName(sourceAction->objectName()); - button->setIcon(sourceAction->icon()); - button->setText(sourceAction->text()); - button->setPopupMode(QToolButton::InstantPopup); - button->setToolButtonStyle(ui->toolBar->toolButtonStyle()); - button->setToolTip(sourceAction->toolTip()); - button->setShortcut(sourceAction->shortcut()); - QMenu *buttonMenu = new QMenu(sourceAction->text()); - button->setMenu(buttonMenu); - QAction *newAction = ui->toolBar->insertWidget(sourceAction, button); - newAction->setObjectName(sourceAction->objectName()); - ui->toolBar->removeAction(sourceAction); - sourceAction->deleteLater(); - sourceAction = newAction; -} - - -void MainWindow::updateToolBar() -{ - QWidget *spacer = new QWidget(ui->toolBar); - spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); - - actionToToolButton(ui->actionTool); - actionToToolButton(ui->actionHelp); - - foreach (QAction *action, ui->toolBar->actions()) { - if (action->objectName().length() == 0) { - // insert spacers - ui->toolBar->insertWidget(action, spacer); - } - } -} - - -void MainWindow::updateProblemsButton() -{ - QString problemDescription; - if (checkForProblems(problemDescription)) { - ui->actionProblems->setEnabled(true); - ui->actionProblems->setIconText(tr("Problems")); - ui->actionProblems->setToolTip(tr("There are potential problems with your setup")); - } else { - ui->actionProblems->setIconText(tr("No Problems")); - ui->actionProblems->setToolTip(tr("Everything seems to be in order")); - } -} - - -bool MainWindow::errorReported(QString &logFile) -{ - QDir dir(ToQString(GameInfo::instance().getLogDir())); - QFileInfoList files = dir.entryInfoList(QStringList("ModOrganizer_??_??_??_??_??.log"), - QDir::Files, QDir::Name | QDir::Reversed); - - if (files.count() > 0) { - logFile = files.at(0).absoluteFilePath(); - QFile file(logFile); - if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { - char buffer[1024]; - int line = 0; - while (!file.atEnd()) { - file.readLine(buffer, 1024); - if (strncmp(buffer, "ERROR", 5) == 0) { - return true; - } - - // prevent this function from taking forever - if (line++ >= 50000) { - break; - } - } - } - } - - return false; -} - - -bool MainWindow::checkForProblems(QString &problemDescription) -{ - problemDescription = ""; - - foreach (IPluginDiagnose *diagnose, m_DiagnosisPlugins) { - std::vector activeProblems = diagnose->activeProblems(); - foreach (unsigned int key, activeProblems) { - problemDescription.append(tr("
  • %1
  • ").arg(diagnose->shortDescription(key))); - } - } - - QString NCCBinary = QCoreApplication::applicationDirPath().mid(0).append("/NCC/NexusClientCLI.exe"); - if (!QFile::exists(NCCBinary)) { - problemDescription.append(tr("
  • NCC not installed. You won't be able to install some scripted mod-installers. Get NCC from the MO page on nexus
  • ")); - } else { - VS_FIXEDFILEINFO versionInfo = GetFileVersion(ToWString(QDir::toNativeSeparators(NCCBinary))); - if ((versionInfo.dwFileVersionMS & 0xFFFF) != 0x02) { - problemDescription.append(tr("
  • NCC version may be incompatible.
  • ")); - } - } - - if (QSettings("HKEY_LOCAL_MACHINE\\Software\\Microsoft\\NET Framework Setup\\NDP\\v3.5", - QSettings::NativeFormat).value("Install", 0) != 1) { - QString dotNetUrl = "http://www.microsoft.com/en-us/download/details.aspx?id=17851"; - problemDescription.append(tr("
  • dotNet is not installed or outdated. This is required to use NCC. " - "Get it from here: %1
  • ").arg(dotNetUrl)); - } - - QString logFile; - if (errorReported(logFile)) { - problemDescription.append(tr("
  • There was an error reported in your last log. Please see %1
  • ").arg(logFile)); - } - - bool res = problemDescription.length() != 0; - if (res) { - problemDescription.prepend("
      ").append("
    "); - - ui->actionProblems->setEnabled(true); - ui->actionProblems->setIconText(tr("Problems")); - ui->actionProblems->setToolTip(tr("There are potential problems with your setup")); - } else { - ui->actionProblems->setToolTip(tr("Everything seems to be in order")); - } - return res; -} - - -void MainWindow::createHelpWidget() -{ - QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionHelp)); - QMenu *buttonMenu = toolBtn->menu(); - - QAction *helpAction = new QAction(tr("Help on UI"), buttonMenu); - connect(helpAction, SIGNAL(triggered()), this, SLOT(helpTriggered())); - buttonMenu->addAction(helpAction); - - QAction *wikiAction = new QAction(tr("Documentation Wiki"), buttonMenu); - connect(wikiAction, SIGNAL(triggered()), this, SLOT(wikiTriggered())); - buttonMenu->addAction(wikiAction); - - QAction *issueAction = new QAction(tr("Report Issue"), buttonMenu); - connect(issueAction, SIGNAL(triggered()), this, SLOT(issueTriggered())); - buttonMenu->addAction(issueAction); - - QMenu *tutorialMenu = new QMenu(tr("Tutorials"), buttonMenu); - - typedef std::vector > ActionList; - - ActionList tutorials; - QDirIterator dirIter("tutorials", QStringList("*.js"), QDir::Files); - while (dirIter.hasNext()) { - dirIter.next(); - QString fileName = dirIter.fileName(); - QFile file(dirIter.filePath()); - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open " << fileName; - continue; - } - QString firstLine = QString::fromUtf8(file.readLine()); - if (firstLine.startsWith("//TL")) { - QStringList params = firstLine.mid(4).trimmed().split('#'); - if (params.size() != 2) { - qCritical() << "invalid header line for tutorial " << fileName << " expected 2 parameters"; - continue; - } - QAction *tutAction = new QAction(params.at(0), tutorialMenu); - tutAction->setData(fileName); - tutorials.push_back(std::make_pair(params.at(1).toInt(), tutAction)); - } - } - - std::sort(tutorials.begin(), tutorials.end(), - [](const ActionList::value_type &LHS, const ActionList::value_type &RHS) { - return LHS.first < RHS.first; } ); - - for (auto iter = tutorials.begin(); iter != tutorials.end(); ++iter) { - connect(iter->second, SIGNAL(triggered()), this, SLOT(tutorialTriggered())); - tutorialMenu->addAction(iter->second); - } - - buttonMenu->addMenu(tutorialMenu); -} - - -bool MainWindow::saveCurrentLists() -{ - if (m_DirectoryUpdate) { - qWarning("not saving lists during directory update"); - return false; - } - - // save plugin list - - try { - m_PluginList.saveTo(m_CurrentProfile->getPluginsFileName(), - m_CurrentProfile->getLoadOrderFileName(), - m_CurrentProfile->getLockedOrderFileName(), - m_CurrentProfile->getDeleterFileName(), - m_Settings.hideUncheckedPlugins()); - - if (!m_PluginList.saveLoadOrder(*m_DirectoryStructure)) { - MessageDialog::showMessage(tr("load order could not be saved"), this); - } else { - ui->btnSave->setEnabled(false); - } - } catch (const std::exception &e) { - reportError(tr("failed to save load order: %1").arg(e.what())); - } - - if (m_ArchivesInit) { - QFile archiveFile(m_CurrentProfile->getArchivesFileName()); - if (archiveFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - for (int i = 0; i < ui->bsaList->topLevelItemCount(); ++i) { - QTreeWidgetItem *item = ui->bsaList->topLevelItem(i); - if ((item != NULL) && (item->checkState(0) == Qt::Checked)) { - archiveFile.write(item->text(0).toUtf8().append("\r\n")); - } - } - } else { - reportError(tr("failed to save archives order, do you have write access " - "to \"%1\"?").arg(m_CurrentProfile->getArchivesFileName())); - } - archiveFile.close(); - } else { - qWarning("archive list not initialised"); - } - return true; -} - - -bool MainWindow::addProfile() -{ - QComboBox *profileBox = findChild("profileBox"); - bool okClicked = false; - - QString name = QInputDialog::getText(this, tr("Name"), - tr("Please enter a name for the new profile"), - QLineEdit::Normal, QString(), &okClicked); - if (okClicked && (name.size() > 0)) { - try { - profileBox->addItem(name); - profileBox->setCurrentIndex(profileBox->count() - 1); - return true; - } catch (const std::exception& e) { - reportError(tr("failed to create profile: %1").arg(e.what())); - return false; - } - } else { - return false; - } -} - - -void MainWindow::hookUpWindowTutorials() -{ - QDirIterator dirIter("tutorials", QStringList("*.js"), QDir::Files); - while (dirIter.hasNext()) { - dirIter.next(); - QString fileName = dirIter.fileName(); - QFile file(dirIter.filePath()); - if (!file.open(QIODevice::ReadOnly)) { - qCritical() << "Failed to open " << fileName; - continue; - } - QString firstLine = QString::fromUtf8(file.readLine()); - if (firstLine.startsWith("//WIN")) { - QString windowName = firstLine.mid(6).trimmed(); - if (!m_Settings.directInterface().value("CompletedWindowTutorials/" + windowName, false).toBool()) { - TutorialManager::instance().activateTutorial(windowName, fileName); - } - } - } -} - - -void MainWindow::showEvent(QShowEvent *event) -{ - refreshFilters(); - - QMainWindow::showEvent(event); - m_Tutorial.registerControl(); - - hookUpWindowTutorials(); - - if (m_Settings.directInterface().value("first_start", true).toBool()) { - QString firstStepsTutorial = ToQString(AppConfig::firstStepsTutorial()); - if (TutorialManager::instance().hasTutorial(firstStepsTutorial)) { - if (QMessageBox::question(this, tr("Show tutorial?"), - tr("You are starting Mod Organizer for the first time. " - "Do you want to show a tutorial of its basic features? If you choose " - "no you can always start the tutorial from the \"Help\"-menu."), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - TutorialManager::instance().activateTutorial("MainWindow", firstStepsTutorial); - } - } else { - qCritical() << firstStepsTutorial << " missing"; - QPoint pos = ui->toolBar->mapToGlobal(QPoint()); - pos.rx() += ui->toolBar->width() / 2; - pos.ry() += ui->toolBar->height(); - QWhatsThis::showText(pos, - QObject::tr("Please use \"Help\" from the toolbar to get usage instructions to all elements")); - } - - m_Settings.directInterface().setValue("first_start", false); - } -} - - -void MainWindow::closeEvent(QCloseEvent* event) -{ - if (m_DownloadManager.downloadsInProgress() && - QMessageBox::question(this, tr("Downloads in progress"), - tr("There are still downloads in progress, do you really want to quit?"), - QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Cancel) { - event->ignore(); - return; - } - - setCursor(Qt::WaitCursor); - - m_NexusDialog.close(); - - storeSettings(); - - // profile has to be cleaned up before the modinfo-buffer is cleared - delete m_CurrentProfile; - m_CurrentProfile = NULL; - - ModInfo::clear(); - LogBuffer::cleanQuit(); - m_ModList.setProfile(NULL); -} - - -void MainWindow::createFirstProfile() -{ - if (!refreshProfiles(false)) { - Profile newProf("Default", false); - refreshProfiles(false); - } -} - - -void MainWindow::setBrowserGeometry(const QByteArray &geometry) -{ - m_NexusDialog.restoreGeometry(geometry); -} - - -SaveGameGamebryo *MainWindow::getSaveGame(const QString &name) -{ - return new SaveGameGamebryo(this, name); -} - - -SaveGameGamebryo *MainWindow::getSaveGame(QListWidgetItem *item) -{ - try { - SaveGameGamebryo *saveGame = getSaveGame(item->data(Qt::UserRole).toString()); - saveGame->setParent(item->listWidget()); - return saveGame; - } catch (const std::exception &e) { - reportError(tr("failed to read savegame: %1").arg(e.what())); - return NULL; - } -} - - -void MainWindow::displaySaveGameInfo(const SaveGameGamebryo *save, QPoint pos) -{ - if (m_CurrentSaveView == NULL) { - m_CurrentSaveView = new SaveGameInfoWidgetGamebryo(save, &m_PluginList, this); - } else { - m_CurrentSaveView->setSave(save); - } - - QRect screenRect = QApplication::desktop()->availableGeometry(m_CurrentSaveView); - - if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) { - pos.rx() -= (m_CurrentSaveView->width() + 2); - } else { - pos.rx() += 5; - } - - if (pos.y() + m_CurrentSaveView->height() > screenRect.bottom()) { - pos.ry() -= (m_CurrentSaveView->height() + 10); - } else { - pos.ry() += 20; - } - m_CurrentSaveView->move(pos); - - m_CurrentSaveView->show(); - ui->savegameList->activateWindow(); - connect(m_CurrentSaveView, SIGNAL(closeSaveInfo()), this, SLOT(hideSaveGameInfo())); -} - - -void MainWindow::saveSelectionChanged(QListWidgetItem *newItem) -{ - if (newItem == NULL) { - hideSaveGameInfo(); - } else if ((m_CurrentSaveView == NULL) || (newItem != m_CurrentSaveView->property("displayItem").value())) { - const SaveGameGamebryo *save = getSaveGame(newItem); - if (save != NULL) { - displaySaveGameInfo(save, QCursor::pos()); - m_CurrentSaveView->setProperty("displayItem", qVariantFromValue((void*)newItem)); - } - } -} - - - -void MainWindow::hideSaveGameInfo() -{ - if (m_CurrentSaveView != NULL) { - disconnect(m_CurrentSaveView, SIGNAL(closeSaveInfo()), this, SLOT(hideSaveGameInfo())); - m_CurrentSaveView->deleteLater(); - m_CurrentSaveView = NULL; - } -} - - -bool MainWindow::eventFilter(QObject* object, QEvent *event) -{ - if ((object == ui->savegameList) && - ((event->type() == QEvent::Leave) || (event->type() == QEvent::WindowDeactivate))) { - hideSaveGameInfo(); - } - - return false; -} - - -bool MainWindow::testForSteam() -{ - DWORD processIDs[1024]; - DWORD bytesReturned; - if (!::EnumProcesses(processIDs, sizeof(processIDs), &bytesReturned)) { - qWarning("failed to determine if steam is running"); - return true; - } - - TCHAR processName[MAX_PATH]; - for (unsigned int i = 0; i < bytesReturned / sizeof(DWORD); ++i) { - memset(processName, '\0', sizeof(TCHAR) * MAX_PATH); - if (processIDs[i] != 0) { - HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processIDs[i]); - - if (process != NULL) { - HMODULE module; - DWORD ignore; - - // first module in a process is always the binary - if (EnumProcessModules(process, &module, sizeof(HMODULE) * 1, &ignore)) { - GetModuleBaseName(process, module, processName, MAX_PATH); - if ((_tcsicmp(processName, TEXT("steam.exe")) == 0) || - (_tcsicmp(processName, TEXT("steamservice.exe")) == 0)) { - return true; - } - } - } - } - } - - return false; -} - - -bool MainWindow::verifyPlugin(IPlugin *plugin) -{ - if (plugin == NULL) { - return false; - } else if (!plugin->init(this)) { - qWarning("plugin failed to initialize"); - return false; - } - return true; -} - - -void MainWindow::toolPluginInvoke() -{ - QAction *triggeredAction = qobject_cast(sender()); - IPluginTool *plugin = (IPluginTool*)triggeredAction->data().value(); - try { - plugin->display(); - } catch (const std::exception &e) { - reportError(tr("Plugin \"%1\" failed: %2").arg(plugin->name()).arg(e.what())); - } -} - - -void MainWindow::registerPluginTool(IPluginTool *tool) -{ - QAction *action = new QAction(tool->icon(), tool->displayName(), ui->toolBar); - action->setToolTip(tool->tooltip()); - tool->setParentWidget(this); - action->setData(qVariantFromValue((void*)tool)); - connect(action, SIGNAL(triggered()), this, SLOT(toolPluginInvoke())); - QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionTool)); - toolBtn->menu()->addAction(action); -} - - -bool MainWindow::registerPlugin(QObject *plugin) -{ - { // generic treatment for all plugins - IPlugin *pluginObj = qobject_cast(plugin); - if (pluginObj == NULL) { - return false; - } - m_Settings.registerPlugin(pluginObj); - } - - { // diagnosis plugins - IPluginDiagnose *diagnose = qobject_cast(plugin); - if (diagnose != NULL) { - m_DiagnosisPlugins.push_back(diagnose); - } - } - - { // tool plugins - IPluginTool *tool = qobject_cast(plugin); - if (verifyPlugin(tool)) { - registerPluginTool(tool); - return true; - } - } - { // installer plugins - IPluginInstaller *installer = qobject_cast(plugin); - if (verifyPlugin(installer)) { - installer->setParentWidget(this); - m_InstallationManager.registerInstaller(installer); - return true; - } - } - return false; -} - - -void MainWindow::loadPlugins() -{ - m_DiagnosisPlugins.clear(); - - m_Settings.clearPlugins(); - - foreach (QObject *plugin, QPluginLoader::staticInstances()) { - registerPlugin(plugin); - } - - QString pluginPath = QDir::fromNativeSeparators(ToQString(GameInfo::instance().getOrganizerDirectory())) + "/" + ToQString(AppConfig::pluginPath()); - qDebug("looking for plugins in %s", pluginPath.toUtf8().constData()); - QDirIterator iter(pluginPath, QDir::Files | QDir::NoDotAndDotDot); - while (iter.hasNext()) { - iter.next(); - QString pluginName = iter.filePath(); - if (QLibrary::isLibrary(pluginName)) { - QPluginLoader pluginLoader(pluginName); - if (pluginLoader.instance() == NULL) { - qCritical("failed to load plugin %s: %s", - pluginName.toUtf8().constData(), pluginLoader.errorString().toUtf8().constData()); - } else { - if (registerPlugin(pluginLoader.instance())) { - qDebug("loaded plugin \"%s\"", pluginName.toUtf8().constData()); - } else { - qWarning("plugin \"%s\" failed to load", pluginName.toUtf8().constData()); - } - } - } - } -} - -IGameInfo &MainWindow::gameInfo() const -{ - return *m_GameInfo; -} - - -QString MainWindow::profileName() const -{ - return m_CurrentProfile->getName(); -} - -QString MainWindow::profilePath() const -{ - return m_CurrentProfile->getPath(); -} - -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(const QString &name) -{ - unsigned int index = ModInfo::getIndex(name); - if (index != UINT_MAX) { - throw MyException(tr("The mod \"%1\" already exists!").arg(name)); - } - - QString targetDirectory = QDir::fromNativeSeparators(m_Settings.getModDirectory()).append("/").append(name.trimmed()); - - QSettings settingsFile(targetDirectory.mid(0).append("/meta.ini"), QSettings::IniFormat); - - settingsFile.setValue("modid", 0); - settingsFile.setValue("version", 0); - settingsFile.setValue("newestVersion", 0); - settingsFile.setValue("category", 0); - settingsFile.setValue("installationFile", 0); - return ModInfo::createFrom(QDir(targetDirectory), &m_DirectoryStructure).data(); -} - -bool MainWindow::removeMod(IModInterface *mod) -{ - return ModInfo::removeMod(ModInfo::getIndex(mod->name())); -} - - -void MainWindow::modDataChanged(IModInterface*) -{ - refreshModList(); -} - -QVariant MainWindow::pluginSetting(const QString &pluginName, const QString &key) const -{ - return m_Settings.pluginSetting(pluginName, key); -} - - -void MainWindow::startSteam() -{ - QSettings steamSettings("HKEY_CURRENT_USER\\Software\\Valve\\Steam", - QSettings::NativeFormat); - QString exe = steamSettings.value("SteamExe", "").toString(); - if (!exe.isEmpty()) { - QString temp = QString("\"%1\"").arg(exe); - if (!QProcess::startDetached(temp)) { - reportError(tr("Failed to start \"%1\"").arg(temp)); - } else { - QMessageBox::information(this, tr("Waiting"), tr("Please press OK once you're logged into steam.")); - } - } -} - - -HANDLE MainWindow::spawnBinaryDirect(const QFileInfo &binary, const QString &arguments, const QString &profileName, - const QDir ¤tDirectory, const QString &steamAppID) -{ - storeSettings(); - - if (!binary.exists()) { - reportError(tr("\"%1\" not found").arg(binary.fileName())); - return INVALID_HANDLE_VALUE; - } - - if (!steamAppID.isEmpty()) { - ::SetEnvironmentVariableW(L"SteamAPPId", ToWString(steamAppID).c_str()); - } else { - ::SetEnvironmentVariableW(L"SteamAPPId", ToWString(m_Settings.getSteamAppID()).c_str()); - } - - if ((GameInfo::instance().requiresSteam()) && - (m_Settings.getLoadMechanism() == LoadMechanism::LOAD_MODORGANIZER)) { - if (!testForSteam()) { - if (QuestionBoxMemory::query(this->isVisible() ? this : NULL, - m_Settings.directInterface(), "steamQuery", tr("Start Steam?"), - tr("Steam is required to be running already to correctly start the game. " - "Should MO try to start steam now?"), - QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::Yes) { - startSteam(); - } - } - } - - return startBinary(binary, arguments, profileName, m_Settings.logLevel(), currentDirectory, true); -} - - -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) -{ - storeSettings(); - - HANDLE processHandle = spawnBinaryDirect(binary, arguments, m_CurrentProfile->getName(), currentDirectory, steamAppID); - if (processHandle != INVALID_HANDLE_VALUE) { - if (closeAfterStart) { - close(); - } else { - this->setEnabled(false); - - LockedDialog *dialog = new LockedDialog(this); - dialog->show(); - - QCoreApplication::processEvents(); - - while ((::WaitForSingleObject(processHandle, 1000) == WAIT_TIMEOUT) && - !dialog->unlockClicked()) { - // keep processing events so the app doesn't appear dead - QCoreApplication::processEvents(); - } - - this->setEnabled(true); - refreshDirectoryStructure(); - refreshDataTree(); - if (GameInfo::instance().getLoadOrderMechanism() == GameInfo::TYPE_FILETIME) { - QFile::remove(m_CurrentProfile->getLoadOrderFileName()); - } - refreshLists(); - dialog->hide(); - } - } -} - - -void MainWindow::refreshModList() -{ - ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure); - m_CurrentProfile->refreshModStatus(); - - m_ModList.notifyChange(-1); - - refreshDirectoryStructure(); -} - - -void MainWindow::setExecutablesList(const ExecutablesList &executablesList) -{ - m_ExecutablesList = executablesList; - refreshExecutablesList(); -} - -void MainWindow::setExecutableIndex(int index) -{ - QComboBox *executableBox = findChild("executablesListBox"); - - if ((index != 0) && (executableBox->count() > index)) { - executableBox->setCurrentIndex(index); - } else { - executableBox->setCurrentIndex(1); - } - - const Executable &selectedExecutable = executableBox->itemData(executableBox->currentIndex()).value(); - - QIcon addIcon(":/MO/gui/link"); - QIcon removeIcon(":/MO/gui/remove"); - - QFileInfo linkDesktopFile(QDir::fromNativeSeparators(getDesktopDirectory()) + "/" + selectedExecutable.m_Title + ".lnk"); - QFileInfo linkMenuFile(QDir::fromNativeSeparators(getStartMenuDirectory()) + "/" + selectedExecutable.m_Title + ".lnk"); - - ui->linkButton->menu()->actions().at(0)->setIcon(linkDesktopFile.exists() ? removeIcon : addIcon); - ui->linkButton->menu()->actions().at(1)->setIcon(linkMenuFile.exists() ? removeIcon : addIcon); -} - - -void MainWindow::activateSelectedProfile() -{ - QString profileName = ui->profileBox->currentText(); - qDebug() << "activate profile " << profileName; - QString profileDir = QDir::fromNativeSeparators(ToQString(GameInfo::instance().getProfilesDir())) - .append("/").append(profileName); - delete m_CurrentProfile; - m_CurrentProfile = new Profile(QDir(profileDir)); - m_ModList.setProfile(m_CurrentProfile); - - m_ModListSortProxy->setProfile(m_CurrentProfile); - - connect(m_CurrentProfile, SIGNAL(modStatusChanged(uint)), this, SLOT(modStatusChanged(uint))); - - refreshModList(); - refreshSaveList(); -} - - -void MainWindow::on_profileBox_currentIndexChanged(int index) -{ - if (ui->profileBox->isEnabled()) { - int previousIndex = m_OldProfileIndex; - m_OldProfileIndex = index; - - if ((previousIndex != -1) && - (m_CurrentProfile != NULL) && - m_CurrentProfile->exists()) { - saveCurrentLists(); - } - - if (ui->profileBox->currentIndex() == 0) { - ui->profileBox->setCurrentIndex(previousIndex); - ProfilesDialog(m_GamePath).exec(); - while (!refreshProfiles()) { - ProfilesDialog(m_GamePath).exec(); - } - } else { - activateSelectedProfile(); - } - } -} - - -void MainWindow::updateTo(QTreeWidgetItem *subTree, const std::wstring &directorySoFar, const DirectoryEntry &directoryEntry, bool conflictsOnly) -{ - { - std::vector files = directoryEntry.getFiles(); - for (auto iter = files.begin(); iter != files.end(); ++iter) { - FileEntry *current = *iter; - if (conflictsOnly && (current->getAlternatives().size() == 0)) { - continue; - } - - QString fileName = ToQString(current->getName()); - QStringList columns(fileName); - bool isArchive = false; - int originID = current->getOrigin(isArchive); - QString source = ToQString(m_DirectoryStructure->getOriginByID(originID).getName()); - std::wstring archive = current->getArchive(); - if (archive.length() != 0) { - source.append(" (").append(ToQString(archive)).append(")"); - } - columns.append(source); - QTreeWidgetItem *fileChild = new QTreeWidgetItem(columns); - if (isArchive) { - QFont font = fileChild->font(0); - font.setItalic(true); - fileChild->setFont(0, font); - fileChild->setFont(1, font); - } else if (fileName.endsWith(ModInfo::s_HiddenExt)) { - QFont font = fileChild->font(0); - font.setStrikeOut(true); - fileChild->setFont(0, font); - fileChild->setFont(1, font); - } - fileChild->setData(0, Qt::UserRole, ToQString(current->getFullPath())); - fileChild->setData(0, Qt::UserRole + 1, isArchive); - fileChild->setData(1, Qt::UserRole, source); - fileChild->setData(1, Qt::UserRole + 1, originID); - - std::vector alternatives = current->getAlternatives(); - - if (!alternatives.empty()) { - std::wostringstream altString; - altString << ToWString(tr("Also in:
    ")); - for (std::vector::iterator altIter = alternatives.begin(); - altIter != alternatives.end(); ++altIter) { - if (altIter != alternatives.begin()) { - altString << " , "; - } - altString << "" << m_DirectoryStructure->getOriginByID(*altIter).getName() << ""; - } - fileChild->setToolTip(1, QString("%1").arg(ToQString(altString.str()))); - fileChild->setForeground(1, QBrush(Qt::red)); - } else { - fileChild->setToolTip(1, tr("No conflict")); - } - subTree->addChild(fileChild); - } - } - - std::wostringstream temp; - temp << directorySoFar << "\\" << directoryEntry.getName(); - { - std::vector::const_iterator current, end; - directoryEntry.getSubDirectories(current, end); - for (; current != end; ++current) { - QStringList columns(ToQString((*current)->getName())); - columns.append(""); - QTreeWidgetItem *directoryChild = new QTreeWidgetItem(columns); - updateTo(directoryChild, temp.str(), **current, conflictsOnly); - if (directoryChild->childCount() != 0) { - subTree->addChild(directoryChild); - } - } - } - subTree->sortChildren(0, Qt::AscendingOrder); -} - - -bool MainWindow::refreshProfiles(bool selectProfile) -{ - QComboBox* profileBox = findChild("profileBox"); - - QString currentProfileName = profileBox->currentText(); - - profileBox->blockSignals(true); - profileBox->clear(); - profileBox->addItem(QObject::tr("")); - - QDir profilesDir(ToQString(GameInfo::instance().getProfilesDir())); - profilesDir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot); - - QDirIterator profileIter(profilesDir); - - int newIndex = profileIter.hasNext() ? 1 : 0; - int currentIndex = 0; - while (profileIter.hasNext()) { - profileIter.next(); - ++currentIndex; - try { - profileBox->addItem(profileIter.fileName()); - if (currentProfileName == profileIter.fileName()) { - newIndex = currentIndex; - } - } catch (const std::runtime_error& error) { - reportError(QObject::tr("failed to parse profile %1: %2").arg(profileIter.fileName()).arg(error.what())); - } - } - - // now select one of the profiles, preferably the one that was selected before - profileBox->blockSignals(false); - - if (selectProfile) { - if (profileBox->count() > 1) { - if (currentProfileName.length() != 0) { - if ((newIndex != 0) && (profileBox->count() > newIndex)) { - profileBox->setCurrentIndex(newIndex); - } else { - profileBox->setCurrentIndex(1); - } - } - return true; - } else { - return false; - } - } else { - return profileBox->count() > 1; - } -} - - -void MainWindow::refreshDirectoryStructure() -{ - m_DirectoryUpdate = true; - std::vector > activeModList = m_CurrentProfile->getActiveMods(); - m_DirectoryRefresher.setMods(activeModList); - - statusBar()->show(); -// m_RefreshProgress->setVisible(true); - m_RefreshProgress->setRange(0, 100); - - QTimer::singleShot(0, &m_DirectoryRefresher, SLOT(refresh())); -} - - -void MainWindow::refreshExecutablesList() -{ - QComboBox* executablesList = findChild("executablesListBox"); - executablesList->setEnabled(false); - executablesList->clear(); - executablesList->addItem(tr("")); - - QAbstractItemModel *model = executablesList->model(); - - std::vector::const_iterator current, end; - m_ExecutablesList.getExecutables(current, end); - for(int i = 0; current != end; ++current, ++i) { - QIcon icon(":/MO/gui/executable"); - HICON winIcon; - QVariant temp; - temp.setValue(*current); - UINT res = ::ExtractIconExW(ToWString(current->m_BinaryInfo.filePath()).c_str(), - 0, &winIcon, NULL, 1); - if (res == 1) { -// icon = pixmapFromHICON(winIcon); - icon = QIcon(QPixmap::fromWinHICON(winIcon)); - ::DestroyIcon(winIcon); - } - executablesList->addItem(icon, current->m_Title, temp); - model->setData(model->index(i, 0), QSize(0, executablesList->iconSize().height() + 4), Qt::SizeHintRole); - } - - setExecutableIndex(1); - executablesList->setEnabled(true); -} - - -void MainWindow::refreshDataTree() -{ - QCheckBox *conflictsBox = findChild("conflictsCheckBox"); - QTreeWidget *tree = findChild("dataTree"); - tree->clear(); - QStringList columns("data"); - columns.append(""); - QTreeWidgetItem *subTree = new QTreeWidgetItem(columns); - updateTo(subTree, L"", *m_DirectoryStructure, conflictsBox->isChecked()); - tree->insertTopLevelItem(0, subTree); - subTree->setExpanded(true); - tree->header()->resizeSection(0, 200); -} - - -void MainWindow::refreshSaveList() -{ - refreshLists(); - ui->savegameList->clear(); - - QDir savesDir; - if (m_CurrentProfile->localSavesEnabled()) { - savesDir.setPath(m_CurrentProfile->getPath() + "/saves"); - } else { - wchar_t path[MAX_PATH]; - ::GetPrivateProfileStringW(L"General", L"SLocalSavePath", L"Saves", - path, MAX_PATH, - (ToWString(m_CurrentProfile->getPath()) + L"\\" + GameInfo::instance().getIniFileNames().at(0)).c_str()); - savesDir.setPath(QDir::fromNativeSeparators(ToQString(GameInfo::instance().getDocumentsDir() + L"\\" + path))); - } - - QStringList filters; - filters << ToQString(GameInfo::instance().getSaveGameExtension()); - savesDir.setNameFilters(filters); - - QFileInfoList files = savesDir.entryInfoList(QDir::Files, QDir::Time); - - foreach (const QFileInfo &file, files) { - QListWidgetItem *item = new QListWidgetItem(file.fileName()); - item->setData(Qt::UserRole, file.absoluteFilePath()); - ui->savegameList->addItem(item); - } -} - - -void MainWindow::refreshLists() -{ - if (m_DirectoryStructure->isPopulated()) { - refreshESPList(); - refreshBSAList(); - } // no point in refreshing lists if no files have been added to the directory tree -} - - -void MainWindow::refreshESPList() -{ - m_CurrentProfile->writeModlist(); - - // clear list - m_PluginList.refresh(m_CurrentProfile->getName(), *m_DirectoryStructure, - m_CurrentProfile->getPluginsFileName(), - m_CurrentProfile->getLoadOrderFileName(), - m_CurrentProfile->getLockedOrderFileName()); - //m_PluginList.readFrom(m_CurrentProfile->getPluginsFileName()); - - findChild("btnSave")->setEnabled(false); -} - - -static bool BySortValue(const std::pair &LHS, const std::pair &RHS) -{ - return LHS.first < RHS.first; -} - - -void MainWindow::refreshBSAList() -{ - m_ArchivesInit = false; - ui->bsaList->clear(); -#if QT_VERSION >= 0x50000 - ui->bsaList->header()->setSectionResizeMode(QHeaderView::ResizeToContents); -#else - ui->bsaList->header()->setResizeMode(QHeaderView::ResizeToContents); -#endif - - m_DefaultArchives.clear(); - - wchar_t buffer[256]; - std::wstring iniFileName = ToWString(QDir::toNativeSeparators(m_CurrentProfile->getIniFileName())); - if (::GetPrivateProfileStringW(L"Archive", GameInfo::instance().archiveListKey().c_str(), - L"", buffer, 256, iniFileName.c_str()) != 0) { - m_DefaultArchives = ToQString(buffer).split(','); - } - - if (::GetPrivateProfileStringW(L"Archive", GameInfo::instance().archiveListKey().append(L"2").c_str(), - L"", buffer, 256, iniFileName.c_str()) != 0) { - m_DefaultArchives.append(ToQString(buffer).split(',')); - } - - for (int i = 0; i < m_DefaultArchives.count(); ++i) { - m_DefaultArchives[i] = m_DefaultArchives[i].trimmed(); - } - - 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 { - m_ActiveArchives = m_DefaultArchives; - } - - std::vector > items; - - std::vector files = m_DirectoryStructure->getFiles(); - for (auto iter = files.begin(); iter != files.end(); ++iter) { - FileEntry *current = *iter; - - QString filename = ToQString(current->getName().c_str()); - QString extension = filename.right(3).toLower(); - - if (extension == "bsa") { - if (filename.compare("update.bsa", Qt::CaseInsensitive) == 0) { - // hack: ignore update.bsa because it confuses people - continue; - } - int index = m_ActiveArchives.indexOf(filename); - QStringList strings(filename); - bool isArchive = false; - int origin = current->getOrigin(isArchive); - strings.append(ToQString(m_DirectoryStructure->getOriginByID(origin).getName())); - QTreeWidgetItem *newItem = new QTreeWidgetItem(strings); - newItem->setData(0, Qt::UserRole, index); - newItem->setData(1, Qt::UserRole, origin); - newItem->setFlags(newItem->flags() & ~Qt::ItemIsDropEnabled | 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); - } - - if (index < 0) index = 0; - UINT32 sortValue = ((m_DirectoryStructure->getOriginByID(origin).getPriority() & 0xFFFF) << 16) | (index & 0xFFFF); - items.push_back(std::make_pair(sortValue, newItem)); - } - } - - std::sort(items.begin(), items.end(), BySortValue); - - for (std::vector >::iterator iter = items.begin(); iter != items.end(); ++iter) { - ui->bsaList->addTopLevelItem(iter->second); - } - - checkBSAList(); - m_ArchivesInit = true; -} - - -void MainWindow::checkBSAList() -{ - ui->bsaList->blockSignals(true); - - bool warning = false; - - for (int i = 0; i < ui->bsaList->topLevelItemCount(); ++i) { - QTreeWidgetItem *item = ui->bsaList->topLevelItem(i); - QString filename = item->text(0); - item->setIcon(0, QIcon()); - item->setToolTip(0, QString()); - - if (item->checkState(0) == Qt::Unchecked) { - if (m_DefaultArchives.contains(filename)) { - item->setIcon(0, QIcon(":/MO/gui/resources/dialog-warning.png")); - item->setToolTip(0, tr("This bsa is enabled in the ini file so it may be required!")); - warning = true; - } 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)) { - item->setIcon(0, QIcon(":/MO/gui/resources/dialog-warning.png")); - 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!")); - warning = true; - } - } - } - } - - if (warning) { - ui->tabWidget->setTabIcon(1, QIcon(":/MO/gui/resources/dialog-warning.png")); - } else { - ui->tabWidget->setTabIcon(1, QIcon()); - } - - ui->bsaList->blockSignals(false); -} - - -void MainWindow::fixCategories() -{ - for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(i); - std::set categories = modInfo->getCategories(); - for (std::set::iterator iter = categories.begin(); - iter != categories.end(); ++iter) { - if (!m_CategoryFactory.categoryExists(*iter)) { - modInfo->setCategory(*iter, false); - } - } - } -} - - -void MainWindow::readSettings() -{ - QSettings settings(ToQString(GameInfo::instance().getIniFilename()), QSettings::IniFormat); - - if (settings.contains("window_geometry")) { - restoreGeometry(settings.value("window_geometry").toByteArray()); - } - - if (settings.contains("browser_geometry")) { - setBrowserGeometry(settings.value("browser_geometry").toByteArray()); - } - - bool filtersVisible = settings.value("filters_visible", false).toBool(); - setCategoryListVisible(filtersVisible); - ui->displayCategoriesBtn->setChecked(filtersVisible); - - - QString language = settings.value("Settings/language", QLocale::system().name()).toString(); - languageChange(language); - int selectedExecutable = settings.value("selected_executable").toInt(); - setExecutableIndex(selectedExecutable); -} - - -void MainWindow::storeSettings() -{ - if (m_CurrentProfile == NULL) { - return; - } - m_CurrentProfile->writeModlist(); - 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()); - settings.setValue("compact_downloads", ui->compactBox->isChecked()); - settings.setValue("ask_for_nexuspw", m_AskForNexusPW); - - settings.setValue("window_geometry", this->saveGeometry()); - settings.setValue("browser_geometry", m_NexusDialog.saveGeometry()); - - settings.setValue("filters_visible", ui->displayCategoriesBtn->isChecked()); - - settings.remove("customExecutables"); - settings.beginWriteArray("customExecutables"); - std::vector::const_iterator current, end; - m_ExecutablesList.getExecutables(current, end); - int count = 0; - for (; current != end; ++current) { - const Executable &item = *current; - if (item.m_Custom) { - 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.endArray(); - - QComboBox *executableBox = findChild("executablesListBox"); - settings.setValue("selected_executable", executableBox->currentIndex()); -} - - -void MainWindow::on_btnRefreshData_clicked() -{ - if (!m_DirectoryUpdate) { - refreshDirectoryStructure(); - refreshDataTree(); - } else { - qDebug("directory update"); - } -} - -void MainWindow::on_tabWidget_currentChanged(int index) -{ - saveCurrentLists(); - if (index == 0) { - refreshESPList(); - } else if (index == 1) { - refreshBSAList(); - } else if (index == 2) { - refreshDataTree(); - } else if (index == 3) { - refreshSaveList(); - } else if (index == 4) { - ui->downloadView->scrollToBottom(); - } -} - - -static QString guessModName(const QString &fileName) -{ - return QFileInfo(fileName).baseName(); -} - - -void MainWindow::installMod(const QString &fileName) -{ - bool hasIniTweaks = false; - QString modName; - m_CurrentProfile->writeModlistNow(); - if (m_InstallationManager.install(fileName, m_CurrentProfile->getPluginsFileName(), m_Settings.getModDirectory(), m_Settings.preferIntegratedInstallers(), - m_Settings.enableQuickInstaller(), modName, hasIniTweaks)) { - MessageDialog::showMessage(tr("Installation successful"), this); - refreshModList(); - - QModelIndexList posList = m_ModList.match(m_ModList.index(0, 0), Qt::DisplayRole, 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); - } -} - - -void MainWindow::installMod() -{ - try { - QStringList extensions = m_InstallationManager.getSupportedExtensions(); - for (auto iter = extensions.begin(); iter != extensions.end(); ++iter) { - *iter = "*." + *iter; - } - - QString fileName = QFileDialog::getOpenFileName(this, - tr("Choose Mod"), QString(), - tr("Mod Archive").append(QString(" (%1)").arg(extensions.join(" ")))); - - if (fileName.length() == 0) { - return; - } else { - installMod(fileName); - } - } catch (const std::exception &e) { - reportError(e.what()); - } -} - - -void MainWindow::on_btnSave_clicked() -{ - saveCurrentLists(); -} - - -void MainWindow::on_startButton_clicked() -{ - QComboBox* executablesList = findChild("executablesListBox"); - - Executable selectedExecutable = executablesList->itemData(executablesList->currentIndex()).value(); - - spawnBinary(selectedExecutable.m_BinaryInfo, - selectedExecutable.m_Arguments, - selectedExecutable.m_WorkingDirectory.length() != 0 ? selectedExecutable.m_WorkingDirectory - : selectedExecutable.m_BinaryInfo.absolutePath(), - selectedExecutable.m_CloseMO == DEFAULT_CLOSE, - selectedExecutable.m_SteamAppID); -} - - -static HRESULT CreateShortcut(LPCWSTR targetFileName, LPCWSTR arguments, - LPCSTR linkFileName, LPCWSTR description, - LPCWSTR currentDirectory) -{ - HRESULT result = E_INVALIDARG; - if ((targetFileName != NULL) && (wcslen(targetFileName) > 0) && - (arguments != NULL) && - (linkFileName != NULL) && (strlen(linkFileName) > 0) && - (description != NULL) && - (currentDirectory != NULL)) { - - IShellLink* shellLink; - result = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, - IID_IShellLink, (LPVOID*)&shellLink); - - if (!SUCCEEDED(result)) { - qCritical("failed to create IShellLink instance"); - return result; - } - if (!SUCCEEDED(result)) return result; - - result = shellLink->SetPath(targetFileName); - if (!SUCCEEDED(result)) { - qCritical("failed to set target path %ls", targetFileName); - shellLink->Release(); - return result; - } - result = shellLink->SetArguments(arguments); - if (!SUCCEEDED(result)) { - qCritical("failed to set arguments: %ls", arguments); - shellLink->Release(); - return result; - } - - if (wcslen(description) > 0) { - result = shellLink->SetDescription(description); - if (!SUCCEEDED(result)) { - qCritical("failed to set description: %ls", description); - shellLink->Release(); - return result; - } - } - - if (wcslen(currentDirectory) > 0) { - result = shellLink->SetWorkingDirectory(currentDirectory); - if (!SUCCEEDED(result)) { - qCritical("failed to set working directory: %ls", currentDirectory); - shellLink->Release(); - return result; - } - } - - IPersistFile* persistFile; - result = shellLink->QueryInterface(IID_IPersistFile, (LPVOID*)&persistFile); - if (SUCCEEDED(result)) { - wchar_t linkFileNameW[MAX_PATH]; - MultiByteToWideChar(CP_ACP, 0, linkFileName, -1, linkFileNameW, MAX_PATH); - result = persistFile->Save(linkFileNameW, TRUE); - persistFile->Release(); - } else { - qCritical("failed to create IPersistFile instance"); - } - - shellLink->Release(); - } - return result; -} - - -bool MainWindow::modifyExecutablesDialog() -{ - bool result = false; - try { - EditExecutablesDialog dialog(m_ExecutablesList); - if (dialog.exec() == QDialog::Accepted) { - m_ExecutablesList = dialog.getExecutablesList(); - result = true; - } - refreshExecutablesList(); - } catch (const std::exception &e) { - reportError(e.what()); - } - return result; -} - -void MainWindow::on_executablesListBox_currentIndexChanged(int index) -{ - QComboBox* executablesList = findChild("executablesListBox"); - - int previousIndex = m_OldExecutableIndex; - m_OldExecutableIndex = index; - - if (executablesList->isEnabled()) { - - if (executablesList->itemData(index).isNull()) { - if (modifyExecutablesDialog()) { - setExecutableIndex(previousIndex); -// executablesList->setCurrentIndex(previousIndex); - } - } else { - setExecutableIndex(index); - } - } -} - -void MainWindow::helpTriggered() -{ - QWhatsThis::enterWhatsThisMode(); -} - -void MainWindow::wikiTriggered() -{ -// ::ShellExecuteW(NULL, L"open", L"http://issue.tannin.eu/tbg/wiki/Modorganizer%3AMainPage", NULL, NULL, SW_SHOWNORMAL); - ::ShellExecuteW(NULL, L"open", L"http://wiki.step-project.com/Guide:Mod_Organizer", NULL, NULL, SW_SHOWNORMAL); -} - -void MainWindow::issueTriggered() -{ - ::ShellExecuteW(NULL, L"open", L"http://issue.tannin.eu/tbg", NULL, NULL, SW_SHOWNORMAL); -} - - -void MainWindow::tutorialTriggered() -{ - QAction *tutorialAction = qobject_cast(sender()); - if (tutorialAction != NULL) { - if (QMessageBox::question(this, tr("Start Tutorial?"), - tr("You're about to start a tutorial. For technical reasons it's not possible to end " - "the tutorial early. Continue?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - TutorialManager::instance().activateTutorial("MainWindow", tutorialAction->data().toString()); - } - } -} - - -void MainWindow::on_actionInstallMod_triggered() -{ - installMod(); -} - -void MainWindow::on_actionAdd_Profile_triggered() -{ - bool repeat = true; - while (repeat) { - ProfilesDialog profilesDialog(m_GamePath); - profilesDialog.exec(); - if (refreshProfiles() && !profilesDialog.failed()) { - repeat = false; - } - } -// addProfile(); -} - -void MainWindow::on_actionModify_Executables_triggered() -{ - if (modifyExecutablesDialog()) { - setExecutableIndex(m_OldExecutableIndex); - } -} - - -void MainWindow::setModListSorting(int index) -{ - Qt::SortOrder order = ((index & 0x01) != 0) ? Qt::DescendingOrder : Qt::AscendingOrder; - int column = index >> 1; - ui->modList->header()->setSortIndicator(column, order); -} - - -void MainWindow::setESPListSorting(int index) -{ - switch (index) { - case 0: { - ui->espList->header()->setSortIndicator(1, Qt::AscendingOrder); - } break; - case 1: { - ui->espList->header()->setSortIndicator(1, Qt::DescendingOrder); - } break; - case 2: { - ui->espList->header()->setSortIndicator(0, Qt::AscendingOrder); - } break; - case 3: { - ui->espList->header()->setSortIndicator(0, Qt::DescendingOrder); - } break; - } -} - - -void MainWindow::setCompactDownloads(bool compact) -{ - ui->compactBox->setChecked(compact); -} - - -bool MainWindow::queryLogin(QString &username, QString &password) -{ - CredentialsDialog dialog(this); - int res = dialog.exec(); - if (dialog.neverAsk()) { - m_AskForNexusPW = false; - } - if (res == QDialog::Accepted) { - username = dialog.username(); - password = dialog.password(); - if (dialog.store()) { - m_Settings.setNexusLogin(username, password); - } - return true; - } else { - return false; - } -} - - -bool MainWindow::setCurrentProfile(int index) -{ - QComboBox *profilesBox = findChild("profileBox"); - if (index >= profilesBox->count()) { - return false; - } else { - profilesBox->setCurrentIndex(index); - return true; - } -} - -bool MainWindow::setCurrentProfile(const QString &name) -{ - QComboBox *profilesBox = findChild("profileBox"); - for (int i = 0; i < profilesBox->count(); ++i) { - if (QString::compare(profilesBox->itemText(i), name, Qt::CaseInsensitive) == 0) { - profilesBox->setCurrentIndex(i); - return true; - } - } - // profile not valid - profilesBox->setCurrentIndex(1); - return false; -} - - -void MainWindow::esplist_changed() -{ - findChild("btnSave")->setEnabled(true); -} - - -void MainWindow::refresher_progress(int percent) -{ - m_RefreshProgress->setValue(percent); -} - - -void MainWindow::directory_refreshed() -{ - DirectoryEntry *newStructure = m_DirectoryRefresher.getDirectoryStructure(); - if (newStructure != NULL) { - delete m_DirectoryStructure; - m_DirectoryStructure = newStructure; - } else { - // TODO: don't know why this happens, this slot seems to get called twice with only one emit - return; - } - m_DirectoryUpdate = false; - refreshLists(); -// m_RefreshProgress->setVisible(false); - statusBar()->hide(); -} - - -void MainWindow::externalMessage(const QString &message) -{ - if (message.left(6).toLower() == "nxm://") { - MessageDialog::showMessage(tr("Download started"), this); - downloadRequestedNXM(message); - } -} - - -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_DirectoryStructure->addFromOrigin(ToWString(modInfo->name()), - ToWString(QDir::toNativeSeparators(modInfo->absolutePath())), - m_CurrentProfile->getModPriority(index));*/ - DirectoryRefresher::cleanStructure(m_DirectoryStructure); - } else { - if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { - FilesOrigin &origin = m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())); - origin.enable(false); - } - } - refreshLists(); - } catch (const std::exception& e) { - reportError(tr("failed to update mod list: %1").arg(e.what())); - } -} - - -void MainWindow::removeOrigin(const QString &name) -{ - FilesOrigin &origin = m_DirectoryStructure->getOriginByName(ToWString(name)); - origin.enable(false); - refreshLists(); -} - - -void MainWindow::modorder_changed() -{ - for (unsigned int i = 0; i < m_CurrentProfile->numMods(); ++i) { - int priority = m_CurrentProfile->getModPriority(i); - if (m_CurrentProfile->modEnabled(i)) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(i); - m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())).setPriority(priority); - } - } - m_DirectoryStructure->getFileRegister()->sortOrigins(); -} - -void MainWindow::procError(QProcess::ProcessError error) -{ - reportError(tr("failed to spawn notepad.exe: %1").arg(error)); - this->sender()->deleteLater(); -} - -void MainWindow::procFinished(int, QProcess::ExitStatus) -{ - this->sender()->deleteLater(); -} - - -void MainWindow::on_profileRefreshBtn_clicked() -{ - m_CurrentProfile->writeModlist(); - -// m_ModList.updateModCollection(); - refreshModList(); -} - - -void MainWindow::showMessage(const QString &message) -{ - MessageDialog::showMessage(message, this); -} - - -void MainWindow::showError(const QString &message) -{ - reportError(message); -} - - -void MainWindow::installMod_clicked() -{ - installMod(); -} - - -void MainWindow::renameModInList(QFile &modList, const QString &oldName, const QString &newName) -{ - //TODO this code needs to be merged with ModList::readFrom - if (!modList.open(QIODevice::ReadWrite)) { - reportError(tr("failed to open %1").arg(modList.fileName())); - return; - } - - QBuffer outBuffer; - outBuffer.open(QIODevice::WriteOnly); - - while (!modList.atEnd()) { - QByteArray line = modList.readLine(); - if (line.length() == 0) { - break; - } - if (line.at(0) == '#') { - outBuffer.write(line); - continue; - } - QString modName = QString::fromUtf8(line.constData()).trimmed(); - if (modName.isEmpty()) { - break; - } - - bool disabled = false; - if (modName.at(0) == '-') { - disabled = true; - modName = modName.mid(1); - } else if (modName.at(0) == '+') { - modName = modName.mid(1); - } - - if (modName == oldName) { - modName = newName; - } - - if (disabled) { - outBuffer.write("-"); - } else { - outBuffer.write("+"); - } - outBuffer.write(modName.toUtf8().constData()); - outBuffer.write("\r\n"); - } - - modList.resize(0); - modList.write(outBuffer.buffer()); - modList.close(); -} - - -void MainWindow::modRenamed(const QString &oldName, const QString &newName) -{ - // fix the profiles directly on disc - for (int i = 0; i < ui->profileBox->count(); ++i) { - QString profileName = ui->profileBox->itemText(i); - - //TODO this functionality should be in the Profile class - QString modlistName = QString("%1/%2/modlist.txt") - .arg(QDir::fromNativeSeparators(ToQString(GameInfo::instance().getProfilesDir()))) - .arg(profileName); - - QFile modList(modlistName); - if (modList.exists()) { - qDebug("rewrite modlist %s", QDir::toNativeSeparators(modlistName).toUtf8().constData()); - renameModInList(modList, oldName, newName); - } - } - - // immediately refresh the active profile because the data in memory is invalid - m_CurrentProfile->refreshModStatus(); - - // also fix the directory structure - try { - if (m_DirectoryStructure->originExists(ToWString(oldName))) { - FilesOrigin &origin = m_DirectoryStructure->getOriginByName(ToWString(oldName)); - origin.setName(ToWString(newName)); - } else { - - } - } catch (const std::exception &e) { - reportError(tr("failed to change origin name: %1").arg(e.what())); - } -} - - -void MainWindow::addFilterItem(const QString &name, int categoryID) -{ - QListWidgetItem *item = new QListWidgetItem(name); - item->setData(Qt::UserRole, categoryID); - ui->categoriesList->addItem(item); -} - - -void MainWindow::refreshFilters() -{ - ui->modList->setCurrentIndex(QModelIndex()); - - // save previous filter text so we can restore it later, in case the filter still exists then - QListWidgetItem *currentItem = ui->categoriesList->currentItem(); - QString previousFilter = currentItem != NULL ? currentItem->text() : tr(""); - - ui->categoriesList->clear(); - addFilterItem(tr(""), CategoryFactory::CATEGORY_NONE); - addFilterItem(tr(""), CategoryFactory::CATEGORY_SPECIAL_CHECKED); - addFilterItem(tr(""), CategoryFactory::CATEGORY_SPECIAL_UNCHECKED); - addFilterItem(tr(""), CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE); - addFilterItem(tr(""), CategoryFactory::CATEGORY_SPECIAL_NOCATEGORY); - addFilterItem(tr(""), CategoryFactory::CATEGORY_SPECIAL_CONFLICT); - - for (unsigned int i = 1; i < m_CategoryFactory.numCategories(); ++i) { - bool categoryUsed = false; - int categoryId = m_CategoryFactory.getCategoryID(i); - for (unsigned int modIdx = 0; modIdx < ModInfo::getNumMods(); ++modIdx) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIdx); - if (modInfo->categorySet(categoryId)) { - categoryUsed = true; - } - } - - if (categoryUsed) { - addFilterItem(m_CategoryFactory.getCategoryName(i), categoryId); - } - } - - for (int i = 0; i < ui->categoriesList->count(); ++i) { - if (ui->categoriesList->item(i)->text() == previousFilter) { - ui->categoriesList->setCurrentRow(i); - break; - } - } -} - - -void MainWindow::renameMod_clicked() -{ - try { - QModelIndex treeIdx = m_ModListSortProxy->mapFromSource(m_ModList.index(m_ContextRow, 0)); - ui->modList->setCurrentIndex(treeIdx); - ui->modList->edit(treeIdx); - } catch (const std::exception &e) { - reportError(tr("failed to rename mod: %1").arg(e.what())); - } -} - - -void MainWindow::restoreBackup_clicked() -{ - QRegExp backupRegEx("(.*)_backup[0-9]*$"); - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - if (backupRegEx.indexIn(modInfo->name()) != -1) { - QString regName = backupRegEx.cap(1); - QDir modDir(QDir::fromNativeSeparators(m_Settings.getModDirectory())); - if (!modDir.exists(regName) || - (QMessageBox::question(this, tr("Overwrite?"), - tr("This will replace the existing mod \"%1\". Continue?").arg(regName), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { - if (modDir.exists(regName) && !removeDir(modDir.absoluteFilePath(regName))) { - reportError(tr("failed to remove mod \"%1\"").arg(regName)); - } else { - QString destinationPath = QDir::fromNativeSeparators(m_Settings.getModDirectory()) + "/" + regName; - if (!modDir.rename(modInfo->absolutePath(), destinationPath)) { - reportError(tr("failed to rename \"%1\" to \"%2\"").arg(modInfo->absolutePath()).arg(destinationPath)); - } - refreshModList(); - } - } - } -} - - -void MainWindow::removeMod_clicked() -{ - try { - QItemSelectionModel *selection = ui->modList->selectionModel(); - if (selection->hasSelection() && selection->selectedRows().count() > 1 ) { - QString mods; - QStringList modNames; - foreach (QModelIndex idx, selection->selectedRows()) { - QString name = ModInfo::getByIndex(m_ModListSortProxy->mapToSource(idx).row())->name(); - mods += "
  • " + name + "
  • "; - modNames.append(name); - } - if (QMessageBox::question(this, tr("Confirm"), - tr("Remove the following mods?
      %1
    ").arg(mods), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - // use mod names instead of indexes because those become invalid during the removal - foreach (QString name, modNames) { - m_ModList.removeRowForce(ModInfo::getIndex(name)); - } - } - } else { - m_ModList.removeRow(m_ContextRow, QModelIndex()); - } - } catch (const std::exception &e) { - reportError(tr("failed to remove mod: %1").arg(e.what())); - } -} - - -void MainWindow::reinstallMod_clicked() -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - QString installationFile = modInfo->getInstallationFile(); - if (installationFile.length() != 0) { - // there was a bug where mods installed through NCC had the absolute download path stored - if (QFileInfo(installationFile).isAbsolute()) { - installationFile = QFileInfo(installationFile).fileName(); - } - QString fullInstallationFile = m_DownloadManager.getOutputDirectory().append("/").append(installationFile); - if (QFile::exists(fullInstallationFile)) { - installMod(fullInstallationFile); - } else { - QMessageBox::information(this, tr("Failed"), tr("Installation file no longer exists")); - } - } else { - QMessageBox::information(this, tr("Failed"), - tr("Mods installed with old versions of MO can't be reinstalled in this way.")); - } -} - - -void MainWindow::endorseMod(ModInfo::Ptr mod) -{ - if (NexusInterface::instance()->getAccessManager()->loggedIn()) { - mod->endorse(true); - } else { - QString username, password; - if (m_Settings.getNexusLogin(username, password)) { - m_PostLoginTasks.push_back(boost::bind(&MainWindow::endorseMod, _1, mod)); - m_NexusDialog.login(username, password); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); - } - } -} - - -void MainWindow::endorse_clicked() -{ - endorseMod(ModInfo::getByIndex(m_ContextRow)); -} - - -void MainWindow::unendorse_clicked() -{ - QString username, password; - if (NexusInterface::instance()->getAccessManager()->loggedIn()) { - ModInfo::getByIndex(m_ContextRow)->endorse(false); - } else { - if (m_Settings.getNexusLogin(username, password)) { - m_PostLoginTasks.push_back(boost::mem_fn(&MainWindow::unendorse_clicked)); - m_NexusDialog.login(username, password); - } else { - MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); - } - } -} - - -void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int index, int tab) -{ - std::vector flags = modInfo->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) { - OverwriteInfoDialog dialog(modInfo, this); - dialog.exec(); - } else { - ModInfoDialog dialog(modInfo, m_DirectoryStructure, this); - connect(&dialog, SIGNAL(nexusLinkActivated(QString)), this, SLOT(nexusLinkActivated(QString))); - connect(&dialog, SIGNAL(downloadRequest(QString)), this, SLOT(downloadRequestedNXM(QString))); - connect(&dialog, SIGNAL(modOpen(QString, int)), this, SLOT(displayModInformation(QString, int))); - connect(&dialog, SIGNAL(originModified(int)), this, SLOT(originModified(int))); - connect(&dialog, SIGNAL(endorseMod(ModInfo::Ptr)), this, SLOT(endorseMod(ModInfo::Ptr))); - - dialog.openTab(tab); - dialog.exec(); - - modInfo->saveMeta(); - emit modInfoDisplayed(); - } - - if (m_CurrentProfile->modEnabled(index)) { - FilesOrigin& origin = m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())); - origin.enable(false); - - if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { - FilesOrigin& origin = m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())); - origin.enable(false); - -/* m_DirectoryStructure->addFromOrigin(ToWString(modInfo->name()), - ToWString(QDir::toNativeSeparators(modInfo->absolutePath())), - m_CurrentProfile->getModPriority(index));*/ - DirectoryRefresher::addModToStructure(m_DirectoryStructure, - modInfo->name(), m_CurrentProfile->getModPriority(index), - modInfo->absolutePath()); - DirectoryRefresher::cleanStructure(m_DirectoryStructure); - refreshLists(); - } - } -} - - -void MainWindow::displayModInformation(const QString &modName, int tab) -{ - unsigned int index = ModInfo::getIndex(modName); - if (index == UINT_MAX) { - qCritical("failed to resolve mod name %s", modName.toUtf8().constData()); - return; - } - - ModInfo::Ptr modInfo = ModInfo::getByIndex(index); - displayModInformation(modInfo, index, tab); -} - - -void MainWindow::displayModInformation(int row, int tab) -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(row); - displayModInformation(modInfo, row, 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, m_Settings.directInterface(), "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().toUtf8().constData(), - boost::bind(&MainWindow::extractProgress, this, boost::ref(progress), _1, _2)); - - 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::visitOnNexus_clicked() -{ - int modID = m_ModList.data(m_ModList.index(m_ContextRow, 0), Qt::UserRole).toInt(); - if (modID > 0) { - nexusLinkActivated(QString("%1/downloads/file.php?id=%2").arg(ToQString(GameInfo::instance().getNexusPage())).arg(modID)); - } else { - MessageDialog::showMessage(tr("Nexus ID for this Mod is unknown"), this); - } -} - -void MainWindow::openExplorer_clicked() -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - - ::ShellExecuteW(NULL, L"explore", ToWString(modInfo->absolutePath()).c_str(), NULL, NULL, SW_SHOWNORMAL); -} - - -void MainWindow::information_clicked() -{ - try { - displayModInformation(m_ContextRow); - } catch (const std::exception &e) { - reportError(e.what()); - } -} - - -void MainWindow::syncOverwrite() -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - SyncOverwriteDialog syncDialog(modInfo->absolutePath(), m_DirectoryStructure, this); - if (syncDialog.exec() == QDialog::Accepted) { - syncDialog.apply(QDir::fromNativeSeparators(m_Settings.getModDirectory())); - modInfo->testValid(); - refreshDirectoryStructure(); - } -} - - -void MainWindow::setPriorityMax() -{ - int newPriority = m_CurrentProfile->numMods() - 1; - m_CurrentProfile->setModPriority(m_ContextRow, newPriority); -} - - -void MainWindow::setPriorityManually() -{ -// m_CurrentProfile->setModPriority(m_ContextRow, m_CurrentProfile->numMods() - 1); - - int current = m_CurrentProfile->getModPriority(m_ContextRow); - int newPriority = QInputDialog::getInt(this, tr("Priority"), tr("Choose Priority"), current, 0, m_ModList.rowCount() - 1); - m_ModList.changeModPriority(m_ContextRow, newPriority); -// m_CurrentProfile->setModPriority(m_ContextRow, newPriority); -} - - -void MainWindow::setPriorityMin() -{ - int newPriority = 0; - m_CurrentProfile->setModPriority(m_ContextRow, newPriority); -} - - -void MainWindow::cancelModListEditor() -{ - ui->modList->setEnabled(false); - ui->modList->setEnabled(true); -} - - -void MainWindow::on_modList_doubleClicked(const QModelIndex &index) -{ - try { - displayModInformation(m_ModListSortProxy->mapToSource(index).row()); - // workaround to cancel the editor that might have opened because of - // selection-click - ui->modList->closePersistentEditor(index); - } catch (const std::exception &e) { - reportError(e.what()); - } -} - - -bool MainWindow::addCategories(QMenu *menu, int targetID) -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - const std::set &categories = modInfo->getCategories(); - - bool childEnabled = false; - - for (unsigned i = 1; i < m_CategoryFactory.numCategories(); ++i) { - if (m_CategoryFactory.getParentID(i) == targetID) { - QMenu *targetMenu = menu; - if (m_CategoryFactory.hasChildren(i)) { - targetMenu = menu->addMenu( - m_CategoryFactory.getCategoryName(i).replace('&', "&&")); - } - - int id = m_CategoryFactory.getCategoryID(i); - QScopedPointer checkBox(new QCheckBox(targetMenu)); - bool enabled = categories.find(id) != categories.end(); - checkBox->setText(m_CategoryFactory.getCategoryName(i).replace('&', "&&")); - if (enabled) { - childEnabled = true; - } - checkBox->setChecked(enabled ? Qt::Checked : Qt::Unchecked); - - QScopedPointer checkableAction(new QWidgetAction(targetMenu)); - checkableAction->setDefaultWidget(checkBox.take()); - checkableAction->setData(id); - targetMenu->addAction(checkableAction.take()); - - if (m_CategoryFactory.hasChildren(i)) { - if (addCategories(targetMenu, m_CategoryFactory.getCategoryID(i)) || enabled) { - targetMenu->setIcon(QIcon(":/MO/gui/resources/check.png")); - } - } - } - } - return childEnabled; -} - - -void MainWindow::saveCategoriesFromMenu(QMenu *menu, int modRow) -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(modRow); - foreach (QAction* action, menu->actions()) { - if (action->menu() != NULL) { - saveCategoriesFromMenu(action->menu(), modRow); - } else { - QWidgetAction *widgetAction = qobject_cast(action); - if (widgetAction != NULL) { - QCheckBox *checkbox = qobject_cast(widgetAction->defaultWidget()); - modInfo->setCategory(widgetAction->data().toInt(), checkbox->isChecked()); -// m_ModList.setModCategory(modRow, widgetAction->data().toInt(), checkbox->isChecked()); - } - } - } -} - - -void MainWindow::saveCategories() -{ - QMenu *menu = qobject_cast(sender()); - if (menu == NULL) { - qCritical("not a menu?"); - return; - } -// m_ModList.resetCategories(m_ContextRow); - saveCategoriesFromMenu(menu, m_ContextRow); - - refreshFilters(); -} - - -void MainWindow::savePrimaryCategory() -{ - QMenu *menu = qobject_cast(sender()); - if (menu == NULL) { - qCritical("not a menu?"); - return; - } - - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - foreach (QAction* action, menu->actions()) { - QWidgetAction *widgetAction = qobject_cast(action); - if (widgetAction != NULL) { - QRadioButton *btn = qobject_cast(widgetAction->defaultWidget()); - if (btn->isChecked()) { - modInfo->setPrimaryCategory(widgetAction->data().toInt()); - break; - } - } - } -} - - -void MainWindow::checkModsForUpdates() -{ - if (NexusInterface::instance()->getAccessManager()->loggedIn()) { - m_ModsToUpdate = ModInfo::checkAllForUpdate(this); - statusBar()->show(); - m_RefreshProgress->setRange(0, m_ModsToUpdate); - } else { - QString username, password; - if (m_Settings.getNexusLogin(username, password)) { - m_PostLoginTasks.push_back(boost::mem_fn(&MainWindow::checkModsForUpdates)); - m_NexusDialog.login(username, password); - } else { // otherwise there will be no endorsement info - m_ModsToUpdate = ModInfo::checkAllForUpdate(this); - } - } -} - - -void MainWindow::addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, ModInfo::Ptr info) -{ - const std::set &categories = info->getCategories(); - foreach (int categoryID, categories) { - int catIdx = m_CategoryFactory.getCategoryIndex(categoryID); - QWidgetAction *action = new QWidgetAction(primaryCategoryMenu); - try { - QRadioButton *categoryBox = new QRadioButton(m_CategoryFactory.getCategoryName(catIdx).replace('&', "&&"), - primaryCategoryMenu); - categoryBox->setChecked(categoryID == info->getPrimaryCategory()); - action->setDefaultWidget(categoryBox); - } catch (const std::exception &e) { - qCritical("failed to create category checkbox: %s", e.what()); - } - - action->setData(categoryID); - primaryCategoryMenu->addAction(action); - } -} - - -void MainWindow::addPrimaryCategoryCandidates() -{ - QMenu *menu = qobject_cast(sender()); - if (menu == NULL) { - qCritical("not a menu?"); - return; - } - menu->clear(); - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - - addPrimaryCategoryCandidates(menu, modInfo); -} - - -void MainWindow::enableVisibleMods() -{ - if (QMessageBox::question(NULL, tr("Confirm"), tr("Really enable all visible mods?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - m_ModListSortProxy->enableAllVisible(); - } -} - - -void MainWindow::disableVisibleMods() -{ - if (QMessageBox::question(NULL, tr("Confirm"), tr("Really disable all visible mods?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - m_ModListSortProxy->disableAllVisible(); - } -} - - -void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) -{ - try { - QTreeView *modList = findChild("modList"); - - m_ContextRow = m_ModListSortProxy->mapToSource(modList->indexAt(pos)).row(); - - QMenu menu; - menu.addAction(tr("Install Mod..."), this, SLOT(installMod_clicked())); - - 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("Refresh"), this, SLOT(on_profileRefreshBtn_clicked())); - - if (m_ContextRow != -1) { - menu.addSeparator(); - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - std::vector flags = info->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) { - menu.addAction(tr("Sync to Mods..."), this, SLOT(syncOverwrite())); - } 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())); - } else { - QMenu *addCategoryMenu = menu.addMenu(tr("Set Category")); - addCategories(addCategoryMenu, 0); - connect(addCategoryMenu, SIGNAL(aboutToHide()), this, SLOT(saveCategories())); - - QMenu *primaryCategoryMenu = menu.addMenu(tr("Primary Category")); - connect(primaryCategoryMenu, SIGNAL(aboutToShow()), this, SLOT(addPrimaryCategoryCandidates())); - connect(primaryCategoryMenu, SIGNAL(aboutToHide()), this, SLOT(savePrimaryCategory())); - - 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())); - } break; - case ModInfo::ENDORSED_FALSE: { - menu.addAction(tr("Endorse"), this, SLOT(endorse_clicked())); - } break; - default: { - QAction *action = new QAction(tr("Endorsement state unknown"), &menu); - action->setEnabled(false); - menu.addAction(action); - } break; - } - - 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); - } - - menu.exec(modList->mapToGlobal(pos)); - } catch (const std::exception &e) { - reportError(tr("Exception: ").arg(e.what())); - } catch (...) { - reportError(tr("Unknown exception")); - } -} - - -void MainWindow::on_categoriesList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *) -{ - if (current != NULL) { - m_ModListSortProxy->setCategoryFilter(current->data(Qt::UserRole).toInt()); - ui->currentCategoryLabel->setText(QString("(%1)").arg(current->text())); - } -} - - -void MainWindow::fixMods_clicked() -{ - QListWidgetItem *selectedItem = ui->savegameList->item(m_SelectedSaveGame); - if (selectedItem == NULL) { - return; - } - - // if required, parse the save game - if (selectedItem->data(Qt::UserRole).isNull()) { - QVariant temp; - SaveGameGamebryo *save = getSaveGame(selectedItem->data(Qt::UserRole).toString()); - save->setParent(selectedItem->listWidget()); - temp.setValue(save); - selectedItem->setData(Qt::UserRole, temp); - } - - const SaveGameGamebryo *save = qvariant_cast(selectedItem->data(Qt::UserRole)); - - // collect the list of missing plugins - std::map > missingPlugins; - - for (int i = 0; i < save->numPlugins(); ++i) { - const QString &pluginName = save->plugin(i); - if (!m_PluginList.isEnabled(pluginName)) { - missingPlugins[pluginName] = std::vector(); - } - } - - // figure out, for each esp/esm, which mod, if any, contains it - QStringList espFilter("*.esp"); - espFilter.append("*.esm"); - - { - QDir dataDir(m_GamePath + "/data"); - QStringList esps = dataDir.entryList(espFilter); - foreach (const QString &esp, esps) { - std::map >::iterator iter = missingPlugins.find(esp); - if (iter != missingPlugins.end()) { - iter->second.push_back(""); - } - } - } - - for (unsigned int i = 0; i < m_CurrentProfile->numRegularMods(); ++i) { - int modIndex = m_CurrentProfile->modIndexByPriority(i); - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - - QStringList esps = QDir(modInfo->absolutePath()).entryList(espFilter); - foreach (const QString &esp, esps) { - std::map >::iterator iter = missingPlugins.find(esp); - if (iter != missingPlugins.end()) { - iter->second.push_back(modInfo->name()); - } - } - } - - ActivateModsDialog dialog(missingPlugins, this); - if (dialog.exec() == QDialog::Accepted) { - // activate the required mods, then enable all esps - std::set modsToActivate = dialog.getModsToActivate(); - for (std::set::iterator iter = modsToActivate.begin(); iter != modsToActivate.end(); ++iter) { - if (*iter != "") { - unsigned int modIndex = ModInfo::getIndex(*iter); - m_CurrentProfile->setModEnabled(modIndex, true); - } - } - - m_CurrentProfile->writeModlist(); - refreshLists(); - - std::set espsToActivate = dialog.getESPsToActivate(); - for (std::set::iterator iter = espsToActivate.begin(); iter != espsToActivate.end(); ++iter) { - m_PluginList.enableESP(*iter); - } - saveCurrentLists(); - } -} - - -void MainWindow::on_savegameList_customContextMenuRequested(const QPoint &pos) -{ - QListWidgetItem *selectedItem = ui->savegameList->itemAt(pos); - if (selectedItem == NULL) { - return; - } - - m_SelectedSaveGame = ui->savegameList->row(selectedItem); - - QMenu menu; - menu.addAction(tr("Fix Mods..."), this, SLOT(fixMods_clicked())); - - menu.exec(ui->savegameList->mapToGlobal(pos)); -} - -void MainWindow::linkDesktop() -{ - QComboBox* executablesList = findChild("executablesListBox"); -// QPushButton *linkButton = findChild("linkDesktopButton"); - - const Executable &selectedExecutable = executablesList->itemData(executablesList->currentIndex()).value(); - QString linkName = getDesktopDirectory() + "\\" + selectedExecutable.m_Title + ".lnk"; - - if (QFile::exists(linkName)) { - if (QFile::remove(linkName)) { - ui->linkButton->menu()->actions().at(0)->setIcon(QIcon(":/MO/gui/link")); - } else { - reportError(tr("failed to remove %1").arg(linkName)); - } - } else { - QFileInfo exeInfo(m_ExeName); - // create link - std::wstring targetFile = ToWString(exeInfo.absoluteFilePath()); - std::wstring parameter = ToWString(QString("\"") + selectedExecutable.m_BinaryInfo.absoluteFilePath() + "\" " + 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) { - ui->linkButton->menu()->actions().at(0)->setIcon(QIcon(":/MO/gui/remove")); - } else { - reportError(tr("failed to create %1").arg(linkName)); - } - } -} - -void MainWindow::linkMenu() -{ - QComboBox* executablesList = findChild("executablesListBox"); - - const Executable &selectedExecutable = executablesList->itemData(executablesList->currentIndex()).value(); - QString linkName = getStartMenuDirectory() + "\\" + selectedExecutable.m_Title + ".lnk"; - - if (QFile::exists(linkName)) { - if (QFile::remove(linkName)) { - ui->linkButton->menu()->actions().at(1)->setIcon(QIcon(":/MO/gui/link")); - } else { - reportError(tr("failed to remove %1").arg(linkName)); - } - } else { - QFileInfo exeInfo(m_ExeName); - // create link - std::wstring targetFile = ToWString(exeInfo.absoluteFilePath()); - std::wstring parameter = ToWString(QString("\"") + selectedExecutable.m_BinaryInfo.absoluteFilePath() + "\" " + 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) { - ui->linkButton->menu()->actions().at(1)->setIcon(QIcon(":/MO/gui/remove")); - } else { - reportError(tr("failed to create %1").arg(linkName)); - } - } -} - -void MainWindow::on_actionSettings_triggered() -{ - QString oldModDirectory(m_Settings.getModDirectory()); - QString oldCacheDirectory(m_Settings.getCacheDirectory()); - m_Settings.query(this); - fixCategories(); - refreshFilters(); - if (QDir::fromNativeSeparators(m_DownloadManager.getOutputDirectory()) != QDir::fromNativeSeparators(m_Settings.getDownloadDirectory())) { - if (m_DownloadManager.downloadsInProgress()) { - MessageDialog::showMessage(tr("Can't change download directory while downloads are in progress!"), this); - } else { - m_DownloadManager.setOutputDirectory(m_Settings.getDownloadDirectory()); - } - } - - if (m_Settings.getModDirectory() != oldModDirectory) { - refreshModList(); - refreshLists(); - } - - if (m_Settings.getCacheDirectory() != oldCacheDirectory) { - NexusInterface::instance()->setCacheDirectory(m_Settings.getCacheDirectory()); - } - - NexusInterface::instance()->setNMMVersion(m_Settings.getNMMVersion()); -} - -void MainWindow::on_actionNexus_triggered() -{ - QString username, password; - m_NexusDialog.openUrl(ToQString(GameInfo::instance().getNexusPage())); - - if (m_Settings.getNexusLogin(username, password)) { - m_NexusDialog.login(username, password); - } else { - m_NexusDialog.loadNexus(); - } - m_NexusDialog.show(); - m_NexusDialog.activateWindow(); - - QTabWidget *tabWidget = findChild("tabWidget"); - tabWidget->setCurrentIndex(4); -} - - -void MainWindow::nexusLinkActivated(const QString &link) -{ - if (m_Settings.preferExternalBrowser()) { - ::ShellExecuteW(NULL, L"open", ToWString(link).c_str(), NULL, NULL, SW_SHOWNORMAL); - } else { - QString username, password; - m_NexusDialog.openUrl(link); - if (m_Settings.getNexusLogin(username, password)) { - m_NexusDialog.login(username, password); - m_LoginAttempted = true; - } else { - m_NexusDialog.loadNexus(); - } - m_NexusDialog.show(); - - QTabWidget *tabWidget = findChild("tabWidget"); - tabWidget->setCurrentIndex(4); - } -} - - -void MainWindow::downloadRequestedNXM(const QString &url) -{ - QString username, password; - - if (!m_LoginAttempted && !NexusInterface::instance()->getAccessManager()->loggedIn() && - (m_Settings.getNexusLogin(username, password) || - (m_AskForNexusPW && queryLogin(username, password)))) { - m_PendingDownloads.append(url); - NexusInterface::instance()->getAccessManager()->login(username, password); - m_LoginAttempted = true; - } else { - m_DownloadManager.addNXMDownload(url); - } -} - - -void MainWindow::downloadRequested(QNetworkReply *reply, int modID, const QString &fileName) -{ - try { - if (m_DownloadManager.addDownload(reply, fileName, modID)) { - MessageDialog::showMessage(tr("Download started"), this); - } - } catch (const std::exception &e) { - MessageDialog::showMessage(tr("Download failed"), this); - qCritical("exception starting download: %s", e.what()); - } -} - - -void MainWindow::languageChange(const QString &newLanguage) -{ - if (m_Translator != NULL) { - QCoreApplication::removeTranslator(m_Translator); - delete m_Translator; - m_Translator = NULL; - } - if (m_TranslatorQt != NULL) { - QCoreApplication::removeTranslator(m_TranslatorQt); - delete m_TranslatorQt; - m_TranslatorQt = NULL; - } - - if (newLanguage != "en_US") { - // add our own translations - m_Translator = new QTranslator(this); - QString locFile = ToQString(AppConfig::translationPrefix()) + "_" + newLanguage; - if (!m_Translator->load(locFile, QCoreApplication::applicationDirPath() + "/translations")) { - qDebug("localization %s not found", locFile.toUtf8().constData()); - } - QCoreApplication::installTranslator(m_Translator); - - // also add the translations for qt default strings - m_TranslatorQt = new QTranslator(this); - locFile = QString("qt_") + newLanguage; - if (!m_TranslatorQt->load(locFile, QCoreApplication::applicationDirPath() + "/translations")) { - qDebug("localization %s not found", locFile.toUtf8().constData()); - } - QCoreApplication::installTranslator(m_TranslatorQt); - } - ui->retranslateUi(this); - ui->profileBox->setItemText(0, QObject::tr("")); -// ui->toolBar->addWidget(createHelpWidget(ui->toolBar)); - - updateDownloadListDelegate(); - updateProblemsButton(); -} - - -void MainWindow::installDownload(int index) -{ - try { - QString fileName = m_DownloadManager.getFilePath(index); - int modID = m_DownloadManager.getModID(index); - QString modName; - // see if there already are mods with the specified mod id - if (modID != 0) { - ModInfo::Ptr modInfo = ModInfo::getByModID(modID, true); - if (!modInfo.isNull()) { - std::vector flags = modInfo->getFlags(); - if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) == flags.end()) { - modName = modInfo->name(); - modInfo->saveMeta(); - } - } - // TODO there may be multiple mods with the same id! -// modName = m_ModList.getModByModID(modID); - } - - m_CurrentProfile->writeModlistNow(); - - bool hasIniTweaks = false; - if (m_InstallationManager.install(fileName, m_CurrentProfile->getPluginsFileName(), m_Settings.getModDirectory(), m_Settings.preferIntegratedInstallers(), - m_Settings.enableQuickInstaller(), modName, hasIniTweaks)) { - MessageDialog::showMessage(tr("Installation successful"), this); - - refreshModList(); - - QModelIndexList posList = m_ModList.match(m_ModList.index(0, 0), Qt::DisplayRole, 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)); - } - - m_DownloadManager.markInstalled(index); - - emit modInstalled(); - } else if (m_InstallationManager.wasCancelled()) { - QMessageBox::information(this, tr("Installation cancelled"), tr("The mod was not installed completely."), QMessageBox::Ok); - } - } catch (const std::exception &e) { - reportError(e.what()); - } -} - - -void MainWindow::writeDataToFile(QFile &file, const QString &directory, const DirectoryEntry &directoryEntry) -{ - { // list files -// std::set::const_iterator current, end; -// directoryEntry.getFiles(current, end); -// for (; current != end; ++current) { - - std::vector files = directoryEntry.getFiles(); - for (auto iter = files.begin(); iter != files.end(); ++iter) { - FileEntry *current = *iter; - bool isArchive = false; - int origin = current->getOrigin(isArchive); - if (isArchive) { - // TODO: don't list files from archives. maybe make this an option? - continue; - } - QString fullName = directory; - fullName.append("\\").append(ToQString(current->getName())); - file.write(fullName.toUtf8()); - - file.write("\t("); - file.write(ToQString(m_DirectoryStructure->getOriginByID(origin).getName()).toUtf8()); - file.write(")\r\n"); - } - } - - { // recurse into subdirectories - std::vector::const_iterator current, end; - directoryEntry.getSubDirectories(current, end); - for (; current != end; ++current) { - writeDataToFile(file, directory.mid(0).append("\\").append(ToQString((*current)->getName())), **current); - } - } -} - - -void MainWindow::writeDataToFile() -{ - QString fileName = QFileDialog::getSaveFileName(this); - QFile file(fileName); - if (!file.open(QIODevice::WriteOnly)) { - reportError(tr("failed to write to file %1").arg(fileName)); - } - - writeDataToFile(file, "data", *m_DirectoryStructure); - file.close(); - - MessageDialog::showMessage(tr("%1 written").arg(QDir::toNativeSeparators(fileName)), this); -} - - -int MainWindow::getBinaryExecuteInfo(const QFileInfo &targetInfo, - QFileInfo &binaryInfo, QString &arguments) -{ - QString extension = targetInfo.completeSuffix(); - if ((extension == "exe") || - (extension == "cmd") || - (extension == "com") || - (extension == "bat")) { - binaryInfo = QFileInfo("C:\\Windows\\System32\\cmd.exe"); - arguments = QString("/C \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - return 1; - } else if (extension == "jar") { - // types that need to be injected into - std::wstring targetPathW = ToWString(targetInfo.absoluteFilePath()); - QString binaryPath; - - { // try to find java automatically - WCHAR buffer[MAX_PATH]; - if (FindExecutableW(targetPathW.c_str(), NULL, buffer) > (HINSTANCE)32) { - DWORD binaryType = 0UL; - if (!::GetBinaryTypeW(targetPathW.c_str(), &binaryType)) { - qDebug("failed to determine binary type: %lu", ::GetLastError()); - } else if (binaryType == SCS_32BIT_BINARY) { - binaryPath = ToQString(buffer); - } - } - } - if (binaryPath.isEmpty() && (extension == "jar")) { - QSettings javaReg("HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment", QSettings::NativeFormat); - if (javaReg.contains("CurrentVersion")) { - QString currentVersion = javaReg.value("CurrentVersion").toString(); - binaryPath = javaReg.value(QString("%1/JavaHome").arg(currentVersion)).toString().append("\\bin\\javaw.exe"); - } - } - if (binaryPath.isEmpty()) { - binaryPath = QFileDialog::getOpenFileName(this, tr("Select binary"), QString(), tr("Binary") + " (*.exe)"); - } - if (binaryPath.isEmpty()) { - return 0; - } - binaryInfo = QFileInfo(binaryPath); - if (extension == "jar") { - arguments = QString("-jar \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - } else { - arguments = QString("\"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); - } - return 1; - } else { - return 2; - } -} - - -void MainWindow::addAsExecutable() -{ - if (m_ContextItem != NULL) { - QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString()); - QFileInfo binaryInfo; - QString arguments; - switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { - case 1: { - QString name = QInputDialog::getText(this, tr("Enter Name"), - tr("Please enter a name for the executable"), QLineEdit::Normal, - targetInfo.baseName()); - if (!name.isEmpty()) { - m_ExecutablesList.addExecutable(name, binaryInfo.absoluteFilePath(), - arguments, targetInfo.absolutePath(), - DEFAULT_STAY, QString()); - refreshExecutablesList(); - } - } break; - case 2: { - QMessageBox::information(this, tr("Not an executable"), tr("This is not a recognized executable.")); - } break; - default: { - // nop - } break; - } - } -} - - -void MainWindow::originModified(int originID) -{ - FilesOrigin &origin = m_DirectoryStructure->getOriginByID(originID); - origin.enable(false); - m_DirectoryStructure->addFromOrigin(origin.getName(), origin.getPath(), origin.getPriority()); - DirectoryRefresher::cleanStructure(m_DirectoryStructure); -} - - -void MainWindow::hideFile() -{ - QString oldName = m_ContextItem->data(0, Qt::UserRole).toString(); - QString newName = oldName + ModInfo::s_HiddenExt; - - if (QFileInfo(newName).exists()) { - if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a hidden version of this file. Replace it?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - if (!QFile(newName).remove()) { - QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); - return; - } - } else { - return; - } - } - - if (QFile::rename(oldName, newName)) { - originModified(m_ContextItem->data(1, Qt::UserRole + 1).toInt()); - refreshDataTree(); - } else { - reportError(tr("failed to rename \"%1\" to \"%2\"").arg(oldName).arg(QDir::toNativeSeparators(newName))); - } -} - - -void MainWindow::unhideFile() -{ - QString oldName = m_ContextItem->data(0, Qt::UserRole).toString(); - QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length()); - if (QFileInfo(newName).exists()) { - if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a visible version of this file. Replace it?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - if (!QFile(newName).remove()) { - QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); - return; - } - } else { - return; - } - } - if (QFile::rename(oldName, newName)) { - originModified(m_ContextItem->data(1, Qt::UserRole + 1).toInt()); - refreshDataTree(); - } else { - reportError(tr("failed to rename \"%1\" to \"%2\"").arg(QDir::toNativeSeparators(oldName)).arg(QDir::toNativeSeparators(newName))); - } -} - - -void MainWindow::openDataFile() -{ - if (m_ContextItem != NULL) { - QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString()); - QFileInfo binaryInfo; - QString arguments; - switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { - case 1: { - spawnBinaryDirect(binaryInfo, arguments, m_CurrentProfile->getName(), targetInfo.absolutePath(), ""); - } break; - case 2: { - ::ShellExecuteW(NULL, L"open", ToWString(targetInfo.absoluteFilePath()).c_str(), NULL, NULL, SW_SHOWNORMAL); - } break; - default: { - // nop - } break; - } - } -} - - -void MainWindow::updateAvailable() -{ - QToolBar *toolBar = findChild("toolBar"); - foreach (QAction *action, toolBar->actions()) { - if (action->text() == tr("Update")) { - action->setEnabled(true); - action->setToolTip(tr("Update available")); - break; - } - } -} - - -void MainWindow::motdReceived(const QString &motd) -{ - // don't show motd after 5 seconds, may be annoying. Hopefully the user's - // internet connection is faster next time - if (m_StartTime.secsTo(QTime::currentTime()) < 5) { - uint hash = qHash(motd); - if (hash != m_Settings.getMotDHash()) { - MotDDialog dialog(motd); - dialog.exec(); - m_Settings.setMotDHash(hash); - } - } - - ui->actionEndorseMO->setVisible(false); -} - - -void MainWindow::notEndorsedYet() -{ - ui->actionEndorseMO->setVisible(true); -} - - -void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos) -{ - QTreeWidget *dataTree = findChild("dataTree"); - m_ContextItem = dataTree->itemAt(pos.x(), pos.y()); - - QMenu menu; - if ((m_ContextItem != NULL) && (m_ContextItem->childCount() == 0)) { - menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile())); - menu.addAction(tr("Add as Executable"), this, SLOT(addAsExecutable())); - // offer to hide/unhide file, but not for files from archives - if (!m_ContextItem->data(0, Qt::UserRole + 1).toBool()) { - if (m_ContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) { - menu.addAction(tr("Un-Hide"), this, SLOT(unhideFile())); - } else { - menu.addAction(tr("Hide"), this, SLOT(hideFile())); - } - } - menu.addSeparator(); - } - menu.addAction(tr("Write To File..."), this, SLOT(writeDataToFile())); - menu.addAction(tr("Refresh"), this, SLOT(on_btnRefreshData_clicked())); - - menu.exec(dataTree->mapToGlobal(pos)); -} - -void MainWindow::on_conflictsCheckBox_toggled(bool) -{ - refreshDataTree(); -} - - -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; - } else { - m_Updater.startUpdate(); - } -} - - -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()); - } -} - - -void MainWindow::updateDownloadListDelegate() -{ - if (ui->compactBox->isChecked()) { - ui->downloadView->setItemDelegate(new DownloadListWidgetCompactDelegate(&m_DownloadManager, ui->downloadView)); - } else { - ui->downloadView->setItemDelegate(new DownloadListWidgetDelegate(&m_DownloadManager, ui->downloadView)); - } - - DownloadListSortProxy *sortProxy = new DownloadListSortProxy(&m_DownloadManager, ui->downloadView); - sortProxy->setSourceModel(new DownloadList(&m_DownloadManager, ui->downloadView)); - connect(ui->downloadFilterEdit, SIGNAL(textChanged(QString)), sortProxy, SLOT(updateFilter(QString))); - - ui->downloadView->setModel(sortProxy); - ui->downloadView->sortByColumn(1, Qt::AscendingOrder); - ui->downloadView->header()->resizeSections(QHeaderView::Fixed); -// ui->downloadView->setFirstColumnSpanned(0, QModelIndex(), true); - - connect(ui->downloadView->itemDelegate(), SIGNAL(installDownload(int)), this, SLOT(installDownload(int))); - connect(ui->downloadView->itemDelegate(), SIGNAL(queryInfo(int)), &m_DownloadManager, SLOT(queryInfo(int))); - connect(ui->downloadView->itemDelegate(), SIGNAL(removeDownload(int, bool)), &m_DownloadManager, SLOT(removeDownload(int, bool))); - 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(); -} - - -void MainWindow::modDetailsUpdated(bool) -{ - --m_ModsToUpdate; - if (m_ModsToUpdate == 0) { - statusBar()->hide(); - m_ModListSortProxy->setCategoryFilter(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE); - for (int i = 0; i < ui->categoriesList->count(); ++i) { - if (ui->categoriesList->item(i)->data(Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { - ui->categoriesList->setCurrentRow(i); - break; - } - } -// m_RefreshProgress->setVisible(false); - } else { - m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); - } -} - -void MainWindow::nxmUpdatesAvailable(const std::vector &modIDs, QVariant userData, QVariant resultData, int) -{ - m_ModsToUpdate -= modIDs.size(); - - QVariantList resultList = resultData.toList(); - for (auto iter = resultList.begin(); iter != resultList.end(); ++iter) { - QVariantMap result = iter->toMap(); - if (result["id"].toInt() == GameInfo::instance().getNexusModID()) { - if (!result["voted_by_user"].toBool()) { - ui->actionEndorseMO->setVisible(true); - } - } else { - ModInfo::Ptr info = ModInfo::getByModID(result["id"].toInt(), true); - if (!info.isNull()) { - info->setNewestVersion(VersionInfo(result["version"].toString())); - info->setNexusDescription(result["description"].toString()); - if (NexusInterface::instance()->getAccessManager()->loggedIn()) { - // don't use endorsement info if we're not logged in - info->setIsEndorsed(result["voted_by_user"].toBool()); - } - } - } - } - - if (m_ModsToUpdate <= 0) { - statusBar()->hide(); - m_ModListSortProxy->setCategoryFilter(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE); - for (int i = 0; i < ui->categoriesList->count(); ++i) { - if (ui->categoriesList->item(i)->data(Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { - ui->categoriesList->setCurrentRow(i); - break; - } - } - } else { - m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); - } -} - - -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::nxmRequestFailed(int modID, QVariant, int, const QString &errorString) -{ - if (modID == -1) { - // must be the update-check that failed - m_ModsToUpdate = 0; - statusBar()->hide(); - } - MessageDialog::showMessage(tr("Request to Nexus failed: %1").arg(errorString), this); -} - - -void MainWindow::loginSuccessful(bool necessary) -{ - if (necessary) { - MessageDialog::showMessage(tr("login successful"), this); - } - foreach (QString url, m_PendingDownloads) { - downloadRequestedNXM(url); - } - m_PendingDownloads.clear(); - foreach (auto task, m_PostLoginTasks) { - task(this); - } - - m_PostLoginTasks.clear(); -} - - -void MainWindow::loginSuccessfulUpdate(bool necessary) -{ - if (necessary) { - MessageDialog::showMessage(tr("login successful"), this); - } - m_Updater.startUpdate(); -} - - -void MainWindow::loginFailed(const QString &message) -{ - if (!m_PendingDownloads.isEmpty()) { - MessageDialog::showMessage(tr("login failed: %1. Trying to download anyway").arg(message), this); - foreach (QString url, m_PendingDownloads) { - downloadRequestedNXM(url); - } - } - m_PendingDownloads.clear(); -} - - -void MainWindow::loginFailedUpdate(const QString &message) -{ - MessageDialog::showMessage(tr("login failed: %1. You need to log-in with Nexus to update MO.").arg(message), this); -} - - -void MainWindow::windowTutorialFinished(const QString &windowName) -{ - m_Settings.directInterface().setValue(QString("CompletedWindowTutorials/") + windowName, true); -} - - -BSA::EErrorCode MainWindow::extractBSA(BSA::Archive &archive, BSA::Folder::Ptr folder, const QString &destination, - QProgressDialog &progress) -{ - QDir().mkdir(destination); - BSA::EErrorCode result = BSA::ERROR_NONE; - QString errorFile; - - for (unsigned int i = 0; i < folder->getNumFiles(); ++i) { - BSA::File::Ptr file = folder->getFile(i); - BSA::EErrorCode res = archive.extract(file, destination.toUtf8().constData()); - if (res != BSA::ERROR_NONE) { - reportError(tr("failed to read %1: %2").arg(file->getName().c_str()).arg(res)); - result = res; - } - progress.setLabelText(file->getName().c_str()); - progress.setValue(progress.value() + 1); - QCoreApplication::processEvents(); - if (progress.wasCanceled()) { - result = BSA::ERROR_CANCELED; - } - } - - if (result != BSA::ERROR_NONE) { - if (QMessageBox::critical(this, tr("Error"), tr("failed to extract %1 (errorcode %2)").arg(errorFile).arg(result), - QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) { - return result; - } - } - - for (unsigned int i = 0; i < folder->getNumSubFolders(); ++i) { - BSA::Folder::Ptr subFolder = folder->getSubFolder(i); - BSA::EErrorCode res = extractBSA(archive, subFolder, - destination.mid(0).append("/").append(subFolder->getName().c_str()), progress); - if (res != BSA::ERROR_NONE) { - return res; - } - } - return BSA::ERROR_NONE; -} - - -bool MainWindow::extractProgress(QProgressDialog &progress, int percentage, std::string fileName) -{ - progress.setLabelText(fileName.c_str()); - progress.setValue(percentage); - QCoreApplication::processEvents(); - return !progress.wasCanceled(); -} - - -void MainWindow::extractBSATriggered() -{ - QTreeWidgetItem *item = ui->bsaList->topLevelItem(m_ContextRow); - - QString targetFolder = FileDialogMemory::getExistingDirectory("extractBSA", this, tr("Extract BSA")); - - if (!targetFolder.isEmpty()) { - BSA::Archive archive; - QString originPath = QDir::fromNativeSeparators(ToQString(m_DirectoryStructure->getOriginByName(ToWString(item->text(1))).getPath())); - QString archivePath = QString("%1\\%2").arg(originPath).arg(item->text(0)); - - BSA::EErrorCode result = archive.read(archivePath.toLocal8Bit().constData()); - if ((result != BSA::ERROR_NONE) && (result != BSA::ERROR_INVALIDHASHES)) { - reportError(tr("failed to read %1: %2").arg(archivePath).arg(result)); - return; - } - - QProgressDialog progress(this); - progress.setMaximum(100); - progress.setValue(0); - progress.show(); - - archive.extractAll(QDir::toNativeSeparators(targetFolder).toUtf8().constData(), - boost::bind(&MainWindow::extractProgress, this, boost::ref(progress), _1, _2)); - - if (result == BSA::ERROR_INVALIDHASHES) { - reportError(tr("This archive contains invalid hashes. Some files may be broken.")); - } - } -} - - -void MainWindow::displayColumnSelection(const QPoint &pos) -{ - QMenu menu; - - // display a list of all headers as checkboxes - QAbstractItemModel *model = ui->modList->header()->model(); - for (int i = 0; i < model->columnCount(); ++i) { - QString columnName = model->headerData(i, Qt::Horizontal).toString(); - QCheckBox *checkBox = new QCheckBox(&menu); - checkBox->setText(columnName); - checkBox->setChecked(!ui->modList->header()->isSectionHidden(i)); - QWidgetAction *checkableAction = new QWidgetAction(&menu); - checkableAction->setDefaultWidget(checkBox); - menu.addAction(checkableAction); - } - menu.exec(pos); - - // view/hide columns depending on check-state - int i = 0; - foreach (const QAction *action, menu.actions()) { - const QWidgetAction *widgetAction = qobject_cast(action); - if (widgetAction != NULL) { - const QCheckBox *checkBox = qobject_cast(widgetAction->defaultWidget()); - if (checkBox != NULL) { - ui->modList->header()->setSectionHidden(i, !checkBox->isChecked()); - } - } - ++i; - } -} - - -void MainWindow::on_bsaList_customContextMenuRequested(const QPoint &pos) -{ - m_ContextRow = ui->bsaList->indexOfTopLevelItem(ui->bsaList->itemAt(pos)); - - QMenu menu; - menu.addAction(tr("Extract..."), this, SLOT(extractBSATriggered())); - - menu.exec(ui->bsaList->mapToGlobal(pos)); -} - -void MainWindow::on_bsaList_itemChanged(QTreeWidgetItem*, int) -{ - checkBSAList(); -} - -void MainWindow::on_actionProblems_triggered() -{ - QString problemDescription; - checkForProblems(problemDescription); - QMessageBox::information(this, tr("Problems"), problemDescription); -} - -void MainWindow::setCategoryListVisible(bool visible) -{ - if (visible) { - ui->categoriesGroup->show(); - ui->displayCategoriesBtn->setText(ToQString(L"\u00ab")); - } else { - ui->categoriesGroup->hide(); - ui->displayCategoriesBtn->setText(ToQString(L"\u00bb")); - } -} - -void MainWindow::on_displayCategoriesBtn_toggled(bool checked) -{ - setCategoryListVisible(checked); -} - -void MainWindow::editCategories() -{ - CategoriesDialog dialog(this); - if (dialog.exec() == QDialog::Accepted) { - dialog.commitChanges(); - } -} - -void MainWindow::on_categoriesList_customContextMenuRequested(const QPoint &pos) -{ - QMenu menu; - menu.addAction(tr("Edit Categories..."), this, SLOT(editCategories())); - - menu.exec(ui->categoriesList->mapToGlobal(pos)); -} - - -void MainWindow::lockESPIndex() -{ - m_PluginList.lockESPIndex(m_ContextRow, true); -} - -void MainWindow::unlockESPIndex() -{ - m_PluginList.lockESPIndex(m_ContextRow, false); -} - - -void MainWindow::on_espList_customContextMenuRequested(const QPoint &pos) -{ - m_ContextRow = m_PluginListSortProxy->mapToSource(ui->espList->indexAt(pos)).row(); - - QMenu menu; - menu.addAction(tr("Enable all"), &m_PluginList, SLOT(enableAll())); - menu.addAction(tr("Disable all"), &m_PluginList, SLOT(disableAll())); - - if ((m_ContextRow != -1) && m_PluginList.isEnabled(m_ContextRow)) { - if (m_PluginList.isESPLocked(m_ContextRow)) { - menu.addAction(tr("Unlock index"), this, SLOT(unlockESPIndex())); - } else { - menu.addAction(tr("Lock index"), this, SLOT(lockESPIndex())); - } - } - - try { - menu.exec(ui->espList->mapToGlobal(pos)); - } catch (const std::exception &e) { - reportError(tr("Exception: ").arg(e.what())); - } catch (...) { - reportError(tr("Unknown exception")); - } -} +#include "mainwindow.h" +#include "ui_mainwindow.h" +#include +#include "spawn.h" +#include "report.h" +#include "modlist.h" +#include "profile.h" +#include "pluginlist.h" +#include "installdialog.h" +#include "profilesdialog.h" +#include "editexecutablesdialog.h" +#include "categories.h" +#include "categoriesdialog.h" +#include "utility.h" +#include "modinfodialog.h" +#include "overwriteinfodialog.h" +#include "activatemodsdialog.h" +#include "downloadlist.h" +#include "downloadlistwidget.h" +#include "downloadlistwidgetcompact.h" +#include "messagedialog.h" +#include "installationmanager.h" +#include "lockeddialog.h" +#include "syncoverwritedialog.h" +#include "logbuffer.h" +#include "downloadlistsortproxy.h" +#include "modlistsortproxy.h" +#include "motddialog.h" +#include "filedialogmemory.h" +#include "questionboxmemory.h" +#include "tutorialmanager.h" +#include "icondelegate.h" +#include "credentialsdialog.h" +#include "selectiondialog.h" +#include "csvbuilder.h" +#include "gameinfoimpl.h" +#include "savetextasdialog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + + +MainWindow::MainWindow(const QString &exeName, QSettings &initSettings, QWidget *parent) + : QMainWindow(parent), ui(new Ui::MainWindow), m_Tutorial(this, "MainWindow"), + m_ExeName(exeName), m_OldProfileIndex(-1), + m_DirectoryStructure(new DirectoryEntry(L"data", NULL, 0)), + m_ModList(NexusInterface::instance()), + m_OldExecutableIndex(-1), m_GamePath(ToQString(GameInfo::instance().getGameDirectory())), + m_NexusDialog(NexusInterface::instance()->getAccessManager(), NULL), + m_DownloadManager(NexusInterface::instance(), this), + m_InstallationManager(this), m_Translator(NULL), m_TranslatorQt(NULL), + m_Updater(NexusInterface::instance(), this), m_CategoryFactory(CategoryFactory::instance()), + m_CurrentProfile(NULL), m_AskForNexusPW(false), m_LoginAttempted(false), + m_ArchivesInit(false), m_ContextItem(NULL), m_CurrentSaveView(NULL), + m_GameInfo(new GameInfoImpl()) +{ + ui->setupUi(this); + + this->setWindowTitle(ToQString(GameInfo::instance().getGameName()).append(" Mod Organizer v").append(m_Updater.getVersion().canonicalString())); + + m_RefreshProgress = new QProgressBar(statusBar()); + m_RefreshProgress->setTextVisible(true); + m_RefreshProgress->setRange(0, 100); + m_RefreshProgress->setValue(0); + statusBar()->addWidget(m_RefreshProgress, 1000); + statusBar()->clearMessage(); + + ui->actionEndorseMO->setVisible(false); + + updateProblemsButton(); + + updateToolBar(); + ui->toolBar->blockSignals(true); + createHelpWidget(); + + ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure); + + // set up mod list + m_ModListSortProxy = new ModListSortProxy(m_CurrentProfile, this); + m_ModListSortProxy->setSourceModel(&m_ModList); + ui->modList->setModel(m_ModListSortProxy); + + ui->modList->sortByColumn(ModList::COL_PRIORITY, Qt::AscendingOrder); + ui->modList->setItemDelegateForColumn(ModList::COL_FLAGS, new IconDelegate(m_ModListSortProxy)); + ui->modList->header()->installEventFilter(&m_ModList); + ui->modList->header()->restoreState(initSettings.value("mod_list_state").toByteArray()); + ui->modList->installEventFilter(&m_ModList); + // restoreState also seems to restores the resize mode from previous session, + // I don't really like that +#if QT_VERSION >= 0x50000 + for (int i = 0; i < ui->modList->header()->count(); ++i) { + ui->modList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + ui->modList->header()->setSectionResizeMode(ModList::COL_NAME, QHeaderView::Stretch); +#else + for (int i = 0; i < ui->modList->header()->count(); ++i) { + ui->modList->header()->setResizeMode(i, QHeaderView::ResizeToContents); + } + ui->modList->header()->setResizeMode(ModList::COL_NAME, QHeaderView::Stretch); +#endif + + // set up plugin list + m_PluginListSortProxy = new PluginListSortProxy(this); + m_PluginListSortProxy->setSourceModel(&m_PluginList); + ui->espList->setModel(m_PluginListSortProxy); + ui->espList->sortByColumn(PluginList::COL_PRIORITY, Qt::AscendingOrder); + ui->espList->header()->restoreState(initSettings.value("plugin_list_state").toByteArray()); + ui->espList->installEventFilter(&m_PluginList); +#if QT_VERSION >= 0x50000 + for (int i = 0; i < ui->espList->header()->count(); ++i) { + ui->espList->header()->setSectionResizeMode(i, QHeaderView::ResizeToContents); + } + ui->espList->header()->setSectionResizeMode(0, QHeaderView::Stretch); +#else + for (int i = 0; i < ui->espList->header()->count(); ++i) { + ui->espList->header()->setResizeMode(i, QHeaderView::ResizeToContents); + } + ui->espList->header()->setResizeMode(0, QHeaderView::Stretch); +#endif + + QMenu *linkMenu = new QMenu(this); + linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Toolbar"), this, SLOT(linkToolbar())); + linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Desktop"), this, SLOT(linkDesktop())); + linkMenu->addAction(QIcon(":/MO/gui/link"), tr("Start Menu"), this, SLOT(linkMenu())); + ui->linkButton->setMenu(linkMenu); + + m_DownloadManager.setOutputDirectory(m_Settings.getDownloadDirectory()); + + NexusInterface::instance()->setCacheDirectory(m_Settings.getCacheDirectory()); + NexusInterface::instance()->setNMMVersion(m_Settings.getNMMVersion()); + + updateDownloadListDelegate(); + connect(&m_ModList, SIGNAL(requestColumnSelect(QPoint)), this, SLOT(displayColumnSelection(QPoint))); + + connect(&m_DownloadManager, SIGNAL(showMessage(QString)), this, SLOT(showMessage(QString))); + + connect(NexusInterface::instance(), SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); + connect(&m_NexusDialog, SIGNAL(requestDownload(QNetworkReply*, int, QString)), this, SLOT(downloadRequested(QNetworkReply*, int, QString))); + + ui->savegameList->installEventFilter(this); + ui->savegameList->setMouseTracking(true); + connect(ui->savegameList, SIGNAL(itemEntered(QListWidgetItem*)), + this, SLOT(saveSelectionChanged(QListWidgetItem*))); + + connect(&m_PluginList, SIGNAL(esplist_changed()), this, SLOT(esplist_changed())); + connect(&m_ModList, SIGNAL(modorder_changed()), this, SLOT(modorder_changed())); + connect(&m_ModList, SIGNAL(removeOrigin(QString)), this, SLOT(removeOrigin(QString))); + connect(&m_ModList, SIGNAL(showMessage(QString)), this, SLOT(showMessage(QString))); + connect(&m_ModList, SIGNAL(modRenamed(QString,QString)), this, SLOT(modRenamed(QString,QString))); + connect(ui->modFilterEdit, SIGNAL(textChanged(QString)), m_ModListSortProxy, SLOT(updateFilter(QString))); + connect(&m_ModList, SIGNAL(modlist_changed(int)), m_ModListSortProxy, SLOT(invalidate())); + connect(&m_ModList, SIGNAL(removeSelectedMods()), this, SLOT(removeMod_clicked())); + + connect(&m_DirectoryRefresher, SIGNAL(refreshed()), this, SLOT(directory_refreshed())); + connect(&m_DirectoryRefresher, SIGNAL(progress(int)), this, SLOT(refresher_progress(int))); + connect(&m_DirectoryRefresher, SIGNAL(error(QString)), this, SLOT(showError(QString))); + + connect(&m_Settings, SIGNAL(languageChanged(QString)), this, SLOT(languageChange(QString))); + connect(&m_Settings, SIGNAL(styleChanged(QString)), this, SIGNAL(styleChanged(QString))); + + connect(&m_Updater, SIGNAL(restart()), this, SLOT(close())); + connect(&m_Updater, SIGNAL(updateAvailable()), this, SLOT(updateAvailable())); + connect(&m_Updater, SIGNAL(motdAvailable(QString)), this, SLOT(motdReceived(QString))); + + connect(NexusInterface::instance()->getAccessManager(), SIGNAL(loginSuccessful(bool)), this, SLOT(loginSuccessful(bool))); + connect(NexusInterface::instance()->getAccessManager(), SIGNAL(loginFailed(QString)), this, SLOT(loginFailed(QString))); + + connect(&TutorialManager::instance(), SIGNAL(windowTutorialFinished(QString)), this, SLOT(windowTutorialFinished(QString))); + + 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); + + fixCategories(); + m_Updater.testForUpdate(); + m_StartTime = QTime::currentTime(); + + m_Tutorial.expose("modList", &m_ModList); + m_Tutorial.expose("espList", &m_PluginList); + loadPlugins(); +} + + +MainWindow::~MainWindow() +{ + m_RefresherThread.exit(); + m_RefresherThread.wait(); + m_NexusDialog.close(); + delete ui; + delete m_GameInfo; +} + + +void MainWindow::resizeEvent(QResizeEvent *event) +{ + m_Tutorial.resize(event->size()); + QMainWindow::resizeEvent(event); +} + + +void MainWindow::actionToToolButton(QAction *&sourceAction) +{ + QToolButton *button = new QToolButton(ui->toolBar); + button->setObjectName(sourceAction->objectName()); + button->setIcon(sourceAction->icon()); + button->setText(sourceAction->text()); + button->setPopupMode(QToolButton::InstantPopup); + button->setToolButtonStyle(ui->toolBar->toolButtonStyle()); + button->setToolTip(sourceAction->toolTip()); + button->setShortcut(sourceAction->shortcut()); + QMenu *buttonMenu = new QMenu(sourceAction->text()); + button->setMenu(buttonMenu); + QAction *newAction = ui->toolBar->insertWidget(sourceAction, button); + newAction->setObjectName(sourceAction->objectName()); + newAction->setIcon(sourceAction->icon()); + newAction->setText(sourceAction->text()); + newAction->setToolTip(sourceAction->toolTip()); + newAction->setShortcut(sourceAction->shortcut()); + ui->toolBar->removeAction(sourceAction); + sourceAction->deleteLater(); + sourceAction = newAction; +} + + +void MainWindow::updateToolBar() +{ + foreach (QAction *action, ui->toolBar->actions()) { + if (action->objectName().startsWith("custom__")) { + ui->toolBar->removeAction(action); + } + } + + QWidget *spacer = new QWidget(ui->toolBar); + spacer->setObjectName("custom__spacer"); + spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + QWidget *widget = ui->toolBar->widgetForAction(ui->actionTool); + QToolButton *toolBtn = qobject_cast(widget); + + if (toolBtn->menu() == NULL) { + actionToToolButton(ui->actionTool); + } + + actionToToolButton(ui->actionHelp); + + foreach (QAction *action, ui->toolBar->actions()) { + if (action->isSeparator()) { + // insert spacers + ui->toolBar->insertWidget(action, spacer); + + std::vector::iterator begin, end; + m_ExecutablesList.getExecutables(begin, end); + for (auto iter = begin; iter != end; ++iter) { + if (iter->m_Toolbar) { + QAction *exeAction = new QAction(iconForExecutable(iter->m_BinaryInfo.filePath()), + iter->m_Title, + ui->toolBar); + QVariant temp; + temp.setValue(*iter); + exeAction->setData(temp); + exeAction->setObjectName(QString("custom__") + iter->m_Title); + if (!connect(exeAction, SIGNAL(triggered()), this, SLOT(startExeAction()))) { + qDebug("failed to connect trigger?"); + } + ui->toolBar->insertAction(action, exeAction); + } + } + } + } +} + + +void MainWindow::updateProblemsButton() +{ + QString problemDescription; + if (checkForProblems(problemDescription)) { + ui->actionProblems->setEnabled(true); + ui->actionProblems->setIconText(tr("Problems")); + ui->actionProblems->setToolTip(tr("There are potential problems with your setup")); + } else { + ui->actionProblems->setIconText(tr("No Problems")); + ui->actionProblems->setToolTip(tr("Everything seems to be in order")); + } +} + + +bool MainWindow::errorReported(QString &logFile) +{ + QDir dir(ToQString(GameInfo::instance().getLogDir())); + QFileInfoList files = dir.entryInfoList(QStringList("ModOrganizer_??_??_??_??_??.log"), + QDir::Files, QDir::Name | QDir::Reversed); + + if (files.count() > 0) { + logFile = files.at(0).absoluteFilePath(); + QFile file(logFile); + if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { + char buffer[1024]; + int line = 0; + while (!file.atEnd()) { + file.readLine(buffer, 1024); + if (strncmp(buffer, "ERROR", 5) == 0) { + return true; + } + + // prevent this function from taking forever + if (line++ >= 50000) { + break; + } + } + } + } + + return false; +} + + +bool MainWindow::checkForProblems(QString &problemDescription) +{ + problemDescription = ""; + + foreach (IPluginDiagnose *diagnose, m_DiagnosisPlugins) { + std::vector activeProblems = diagnose->activeProblems(); + foreach (unsigned int key, activeProblems) { + problemDescription.append(tr("
  • %1
  • ").arg(diagnose->shortDescription(key))); + } + } + + QString NCCBinary = QCoreApplication::applicationDirPath().mid(0).append("/NCC/NexusClientCLI.exe"); + if (!QFile::exists(NCCBinary)) { + problemDescription.append(tr("
  • NCC not installed. You won't be able to install some scripted mod-installers. Get NCC from the MO page on nexus
  • ")); + } else { + VS_FIXEDFILEINFO versionInfo = GetFileVersion(ToWString(QDir::toNativeSeparators(NCCBinary))); + if ((versionInfo.dwFileVersionMS & 0xFFFF) != 0x02) { + problemDescription.append(tr("
  • NCC version may be incompatible.
  • ")); + } + } + + if (QSettings("HKEY_LOCAL_MACHINE\\Software\\Microsoft\\NET Framework Setup\\NDP\\v3.5", + QSettings::NativeFormat).value("Install", 0) != 1) { + QString dotNetUrl = "http://www.microsoft.com/en-us/download/details.aspx?id=17851"; + problemDescription.append(tr("
  • dotNet is not installed or outdated. This is required to use NCC. " + "Get it from here: %1
  • ").arg(dotNetUrl)); + } + + QString logFile; + if (errorReported(logFile)) { + problemDescription.append(tr("
  • There was an error reported in your last log. Please see %1
  • ").arg(logFile)); + } + + bool res = problemDescription.length() != 0; + if (res) { + problemDescription.prepend("
      ").append("
    "); + + ui->actionProblems->setEnabled(true); + ui->actionProblems->setIconText(tr("Problems")); + ui->actionProblems->setToolTip(tr("There are potential problems with your setup")); + } else { + ui->actionProblems->setToolTip(tr("Everything seems to be in order")); + } + return res; +} + + +void MainWindow::createHelpWidget() +{ + QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionHelp)); + QMenu *buttonMenu = toolBtn->menu(); + + QAction *helpAction = new QAction(tr("Help on UI"), buttonMenu); + connect(helpAction, SIGNAL(triggered()), this, SLOT(helpTriggered())); + buttonMenu->addAction(helpAction); + + QAction *wikiAction = new QAction(tr("Documentation Wiki"), buttonMenu); + connect(wikiAction, SIGNAL(triggered()), this, SLOT(wikiTriggered())); + buttonMenu->addAction(wikiAction); + + QAction *issueAction = new QAction(tr("Report Issue"), buttonMenu); + connect(issueAction, SIGNAL(triggered()), this, SLOT(issueTriggered())); + buttonMenu->addAction(issueAction); + + QMenu *tutorialMenu = new QMenu(tr("Tutorials"), buttonMenu); + + typedef std::vector > ActionList; + + ActionList tutorials; + QDirIterator dirIter("tutorials", QStringList("*.js"), QDir::Files); + while (dirIter.hasNext()) { + dirIter.next(); + QString fileName = dirIter.fileName(); + QFile file(dirIter.filePath()); + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open " << fileName; + continue; + } + QString firstLine = QString::fromUtf8(file.readLine()); + if (firstLine.startsWith("//TL")) { + QStringList params = firstLine.mid(4).trimmed().split('#'); + if (params.size() != 2) { + qCritical() << "invalid header line for tutorial " << fileName << " expected 2 parameters"; + continue; + } + QAction *tutAction = new QAction(params.at(0), tutorialMenu); + tutAction->setData(fileName); + tutorials.push_back(std::make_pair(params.at(1).toInt(), tutAction)); + } + } + + std::sort(tutorials.begin(), tutorials.end(), + [](const ActionList::value_type &LHS, const ActionList::value_type &RHS) { + return LHS.first < RHS.first; } ); + + for (auto iter = tutorials.begin(); iter != tutorials.end(); ++iter) { + connect(iter->second, SIGNAL(triggered()), this, SLOT(tutorialTriggered())); + tutorialMenu->addAction(iter->second); + } + + buttonMenu->addMenu(tutorialMenu); +} + + +bool MainWindow::saveCurrentLists() +{ + if (m_DirectoryUpdate) { + qWarning("not saving lists during directory update"); + return false; + } + + // save plugin list + + try { + m_PluginList.saveTo(m_CurrentProfile->getPluginsFileName(), + m_CurrentProfile->getLoadOrderFileName(), + m_CurrentProfile->getLockedOrderFileName(), + m_CurrentProfile->getDeleterFileName(), + m_Settings.hideUncheckedPlugins()); + + if (!m_PluginList.saveLoadOrder(*m_DirectoryStructure)) { + MessageDialog::showMessage(tr("load order could not be saved"), this); + } else { + ui->btnSave->setEnabled(false); + } + } catch (const std::exception &e) { + reportError(tr("failed to save load order: %1").arg(e.what())); + } + + if (m_ArchivesInit) { + QFile archiveFile(m_CurrentProfile->getArchivesFileName()); + if (archiveFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + for (int i = 0; i < ui->bsaList->topLevelItemCount(); ++i) { + QTreeWidgetItem *item = ui->bsaList->topLevelItem(i); + if ((item != NULL) && (item->checkState(0) == Qt::Checked)) { + archiveFile.write(item->text(0).toUtf8().append("\r\n")); + } + } + } else { + reportError(tr("failed to save archives order, do you have write access " + "to \"%1\"?").arg(m_CurrentProfile->getArchivesFileName())); + } + archiveFile.close(); + } else { + qWarning("archive list not initialised"); + } + return true; +} + + +bool MainWindow::addProfile() +{ + QComboBox *profileBox = findChild("profileBox"); + bool okClicked = false; + + QString name = QInputDialog::getText(this, tr("Name"), + tr("Please enter a name for the new profile"), + QLineEdit::Normal, QString(), &okClicked); + if (okClicked && (name.size() > 0)) { + try { + profileBox->addItem(name); + profileBox->setCurrentIndex(profileBox->count() - 1); + return true; + } catch (const std::exception& e) { + reportError(tr("failed to create profile: %1").arg(e.what())); + return false; + } + } else { + return false; + } +} + + +void MainWindow::hookUpWindowTutorials() +{ + QDirIterator dirIter("tutorials", QStringList("*.js"), QDir::Files); + while (dirIter.hasNext()) { + dirIter.next(); + QString fileName = dirIter.fileName(); + QFile file(dirIter.filePath()); + if (!file.open(QIODevice::ReadOnly)) { + qCritical() << "Failed to open " << fileName; + continue; + } + QString firstLine = QString::fromUtf8(file.readLine()); + if (firstLine.startsWith("//WIN")) { + QString windowName = firstLine.mid(6).trimmed(); + if (!m_Settings.directInterface().value("CompletedWindowTutorials/" + windowName, false).toBool()) { + TutorialManager::instance().activateTutorial(windowName, fileName); + } + } + } +} + + +void MainWindow::showEvent(QShowEvent *event) +{ + refreshFilters(); + + QMainWindow::showEvent(event); + m_Tutorial.registerControl(); + + hookUpWindowTutorials(); + + if (m_Settings.directInterface().value("first_start", true).toBool()) { + QString firstStepsTutorial = ToQString(AppConfig::firstStepsTutorial()); + if (TutorialManager::instance().hasTutorial(firstStepsTutorial)) { + if (QMessageBox::question(this, tr("Show tutorial?"), + tr("You are starting Mod Organizer for the first time. " + "Do you want to show a tutorial of its basic features? If you choose " + "no you can always start the tutorial from the \"Help\"-menu."), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + TutorialManager::instance().activateTutorial("MainWindow", firstStepsTutorial); + } + } else { + qCritical() << firstStepsTutorial << " missing"; + QPoint pos = ui->toolBar->mapToGlobal(QPoint()); + pos.rx() += ui->toolBar->width() / 2; + pos.ry() += ui->toolBar->height(); + QWhatsThis::showText(pos, + QObject::tr("Please use \"Help\" from the toolbar to get usage instructions to all elements")); + } + + m_Settings.directInterface().setValue("first_start", false); + } +} + + +void MainWindow::closeEvent(QCloseEvent* event) +{ + if (m_DownloadManager.downloadsInProgress() && + QMessageBox::question(this, tr("Downloads in progress"), + tr("There are still downloads in progress, do you really want to quit?"), + QMessageBox::Yes | QMessageBox::Cancel) == QMessageBox::Cancel) { + event->ignore(); + return; + } + + setCursor(Qt::WaitCursor); + + m_NexusDialog.close(); + + storeSettings(); + + // profile has to be cleaned up before the modinfo-buffer is cleared + delete m_CurrentProfile; + m_CurrentProfile = NULL; + + ModInfo::clear(); + LogBuffer::cleanQuit(); + m_ModList.setProfile(NULL); +} + + +void MainWindow::createFirstProfile() +{ + if (!refreshProfiles(false)) { + Profile newProf("Default", false); + refreshProfiles(false); + } +} + + +void MainWindow::setBrowserGeometry(const QByteArray &geometry) +{ + m_NexusDialog.restoreGeometry(geometry); +} + + +SaveGameGamebryo *MainWindow::getSaveGame(const QString &name) +{ + return new SaveGameGamebryo(this, name); +} + + +SaveGameGamebryo *MainWindow::getSaveGame(QListWidgetItem *item) +{ + try { + SaveGameGamebryo *saveGame = getSaveGame(item->data(Qt::UserRole).toString()); + saveGame->setParent(item->listWidget()); + return saveGame; + } catch (const std::exception &e) { + reportError(tr("failed to read savegame: %1").arg(e.what())); + return NULL; + } +} + + +void MainWindow::displaySaveGameInfo(const SaveGameGamebryo *save, QPoint pos) +{ + if (m_CurrentSaveView == NULL) { + m_CurrentSaveView = new SaveGameInfoWidgetGamebryo(save, &m_PluginList, this); + } else { + m_CurrentSaveView->setSave(save); + } + + QRect screenRect = QApplication::desktop()->availableGeometry(m_CurrentSaveView); + + if (pos.x() + m_CurrentSaveView->width() > screenRect.right()) { + pos.rx() -= (m_CurrentSaveView->width() + 2); + } else { + pos.rx() += 5; + } + + if (pos.y() + m_CurrentSaveView->height() > screenRect.bottom()) { + pos.ry() -= (m_CurrentSaveView->height() + 10); + } else { + pos.ry() += 20; + } + m_CurrentSaveView->move(pos); + + m_CurrentSaveView->show(); + ui->savegameList->activateWindow(); + connect(m_CurrentSaveView, SIGNAL(closeSaveInfo()), this, SLOT(hideSaveGameInfo())); +} + + +void MainWindow::saveSelectionChanged(QListWidgetItem *newItem) +{ + if (newItem == NULL) { + hideSaveGameInfo(); + } else if ((m_CurrentSaveView == NULL) || (newItem != m_CurrentSaveView->property("displayItem").value())) { + const SaveGameGamebryo *save = getSaveGame(newItem); + if (save != NULL) { + displaySaveGameInfo(save, QCursor::pos()); + m_CurrentSaveView->setProperty("displayItem", qVariantFromValue((void*)newItem)); + } + } +} + + + +void MainWindow::hideSaveGameInfo() +{ + if (m_CurrentSaveView != NULL) { + disconnect(m_CurrentSaveView, SIGNAL(closeSaveInfo()), this, SLOT(hideSaveGameInfo())); + m_CurrentSaveView->deleteLater(); + m_CurrentSaveView = NULL; + } +} + + +bool MainWindow::eventFilter(QObject* object, QEvent *event) +{ + if ((object == ui->savegameList) && + ((event->type() == QEvent::Leave) || (event->type() == QEvent::WindowDeactivate))) { + hideSaveGameInfo(); + } + + return false; +} + + +bool MainWindow::testForSteam() +{ + DWORD processIDs[1024]; + DWORD bytesReturned; + if (!::EnumProcesses(processIDs, sizeof(processIDs), &bytesReturned)) { + qWarning("failed to determine if steam is running"); + return true; + } + + TCHAR processName[MAX_PATH]; + for (unsigned int i = 0; i < bytesReturned / sizeof(DWORD); ++i) { + memset(processName, '\0', sizeof(TCHAR) * MAX_PATH); + if (processIDs[i] != 0) { + HANDLE process = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processIDs[i]); + + if (process != NULL) { + HMODULE module; + DWORD ignore; + + // first module in a process is always the binary + if (EnumProcessModules(process, &module, sizeof(HMODULE) * 1, &ignore)) { + GetModuleBaseName(process, module, processName, MAX_PATH); + if ((_tcsicmp(processName, TEXT("steam.exe")) == 0) || + (_tcsicmp(processName, TEXT("steamservice.exe")) == 0)) { + return true; + } + } + } + } + } + + return false; +} + + +bool MainWindow::verifyPlugin(IPlugin *plugin) +{ + if (plugin == NULL) { + return false; + } else if (!plugin->init(this)) { + qWarning("plugin failed to initialize"); + return false; + } + return true; +} + + +void MainWindow::toolPluginInvoke() +{ + QAction *triggeredAction = qobject_cast(sender()); + IPluginTool *plugin = (IPluginTool*)triggeredAction->data().value(); + try { + plugin->display(); + } catch (const std::exception &e) { + reportError(tr("Plugin \"%1\" failed: %2").arg(plugin->name()).arg(e.what())); + } +} + + +void MainWindow::registerPluginTool(IPluginTool *tool) +{ + QAction *action = new QAction(tool->icon(), tool->displayName(), ui->toolBar); + action->setToolTip(tool->tooltip()); + tool->setParentWidget(this); + action->setData(qVariantFromValue((void*)tool)); + connect(action, SIGNAL(triggered()), this, SLOT(toolPluginInvoke())); + QToolButton *toolBtn = qobject_cast(ui->toolBar->widgetForAction(ui->actionTool)); + toolBtn->menu()->addAction(action); +} + + +bool MainWindow::registerPlugin(QObject *plugin) +{ + { // generic treatment for all plugins + IPlugin *pluginObj = qobject_cast(plugin); + if (pluginObj == NULL) { + return false; + } + m_Settings.registerPlugin(pluginObj); + } + + { // diagnosis plugins + IPluginDiagnose *diagnose = qobject_cast(plugin); + if (diagnose != NULL) { + m_DiagnosisPlugins.push_back(diagnose); + } + } + + { // tool plugins + IPluginTool *tool = qobject_cast(plugin); + if (verifyPlugin(tool)) { + registerPluginTool(tool); + return true; + } + } + { // installer plugins + IPluginInstaller *installer = qobject_cast(plugin); + if (verifyPlugin(installer)) { + installer->setParentWidget(this); + m_InstallationManager.registerInstaller(installer); + return true; + } + } + return false; +} + + +void MainWindow::loadPlugins() +{ + m_DiagnosisPlugins.clear(); + + m_Settings.clearPlugins(); + + foreach (QObject *plugin, QPluginLoader::staticInstances()) { + registerPlugin(plugin); + } + + QString pluginPath = QDir::fromNativeSeparators(ToQString(GameInfo::instance().getOrganizerDirectory())) + "/" + ToQString(AppConfig::pluginPath()); + qDebug("looking for plugins in %s", pluginPath.toUtf8().constData()); + QDirIterator iter(pluginPath, QDir::Files | QDir::NoDotAndDotDot); + while (iter.hasNext()) { + iter.next(); + QString pluginName = iter.filePath(); + if (QLibrary::isLibrary(pluginName)) { + QPluginLoader pluginLoader(pluginName); + if (pluginLoader.instance() == NULL) { + qCritical("failed to load plugin %s: %s", + pluginName.toUtf8().constData(), pluginLoader.errorString().toUtf8().constData()); + } else { + if (registerPlugin(pluginLoader.instance())) { + qDebug("loaded plugin \"%s\"", pluginName.toUtf8().constData()); + } else { + qWarning("plugin \"%s\" failed to load", pluginName.toUtf8().constData()); + } + } + } + } +} + +IGameInfo &MainWindow::gameInfo() const +{ + return *m_GameInfo; +} + + +QString MainWindow::profileName() const +{ + return m_CurrentProfile->getName(); +} + +QString MainWindow::profilePath() const +{ + return m_CurrentProfile->getPath(); +} + +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(const QString &name) +{ + unsigned int index = ModInfo::getIndex(name); + if (index != UINT_MAX) { + throw MyException(tr("The mod \"%1\" already exists!").arg(name)); + } + + QString targetDirectory = QDir::fromNativeSeparators(m_Settings.getModDirectory()).append("/").append(name.trimmed()); + + QSettings settingsFile(targetDirectory.mid(0).append("/meta.ini"), QSettings::IniFormat); + + settingsFile.setValue("modid", 0); + settingsFile.setValue("version", 0); + settingsFile.setValue("newestVersion", 0); + settingsFile.setValue("category", 0); + settingsFile.setValue("installationFile", 0); + return ModInfo::createFrom(QDir(targetDirectory), &m_DirectoryStructure).data(); +} + +bool MainWindow::removeMod(IModInterface *mod) +{ + return ModInfo::removeMod(ModInfo::getIndex(mod->name())); +} + + +void MainWindow::modDataChanged(IModInterface*) +{ + refreshModList(); +} + +QVariant MainWindow::pluginSetting(const QString &pluginName, const QString &key) const +{ + return m_Settings.pluginSetting(pluginName, key); +} + + +void MainWindow::startSteam() +{ + QSettings steamSettings("HKEY_CURRENT_USER\\Software\\Valve\\Steam", + QSettings::NativeFormat); + QString exe = steamSettings.value("SteamExe", "").toString(); + if (!exe.isEmpty()) { + QString temp = QString("\"%1\"").arg(exe); + if (!QProcess::startDetached(temp)) { + reportError(tr("Failed to start \"%1\"").arg(temp)); + } else { + QMessageBox::information(this, tr("Waiting"), tr("Please press OK once you're logged into steam.")); + } + } +} + + +HANDLE MainWindow::spawnBinaryDirect(const QFileInfo &binary, const QString &arguments, const QString &profileName, + const QDir ¤tDirectory, const QString &steamAppID) +{ + storeSettings(); + + if (!binary.exists()) { + reportError(tr("\"%1\" not found").arg(binary.fileName())); + return INVALID_HANDLE_VALUE; + } + + if (!steamAppID.isEmpty()) { + ::SetEnvironmentVariableW(L"SteamAPPId", ToWString(steamAppID).c_str()); + } else { + ::SetEnvironmentVariableW(L"SteamAPPId", ToWString(m_Settings.getSteamAppID()).c_str()); + } + + if ((GameInfo::instance().requiresSteam()) && + (m_Settings.getLoadMechanism() == LoadMechanism::LOAD_MODORGANIZER)) { + if (!testForSteam()) { + if (QuestionBoxMemory::query(this->isVisible() ? this : NULL, + m_Settings.directInterface(), "steamQuery", tr("Start Steam?"), + tr("Steam is required to be running already to correctly start the game. " + "Should MO try to start steam now?"), + QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::Yes) { + startSteam(); + } + } + } + + return startBinary(binary, arguments, profileName, m_Settings.logLevel(), currentDirectory, true); +} + + +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) +{ + storeSettings(); + + HANDLE processHandle = spawnBinaryDirect(binary, arguments, m_CurrentProfile->getName(), currentDirectory, steamAppID); + if (processHandle != INVALID_HANDLE_VALUE) { + if (closeAfterStart) { + close(); + } else { + this->setEnabled(false); + + LockedDialog *dialog = new LockedDialog(this); + dialog->show(); + + QCoreApplication::processEvents(); + + while ((::WaitForSingleObject(processHandle, 1000) == WAIT_TIMEOUT) && + !dialog->unlockClicked()) { + // keep processing events so the app doesn't appear dead + QCoreApplication::processEvents(); + } + + this->setEnabled(true); + refreshDirectoryStructure(); + refreshDataTree(); + if (GameInfo::instance().getLoadOrderMechanism() == GameInfo::TYPE_FILETIME) { + QFile::remove(m_CurrentProfile->getLoadOrderFileName()); + } + refreshLists(); + dialog->hide(); + } + } +} + + +void MainWindow::startExeAction() +{ + QAction *action = qobject_cast(sender()); + if (action != NULL) { + Executable selectedExecutable = action->data().value(); + spawnBinary(selectedExecutable.m_BinaryInfo, + selectedExecutable.m_Arguments, + selectedExecutable.m_WorkingDirectory.length() != 0 ? selectedExecutable.m_WorkingDirectory + : selectedExecutable.m_BinaryInfo.absolutePath(), + selectedExecutable.m_CloseMO == DEFAULT_CLOSE, + selectedExecutable.m_SteamAppID); + } else { + qCritical("not an action?"); + } +} + + +void MainWindow::refreshModList() +{ + ModInfo::updateFromDisc(m_Settings.getModDirectory(), &m_DirectoryStructure); + m_CurrentProfile->refreshModStatus(); + + m_ModList.notifyChange(-1); + + refreshDirectoryStructure(); +} + + +void MainWindow::setExecutablesList(const ExecutablesList &executablesList) +{ + m_ExecutablesList = executablesList; + refreshExecutablesList(); + updateToolBar(); +} + +void MainWindow::setExecutableIndex(int index) +{ + QComboBox *executableBox = findChild("executablesListBox"); + + if ((index != 0) && (executableBox->count() > index)) { + executableBox->setCurrentIndex(index); + } else { + executableBox->setCurrentIndex(1); + } + + const Executable &selectedExecutable = executableBox->itemData(executableBox->currentIndex()).value(); + + QIcon addIcon(":/MO/gui/link"); + QIcon removeIcon(":/MO/gui/remove"); + + QFileInfo linkDesktopFile(QDir::fromNativeSeparators(getDesktopDirectory()) + "/" + selectedExecutable.m_Title + ".lnk"); + QFileInfo linkMenuFile(QDir::fromNativeSeparators(getStartMenuDirectory()) + "/" + selectedExecutable.m_Title + ".lnk"); + + ui->linkButton->menu()->actions().at(0)->setIcon(selectedExecutable.m_Toolbar ? removeIcon : addIcon); + ui->linkButton->menu()->actions().at(1)->setIcon(linkDesktopFile.exists() ? removeIcon : addIcon); + ui->linkButton->menu()->actions().at(2)->setIcon(linkMenuFile.exists() ? removeIcon : addIcon); +} + + +void MainWindow::activateSelectedProfile() +{ + QString profileName = ui->profileBox->currentText(); + qDebug() << "activate profile " << profileName; + QString profileDir = QDir::fromNativeSeparators(ToQString(GameInfo::instance().getProfilesDir())) + .append("/").append(profileName); + delete m_CurrentProfile; + m_CurrentProfile = new Profile(QDir(profileDir)); + m_ModList.setProfile(m_CurrentProfile); + + m_ModListSortProxy->setProfile(m_CurrentProfile); + + connect(m_CurrentProfile, SIGNAL(modStatusChanged(uint)), this, SLOT(modStatusChanged(uint))); + + refreshModList(); + refreshSaveList(); +} + + +void MainWindow::on_profileBox_currentIndexChanged(int index) +{ + if (ui->profileBox->isEnabled()) { + int previousIndex = m_OldProfileIndex; + m_OldProfileIndex = index; + + if ((previousIndex != -1) && + (m_CurrentProfile != NULL) && + m_CurrentProfile->exists()) { + saveCurrentLists(); + } + + if (ui->profileBox->currentIndex() == 0) { + ui->profileBox->setCurrentIndex(previousIndex); + ProfilesDialog(m_GamePath).exec(); + while (!refreshProfiles()) { + ProfilesDialog(m_GamePath).exec(); + } + } else { + activateSelectedProfile(); + } + } +} + + +void MainWindow::updateTo(QTreeWidgetItem *subTree, const std::wstring &directorySoFar, const DirectoryEntry &directoryEntry, bool conflictsOnly) +{ + { + std::vector files = directoryEntry.getFiles(); + for (auto iter = files.begin(); iter != files.end(); ++iter) { + FileEntry *current = *iter; + if (conflictsOnly && (current->getAlternatives().size() == 0)) { + continue; + } + + QString fileName = ToQString(current->getName()); + QStringList columns(fileName); + bool isArchive = false; + int originID = current->getOrigin(isArchive); + QString source = ToQString(m_DirectoryStructure->getOriginByID(originID).getName()); + std::wstring archive = current->getArchive(); + if (archive.length() != 0) { + source.append(" (").append(ToQString(archive)).append(")"); + } + columns.append(source); + QTreeWidgetItem *fileChild = new QTreeWidgetItem(columns); + if (isArchive) { + QFont font = fileChild->font(0); + font.setItalic(true); + fileChild->setFont(0, font); + fileChild->setFont(1, font); + } else if (fileName.endsWith(ModInfo::s_HiddenExt)) { + QFont font = fileChild->font(0); + font.setStrikeOut(true); + fileChild->setFont(0, font); + fileChild->setFont(1, font); + } + fileChild->setData(0, Qt::UserRole, ToQString(current->getFullPath())); + fileChild->setData(0, Qt::UserRole + 1, isArchive); + fileChild->setData(1, Qt::UserRole, source); + fileChild->setData(1, Qt::UserRole + 1, originID); + + std::vector alternatives = current->getAlternatives(); + + if (!alternatives.empty()) { + std::wostringstream altString; + altString << ToWString(tr("Also in:
    ")); + for (std::vector::iterator altIter = alternatives.begin(); + altIter != alternatives.end(); ++altIter) { + if (altIter != alternatives.begin()) { + altString << " , "; + } + altString << "" << m_DirectoryStructure->getOriginByID(*altIter).getName() << ""; + } + fileChild->setToolTip(1, QString("%1").arg(ToQString(altString.str()))); + fileChild->setForeground(1, QBrush(Qt::red)); + } else { + fileChild->setToolTip(1, tr("No conflict")); + } + subTree->addChild(fileChild); + } + } + + std::wostringstream temp; + temp << directorySoFar << "\\" << directoryEntry.getName(); + { + std::vector::const_iterator current, end; + directoryEntry.getSubDirectories(current, end); + for (; current != end; ++current) { + QStringList columns(ToQString((*current)->getName())); + columns.append(""); + QTreeWidgetItem *directoryChild = new QTreeWidgetItem(columns); + updateTo(directoryChild, temp.str(), **current, conflictsOnly); + if (directoryChild->childCount() != 0) { + subTree->addChild(directoryChild); + } + } + } + subTree->sortChildren(0, Qt::AscendingOrder); +} + + +bool MainWindow::refreshProfiles(bool selectProfile) +{ + QComboBox* profileBox = findChild("profileBox"); + + QString currentProfileName = profileBox->currentText(); + + profileBox->blockSignals(true); + profileBox->clear(); + profileBox->addItem(QObject::tr("")); + + QDir profilesDir(ToQString(GameInfo::instance().getProfilesDir())); + profilesDir.setFilter(QDir::AllDirs | QDir::NoDotAndDotDot); + + QDirIterator profileIter(profilesDir); + + int newIndex = profileIter.hasNext() ? 1 : 0; + int currentIndex = 0; + while (profileIter.hasNext()) { + profileIter.next(); + ++currentIndex; + try { + profileBox->addItem(profileIter.fileName()); + if (currentProfileName == profileIter.fileName()) { + newIndex = currentIndex; + } + } catch (const std::runtime_error& error) { + reportError(QObject::tr("failed to parse profile %1: %2").arg(profileIter.fileName()).arg(error.what())); + } + } + + // now select one of the profiles, preferably the one that was selected before + profileBox->blockSignals(false); + + if (selectProfile) { + if (profileBox->count() > 1) { + if (currentProfileName.length() != 0) { + if ((newIndex != 0) && (profileBox->count() > newIndex)) { + profileBox->setCurrentIndex(newIndex); + } else { + profileBox->setCurrentIndex(1); + } + } + return true; + } else { + return false; + } + } else { + return profileBox->count() > 1; + } +} + + +void MainWindow::refreshDirectoryStructure() +{ + m_DirectoryUpdate = true; + std::vector > activeModList = m_CurrentProfile->getActiveMods(); + m_DirectoryRefresher.setMods(activeModList); + + statusBar()->show(); +// m_RefreshProgress->setVisible(true); + m_RefreshProgress->setRange(0, 100); + + QTimer::singleShot(0, &m_DirectoryRefresher, SLOT(refresh())); +} + + +QIcon MainWindow::iconForExecutable(const QString &filePath) +{ + HICON winIcon; + UINT res = ::ExtractIconExW(ToWString(filePath).c_str(), 0, &winIcon, NULL, 1); + if (res == 1) { + QIcon result = QIcon(QPixmap::fromWinHICON(winIcon)); + ::DestroyIcon(winIcon); + return result; + } else { + return QIcon(":/MO/gui/executable"); + } +} + + +void MainWindow::refreshExecutablesList() +{ + QComboBox* executablesList = findChild("executablesListBox"); + executablesList->setEnabled(false); + executablesList->clear(); + executablesList->addItem(tr("")); + + QAbstractItemModel *model = executablesList->model(); + + std::vector::const_iterator current, end; + m_ExecutablesList.getExecutables(current, end); + for(int i = 0; current != end; ++current, ++i) { + QVariant temp; + temp.setValue(*current); + QIcon icon = iconForExecutable(current->m_BinaryInfo.filePath()); + executablesList->addItem(icon, current->m_Title, temp); + model->setData(model->index(i, 0), QSize(0, executablesList->iconSize().height() + 4), Qt::SizeHintRole); + } + + setExecutableIndex(1); + executablesList->setEnabled(true); +} + + +void MainWindow::refreshDataTree() +{ + QCheckBox *conflictsBox = findChild("conflictsCheckBox"); + QTreeWidget *tree = findChild("dataTree"); + tree->clear(); + QStringList columns("data"); + columns.append(""); + QTreeWidgetItem *subTree = new QTreeWidgetItem(columns); + updateTo(subTree, L"", *m_DirectoryStructure, conflictsBox->isChecked()); + tree->insertTopLevelItem(0, subTree); + subTree->setExpanded(true); + tree->header()->resizeSection(0, 200); +} + + +void MainWindow::refreshSaveList() +{ + refreshLists(); + ui->savegameList->clear(); + + QDir savesDir; + if (m_CurrentProfile->localSavesEnabled()) { + savesDir.setPath(m_CurrentProfile->getPath() + "/saves"); + } else { + wchar_t path[MAX_PATH]; + ::GetPrivateProfileStringW(L"General", L"SLocalSavePath", L"Saves", + path, MAX_PATH, + (ToWString(m_CurrentProfile->getPath()) + L"\\" + GameInfo::instance().getIniFileNames().at(0)).c_str()); + savesDir.setPath(QDir::fromNativeSeparators(ToQString(GameInfo::instance().getDocumentsDir() + L"\\" + path))); + } + + QStringList filters; + filters << ToQString(GameInfo::instance().getSaveGameExtension()); + savesDir.setNameFilters(filters); + + QFileInfoList files = savesDir.entryInfoList(QDir::Files, QDir::Time); + + foreach (const QFileInfo &file, files) { + QListWidgetItem *item = new QListWidgetItem(file.fileName()); + item->setData(Qt::UserRole, file.absoluteFilePath()); + ui->savegameList->addItem(item); + } +} + + +void MainWindow::refreshLists() +{ + if (m_DirectoryStructure->isPopulated()) { + refreshESPList(); + refreshBSAList(); + } // no point in refreshing lists if no files have been added to the directory tree +} + + +void MainWindow::refreshESPList() +{ + m_CurrentProfile->writeModlist(); + + // clear list + m_PluginList.refresh(m_CurrentProfile->getName(), *m_DirectoryStructure, + m_CurrentProfile->getPluginsFileName(), + m_CurrentProfile->getLoadOrderFileName(), + m_CurrentProfile->getLockedOrderFileName()); + //m_PluginList.readFrom(m_CurrentProfile->getPluginsFileName()); + + findChild("btnSave")->setEnabled(false); +} + + +static bool BySortValue(const std::pair &LHS, const std::pair &RHS) +{ + return LHS.first < RHS.first; +} + + +void MainWindow::refreshBSAList() +{ + m_ArchivesInit = false; + ui->bsaList->clear(); +#if QT_VERSION >= 0x50000 + ui->bsaList->header()->setSectionResizeMode(QHeaderView::ResizeToContents); +#else + ui->bsaList->header()->setResizeMode(QHeaderView::ResizeToContents); +#endif + + m_DefaultArchives.clear(); + + wchar_t buffer[256]; + std::wstring iniFileName = ToWString(QDir::toNativeSeparators(m_CurrentProfile->getIniFileName())); + if (::GetPrivateProfileStringW(L"Archive", GameInfo::instance().archiveListKey().c_str(), + L"", buffer, 256, iniFileName.c_str()) != 0) { + m_DefaultArchives = ToQString(buffer).split(','); + } + + if (::GetPrivateProfileStringW(L"Archive", GameInfo::instance().archiveListKey().append(L"2").c_str(), + L"", buffer, 256, iniFileName.c_str()) != 0) { + m_DefaultArchives.append(ToQString(buffer).split(',')); + } + + for (int i = 0; i < m_DefaultArchives.count(); ++i) { + m_DefaultArchives[i] = m_DefaultArchives[i].trimmed(); + } + + 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 { + m_ActiveArchives = m_DefaultArchives; + } + + std::vector > items; + + std::vector files = m_DirectoryStructure->getFiles(); + for (auto iter = files.begin(); iter != files.end(); ++iter) { + FileEntry *current = *iter; + + QString filename = ToQString(current->getName().c_str()); + QString extension = filename.right(3).toLower(); + + if (extension == "bsa") { + if (filename.compare("update.bsa", Qt::CaseInsensitive) == 0) { + // hack: ignore update.bsa because it confuses people + continue; + } + int index = m_ActiveArchives.indexOf(filename); + QStringList strings(filename); + bool isArchive = false; + int origin = current->getOrigin(isArchive); + strings.append(ToQString(m_DirectoryStructure->getOriginByID(origin).getName())); + QTreeWidgetItem *newItem = new QTreeWidgetItem(strings); + newItem->setData(0, Qt::UserRole, index); + newItem->setData(1, Qt::UserRole, origin); + newItem->setFlags(newItem->flags() & ~Qt::ItemIsDropEnabled | 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); + } + + if (index < 0) index = 0; + UINT32 sortValue = ((m_DirectoryStructure->getOriginByID(origin).getPriority() & 0xFFFF) << 16) | (index & 0xFFFF); + items.push_back(std::make_pair(sortValue, newItem)); + } + } + + std::sort(items.begin(), items.end(), BySortValue); + + for (std::vector >::iterator iter = items.begin(); iter != items.end(); ++iter) { + ui->bsaList->addTopLevelItem(iter->second); + } + + checkBSAList(); + m_ArchivesInit = true; +} + + +void MainWindow::checkBSAList() +{ + ui->bsaList->blockSignals(true); + + bool warning = false; + + for (int i = 0; i < ui->bsaList->topLevelItemCount(); ++i) { + QTreeWidgetItem *item = ui->bsaList->topLevelItem(i); + QString filename = item->text(0); + item->setIcon(0, QIcon()); + item->setToolTip(0, QString()); + + if (item->checkState(0) == Qt::Unchecked) { + if (m_DefaultArchives.contains(filename)) { + item->setIcon(0, QIcon(":/MO/gui/resources/dialog-warning.png")); + item->setToolTip(0, tr("This bsa is enabled in the ini file so it may be required!")); + warning = true; + } 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)) { + item->setIcon(0, QIcon(":/MO/gui/resources/dialog-warning.png")); + 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!")); + warning = true; + } + } + } + } + + if (warning) { + ui->tabWidget->setTabIcon(1, QIcon(":/MO/gui/resources/dialog-warning.png")); + } else { + ui->tabWidget->setTabIcon(1, QIcon()); + } + + ui->bsaList->blockSignals(false); +} + + +void MainWindow::fixCategories() +{ + for (unsigned int i = 0; i < ModInfo::getNumMods(); ++i) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(i); + std::set categories = modInfo->getCategories(); + for (std::set::iterator iter = categories.begin(); + iter != categories.end(); ++iter) { + if (!m_CategoryFactory.categoryExists(*iter)) { + modInfo->setCategory(*iter, false); + } + } + } +} + + +void MainWindow::readSettings() +{ + QSettings settings(ToQString(GameInfo::instance().getIniFilename()), QSettings::IniFormat); + + if (settings.contains("window_geometry")) { + restoreGeometry(settings.value("window_geometry").toByteArray()); + } + + if (settings.contains("browser_geometry")) { + setBrowserGeometry(settings.value("browser_geometry").toByteArray()); + } + + bool filtersVisible = settings.value("filters_visible", false).toBool(); + setCategoryListVisible(filtersVisible); + ui->displayCategoriesBtn->setChecked(filtersVisible); + + + QString language = settings.value("Settings/language", QLocale::system().name()).toString(); + languageChange(language); + int selectedExecutable = settings.value("selected_executable").toInt(); + setExecutableIndex(selectedExecutable); +} + + +void MainWindow::storeSettings() +{ + if (m_CurrentProfile == NULL) { + return; + } + m_CurrentProfile->writeModlist(); + 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()); + settings.setValue("compact_downloads", ui->compactBox->isChecked()); + settings.setValue("ask_for_nexuspw", m_AskForNexusPW); + + settings.setValue("window_geometry", this->saveGeometry()); + settings.setValue("browser_geometry", m_NexusDialog.saveGeometry()); + + settings.setValue("filters_visible", ui->displayCategoriesBtn->isChecked()); + + settings.remove("customExecutables"); + settings.beginWriteArray("customExecutables"); + std::vector::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(); + + QComboBox *executableBox = findChild("executablesListBox"); + settings.setValue("selected_executable", executableBox->currentIndex()); +} + + +void MainWindow::on_btnRefreshData_clicked() +{ + if (!m_DirectoryUpdate) { + refreshDirectoryStructure(); + refreshDataTree(); + } else { + qDebug("directory update"); + } +} + +void MainWindow::on_tabWidget_currentChanged(int index) +{ + saveCurrentLists(); + if (index == 0) { + refreshESPList(); + } else if (index == 1) { + refreshBSAList(); + } else if (index == 2) { + refreshDataTree(); + } else if (index == 3) { + refreshSaveList(); + } else if (index == 4) { + ui->downloadView->scrollToBottom(); + } +} + + +static QString guessModName(const QString &fileName) +{ + return QFileInfo(fileName).baseName(); +} + + +void MainWindow::installMod(const QString &fileName) +{ + bool hasIniTweaks = false; + QString modName; + m_CurrentProfile->writeModlistNow(); + if (m_InstallationManager.install(fileName, m_CurrentProfile->getPluginsFileName(), m_Settings.getModDirectory(), m_Settings.preferIntegratedInstallers(), + m_Settings.enableQuickInstaller(), modName, hasIniTweaks)) { + MessageDialog::showMessage(tr("Installation successful"), this); + refreshModList(); + + QModelIndexList posList = m_ModList.match(m_ModList.index(0, 0), Qt::DisplayRole, 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); + } +} + + +void MainWindow::installMod() +{ + try { + QStringList extensions = m_InstallationManager.getSupportedExtensions(); + for (auto iter = extensions.begin(); iter != extensions.end(); ++iter) { + *iter = "*." + *iter; + } + + QString fileName = QFileDialog::getOpenFileName(this, + tr("Choose Mod"), QString(), + tr("Mod Archive").append(QString(" (%1)").arg(extensions.join(" ")))); + + if (fileName.length() == 0) { + return; + } else { + installMod(fileName); + } + } catch (const std::exception &e) { + reportError(e.what()); + } +} + + +void MainWindow::on_btnSave_clicked() +{ + saveCurrentLists(); +} + + +void MainWindow::on_startButton_clicked() +{ + QComboBox* executablesList = findChild("executablesListBox"); + + Executable selectedExecutable = executablesList->itemData(executablesList->currentIndex()).value(); + + spawnBinary(selectedExecutable.m_BinaryInfo, + selectedExecutable.m_Arguments, + selectedExecutable.m_WorkingDirectory.length() != 0 ? selectedExecutable.m_WorkingDirectory + : selectedExecutable.m_BinaryInfo.absolutePath(), + selectedExecutable.m_CloseMO == DEFAULT_CLOSE, + selectedExecutable.m_SteamAppID); +} + + +static HRESULT CreateShortcut(LPCWSTR targetFileName, LPCWSTR arguments, + LPCSTR linkFileName, LPCWSTR description, + LPCWSTR currentDirectory) +{ + HRESULT result = E_INVALIDARG; + if ((targetFileName != NULL) && (wcslen(targetFileName) > 0) && + (arguments != NULL) && + (linkFileName != NULL) && (strlen(linkFileName) > 0) && + (description != NULL) && + (currentDirectory != NULL)) { + + IShellLink* shellLink; + result = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER, + IID_IShellLink, (LPVOID*)&shellLink); + + if (!SUCCEEDED(result)) { + qCritical("failed to create IShellLink instance"); + return result; + } + if (!SUCCEEDED(result)) return result; + + result = shellLink->SetPath(targetFileName); + if (!SUCCEEDED(result)) { + qCritical("failed to set target path %ls", targetFileName); + shellLink->Release(); + return result; + } + result = shellLink->SetArguments(arguments); + if (!SUCCEEDED(result)) { + qCritical("failed to set arguments: %ls", arguments); + shellLink->Release(); + return result; + } + + if (wcslen(description) > 0) { + result = shellLink->SetDescription(description); + if (!SUCCEEDED(result)) { + qCritical("failed to set description: %ls", description); + shellLink->Release(); + return result; + } + } + + if (wcslen(currentDirectory) > 0) { + result = shellLink->SetWorkingDirectory(currentDirectory); + if (!SUCCEEDED(result)) { + qCritical("failed to set working directory: %ls", currentDirectory); + shellLink->Release(); + return result; + } + } + + IPersistFile* persistFile; + result = shellLink->QueryInterface(IID_IPersistFile, (LPVOID*)&persistFile); + if (SUCCEEDED(result)) { + wchar_t linkFileNameW[MAX_PATH]; + MultiByteToWideChar(CP_ACP, 0, linkFileName, -1, linkFileNameW, MAX_PATH); + result = persistFile->Save(linkFileNameW, TRUE); + persistFile->Release(); + } else { + qCritical("failed to create IPersistFile instance"); + } + + shellLink->Release(); + } + return result; +} + + +bool MainWindow::modifyExecutablesDialog() +{ + bool result = false; + try { + EditExecutablesDialog dialog(m_ExecutablesList); + if (dialog.exec() == QDialog::Accepted) { + m_ExecutablesList = dialog.getExecutablesList(); + result = true; + } + refreshExecutablesList(); + } catch (const std::exception &e) { + reportError(e.what()); + } + return result; +} + +void MainWindow::on_executablesListBox_currentIndexChanged(int index) +{ + QComboBox* executablesList = findChild("executablesListBox"); + + int previousIndex = m_OldExecutableIndex; + m_OldExecutableIndex = index; + + if (executablesList->isEnabled()) { + + if (executablesList->itemData(index).isNull()) { + if (modifyExecutablesDialog()) { + setExecutableIndex(previousIndex); +// executablesList->setCurrentIndex(previousIndex); + } + } else { + setExecutableIndex(index); + } + } +} + +void MainWindow::helpTriggered() +{ + QWhatsThis::enterWhatsThisMode(); +} + +void MainWindow::wikiTriggered() +{ +// ::ShellExecuteW(NULL, L"open", L"http://issue.tannin.eu/tbg/wiki/Modorganizer%3AMainPage", NULL, NULL, SW_SHOWNORMAL); + ::ShellExecuteW(NULL, L"open", L"http://wiki.step-project.com/Guide:Mod_Organizer", NULL, NULL, SW_SHOWNORMAL); +} + +void MainWindow::issueTriggered() +{ + ::ShellExecuteW(NULL, L"open", L"http://issue.tannin.eu/tbg", NULL, NULL, SW_SHOWNORMAL); +} + + +void MainWindow::tutorialTriggered() +{ + QAction *tutorialAction = qobject_cast(sender()); + if (tutorialAction != NULL) { + if (QMessageBox::question(this, tr("Start Tutorial?"), + tr("You're about to start a tutorial. For technical reasons it's not possible to end " + "the tutorial early. Continue?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + TutorialManager::instance().activateTutorial("MainWindow", tutorialAction->data().toString()); + } + } +} + + +void MainWindow::on_actionInstallMod_triggered() +{ + installMod(); +} + +void MainWindow::on_actionAdd_Profile_triggered() +{ + bool repeat = true; + while (repeat) { + ProfilesDialog profilesDialog(m_GamePath); + profilesDialog.exec(); + if (refreshProfiles() && !profilesDialog.failed()) { + repeat = false; + } + } +// addProfile(); +} + +void MainWindow::on_actionModify_Executables_triggered() +{ + if (modifyExecutablesDialog()) { + setExecutableIndex(m_OldExecutableIndex); + } +} + + +void MainWindow::setModListSorting(int index) +{ + Qt::SortOrder order = ((index & 0x01) != 0) ? Qt::DescendingOrder : Qt::AscendingOrder; + int column = index >> 1; + ui->modList->header()->setSortIndicator(column, order); +} + + +void MainWindow::setESPListSorting(int index) +{ + switch (index) { + case 0: { + ui->espList->header()->setSortIndicator(1, Qt::AscendingOrder); + } break; + case 1: { + ui->espList->header()->setSortIndicator(1, Qt::DescendingOrder); + } break; + case 2: { + ui->espList->header()->setSortIndicator(0, Qt::AscendingOrder); + } break; + case 3: { + ui->espList->header()->setSortIndicator(0, Qt::DescendingOrder); + } break; + } +} + + +void MainWindow::setCompactDownloads(bool compact) +{ + ui->compactBox->setChecked(compact); +} + + +bool MainWindow::queryLogin(QString &username, QString &password) +{ + CredentialsDialog dialog(this); + int res = dialog.exec(); + if (dialog.neverAsk()) { + m_AskForNexusPW = false; + } + if (res == QDialog::Accepted) { + username = dialog.username(); + password = dialog.password(); + if (dialog.store()) { + m_Settings.setNexusLogin(username, password); + } + return true; + } else { + return false; + } +} + + +bool MainWindow::setCurrentProfile(int index) +{ + QComboBox *profilesBox = findChild("profileBox"); + if (index >= profilesBox->count()) { + return false; + } else { + profilesBox->setCurrentIndex(index); + return true; + } +} + +bool MainWindow::setCurrentProfile(const QString &name) +{ + QComboBox *profilesBox = findChild("profileBox"); + for (int i = 0; i < profilesBox->count(); ++i) { + if (QString::compare(profilesBox->itemText(i), name, Qt::CaseInsensitive) == 0) { + profilesBox->setCurrentIndex(i); + return true; + } + } + // profile not valid + profilesBox->setCurrentIndex(1); + return false; +} + + +void MainWindow::esplist_changed() +{ + findChild("btnSave")->setEnabled(true); +} + + +void MainWindow::refresher_progress(int percent) +{ + m_RefreshProgress->setValue(percent); +} + + +void MainWindow::directory_refreshed() +{ + DirectoryEntry *newStructure = m_DirectoryRefresher.getDirectoryStructure(); + if (newStructure != NULL) { + delete m_DirectoryStructure; + m_DirectoryStructure = newStructure; + } else { + // TODO: don't know why this happens, this slot seems to get called twice with only one emit + return; + } + m_DirectoryUpdate = false; + refreshLists(); +// m_RefreshProgress->setVisible(false); + statusBar()->hide(); +} + + +void MainWindow::externalMessage(const QString &message) +{ + if (message.left(6).toLower() == "nxm://") { + MessageDialog::showMessage(tr("Download started"), this); + downloadRequestedNXM(message); + } +} + + +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_DirectoryStructure->addFromOrigin(ToWString(modInfo->name()), + ToWString(QDir::toNativeSeparators(modInfo->absolutePath())), + m_CurrentProfile->getModPriority(index));*/ + DirectoryRefresher::cleanStructure(m_DirectoryStructure); + } else { + if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { + FilesOrigin &origin = m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())); + origin.enable(false); + } + } + refreshLists(); + } catch (const std::exception& e) { + reportError(tr("failed to update mod list: %1").arg(e.what())); + } +} + + +void MainWindow::removeOrigin(const QString &name) +{ + FilesOrigin &origin = m_DirectoryStructure->getOriginByName(ToWString(name)); + origin.enable(false); + refreshLists(); +} + + +void MainWindow::modorder_changed() +{ + for (unsigned int i = 0; i < m_CurrentProfile->numMods(); ++i) { + int priority = m_CurrentProfile->getModPriority(i); + if (m_CurrentProfile->modEnabled(i)) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(i); + m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())).setPriority(priority); + } + } + m_DirectoryStructure->getFileRegister()->sortOrigins(); +} + +void MainWindow::procError(QProcess::ProcessError error) +{ + reportError(tr("failed to spawn notepad.exe: %1").arg(error)); + this->sender()->deleteLater(); +} + +void MainWindow::procFinished(int, QProcess::ExitStatus) +{ + this->sender()->deleteLater(); +} + + +void MainWindow::on_profileRefreshBtn_clicked() +{ + m_CurrentProfile->writeModlist(); + +// m_ModList.updateModCollection(); + refreshModList(); +} + + +void MainWindow::showMessage(const QString &message) +{ + MessageDialog::showMessage(message, this); +} + + +void MainWindow::showError(const QString &message) +{ + reportError(message); +} + + +void MainWindow::installMod_clicked() +{ + installMod(); +} + + +void MainWindow::renameModInList(QFile &modList, const QString &oldName, const QString &newName) +{ + //TODO this code needs to be merged with ModList::readFrom + if (!modList.open(QIODevice::ReadWrite)) { + reportError(tr("failed to open %1").arg(modList.fileName())); + return; + } + + QBuffer outBuffer; + outBuffer.open(QIODevice::WriteOnly); + + while (!modList.atEnd()) { + QByteArray line = modList.readLine(); + if (line.length() == 0) { + break; + } + if (line.at(0) == '#') { + outBuffer.write(line); + continue; + } + QString modName = QString::fromUtf8(line.constData()).trimmed(); + if (modName.isEmpty()) { + break; + } + + bool disabled = false; + if (modName.at(0) == '-') { + disabled = true; + modName = modName.mid(1); + } else if (modName.at(0) == '+') { + modName = modName.mid(1); + } + + if (modName == oldName) { + modName = newName; + } + + if (disabled) { + outBuffer.write("-"); + } else { + outBuffer.write("+"); + } + outBuffer.write(modName.toUtf8().constData()); + outBuffer.write("\r\n"); + } + + modList.resize(0); + modList.write(outBuffer.buffer()); + modList.close(); +} + + +void MainWindow::modRenamed(const QString &oldName, const QString &newName) +{ + // fix the profiles directly on disc + for (int i = 0; i < ui->profileBox->count(); ++i) { + QString profileName = ui->profileBox->itemText(i); + + //TODO this functionality should be in the Profile class + QString modlistName = QString("%1/%2/modlist.txt") + .arg(QDir::fromNativeSeparators(ToQString(GameInfo::instance().getProfilesDir()))) + .arg(profileName); + + QFile modList(modlistName); + if (modList.exists()) { + qDebug("rewrite modlist %s", QDir::toNativeSeparators(modlistName).toUtf8().constData()); + renameModInList(modList, oldName, newName); + } + } + + // immediately refresh the active profile because the data in memory is invalid + m_CurrentProfile->refreshModStatus(); + + // also fix the directory structure + try { + if (m_DirectoryStructure->originExists(ToWString(oldName))) { + FilesOrigin &origin = m_DirectoryStructure->getOriginByName(ToWString(oldName)); + origin.setName(ToWString(newName)); + } else { + + } + } catch (const std::exception &e) { + reportError(tr("failed to change origin name: %1").arg(e.what())); + } +} + + +QTreeWidgetItem *MainWindow::addFilterItem(QTreeWidgetItem *root, const QString &name, int categoryID) +{ + QTreeWidgetItem *item = new QTreeWidgetItem(QStringList(name)); + item->setData(0, Qt::UserRole, categoryID); + if (root != NULL) { + root->addChild(item); + } else { + ui->categoriesList->addTopLevelItem(item); + } + return item; +} + + +void MainWindow::addCategoryFilters(QTreeWidgetItem *root, const std::set &categoriesUsed, int targetID) +{ + for (unsigned i = 1; i < m_CategoryFactory.numCategories(); ++i) { + if ((m_CategoryFactory.getParentID(i) == targetID)) { + int categoryID = m_CategoryFactory.getCategoryID(i); + if (categoriesUsed.find(categoryID) != categoriesUsed.end()) { + QTreeWidgetItem *item = addFilterItem(root, m_CategoryFactory.getCategoryName(i), categoryID); + if (m_CategoryFactory.hasChildren(i)) { + addCategoryFilters(item, categoriesUsed, categoryID); + } + } + } + } +} + + +void MainWindow::refreshFilters() +{ + ui->modList->setCurrentIndex(QModelIndex()); + + // save previous filter text so we can restore it later, in case the filter still exists then + QTreeWidgetItem *currentItem = ui->categoriesList->currentItem(); + QString previousFilter = currentItem != NULL ? currentItem->text(0) : tr(""); + + ui->categoriesList->clear(); + addFilterItem(NULL, tr(""), CategoryFactory::CATEGORY_NONE); + addFilterItem(NULL, tr(""), CategoryFactory::CATEGORY_SPECIAL_CHECKED); + addFilterItem(NULL, tr(""), CategoryFactory::CATEGORY_SPECIAL_UNCHECKED); + addFilterItem(NULL, tr(""), CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE); + addFilterItem(NULL, tr(""), CategoryFactory::CATEGORY_SPECIAL_NOCATEGORY); + addFilterItem(NULL, tr(""), CategoryFactory::CATEGORY_SPECIAL_CONFLICT); + + std::set categoriesUsed; + for (unsigned int modIdx = 0; modIdx < ModInfo::getNumMods(); ++modIdx) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIdx); + BOOST_FOREACH (int categoryID, modInfo->getCategories()) { + int currentID = categoryID; + // also add parents so they show up in the tree + while (currentID != 0) { + categoriesUsed.insert(currentID); + currentID = m_CategoryFactory.getParentID(m_CategoryFactory.getCategoryIndex(currentID)); + } + } + } + + addCategoryFilters(NULL, categoriesUsed, 0); + + QList matches = ui->categoriesList->findItems(previousFilter, Qt::MatchFixedString | Qt::MatchRecursive); + if (matches.size() > 0) { + QTreeWidgetItem *currentItem = matches.at(0); + ui->categoriesList->setCurrentItem(currentItem); + while (currentItem != NULL) { + currentItem->setExpanded(true); + currentItem = currentItem->parent(); + } + } +} + + +void MainWindow::renameMod_clicked() +{ + try { + QModelIndex treeIdx = m_ModListSortProxy->mapFromSource(m_ModList.index(m_ContextRow, 0)); + ui->modList->setCurrentIndex(treeIdx); + ui->modList->edit(treeIdx); + } catch (const std::exception &e) { + reportError(tr("failed to rename mod: %1").arg(e.what())); + } +} + + +void MainWindow::restoreBackup_clicked() +{ + QRegExp backupRegEx("(.*)_backup[0-9]*$"); + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + if (backupRegEx.indexIn(modInfo->name()) != -1) { + QString regName = backupRegEx.cap(1); + QDir modDir(QDir::fromNativeSeparators(m_Settings.getModDirectory())); + if (!modDir.exists(regName) || + (QMessageBox::question(this, tr("Overwrite?"), + tr("This will replace the existing mod \"%1\". Continue?").arg(regName), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes)) { + if (modDir.exists(regName) && !removeDir(modDir.absoluteFilePath(regName))) { + reportError(tr("failed to remove mod \"%1\"").arg(regName)); + } else { + QString destinationPath = QDir::fromNativeSeparators(m_Settings.getModDirectory()) + "/" + regName; + if (!modDir.rename(modInfo->absolutePath(), destinationPath)) { + reportError(tr("failed to rename \"%1\" to \"%2\"").arg(modInfo->absolutePath()).arg(destinationPath)); + } + refreshModList(); + } + } + } +} + + +void MainWindow::removeMod_clicked() +{ + try { + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1 ) { + QString mods; + QStringList modNames; + foreach (QModelIndex idx, selection->selectedRows()) { + QString name = ModInfo::getByIndex(m_ModListSortProxy->mapToSource(idx).row())->name(); + mods += "
  • " + name + "
  • "; + modNames.append(name); + } + if (QMessageBox::question(this, tr("Confirm"), + tr("Remove the following mods?
      %1
    ").arg(mods), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + // use mod names instead of indexes because those become invalid during the removal + foreach (QString name, modNames) { + m_ModList.removeRowForce(ModInfo::getIndex(name)); + } + } + } else { + m_ModList.removeRow(m_ContextRow, QModelIndex()); + } + } catch (const std::exception &e) { + reportError(tr("failed to remove mod: %1").arg(e.what())); + } +} + + +void MainWindow::reinstallMod_clicked() +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + QString installationFile = modInfo->getInstallationFile(); + if (installationFile.length() != 0) { + // there was a bug where mods installed through NCC had the absolute download path stored + if (QFileInfo(installationFile).isAbsolute()) { + installationFile = QFileInfo(installationFile).fileName(); + } + QString fullInstallationFile = m_DownloadManager.getOutputDirectory().append("/").append(installationFile); + if (QFile::exists(fullInstallationFile)) { + installMod(fullInstallationFile); + } else { + QMessageBox::information(this, tr("Failed"), tr("Installation file no longer exists")); + } + } else { + QMessageBox::information(this, tr("Failed"), + tr("Mods installed with old versions of MO can't be reinstalled in this way.")); + } +} + + +void MainWindow::endorseMod(ModInfo::Ptr mod) +{ + if (NexusInterface::instance()->getAccessManager()->loggedIn()) { + mod->endorse(true); + } else { + QString username, password; + if (m_Settings.getNexusLogin(username, password)) { + m_PostLoginTasks.push_back(boost::bind(&MainWindow::endorseMod, _1, mod)); + m_NexusDialog.login(username, password); + } else { + MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); + } + } +} + + +void MainWindow::endorse_clicked() +{ + endorseMod(ModInfo::getByIndex(m_ContextRow)); +} + + +void MainWindow::unendorse_clicked() +{ + QString username, password; + if (NexusInterface::instance()->getAccessManager()->loggedIn()) { + ModInfo::getByIndex(m_ContextRow)->endorse(false); + } else { + if (m_Settings.getNexusLogin(username, password)) { + m_PostLoginTasks.push_back(boost::mem_fn(&MainWindow::unendorse_clicked)); + m_NexusDialog.login(username, password); + } else { + MessageDialog::showMessage(tr("You need to be logged in with Nexus to endorse"), this); + } + } +} + + +void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int index, int tab) +{ + std::vector flags = modInfo->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) { + OverwriteInfoDialog dialog(modInfo, this); + dialog.exec(); + } else { + ModInfoDialog dialog(modInfo, m_DirectoryStructure, this); + connect(&dialog, SIGNAL(nexusLinkActivated(QString)), this, SLOT(nexusLinkActivated(QString))); + connect(&dialog, SIGNAL(downloadRequest(QString)), this, SLOT(downloadRequestedNXM(QString))); + connect(&dialog, SIGNAL(modOpen(QString, int)), this, SLOT(displayModInformation(QString, int))); + connect(&dialog, SIGNAL(originModified(int)), this, SLOT(originModified(int))); + connect(&dialog, SIGNAL(endorseMod(ModInfo::Ptr)), this, SLOT(endorseMod(ModInfo::Ptr))); + + dialog.openTab(tab); + dialog.exec(); + + modInfo->saveMeta(); + emit modInfoDisplayed(); + } + + if (m_CurrentProfile->modEnabled(index)) { + FilesOrigin& origin = m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())); + origin.enable(false); + + if (m_DirectoryStructure->originExists(ToWString(modInfo->name()))) { + FilesOrigin& origin = m_DirectoryStructure->getOriginByName(ToWString(modInfo->name())); + origin.enable(false); + +/* m_DirectoryStructure->addFromOrigin(ToWString(modInfo->name()), + ToWString(QDir::toNativeSeparators(modInfo->absolutePath())), + m_CurrentProfile->getModPriority(index));*/ + DirectoryRefresher::addModToStructure(m_DirectoryStructure, + modInfo->name(), m_CurrentProfile->getModPriority(index), + modInfo->absolutePath()); + DirectoryRefresher::cleanStructure(m_DirectoryStructure); + refreshLists(); + } + } +} + + +void MainWindow::displayModInformation(const QString &modName, int tab) +{ + unsigned int index = ModInfo::getIndex(modName); + if (index == UINT_MAX) { + qCritical("failed to resolve mod name %s", modName.toUtf8().constData()); + return; + } + + ModInfo::Ptr modInfo = ModInfo::getByIndex(index); + displayModInformation(modInfo, index, tab); +} + + +void MainWindow::displayModInformation(int row, int tab) +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(row); + displayModInformation(modInfo, row, 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, m_Settings.directInterface(), "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().toUtf8().constData(), + boost::bind(&MainWindow::extractProgress, this, boost::ref(progress), _1, _2)); + + 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::visitOnNexus_clicked() +{ + int modID = m_ModList.data(m_ModList.index(m_ContextRow, 0), Qt::UserRole).toInt(); + if (modID > 0) { + nexusLinkActivated(QString("%1/downloads/file.php?id=%2").arg(ToQString(GameInfo::instance().getNexusPage())).arg(modID)); + } else { + MessageDialog::showMessage(tr("Nexus ID for this Mod is unknown"), this); + } +} + +void MainWindow::openExplorer_clicked() +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + + ::ShellExecuteW(NULL, L"explore", ToWString(modInfo->absolutePath()).c_str(), NULL, NULL, SW_SHOWNORMAL); +} + + +void MainWindow::information_clicked() +{ + try { + displayModInformation(m_ContextRow); + } catch (const std::exception &e) { + reportError(e.what()); + } +} + + +void MainWindow::syncOverwrite() +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + SyncOverwriteDialog syncDialog(modInfo->absolutePath(), m_DirectoryStructure, this); + if (syncDialog.exec() == QDialog::Accepted) { + syncDialog.apply(QDir::fromNativeSeparators(m_Settings.getModDirectory())); + modInfo->testValid(); + refreshDirectoryStructure(); + } +} + + +void MainWindow::setPriorityMax() +{ + int newPriority = m_CurrentProfile->numMods() - 1; + m_CurrentProfile->setModPriority(m_ContextRow, newPriority); +} + + +void MainWindow::setPriorityManually() +{ +// m_CurrentProfile->setModPriority(m_ContextRow, m_CurrentProfile->numMods() - 1); + + int current = m_CurrentProfile->getModPriority(m_ContextRow); + int newPriority = QInputDialog::getInt(this, tr("Priority"), tr("Choose Priority"), current, 0, m_ModList.rowCount() - 1); + m_ModList.changeModPriority(m_ContextRow, newPriority); +// m_CurrentProfile->setModPriority(m_ContextRow, newPriority); +} + + +void MainWindow::setPriorityMin() +{ + int newPriority = 0; + m_CurrentProfile->setModPriority(m_ContextRow, newPriority); +} + + +void MainWindow::cancelModListEditor() +{ + ui->modList->setEnabled(false); + ui->modList->setEnabled(true); +} + + +void MainWindow::on_modList_doubleClicked(const QModelIndex &index) +{ + try { + displayModInformation(m_ModListSortProxy->mapToSource(index).row()); + // workaround to cancel the editor that might have opened because of + // selection-click + ui->modList->closePersistentEditor(index); + } catch (const std::exception &e) { + reportError(e.what()); + } +} + + +bool MainWindow::addCategories(QMenu *menu, int targetID) +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + const std::set &categories = modInfo->getCategories(); + + bool childEnabled = false; + + for (unsigned i = 1; i < m_CategoryFactory.numCategories(); ++i) { + if (m_CategoryFactory.getParentID(i) == targetID) { + QMenu *targetMenu = menu; + if (m_CategoryFactory.hasChildren(i)) { + targetMenu = menu->addMenu( + m_CategoryFactory.getCategoryName(i).replace('&', "&&")); + } + + int id = m_CategoryFactory.getCategoryID(i); + QScopedPointer checkBox(new QCheckBox(targetMenu)); + bool enabled = categories.find(id) != categories.end(); + checkBox->setText(m_CategoryFactory.getCategoryName(i).replace('&', "&&")); + if (enabled) { + childEnabled = true; + } + checkBox->setChecked(enabled ? Qt::Checked : Qt::Unchecked); + + QScopedPointer checkableAction(new QWidgetAction(targetMenu)); + checkableAction->setDefaultWidget(checkBox.take()); + checkableAction->setData(id); + targetMenu->addAction(checkableAction.take()); + + if (m_CategoryFactory.hasChildren(i)) { + if (addCategories(targetMenu, m_CategoryFactory.getCategoryID(i)) || enabled) { + targetMenu->setIcon(QIcon(":/MO/gui/resources/check.png")); + } + } + } + } + return childEnabled; +} + + +void MainWindow::saveCategoriesFromMenu(QMenu *menu, int modRow) +{ + ModInfo::Ptr modInfo = ModInfo::getByIndex(modRow); + foreach (QAction* action, menu->actions()) { + if (action->menu() != NULL) { + saveCategoriesFromMenu(action->menu(), modRow); + } else { + QWidgetAction *widgetAction = qobject_cast(action); + if (widgetAction != NULL) { + QCheckBox *checkbox = qobject_cast(widgetAction->defaultWidget()); + modInfo->setCategory(widgetAction->data().toInt(), checkbox->isChecked()); +// m_ModList.setModCategory(modRow, widgetAction->data().toInt(), checkbox->isChecked()); + } + } + } +} + + +void MainWindow::saveCategories() +{ + QMenu *menu = qobject_cast(sender()); + if (menu == NULL) { + qCritical("not a menu?"); + return; + } +// m_ModList.resetCategories(m_ContextRow); + saveCategoriesFromMenu(menu, m_ContextRow); + + refreshFilters(); +} + + +void MainWindow::savePrimaryCategory() +{ + QMenu *menu = qobject_cast(sender()); + if (menu == NULL) { + qCritical("not a menu?"); + return; + } + + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + foreach (QAction* action, menu->actions()) { + QWidgetAction *widgetAction = qobject_cast(action); + if (widgetAction != NULL) { + QRadioButton *btn = qobject_cast(widgetAction->defaultWidget()); + if (btn->isChecked()) { + modInfo->setPrimaryCategory(widgetAction->data().toInt()); + break; + } + } + } +} + + +void MainWindow::checkModsForUpdates() +{ + if (NexusInterface::instance()->getAccessManager()->loggedIn()) { + m_ModsToUpdate = ModInfo::checkAllForUpdate(this); + statusBar()->show(); + m_RefreshProgress->setRange(0, m_ModsToUpdate); + } else { + QString username, password; + if (m_Settings.getNexusLogin(username, password)) { + m_PostLoginTasks.push_back(boost::mem_fn(&MainWindow::checkModsForUpdates)); + m_NexusDialog.login(username, password); + } else { // otherwise there will be no endorsement info + m_ModsToUpdate = ModInfo::checkAllForUpdate(this); + } + } +} + + +void MainWindow::addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, ModInfo::Ptr info) +{ + const std::set &categories = info->getCategories(); + foreach (int categoryID, categories) { + int catIdx = m_CategoryFactory.getCategoryIndex(categoryID); + QWidgetAction *action = new QWidgetAction(primaryCategoryMenu); + try { + QRadioButton *categoryBox = new QRadioButton(m_CategoryFactory.getCategoryName(catIdx).replace('&', "&&"), + primaryCategoryMenu); + categoryBox->setChecked(categoryID == info->getPrimaryCategory()); + action->setDefaultWidget(categoryBox); + } catch (const std::exception &e) { + qCritical("failed to create category checkbox: %s", e.what()); + } + + action->setData(categoryID); + primaryCategoryMenu->addAction(action); + } +} + + +void MainWindow::addPrimaryCategoryCandidates() +{ + QMenu *menu = qobject_cast(sender()); + if (menu == NULL) { + qCritical("not a menu?"); + return; + } + menu->clear(); + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + + addPrimaryCategoryCandidates(menu, modInfo); +} + + +void MainWindow::enableVisibleMods() +{ + if (QMessageBox::question(NULL, tr("Confirm"), tr("Really enable all visible mods?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_ModListSortProxy->enableAllVisible(); + } +} + + +void MainWindow::disableVisibleMods() +{ + if (QMessageBox::question(NULL, tr("Confirm"), tr("Really disable all visible mods?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + m_ModListSortProxy->disableAllVisible(); + } +} + + +void MainWindow::exportModListCSV() +{ + SelectionDialog selection(tr("Choose what to export")); + + selection.addChoice(tr("Everything"), tr("All installed mods are included in the list"), 0); + selection.addChoice(tr("Active Mods"), tr("Only active (checked) mods from your current profile are included"), 1); + selection.addChoice(tr("Visible"), tr("All mods visible in the mod list are included"), 2); + + if (selection.exec() == QDialog::Accepted) { + unsigned int numMods = ModInfo::getNumMods(); + + try { + QBuffer buffer; + buffer.open(QIODevice::ReadWrite); + CSVBuilder builder(&buffer); + builder.setEscapeMode(CSVBuilder::TYPE_STRING, CSVBuilder::QUOTE_ALWAYS); + std::vector > fields; + fields.push_back(std::make_pair(QString("mod_id"), CSVBuilder::TYPE_INTEGER)); + fields.push_back(std::make_pair(QString("mod_installed_name"), CSVBuilder::TYPE_STRING)); + fields.push_back(std::make_pair(QString("mod_version"), CSVBuilder::TYPE_STRING)); + fields.push_back(std::make_pair(QString("file_installed_name"), CSVBuilder::TYPE_STRING)); + builder.setFields(fields); + + builder.writeHeader(); + + for (unsigned int i = 0; i < numMods; ++i) { + ModInfo::Ptr info = ModInfo::getByIndex(i); + bool enabled = m_CurrentProfile->modEnabled(i); + if ((selection.getChoiceData().toInt() == 1) && !enabled) { + continue; + } else if ((selection.getChoiceData().toInt() == 2) && !m_ModListSortProxy->filterMatches(info, enabled)) { + continue; + } + std::vector flags = info->getFlags(); + if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) == flags.end()) && + (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) == flags.end())) { + builder.setRowField("mod_id", info->getNexusID()); + builder.setRowField("mod_installed_name", info->name()); + builder.setRowField("mod_version", info->getVersion().canonicalString()); + builder.setRowField("file_installed_name", info->getInstallationFile()); + builder.writeRow(); + } + } + + SaveTextAsDialog saveDialog(this); + saveDialog.setText(buffer.data()); + saveDialog.exec(); + } catch (const std::exception &e) { + reportError(tr("export failed: %1").arg(e.what())); + } + } +} + + +void MainWindow::on_modList_customContextMenuRequested(const QPoint &pos) +{ + try { + QTreeView *modList = findChild("modList"); + + m_ContextRow = m_ModListSortProxy->mapToSource(modList->indexAt(pos)).row(); + + QMenu menu; + menu.addAction(tr("Install Mod..."), this, SLOT(installMod_clicked())); + + 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("Refresh"), this, SLOT(on_profileRefreshBtn_clicked())); + + menu.addAction(tr("Export to csv..."), this, SLOT(exportModListCSV())); + + if (m_ContextRow != -1) { + menu.addSeparator(); + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + std::vector flags = info->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) { + menu.addAction(tr("Sync to Mods..."), this, SLOT(syncOverwrite())); + } 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())); + } else { + QMenu *addCategoryMenu = menu.addMenu(tr("Set Category")); + addCategories(addCategoryMenu, 0); + connect(addCategoryMenu, SIGNAL(aboutToHide()), this, SLOT(saveCategories())); + + QMenu *primaryCategoryMenu = menu.addMenu(tr("Primary Category")); + connect(primaryCategoryMenu, SIGNAL(aboutToShow()), this, SLOT(addPrimaryCategoryCandidates())); + connect(primaryCategoryMenu, SIGNAL(aboutToHide()), this, SLOT(savePrimaryCategory())); + + 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())); + } break; + case ModInfo::ENDORSED_FALSE: { + menu.addAction(tr("Endorse"), this, SLOT(endorse_clicked())); + } break; + default: { + QAction *action = new QAction(tr("Endorsement state unknown"), &menu); + action->setEnabled(false); + menu.addAction(action); + } break; + } + + 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); + } + + menu.exec(modList->mapToGlobal(pos)); + } catch (const std::exception &e) { + reportError(tr("Exception: ").arg(e.what())); + } catch (...) { + reportError(tr("Unknown exception")); + } +} + + +void MainWindow::on_categoriesList_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *) +{ + if (current != NULL) { + m_ModListSortProxy->setCategoryFilter(current->data(0, Qt::UserRole).toInt()); + ui->currentCategoryLabel->setText(QString("(%1)").arg(current->text(0))); + } +} + + +void MainWindow::fixMods_clicked() +{ + QListWidgetItem *selectedItem = ui->savegameList->item(m_SelectedSaveGame); + if (selectedItem == NULL) { + return; + } + + // if required, parse the save game + if (selectedItem->data(Qt::UserRole).isNull()) { + QVariant temp; + SaveGameGamebryo *save = getSaveGame(selectedItem->data(Qt::UserRole).toString()); + save->setParent(selectedItem->listWidget()); + temp.setValue(save); + selectedItem->setData(Qt::UserRole, temp); + } + + const SaveGameGamebryo *save = qvariant_cast(selectedItem->data(Qt::UserRole)); + + // collect the list of missing plugins + std::map > missingPlugins; + + for (int i = 0; i < save->numPlugins(); ++i) { + const QString &pluginName = save->plugin(i); + if (!m_PluginList.isEnabled(pluginName)) { + missingPlugins[pluginName] = std::vector(); + } + } + + // figure out, for each esp/esm, which mod, if any, contains it + QStringList espFilter("*.esp"); + espFilter.append("*.esm"); + + { + QDir dataDir(m_GamePath + "/data"); + QStringList esps = dataDir.entryList(espFilter); + foreach (const QString &esp, esps) { + std::map >::iterator iter = missingPlugins.find(esp); + if (iter != missingPlugins.end()) { + iter->second.push_back(""); + } + } + } + + for (unsigned int i = 0; i < m_CurrentProfile->numRegularMods(); ++i) { + int modIndex = m_CurrentProfile->modIndexByPriority(i); + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + + QStringList esps = QDir(modInfo->absolutePath()).entryList(espFilter); + foreach (const QString &esp, esps) { + std::map >::iterator iter = missingPlugins.find(esp); + if (iter != missingPlugins.end()) { + iter->second.push_back(modInfo->name()); + } + } + } + + ActivateModsDialog dialog(missingPlugins, this); + if (dialog.exec() == QDialog::Accepted) { + // activate the required mods, then enable all esps + std::set modsToActivate = dialog.getModsToActivate(); + for (std::set::iterator iter = modsToActivate.begin(); iter != modsToActivate.end(); ++iter) { + if (*iter != "") { + unsigned int modIndex = ModInfo::getIndex(*iter); + m_CurrentProfile->setModEnabled(modIndex, true); + } + } + + m_CurrentProfile->writeModlist(); + refreshLists(); + + std::set espsToActivate = dialog.getESPsToActivate(); + for (std::set::iterator iter = espsToActivate.begin(); iter != espsToActivate.end(); ++iter) { + m_PluginList.enableESP(*iter); + } + saveCurrentLists(); + } +} + + +void MainWindow::on_savegameList_customContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *selectedItem = ui->savegameList->itemAt(pos); + if (selectedItem == NULL) { + return; + } + + m_SelectedSaveGame = ui->savegameList->row(selectedItem); + + QMenu menu; + menu.addAction(tr("Fix Mods..."), this, SLOT(fixMods_clicked())); + + menu.exec(ui->savegameList->mapToGlobal(pos)); +} + +void MainWindow::linkToolbar() +{ + const Executable &selectedExecutable = ui->executablesListBox->itemData(ui->executablesListBox->currentIndex()).value(); + Executable &exe = m_ExecutablesList.find(selectedExecutable.m_Title); + exe.m_Toolbar = !exe.m_Toolbar; + updateToolBar(); +} + +void MainWindow::linkDesktop() +{ + QComboBox* executablesList = findChild("executablesListBox"); +// QPushButton *linkButton = findChild("linkDesktopButton"); + + const Executable &selectedExecutable = executablesList->itemData(executablesList->currentIndex()).value(); + QString linkName = getDesktopDirectory() + "\\" + selectedExecutable.m_Title + ".lnk"; + + if (QFile::exists(linkName)) { + if (QFile::remove(linkName)) { + ui->linkButton->menu()->actions().at(0)->setIcon(QIcon(":/MO/gui/link")); + } else { + reportError(tr("failed to remove %1").arg(linkName)); + } + } else { + QFileInfo exeInfo(m_ExeName); + // create link + std::wstring targetFile = ToWString(exeInfo.absoluteFilePath()); + std::wstring parameter = ToWString(QString("\"") + selectedExecutable.m_BinaryInfo.absoluteFilePath() + "\" " + 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) { + ui->linkButton->menu()->actions().at(0)->setIcon(QIcon(":/MO/gui/remove")); + } else { + reportError(tr("failed to create %1").arg(linkName)); + } + } +} + +void MainWindow::linkMenu() +{ + QComboBox* executablesList = findChild("executablesListBox"); + + const Executable &selectedExecutable = executablesList->itemData(executablesList->currentIndex()).value(); + QString linkName = getStartMenuDirectory() + "\\" + selectedExecutable.m_Title + ".lnk"; + + if (QFile::exists(linkName)) { + if (QFile::remove(linkName)) { + ui->linkButton->menu()->actions().at(1)->setIcon(QIcon(":/MO/gui/link")); + } else { + reportError(tr("failed to remove %1").arg(linkName)); + } + } else { + QFileInfo exeInfo(m_ExeName); + // create link + std::wstring targetFile = ToWString(exeInfo.absoluteFilePath()); + std::wstring parameter = ToWString(QString("\"") + selectedExecutable.m_BinaryInfo.absoluteFilePath() + "\" " + 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) { + ui->linkButton->menu()->actions().at(1)->setIcon(QIcon(":/MO/gui/remove")); + } else { + reportError(tr("failed to create %1").arg(linkName)); + } + } +} + +void MainWindow::on_actionSettings_triggered() +{ + QString oldModDirectory(m_Settings.getModDirectory()); + QString oldCacheDirectory(m_Settings.getCacheDirectory()); + m_Settings.query(this); + fixCategories(); + refreshFilters(); + if (QDir::fromNativeSeparators(m_DownloadManager.getOutputDirectory()) != QDir::fromNativeSeparators(m_Settings.getDownloadDirectory())) { + if (m_DownloadManager.downloadsInProgress()) { + MessageDialog::showMessage(tr("Can't change download directory while downloads are in progress!"), this); + } else { + m_DownloadManager.setOutputDirectory(m_Settings.getDownloadDirectory()); + } + } + + if (m_Settings.getModDirectory() != oldModDirectory) { + refreshModList(); + refreshLists(); + } + + if (m_Settings.getCacheDirectory() != oldCacheDirectory) { + NexusInterface::instance()->setCacheDirectory(m_Settings.getCacheDirectory()); + } + + NexusInterface::instance()->setNMMVersion(m_Settings.getNMMVersion()); +} + +void MainWindow::on_actionNexus_triggered() +{ + QString username, password; + m_NexusDialog.openUrl(ToQString(GameInfo::instance().getNexusPage())); + + if (m_Settings.getNexusLogin(username, password)) { + m_NexusDialog.login(username, password); + } else { + m_NexusDialog.loadNexus(); + } + m_NexusDialog.show(); + m_NexusDialog.activateWindow(); + + QTabWidget *tabWidget = findChild("tabWidget"); + tabWidget->setCurrentIndex(4); +} + + +void MainWindow::nexusLinkActivated(const QString &link) +{ + if (m_Settings.preferExternalBrowser()) { + ::ShellExecuteW(NULL, L"open", ToWString(link).c_str(), NULL, NULL, SW_SHOWNORMAL); + } else { + QString username, password; + m_NexusDialog.openUrl(link); + if (m_Settings.getNexusLogin(username, password)) { + m_NexusDialog.login(username, password); + m_LoginAttempted = true; + } else { + m_NexusDialog.loadNexus(); + } + m_NexusDialog.show(); + + QTabWidget *tabWidget = findChild("tabWidget"); + tabWidget->setCurrentIndex(4); + } +} + + +void MainWindow::downloadRequestedNXM(const QString &url) +{ + QString username, password; + + if (!m_LoginAttempted && !NexusInterface::instance()->getAccessManager()->loggedIn() && + (m_Settings.getNexusLogin(username, password) || + (m_AskForNexusPW && queryLogin(username, password)))) { + m_PendingDownloads.append(url); + NexusInterface::instance()->getAccessManager()->login(username, password); + m_LoginAttempted = true; + } else { + m_DownloadManager.addNXMDownload(url); + } +} + + +void MainWindow::downloadRequested(QNetworkReply *reply, int modID, const QString &fileName) +{ + try { + if (m_DownloadManager.addDownload(reply, fileName, modID)) { + MessageDialog::showMessage(tr("Download started"), this); + } + } catch (const std::exception &e) { + MessageDialog::showMessage(tr("Download failed"), this); + qCritical("exception starting download: %s", e.what()); + } +} + + +void MainWindow::languageChange(const QString &newLanguage) +{ + if (m_Translator != NULL) { + QCoreApplication::removeTranslator(m_Translator); + delete m_Translator; + m_Translator = NULL; + } + if (m_TranslatorQt != NULL) { + QCoreApplication::removeTranslator(m_TranslatorQt); + delete m_TranslatorQt; + m_TranslatorQt = NULL; + } + + if (newLanguage != "en_US") { + // add our own translations + m_Translator = new QTranslator(this); + QString locFile = ToQString(AppConfig::translationPrefix()) + "_" + newLanguage; + if (!m_Translator->load(locFile, QCoreApplication::applicationDirPath() + "/translations")) { + qDebug("localization %s not found", locFile.toUtf8().constData()); + } + QCoreApplication::installTranslator(m_Translator); + + // also add the translations for qt default strings + m_TranslatorQt = new QTranslator(this); + locFile = QString("qt_") + newLanguage; + if (!m_TranslatorQt->load(locFile, QCoreApplication::applicationDirPath() + "/translations")) { + qDebug("localization %s not found", locFile.toUtf8().constData()); + } + QCoreApplication::installTranslator(m_TranslatorQt); + } + ui->retranslateUi(this); + ui->profileBox->setItemText(0, QObject::tr("")); +// ui->toolBar->addWidget(createHelpWidget(ui->toolBar)); + + updateDownloadListDelegate(); + updateProblemsButton(); +} + + +void MainWindow::installDownload(int index) +{ + try { + QString fileName = m_DownloadManager.getFilePath(index); + int modID = m_DownloadManager.getModID(index); + QString modName; + + // see if there already are mods with the specified mod id + if (modID != 0) { + ModInfo::Ptr modInfo = ModInfo::getByModID(modID, true); + if (!modInfo.isNull()) { + std::vector flags = modInfo->getFlags(); + if (std::find(flags.begin(), flags.end(), ModInfo::FLAG_BACKUP) == flags.end()) { + modName = modInfo->name(); + modInfo->saveMeta(); + } + } + // TODO there may be multiple mods with the same id! +// modName = m_ModList.getModByModID(modID); + } + + m_CurrentProfile->writeModlistNow(); + + bool hasIniTweaks = false; + if (m_InstallationManager.install(fileName, m_CurrentProfile->getPluginsFileName(), m_Settings.getModDirectory(), m_Settings.preferIntegratedInstallers(), + m_Settings.enableQuickInstaller(), modName, hasIniTweaks)) { + MessageDialog::showMessage(tr("Installation successful"), this); + + refreshModList(); + + QModelIndexList posList = m_ModList.match(m_ModList.index(0, 0), Qt::DisplayRole, 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)); + } + + m_DownloadManager.markInstalled(index); + + emit modInstalled(); + } else if (m_InstallationManager.wasCancelled()) { + QMessageBox::information(this, tr("Installation cancelled"), tr("The mod was not installed completely."), QMessageBox::Ok); + } + } catch (const std::exception &e) { + reportError(e.what()); + } +} + + +void MainWindow::writeDataToFile(QFile &file, const QString &directory, const DirectoryEntry &directoryEntry) +{ + { // list files +// std::set::const_iterator current, end; +// directoryEntry.getFiles(current, end); +// for (; current != end; ++current) { + + std::vector files = directoryEntry.getFiles(); + for (auto iter = files.begin(); iter != files.end(); ++iter) { + FileEntry *current = *iter; + bool isArchive = false; + int origin = current->getOrigin(isArchive); + if (isArchive) { + // TODO: don't list files from archives. maybe make this an option? + continue; + } + QString fullName = directory; + fullName.append("\\").append(ToQString(current->getName())); + file.write(fullName.toUtf8()); + + file.write("\t("); + file.write(ToQString(m_DirectoryStructure->getOriginByID(origin).getName()).toUtf8()); + file.write(")\r\n"); + } + } + + { // recurse into subdirectories + std::vector::const_iterator current, end; + directoryEntry.getSubDirectories(current, end); + for (; current != end; ++current) { + writeDataToFile(file, directory.mid(0).append("\\").append(ToQString((*current)->getName())), **current); + } + } +} + + +void MainWindow::writeDataToFile() +{ + QString fileName = QFileDialog::getSaveFileName(this); + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + reportError(tr("failed to write to file %1").arg(fileName)); + } + + writeDataToFile(file, "data", *m_DirectoryStructure); + file.close(); + + MessageDialog::showMessage(tr("%1 written").arg(QDir::toNativeSeparators(fileName)), this); +} + + +int MainWindow::getBinaryExecuteInfo(const QFileInfo &targetInfo, + QFileInfo &binaryInfo, QString &arguments) +{ + QString extension = targetInfo.completeSuffix(); + if ((extension == "exe") || + (extension == "cmd") || + (extension == "com") || + (extension == "bat")) { + binaryInfo = QFileInfo("C:\\Windows\\System32\\cmd.exe"); + arguments = QString("/C \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); + return 1; + } else if (extension == "jar") { + // types that need to be injected into + std::wstring targetPathW = ToWString(targetInfo.absoluteFilePath()); + QString binaryPath; + + { // try to find java automatically + WCHAR buffer[MAX_PATH]; + if (FindExecutableW(targetPathW.c_str(), NULL, buffer) > (HINSTANCE)32) { + DWORD binaryType = 0UL; + if (!::GetBinaryTypeW(targetPathW.c_str(), &binaryType)) { + qDebug("failed to determine binary type: %lu", ::GetLastError()); + } else if (binaryType == SCS_32BIT_BINARY) { + binaryPath = ToQString(buffer); + } + } + } + if (binaryPath.isEmpty() && (extension == "jar")) { + QSettings javaReg("HKEY_LOCAL_MACHINE\\Software\\JavaSoft\\Java Runtime Environment", QSettings::NativeFormat); + if (javaReg.contains("CurrentVersion")) { + QString currentVersion = javaReg.value("CurrentVersion").toString(); + binaryPath = javaReg.value(QString("%1/JavaHome").arg(currentVersion)).toString().append("\\bin\\javaw.exe"); + } + } + if (binaryPath.isEmpty()) { + binaryPath = QFileDialog::getOpenFileName(this, tr("Select binary"), QString(), tr("Binary") + " (*.exe)"); + } + if (binaryPath.isEmpty()) { + return 0; + } + binaryInfo = QFileInfo(binaryPath); + if (extension == "jar") { + arguments = QString("-jar \"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); + } else { + arguments = QString("\"%1\"").arg(QDir::toNativeSeparators(targetInfo.absoluteFilePath())); + } + return 1; + } else { + return 2; + } +} + + +void MainWindow::addAsExecutable() +{ + if (m_ContextItem != NULL) { + QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString()); + QFileInfo binaryInfo; + QString arguments; + switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { + case 1: { + QString name = QInputDialog::getText(this, tr("Enter Name"), + tr("Please enter a name for the executable"), QLineEdit::Normal, + targetInfo.baseName()); + if (!name.isEmpty()) { + m_ExecutablesList.addExecutable(name, binaryInfo.absoluteFilePath(), + arguments, targetInfo.absolutePath(), + DEFAULT_STAY, QString(), + true, false); + refreshExecutablesList(); + } + } break; + case 2: { + QMessageBox::information(this, tr("Not an executable"), tr("This is not a recognized executable.")); + } break; + default: { + // nop + } break; + } + } +} + + +void MainWindow::originModified(int originID) +{ + FilesOrigin &origin = m_DirectoryStructure->getOriginByID(originID); + origin.enable(false); + m_DirectoryStructure->addFromOrigin(origin.getName(), origin.getPath(), origin.getPriority()); + DirectoryRefresher::cleanStructure(m_DirectoryStructure); +} + + +void MainWindow::hideFile() +{ + QString oldName = m_ContextItem->data(0, Qt::UserRole).toString(); + QString newName = oldName + ModInfo::s_HiddenExt; + + if (QFileInfo(newName).exists()) { + if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a hidden version of this file. Replace it?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + if (!QFile(newName).remove()) { + QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); + return; + } + } else { + return; + } + } + + if (QFile::rename(oldName, newName)) { + originModified(m_ContextItem->data(1, Qt::UserRole + 1).toInt()); + refreshDataTree(); + } else { + reportError(tr("failed to rename \"%1\" to \"%2\"").arg(oldName).arg(QDir::toNativeSeparators(newName))); + } +} + + +void MainWindow::unhideFile() +{ + QString oldName = m_ContextItem->data(0, Qt::UserRole).toString(); + QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length()); + if (QFileInfo(newName).exists()) { + if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a visible version of this file. Replace it?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + if (!QFile(newName).remove()) { + QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); + return; + } + } else { + return; + } + } + if (QFile::rename(oldName, newName)) { + originModified(m_ContextItem->data(1, Qt::UserRole + 1).toInt()); + refreshDataTree(); + } else { + reportError(tr("failed to rename \"%1\" to \"%2\"").arg(QDir::toNativeSeparators(oldName)).arg(QDir::toNativeSeparators(newName))); + } +} + + +void MainWindow::openDataFile() +{ + if (m_ContextItem != NULL) { + QFileInfo targetInfo(m_ContextItem->data(0, Qt::UserRole).toString()); + QFileInfo binaryInfo; + QString arguments; + switch (getBinaryExecuteInfo(targetInfo, binaryInfo, arguments)) { + case 1: { + spawnBinaryDirect(binaryInfo, arguments, m_CurrentProfile->getName(), targetInfo.absolutePath(), ""); + } break; + case 2: { + ::ShellExecuteW(NULL, L"open", ToWString(targetInfo.absoluteFilePath()).c_str(), NULL, NULL, SW_SHOWNORMAL); + } break; + default: { + // nop + } break; + } + } +} + + +void MainWindow::updateAvailable() +{ + QToolBar *toolBar = findChild("toolBar"); + foreach (QAction *action, toolBar->actions()) { + if (action->text() == tr("Update")) { + action->setEnabled(true); + action->setToolTip(tr("Update available")); + break; + } + } +} + + +void MainWindow::motdReceived(const QString &motd) +{ + // don't show motd after 5 seconds, may be annoying. Hopefully the user's + // internet connection is faster next time + if (m_StartTime.secsTo(QTime::currentTime()) < 5) { + uint hash = qHash(motd); + if (hash != m_Settings.getMotDHash()) { + MotDDialog dialog(motd); + dialog.exec(); + m_Settings.setMotDHash(hash); + } + } + + ui->actionEndorseMO->setVisible(false); +} + + +void MainWindow::notEndorsedYet() +{ + ui->actionEndorseMO->setVisible(true); +} + + +void MainWindow::on_dataTree_customContextMenuRequested(const QPoint &pos) +{ + QTreeWidget *dataTree = findChild("dataTree"); + m_ContextItem = dataTree->itemAt(pos.x(), pos.y()); + + QMenu menu; + if ((m_ContextItem != NULL) && (m_ContextItem->childCount() == 0)) { + menu.addAction(tr("Open/Execute"), this, SLOT(openDataFile())); + menu.addAction(tr("Add as Executable"), this, SLOT(addAsExecutable())); + // offer to hide/unhide file, but not for files from archives + if (!m_ContextItem->data(0, Qt::UserRole + 1).toBool()) { + if (m_ContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) { + menu.addAction(tr("Un-Hide"), this, SLOT(unhideFile())); + } else { + menu.addAction(tr("Hide"), this, SLOT(hideFile())); + } + } + menu.addSeparator(); + } + menu.addAction(tr("Write To File..."), this, SLOT(writeDataToFile())); + menu.addAction(tr("Refresh"), this, SLOT(on_btnRefreshData_clicked())); + + menu.exec(dataTree->mapToGlobal(pos)); +} + +void MainWindow::on_conflictsCheckBox_toggled(bool) +{ + refreshDataTree(); +} + + +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; + } else { + m_Updater.startUpdate(); + } +} + + +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()); + } +} + + +void MainWindow::updateDownloadListDelegate() +{ + if (ui->compactBox->isChecked()) { + ui->downloadView->setItemDelegate(new DownloadListWidgetCompactDelegate(&m_DownloadManager, ui->downloadView)); + } else { + ui->downloadView->setItemDelegate(new DownloadListWidgetDelegate(&m_DownloadManager, ui->downloadView)); + } + + DownloadListSortProxy *sortProxy = new DownloadListSortProxy(&m_DownloadManager, ui->downloadView); + sortProxy->setSourceModel(new DownloadList(&m_DownloadManager, ui->downloadView)); + connect(ui->downloadFilterEdit, SIGNAL(textChanged(QString)), sortProxy, SLOT(updateFilter(QString))); + + ui->downloadView->setModel(sortProxy); + ui->downloadView->sortByColumn(1, Qt::AscendingOrder); + ui->downloadView->header()->resizeSections(QHeaderView::Fixed); +// ui->downloadView->setFirstColumnSpanned(0, QModelIndex(), true); + + connect(ui->downloadView->itemDelegate(), SIGNAL(installDownload(int)), this, SLOT(installDownload(int))); + connect(ui->downloadView->itemDelegate(), SIGNAL(queryInfo(int)), &m_DownloadManager, SLOT(queryInfo(int))); + connect(ui->downloadView->itemDelegate(), SIGNAL(removeDownload(int, bool)), &m_DownloadManager, SLOT(removeDownload(int, bool))); + 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(); +} + + +void MainWindow::modDetailsUpdated(bool) +{ + --m_ModsToUpdate; + if (m_ModsToUpdate == 0) { + statusBar()->hide(); + m_ModListSortProxy->setCategoryFilter(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE); + for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { + if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { + ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); + break; + } + } +// m_RefreshProgress->setVisible(false); + } else { + m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); + } +} + +void MainWindow::nxmUpdatesAvailable(const std::vector &modIDs, QVariant userData, QVariant resultData, int) +{ + m_ModsToUpdate -= modIDs.size(); + + QVariantList resultList = resultData.toList(); + for (auto iter = resultList.begin(); iter != resultList.end(); ++iter) { + QVariantMap result = iter->toMap(); + if (result["id"].toInt() == GameInfo::instance().getNexusModID()) { + if (!result["voted_by_user"].toBool()) { + ui->actionEndorseMO->setVisible(true); + } + } else { + ModInfo::Ptr info = ModInfo::getByModID(result["id"].toInt(), true); + if (!info.isNull()) { + info->setNewestVersion(VersionInfo(result["version"].toString())); + info->setNexusDescription(result["description"].toString()); + if (NexusInterface::instance()->getAccessManager()->loggedIn()) { + // don't use endorsement info if we're not logged in + info->setIsEndorsed(result["voted_by_user"].toBool()); + } + } + } + } + + if (m_ModsToUpdate <= 0) { + statusBar()->hide(); + m_ModListSortProxy->setCategoryFilter(CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE); + for (int i = 0; i < ui->categoriesList->topLevelItemCount(); ++i) { + if (ui->categoriesList->topLevelItem(i)->data(0, Qt::UserRole) == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) { + ui->categoriesList->setCurrentItem(ui->categoriesList->topLevelItem(i)); + break; + } + } + } else { + m_RefreshProgress->setValue(m_RefreshProgress->maximum() - m_ModsToUpdate); + } +} + + +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::nxmRequestFailed(int modID, QVariant, int, const QString &errorString) +{ + if (modID == -1) { + // must be the update-check that failed + m_ModsToUpdate = 0; + statusBar()->hide(); + } + MessageDialog::showMessage(tr("Request to Nexus failed: %1").arg(errorString), this); +} + + +void MainWindow::loginSuccessful(bool necessary) +{ + if (necessary) { + MessageDialog::showMessage(tr("login successful"), this); + } + foreach (QString url, m_PendingDownloads) { + downloadRequestedNXM(url); + } + m_PendingDownloads.clear(); + foreach (auto task, m_PostLoginTasks) { + task(this); + } + + m_PostLoginTasks.clear(); +} + + +void MainWindow::loginSuccessfulUpdate(bool necessary) +{ + if (necessary) { + MessageDialog::showMessage(tr("login successful"), this); + } + m_Updater.startUpdate(); +} + + +void MainWindow::loginFailed(const QString &message) +{ + if (!m_PendingDownloads.isEmpty()) { + MessageDialog::showMessage(tr("login failed: %1. Trying to download anyway").arg(message), this); + foreach (QString url, m_PendingDownloads) { + downloadRequestedNXM(url); + } + } + m_PendingDownloads.clear(); +} + + +void MainWindow::loginFailedUpdate(const QString &message) +{ + MessageDialog::showMessage(tr("login failed: %1. You need to log-in with Nexus to update MO.").arg(message), this); +} + + +void MainWindow::windowTutorialFinished(const QString &windowName) +{ + m_Settings.directInterface().setValue(QString("CompletedWindowTutorials/") + windowName, true); +} + + +BSA::EErrorCode MainWindow::extractBSA(BSA::Archive &archive, BSA::Folder::Ptr folder, const QString &destination, + QProgressDialog &progress) +{ + QDir().mkdir(destination); + BSA::EErrorCode result = BSA::ERROR_NONE; + QString errorFile; + + for (unsigned int i = 0; i < folder->getNumFiles(); ++i) { + BSA::File::Ptr file = folder->getFile(i); + BSA::EErrorCode res = archive.extract(file, destination.toUtf8().constData()); + if (res != BSA::ERROR_NONE) { + reportError(tr("failed to read %1: %2").arg(file->getName().c_str()).arg(res)); + result = res; + } + progress.setLabelText(file->getName().c_str()); + progress.setValue(progress.value() + 1); + QCoreApplication::processEvents(); + if (progress.wasCanceled()) { + result = BSA::ERROR_CANCELED; + } + } + + if (result != BSA::ERROR_NONE) { + if (QMessageBox::critical(this, tr("Error"), tr("failed to extract %1 (errorcode %2)").arg(errorFile).arg(result), + QMessageBox::Ok | QMessageBox::Cancel) == QMessageBox::Cancel) { + return result; + } + } + + for (unsigned int i = 0; i < folder->getNumSubFolders(); ++i) { + BSA::Folder::Ptr subFolder = folder->getSubFolder(i); + BSA::EErrorCode res = extractBSA(archive, subFolder, + destination.mid(0).append("/").append(subFolder->getName().c_str()), progress); + if (res != BSA::ERROR_NONE) { + return res; + } + } + return BSA::ERROR_NONE; +} + + +bool MainWindow::extractProgress(QProgressDialog &progress, int percentage, std::string fileName) +{ + progress.setLabelText(fileName.c_str()); + progress.setValue(percentage); + QCoreApplication::processEvents(); + return !progress.wasCanceled(); +} + + +void MainWindow::extractBSATriggered() +{ + QTreeWidgetItem *item = ui->bsaList->topLevelItem(m_ContextRow); + + QString targetFolder = FileDialogMemory::getExistingDirectory("extractBSA", this, tr("Extract BSA")); + + if (!targetFolder.isEmpty()) { + BSA::Archive archive; + QString originPath = QDir::fromNativeSeparators(ToQString(m_DirectoryStructure->getOriginByName(ToWString(item->text(1))).getPath())); + QString archivePath = QString("%1\\%2").arg(originPath).arg(item->text(0)); + + BSA::EErrorCode result = archive.read(archivePath.toLocal8Bit().constData()); + if ((result != BSA::ERROR_NONE) && (result != BSA::ERROR_INVALIDHASHES)) { + reportError(tr("failed to read %1: %2").arg(archivePath).arg(result)); + return; + } + + QProgressDialog progress(this); + progress.setMaximum(100); + progress.setValue(0); + progress.show(); + + archive.extractAll(QDir::toNativeSeparators(targetFolder).toUtf8().constData(), + boost::bind(&MainWindow::extractProgress, this, boost::ref(progress), _1, _2)); + + if (result == BSA::ERROR_INVALIDHASHES) { + reportError(tr("This archive contains invalid hashes. Some files may be broken.")); + } + } +} + + +void MainWindow::displayColumnSelection(const QPoint &pos) +{ + QMenu menu; + + // display a list of all headers as checkboxes + QAbstractItemModel *model = ui->modList->header()->model(); + for (int i = 0; i < model->columnCount(); ++i) { + QString columnName = model->headerData(i, Qt::Horizontal).toString(); + QCheckBox *checkBox = new QCheckBox(&menu); + checkBox->setText(columnName); + checkBox->setChecked(!ui->modList->header()->isSectionHidden(i)); + QWidgetAction *checkableAction = new QWidgetAction(&menu); + checkableAction->setDefaultWidget(checkBox); + menu.addAction(checkableAction); + } + menu.exec(pos); + + // view/hide columns depending on check-state + int i = 0; + foreach (const QAction *action, menu.actions()) { + const QWidgetAction *widgetAction = qobject_cast(action); + if (widgetAction != NULL) { + const QCheckBox *checkBox = qobject_cast(widgetAction->defaultWidget()); + if (checkBox != NULL) { + ui->modList->header()->setSectionHidden(i, !checkBox->isChecked()); + } + } + ++i; + } +} + + +void MainWindow::on_bsaList_customContextMenuRequested(const QPoint &pos) +{ + m_ContextRow = ui->bsaList->indexOfTopLevelItem(ui->bsaList->itemAt(pos)); + + QMenu menu; + menu.addAction(tr("Extract..."), this, SLOT(extractBSATriggered())); + + menu.exec(ui->bsaList->mapToGlobal(pos)); +} + +void MainWindow::on_bsaList_itemChanged(QTreeWidgetItem*, int) +{ + checkBSAList(); +} + +void MainWindow::on_actionProblems_triggered() +{ + QString problemDescription; + checkForProblems(problemDescription); + QMessageBox::information(this, tr("Problems"), problemDescription); +} + +void MainWindow::setCategoryListVisible(bool visible) +{ + if (visible) { + ui->categoriesGroup->show(); + ui->displayCategoriesBtn->setText(ToQString(L"\u00ab")); + } else { + ui->categoriesGroup->hide(); + ui->displayCategoriesBtn->setText(ToQString(L"\u00bb")); + } +} + +void MainWindow::on_displayCategoriesBtn_toggled(bool checked) +{ + setCategoryListVisible(checked); +} + +void MainWindow::editCategories() +{ + CategoriesDialog dialog(this); + if (dialog.exec() == QDialog::Accepted) { + dialog.commitChanges(); + } +} + +void MainWindow::on_categoriesList_customContextMenuRequested(const QPoint &pos) +{ + QMenu menu; + menu.addAction(tr("Edit Categories..."), this, SLOT(editCategories())); + + menu.exec(ui->categoriesList->mapToGlobal(pos)); +} + + +void MainWindow::lockESPIndex() +{ + m_PluginList.lockESPIndex(m_ContextRow, true); +} + +void MainWindow::unlockESPIndex() +{ + m_PluginList.lockESPIndex(m_ContextRow, false); +} + + +void MainWindow::on_espList_customContextMenuRequested(const QPoint &pos) +{ + m_ContextRow = m_PluginListSortProxy->mapToSource(ui->espList->indexAt(pos)).row(); + + QMenu menu; + menu.addAction(tr("Enable all"), &m_PluginList, SLOT(enableAll())); + menu.addAction(tr("Disable all"), &m_PluginList, SLOT(disableAll())); + + if ((m_ContextRow != -1) && m_PluginList.isEnabled(m_ContextRow)) { + if (m_PluginList.isESPLocked(m_ContextRow)) { + menu.addAction(tr("Unlock index"), this, SLOT(unlockESPIndex())); + } else { + menu.addAction(tr("Lock index"), this, SLOT(lockESPIndex())); + } + } + + try { + menu.exec(ui->espList->mapToGlobal(pos)); + } catch (const std::exception &e) { + reportError(tr("Exception: ").arg(e.what())); + } catch (...) { + reportError(tr("Unknown exception")); + } +} diff --git a/src/mainwindow.h b/src/mainwindow.h index 7aaa5dc3..1d6508be 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -17,407 +17,414 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#ifndef MAINWINDOW_H -#define MAINWINDOW_H - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include "executableslist.h" -#include "modlist.h" -#include "pluginlist.h" -#define WIN32_LEAN_AND_MEAN -#include -#include -#include "directoryrefresher.h" -#include -#include -#include -#include "settings.h" -#include "nexusdialog.h" -#include "downloadmanager.h" -#include "installationmanager.h" -#include "selfupdater.h" -#include "savegamegamebyro.h" -#include "modlistsortproxy.h" -#include "pluginlistsortproxy.h" -#include "tutorialcontrol.h" -#include "savegameinfowidgetgamebryo.h" - -namespace Ui { - class MainWindow; -} - -class QToolButton; - -class MainWindow : public QMainWindow, public IOrganizer -{ - Q_OBJECT - -public: - explicit MainWindow(const QString &exeName, QSettings &initSettings, QWidget *parent = 0); - ~MainWindow(); - - void readSettings(); - - bool addProfile(); - void refreshLists(); - void refreshESPList(); - void refreshBSAList(); - void refreshDataTree(); - void refreshSaveList(); - void refreshModList(); - - void setExecutablesList(const ExecutablesList &executablesList); - - void setModListSorting(int index); - void setESPListSorting(int index); - void setCompactDownloads(bool compact); - - bool setCurrentProfile(int index); - bool setCurrentProfile(const QString &name); - - void createFirstProfile(); - - void spawnProgram(const QString &fileName, const QString &argumentsArg, - const QString &profileName, const QDir ¤tDirectory); - - void loadPlugins(); - - virtual IGameInfo &gameInfo() const; - virtual QString profileName() const; - virtual QString profilePath() const; - virtual VersionInfo appVersion() const; - virtual IModInterface *getMod(const QString &name); - virtual IModInterface *createMod(const QString &name); - virtual bool removeMod(IModInterface *mod); - virtual void modDataChanged(IModInterface *mod); - virtual QVariant pluginSetting(const QString &pluginName, const QString &key) const; - - void addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, ModInfo::Ptr info); - -public slots: - - void displayColumnSelection(const QPoint &pos); - - void externalMessage(const QString &message); - void modorder_changed(); - void esplist_changed(); - void refresher_progress(int percent); - void directory_refreshed(); - - void toolPluginInvoke(); - -signals: - - /** - * @brief emitted after a mod has been installed - * @node this is currently only used for tutorials - */ - void modInstalled(); - - /** - * @brief emitted after the information dialog has been closed - */ - void modInfoDisplayed(); - - /** - * @brief emitted when the selected style changes - */ - void styleChanged(const QString &styleFile); - -protected: - - virtual void showEvent(QShowEvent *event); - virtual void closeEvent(QCloseEvent *event); - virtual bool eventFilter(QObject *obj, QEvent *event); - virtual void resizeEvent(QResizeEvent *event); - -private: - - void actionToToolButton(QAction *&sourceAction); - bool verifyPlugin(IPlugin *plugin); - void registerPluginTool(IPluginTool *tool); - bool registerPlugin(QObject *pluginObj); - - void updateToolBar(); - void activateSelectedProfile(); - - void setBrowserGeometry(const QByteArray &geometry); - void setExecutableIndex(int index); - - bool nexusLogin(); - - void saveCurrentESPList(); - - bool testForSteam(); - void startSteam(); - - HANDLE spawnBinaryDirect(const QFileInfo &binary, const QString &arguments, const QString &profileName, const QDir ¤tDirectory, const QString &steamAppID); - void spawnBinary(const QFileInfo &binary, const QString &arguments = "", const QDir ¤tDirectory = QDir(), bool closeAfterStart = true, const QString &steamAppID = ""); - - void updateTo(QTreeWidgetItem *subTree, const std::wstring &directorySoFar, const DirectoryEntry &directoryEntry, bool conflictsOnly); - void refreshDirectoryStructure(); - bool refreshProfiles(bool selectProfile = true); - void refreshExecutablesList(); - void installMod(const QString &fileName); - void installMod(); - bool modifyExecutablesDialog(); - void displayModInformation(ModInfo::Ptr modInfo, unsigned int index, int tab); - void displayModInformation(int row, int tab = 0); - void testExtractBSA(int modIndex); - - void writeDataToFile(QFile &file, const QString &directory, const DirectoryEntry &directoryEntry); - - void renameModInList(QFile &modList, const QString &oldName, const QString &newName); - - void refreshFilters(); - - void saveCategoriesFromMenu(QMenu *menu, int modRow); - - bool addCategories(QMenu *menu, int targetID); - - void updateDownloadListDelegate(); - - // remove invalid category-references from mods - void fixCategories(); - - void storeSettings(); - - bool queryLogin(QString &username, QString &password); - - void createHelpWidget(); - - bool extractProgress(QProgressDialog &extractProgress, int percentage, std::string fileName); - - void checkBSAList(); - - bool checkForProblems(QString &problemDescription); - - int getBinaryExecuteInfo(const QFileInfo &targetInfo, QFileInfo &binaryInfo, QString &arguments); - void addFilterItem(const QString &name, int categoryID); - - void setCategoryListVisible(bool visible); - - void updateProblemsButton(); - - SaveGameGamebryo *getSaveGame(const QString &name); - SaveGameGamebryo *getSaveGame(QListWidgetItem *item); - - void displaySaveGameInfo(const SaveGameGamebryo *save, QPoint pos); - - HANDLE nextChildProcess(); - - bool errorReported(QString &logFile); - -private: - - Ui::MainWindow *ui; - - TutorialControl m_Tutorial; - - QString m_ExeName; - - int m_OldProfileIndex; - - QThread m_RefresherThread; - DirectoryRefresher m_DirectoryRefresher; - DirectoryEntry *m_DirectoryStructure; - std::vector m_ModNameList; // the mod-list to go with the directory structure - QProgressBar *m_RefreshProgress; - bool m_Refreshing; - - ModList m_ModList; - ModListSortProxy *m_ModListSortProxy; - PluginList m_PluginList; - PluginListSortProxy *m_PluginListSortProxy; - - ExecutablesList m_ExecutablesList; - int m_OldExecutableIndex; - - QString m_GamePath; - - int m_ContextRow; - QTreeWidgetItem *m_ContextItem; - int m_SelectedSaveGame; - - Settings m_Settings; - - NexusDialog m_NexusDialog; - DownloadManager m_DownloadManager; - InstallationManager m_InstallationManager; - - QTranslator *m_Translator; - QTranslator *m_TranslatorQt; - - SelfUpdater m_Updater; - - CategoryFactory &m_CategoryFactory; - - Profile *m_CurrentProfile; - - int m_ModsToUpdate; - - QStringList m_PendingDownloads; - QList > m_PostLoginTasks; - bool m_AskForNexusPW; - bool m_LoginAttempted; - - QStringList m_DefaultArchives; - QStringList m_ActiveArchives; - bool m_DirectoryUpdate; - bool m_ArchivesInit; - - QTime m_StartTime; - SaveGameInfoWidget *m_CurrentSaveView; - - IGameInfo *m_GameInfo; - - std::vector m_DiagnosisPlugins; - -private slots: - - void showMessage(const QString &message); - void showError(const QString &message); - - // main window actions - void helpTriggered(); - void issueTriggered(); - void wikiTriggered(); - void tutorialTriggered(); - void extractBSATriggered(); - - // modlist context menu - void installMod_clicked(); - void restoreBackup_clicked(); - void renameMod_clicked(); - void removeMod_clicked(); - void reinstallMod_clicked(); - void endorse_clicked(); - void unendorse_clicked(); - void visitOnNexus_clicked(); - void openExplorer_clicked(); - void information_clicked(); - // savegame context menu - void fixMods_clicked(); - // data-tree context menu - void writeDataToFile(); - void openDataFile(); - void addAsExecutable(); - void hideFile(); - void unhideFile(); - - void linkDesktop(); - void linkMenu(); - - void languageChange(const QString &newLanguage); - void modStatusChanged(unsigned int index); - void saveSelectionChanged(QListWidgetItem *newItem); - - bool saveCurrentLists(); - - void windowTutorialFinished(const QString &windowName); - - BSA::EErrorCode extractBSA(BSA::Archive &archive, BSA::Folder::Ptr folder, const QString &destination, QProgressDialog &extractProgress); - - void syncOverwrite(); - - void removeOrigin(const QString &name); - - void setPriorityMax(); - void setPriorityManually(); - void setPriorityMin(); - - void procError(QProcess::ProcessError error); - void procFinished(int exitCode, QProcess::ExitStatus exitStatus); - - // nexus related - void checkModsForUpdates(); - void nexusLinkActivated(const QString &link); - - void loginSuccessful(bool necessary); - void loginSuccessfulUpdate(bool necessary); - void loginFailed(const QString &message); - void loginFailedUpdate(const QString &message); - - void downloadRequestedNXM(const QString &url); - void downloadRequested(QNetworkReply *reply, int modID, const QString &fileName); - - void installDownload(int index); - void updateAvailable(); - - void motdReceived(const QString &motd); - void notEndorsedYet(); - - void originModified(int originID); - - void saveCategories(); - void savePrimaryCategory(); - void addPrimaryCategoryCandidates(); - - void modDetailsUpdated(bool success); - - void nxmUpdatesAvailable(const std::vector &modIDs, QVariant userData, QVariant resultData, int requestID); - void nxmEndorsementToggled(int, QVariant, QVariant resultData, int); - void nxmRequestFailed(int modID, QVariant userData, int requestID, const QString &errorString); - - void editCategories(); - - void displayModInformation(const QString &modName, int tab); - - void modRenamed(const QString &oldName, const QString &newName); - - void hideSaveGameInfo(); - - void hookUpWindowTutorials(); - - void endorseMod(ModInfo::Ptr mod); - void cancelModListEditor(); - - void lockESPIndex(); - void unlockESPIndex(); - - void enableVisibleMods(); - void disableVisibleMods(); - -private slots: // ui slots - // actions - void on_actionAdd_Profile_triggered(); - void on_actionInstallMod_triggered(); - void on_actionModify_Executables_triggered(); - void on_actionNexus_triggered(); - void on_actionProblems_triggered(); - void on_actionSettings_triggered(); - void on_actionUpdate_triggered(); - void on_actionEndorseMO_triggered(); - - void on_bsaList_customContextMenuRequested(const QPoint &pos); - void on_bsaList_itemChanged(QTreeWidgetItem *item, int column); - void on_btnRefreshData_clicked(); - void on_btnSave_clicked(); - void on_categoriesList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); - 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); - - void on_espList_customContextMenuRequested(const QPoint &pos); - void on_displayCategoriesBtn_toggled(bool checked); -}; - -#endif // MAINWINDOW_H +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "executableslist.h" +#include "modlist.h" +#include "pluginlist.h" +#define WIN32_LEAN_AND_MEAN +#include +#include +#include "directoryrefresher.h" +#include +#include +#include +#include "settings.h" +#include "nexusdialog.h" +#include "downloadmanager.h" +#include "installationmanager.h" +#include "selfupdater.h" +#include "savegamegamebyro.h" +#include "modlistsortproxy.h" +#include "pluginlistsortproxy.h" +#include "tutorialcontrol.h" +#include "savegameinfowidgetgamebryo.h" + +namespace Ui { + class MainWindow; +} + +class QToolButton; + +class MainWindow : public QMainWindow, public IOrganizer +{ + Q_OBJECT + +public: + explicit MainWindow(const QString &exeName, QSettings &initSettings, QWidget *parent = 0); + ~MainWindow(); + + void readSettings(); + + bool addProfile(); + void refreshLists(); + void refreshESPList(); + void refreshBSAList(); + void refreshDataTree(); + void refreshSaveList(); + void refreshModList(); + + void setExecutablesList(const ExecutablesList &executablesList); + + void setModListSorting(int index); + void setESPListSorting(int index); + void setCompactDownloads(bool compact); + + bool setCurrentProfile(int index); + bool setCurrentProfile(const QString &name); + + void createFirstProfile(); + + void spawnProgram(const QString &fileName, const QString &argumentsArg, + const QString &profileName, const QDir ¤tDirectory); + + void loadPlugins(); + + virtual IGameInfo &gameInfo() const; + virtual QString profileName() const; + virtual QString profilePath() const; + virtual VersionInfo appVersion() const; + virtual IModInterface *getMod(const QString &name); + virtual IModInterface *createMod(const QString &name); + virtual bool removeMod(IModInterface *mod); + virtual void modDataChanged(IModInterface *mod); + virtual QVariant pluginSetting(const QString &pluginName, const QString &key) const; + + void addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, ModInfo::Ptr info); + +public slots: + + void displayColumnSelection(const QPoint &pos); + + void externalMessage(const QString &message); + void modorder_changed(); + void esplist_changed(); + void refresher_progress(int percent); + void directory_refreshed(); + + void toolPluginInvoke(); + +signals: + + /** + * @brief emitted after a mod has been installed + * @node this is currently only used for tutorials + */ + void modInstalled(); + + /** + * @brief emitted after the information dialog has been closed + */ + void modInfoDisplayed(); + + /** + * @brief emitted when the selected style changes + */ + void styleChanged(const QString &styleFile); + +protected: + + virtual void showEvent(QShowEvent *event); + virtual void closeEvent(QCloseEvent *event); + virtual bool eventFilter(QObject *obj, QEvent *event); + virtual void resizeEvent(QResizeEvent *event); + +private: + + void actionToToolButton(QAction *&sourceAction); + bool verifyPlugin(IPlugin *plugin); + void registerPluginTool(IPluginTool *tool); + bool registerPlugin(QObject *pluginObj); + + void updateToolBar(); + void activateSelectedProfile(); + + void setBrowserGeometry(const QByteArray &geometry); + void setExecutableIndex(int index); + + bool nexusLogin(); + + void saveCurrentESPList(); + + bool testForSteam(); + void startSteam(); + + HANDLE spawnBinaryDirect(const QFileInfo &binary, const QString &arguments, const QString &profileName, const QDir ¤tDirectory, const QString &steamAppID); + void spawnBinary(const QFileInfo &binary, const QString &arguments = "", const QDir ¤tDirectory = QDir(), bool closeAfterStart = true, const QString &steamAppID = ""); + + void updateTo(QTreeWidgetItem *subTree, const std::wstring &directorySoFar, const DirectoryEntry &directoryEntry, bool conflictsOnly); + void refreshDirectoryStructure(); + bool refreshProfiles(bool selectProfile = true); + void refreshExecutablesList(); + void installMod(const QString &fileName); + void installMod(); + bool modifyExecutablesDialog(); + void displayModInformation(ModInfo::Ptr modInfo, unsigned int index, int tab); + void displayModInformation(int row, int tab = 0); + void testExtractBSA(int modIndex); + + void writeDataToFile(QFile &file, const QString &directory, const DirectoryEntry &directoryEntry); + + void renameModInList(QFile &modList, const QString &oldName, const QString &newName); + + void refreshFilters(); + + void saveCategoriesFromMenu(QMenu *menu, int modRow); + + bool addCategories(QMenu *menu, int targetID); + + void updateDownloadListDelegate(); + + // remove invalid category-references from mods + void fixCategories(); + + void storeSettings(); + + bool queryLogin(QString &username, QString &password); + + void createHelpWidget(); + + bool extractProgress(QProgressDialog &extractProgress, int percentage, std::string fileName); + + void checkBSAList(); + + bool checkForProblems(QString &problemDescription); + + int getBinaryExecuteInfo(const QFileInfo &targetInfo, QFileInfo &binaryInfo, QString &arguments); + QTreeWidgetItem *addFilterItem(QTreeWidgetItem *root, const QString &name, int categoryID); + void addCategoryFilters(QTreeWidgetItem *root, const std::set &categoriesUsed, int targetID); + + void setCategoryListVisible(bool visible); + + void updateProblemsButton(); + + SaveGameGamebryo *getSaveGame(const QString &name); + SaveGameGamebryo *getSaveGame(QListWidgetItem *item); + + void displaySaveGameInfo(const SaveGameGamebryo *save, QPoint pos); + + HANDLE nextChildProcess(); + + bool errorReported(QString &logFile); + + QIcon iconForExecutable(const QString &filePath); + +private: + + Ui::MainWindow *ui; + + TutorialControl m_Tutorial; + + QString m_ExeName; + + int m_OldProfileIndex; + + QThread m_RefresherThread; + DirectoryRefresher m_DirectoryRefresher; + DirectoryEntry *m_DirectoryStructure; + std::vector m_ModNameList; // the mod-list to go with the directory structure + QProgressBar *m_RefreshProgress; + bool m_Refreshing; + + ModList m_ModList; + ModListSortProxy *m_ModListSortProxy; + PluginList m_PluginList; + PluginListSortProxy *m_PluginListSortProxy; + + ExecutablesList m_ExecutablesList; + int m_OldExecutableIndex; + + QString m_GamePath; + + int m_ContextRow; + QTreeWidgetItem *m_ContextItem; + int m_SelectedSaveGame; + + Settings m_Settings; + + NexusDialog m_NexusDialog; + DownloadManager m_DownloadManager; + InstallationManager m_InstallationManager; + + QTranslator *m_Translator; + QTranslator *m_TranslatorQt; + + SelfUpdater m_Updater; + + CategoryFactory &m_CategoryFactory; + + Profile *m_CurrentProfile; + + int m_ModsToUpdate; + + QStringList m_PendingDownloads; + QList > m_PostLoginTasks; + bool m_AskForNexusPW; + bool m_LoginAttempted; + + QStringList m_DefaultArchives; + QStringList m_ActiveArchives; + bool m_DirectoryUpdate; + bool m_ArchivesInit; + + QTime m_StartTime; + SaveGameInfoWidget *m_CurrentSaveView; + + IGameInfo *m_GameInfo; + + std::vector m_DiagnosisPlugins; + +private slots: + + void showMessage(const QString &message); + void showError(const QString &message); + + // main window actions + void helpTriggered(); + void issueTriggered(); + void wikiTriggered(); + void tutorialTriggered(); + void extractBSATriggered(); + + // modlist context menu + void installMod_clicked(); + void restoreBackup_clicked(); + void renameMod_clicked(); + void removeMod_clicked(); + void reinstallMod_clicked(); + void endorse_clicked(); + void unendorse_clicked(); + void visitOnNexus_clicked(); + void openExplorer_clicked(); + void information_clicked(); + // savegame context menu + void fixMods_clicked(); + // data-tree context menu + void writeDataToFile(); + void openDataFile(); + void addAsExecutable(); + void hideFile(); + void unhideFile(); + + void linkToolbar(); + void linkDesktop(); + void linkMenu(); + + void languageChange(const QString &newLanguage); + void modStatusChanged(unsigned int index); + void saveSelectionChanged(QListWidgetItem *newItem); + + bool saveCurrentLists(); + + void windowTutorialFinished(const QString &windowName); + + BSA::EErrorCode extractBSA(BSA::Archive &archive, BSA::Folder::Ptr folder, const QString &destination, QProgressDialog &extractProgress); + + void syncOverwrite(); + + void removeOrigin(const QString &name); + + void setPriorityMax(); + void setPriorityManually(); + void setPriorityMin(); + + void procError(QProcess::ProcessError error); + void procFinished(int exitCode, QProcess::ExitStatus exitStatus); + + // nexus related + void checkModsForUpdates(); + void nexusLinkActivated(const QString &link); + + void loginSuccessful(bool necessary); + void loginSuccessfulUpdate(bool necessary); + void loginFailed(const QString &message); + void loginFailedUpdate(const QString &message); + + void downloadRequestedNXM(const QString &url); + void downloadRequested(QNetworkReply *reply, int modID, const QString &fileName); + + void installDownload(int index); + void updateAvailable(); + + void motdReceived(const QString &motd); + void notEndorsedYet(); + + void originModified(int originID); + + void saveCategories(); + void savePrimaryCategory(); + void addPrimaryCategoryCandidates(); + + void modDetailsUpdated(bool success); + + void nxmUpdatesAvailable(const std::vector &modIDs, QVariant userData, QVariant resultData, int requestID); + void nxmEndorsementToggled(int, QVariant, QVariant resultData, int); + void nxmRequestFailed(int modID, QVariant userData, int requestID, const QString &errorString); + + void editCategories(); + + void displayModInformation(const QString &modName, int tab); + + void modRenamed(const QString &oldName, const QString &newName); + + void hideSaveGameInfo(); + + void hookUpWindowTutorials(); + + void endorseMod(ModInfo::Ptr mod); + void cancelModListEditor(); + + void lockESPIndex(); + void unlockESPIndex(); + + void enableVisibleMods(); + void disableVisibleMods(); + void exportModListCSV(); + + void startExeAction(); + +private slots: // ui slots + // actions + void on_actionAdd_Profile_triggered(); + void on_actionInstallMod_triggered(); + void on_actionModify_Executables_triggered(); + void on_actionNexus_triggered(); + void on_actionProblems_triggered(); + void on_actionSettings_triggered(); + void on_actionUpdate_triggered(); + void on_actionEndorseMO_triggered(); + + void on_bsaList_customContextMenuRequested(const QPoint &pos); + void on_bsaList_itemChanged(QTreeWidgetItem *item, int column); + void on_btnRefreshData_clicked(); + void on_btnSave_clicked(); + void on_categoriesList_currentItemChanged(QTreeWidgetItem *current, QTreeWidgetItem *previous); + 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); + + void on_espList_customContextMenuRequested(const QPoint &pos); + void on_displayCategoriesBtn_toggled(bool checked); +}; + +#endif // MAINWINDOW_H diff --git a/src/mainwindow.ui b/src/mainwindow.ui index c6742dfd..517434a7 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -55,7 +55,7 @@ 1 - + 100 @@ -71,6 +71,14 @@ Qt::CustomContextMenu + + false + + + + 1 + + diff --git a/src/modinfo.cpp b/src/modinfo.cpp index a46774c2..0a0cde68 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -17,828 +17,829 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include "modinfo.h" -#include "utility.h" -#include "installationtester.h" -#include "categories.h" -#include "report.h" -#include "modinfodialog.h" -#include "overwriteinfodialog.h" -#include "json.h" -#include "messagedialog.h" - -#include -#include - -#include - -#include -#include -#include -#include - - -std::vector ModInfo::s_Collection; -std::map ModInfo::s_ModsByName; -std::map ModInfo::s_ModsByModID; -int ModInfo::s_NextID; -QMutex ModInfo::s_Mutex(QMutex::Recursive); - -QString ModInfo::s_HiddenExt(".mohidden"); - - -static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS) -{ - return QString::compare(LHS->name(), RHS->name(), Qt::CaseInsensitive) < 0; -} - -ModInfo::NexusFileInfo::NexusFileInfo(const QString &data) -{ - QVariantList result = QtJson::Json::parse(data).toList(); - - while (result.length() < 7) { - result.append(QVariant()); - } - id = result.at(0).toInt(); - name = result.at(1).toString(); - url = result.at(2).toString(); - version = result.at(3).toString(); - description = result.at(4).toString(); - category = result.at(5).toInt(); - size = result.at(6).toInt(); -} - - -QString ModInfo::NexusFileInfo::toString() const -{ - return QString("[ %1,\"%2\",\"%3\",\"%4\",\"%5\",%6,%7 ]") - .arg(id) - .arg(name) - .arg(url) - .arg(version) - .arg(description.mid(0).replace("\"", "'")) - .arg(category) - .arg(size); -} - - -ModInfo::Ptr ModInfo::createFrom(const QDir &dir, DirectoryEntry **directoryStructure) -{ - QMutexLocker locker(&s_Mutex); -// int id = s_NextID++; - static QRegExp backupExp(".*backup[0-9]*"); - ModInfo::Ptr result; - if (backupExp.exactMatch(dir.dirName())) { - result = ModInfo::Ptr(new ModInfoBackup(dir, directoryStructure)); - } else { - result = ModInfo::Ptr(new ModInfoRegular(dir, directoryStructure)); - } - s_Collection.push_back(result); - return result; -} - - -void ModInfo::createFromOverwrite() -{ - QMutexLocker locker(&s_Mutex); - - s_Collection.push_back(ModInfo::Ptr(new ModInfoOverwrite)); -} - - -unsigned int ModInfo::getNumMods() -{ - QMutexLocker locker(&s_Mutex); - return s_Collection.size(); -} - - -ModInfo::Ptr ModInfo::getByIndex(unsigned int index) -{ - QMutexLocker locker(&s_Mutex); - - if (index >= s_Collection.size()) { - throw MyException(tr("invalid index %1").arg(index)); - } - return s_Collection[index]; -} - - -ModInfo::Ptr ModInfo::getByModID(int modID, bool missingAcceptable) -{ - QMutexLocker locker(&s_Mutex); - - std::map::iterator iter = s_ModsByModID.find(modID); - if (iter == s_ModsByModID.end()) { - if (missingAcceptable) { - return ModInfo::Ptr(); - } else { - throw MyException(tr("invalid mod id %1").arg(modID)); - } - } - - return getByIndex(iter->second); -} - - -bool ModInfo::removeMod(unsigned int index) -{ - QMutexLocker locker(&s_Mutex); - - if (index >= s_Collection.size()) { - throw MyException(tr("invalid index %1").arg(index)); - } - - // update the indices first - ModInfo::Ptr modInfo = s_Collection[index]; - s_ModsByName.erase(s_ModsByName.find(modInfo->name())); - - //TODO this is a bit more complicated since multiple mods may have the - // same mod id but only one appears in the index - std::map::iterator iter = s_ModsByModID.find(modInfo->getNexusID()) ; - if ((iter != s_ModsByModID.end()) && - (iter->second == index)) { - s_ModsByModID.erase(iter); - } - - // physically remove the mod directory - //TODO the return value is ignored because the indices were already removed here, so stopping - // would cause data inconsistencies. Instead we go through with the removal but the mod will show up - // again if the user refreshes - modInfo->remove(); - - // finally, remove the mod from the collection - s_Collection.erase(s_Collection.begin() + index); - - // and update the indices - updateIndices(); - return true; -} - - -unsigned int ModInfo::getIndex(const QString &name) -{ - QMutexLocker locker(&s_Mutex); - - std::map::iterator iter = s_ModsByName.find(name); - if (iter == s_ModsByName.end()) { - return UINT_MAX; - } - - return iter->second; -} - -unsigned int ModInfo::findMod(const boost::function &filter) -{ - for (unsigned int i = 0U; i < s_Collection.size(); ++i) { - if (filter(s_Collection[i])) { - return i; - } - } - return UINT_MAX; -} - - -void ModInfo::updateFromDisc(const QString &modDirectory, DirectoryEntry **directoryStructure) -{ - QMutexLocker lock(&s_Mutex); - s_Collection.clear(); - s_NextID = 0; - // list all directories in the mod directory and make a mod out of each - QDir mods(QDir::fromNativeSeparators(modDirectory)); - mods.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); - QDirIterator modIter(mods); - while (modIter.hasNext()) { - createFrom(QDir(modIter.next()), directoryStructure); - } - - createFromOverwrite(); - - std::sort(s_Collection.begin(), s_Collection.end(), ByName); - - updateIndices(); -} - - -void ModInfo::updateIndices() -{ - s_ModsByName.clear(); - s_ModsByModID.clear(); - QRegExp backupRegEx(".*backup[0-9]*$"); - - for (unsigned int i = 0; i < s_Collection.size(); ++i) { - QString modName = s_Collection[i]->name(); - int modID = s_Collection[i]->getNexusID(); - s_ModsByName[modName] = i; - - // don't overwrite a modid-entry with a backup entry. This is a bit of a workaround - if ((s_ModsByModID.find(modID) == s_ModsByModID.end()) || - !backupRegEx.exactMatch(modName)) { - s_ModsByModID[modID] = i; - } - } -} - - -ModInfo::ModInfo() - : m_Valid(false), m_PrimaryCategory(-1) -{ -} - - -void ModInfo::checkChunkForUpdate(const std::vector &modIDs, QObject *receiver) -{ - if (modIDs.size() != 0) { - NexusInterface::instance()->requestUpdates(modIDs, receiver, QVariant()); - } -} - - -int ModInfo::checkAllForUpdate(QObject *receiver) -{ - int result = 0; - std::vector modIDs; - - modIDs.push_back(GameInfo::instance().getNexusModID()); - - for (std::vector::iterator iter = s_Collection.begin(); - iter != s_Collection.end(); ++iter) { - if ((*iter)->canBeUpdated()) { - modIDs.push_back((*iter)->getNexusID()); - if (modIDs.size() >= 255) { - checkChunkForUpdate(modIDs, receiver); - modIDs.clear(); - } - } - } - - checkChunkForUpdate(modIDs, receiver); - - return result; -} - -void ModInfo::setVersion(const VersionInfo &version) -{ - m_Version = version; -} - - -bool ModInfo::categorySet(int categoryID) const -{ - for (std::set::const_iterator iter = m_Categories.begin(); iter != m_Categories.end(); ++iter) { - if ((*iter == categoryID) || - (CategoryFactory::instance().isDecendantOf(*iter, categoryID))) { - return true; - } - } - - return false; -} - - -void ModInfo::testValid() -{ - m_Valid = false; - QDirIterator dirIter(absolutePath()); - while (dirIter.hasNext()) { - dirIter.next(); - if (dirIter.fileInfo().isDir()) { - if (InstallationTester::isTopLevelDirectory(dirIter.fileName())) { - m_Valid = true; - break; - } - } else { - if (InstallationTester::isTopLevelSuffix(dirIter.fileName())) { - m_Valid = true; - break; - } - } - } - - // NOTE: in Qt 4.7 it seems that QDirIterator leaves a file handle open if it is not iterated to the - // end - while (dirIter.hasNext()) { - dirIter.next(); - } -} - - -ModInfoRegular::ModInfoRegular(const QDir &path, DirectoryEntry **directoryStructure) - : ModInfo(), m_Name(path.dirName()), m_Path(path.absolutePath()), m_MetaInfoChanged(false), - m_EndorsedState(ENDORSED_UNKNOWN), m_DirectoryStructure(directoryStructure) -{ - testValid(); - - // read out the meta-file for information - QString metaFileName = path.absoluteFilePath("meta.ini"); - QSettings metaFile(metaFileName, QSettings::IniFormat); - - m_Notes = metaFile.value("notes", "").toString(); - m_NexusID = metaFile.value("modid", -1).toInt(); - m_Version.parse(metaFile.value("version", "").toString()); - m_NewestVersion = metaFile.value("newestVersion", "").toString(); - m_InstallationFile = metaFile.value("installationFile", "").toString(); - m_NexusDescription = metaFile.value("nexusDescription", "").toString(); - m_LastNexusQuery = QDateTime::fromString(metaFile.value("lastNexusQuery", "").toString(), Qt::ISODate); - if (metaFile.contains("endorsed")) { - m_EndorsedState = metaFile.value("endorsed", false).toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; - } - - int numNexusFiles = metaFile.beginReadArray("nexusFiles"); - for (int i = 0; i < numNexusFiles; ++i) { - metaFile.setArrayIndex(i); - QString infoString = metaFile.value("info", "").toString(); - NexusFileInfo info(infoString); - m_NexusFileInfos.push_back(info); - } - metaFile.endArray(); - - QString categoriesString = metaFile.value("category", "").toString(); - - QStringList categories = categoriesString.split(',', QString::SkipEmptyParts); - for (QStringList::iterator iter = categories.begin(); iter != categories.end(); ++iter) { - bool ok = false; - int categoryID = iter->toInt(&ok); - if (categoryID < 0) { - // ignore invalid id - continue; - } - if (ok && (categoryID != 0) && (CategoryFactory::instance().categoryExists(categoryID))) { - m_Categories.insert(categoryID); - if (iter == categories.begin()) { - m_PrimaryCategory = categoryID; - } - } - } - - connect(&m_NexusBridge, SIGNAL(nxmDescriptionAvailable(int,QVariant,QVariant)), this, SLOT(nxmDescriptionAvailable(int,QVariant,QVariant))); - connect(&m_NexusBridge, SIGNAL(nxmFilesAvailable(int,QVariant,QVariant)), this, SLOT(nxmFilesAvailable(int,QVariant,QVariant))); - connect(&m_NexusBridge, SIGNAL(nxmEndorsementToggled(int,QVariant,QVariant)), this, SLOT(nxmEndorsementToggled(int,QVariant,QVariant))); - connect(&m_NexusBridge, SIGNAL(nxmRequestFailed(int,QVariant,QString)), this, SLOT(nxmRequestFailed(int,QVariant,QString))); -} - - -ModInfoRegular::~ModInfoRegular() -{ - try { - //TODO this may cause the meta-file and the directory to be - // re-created after a remove - saveMeta(); - } catch (const std::exception &e) { - qCritical("failed to save meta information for \"%s\": %s", - m_Name.toUtf8().constData(), e.what()); - } -} - - -void ModInfoRegular::saveMeta() -{ - if (m_MetaInfoChanged) { - if (QFile::exists(absolutePath().append("/meta.ini"))) { - QSettings metaFile(absolutePath().append("/meta.ini"), QSettings::IniFormat); - if (metaFile.status() == QSettings::NoError) { - std::set temp = m_Categories; - temp.erase(m_PrimaryCategory); - metaFile.setValue("category", QString("%1").arg(m_PrimaryCategory) + "," + SetJoin(temp, ",")); - metaFile.setValue("newestVersion", m_NewestVersion.canonicalString()); - metaFile.setValue("version", m_Version.canonicalString()); - metaFile.setValue("modid", m_NexusID); - metaFile.setValue("notes", m_Notes); - metaFile.setValue("nexusDescription", m_NexusDescription); - metaFile.setValue("lastNexusQuery", m_LastNexusQuery.toString(Qt::ISODate)); - if (m_EndorsedState != ENDORSED_UNKNOWN) { - metaFile.setValue("endorsed", m_EndorsedState == ENDORSED_TRUE ? true : false); - } - - metaFile.beginWriteArray("nexusFiles"); - for (int i = 0; i < m_NexusFileInfos.size(); ++i) { - metaFile.setArrayIndex(i); - metaFile.setValue("info", m_NexusFileInfos.at(i).toString()); - } - metaFile.endArray(); - } else { - reportError(tr("failed to write %1/meta.ini: %2").arg(absolutePath()).arg(metaFile.status())); - } - } else { - qWarning("mod %s has no meta.ini at %s/meta.ini", m_Name.toUtf8().constData(), absolutePath().toUtf8().constData()); - } - m_MetaInfoChanged = false; - } -} - - -bool ModInfoRegular::updateAvailable() const -{ - return m_NewestVersion.isValid() && (m_Version < m_NewestVersion); -} - - -void ModInfoRegular::nxmDescriptionAvailable(int, QVariant, QVariant resultData) -{ - QVariantMap result = resultData.toMap(); - m_NewestVersion.parse(result["version"].toString()); - m_NexusDescription = result["description"].toString(); - m_EndorsedState = result["voted_by_user"].toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; - m_NexusBridge.requestFiles(m_NexusID, QVariant()); -} - - -void ModInfoRegular::nxmFilesAvailable(int, QVariant, QVariant resultData) -{ - m_NexusFileInfos.clear(); - - QVariantList result = resultData.toList(); - - foreach(QVariant file, result) { - QVariantMap fileInfo = file.toMap(); - - m_NexusFileInfos.push_back(NexusFileInfo(fileInfo["id"].toInt(), - fileInfo["name"].toString(), - fileInfo["uri"].toString(), - fileInfo["version"].toString(), - fileInfo["description"].toString(), - fileInfo["category_id"].toInt(), - fileInfo["size"].toInt())); - } - - m_LastNexusQuery = QDateTime::currentDateTime(); - m_MetaInfoChanged = true; - saveMeta(); - emit modDetailsUpdated(true); -} - - -void ModInfoRegular::nxmEndorsementToggled(int, QVariant, QVariant resultData) -{ - m_EndorsedState = resultData.toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; - m_MetaInfoChanged = true; - saveMeta(); - emit modDetailsUpdated(true); -} - - -void ModInfoRegular::nxmRequestFailed(int, QVariant userData, const QString &errorMessage) -{ - QString fullMessage = errorMessage; - if (userData.canConvert() && (userData.toInt() == 1)) { - fullMessage += "\nNexus will reject endorsements within 15 Minutes of a failed attempt, the error message may be misleading."; - } - if (QApplication::activeWindow() != NULL) { - MessageDialog::showMessage(fullMessage, QApplication::activeWindow()); - } - emit modDetailsUpdated(false); -} - - -bool ModInfoRegular::updateNXMInfo() -{ - if (m_NexusID > 0) { - m_NexusBridge.requestDescription(m_NexusID, QVariant()); - return true; - } - return false; -} - - -void ModInfoRegular::setCategory(int categoryID, bool active) -{ - m_MetaInfoChanged = true; - - if (active) { - m_Categories.insert(categoryID); - if (m_PrimaryCategory == -1) { - m_PrimaryCategory = categoryID; - } - } else { - std::set::iterator iter = m_Categories.find(categoryID); - if (iter != m_Categories.end()) { - m_Categories.erase(iter); - } - if (categoryID == m_PrimaryCategory) { - if (m_Categories.size() == 0) { - m_PrimaryCategory = -1; - } else { - m_PrimaryCategory = *(m_Categories.begin()); - } - } - } -} - - -bool ModInfoRegular::setName(const QString &name) -{ - if (name.contains('/') || name.contains('\\')) { - return false; - } - - QString newPath = m_Path.mid(0).replace(m_Path.length() - m_Name.length(), m_Name.length(), name); - QDir modDir(m_Path.mid(0, m_Path.length() - m_Name.length())); - if (m_Name.compare(name, Qt::CaseInsensitive) == 0) { - QString tempName = name; - tempName.append("_temp"); - while (modDir.exists(tempName)) { - tempName.append("_"); - } - if (!modDir.rename(m_Name, tempName)) { - return false; - } - if (!modDir.rename(tempName, name)) { - qCritical("rename to final name failed after successful rename to intermediate name"); - modDir.rename(tempName, m_Name); - return false; - } - } else { - if (!modDir.rename(m_Name, name)) { - return false; - } - } - - std::map::iterator nameIter = s_ModsByName.find(m_Name); - unsigned int index = nameIter->second; - s_ModsByName.erase(nameIter); - - m_Name = name; - m_Path = newPath; - - s_ModsByName[m_Name] = index; - - std::sort(s_Collection.begin(), s_Collection.end(), ByName); - - updateIndices(); - - return true; -} - -void ModInfoRegular::setNotes(const QString ¬es) -{ - m_Notes = notes; -} - -void ModInfoRegular::setVersion(const VersionInfo &version) -{ - m_Version = version; - m_MetaInfoChanged = true; -} - -void ModInfoRegular::setNexusDescription(const QString &description) -{ - m_NexusDescription = description; - m_MetaInfoChanged = true; -} - - -void ModInfoRegular::setIsEndorsed(bool endorsed) -{ - m_EndorsedState = endorsed ? ENDORSED_TRUE : ENDORSED_FALSE; - m_MetaInfoChanged = true; -} - - -bool ModInfoRegular::remove() -{ - m_MetaInfoChanged = false; - return removeDir(absolutePath()); -} - -void ModInfoRegular::endorse(bool doEndorse) -{ - if (doEndorse != (m_EndorsedState == ENDORSED_TRUE)) { - m_NexusBridge.requestToggleEndorsement(getNexusID(), doEndorse, QVariant(1)); - } -} - - -QString ModInfoRegular::absolutePath() const -{ - return m_Path; -} - - -std::vector ModInfoRegular::getFlags() const -{ - std::vector result; - if (!isValid()) { - result.push_back(ModInfo::FLAG_INVALID); - } - if ((m_NexusID != -1) && (endorsedState() == ENDORSED_FALSE)) { - result.push_back(ModInfo::FLAG_NOTENDORSED); - } - switch (isConflicted()) { - case CONFLICT_MIXED: { - result.push_back(ModInfo::FLAG_CONFLICT_MIXED); - } break; - case CONFLICT_OVERWRITE: { - result.push_back(ModInfo::FLAG_CONFLICT_OVERWRITE); - } break; - case CONFLICT_OVERWRITTEN: { - result.push_back(ModInfo::FLAG_CONFLICT_OVERWRITTEN); - } break; - case CONFLICT_REDUNDANT: { - result.push_back(ModInfo::FLAG_CONFLICT_REDUNDANT); - } break; - default: { /* NOP */ } - } - if (m_Notes.length() != 0) { - result.push_back(ModInfo::FLAG_NOTES); - } - return result; -} - - -int ModInfoRegular::getHighlight() const -{ - return isValid() ? HIGHLIGHT_NONE: HIGHLIGHT_INVALID; -} - - -QString ModInfoRegular::getDescription() const -{ - if (!isValid()) { - return tr("%1 contains no esp/esm and no asset (textures, meshes, interface, ...) directory").arg(name()); - } else { - const std::set &categories = getCategories(); - std::wostringstream categoryString; - categoryString << ToWString(tr("Categories:
    ")); - CategoryFactory &categoryFactory = CategoryFactory::instance(); - for (std::set::const_iterator catIter = categories.begin(); - catIter != categories.end(); ++catIter) { - if (catIter != categories.begin()) { - categoryString << " , "; - } - categoryString << "" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << ""; - } - - return ToQString(categoryString.str()); - } -} - -QString ModInfoRegular::notes() const -{ - return m_Notes; -} - -void ModInfoRegular::getNexusFiles(QList::const_iterator &begin, QList::const_iterator &end) -{ - begin = m_NexusFileInfos.begin(); - end = m_NexusFileInfos.end(); -} - -QString ModInfoRegular::getNexusDescription() const -{ - return m_NexusDescription; -} - - -ModInfoRegular::EEndorsedState ModInfoRegular::endorsedState() const -{ - return m_EndorsedState; -} - - -ModInfoRegular::EConflictType ModInfoRegular::isConflicted() const -{ - bool overwrite = false; - bool overwritten = false; - bool regular = false; - - int dataID = 0; - if ((*m_DirectoryStructure)->originExists(L"data")) { - dataID = (*m_DirectoryStructure)->getOriginByName(L"data").getID(); - } - - std::wstring name = ToWString(m_Name); - if ((*m_DirectoryStructure)->originExists(name)) { - FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name); - std::vector files = origin.getFiles(); - for (auto iter = files.begin(); iter != files.end() && (!overwrite || !overwritten || !regular); ++iter) { - const std::vector &alternatives = (*iter)->getAlternatives(); - if (alternatives.size() == 0) { - regular = true; - } else { - for (auto altIter = alternatives.begin(); altIter != alternatives.end(); ++altIter) { - // don't treat files overwritten in data as "conflict"+ - if (*altIter != dataID) { - bool ignore = false; - if ((*iter)->getOrigin(ignore) == origin.getID()) { - overwrite = true; - break; - } else { - overwritten = true; - break; - } - } - } - } - } - } - - if (overwrite && overwritten) return CONFLICT_MIXED; - else if (overwrite) return CONFLICT_OVERWRITE; - else if (overwritten) { - if (!regular) { - return CONFLICT_REDUNDANT; - } else { - return CONFLICT_OVERWRITTEN; - } - } - else return CONFLICT_NONE; -} - - -bool ModInfoRegular::isRedundant() const -{ - std::wstring name = ToWString(m_Name); - if ((*m_DirectoryStructure)->originExists(name)) { - FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name); - std::vector files = origin.getFiles(); - bool ignore = false; - for (auto iter = files.begin(); iter != files.end(); ++iter) { - if ((*iter)->getOrigin(ignore) == origin.getID()) { - return false; - } - } - return true; - } else { - return false; - } -} - - -QDateTime ModInfoRegular::getLastNexusQuery() const -{ - return m_LastNexusQuery; -} - -std::vector ModInfoRegular::getIniTweaks() const -{ - QString metaFileName = absolutePath().append("/meta.ini"); - QSettings metaFile(metaFileName, QSettings::IniFormat); - - std::vector result; - - int numTweaks = metaFile.beginReadArray("INI Tweaks"); - - if (numTweaks != 0) { - qDebug("%d active ini tweaks in %s", - numTweaks, metaFileName.toUtf8().constData()); - } - - for (int i = 0; i < numTweaks; ++i) { - metaFile.setArrayIndex(i); - QString filename = absolutePath().append("/INI Tweaks/").append(metaFile.value("name").toString()); - result.push_back(filename); - } - metaFile.endArray(); - return result; -} - -std::vector ModInfoBackup::getFlags() const -{ - std::vector result = ModInfoRegular::getFlags(); - result.insert(result.begin(), ModInfo::FLAG_BACKUP); - return result; -} - - -QString ModInfoBackup::getDescription() const -{ - return tr("This is the backup of a mod"); -} - - -ModInfoBackup::ModInfoBackup(const QDir &path, DirectoryEntry **directoryStructure) - : ModInfoRegular(path, directoryStructure) -{ -} - - -ModInfoOverwrite::ModInfoOverwrite() -{ - testValid(); - -} - - -QString ModInfoOverwrite::absolutePath() const -{ - return QDir::fromNativeSeparators(ToQString(GameInfo::instance().getOverwriteDir())); -} - -std::vector ModInfoOverwrite::getFlags() const -{ - std::vector result; - result.push_back(FLAG_OVERWRITE); - return result; -} - -int ModInfoOverwrite::getHighlight() const -{ - return (isValid() ? HIGHLIGHT_IMPORTANT : HIGHLIGHT_INVALID) | HIGHLIGHT_CENTER; -} - - -QString ModInfoOverwrite::getDescription() const -{ - return tr("This pseudo mod contains files from the virtual data tree that got " - "modified (i.e. by the construction kit)"); -} +#include "modinfo.h" +#include "utility.h" +#include "installationtester.h" +#include "categories.h" +#include "report.h" +#include "modinfodialog.h" +#include "overwriteinfodialog.h" +#include "json.h" +#include "messagedialog.h" + +#include +#include + +#include + +#include +#include +#include +#include + + +std::vector ModInfo::s_Collection; +std::map ModInfo::s_ModsByName; +std::map ModInfo::s_ModsByModID; +int ModInfo::s_NextID; +QMutex ModInfo::s_Mutex(QMutex::Recursive); + +QString ModInfo::s_HiddenExt(".mohidden"); + + +static bool ByName(const ModInfo::Ptr &LHS, const ModInfo::Ptr &RHS) +{ + return QString::compare(LHS->name(), RHS->name(), Qt::CaseInsensitive) < 0; +} + +ModInfo::NexusFileInfo::NexusFileInfo(const QString &data) +{ + QVariantList result = QtJson::Json::parse(data).toList(); + + while (result.length() < 7) { + result.append(QVariant()); + } + id = result.at(0).toInt(); + name = result.at(1).toString(); + url = result.at(2).toString(); + version = result.at(3).toString(); + description = result.at(4).toString(); + category = result.at(5).toInt(); + size = result.at(6).toInt(); +} + + +QString ModInfo::NexusFileInfo::toString() const +{ + return QString("[ %1,\"%2\",\"%3\",\"%4\",\"%5\",%6,%7 ]") + .arg(id) + .arg(name) + .arg(url) + .arg(version) + .arg(description.mid(0).replace("\"", "'")) + .arg(category) + .arg(size); +} + + +ModInfo::Ptr ModInfo::createFrom(const QDir &dir, DirectoryEntry **directoryStructure) +{ + QMutexLocker locker(&s_Mutex); +// int id = s_NextID++; + static QRegExp backupExp(".*backup[0-9]*"); + ModInfo::Ptr result; + if (backupExp.exactMatch(dir.dirName())) { + result = ModInfo::Ptr(new ModInfoBackup(dir, directoryStructure)); + } else { + result = ModInfo::Ptr(new ModInfoRegular(dir, directoryStructure)); + } + s_Collection.push_back(result); + return result; +} + + +void ModInfo::createFromOverwrite() +{ + QMutexLocker locker(&s_Mutex); + + s_Collection.push_back(ModInfo::Ptr(new ModInfoOverwrite)); +} + + +unsigned int ModInfo::getNumMods() +{ + QMutexLocker locker(&s_Mutex); + return s_Collection.size(); +} + + +ModInfo::Ptr ModInfo::getByIndex(unsigned int index) +{ + QMutexLocker locker(&s_Mutex); + + if (index >= s_Collection.size()) { + throw MyException(tr("invalid index %1").arg(index)); + } + return s_Collection[index]; +} + + +ModInfo::Ptr ModInfo::getByModID(int modID, bool missingAcceptable) +{ + QMutexLocker locker(&s_Mutex); + + std::map::iterator iter = s_ModsByModID.find(modID); + if (iter == s_ModsByModID.end()) { + if (missingAcceptable) { + return ModInfo::Ptr(); + } else { + throw MyException(tr("invalid mod id %1").arg(modID)); + } + } + + return getByIndex(iter->second); +} + + +bool ModInfo::removeMod(unsigned int index) +{ + QMutexLocker locker(&s_Mutex); + + if (index >= s_Collection.size()) { + throw MyException(tr("invalid index %1").arg(index)); + } + + // update the indices first + ModInfo::Ptr modInfo = s_Collection[index]; + s_ModsByName.erase(s_ModsByName.find(modInfo->name())); + + //TODO this is a bit more complicated since multiple mods may have the + // same mod id but only one appears in the index + std::map::iterator iter = s_ModsByModID.find(modInfo->getNexusID()) ; + if ((iter != s_ModsByModID.end()) && + (iter->second == index)) { + s_ModsByModID.erase(iter); + } + + // physically remove the mod directory + //TODO the return value is ignored because the indices were already removed here, so stopping + // would cause data inconsistencies. Instead we go through with the removal but the mod will show up + // again if the user refreshes + modInfo->remove(); + + // finally, remove the mod from the collection + s_Collection.erase(s_Collection.begin() + index); + + // and update the indices + updateIndices(); + return true; +} + + +unsigned int ModInfo::getIndex(const QString &name) +{ + QMutexLocker locker(&s_Mutex); + + std::map::iterator iter = s_ModsByName.find(name); + if (iter == s_ModsByName.end()) { + return UINT_MAX; + } + + return iter->second; +} + +unsigned int ModInfo::findMod(const boost::function &filter) +{ + for (unsigned int i = 0U; i < s_Collection.size(); ++i) { + if (filter(s_Collection[i])) { + return i; + } + } + return UINT_MAX; +} + + +void ModInfo::updateFromDisc(const QString &modDirectory, DirectoryEntry **directoryStructure) +{ + QMutexLocker lock(&s_Mutex); + s_Collection.clear(); + s_NextID = 0; + // list all directories in the mod directory and make a mod out of each + QDir mods(QDir::fromNativeSeparators(modDirectory)); + mods.setFilter(QDir::Dirs | QDir::NoDotAndDotDot); + QDirIterator modIter(mods); + while (modIter.hasNext()) { + createFrom(QDir(modIter.next()), directoryStructure); + } + + createFromOverwrite(); + + std::sort(s_Collection.begin(), s_Collection.end(), ByName); + + updateIndices(); +} + + +void ModInfo::updateIndices() +{ + s_ModsByName.clear(); + s_ModsByModID.clear(); + QRegExp backupRegEx(".*backup[0-9]*$"); + + for (unsigned int i = 0; i < s_Collection.size(); ++i) { + QString modName = s_Collection[i]->name(); + int modID = s_Collection[i]->getNexusID(); + s_ModsByName[modName] = i; + + // don't overwrite a modid-entry with a backup entry. This is a bit of a workaround + if ((s_ModsByModID.find(modID) == s_ModsByModID.end()) || + !backupRegEx.exactMatch(modName)) { + s_ModsByModID[modID] = i; + } + } +} + + +ModInfo::ModInfo() + : m_Valid(false), m_PrimaryCategory(-1) +{ +} + + +void ModInfo::checkChunkForUpdate(const std::vector &modIDs, QObject *receiver) +{ + if (modIDs.size() != 0) { + NexusInterface::instance()->requestUpdates(modIDs, receiver, QVariant()); + } +} + + +int ModInfo::checkAllForUpdate(QObject *receiver) +{ + int result = 0; + std::vector modIDs; + + modIDs.push_back(GameInfo::instance().getNexusModID()); + + for (std::vector::iterator iter = s_Collection.begin(); + iter != s_Collection.end(); ++iter) { + if ((*iter)->canBeUpdated()) { + modIDs.push_back((*iter)->getNexusID()); + if (modIDs.size() >= 255) { + checkChunkForUpdate(modIDs, receiver); + modIDs.clear(); + } + } + } + + checkChunkForUpdate(modIDs, receiver); + + return result; +} + +void ModInfo::setVersion(const VersionInfo &version) +{ + m_Version = version; +} + + +bool ModInfo::categorySet(int categoryID) const +{ + for (std::set::const_iterator iter = m_Categories.begin(); iter != m_Categories.end(); ++iter) { + if ((*iter == categoryID) || + (CategoryFactory::instance().isDecendantOf(*iter, categoryID))) { + return true; + } + } + + return false; +} + + +void ModInfo::testValid() +{ + m_Valid = false; + QDirIterator dirIter(absolutePath()); + while (dirIter.hasNext()) { + dirIter.next(); + if (dirIter.fileInfo().isDir()) { + if (InstallationTester::isTopLevelDirectory(dirIter.fileName())) { + m_Valid = true; + break; + } + } else { + if (InstallationTester::isTopLevelSuffix(dirIter.fileName())) { + m_Valid = true; + break; + } + } + } + + // NOTE: in Qt 4.7 it seems that QDirIterator leaves a file handle open if it is not iterated to the + // end + while (dirIter.hasNext()) { + dirIter.next(); + } +} + + +ModInfoRegular::ModInfoRegular(const QDir &path, DirectoryEntry **directoryStructure) + : ModInfo(), m_Name(path.dirName()), m_Path(path.absolutePath()), m_MetaInfoChanged(false), + m_EndorsedState(ENDORSED_UNKNOWN), m_DirectoryStructure(directoryStructure) +{ + testValid(); + + // read out the meta-file for information + QString metaFileName = path.absoluteFilePath("meta.ini"); + QSettings metaFile(metaFileName, QSettings::IniFormat); + + m_Notes = metaFile.value("notes", "").toString(); + m_NexusID = metaFile.value("modid", -1).toInt(); + m_Version.parse(metaFile.value("version", "").toString()); + m_NewestVersion = metaFile.value("newestVersion", "").toString(); + m_InstallationFile = metaFile.value("installationFile", "").toString(); + m_NexusDescription = metaFile.value("nexusDescription", "").toString(); + m_LastNexusQuery = QDateTime::fromString(metaFile.value("lastNexusQuery", "").toString(), Qt::ISODate); + if (metaFile.contains("endorsed")) { + m_EndorsedState = metaFile.value("endorsed", false).toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; + } + + int numNexusFiles = metaFile.beginReadArray("nexusFiles"); + for (int i = 0; i < numNexusFiles; ++i) { + metaFile.setArrayIndex(i); + QString infoString = metaFile.value("info", "").toString(); + NexusFileInfo info(infoString); + m_NexusFileInfos.push_back(info); + } + metaFile.endArray(); + + QString categoriesString = metaFile.value("category", "").toString(); + + QStringList categories = categoriesString.split(',', QString::SkipEmptyParts); + for (QStringList::iterator iter = categories.begin(); iter != categories.end(); ++iter) { + bool ok = false; + int categoryID = iter->toInt(&ok); + if (categoryID < 0) { + // ignore invalid id + continue; + } + if (ok && (categoryID != 0) && (CategoryFactory::instance().categoryExists(categoryID))) { + m_Categories.insert(categoryID); + if (iter == categories.begin()) { + m_PrimaryCategory = categoryID; + } + } + } + + connect(&m_NexusBridge, SIGNAL(nxmDescriptionAvailable(int,QVariant,QVariant)), this, SLOT(nxmDescriptionAvailable(int,QVariant,QVariant))); + connect(&m_NexusBridge, SIGNAL(nxmFilesAvailable(int,QVariant,QVariant)), this, SLOT(nxmFilesAvailable(int,QVariant,QVariant))); + connect(&m_NexusBridge, SIGNAL(nxmEndorsementToggled(int,QVariant,QVariant)), this, SLOT(nxmEndorsementToggled(int,QVariant,QVariant))); + connect(&m_NexusBridge, SIGNAL(nxmRequestFailed(int,QVariant,QString)), this, SLOT(nxmRequestFailed(int,QVariant,QString))); +} + + +ModInfoRegular::~ModInfoRegular() +{ + try { + //TODO this may cause the meta-file and the directory to be + // re-created after a remove + saveMeta(); + } catch (const std::exception &e) { + qCritical("failed to save meta information for \"%s\": %s", + m_Name.toUtf8().constData(), e.what()); + } +} + + +void ModInfoRegular::saveMeta() +{ + if (m_MetaInfoChanged) { + if (QFile::exists(absolutePath().append("/meta.ini"))) { + QSettings metaFile(absolutePath().append("/meta.ini"), QSettings::IniFormat); + if (metaFile.status() == QSettings::NoError) { + std::set temp = m_Categories; + temp.erase(m_PrimaryCategory); + metaFile.setValue("category", QString("%1").arg(m_PrimaryCategory) + "," + SetJoin(temp, ",")); + metaFile.setValue("newestVersion", m_NewestVersion.canonicalString()); + metaFile.setValue("version", m_Version.canonicalString()); + metaFile.setValue("modid", m_NexusID); + metaFile.setValue("notes", m_Notes); + metaFile.setValue("nexusDescription", m_NexusDescription); + metaFile.setValue("lastNexusQuery", m_LastNexusQuery.toString(Qt::ISODate)); + if (m_EndorsedState != ENDORSED_UNKNOWN) { + metaFile.setValue("endorsed", m_EndorsedState == ENDORSED_TRUE ? true : false); + } + + metaFile.beginWriteArray("nexusFiles"); + for (int i = 0; i < m_NexusFileInfos.size(); ++i) { + metaFile.setArrayIndex(i); + metaFile.setValue("info", m_NexusFileInfos.at(i).toString()); + } + metaFile.endArray(); + } else { + reportError(tr("failed to write %1/meta.ini: %2").arg(absolutePath()).arg(metaFile.status())); + } + } else { + qWarning("mod %s has no meta.ini at %s/meta.ini", m_Name.toUtf8().constData(), absolutePath().toUtf8().constData()); + } + m_MetaInfoChanged = false; + } +} + + +bool ModInfoRegular::updateAvailable() const +{ + return m_NewestVersion.isValid() && (m_Version < m_NewestVersion); +} + + +void ModInfoRegular::nxmDescriptionAvailable(int, QVariant, QVariant resultData) +{ + QVariantMap result = resultData.toMap(); + m_NewestVersion.parse(result["version"].toString()); + m_NexusDescription = result["description"].toString(); + m_EndorsedState = result["voted_by_user"].toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; + m_NexusBridge.requestFiles(m_NexusID, QVariant()); +} + + +void ModInfoRegular::nxmFilesAvailable(int, QVariant, QVariant resultData) +{ + m_NexusFileInfos.clear(); + + QVariantList result = resultData.toList(); + + foreach(QVariant file, result) { + QVariantMap fileInfo = file.toMap(); + + m_NexusFileInfos.push_back(NexusFileInfo(fileInfo["id"].toInt(), + fileInfo["name"].toString(), + fileInfo["uri"].toString(), + fileInfo["version"].toString(), + fileInfo["description"].toString(), + fileInfo["category_id"].toInt(), + fileInfo["size"].toInt())); + } + + m_LastNexusQuery = QDateTime::currentDateTime(); + m_MetaInfoChanged = true; + saveMeta(); + emit modDetailsUpdated(true); +} + + +void ModInfoRegular::nxmEndorsementToggled(int, QVariant, QVariant resultData) +{ + m_EndorsedState = resultData.toBool() ? ENDORSED_TRUE : ENDORSED_FALSE; + m_MetaInfoChanged = true; + saveMeta(); + emit modDetailsUpdated(true); +} + + +void ModInfoRegular::nxmRequestFailed(int, QVariant userData, const QString &errorMessage) +{ + QString fullMessage = errorMessage; + if (userData.canConvert() && (userData.toInt() == 1)) { + fullMessage += "\nNexus will reject endorsements within 15 Minutes of a failed attempt, the error message may be misleading."; + } + if (QApplication::activeWindow() != NULL) { + MessageDialog::showMessage(fullMessage, QApplication::activeWindow()); + } + emit modDetailsUpdated(false); +} + + +bool ModInfoRegular::updateNXMInfo() +{ + if (m_NexusID > 0) { + m_NexusBridge.requestDescription(m_NexusID, QVariant()); + return true; + } + return false; +} + + +void ModInfoRegular::setCategory(int categoryID, bool active) +{ + m_MetaInfoChanged = true; + + if (active) { + m_Categories.insert(categoryID); + if (m_PrimaryCategory == -1) { + m_PrimaryCategory = categoryID; + } + } else { + std::set::iterator iter = m_Categories.find(categoryID); + if (iter != m_Categories.end()) { + m_Categories.erase(iter); + } + if (categoryID == m_PrimaryCategory) { + if (m_Categories.size() == 0) { + m_PrimaryCategory = -1; + } else { + m_PrimaryCategory = *(m_Categories.begin()); + } + } + } +} + + +bool ModInfoRegular::setName(const QString &name) +{ + if (name.contains('/') || name.contains('\\')) { + return false; + } + + QString newPath = m_Path.mid(0).replace(m_Path.length() - m_Name.length(), m_Name.length(), name); + QDir modDir(m_Path.mid(0, m_Path.length() - m_Name.length())); + if (m_Name.compare(name, Qt::CaseInsensitive) == 0) { + QString tempName = name; + tempName.append("_temp"); + while (modDir.exists(tempName)) { + tempName.append("_"); + } + if (!modDir.rename(m_Name, tempName)) { + return false; + } + if (!modDir.rename(tempName, name)) { + qCritical("rename to final name failed after successful rename to intermediate name"); + modDir.rename(tempName, m_Name); + return false; + } + } else { + if (!modDir.rename(m_Name, name)) { + return false; + } + } + + std::map::iterator nameIter = s_ModsByName.find(m_Name); + unsigned int index = nameIter->second; + s_ModsByName.erase(nameIter); + + m_Name = name; + m_Path = newPath; + + s_ModsByName[m_Name] = index; + + std::sort(s_Collection.begin(), s_Collection.end(), ByName); + + updateIndices(); + + return true; +} + +void ModInfoRegular::setNotes(const QString ¬es) +{ + m_Notes = notes; +} + +void ModInfoRegular::setVersion(const VersionInfo &version) +{ + m_Version = version; + m_MetaInfoChanged = true; +} + +void ModInfoRegular::setNexusDescription(const QString &description) +{ + m_NexusDescription = description; + m_MetaInfoChanged = true; +} + + +void ModInfoRegular::setIsEndorsed(bool endorsed) +{ + m_EndorsedState = endorsed ? ENDORSED_TRUE : ENDORSED_FALSE; + m_MetaInfoChanged = true; +} + + +bool ModInfoRegular::remove() +{ + m_MetaInfoChanged = false; + return removeDir(absolutePath()); +} + +void ModInfoRegular::endorse(bool doEndorse) +{ + if (doEndorse != (m_EndorsedState == ENDORSED_TRUE)) { + m_NexusBridge.requestToggleEndorsement(getNexusID(), doEndorse, QVariant(1)); + } +} + + +QString ModInfoRegular::absolutePath() const +{ + return m_Path; +} + + +std::vector ModInfoRegular::getFlags() const +{ + std::vector result; + if (!isValid()) { + result.push_back(ModInfo::FLAG_INVALID); + } + if ((m_NexusID != -1) && (endorsedState() == ENDORSED_FALSE)) { + result.push_back(ModInfo::FLAG_NOTENDORSED); + } + switch (isConflicted()) { + case CONFLICT_MIXED: { + result.push_back(ModInfo::FLAG_CONFLICT_MIXED); + } break; + case CONFLICT_OVERWRITE: { + result.push_back(ModInfo::FLAG_CONFLICT_OVERWRITE); + } break; + case CONFLICT_OVERWRITTEN: { + result.push_back(ModInfo::FLAG_CONFLICT_OVERWRITTEN); + } break; + case CONFLICT_REDUNDANT: { + result.push_back(ModInfo::FLAG_CONFLICT_REDUNDANT); + } break; + default: { /* NOP */ } + } + if (m_Notes.length() != 0) { + result.push_back(ModInfo::FLAG_NOTES); + } + return result; +} + + +int ModInfoRegular::getHighlight() const +{ + return isValid() ? HIGHLIGHT_NONE: HIGHLIGHT_INVALID; +} + + +QString ModInfoRegular::getDescription() const +{ + if (!isValid()) { + return tr("%1 contains no esp/esm and no asset (textures, meshes, interface, ...) directory").arg(name()); + } else { + const std::set &categories = getCategories(); + std::wostringstream categoryString; + categoryString << ToWString(tr("Categories:
    ")); + CategoryFactory &categoryFactory = CategoryFactory::instance(); + for (std::set::const_iterator catIter = categories.begin(); + catIter != categories.end(); ++catIter) { + if (catIter != categories.begin()) { + categoryString << " , "; + } + categoryString << "" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << ""; + } + + return ToQString(categoryString.str()); + } +} + +QString ModInfoRegular::notes() const +{ + return m_Notes; +} + +void ModInfoRegular::getNexusFiles(QList::const_iterator &begin, QList::const_iterator &end) +{ + begin = m_NexusFileInfos.begin(); + end = m_NexusFileInfos.end(); +} + +QString ModInfoRegular::getNexusDescription() const +{ + return m_NexusDescription; +} + + +ModInfoRegular::EEndorsedState ModInfoRegular::endorsedState() const +{ + return m_EndorsedState; +} + + +ModInfoRegular::EConflictType ModInfoRegular::isConflicted() const +{ + bool overwrite = false; + bool overwritten = false; + bool regular = false; + + int dataID = 0; + if ((*m_DirectoryStructure)->originExists(L"data")) { + dataID = (*m_DirectoryStructure)->getOriginByName(L"data").getID(); + } + + std::wstring name = ToWString(m_Name); + if ((*m_DirectoryStructure)->originExists(name)) { + FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name); + std::vector files = origin.getFiles(); + for (auto iter = files.begin(); iter != files.end() && (!overwrite || !overwritten || !regular); ++iter) { + const std::vector &alternatives = (*iter)->getAlternatives(); + if (alternatives.size() == 0) { + // no alternatives -> no conflict + regular = true; + } else { + for (auto altIter = alternatives.begin(); altIter != alternatives.end(); ++altIter) { + // don't treat files overwritten in data as "conflict" + if (*altIter != dataID) { + bool ignore = false; + if ((*iter)->getOrigin(ignore) == origin.getID()) { + overwrite = true; + break; + } else { + overwritten = true; + break; + } + } + } + } + } + } + + if (overwrite && overwritten) return CONFLICT_MIXED; + else if (overwrite) return CONFLICT_OVERWRITE; + else if (overwritten) { + if (!regular) { + return CONFLICT_REDUNDANT; + } else { + return CONFLICT_OVERWRITTEN; + } + } + else return CONFLICT_NONE; +} + + +bool ModInfoRegular::isRedundant() const +{ + std::wstring name = ToWString(m_Name); + if ((*m_DirectoryStructure)->originExists(name)) { + FilesOrigin &origin = (*m_DirectoryStructure)->getOriginByName(name); + std::vector files = origin.getFiles(); + bool ignore = false; + for (auto iter = files.begin(); iter != files.end(); ++iter) { + if ((*iter)->getOrigin(ignore) == origin.getID()) { + return false; + } + } + return true; + } else { + return false; + } +} + + +QDateTime ModInfoRegular::getLastNexusQuery() const +{ + return m_LastNexusQuery; +} + +std::vector ModInfoRegular::getIniTweaks() const +{ + QString metaFileName = absolutePath().append("/meta.ini"); + QSettings metaFile(metaFileName, QSettings::IniFormat); + + std::vector result; + + int numTweaks = metaFile.beginReadArray("INI Tweaks"); + + if (numTweaks != 0) { + qDebug("%d active ini tweaks in %s", + numTweaks, metaFileName.toUtf8().constData()); + } + + for (int i = 0; i < numTweaks; ++i) { + metaFile.setArrayIndex(i); + QString filename = absolutePath().append("/INI Tweaks/").append(metaFile.value("name").toString()); + result.push_back(filename); + } + metaFile.endArray(); + return result; +} + +std::vector ModInfoBackup::getFlags() const +{ + std::vector result = ModInfoRegular::getFlags(); + result.insert(result.begin(), ModInfo::FLAG_BACKUP); + return result; +} + + +QString ModInfoBackup::getDescription() const +{ + return tr("This is the backup of a mod"); +} + + +ModInfoBackup::ModInfoBackup(const QDir &path, DirectoryEntry **directoryStructure) + : ModInfoRegular(path, directoryStructure) +{ +} + + +ModInfoOverwrite::ModInfoOverwrite() +{ + testValid(); + +} + + +QString ModInfoOverwrite::absolutePath() const +{ + return QDir::fromNativeSeparators(ToQString(GameInfo::instance().getOverwriteDir())); +} + +std::vector ModInfoOverwrite::getFlags() const +{ + std::vector result; + result.push_back(FLAG_OVERWRITE); + return result; +} + +int ModInfoOverwrite::getHighlight() const +{ + return (isValid() ? HIGHLIGHT_IMPORTANT : HIGHLIGHT_INVALID) | HIGHLIGHT_CENTER; +} + + +QString ModInfoOverwrite::getDescription() const +{ + return tr("This pseudo mod contains files from the virtual data tree that got " + "modified (i.e. by the construction kit)"); +} diff --git a/src/modinfo.h b/src/modinfo.h index 0e5367e4..12ba89fd 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -17,808 +17,810 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#ifndef MODINFO_H -#define MODINFO_H - -#include "nexusinterface.h" -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -/** - * @brief Represents meta information about a single mod. - * - * Represents meta information about a single mod. The class interface is used - * to manage the mod collection - * - **/ -class ModInfo : public QObject, public IModInterface -{ - - Q_OBJECT - -public: - - typedef QSharedPointer Ptr; - - static QString s_HiddenExt; - - enum EFlag { - FLAG_INVALID, - FLAG_BACKUP, - FLAG_OVERWRITE, - FLAG_NOTENDORSED, - FLAG_NOTES, - FLAG_CONFLICT_OVERWRITE, - FLAG_CONFLICT_OVERWRITTEN, - FLAG_CONFLICT_MIXED, - FLAG_CONFLICT_REDUNDANT - }; - - enum EHighlight { - HIGHLIGHT_NONE = 0, - HIGHLIGHT_INVALID = 1, - HIGHLIGHT_CENTER = 2, - HIGHLIGHT_IMPORTANT = 4 - }; - - enum EEndorsedState { - ENDORSED_FALSE, - ENDORSED_TRUE, - ENDORSED_UNKNOWN - }; - - struct NexusFileInfo { - NexusFileInfo(const QString &data); - NexusFileInfo(int id, const QString &name, const QString &url, const QString &version, - const QString &description, int category, int size) - : id(id), name(name), url(url), version(version), description(description), - category(category), size(size) {} - int id; - QString name; - QString url; - QString version; - QString description; - int category; - int size; - - QString toString() const; - }; - -public: - - /** - * @brief read the mod directory and Mod ModInfo objects for all subdirectories - **/ - static void updateFromDisc(const QString &modDirectory, DirectoryEntry **directoryStructure); - - static void clear() { s_Collection.clear(); s_ModsByName.clear(); s_ModsByModID.clear(); } - - /** - * @brief retrieve the number of mods - * - * @return number of mods - **/ - static unsigned int getNumMods(); - - /** - * @brief retrieve a ModInfo object based on its index - * - * @param index the index to look up. the maximum is getNumMods() - 1 - * @return a reference counting pointer to the mod info. - * @note since the pointer is reference counting, the pointer remains valid even if the collection is refreshed in a different thread - **/ - static ModInfo::Ptr getByIndex(unsigned int index); - - /** - * @brief retrieve a ModInfo object based on its nexus mod id - * - * @param modID the nexus mod id to look up - * @param missingAcceptable if true, this function will return a null-pointer if no mod has the specified mod id, otherwise an exception is thrown - * @return a reference counting pointer to the mod info - * @todo in its current form, this function is broken! There may be multiple mods with the same nexus id, - * this function will return only one of them - **/ - static ModInfo::Ptr getByModID(int modID, bool missingAcceptable); - - /** - * @brief remove a mod by index - * - * this physically deletes the specified mod from the disc and updates the ModInfo collection - * but not other structures that reference mods - * @param index index of the mod to delete - * @return true if removal was successful, fals otherwise - **/ - static bool removeMod(unsigned int index); - - /** - * @brief retrieve the mod index by the mod name - * - * @param name name of the mod to look up - * @return the index of the mod. If the mod doesn't exist, UINT_MAX is returned - **/ - static unsigned int getIndex(const QString &name); - - /** - * @brief find the first mod that fulfills the filter function (after no particular order) - * @param filter a function to filter by. should return true for a match - * @return index of the matching mod or UINT_MAX if there wasn't a match - */ - static unsigned int findMod(const boost::function &filter); - - /** - * @brief check a bunch of mods for updates - * @param modIDs list of mods (Nexus Mod IDs) to check for updates - * @return - */ - static void checkChunkForUpdate(const std::vector &modIDs, QObject *receiver); - - /** - * @brief query nexus information for every mod and update the "newest version" information - **/ - static int checkAllForUpdate(QObject *receiver); - - /** - * @brief create a new mod from the specified directory and add it to the collection - * @param dir directory to create from - * @return pointer to the info-structure of the newly created/added mod - */ - static ModInfo::Ptr createFrom(const QDir &dir, DirectoryEntry **directoryStructure); - - /** - * @brief test if there is a newer version of the mod - * - * test if there is a newer version of the mod. This does NOT cause - * information to be retrieved from the nexus, it will only test version information already - * available locally. Use checkAllForUpdate() to update this version information - * - * @return true if there is a newer version - **/ - virtual bool updateAvailable() const = 0; - - /** - * @brief request an update of nexus description for this mod. - * - * This requests mod information from the nexus. This is an asynchronous request, - * so there is no immediate effect of this call. - * Right now, Mod Organizer interprets the "newest version" and "description" from the - * response, though the description is only stored in memory - * - **/ - virtual bool updateNXMInfo() = 0; - - /** - * @brief assign or unassign the specified category - * - * Every mod can have an arbitrary number of categories assigned to it - * - * @param categoryID id of the category to set - * @param active determines wheter the category is assigned or unassigned - * @note this function does not test whether categoryID actually identifies a valid category - **/ - virtual void setCategory(int categoryID, bool active) = 0; - - /** - * @brief set the name of this mod - * - * set the name of this mod. This will also update the name of the - * directory that contains this mod - * - * @param name new name of the mod - * @return true on success, false if the new name can't be used (i.e. because the new - * directory name wouldn't be valid) - **/ - virtual bool setName(const QString &name) = 0; - - /** - * @brief change the notes (manually set information) for this mod - * @param notes new notes - */ - virtual void setNotes(const QString ¬es) = 0; - - /** - * @brief set/change the nexus mod id of this mod - * - * @param modID the nexus mod id - **/ - virtual void setNexusID(int modID) = 0; - - /** - * @brief set/change the version of this mod - * @param version new version of the mod - */ - virtual void setVersion(const VersionInfo &version); - - /** - * @brief set the newest version of this mod on the nexus - * - * this can be used to overwrite the version of a mod without actually - * updating the mod - * - * @param version the new version to use - * @todo this function should be made obsolete. All queries for mod information should go through - * this class so no public function for this change is required - **/ - virtual void setNewestVersion(const VersionInfo &version) = 0; - - /** - * @brief changes/updates the nexus description text - * @param description the current description text - */ - virtual void setNexusDescription(const QString &description) = 0; - - /** - * update the endorsement state for the mod. This only changes the - * buffered state, it does not sync with Nexus - * @param endorsed the new endorsement state - */ - virtual void setIsEndorsed(bool endorsed) = 0; - - /** - * @brief delete the mod from the disc. This does not update the global ModInfo structure or indices - * @return true if the mod was successfully removed - **/ - virtual bool remove() = 0; - - /** - * @brief endorse or un-endorse the mod. This will sync with nexus! - * @param doEndorse if true, the mod is endorsed, if false, it's un-endorsed. - * @note if doEndorse doesn't differ from the current value, nothing happens. - */ - virtual void endorse(bool doEndorse) = 0; - - /** - * @brief getter for the mod name - * - * @return the mod name - **/ - virtual QString name() const = 0; - - /** - * @brief getter for the mod path - * - * @return the (absolute) path to the mod - **/ - virtual QString absolutePath() const = 0; - - /** - * @brief getter for the installation file - * - * @return file used to install this mod from - */ - virtual QString getInstallationFile() const = 0; - - /** - * @return version object for machine based comparisons - **/ - virtual VersionInfo getVersion() const { return m_Version; } - - /** - * @brief getter for the newest version number of this mod - * - * @return newest version of the mod - **/ - virtual VersionInfo getNewestVersion() const = 0; - - /** - * @brief getter for the nexus mod id - * - * @return the nexus mod id. may be 0 if the mod id isn't known or doesn't exist - **/ - virtual int getNexusID() const = 0; - - /** - * @return the fixed priority of mods of this type or INT_MIN if the priority of mods - * needs to be user-modifiable. Can be < 0 to force a priority below user-modifable mods - * or INT_MAX to force priority above all user-modifiables - */ - virtual int getFixedPriority() const = 0; - - /** - * @return true if the mod can be updated - */ - virtual bool canBeUpdated() const { return false; } - - /** - * @return true if the mod can be enabled/disabled - */ - virtual bool canBeEnabled() const { return false; } - - /** - * @return a list of flags for this mod - */ - virtual std::vector getFlags() const = 0; - - /** - * @return an indicator if and how this mod should be highlighted by the UI - */ - virtual int getHighlight() const { return HIGHLIGHT_NONE; } - - /** - * @return list of names of ini tweaks - **/ - virtual std::vector getIniTweaks() const = 0; - - /** - * @return a description about the mod, to be displayed in the ui - */ - virtual QString getDescription() const = 0; - - /** - * @return manually set notes for this mod - */ - virtual QString notes() const = 0; - - /** - * @brief return the list of files on nexus related to this mod in the form of iterators - * @note this is valid only after querying the files - * @param begin iterator to the first file - * @param end iterator to one past the last file - */ - virtual void getNexusFiles(QList::const_iterator &begin, QList::const_iterator &end) = 0; - - /** - * @return nexus description of the mod (html) - */ - virtual QString getNexusDescription() const = 0; - - /** - * @return last time nexus was queried for infos on this mod - */ - virtual QDateTime getLastNexusQuery() const = 0; - - /** - * @brief test if the mod belongs to the specified category - * - * @param categoryID the category to test for. - * @return true if the mod belongs to the specified category - * @note this does not verify the id actually identifies a category - **/ - bool categorySet(int categoryID) const; - - /** - * @brief retrive the whole list of categories this mod belongs to - * - * @return list of categories - **/ - const std::set &getCategories() const { return m_Categories; } - - /** - * @return the primary category of this mod - */ - int getPrimaryCategory() const { return m_PrimaryCategory; } - - /** - * @brief sets the new primary category of the mod - * @param categoryID the category to set - */ - void setPrimaryCategory(int categoryID) { m_PrimaryCategory = categoryID; } - - /** - * @return true if this mod is considered "valid", that is: it contains data used by the game - **/ - bool isValid() const { return m_Valid; } - - /** - * @return true if the file has been endorsed on nexus - */ - virtual EEndorsedState endorsedState() const { return ENDORSED_UNKNOWN; } - - /** - * @brief updates the valid-flag for this mod - */ - void testValid(); - - /** - * @brief stores meta information back to disk - */ - virtual void saveMeta() {} - -signals: - - /** - * @brief emitted whenever the information of a mod changes - * - * @param success true if the mod details were updated successfully, false if not - **/ - void modDetailsUpdated(bool success); - -protected: - - ModInfo(); - - static void updateIndices(); - -private: - - static void createFromOverwrite(); - -protected: - - static std::vector s_Collection; - static std::map s_ModsByName; - - int m_PrimaryCategory; - std::set m_Categories; - - VersionInfo m_Version; - -private: - - static QMutex s_Mutex; - static std::map s_ModsByModID; - static int s_NextID; - - bool m_Valid; - -}; - - - -/** - * @brief Represents meta information about a single mod. - * - * Represents meta information about a single mod. The class interface is used - * to manage the mod collection - * - **/ -class ModInfoRegular : public ModInfo -{ - - Q_OBJECT - - friend class ModInfo; - -public: - - ~ModInfoRegular(); - - virtual bool isRegular() const { return true; } - - /** - * @brief test if there is a newer version of the mod - * - * test if there is a newer version of the mod. This does NOT cause - * information to be retrieved from the nexus, it will only test version information already - * available locally. Use checkAllForUpdate() to update this version information - * - * @return true if there is a newer version - **/ - bool updateAvailable() const; - - /** - * @brief request an update of nexus description for this mod. - * - * This requests mod information from the nexus. This is an asynchronous request, - * so there is no immediate effect of this call. - * - * @return returns true if information for this mod will be updated, false if there is no nexus mod id to use - **/ - bool updateNXMInfo(); - - /** - * @brief assign or unassign the specified category - * - * Every mod can have an arbitrary number of categories assigned to it - * - * @param categoryID id of the category to set - * @param active determines wheter the category is assigned or unassigned - * @note this function does not test whether categoryID actually identifies a valid category - **/ - void setCategory(int categoryID, bool active); - - /** - * @brief set the name of this mod - * - * set the name of this mod. This will also update the name of the - * directory that contains this mod - * - * @param name new name of the mod - * @return true on success, false if the new name can't be used (i.e. because the new - * directory name wouldn't be valid) - **/ - bool setName(const QString &name); - - /** - * @brief change the notes (manually set information) for this mod - * @param notes new notes - */ - void setNotes(const QString ¬es); - - /** - * @brief set/change the nexus mod id of this mod - * - * @param modID the nexus mod id - **/ - void setNexusID(int modID) { m_NexusID = modID; } - - /** - * @brief set the version of this mod - * - * this can be used to overwrite the version of a mod without actually - * updating the mod - * - * @param version the new version to use - **/ - void setVersion(const VersionInfo &version); - - /** - * @brief set the newest version of this mod on the nexus - * - * this can be used to overwrite the version of a mod without actually - * updating the mod - * - * @param version the new version to use - * @todo this function should be made obsolete. All queries for mod information should go through - * this class so no public function for this change is required - **/ - void setNewestVersion(const VersionInfo &version) { m_NewestVersion = version; } - - /** - * @brief changes/updates the nexus description text - * @param description the current description text - */ - virtual void setNexusDescription(const QString &description); - - /** - * update the endorsement state for the mod. This only changes the - * buffered state, it does not sync with Nexus - * @param endorsed the new endorsement state - */ - virtual void setIsEndorsed(bool endorsed); - - /** - * @brief delete the mod from the disc. This does not update the global ModInfo structure or indices - * @return true if the mod was successfully removed - **/ - bool remove(); - - /** - * @brief endorse or un-endorse the mod - * @param doEndorse if true, the mod is endorsed, if false, it's un-endorsed. - * @note if doEndorse doesn't differ from the current value, nothing happens. - */ - virtual void endorse(bool doEndorse); - - /** - * @brief getter for the mod name - * - * @return the mod name - **/ - QString name() const { return m_Name; } - - /** - * @brief getter for the mod path - * - * @return the (absolute) path to the mod - **/ - QString absolutePath() const; - - /** - * @brief getter for the newest version number of this mod - * - * @return newest version of the mod - **/ - VersionInfo getNewestVersion() const { return m_NewestVersion; } - - /** - * @brief getter for the installation file - * - * @return file used to install this mod from - */ - virtual QString getInstallationFile() const { return m_InstallationFile; } - /** - * @brief getter for the nexus mod id - * - * @return the nexus mod id. may be 0 if the mod id isn't known or doesn't exist - **/ - int getNexusID() const { return m_NexusID; } - - /** - * @return the fixed priority of mods of this type or INT_MIN if the priority of mods - * needs to be user-modifiable - */ - virtual int getFixedPriority() const { return INT_MIN; } - - /** - * @return true if the mod can be updated - */ - virtual bool canBeUpdated() const { return m_NexusID >= 0; } - - /** - * @return true if the mod can be enabled/disabled - */ - virtual bool canBeEnabled() const { return true; } - - /** - * @return a list of flags for this mod - */ - virtual std::vector getFlags() const; - - /** - * @return an indicator if and how this mod should be highlighted by the UI - */ - virtual int getHighlight() const; - - /** - * @return list of names of ini tweaks - **/ - std::vector getIniTweaks() const; - - /** - * @return a description about the mod, to be displayed in the ui - */ - virtual QString getDescription() const; - - /** - * @return manually set notes for this mod - */ - virtual QString notes() const; - - /** - * @brief return the list of files on nexus related to this mod in the form of iterators - * @note this is valid only after querying the files - * @param begin iterator to the first file - * @param end iterator to one past the last file - */ - virtual void getNexusFiles(QList::const_iterator &begin, QList::const_iterator &end); - - /** - * @return nexus description of the mod (html) - */ - QString getNexusDescription() const; - - /** - * @return true if the file has been endorsed on nexus - */ - virtual EEndorsedState endorsedState() const; - - /** - * @return last time nexus was queried for infos on this mod - */ - QDateTime getLastNexusQuery() const; - - /** - * @brief stores meta information back to disk - */ - virtual void saveMeta(); - -private: - - enum EConflictType { - CONFLICT_NONE, - CONFLICT_OVERWRITE, - CONFLICT_OVERWRITTEN, - CONFLICT_MIXED, - CONFLICT_REDUNDANT - }; - -private slots: - - void nxmDescriptionAvailable(int modID, QVariant userData, QVariant resultData); - void nxmFilesAvailable(int, QVariant, QVariant resultData); - void nxmEndorsementToggled(int, QVariant userData, QVariant resultData); - void nxmRequestFailed(int modID, QVariant userData, const QString &errorMessage); - -private: - - /** - * @return true if there is a conflict for files in this mod - */ - EConflictType isConflicted() const; - - /** - * @return true if this mod is completely replaced by others - */ - bool isRedundant() const; - -protected: - - ModInfoRegular(const QDir &path, DirectoryEntry **directoryStructure); - -private: - - QString m_Name; - QString m_Path; - QString m_InstallationFile; - QString m_Notes; - QString m_NexusDescription; - QList m_NexusFileInfos; - - QDateTime m_LastNexusQuery; - - int m_NexusID; - - bool m_MetaInfoChanged; - VersionInfo m_NewestVersion; - - EEndorsedState m_EndorsedState; - - NexusBridge m_NexusBridge; - - DirectoryEntry **m_DirectoryStructure; - -}; - - -class ModInfoBackup : public ModInfoRegular -{ - - friend class ModInfo; - -public: - - virtual bool updateAvailable() const { return false; } - virtual bool updateNXMInfo() { return false; } - virtual void setNexusID(int) {} - virtual void endorse(bool) {} - virtual int getFixedPriority() const { return -1; } - virtual bool canBeUpdated() const { return false; } - virtual bool canBeEnabled() const { return false; } - virtual std::vector getIniTweaks() const { return std::vector(); } - virtual std::vector getFlags() const; - virtual QString getDescription() const; - virtual QDateTime getLastNexusQuery() const { return QDateTime(); } - virtual void getNexusFiles(QList::const_iterator&, QList::const_iterator&) {} - virtual QString getNexusDescription() const { return QString(); } - -private: - - ModInfoBackup(const QDir &path, DirectoryEntry **directoryStructure); - -}; - - -class ModInfoOverwrite : public ModInfo -{ - - Q_OBJECT - - friend class ModInfo; - -public: - - virtual bool updateAvailable() const { return false; } - virtual bool updateNXMInfo() { return false; } - virtual void setCategory(int, bool) {} - virtual bool setName(const QString&) { return false; } - virtual void setNotes(const QString&) {} - virtual void setNexusID(int) {} - virtual void setNewestVersion(const VersionInfo&) {} - virtual void setNexusDescription(const QString&) {} - virtual void setIsEndorsed(bool) {} - virtual bool remove() { return false; } - virtual void endorse(bool) {} - virtual QString name() const { return tr("Overwrite"); } - virtual QString notes() const { return ""; } - virtual QString absolutePath() const; - virtual VersionInfo getNewestVersion() const { return ""; } - virtual QString getInstallationFile() const { return ""; } - virtual int getFixedPriority() const { return INT_MAX; } - virtual int getNexusID() const { return -1; } - virtual std::vector getIniTweaks() const { return std::vector(); } - virtual std::vector getFlags() const; - virtual int getHighlight() const; - virtual QString getDescription() const; - virtual QDateTime getLastNexusQuery() const { return QDateTime(); } - virtual void getNexusFiles(QList::const_iterator&, QList::const_iterator&) {} - virtual QString getNexusDescription() const { return QString(); } - -private: - - ModInfoOverwrite(); - -}; - -#endif // MODINFO_H +#ifndef MODINFO_H +#define MODINFO_H + +#include "nexusinterface.h" +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +/** + * @brief Represents meta information about a single mod. + * + * Represents meta information about a single mod. The class interface is used + * to manage the mod collection + * + **/ +class ModInfo : public QObject, public IModInterface +{ + + Q_OBJECT + +public: + + typedef QSharedPointer Ptr; + + static QString s_HiddenExt; + + enum EFlag { + FLAG_INVALID, + FLAG_BACKUP, + FLAG_OVERWRITE, + FLAG_NOTENDORSED, + FLAG_NOTES, + FLAG_CONFLICT_OVERWRITE, + FLAG_CONFLICT_OVERWRITTEN, + FLAG_CONFLICT_MIXED, + FLAG_CONFLICT_REDUNDANT + }; + + enum EHighlight { + HIGHLIGHT_NONE = 0, + HIGHLIGHT_INVALID = 1, + HIGHLIGHT_CENTER = 2, + HIGHLIGHT_IMPORTANT = 4 + }; + + enum EEndorsedState { + ENDORSED_FALSE, + ENDORSED_TRUE, + ENDORSED_UNKNOWN + }; + + struct NexusFileInfo { + NexusFileInfo(const QString &data); + NexusFileInfo(int id, const QString &name, const QString &url, const QString &version, + const QString &description, int category, int size) + : id(id), name(name), url(url), version(version), description(description), + category(category), size(size) {} + int id; + QString name; + QString url; + QString version; + QString description; + int category; + int size; + + QString toString() const; + }; + +public: + + /** + * @brief read the mod directory and Mod ModInfo objects for all subdirectories + **/ + static void updateFromDisc(const QString &modDirectory, DirectoryEntry **directoryStructure); + + static void clear() { s_Collection.clear(); s_ModsByName.clear(); s_ModsByModID.clear(); } + + /** + * @brief retrieve the number of mods + * + * @return number of mods + **/ + static unsigned int getNumMods(); + + /** + * @brief retrieve a ModInfo object based on its index + * + * @param index the index to look up. the maximum is getNumMods() - 1 + * @return a reference counting pointer to the mod info. + * @note since the pointer is reference counting, the pointer remains valid even if the collection is refreshed in a different thread + **/ + static ModInfo::Ptr getByIndex(unsigned int index); + + /** + * @brief retrieve a ModInfo object based on its nexus mod id + * + * @param modID the nexus mod id to look up + * @param missingAcceptable if true, this function will return a null-pointer if no mod has the specified mod id, otherwise an exception is thrown + * @return a reference counting pointer to the mod info + * @todo in its current form, this function is broken! There may be multiple mods with the same nexus id, + * this function will return only one of them + **/ + static ModInfo::Ptr getByModID(int modID, bool missingAcceptable); + + /** + * @brief remove a mod by index + * + * this physically deletes the specified mod from the disc and updates the ModInfo collection + * but not other structures that reference mods + * @param index index of the mod to delete + * @return true if removal was successful, fals otherwise + **/ + static bool removeMod(unsigned int index); + + /** + * @brief retrieve the mod index by the mod name + * + * @param name name of the mod to look up + * @return the index of the mod. If the mod doesn't exist, UINT_MAX is returned + **/ + static unsigned int getIndex(const QString &name); + + /** + * @brief find the first mod that fulfills the filter function (after no particular order) + * @param filter a function to filter by. should return true for a match + * @return index of the matching mod or UINT_MAX if there wasn't a match + */ + static unsigned int findMod(const boost::function &filter); + + /** + * @brief check a bunch of mods for updates + * @param modIDs list of mods (Nexus Mod IDs) to check for updates + * @return + */ + static void checkChunkForUpdate(const std::vector &modIDs, QObject *receiver); + + /** + * @brief query nexus information for every mod and update the "newest version" information + **/ + static int checkAllForUpdate(QObject *receiver); + + /** + * @brief create a new mod from the specified directory and add it to the collection + * @param dir directory to create from + * @return pointer to the info-structure of the newly created/added mod + */ + static ModInfo::Ptr createFrom(const QDir &dir, DirectoryEntry **directoryStructure); + + virtual bool isRegular() const { return false; } + + /** + * @brief test if there is a newer version of the mod + * + * test if there is a newer version of the mod. This does NOT cause + * information to be retrieved from the nexus, it will only test version information already + * available locally. Use checkAllForUpdate() to update this version information + * + * @return true if there is a newer version + **/ + virtual bool updateAvailable() const = 0; + + /** + * @brief request an update of nexus description for this mod. + * + * This requests mod information from the nexus. This is an asynchronous request, + * so there is no immediate effect of this call. + * Right now, Mod Organizer interprets the "newest version" and "description" from the + * response, though the description is only stored in memory + * + **/ + virtual bool updateNXMInfo() = 0; + + /** + * @brief assign or unassign the specified category + * + * Every mod can have an arbitrary number of categories assigned to it + * + * @param categoryID id of the category to set + * @param active determines wheter the category is assigned or unassigned + * @note this function does not test whether categoryID actually identifies a valid category + **/ + virtual void setCategory(int categoryID, bool active) = 0; + + /** + * @brief set the name of this mod + * + * set the name of this mod. This will also update the name of the + * directory that contains this mod + * + * @param name new name of the mod + * @return true on success, false if the new name can't be used (i.e. because the new + * directory name wouldn't be valid) + **/ + virtual bool setName(const QString &name) = 0; + + /** + * @brief change the notes (manually set information) for this mod + * @param notes new notes + */ + virtual void setNotes(const QString ¬es) = 0; + + /** + * @brief set/change the nexus mod id of this mod + * + * @param modID the nexus mod id + **/ + virtual void setNexusID(int modID) = 0; + + /** + * @brief set/change the version of this mod + * @param version new version of the mod + */ + virtual void setVersion(const VersionInfo &version); + + /** + * @brief set the newest version of this mod on the nexus + * + * this can be used to overwrite the version of a mod without actually + * updating the mod + * + * @param version the new version to use + * @todo this function should be made obsolete. All queries for mod information should go through + * this class so no public function for this change is required + **/ + virtual void setNewestVersion(const VersionInfo &version) = 0; + + /** + * @brief changes/updates the nexus description text + * @param description the current description text + */ + virtual void setNexusDescription(const QString &description) = 0; + + /** + * update the endorsement state for the mod. This only changes the + * buffered state, it does not sync with Nexus + * @param endorsed the new endorsement state + */ + virtual void setIsEndorsed(bool endorsed) = 0; + + /** + * @brief delete the mod from the disc. This does not update the global ModInfo structure or indices + * @return true if the mod was successfully removed + **/ + virtual bool remove() = 0; + + /** + * @brief endorse or un-endorse the mod. This will sync with nexus! + * @param doEndorse if true, the mod is endorsed, if false, it's un-endorsed. + * @note if doEndorse doesn't differ from the current value, nothing happens. + */ + virtual void endorse(bool doEndorse) = 0; + + /** + * @brief getter for the mod name + * + * @return the mod name + **/ + virtual QString name() const = 0; + + /** + * @brief getter for the mod path + * + * @return the (absolute) path to the mod + **/ + virtual QString absolutePath() const = 0; + + /** + * @brief getter for the installation file + * + * @return file used to install this mod from + */ + virtual QString getInstallationFile() const = 0; + + /** + * @return version object for machine based comparisons + **/ + virtual VersionInfo getVersion() const { return m_Version; } + + /** + * @brief getter for the newest version number of this mod + * + * @return newest version of the mod + **/ + virtual VersionInfo getNewestVersion() const = 0; + + /** + * @brief getter for the nexus mod id + * + * @return the nexus mod id. may be 0 if the mod id isn't known or doesn't exist + **/ + virtual int getNexusID() const = 0; + + /** + * @return the fixed priority of mods of this type or INT_MIN if the priority of mods + * needs to be user-modifiable. Can be < 0 to force a priority below user-modifable mods + * or INT_MAX to force priority above all user-modifiables + */ + virtual int getFixedPriority() const = 0; + + /** + * @return true if the mod can be updated + */ + virtual bool canBeUpdated() const { return false; } + + /** + * @return true if the mod can be enabled/disabled + */ + virtual bool canBeEnabled() const { return false; } + + /** + * @return a list of flags for this mod + */ + virtual std::vector getFlags() const = 0; + + /** + * @return an indicator if and how this mod should be highlighted by the UI + */ + virtual int getHighlight() const { return HIGHLIGHT_NONE; } + + /** + * @return list of names of ini tweaks + **/ + virtual std::vector getIniTweaks() const = 0; + + /** + * @return a description about the mod, to be displayed in the ui + */ + virtual QString getDescription() const = 0; + + /** + * @return manually set notes for this mod + */ + virtual QString notes() const = 0; + + /** + * @brief return the list of files on nexus related to this mod in the form of iterators + * @note this is valid only after querying the files + * @param begin iterator to the first file + * @param end iterator to one past the last file + */ + virtual void getNexusFiles(QList::const_iterator &begin, QList::const_iterator &end) = 0; + + /** + * @return nexus description of the mod (html) + */ + virtual QString getNexusDescription() const = 0; + + /** + * @return last time nexus was queried for infos on this mod + */ + virtual QDateTime getLastNexusQuery() const = 0; + + /** + * @brief test if the mod belongs to the specified category + * + * @param categoryID the category to test for. + * @return true if the mod belongs to the specified category + * @note this does not verify the id actually identifies a category + **/ + bool categorySet(int categoryID) const; + + /** + * @brief retrive the whole list of categories this mod belongs to + * + * @return list of categories + **/ + const std::set &getCategories() const { return m_Categories; } + + /** + * @return the primary category of this mod + */ + int getPrimaryCategory() const { return m_PrimaryCategory; } + + /** + * @brief sets the new primary category of the mod + * @param categoryID the category to set + */ + void setPrimaryCategory(int categoryID) { m_PrimaryCategory = categoryID; } + + /** + * @return true if this mod is considered "valid", that is: it contains data used by the game + **/ + bool isValid() const { return m_Valid; } + + /** + * @return true if the file has been endorsed on nexus + */ + virtual EEndorsedState endorsedState() const { return ENDORSED_UNKNOWN; } + + /** + * @brief updates the valid-flag for this mod + */ + void testValid(); + + /** + * @brief stores meta information back to disk + */ + virtual void saveMeta() {} + +signals: + + /** + * @brief emitted whenever the information of a mod changes + * + * @param success true if the mod details were updated successfully, false if not + **/ + void modDetailsUpdated(bool success); + +protected: + + ModInfo(); + + static void updateIndices(); + +private: + + static void createFromOverwrite(); + +protected: + + static std::vector s_Collection; + static std::map s_ModsByName; + + int m_PrimaryCategory; + std::set m_Categories; + + VersionInfo m_Version; + +private: + + static QMutex s_Mutex; + static std::map s_ModsByModID; + static int s_NextID; + + bool m_Valid; + +}; + + + +/** + * @brief Represents meta information about a single mod. + * + * Represents meta information about a single mod. The class interface is used + * to manage the mod collection + * + **/ +class ModInfoRegular : public ModInfo +{ + + Q_OBJECT + + friend class ModInfo; + +public: + + ~ModInfoRegular(); + + virtual bool isRegular() const { return true; } + + /** + * @brief test if there is a newer version of the mod + * + * test if there is a newer version of the mod. This does NOT cause + * information to be retrieved from the nexus, it will only test version information already + * available locally. Use checkAllForUpdate() to update this version information + * + * @return true if there is a newer version + **/ + bool updateAvailable() const; + + /** + * @brief request an update of nexus description for this mod. + * + * This requests mod information from the nexus. This is an asynchronous request, + * so there is no immediate effect of this call. + * + * @return returns true if information for this mod will be updated, false if there is no nexus mod id to use + **/ + bool updateNXMInfo(); + + /** + * @brief assign or unassign the specified category + * + * Every mod can have an arbitrary number of categories assigned to it + * + * @param categoryID id of the category to set + * @param active determines wheter the category is assigned or unassigned + * @note this function does not test whether categoryID actually identifies a valid category + **/ + void setCategory(int categoryID, bool active); + + /** + * @brief set the name of this mod + * + * set the name of this mod. This will also update the name of the + * directory that contains this mod + * + * @param name new name of the mod + * @return true on success, false if the new name can't be used (i.e. because the new + * directory name wouldn't be valid) + **/ + bool setName(const QString &name); + + /** + * @brief change the notes (manually set information) for this mod + * @param notes new notes + */ + void setNotes(const QString ¬es); + + /** + * @brief set/change the nexus mod id of this mod + * + * @param modID the nexus mod id + **/ + void setNexusID(int modID) { m_NexusID = modID; } + + /** + * @brief set the version of this mod + * + * this can be used to overwrite the version of a mod without actually + * updating the mod + * + * @param version the new version to use + **/ + void setVersion(const VersionInfo &version); + + /** + * @brief set the newest version of this mod on the nexus + * + * this can be used to overwrite the version of a mod without actually + * updating the mod + * + * @param version the new version to use + * @todo this function should be made obsolete. All queries for mod information should go through + * this class so no public function for this change is required + **/ + void setNewestVersion(const VersionInfo &version) { m_NewestVersion = version; } + + /** + * @brief changes/updates the nexus description text + * @param description the current description text + */ + virtual void setNexusDescription(const QString &description); + + /** + * update the endorsement state for the mod. This only changes the + * buffered state, it does not sync with Nexus + * @param endorsed the new endorsement state + */ + virtual void setIsEndorsed(bool endorsed); + + /** + * @brief delete the mod from the disc. This does not update the global ModInfo structure or indices + * @return true if the mod was successfully removed + **/ + bool remove(); + + /** + * @brief endorse or un-endorse the mod + * @param doEndorse if true, the mod is endorsed, if false, it's un-endorsed. + * @note if doEndorse doesn't differ from the current value, nothing happens. + */ + virtual void endorse(bool doEndorse); + + /** + * @brief getter for the mod name + * + * @return the mod name + **/ + QString name() const { return m_Name; } + + /** + * @brief getter for the mod path + * + * @return the (absolute) path to the mod + **/ + QString absolutePath() const; + + /** + * @brief getter for the newest version number of this mod + * + * @return newest version of the mod + **/ + VersionInfo getNewestVersion() const { return m_NewestVersion; } + + /** + * @brief getter for the installation file + * + * @return file used to install this mod from + */ + virtual QString getInstallationFile() const { return m_InstallationFile; } + /** + * @brief getter for the nexus mod id + * + * @return the nexus mod id. may be 0 if the mod id isn't known or doesn't exist + **/ + int getNexusID() const { return m_NexusID; } + + /** + * @return the fixed priority of mods of this type or INT_MIN if the priority of mods + * needs to be user-modifiable + */ + virtual int getFixedPriority() const { return INT_MIN; } + + /** + * @return true if the mod can be updated + */ + virtual bool canBeUpdated() const { return m_NexusID >= 0; } + + /** + * @return true if the mod can be enabled/disabled + */ + virtual bool canBeEnabled() const { return true; } + + /** + * @return a list of flags for this mod + */ + virtual std::vector getFlags() const; + + /** + * @return an indicator if and how this mod should be highlighted by the UI + */ + virtual int getHighlight() const; + + /** + * @return list of names of ini tweaks + **/ + std::vector getIniTweaks() const; + + /** + * @return a description about the mod, to be displayed in the ui + */ + virtual QString getDescription() const; + + /** + * @return manually set notes for this mod + */ + virtual QString notes() const; + + /** + * @brief return the list of files on nexus related to this mod in the form of iterators + * @note this is valid only after querying the files + * @param begin iterator to the first file + * @param end iterator to one past the last file + */ + virtual void getNexusFiles(QList::const_iterator &begin, QList::const_iterator &end); + + /** + * @return nexus description of the mod (html) + */ + QString getNexusDescription() const; + + /** + * @return true if the file has been endorsed on nexus + */ + virtual EEndorsedState endorsedState() const; + + /** + * @return last time nexus was queried for infos on this mod + */ + QDateTime getLastNexusQuery() const; + + /** + * @brief stores meta information back to disk + */ + virtual void saveMeta(); + +private: + + enum EConflictType { + CONFLICT_NONE, + CONFLICT_OVERWRITE, + CONFLICT_OVERWRITTEN, + CONFLICT_MIXED, + CONFLICT_REDUNDANT + }; + +private slots: + + void nxmDescriptionAvailable(int modID, QVariant userData, QVariant resultData); + void nxmFilesAvailable(int, QVariant, QVariant resultData); + void nxmEndorsementToggled(int, QVariant userData, QVariant resultData); + void nxmRequestFailed(int modID, QVariant userData, const QString &errorMessage); + +private: + + /** + * @return true if there is a conflict for files in this mod + */ + EConflictType isConflicted() const; + + /** + * @return true if this mod is completely replaced by others + */ + bool isRedundant() const; + +protected: + + ModInfoRegular(const QDir &path, DirectoryEntry **directoryStructure); + +private: + + QString m_Name; + QString m_Path; + QString m_InstallationFile; + QString m_Notes; + QString m_NexusDescription; + QList m_NexusFileInfos; + + QDateTime m_LastNexusQuery; + + int m_NexusID; + + bool m_MetaInfoChanged; + VersionInfo m_NewestVersion; + + EEndorsedState m_EndorsedState; + + NexusBridge m_NexusBridge; + + DirectoryEntry **m_DirectoryStructure; + +}; + + +class ModInfoBackup : public ModInfoRegular +{ + + friend class ModInfo; + +public: + + virtual bool updateAvailable() const { return false; } + virtual bool updateNXMInfo() { return false; } + virtual void setNexusID(int) {} + virtual void endorse(bool) {} + virtual int getFixedPriority() const { return -1; } + virtual bool canBeUpdated() const { return false; } + virtual bool canBeEnabled() const { return false; } + virtual std::vector getIniTweaks() const { return std::vector(); } + virtual std::vector getFlags() const; + virtual QString getDescription() const; + virtual QDateTime getLastNexusQuery() const { return QDateTime(); } + virtual void getNexusFiles(QList::const_iterator&, QList::const_iterator&) {} + virtual QString getNexusDescription() const { return QString(); } + +private: + + ModInfoBackup(const QDir &path, DirectoryEntry **directoryStructure); + +}; + + +class ModInfoOverwrite : public ModInfo +{ + + Q_OBJECT + + friend class ModInfo; + +public: + + virtual bool updateAvailable() const { return false; } + virtual bool updateNXMInfo() { return false; } + virtual void setCategory(int, bool) {} + virtual bool setName(const QString&) { return false; } + virtual void setNotes(const QString&) {} + virtual void setNexusID(int) {} + virtual void setNewestVersion(const VersionInfo&) {} + virtual void setNexusDescription(const QString&) {} + virtual void setIsEndorsed(bool) {} + virtual bool remove() { return false; } + virtual void endorse(bool) {} + virtual QString name() const { return tr("Overwrite"); } + virtual QString notes() const { return ""; } + virtual QString absolutePath() const; + virtual VersionInfo getNewestVersion() const { return ""; } + virtual QString getInstallationFile() const { return ""; } + virtual int getFixedPriority() const { return INT_MAX; } + virtual int getNexusID() const { return -1; } + virtual std::vector getIniTweaks() const { return std::vector(); } + virtual std::vector getFlags() const; + virtual int getHighlight() const; + virtual QString getDescription() const; + virtual QDateTime getLastNexusQuery() const { return QDateTime(); } + virtual void getNexusFiles(QList::const_iterator&, QList::const_iterator&) {} + virtual QString getNexusDescription() const { return QString(); } + +private: + + ModInfoOverwrite(); + +}; + +#endif // MODINFO_H diff --git a/src/modinfodialog.cpp b/src/modinfodialog.cpp index 88440d99..14b40c91 100644 --- a/src/modinfodialog.cpp +++ b/src/modinfodialog.cpp @@ -17,1202 +17,1201 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include "modinfodialog.h" -#include "ui_modinfodialog.h" - -#include "report.h" -#include "utility.h" -#include "json.h" -#include "messagedialog.h" -#include "bbcode.h" -#include "questionboxmemory.h" -#include "settings.h" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -using QtJson::Json; - - -class ModFileListWidget : public QListWidgetItem { - friend bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS); -public: - ModFileListWidget(const QString &text, int sortValue, QListWidget *parent = 0) - : QListWidgetItem(text, parent, QListWidgetItem::UserType + 1), m_SortValue(sortValue) {} -private: - int m_SortValue; -}; - - -bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS) -{ - return LHS.m_SortValue < RHS.m_SortValue; -} - - -ModInfoDialog::ModInfoDialog(ModInfo::Ptr modInfo, const DirectoryEntry *directory, QWidget *parent) - : TutorableDialog("ModInfoDialog", parent), ui(new Ui::ModInfoDialog), m_ModInfo(modInfo), - m_ThumbnailMapper(this), m_RequestStarted(false), - m_DeleteAction(NULL), m_RenameAction(NULL), m_OpenAction(NULL), - m_Directory(directory), m_Origin(NULL) -{ - ui->setupUi(this); - this->setWindowTitle(modInfo->name()); - - m_UTF8Codec = QTextCodec::codecForName("utf-8"); - - QListWidget *textFileList = findChild("textFileList"); - QListWidget *iniFileList = findChild("iniFileList"); - QListWidget *iniTweaksList = findChild("iniTweaksList"); - QListWidget *activeESPList = findChild("activeESPList"); - QListWidget *inactiveESPList = findChild("inactiveESPList"); - QTreeWidget *categoriesTree = findChild("categoriesTree"); - QHBoxLayout *thumbnailArea = findChild("thumbnailArea"); - - m_FileTree = findChild("fileTree"); - - m_RootPath = modInfo->absolutePath(); - - QString metaFileName = m_RootPath.mid(0).append("/meta.ini"); - m_Settings = new QSettings(metaFileName, QSettings::IniFormat); - - QLineEdit *modIDEdit = findChild("modIDEdit"); - modIDEdit->setValidator(new QIntValidator(modIDEdit)); - modIDEdit->setText(QString("%1").arg(modInfo->getNexusID())); - - ui->notesEdit->setText(modInfo->notes()); - - m_FileSystemModel = new QFileSystemModel(this); - m_FileSystemModel->setReadOnly(false); - m_FileSystemModel->setRootPath(m_RootPath); - m_FileTree->setModel(m_FileSystemModel); - m_FileTree->setRootIndex(m_FileSystemModel->index(m_RootPath)); - m_FileTree->setColumnWidth(0, 300); - - connect(&m_ThumbnailMapper, SIGNAL(mapped(const QString&)), this, SIGNAL(thumbnailClickedSignal(const QString&))); - connect(this, SIGNAL(thumbnailClickedSignal(const QString&)), this, SLOT(thumbnailClicked(const QString&))); - - m_DeleteAction = new QAction(tr("&Delete"), m_FileTree); - m_RenameAction = new QAction(tr("&Rename"), m_FileTree); - m_HideAction = new QAction(tr("&Hide"), m_FileTree); - m_UnhideAction = new QAction(tr("&Unhide"), m_FileTree); - m_OpenAction = new QAction(tr("&Open"), m_FileTree); - m_NewFolderAction = new QAction(tr("&New Folder"), m_FileTree); - QObject::connect(m_DeleteAction, SIGNAL(triggered()), this, SLOT(deleteTriggered())); - QObject::connect(m_RenameAction, SIGNAL(triggered()), this, SLOT(renameTriggered())); - QObject::connect(m_OpenAction, SIGNAL(triggered()), this, SLOT(openTriggered())); - QObject::connect(m_NewFolderAction, SIGNAL(triggered()), this, SLOT(createDirectoryTriggered())); - QObject::connect(m_HideAction, SIGNAL(triggered()), this, SLOT(hideTriggered())); - connect(m_UnhideAction, SIGNAL(triggered()), this, SLOT(unhideTriggered())); - connect(m_ModInfo.data(), SIGNAL(modDetailsUpdated(bool)), this, SLOT(modDetailsUpdated(bool))); - - ui->descriptionView->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); - QObject::connect(ui->descriptionView, SIGNAL(linkClicked(QUrl)), this, SLOT(linkClicked(QUrl))); - - if (directory->originExists(ToWString(modInfo->name()))) { - m_Origin = &directory->getOriginByName(ToWString(modInfo->name())); - if (m_Origin->isDisabled()) { - m_Origin = NULL; - } - } - - addCategories(CategoryFactory::instance(), modInfo->getCategories(), categoriesTree->invisibleRootItem(), 0); - refreshPrimaryCategoriesBox(); - refreshLists(); - - int numTweaks = m_Settings->beginReadArray("INI Tweaks"); - for (int i = 0; i < numTweaks; ++i) { - m_Settings->setArrayIndex(i); - QList items = iniTweaksList->findItems(m_Settings->value("name").toString(), Qt::MatchFixedString); - if (items.size() != 0) { - items.at(0)->setCheckState(Qt::Checked); - } - } - m_Settings->endArray(); - - QTabWidget *tabWidget = findChild("tabWidget"); - tabWidget->setTabEnabled(TAB_TEXTFILES, textFileList->count() != 0); - tabWidget->setTabEnabled(TAB_INIFILES, (iniFileList->count() != 0) || (iniTweaksList->count() != 0)); - tabWidget->setTabEnabled(TAB_IMAGES, thumbnailArea->count() != 0); - tabWidget->setTabEnabled(TAB_ESPS, (inactiveESPList->count() != 0) || (activeESPList->count() != 0)); - tabWidget->setTabEnabled(TAB_CONFLICTS, m_Origin != NULL); - - if (tabWidget->currentIndex() == TAB_NEXUS) { - activateNexusTab(); - } - - ui->endorseBtn->setEnabled(m_ModInfo->endorsedState() == ModInfo::ENDORSED_FALSE); -} - - -ModInfoDialog::~ModInfoDialog() -{ - m_ModInfo->setNotes(ui->notesEdit->toPlainText()); - saveIniTweaks(); - saveCategories(ui->categoriesTree->invisibleRootItem()); - - delete ui; - delete m_Settings; -} - - -void ModInfoDialog::refreshLists() -{ - int numNonConflicting = 0; - int numOverwrite = 0; - int numOverwritten = 0; - - ui->overwriteTree->clear(); - ui->overwrittenTree->clear(); - - if (m_Origin != NULL) { - std::vector files = m_Origin->getFiles(); - for (auto iter = files.begin(); iter != files.end(); ++iter) { - QString relativeName = QDir::fromNativeSeparators(ToQString((*iter)->getRelativePath())); - QString fileName = relativeName.mid(0).prepend(m_RootPath); - bool ignore; - if ((*iter)->getOrigin(ignore) == m_Origin->getID()) { - std::vector alternatives = (*iter)->getAlternatives(); - if (!alternatives.empty()) { - std::wostringstream altString; - for (std::vector::iterator altIter = alternatives.begin(); - altIter != alternatives.end(); ++altIter) { - if (altIter != alternatives.begin()) { - altString << ", "; - } - altString << m_Directory->getOriginByID(*altIter).getName(); - } - QStringList fields(relativeName.prepend("...")); - fields.append(ToQString(altString.str())); - QTreeWidgetItem *item = new QTreeWidgetItem(fields); - item->setData(0, Qt::UserRole, fileName); - item->setData(1, Qt::UserRole, ToQString(m_Directory->getOriginByID(alternatives[0]).getName())); - item->setData(1, Qt::UserRole + 1, alternatives[0]); - ui->overwriteTree->addTopLevelItem(item); - ++numOverwrite; - } else {// otherwise don't display the file - ++numNonConflicting; - } - } else { - FilesOrigin &realOrigin = m_Directory->getOriginByID((*iter)->getOrigin(ignore)); - QStringList fields(relativeName); - fields.append(ToQString(realOrigin.getName())); - QTreeWidgetItem *item = new QTreeWidgetItem(fields); - item->setData(1, Qt::UserRole, ToQString(realOrigin.getName())); - ui->overwrittenTree->addTopLevelItem(item); - ++numOverwritten; - } - } - } - - - QDirIterator dirIterator(m_RootPath, QDir::Files, QDirIterator::Subdirectories); - while (dirIterator.hasNext()) { - QString fileName = dirIterator.next(); - - if (fileName.endsWith(".txt", Qt::CaseInsensitive)) { - ui->textFileList->addItem(fileName.mid(m_RootPath.length() + 1)); - } else if ((fileName.endsWith(".ini", Qt::CaseInsensitive) || fileName.endsWith(".cfg", Qt::CaseInsensitive)) && - !fileName.endsWith("meta.ini")) { - QString namePart = fileName.mid(m_RootPath.length() + 1); - if (namePart.startsWith("INI Tweaks", Qt::CaseInsensitive)) { - QListWidgetItem *newItem = new QListWidgetItem(namePart.mid(11), ui->iniTweaksList); - newItem->setData(Qt::UserRole, namePart); - newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); - newItem->setCheckState(Qt::Unchecked); - ui->iniTweaksList->addItem(newItem); - } else { - ui->iniFileList->addItem(namePart); - } - } else if (fileName.endsWith(".esp", Qt::CaseInsensitive) || - fileName.endsWith(".esm", Qt::CaseInsensitive)) { - QString relativePath = fileName.mid(m_RootPath.length() + 1); - if (relativePath.contains('/')) { - QFileInfo fileInfo(fileName); - QListWidgetItem *newItem = new QListWidgetItem(fileInfo.fileName()); - newItem->setData(Qt::UserRole, relativePath); - ui->inactiveESPList->addItem(newItem); - } else { - ui->activeESPList->addItem(relativePath); - } - } else if ((fileName.endsWith(".png", Qt::CaseInsensitive)) || - (fileName.endsWith(".jpg", Qt::CaseInsensitive))) { - QImage image = QImage(fileName); - if (static_cast(image.width()) / static_cast(image.height()) > 1.34) { - image = image.scaledToWidth(128); - } else { - image = image.scaledToHeight(96); - } - - QPushButton *thumbnailButton = new QPushButton(QPixmap::fromImage(image), ""); - thumbnailButton->setIconSize(QSize(image.width(), image.height())); - connect(thumbnailButton, SIGNAL(clicked()), &m_ThumbnailMapper, SLOT(map())); - m_ThumbnailMapper.setMapping(thumbnailButton, fileName); - ui->thumbnailArea->addWidget(thumbnailButton); - } - } - - ui->overwriteCount->display(numOverwrite); - ui->overwrittenCount->display(numOverwritten); - ui->noConflictCount->display(numNonConflicting); -} - - -void ModInfoDialog::addCategories(const CategoryFactory &factory, const std::set &enabledCategories, QTreeWidgetItem *root, int rootLevel) -{ - for (unsigned int i = 0; i < factory.numCategories(); ++i) { - if (factory.getParentID(i) != rootLevel) { - continue; - } - int categoryID = factory.getCategoryID(i); - QTreeWidgetItem *newItem = new QTreeWidgetItem(QStringList(factory.getCategoryName(i))); - newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); - newItem->setCheckState(0, enabledCategories.find(categoryID) != enabledCategories.end() ? Qt::Checked : Qt::Unchecked); - newItem->setData(0, Qt::UserRole, categoryID); - if (factory.hasChildren(i)) { - addCategories(factory, enabledCategories, newItem, categoryID); - } - root->addChild(newItem); - } -} - - -void ModInfoDialog::saveCategories(QTreeWidgetItem *currentNode) -{ - for (int i = 0; i < currentNode->childCount(); ++i) { - QTreeWidgetItem *childNode = currentNode->child(i); - m_ModInfo->setCategory(childNode->data(0, Qt::UserRole).toInt(), childNode->checkState(0)); - saveCategories(childNode); - } -} - - -void ModInfoDialog::on_closeButton_clicked() -{ - if (allowNavigateFromTXT() && allowNavigateFromINI()) { - this->close(); - } -} - - - -QString ModInfoDialog::getModVersion() const -{ - return m_Settings->value("version", "").toString(); -} - - -const int ModInfoDialog::getModID() const -{ - return m_Settings->value("modid", 0).toInt(); -} - - -void ModInfoDialog::openTab(int tab) -{ - QTabWidget *tabWidget = findChild("tabWidget"); - if (tabWidget->isTabEnabled(tab)) { - tabWidget->setCurrentIndex(tab); - } -} - - -void ModInfoDialog::thumbnailClicked(const QString &fileName) -{ - QLabel *imageLabel = findChild("imageLabel"); - QImage image(fileName); - if (static_cast(image.width()) / static_cast(image.height()) > 1.34) { - image = image.scaledToWidth(imageLabel->width()); - } else { - image = image.scaledToHeight(imageLabel->height()); - } - imageLabel->setPixmap(QPixmap::fromImage(image)); -} - - -bool ModInfoDialog::allowNavigateFromTXT() -{ - if (ui->saveTXTButton->isEnabled()) { - int res = QMessageBox::question(this, tr("Save changes?"), tr("Save changes to \"%1\"?").arg(ui->textFileView->property("currentFile").toString()), - QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); - if (res == QMessageBox::Cancel) { - return false; - } else if (res == QMessageBox::Yes) { - saveCurrentTextFile(); - } - } - return true; -} - - -bool ModInfoDialog::allowNavigateFromINI() -{ - if (ui->saveButton->isEnabled()) { - int res = QMessageBox::question(this, tr("Save changes?"), tr("Save changes to \"%1\"?").arg(ui->iniFileView->property("currentFile").toString()), - QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); - if (res == QMessageBox::Cancel) { - return false; - } else if (res == QMessageBox::Yes) { - saveCurrentIniFile(); - } - } - return true; -} - - -void ModInfoDialog::on_textFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) -{ - QString fullPath = m_RootPath + "/" + current->text(); - - QVariant currentFile = ui->textFileView->property("currentFile"); - if (currentFile.isValid() && (currentFile.toString() == fullPath)) { - // the new file is the same as the currently displayed file. May be the result of a cancelation - return; - } - - if (allowNavigateFromTXT()) { - openTextFile(fullPath); - } else { - ui->textFileList->setCurrentItem(previous, QItemSelectionModel::Current); - } -} - - -void ModInfoDialog::openTextFile(const QString &fileName) -{ - QFile textFile(fileName); - textFile.open(QIODevice::ReadOnly); - QByteArray buffer = textFile.readAll(); - QTextCodec *codec = QTextCodec::codecForUtfText(buffer, m_UTF8Codec); - ui->textFileView->setText(codec->toUnicode(buffer)); - ui->textFileView->setProperty("currentFile", fileName); - ui->textFileView->setProperty("encoding", codec->name()); - ui->saveTXTButton->setEnabled(false); -} - - -void ModInfoDialog::openIniFile(const QString &fileName) -{ - QPushButton* saveButton = findChild("saveButton"); - - QFile iniFile(fileName); - iniFile.open(QIODevice::ReadOnly); - QByteArray buffer = iniFile.readAll(); - QTextCodec *codec = QTextCodec::codecForUtfText(buffer, m_UTF8Codec); - QTextEdit *iniFileView = findChild("iniFileView"); - iniFileView->setText(codec->toUnicode(buffer)); - iniFileView->setProperty("currentFile", fileName); - iniFileView->setProperty("encoding", codec->name()); - iniFile.close(); - - saveButton->setEnabled(false); -} - - -void ModInfoDialog::saveIniTweaks() -{ - QListWidget *iniTweaksList = findChild("iniTweaksList"); - - m_Settings->beginWriteArray("INI Tweaks"); - - int countEnabled = 0; - for (int i = 0; i < iniTweaksList->count(); ++i) { - if (iniTweaksList->item(i)->checkState() == Qt::Checked) { - m_Settings->setArrayIndex(countEnabled++); - m_Settings->setValue("name", iniTweaksList->item(i)->text()); - } - } - - m_Settings->endArray(); -} - - -void ModInfoDialog::on_iniFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) -{ - QString fullPath = m_RootPath + "/" + current->text(); - - QVariant currentFile = ui->iniFileView->property("currentFile"); - if (currentFile.isValid() && (currentFile.toString() == fullPath)) { - // the new file is the same as the currently displayed file. May be the result of a cancelation - return; - } - - if (allowNavigateFromINI()) { - openIniFile(fullPath); - } else { - ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current); - } -} - - -void ModInfoDialog::on_iniTweaksList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) -{ - QString fullPath = m_RootPath + "/" + current->data(Qt::UserRole).toString(); - - QVariant currentFile = ui->iniFileView->property("currentFile"); - if (currentFile.isValid() && (currentFile.toString() == fullPath)) { - // the new file is the same as the currently displayed file. May be the result of a cancelation - return; - } - - if (allowNavigateFromINI()) { - openIniFile(fullPath); - } else { - ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current); - } - -} - - -void ModInfoDialog::on_saveButton_clicked() -{ - saveCurrentIniFile(); -} - - -void ModInfoDialog::on_saveTXTButton_clicked() -{ - saveCurrentTextFile(); -} - - -void ModInfoDialog::saveCurrentTextFile() -{ - QVariant fileNameVar = ui->textFileView->property("currentFile"); - QVariant encodingVar = ui->textFileView->property("encoding"); - if (fileNameVar.isValid() && encodingVar.isValid()) { - QString fileName = fileNameVar.toString(); - QFile txtFile(fileName); - txtFile.open(QIODevice::WriteOnly); - txtFile.resize(0); - QTextCodec *codec = QTextCodec::codecForName(encodingVar.toString().toUtf8()); - QString data = ui->textFileView->toPlainText().replace("\n", "\r\n"); - txtFile.write(codec->fromUnicode(data)); - } else { - reportError("no file selected"); - } - ui->saveTXTButton->setEnabled(false); -} - - -void ModInfoDialog::saveCurrentIniFile() -{ - QVariant fileNameVar = ui->iniFileView->property("currentFile"); - QVariant encodingVar = ui->iniFileView->property("encoding"); - if (fileNameVar.isValid()) { - QString fileName = fileNameVar.toString(); - QFile txtFile(fileName); - txtFile.open(QIODevice::WriteOnly); - txtFile.resize(0); - QTextCodec *codec = QTextCodec::codecForName(encodingVar.toString().toUtf8()); - QString data = ui->iniFileView->toPlainText().replace("\n", "\r\n"); - txtFile.write(codec->fromUnicode(data)); - } else { - reportError("no file selected"); - } - ui->saveButton->setEnabled(false); -} - - -void ModInfoDialog::on_iniFileView_textChanged() -{ - QPushButton* saveButton = findChild("saveButton"); - saveButton->setEnabled(true); -} - - -void ModInfoDialog::on_textFileView_textChanged() -{ - ui->saveTXTButton->setEnabled(true); -} - - -void ModInfoDialog::on_activateESP_clicked() -{ - QListWidget *activeESPList = findChild("activeESPList"); - QListWidget *inactiveESPList = findChild("inactiveESPList"); - - int selectedRow = inactiveESPList->currentRow(); - if (selectedRow < 0) { - return; - } - - QListWidgetItem *selectedItem = inactiveESPList->takeItem(selectedRow); - - QDir root(m_RootPath); - bool renamed = false; - - while (root.exists(selectedItem->text())) { - bool okClicked = false; - QString newName = QInputDialog::getText(this, tr("File Exists"), tr("A file with that name exists, please enter a new one"), QLineEdit::Normal, selectedItem->text(), &okClicked); - if (!okClicked) { - inactiveESPList->insertItem(selectedRow, selectedItem); - return; - } else if (newName.size() > 0) { - selectedItem->setText(newName); - renamed = true; - } - } - - if (root.rename(selectedItem->data(Qt::UserRole).toString(), selectedItem->text())) { - activeESPList->addItem(selectedItem); - if (renamed) { - selectedItem->setData(Qt::UserRole, QVariant()); - } - } else { - inactiveESPList->insertItem(selectedRow, selectedItem); - reportError(tr("failed to move file")); - } -} - - -void ModInfoDialog::on_deactivateESP_clicked() -{ - QListWidget *activeESPList = findChild("activeESPList"); - QListWidget *inactiveESPList = findChild("inactiveESPList"); - - int selectedRow = activeESPList->currentRow(); - if (selectedRow < 0) { - return; - } - - QDir root(m_RootPath); - - QListWidgetItem *selectedItem = activeESPList->takeItem(selectedRow); - - // if we moved the file from optional to active in this session, we move the file back to - // where it came from. Otherwise, it is moved to the new folder "optional" - if (selectedItem->data(Qt::UserRole).isNull()) { - selectedItem->setData(Qt::UserRole, QString("optional/") + selectedItem->text()); - if (!root.exists("optional")) { - if (!root.mkdir("optional")) { - reportError(tr("failed to create directory \"optional\"")); - activeESPList->insertItem(selectedRow, selectedItem); - return; - } - } - } - - if (root.rename(selectedItem->text(), selectedItem->data(Qt::UserRole).toString())) { - inactiveESPList->addItem(selectedItem); - } else { - activeESPList->insertItem(selectedRow, selectedItem); - } -} - -void ModInfoDialog::on_visitNexusLabel_linkActivated(const QString &link) -{ - this->close(); - emit nexusLinkActivated(link); -} - -void ModInfoDialog::linkClicked(const QUrl &url) -{ - if (url.toString().startsWith(ToQString(GameInfo::instance().getNexusPage()))) { - this->close(); - emit nexusLinkActivated(url.toString()); - } else { - ::ShellExecuteW(NULL, L"open", ToWString(url.toString()).c_str(), NULL, NULL, SW_SHOWNORMAL); - } -} - - -void ModInfoDialog::refreshNexusData(int modID) -{ - if ((!m_RequestStarted) && (modID > 0)) { - m_RequestStarted = true; - - m_ModInfo->updateNXMInfo(); - - MessageDialog::showMessage(tr("Info requested, please wait"), this); - } -} - - -/*void ModInfoDialog::nxmDescriptionAvailable(int, QVariant, QVariant resultData, int requestID) -{ - std::set::iterator idIter = m_RequestIDs.find(requestID); - if (idIter == m_RequestIDs.end()) { - return; - } else { - m_RequestIDs.erase(idIter); - } - - QVariantMap result = resultData.toMap(); - - if (!result["description"].isNull()) { - QString descriptionAsHTML = - QString("" - "" - "%1" - "").arg(BBCode::convertToHTML(result["description"].toString())); - -// QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString()); - ui->descriptionView->setHtml(descriptionAsHTML); - } else { - ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)"))); - } - - QLineEdit *versionEdit = findChild("versionEdit"); - QString version = result["version"].toString(); - - if (!version.isEmpty()) { - m_ModInfo->setNewestVersion(version); - - VersionInfo currentVersion(versionEdit->text()); - VersionInfo newestVersion(version); - - QPalette versionColor; - if (currentVersion < newestVersion) { - versionColor.setColor(QPalette::Text, Qt::red); - versionEdit->setToolTip(tr("Current Version: %1").arg(version)); - } else { - versionColor.setColor(QPalette::Text, Qt::green); - versionEdit->setToolTip(tr("No update available")); - } - versionEdit->setPalette(versionColor); - } -}*/ - - -QString ModInfoDialog::getFileCategory(int categoryID) -{ - switch (categoryID) { - 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"); - } -} - - -void ModInfoDialog::updateVersionColor() -{ - QPalette versionColor; - if (m_ModInfo->getVersion() < m_ModInfo->getNewestVersion()) { - versionColor.setColor(QPalette::Text, Qt::red); - ui->versionEdit->setToolTip(tr("Current Version: %1").arg(m_ModInfo->getNewestVersion().canonicalString())); - } else { - versionColor.setColor(QPalette::Text, Qt::green); - ui->versionEdit->setToolTip(tr("No update available")); - } - ui->versionEdit->setPalette(versionColor); -} - - -void ModInfoDialog::modDetailsUpdated(bool success) -{ - if (success) { - QString nexusDescription = m_ModInfo->getNexusDescription(); - if (!nexusDescription.isEmpty()) { - /* QString input = - "[size=20]sizetest[/size]\r\n" - "[COLOR=yellow]colortest[/COLOR]\r\n" - "[center]centertest[/center]\r\n" - "[quote]quotetest 1[/quote]\r\n" - "[quote=bla]quotetest 2[/quote]\r\n" - "[url]www.skyrimnexus.com[/url]\r\n" - "[url=www.skyrimnexus.com]urltest 2[/url]\r\n" - "[ol]\r\n" - "[li]item 2[/li]" - "[*]item 1\r\n" - "[/ol]\r\n" - "[img]http://www.bbcode.org/images/bbcode_logo.png[/img]\r\n" - "[table][tr][th]headertest1[/th]" - "[th]headertest2[/th][/tr]" - "[tr][td]rowtest11[/td][td]rowtest12[/td][/tr]" - "[tr][td]rowtest21[/td][td]rowtest22[/td][/tr][/table]" - "[email=\"sherb@gmx.net\"]mail me[/email]"; - ui->descriptionView->setHtml(BBCode::convertToHTML(input));*/ - - QString descriptionAsHTML = - QString("" - "" - "%1" - "").arg(BBCode::convertToHTML(nexusDescription)); - - // QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString()); - ui->descriptionView->setHtml(descriptionAsHTML); - } else { - // ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)"))); - ui->descriptionView->setHtml(tr("(description incomplete, please visit nexus)")); - } - - QString version = m_ModInfo->getNewestVersion().canonicalString(); - - if (!version.isEmpty()) { - m_ModInfo->setNewestVersion(version); - - updateVersionColor(); - } - - QTableWidget *filesWidget = findChild("filesWidget"); - filesWidget->clearContents(); - filesWidget->setRowCount(0); - filesWidget->setColumnCount(5); -#if QT_VERSION >= 0x050000 - filesWidget->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); -#else - filesWidget->horizontalHeader()->setResizeMode(1, QHeaderView::Stretch); -#endif - filesWidget->horizontalHeader()->setVisible(true); - filesWidget->setColumnHidden(4, true); - - - // prevent sorting while we change the table - filesWidget->setSortingEnabled(false); - int row = 0; - QList::const_iterator current, end; - m_ModInfo->getNexusFiles(current, end); - for (; current != end; ++current) { - filesWidget->insertRow(row); - int type = current->category; - filesWidget->setItem(row, 4, new QTableWidgetItem(QString("%1").arg(type))); - filesWidget->setItem(row, 0, new QTableWidgetItem(getFileCategory(type))); - QTableWidgetItem *nameItem = new QTableWidgetItem(current->name); - nameItem->setToolTip(current->description); - nameItem->setData(Qt::UserRole, current->id); - filesWidget->setItem(row, 1, nameItem); - filesWidget->setItem(row, 2, new QTableWidgetItem(current->version)); - QTableWidgetItem *sizeItem = new QTableWidgetItem(); - sizeItem->setData(Qt::DisplayRole, current->size); - filesWidget->setItem(row, 3, sizeItem); - ++row; - } - filesWidget->setSortingEnabled(true); - filesWidget->sortItems(4); - } -} - - -void ModInfoDialog::activateNexusTab() -{ - QLineEdit *modIDEdit = findChild("modIDEdit"); - int modID = modIDEdit->text().toInt(); - if (modID != 0) { - QString nexusLink = QString("%1/downloads/file.php?id=%2").arg(ToQString(GameInfo::instance().getNexusPage())).arg(modID); - QLabel *visitNexusLabel = findChild("visitNexusLabel"); - visitNexusLabel->setText(tr("Visit on Nexus").arg(nexusLink)); - visitNexusLabel->setToolTip(nexusLink); - - - if (m_ModInfo->getNexusDescription().isEmpty() || - QDateTime::currentDateTime() > m_ModInfo->getLastNexusQuery().addDays(1)) { - refreshNexusData(modID); - } else { - this->modDetailsUpdated(true); - } - } - QLineEdit *versionEdit = findChild("versionEdit"); - QString currentVersion = m_Settings->value("version", "0.0").toString(); - versionEdit->setText(currentVersion); -} - - -void ModInfoDialog::on_tabWidget_currentChanged(int index) -{ - if (index == TAB_NEXUS) { - activateNexusTab(); - } -} - - -void ModInfoDialog::on_modIDEdit_editingFinished() -{ - int oldID = m_Settings->value("modid", 0).toInt(); - int modID = ui->modIDEdit->text().toInt(); - if (oldID != modID){ - m_ModInfo->setNexusID(modID); - - ui->descriptionView->setHtml(""); - if (modID != 0) { - m_RequestStarted = false; - refreshNexusData(modID); - } - } -} - -void ModInfoDialog::on_versionEdit_editingFinished() -{ - VersionInfo version(ui->versionEdit->text()); - m_ModInfo->setVersion(version); - updateVersionColor(); -} - -void ModInfoDialog::on_filesWidget_doubleClicked(const QModelIndex &index) -{ - QTableWidget *filesWidget = findChild("filesWidget"); - - QTableWidgetItem *selectedItem = filesWidget->item(index.row(), 1); - if (selectedItem == NULL) { - qDebug("no item in row %d", index.row()); - return; - } - - QString nexusLink = QString("nxm://%1/mods/%2/files/%3") - .arg(ToQString(GameInfo::instance().getGameName())) - .arg(m_Settings->value("modid").toInt()) - .arg(selectedItem->data(Qt::UserRole).toInt()); - - try { - if (QuestionBoxMemory::query(this, Settings::instance().directInterface(), "confirmNexusDownload", - tr("Confirm"), tr("Download \"%1\"?").arg(selectedItem->text()), - QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::Yes) { - emit downloadRequest(nexusLink); - MessageDialog::showMessage(tr("Download started"), this); - } - } catch (const std::exception &e) { - reportError(tr("Exception: %1").arg(e.what())); - } -} - - -bool ModInfoDialog::recursiveDelete(const QModelIndex &index) -{ - for (int childRow = 0; childRow < m_FileSystemModel->rowCount(index); ++childRow) { - QModelIndex childIndex = m_FileSystemModel->index(childRow, 0, index); - if (m_FileSystemModel->isDir(childIndex)) { - if (!recursiveDelete(childIndex)) { - qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData()); - return false; - } - } else { - if (!m_FileSystemModel->remove(childIndex)) { - qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData()); - return false; - } - } - } - if (!m_FileSystemModel->remove(index)) { - qCritical("failed to delete %s", m_FileSystemModel->fileName(index).toUtf8().constData()); - return false; - } - return true; -} - - -void ModInfoDialog::deleteFile(const QModelIndex &index) -{ - - bool res = m_FileSystemModel->isDir(index) ? recursiveDelete(index) - : m_FileSystemModel->remove(index); - if (!res) { - QString fileName = m_FileSystemModel->fileName(index); - reportError(tr("Failed to delete %1").arg(fileName)); - } -} - - -void ModInfoDialog::deleteTriggered() -{ - if (m_FileSelection.count() == 0) { - return; - } else if (m_FileSelection.count() == 1) { - QString fileName = m_FileSystemModel->fileName(m_FileSelection.at(0)); - if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete \"%1\"?").arg(fileName), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } else { - if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete the selected files?"), - QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { - return; - } - } - - foreach(QModelIndex index, m_FileSelection) { - deleteFile(index); - } -} - - -void ModInfoDialog::renameTriggered() -{ - QModelIndex selection = m_FileSelection.at(0); - QModelIndex index = selection.sibling(selection.row(), 0); - if (!index.isValid() || m_FileSystemModel->isReadOnly()) { - return; - } - - m_FileTree->edit(index); -} - - -void ModInfoDialog::hideTriggered() -{ - for (QModelIndexList::const_iterator iter = m_FileSelection.constBegin(); - iter != m_FileSelection.constEnd(); ++iter) { - QString path = m_FileSystemModel->filePath(*iter); - if (!path.endsWith(ModInfo::s_HiddenExt)) { - hideFile(path); - } - } -} - - -void ModInfoDialog::unhideTriggered() -{ - for (QModelIndexList::const_iterator iter = m_FileSelection.constBegin(); - iter != m_FileSelection.constEnd(); ++iter) { - QString path = m_FileSystemModel->filePath(*iter); - if (path.endsWith(ModInfo::s_HiddenExt)) { - unhideFile(path); - } - } -} - - -void ModInfoDialog::openFile(const QModelIndex &index) -{ - QString fileName = m_FileSystemModel->filePath(index); - - HINSTANCE res = ::ShellExecuteW(NULL, L"open", ToWString(fileName).c_str(), NULL, NULL, SW_SHOW); - if ((int)res <= 32) { - qCritical("failed to invoke %s: %d", fileName.toUtf8().constData(), res); - } -} - - -void ModInfoDialog::openTriggered() -{ - foreach(QModelIndex idx, m_FileSelection) { - openFile(idx); - } -} - -void ModInfoDialog::createDirectoryTriggered() -{ - QModelIndex selection = m_FileSelection.at(0); - - QModelIndex index = m_FileSystemModel->isDir(selection) ? selection - : selection.parent(); - index = index.sibling(index.row(), 0); - - QString name = tr("New Folder"); - QString path = m_FileSystemModel->filePath(index).append("/"); - - QModelIndex existingIndex = m_FileSystemModel->index(path + name); - int suffix = 1; - while (existingIndex.isValid()) { - name = tr("New Folder") + QString::number(suffix++); - existingIndex = m_FileSystemModel->index(path + name); - } - - QModelIndex newIndex = m_FileSystemModel->mkdir(index, name); - if (!newIndex.isValid()) { - reportError(tr("Failed to create \"%1\"").arg(name)); - return; - } - - m_FileTree->setCurrentIndex(newIndex); - m_FileTree->edit(newIndex); -} - - -void ModInfoDialog::on_fileTree_customContextMenuRequested(const QPoint &pos) -{ - QItemSelectionModel *selectionModel = m_FileTree->selectionModel(); - m_FileSelection = selectionModel->selectedRows(0); - -// m_FileSelection = m_FileTree->indexAt(pos); - QMenu menu(m_FileTree); - - menu.addAction(m_NewFolderAction); - - bool hasFiles = false; - - foreach(QModelIndex idx, m_FileSelection) { - if (m_FileSystemModel->fileInfo(idx).isFile()) { - hasFiles = true; - break; - } - } - - if (selectionModel->hasSelection()) { - if (hasFiles) { - menu.addAction(m_OpenAction); - } - menu.addAction(m_RenameAction); - menu.addAction(m_DeleteAction); - if (m_FileSystemModel->fileName(m_FileSelection.at(0)).endsWith(ModInfo::s_HiddenExt)) { - menu.addAction(m_UnhideAction); - } else { - menu.addAction(m_HideAction); - } - } else { - m_FileSelection.clear(); - m_FileSelection.append(m_FileSystemModel->index(m_FileSystemModel->rootPath(), 0)); - } - menu.exec(m_FileTree->mapToGlobal(pos)); -} - - -void ModInfoDialog::on_categoriesTree_itemChanged(QTreeWidgetItem *item, int) -{ - QTreeWidgetItem *parent = item->parent(); - while ((parent != NULL) && ((parent->flags() & Qt::ItemIsUserCheckable) != 0) && (parent->checkState(0) == Qt::Unchecked)) { - parent->setCheckState(0, Qt::Checked); - parent = parent->parent(); - } - refreshPrimaryCategoriesBox(); -} - - -void ModInfoDialog::addCheckedCategories(QTreeWidgetItem *tree) -{ - for (int i = 0; i < tree->childCount(); ++i) { - QTreeWidgetItem *child = tree->child(i); - if (child->checkState(0) == Qt::Checked) { - ui->primaryCategoryBox->addItem(child->text(0), child->data(0, Qt::UserRole)); - addCheckedCategories(child); - } - } -} - - -void ModInfoDialog::refreshPrimaryCategoriesBox() -{ - ui->primaryCategoryBox->clear(); - int primaryCategory = m_ModInfo->getPrimaryCategory(); - addCheckedCategories(ui->categoriesTree->invisibleRootItem()); - for (int i = 0; i < ui->primaryCategoryBox->count(); ++i) { - if (ui->primaryCategoryBox->itemData(i).toInt() == primaryCategory) { - ui->primaryCategoryBox->setCurrentIndex(i); - break; - } - } -} - - -void ModInfoDialog::on_primaryCategoryBox_currentIndexChanged(int index) -{ - if (index != -1) { - m_ModInfo->setPrimaryCategory(ui->primaryCategoryBox->itemData(index).toInt()); - } -} - - -void ModInfoDialog::on_overwriteTree_itemDoubleClicked(QTreeWidgetItem *item, int) -{ - this->close(); - emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS); -} - - -bool ModInfoDialog::hideFile(const QString &oldName) -{ - QString newName = oldName + ModInfo::s_HiddenExt; - - if (QFileInfo(newName).exists()) { - if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a hidden version of this file. Replace it?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - if (!QFile(newName).remove()) { - QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); - return false; - } - } else { - return false; - } - } - - if (QFile::rename(oldName, newName)) { - return true; - } else { - reportError(tr("failed to rename %1 to %2").arg(oldName).arg(QDir::toNativeSeparators(newName))); - return false; - } -} - - -bool ModInfoDialog::unhideFile(const QString &oldName) -{ - QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length()); - if (QFileInfo(newName).exists()) { - if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a visible version of this file. Replace it?"), - QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { - if (!QFile(newName).remove()) { - QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); - return false; - } - } else { - return false; - } - } - if (QFile::rename(oldName, newName)) { - return true; - } else { - reportError(tr("failed to rename %1 to %2").arg(QDir::toNativeSeparators(oldName)).arg(QDir::toNativeSeparators(newName))); - return false; - } -} - - -void ModInfoDialog::hideConflictFile() -{ - if (hideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) { - int originID = m_ConflictsContextItem->data(1, Qt::UserRole + 1).toInt(); - emit originModified(originID); - refreshLists(); - } -} - - -void ModInfoDialog::unhideConflictFile() -{ - if (unhideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) { - int originID = m_ConflictsContextItem->data(1, Qt::UserRole + 1).toInt(); - emit originModified(originID); - refreshLists(); - } -} - - -void ModInfoDialog::on_overwriteTree_customContextMenuRequested(const QPoint &pos) -{ - m_ConflictsContextItem = ui->overwriteTree->itemAt(pos.x(), pos.y()); - - QMenu menu; - if (m_ConflictsContextItem != NULL) { - // offer to hide/unhide file, but not for files from archives -// if (!m_ConflictsContextItem->data(0, Qt::UserRole + 1).toBool()) { - if (true) { - if (m_ConflictsContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) { - menu.addAction(tr("Un-Hide"), this, SLOT(unhideConflictFile())); - } else { - menu.addAction(tr("Hide"), this, SLOT(hideConflictFile())); - } - } - } - - menu.exec(ui->overwriteTree->mapToGlobal(pos)); -} - - -void ModInfoDialog::on_overwrittenTree_itemDoubleClicked(QTreeWidgetItem *item, int) -{ - this->close(); - emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS); -} - -void ModInfoDialog::on_refreshButton_clicked() -{ - m_ModInfo->updateNXMInfo(); - - MessageDialog::showMessage(tr("Info requested, please wait"), this); -} - -void ModInfoDialog::on_endorseBtn_clicked() -{ - emit endorseMod(m_ModInfo); -} +#include "modinfodialog.h" +#include "ui_modinfodialog.h" + +#include "report.h" +#include "utility.h" +#include "json.h" +#include "messagedialog.h" +#include "bbcode.h" +#include "questionboxmemory.h" +#include "settings.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +using QtJson::Json; + + +class ModFileListWidget : public QListWidgetItem { + friend bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS); +public: + ModFileListWidget(const QString &text, int sortValue, QListWidget *parent = 0) + : QListWidgetItem(text, parent, QListWidgetItem::UserType + 1), m_SortValue(sortValue) {} +private: + int m_SortValue; +}; + + +bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS) +{ + return LHS.m_SortValue < RHS.m_SortValue; +} + + +ModInfoDialog::ModInfoDialog(ModInfo::Ptr modInfo, const DirectoryEntry *directory, QWidget *parent) + : TutorableDialog("ModInfoDialog", parent), ui(new Ui::ModInfoDialog), m_ModInfo(modInfo), + m_ThumbnailMapper(this), m_RequestStarted(false), + m_DeleteAction(NULL), m_RenameAction(NULL), m_OpenAction(NULL), + m_Directory(directory), m_Origin(NULL) +{ + ui->setupUi(this); + this->setWindowTitle(modInfo->name()); + this->setWindowModality(Qt::WindowModal); + + m_UTF8Codec = QTextCodec::codecForName("utf-8"); + + QListWidget *textFileList = findChild("textFileList"); + QListWidget *iniFileList = findChild("iniFileList"); + QListWidget *iniTweaksList = findChild("iniTweaksList"); + QListWidget *activeESPList = findChild("activeESPList"); + QListWidget *inactiveESPList = findChild("inactiveESPList"); + QTreeWidget *categoriesTree = findChild("categoriesTree"); + QHBoxLayout *thumbnailArea = findChild("thumbnailArea"); + + m_FileTree = findChild("fileTree"); + + m_RootPath = modInfo->absolutePath(); + + QString metaFileName = m_RootPath.mid(0).append("/meta.ini"); + m_Settings = new QSettings(metaFileName, QSettings::IniFormat); + + QLineEdit *modIDEdit = findChild("modIDEdit"); + modIDEdit->setValidator(new QIntValidator(modIDEdit)); + modIDEdit->setText(QString("%1").arg(modInfo->getNexusID())); + + ui->notesEdit->setText(modInfo->notes()); + + m_FileSystemModel = new QFileSystemModel(this); + m_FileSystemModel->setReadOnly(false); + m_FileSystemModel->setRootPath(m_RootPath); + m_FileTree->setModel(m_FileSystemModel); + m_FileTree->setRootIndex(m_FileSystemModel->index(m_RootPath)); + m_FileTree->setColumnWidth(0, 300); + + connect(&m_ThumbnailMapper, SIGNAL(mapped(const QString&)), this, SIGNAL(thumbnailClickedSignal(const QString&))); + connect(this, SIGNAL(thumbnailClickedSignal(const QString&)), this, SLOT(thumbnailClicked(const QString&))); + + m_DeleteAction = new QAction(tr("&Delete"), m_FileTree); + m_RenameAction = new QAction(tr("&Rename"), m_FileTree); + m_HideAction = new QAction(tr("&Hide"), m_FileTree); + m_UnhideAction = new QAction(tr("&Unhide"), m_FileTree); + m_OpenAction = new QAction(tr("&Open"), m_FileTree); + m_NewFolderAction = new QAction(tr("&New Folder"), m_FileTree); + QObject::connect(m_DeleteAction, SIGNAL(triggered()), this, SLOT(deleteTriggered())); + QObject::connect(m_RenameAction, SIGNAL(triggered()), this, SLOT(renameTriggered())); + QObject::connect(m_OpenAction, SIGNAL(triggered()), this, SLOT(openTriggered())); + QObject::connect(m_NewFolderAction, SIGNAL(triggered()), this, SLOT(createDirectoryTriggered())); + QObject::connect(m_HideAction, SIGNAL(triggered()), this, SLOT(hideTriggered())); + connect(m_UnhideAction, SIGNAL(triggered()), this, SLOT(unhideTriggered())); + connect(m_ModInfo.data(), SIGNAL(modDetailsUpdated(bool)), this, SLOT(modDetailsUpdated(bool))); + + ui->descriptionView->page()->setLinkDelegationPolicy(QWebPage::DelegateAllLinks); + QObject::connect(ui->descriptionView, SIGNAL(linkClicked(QUrl)), this, SLOT(linkClicked(QUrl))); + + if (directory->originExists(ToWString(modInfo->name()))) { + m_Origin = &directory->getOriginByName(ToWString(modInfo->name())); + if (m_Origin->isDisabled()) { + m_Origin = NULL; + } + } + + addCategories(CategoryFactory::instance(), modInfo->getCategories(), categoriesTree->invisibleRootItem(), 0); + refreshPrimaryCategoriesBox(); + refreshLists(); + + int numTweaks = m_Settings->beginReadArray("INI Tweaks"); + for (int i = 0; i < numTweaks; ++i) { + m_Settings->setArrayIndex(i); + QList items = iniTweaksList->findItems(m_Settings->value("name").toString(), Qt::MatchFixedString); + if (items.size() != 0) { + items.at(0)->setCheckState(Qt::Checked); + } + } + m_Settings->endArray(); + + QTabWidget *tabWidget = findChild("tabWidget"); + tabWidget->setTabEnabled(TAB_TEXTFILES, textFileList->count() != 0); + tabWidget->setTabEnabled(TAB_INIFILES, (iniFileList->count() != 0) || (iniTweaksList->count() != 0)); + tabWidget->setTabEnabled(TAB_IMAGES, thumbnailArea->count() != 0); + tabWidget->setTabEnabled(TAB_ESPS, (inactiveESPList->count() != 0) || (activeESPList->count() != 0)); + tabWidget->setTabEnabled(TAB_CONFLICTS, m_Origin != NULL); + + if (tabWidget->currentIndex() == TAB_NEXUS) { + activateNexusTab(); + } + + ui->endorseBtn->setEnabled(m_ModInfo->endorsedState() == ModInfo::ENDORSED_FALSE); +} + + +ModInfoDialog::~ModInfoDialog() +{ + m_ModInfo->setNotes(ui->notesEdit->toPlainText()); + saveIniTweaks(); + saveCategories(ui->categoriesTree->invisibleRootItem()); + + delete ui; + delete m_Settings; +} + + +void ModInfoDialog::refreshLists() +{ + int numNonConflicting = 0; + int numOverwrite = 0; + int numOverwritten = 0; + + ui->overwriteTree->clear(); + ui->overwrittenTree->clear(); + + if (m_Origin != NULL) { + std::vector files = m_Origin->getFiles(); + for (auto iter = files.begin(); iter != files.end(); ++iter) { + QString relativeName = QDir::fromNativeSeparators(ToQString((*iter)->getRelativePath())); + QString fileName = relativeName.mid(0).prepend(m_RootPath); + bool archive; + if ((*iter)->getOrigin(archive) == m_Origin->getID()) { + std::vector alternatives = (*iter)->getAlternatives(); + if (!alternatives.empty()) { + std::wostringstream altString; + for (std::vector::iterator altIter = alternatives.begin(); + altIter != alternatives.end(); ++altIter) { + if (altIter != alternatives.begin()) { + altString << ", "; + } + altString << m_Directory->getOriginByID(*altIter).getName(); + } + QStringList fields(relativeName.prepend("...")); + fields.append(ToQString(altString.str())); + QTreeWidgetItem *item = new QTreeWidgetItem(fields); + item->setData(0, Qt::UserRole, fileName); + item->setData(1, Qt::UserRole, ToQString(m_Directory->getOriginByID(alternatives[0]).getName())); + item->setData(1, Qt::UserRole + 1, alternatives[0]); + item->setData(1, Qt::UserRole + 2, archive); + ui->overwriteTree->addTopLevelItem(item); + ++numOverwrite; + } else {// otherwise don't display the file + ++numNonConflicting; + } + } else { + FilesOrigin &realOrigin = m_Directory->getOriginByID((*iter)->getOrigin(archive)); + QStringList fields(relativeName); + fields.append(ToQString(realOrigin.getName())); + QTreeWidgetItem *item = new QTreeWidgetItem(fields); + item->setData(1, Qt::UserRole, ToQString(realOrigin.getName())); + ui->overwrittenTree->addTopLevelItem(item); + ++numOverwritten; + } + } + } + + + QDirIterator dirIterator(m_RootPath, QDir::Files, QDirIterator::Subdirectories); + while (dirIterator.hasNext()) { + QString fileName = dirIterator.next(); + + if (fileName.endsWith(".txt", Qt::CaseInsensitive)) { + ui->textFileList->addItem(fileName.mid(m_RootPath.length() + 1)); + } else if ((fileName.endsWith(".ini", Qt::CaseInsensitive) || fileName.endsWith(".cfg", Qt::CaseInsensitive)) && + !fileName.endsWith("meta.ini")) { + QString namePart = fileName.mid(m_RootPath.length() + 1); + if (namePart.startsWith("INI Tweaks", Qt::CaseInsensitive)) { + QListWidgetItem *newItem = new QListWidgetItem(namePart.mid(11), ui->iniTweaksList); + newItem->setData(Qt::UserRole, namePart); + newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); + newItem->setCheckState(Qt::Unchecked); + ui->iniTweaksList->addItem(newItem); + } else { + ui->iniFileList->addItem(namePart); + } + } else if (fileName.endsWith(".esp", Qt::CaseInsensitive) || + fileName.endsWith(".esm", Qt::CaseInsensitive)) { + QString relativePath = fileName.mid(m_RootPath.length() + 1); + if (relativePath.contains('/')) { + QFileInfo fileInfo(fileName); + QListWidgetItem *newItem = new QListWidgetItem(fileInfo.fileName()); + newItem->setData(Qt::UserRole, relativePath); + ui->inactiveESPList->addItem(newItem); + } else { + ui->activeESPList->addItem(relativePath); + } + } else if ((fileName.endsWith(".png", Qt::CaseInsensitive)) || + (fileName.endsWith(".jpg", Qt::CaseInsensitive))) { + QImage image = QImage(fileName); + if (static_cast(image.width()) / static_cast(image.height()) > 1.34) { + image = image.scaledToWidth(128); + } else { + image = image.scaledToHeight(96); + } + + QPushButton *thumbnailButton = new QPushButton(QPixmap::fromImage(image), ""); + thumbnailButton->setIconSize(QSize(image.width(), image.height())); + connect(thumbnailButton, SIGNAL(clicked()), &m_ThumbnailMapper, SLOT(map())); + m_ThumbnailMapper.setMapping(thumbnailButton, fileName); + ui->thumbnailArea->addWidget(thumbnailButton); + } + } + + ui->overwriteCount->display(numOverwrite); + ui->overwrittenCount->display(numOverwritten); + ui->noConflictCount->display(numNonConflicting); +} + + +void ModInfoDialog::addCategories(const CategoryFactory &factory, const std::set &enabledCategories, QTreeWidgetItem *root, int rootLevel) +{ + for (unsigned int i = 0; i < factory.numCategories(); ++i) { + if (factory.getParentID(i) != rootLevel) { + continue; + } + int categoryID = factory.getCategoryID(i); + QTreeWidgetItem *newItem = new QTreeWidgetItem(QStringList(factory.getCategoryName(i))); + newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable); + newItem->setCheckState(0, enabledCategories.find(categoryID) != enabledCategories.end() ? Qt::Checked : Qt::Unchecked); + newItem->setData(0, Qt::UserRole, categoryID); + if (factory.hasChildren(i)) { + addCategories(factory, enabledCategories, newItem, categoryID); + } + root->addChild(newItem); + } +} + + +void ModInfoDialog::saveCategories(QTreeWidgetItem *currentNode) +{ + for (int i = 0; i < currentNode->childCount(); ++i) { + QTreeWidgetItem *childNode = currentNode->child(i); + m_ModInfo->setCategory(childNode->data(0, Qt::UserRole).toInt(), childNode->checkState(0)); + saveCategories(childNode); + } +} + + +void ModInfoDialog::on_closeButton_clicked() +{ + if (allowNavigateFromTXT() && allowNavigateFromINI()) { + this->close(); + } +} + + + +QString ModInfoDialog::getModVersion() const +{ + return m_Settings->value("version", "").toString(); +} + + +const int ModInfoDialog::getModID() const +{ + return m_Settings->value("modid", 0).toInt(); +} + + +void ModInfoDialog::openTab(int tab) +{ + QTabWidget *tabWidget = findChild("tabWidget"); + if (tabWidget->isTabEnabled(tab)) { + tabWidget->setCurrentIndex(tab); + } +} + + +void ModInfoDialog::thumbnailClicked(const QString &fileName) +{ + QLabel *imageLabel = findChild("imageLabel"); + QImage image(fileName); + if (static_cast(image.width()) / static_cast(image.height()) > 1.34) { + image = image.scaledToWidth(imageLabel->width()); + } else { + image = image.scaledToHeight(imageLabel->height()); + } + imageLabel->setPixmap(QPixmap::fromImage(image)); +} + + +bool ModInfoDialog::allowNavigateFromTXT() +{ + if (ui->saveTXTButton->isEnabled()) { + int res = QMessageBox::question(this, tr("Save changes?"), tr("Save changes to \"%1\"?").arg(ui->textFileView->property("currentFile").toString()), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (res == QMessageBox::Cancel) { + return false; + } else if (res == QMessageBox::Yes) { + saveCurrentTextFile(); + } + } + return true; +} + + +bool ModInfoDialog::allowNavigateFromINI() +{ + if (ui->saveButton->isEnabled()) { + int res = QMessageBox::question(this, tr("Save changes?"), tr("Save changes to \"%1\"?").arg(ui->iniFileView->property("currentFile").toString()), + QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel); + if (res == QMessageBox::Cancel) { + return false; + } else if (res == QMessageBox::Yes) { + saveCurrentIniFile(); + } + } + return true; +} + + +void ModInfoDialog::on_textFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) +{ + QString fullPath = m_RootPath + "/" + current->text(); + + QVariant currentFile = ui->textFileView->property("currentFile"); + if (currentFile.isValid() && (currentFile.toString() == fullPath)) { + // the new file is the same as the currently displayed file. May be the result of a cancelation + return; + } + + if (allowNavigateFromTXT()) { + openTextFile(fullPath); + } else { + ui->textFileList->setCurrentItem(previous, QItemSelectionModel::Current); + } +} + + +void ModInfoDialog::openTextFile(const QString &fileName) +{ + QFile textFile(fileName); + textFile.open(QIODevice::ReadOnly); + QByteArray buffer = textFile.readAll(); + QTextCodec *codec = QTextCodec::codecForUtfText(buffer, m_UTF8Codec); + ui->textFileView->setText(codec->toUnicode(buffer)); + ui->textFileView->setProperty("currentFile", fileName); + ui->textFileView->setProperty("encoding", codec->name()); + ui->saveTXTButton->setEnabled(false); +} + + +void ModInfoDialog::openIniFile(const QString &fileName) +{ + QPushButton* saveButton = findChild("saveButton"); + + QFile iniFile(fileName); + iniFile.open(QIODevice::ReadOnly); + QByteArray buffer = iniFile.readAll(); + QTextCodec *codec = QTextCodec::codecForUtfText(buffer, m_UTF8Codec); + QTextEdit *iniFileView = findChild("iniFileView"); + iniFileView->setText(codec->toUnicode(buffer)); + iniFileView->setProperty("currentFile", fileName); + iniFileView->setProperty("encoding", codec->name()); + iniFile.close(); + + saveButton->setEnabled(false); +} + + +void ModInfoDialog::saveIniTweaks() +{ + QListWidget *iniTweaksList = findChild("iniTweaksList"); + + m_Settings->beginWriteArray("INI Tweaks"); + + int countEnabled = 0; + for (int i = 0; i < iniTweaksList->count(); ++i) { + if (iniTweaksList->item(i)->checkState() == Qt::Checked) { + m_Settings->setArrayIndex(countEnabled++); + m_Settings->setValue("name", iniTweaksList->item(i)->text()); + } + } + + m_Settings->endArray(); +} + + +void ModInfoDialog::on_iniFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) +{ + QString fullPath = m_RootPath + "/" + current->text(); + + QVariant currentFile = ui->iniFileView->property("currentFile"); + if (currentFile.isValid() && (currentFile.toString() == fullPath)) { + // the new file is the same as the currently displayed file. May be the result of a cancelation + return; + } + + if (allowNavigateFromINI()) { + openIniFile(fullPath); + } else { + ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current); + } +} + + +void ModInfoDialog::on_iniTweaksList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous) +{ + QString fullPath = m_RootPath + "/" + current->data(Qt::UserRole).toString(); + + QVariant currentFile = ui->iniFileView->property("currentFile"); + if (currentFile.isValid() && (currentFile.toString() == fullPath)) { + // the new file is the same as the currently displayed file. May be the result of a cancelation + return; + } + + if (allowNavigateFromINI()) { + openIniFile(fullPath); + } else { + ui->iniFileList->setCurrentItem(previous, QItemSelectionModel::Current); + } + +} + + +void ModInfoDialog::on_saveButton_clicked() +{ + saveCurrentIniFile(); +} + + +void ModInfoDialog::on_saveTXTButton_clicked() +{ + saveCurrentTextFile(); +} + + +void ModInfoDialog::saveCurrentTextFile() +{ + QVariant fileNameVar = ui->textFileView->property("currentFile"); + QVariant encodingVar = ui->textFileView->property("encoding"); + if (fileNameVar.isValid() && encodingVar.isValid()) { + QString fileName = fileNameVar.toString(); + QFile txtFile(fileName); + txtFile.open(QIODevice::WriteOnly); + txtFile.resize(0); + QTextCodec *codec = QTextCodec::codecForName(encodingVar.toString().toUtf8()); + QString data = ui->textFileView->toPlainText().replace("\n", "\r\n"); + txtFile.write(codec->fromUnicode(data)); + } else { + reportError("no file selected"); + } + ui->saveTXTButton->setEnabled(false); +} + + +void ModInfoDialog::saveCurrentIniFile() +{ + QVariant fileNameVar = ui->iniFileView->property("currentFile"); + QVariant encodingVar = ui->iniFileView->property("encoding"); + if (fileNameVar.isValid()) { + QString fileName = fileNameVar.toString(); + QFile txtFile(fileName); + txtFile.open(QIODevice::WriteOnly); + txtFile.resize(0); + QTextCodec *codec = QTextCodec::codecForName(encodingVar.toString().toUtf8()); + QString data = ui->iniFileView->toPlainText().replace("\n", "\r\n"); + txtFile.write(codec->fromUnicode(data)); + } else { + reportError("no file selected"); + } + ui->saveButton->setEnabled(false); +} + + +void ModInfoDialog::on_iniFileView_textChanged() +{ + QPushButton* saveButton = findChild("saveButton"); + saveButton->setEnabled(true); +} + + +void ModInfoDialog::on_textFileView_textChanged() +{ + ui->saveTXTButton->setEnabled(true); +} + + +void ModInfoDialog::on_activateESP_clicked() +{ + QListWidget *activeESPList = findChild("activeESPList"); + QListWidget *inactiveESPList = findChild("inactiveESPList"); + + int selectedRow = inactiveESPList->currentRow(); + if (selectedRow < 0) { + return; + } + + QListWidgetItem *selectedItem = inactiveESPList->takeItem(selectedRow); + + QDir root(m_RootPath); + bool renamed = false; + + while (root.exists(selectedItem->text())) { + bool okClicked = false; + QString newName = QInputDialog::getText(this, tr("File Exists"), tr("A file with that name exists, please enter a new one"), QLineEdit::Normal, selectedItem->text(), &okClicked); + if (!okClicked) { + inactiveESPList->insertItem(selectedRow, selectedItem); + return; + } else if (newName.size() > 0) { + selectedItem->setText(newName); + renamed = true; + } + } + + if (root.rename(selectedItem->data(Qt::UserRole).toString(), selectedItem->text())) { + activeESPList->addItem(selectedItem); + if (renamed) { + selectedItem->setData(Qt::UserRole, QVariant()); + } + } else { + inactiveESPList->insertItem(selectedRow, selectedItem); + reportError(tr("failed to move file")); + } +} + + +void ModInfoDialog::on_deactivateESP_clicked() +{ + QListWidget *activeESPList = findChild("activeESPList"); + QListWidget *inactiveESPList = findChild("inactiveESPList"); + + int selectedRow = activeESPList->currentRow(); + if (selectedRow < 0) { + return; + } + + QDir root(m_RootPath); + + QListWidgetItem *selectedItem = activeESPList->takeItem(selectedRow); + + // if we moved the file from optional to active in this session, we move the file back to + // where it came from. Otherwise, it is moved to the new folder "optional" + if (selectedItem->data(Qt::UserRole).isNull()) { + selectedItem->setData(Qt::UserRole, QString("optional/") + selectedItem->text()); + if (!root.exists("optional")) { + if (!root.mkdir("optional")) { + reportError(tr("failed to create directory \"optional\"")); + activeESPList->insertItem(selectedRow, selectedItem); + return; + } + } + } + + if (root.rename(selectedItem->text(), selectedItem->data(Qt::UserRole).toString())) { + inactiveESPList->addItem(selectedItem); + } else { + activeESPList->insertItem(selectedRow, selectedItem); + } +} + +void ModInfoDialog::on_visitNexusLabel_linkActivated(const QString &link) +{ + this->close(); + emit nexusLinkActivated(link); +} + +void ModInfoDialog::linkClicked(const QUrl &url) +{ + if (url.toString().startsWith(ToQString(GameInfo::instance().getNexusPage()))) { + this->close(); + emit nexusLinkActivated(url.toString()); + } else { + ::ShellExecuteW(NULL, L"open", ToWString(url.toString()).c_str(), NULL, NULL, SW_SHOWNORMAL); + } +} + + +void ModInfoDialog::refreshNexusData(int modID) +{ + if ((!m_RequestStarted) && (modID > 0)) { + m_RequestStarted = true; + + m_ModInfo->updateNXMInfo(); + + MessageDialog::showMessage(tr("Info requested, please wait"), this); + } +} + + +/*void ModInfoDialog::nxmDescriptionAvailable(int, QVariant, QVariant resultData, int requestID) +{ + std::set::iterator idIter = m_RequestIDs.find(requestID); + if (idIter == m_RequestIDs.end()) { + return; + } else { + m_RequestIDs.erase(idIter); + } + + QVariantMap result = resultData.toMap(); + + if (!result["description"].isNull()) { + QString descriptionAsHTML = + QString("" + "" + "%1" + "").arg(BBCode::convertToHTML(result["description"].toString())); + +// QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString()); + ui->descriptionView->setHtml(descriptionAsHTML); + } else { + ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)"))); + } + + QLineEdit *versionEdit = findChild("versionEdit"); + QString version = result["version"].toString(); + + if (!version.isEmpty()) { + m_ModInfo->setNewestVersion(version); + + VersionInfo currentVersion(versionEdit->text()); + VersionInfo newestVersion(version); + + QPalette versionColor; + if (currentVersion < newestVersion) { + versionColor.setColor(QPalette::Text, Qt::red); + versionEdit->setToolTip(tr("Current Version: %1").arg(version)); + } else { + versionColor.setColor(QPalette::Text, Qt::green); + versionEdit->setToolTip(tr("No update available")); + } + versionEdit->setPalette(versionColor); + } +}*/ + + +QString ModInfoDialog::getFileCategory(int categoryID) +{ + switch (categoryID) { + 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"); + } +} + + +void ModInfoDialog::updateVersionColor() +{ + QPalette versionColor; + if (m_ModInfo->getVersion() < m_ModInfo->getNewestVersion()) { + versionColor.setColor(QPalette::Text, Qt::red); + ui->versionEdit->setToolTip(tr("Current Version: %1").arg(m_ModInfo->getNewestVersion().canonicalString())); + } else { + versionColor.setColor(QPalette::Text, Qt::green); + ui->versionEdit->setToolTip(tr("No update available")); + } + ui->versionEdit->setPalette(versionColor); +} + + +void ModInfoDialog::modDetailsUpdated(bool success) +{ + if (success) { + QString nexusDescription = m_ModInfo->getNexusDescription(); + if (!nexusDescription.isEmpty()) { + /* QString input = + "[size=20]sizetest[/size]\r\n" + "[COLOR=yellow]colortest[/COLOR]\r\n" + "[center]centertest[/center]\r\n" + "[quote]quotetest 1[/quote]\r\n" + "[quote=bla]quotetest 2[/quote]\r\n" + "[url]www.skyrimnexus.com[/url]\r\n" + "[url=www.skyrimnexus.com]urltest 2[/url]\r\n" + "[ol]\r\n" + "[li]item 2[/li]" + "[*]item 1\r\n" + "[/ol]\r\n" + "[img]http://www.bbcode.org/images/bbcode_logo.png[/img]\r\n" + "[table][tr][th]headertest1[/th]" + "[th]headertest2[/th][/tr]" + "[tr][td]rowtest11[/td][td]rowtest12[/td][/tr]" + "[tr][td]rowtest21[/td][td]rowtest22[/td][/tr][/table]" + "[email=\"sherb@gmx.net\"]mail me[/email]"; + ui->descriptionView->setHtml(BBCode::convertToHTML(input));*/ + + QString descriptionAsHTML = + QString("" + "" + "%1" + "").arg(BBCode::convertToHTML(nexusDescription)); + + // QString descriptionAsHTML = BBCode::convertToHTML(result["description"].toString()); + ui->descriptionView->setHtml(descriptionAsHTML); + } else { + // ui->descriptionView->setHtml(result["summary"].toString().append(QString("\r\n") + tr("(description incomplete, please visit nexus)"))); + ui->descriptionView->setHtml(tr("(description incomplete, please visit nexus)")); + } + + QString version = m_ModInfo->getNewestVersion().canonicalString(); + + if (!version.isEmpty()) { + m_ModInfo->setNewestVersion(version); + + updateVersionColor(); + } + + QTableWidget *filesWidget = findChild("filesWidget"); + filesWidget->clearContents(); + filesWidget->setRowCount(0); + filesWidget->setColumnCount(5); +#if QT_VERSION >= 0x050000 + filesWidget->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); +#else + filesWidget->horizontalHeader()->setResizeMode(1, QHeaderView::Stretch); +#endif + filesWidget->horizontalHeader()->setVisible(true); + filesWidget->setColumnHidden(4, true); + + + // prevent sorting while we change the table + filesWidget->setSortingEnabled(false); + int row = 0; + QList::const_iterator current, end; + m_ModInfo->getNexusFiles(current, end); + for (; current != end; ++current) { + filesWidget->insertRow(row); + int type = current->category; + filesWidget->setItem(row, 4, new QTableWidgetItem(QString("%1").arg(type))); + filesWidget->setItem(row, 0, new QTableWidgetItem(getFileCategory(type))); + QTableWidgetItem *nameItem = new QTableWidgetItem(current->name); + nameItem->setToolTip(current->description); + nameItem->setData(Qt::UserRole, current->id); + filesWidget->setItem(row, 1, nameItem); + filesWidget->setItem(row, 2, new QTableWidgetItem(current->version)); + QTableWidgetItem *sizeItem = new QTableWidgetItem(); + sizeItem->setData(Qt::DisplayRole, current->size); + filesWidget->setItem(row, 3, sizeItem); + ++row; + } + filesWidget->setSortingEnabled(true); + filesWidget->sortItems(4); + } +} + + +void ModInfoDialog::activateNexusTab() +{ + QLineEdit *modIDEdit = findChild("modIDEdit"); + int modID = modIDEdit->text().toInt(); + if (modID != 0) { + QString nexusLink = QString("%1/downloads/file.php?id=%2").arg(ToQString(GameInfo::instance().getNexusPage())).arg(modID); + QLabel *visitNexusLabel = findChild("visitNexusLabel"); + visitNexusLabel->setText(tr("Visit on Nexus").arg(nexusLink)); + visitNexusLabel->setToolTip(nexusLink); + + + if (m_ModInfo->getNexusDescription().isEmpty() || + QDateTime::currentDateTime() > m_ModInfo->getLastNexusQuery().addDays(1)) { + refreshNexusData(modID); + } else { + this->modDetailsUpdated(true); + } + } + QLineEdit *versionEdit = findChild("versionEdit"); + QString currentVersion = m_Settings->value("version", "0.0").toString(); + versionEdit->setText(currentVersion); +} + + +void ModInfoDialog::on_tabWidget_currentChanged(int index) +{ + if (index == TAB_NEXUS) { + activateNexusTab(); + } +} + + +void ModInfoDialog::on_modIDEdit_editingFinished() +{ + int oldID = m_Settings->value("modid", 0).toInt(); + int modID = ui->modIDEdit->text().toInt(); + if (oldID != modID){ + m_ModInfo->setNexusID(modID); + + ui->descriptionView->setHtml(""); + if (modID != 0) { + m_RequestStarted = false; + refreshNexusData(modID); + } + } +} + +void ModInfoDialog::on_versionEdit_editingFinished() +{ + VersionInfo version(ui->versionEdit->text()); + m_ModInfo->setVersion(version); + updateVersionColor(); +} + +void ModInfoDialog::on_filesWidget_doubleClicked(const QModelIndex &index) +{ + QTableWidget *filesWidget = findChild("filesWidget"); + + QTableWidgetItem *selectedItem = filesWidget->item(index.row(), 1); + if (selectedItem == NULL) { + qDebug("no item in row %d", index.row()); + return; + } + + QString nexusLink = QString("nxm://%1/mods/%2/files/%3") + .arg(ToQString(GameInfo::instance().getGameName())) + .arg(m_Settings->value("modid").toInt()) + .arg(selectedItem->data(Qt::UserRole).toInt()); + + try { + if (QuestionBoxMemory::query(this, Settings::instance().directInterface(), "confirmNexusDownload", + tr("Confirm"), tr("Download \"%1\"?").arg(selectedItem->text()), + QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::Yes) { + emit downloadRequest(nexusLink); + MessageDialog::showMessage(tr("Download started"), this); + } + } catch (const std::exception &e) { + reportError(tr("Exception: %1").arg(e.what())); + } +} + + +bool ModInfoDialog::recursiveDelete(const QModelIndex &index) +{ + for (int childRow = 0; childRow < m_FileSystemModel->rowCount(index); ++childRow) { + QModelIndex childIndex = m_FileSystemModel->index(childRow, 0, index); + if (m_FileSystemModel->isDir(childIndex)) { + if (!recursiveDelete(childIndex)) { + qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData()); + return false; + } + } else { + if (!m_FileSystemModel->remove(childIndex)) { + qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData()); + return false; + } + } + } + if (!m_FileSystemModel->remove(index)) { + qCritical("failed to delete %s", m_FileSystemModel->fileName(index).toUtf8().constData()); + return false; + } + return true; +} + + +void ModInfoDialog::deleteFile(const QModelIndex &index) +{ + + bool res = m_FileSystemModel->isDir(index) ? recursiveDelete(index) + : m_FileSystemModel->remove(index); + if (!res) { + QString fileName = m_FileSystemModel->fileName(index); + reportError(tr("Failed to delete %1").arg(fileName)); + } +} + + +void ModInfoDialog::deleteTriggered() +{ + if (m_FileSelection.count() == 0) { + return; + } else if (m_FileSelection.count() == 1) { + QString fileName = m_FileSystemModel->fileName(m_FileSelection.at(0)); + if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete \"%1\"?").arg(fileName), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } else { + if (QMessageBox::question(this, tr("Confirm"), tr("Are sure you want to delete the selected files?"), + QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) { + return; + } + } + + foreach(QModelIndex index, m_FileSelection) { + deleteFile(index); + } +} + + +void ModInfoDialog::renameTriggered() +{ + QModelIndex selection = m_FileSelection.at(0); + QModelIndex index = selection.sibling(selection.row(), 0); + if (!index.isValid() || m_FileSystemModel->isReadOnly()) { + return; + } + + m_FileTree->edit(index); +} + + +void ModInfoDialog::hideTriggered() +{ + for (QModelIndexList::const_iterator iter = m_FileSelection.constBegin(); + iter != m_FileSelection.constEnd(); ++iter) { + QString path = m_FileSystemModel->filePath(*iter); + if (!path.endsWith(ModInfo::s_HiddenExt)) { + hideFile(path); + } + } +} + + +void ModInfoDialog::unhideTriggered() +{ + for (QModelIndexList::const_iterator iter = m_FileSelection.constBegin(); + iter != m_FileSelection.constEnd(); ++iter) { + QString path = m_FileSystemModel->filePath(*iter); + if (path.endsWith(ModInfo::s_HiddenExt)) { + unhideFile(path); + } + } +} + + +void ModInfoDialog::openFile(const QModelIndex &index) +{ + QString fileName = m_FileSystemModel->filePath(index); + + HINSTANCE res = ::ShellExecuteW(NULL, L"open", ToWString(fileName).c_str(), NULL, NULL, SW_SHOW); + if ((int)res <= 32) { + qCritical("failed to invoke %s: %d", fileName.toUtf8().constData(), res); + } +} + + +void ModInfoDialog::openTriggered() +{ + foreach(QModelIndex idx, m_FileSelection) { + openFile(idx); + } +} + +void ModInfoDialog::createDirectoryTriggered() +{ + QModelIndex selection = m_FileSelection.at(0); + + QModelIndex index = m_FileSystemModel->isDir(selection) ? selection + : selection.parent(); + index = index.sibling(index.row(), 0); + + QString name = tr("New Folder"); + QString path = m_FileSystemModel->filePath(index).append("/"); + + QModelIndex existingIndex = m_FileSystemModel->index(path + name); + int suffix = 1; + while (existingIndex.isValid()) { + name = tr("New Folder") + QString::number(suffix++); + existingIndex = m_FileSystemModel->index(path + name); + } + + QModelIndex newIndex = m_FileSystemModel->mkdir(index, name); + if (!newIndex.isValid()) { + reportError(tr("Failed to create \"%1\"").arg(name)); + return; + } + + m_FileTree->setCurrentIndex(newIndex); + m_FileTree->edit(newIndex); +} + + +void ModInfoDialog::on_fileTree_customContextMenuRequested(const QPoint &pos) +{ + QItemSelectionModel *selectionModel = m_FileTree->selectionModel(); + m_FileSelection = selectionModel->selectedRows(0); + +// m_FileSelection = m_FileTree->indexAt(pos); + QMenu menu(m_FileTree); + + menu.addAction(m_NewFolderAction); + + bool hasFiles = false; + + foreach(QModelIndex idx, m_FileSelection) { + if (m_FileSystemModel->fileInfo(idx).isFile()) { + hasFiles = true; + break; + } + } + + if (selectionModel->hasSelection()) { + if (hasFiles) { + menu.addAction(m_OpenAction); + } + menu.addAction(m_RenameAction); + menu.addAction(m_DeleteAction); + if (m_FileSystemModel->fileName(m_FileSelection.at(0)).endsWith(ModInfo::s_HiddenExt)) { + menu.addAction(m_UnhideAction); + } else { + menu.addAction(m_HideAction); + } + } else { + m_FileSelection.clear(); + m_FileSelection.append(m_FileSystemModel->index(m_FileSystemModel->rootPath(), 0)); + } + menu.exec(m_FileTree->mapToGlobal(pos)); +} + + +void ModInfoDialog::on_categoriesTree_itemChanged(QTreeWidgetItem *item, int) +{ + QTreeWidgetItem *parent = item->parent(); + while ((parent != NULL) && ((parent->flags() & Qt::ItemIsUserCheckable) != 0) && (parent->checkState(0) == Qt::Unchecked)) { + parent->setCheckState(0, Qt::Checked); + parent = parent->parent(); + } + refreshPrimaryCategoriesBox(); +} + + +void ModInfoDialog::addCheckedCategories(QTreeWidgetItem *tree) +{ + for (int i = 0; i < tree->childCount(); ++i) { + QTreeWidgetItem *child = tree->child(i); + if (child->checkState(0) == Qt::Checked) { + ui->primaryCategoryBox->addItem(child->text(0), child->data(0, Qt::UserRole)); + addCheckedCategories(child); + } + } +} + + +void ModInfoDialog::refreshPrimaryCategoriesBox() +{ + ui->primaryCategoryBox->clear(); + int primaryCategory = m_ModInfo->getPrimaryCategory(); + addCheckedCategories(ui->categoriesTree->invisibleRootItem()); + for (int i = 0; i < ui->primaryCategoryBox->count(); ++i) { + if (ui->primaryCategoryBox->itemData(i).toInt() == primaryCategory) { + ui->primaryCategoryBox->setCurrentIndex(i); + break; + } + } +} + + +void ModInfoDialog::on_primaryCategoryBox_currentIndexChanged(int index) +{ + if (index != -1) { + m_ModInfo->setPrimaryCategory(ui->primaryCategoryBox->itemData(index).toInt()); + } +} + + +void ModInfoDialog::on_overwriteTree_itemDoubleClicked(QTreeWidgetItem *item, int) +{ + this->close(); + emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS); +} + + +bool ModInfoDialog::hideFile(const QString &oldName) +{ + QString newName = oldName + ModInfo::s_HiddenExt; + + if (QFileInfo(newName).exists()) { + if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a hidden version of this file. Replace it?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + if (!QFile(newName).remove()) { + QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); + return false; + } + } else { + return false; + } + } + + if (QFile::rename(oldName, newName)) { + return true; + } else { + reportError(tr("failed to rename %1 to %2").arg(oldName).arg(QDir::toNativeSeparators(newName))); + return false; + } +} + + +bool ModInfoDialog::unhideFile(const QString &oldName) +{ + QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length()); + if (QFileInfo(newName).exists()) { + if (QMessageBox::question(this, tr("Replace file?"), tr("There already is a visible version of this file. Replace it?"), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + if (!QFile(newName).remove()) { + QMessageBox::critical(this, tr("File operation failed"), tr("Failed to remove \"%1\". Maybe you lack the required file permissions?").arg(newName)); + return false; + } + } else { + return false; + } + } + if (QFile::rename(oldName, newName)) { + return true; + } else { + reportError(tr("failed to rename %1 to %2").arg(QDir::toNativeSeparators(oldName)).arg(QDir::toNativeSeparators(newName))); + return false; + } +} + + +void ModInfoDialog::hideConflictFile() +{ + if (hideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) { + emit originModified(m_Origin->getID()); + refreshLists(); + } +} + + +void ModInfoDialog::unhideConflictFile() +{ + if (unhideFile(m_ConflictsContextItem->data(0, Qt::UserRole).toString())) { + emit originModified(m_Origin->getID()); + refreshLists(); + } +} + + +void ModInfoDialog::on_overwriteTree_customContextMenuRequested(const QPoint &pos) +{ + m_ConflictsContextItem = ui->overwriteTree->itemAt(pos.x(), pos.y()); + + QMenu menu; + if (m_ConflictsContextItem != NULL) { + // offer to hide/unhide file, but not for files from archives +// if (!m_ConflictsContextItem->data(0, Qt::UserRole + 1).toBool()) { + if (!m_ConflictsContextItem->data(0, Qt::UserRole + 2).toBool()) { + if (m_ConflictsContextItem->text(0).endsWith(ModInfo::s_HiddenExt)) { + menu.addAction(tr("Un-Hide"), this, SLOT(unhideConflictFile())); + } else { + menu.addAction(tr("Hide"), this, SLOT(hideConflictFile())); + } + } + menu.exec(ui->overwriteTree->mapToGlobal(pos)); + } +} + + +void ModInfoDialog::on_overwrittenTree_itemDoubleClicked(QTreeWidgetItem *item, int) +{ + this->close(); + emit modOpen(item->data(1, Qt::UserRole).toString(), TAB_CONFLICTS); +} + +void ModInfoDialog::on_refreshButton_clicked() +{ + m_ModInfo->updateNXMInfo(); + + MessageDialog::showMessage(tr("Info requested, please wait"), this); +} + +void ModInfoDialog::on_endorseBtn_clicked() +{ + emit endorseMod(m_ModInfo); +} diff --git a/src/modinfodialog.h b/src/modinfodialog.h index 8c91b74d..07aa8e84 100644 --- a/src/modinfodialog.h +++ b/src/modinfodialog.h @@ -17,201 +17,202 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#ifndef MODINFODIALOG_H -#define MODINFODIALOG_H - - -#include "modinfo.h" -#include "categories.h" -#include "tutorabledialog.h" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -namespace Ui { - class ModInfoDialog; -} - -class QFileSystemModel; -class QTreeView; - - -/** - * this is a larger dialog used to visualise information abount the mod. - * @todo this would probably a good place for a plugin-system - **/ -class ModInfoDialog : public TutorableDialog -{ - Q_OBJECT - -public: - - enum ETabs { - TAB_TEXTFILES, - TAB_INIFILES, - TAB_IMAGES, - TAB_ESPS, - TAB_CONFLICTS, - TAB_CATEGORIES, - TAB_NEXUS, - TAB_FILETREE - }; - -public: - - /** - * @brief constructor - * - * @param modInfo info structure about the mod to display - * @param parent parend widget - **/ - explicit ModInfoDialog(ModInfo::Ptr modInfo, const DirectoryEntry *directory, QWidget *parent = 0); - ~ModInfoDialog(); - - /** - * @brief retrieve the (user-modified) version of the mod - * - * @return the (user-modified) version of the mod - **/ - QString getModVersion() const; - - /** - * @brief retrieve the (user-modified) mod id - * - * @return the (user-modified) id of the mod - **/ - const int getModID() const; - - /** - * @brief open the specified tab in the dialog if it's enabled - * - * @param tab the tab to activate - **/ - void openTab(int tab); - -signals: - - void thumbnailClickedSignal(const QString &filename); - void nexusLinkActivated(const QString &link); - void downloadRequest(const QString &link); - void modOpen(const QString &modName, int tab); - void originModified(int originID); - void endorseMod(ModInfo::Ptr nexusID); - -public slots: - - void modDetailsUpdated(bool success); - -private: - - void refreshLists(); - - void addCategories(const CategoryFactory &factory, const std::set &enabledCategories, QTreeWidgetItem *root, int rootLevel); - - void updateVersionColor(); - - void refreshNexusData(int modID); - void activateNexusTab(); - QString getFileCategory(int categoryID); - bool recursiveDelete(const QModelIndex &index); - void deleteFile(const QModelIndex &index); - void openFile(const QModelIndex &index); - void saveIniTweaks(); - void saveCategories(QTreeWidgetItem *currentNode); - void saveCurrentTextFile(); - void saveCurrentIniFile(); - void openTextFile(const QString &fileName); - void openIniFile(const QString &fileName); - bool allowNavigateFromTXT(); - bool allowNavigateFromINI(); - bool hideFile(const QString &oldName); - bool unhideFile(const QString &oldName); - void addCheckedCategories(QTreeWidgetItem *tree); - void refreshPrimaryCategoriesBox(); - -private slots: - - void hideConflictFile(); - void unhideConflictFile(); - - void thumbnailClicked(const QString &fileName); - void linkClicked(const QUrl &url); - - void deleteTriggered(); - void renameTriggered(); - void openTriggered(); - void createDirectoryTriggered(); - void hideTriggered(); - void unhideTriggered(); - - void on_closeButton_clicked(); - void on_saveButton_clicked(); - void on_activateESP_clicked(); - void on_deactivateESP_clicked(); - void on_saveTXTButton_clicked(); - void on_filesWidget_doubleClicked(const QModelIndex &index); - void on_visitNexusLabel_linkActivated(const QString &link); - void on_modIDEdit_editingFinished(); - void on_versionEdit_editingFinished(); - void on_iniFileView_textChanged(); - void on_textFileView_textChanged(); - void on_tabWidget_currentChanged(int index); - void on_primaryCategoryBox_currentIndexChanged(int index); - void on_categoriesTree_itemChanged(QTreeWidgetItem *item, int column); - void on_textFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); - void on_iniFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); - void on_iniTweaksList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); - void on_overwriteTree_itemDoubleClicked(QTreeWidgetItem *item, int column); - void on_overwrittenTree_itemDoubleClicked(QTreeWidgetItem *item, int column); - void on_overwriteTree_customContextMenuRequested(const QPoint &pos); - void on_fileTree_customContextMenuRequested(const QPoint &pos); - - void on_refreshButton_clicked(); - - void on_endorseBtn_clicked(); - -private: - - Ui::ModInfoDialog *ui; - - ModInfo::Ptr m_ModInfo; - - QSignalMapper m_ThumbnailMapper; - QString m_RootPath; - - QFileSystemModel *m_FileSystemModel; - QTreeView *m_FileTree; - QModelIndexList m_FileSelection; - - QSettings *m_Settings; - - std::set m_RequestIDs; - bool m_RequestStarted; - - QAction *m_DeleteAction; - QAction *m_RenameAction; - QAction *m_OpenAction; - QAction *m_NewFolderAction; - QAction *m_HideAction; - QAction *m_UnhideAction; - - QTreeWidgetItem *m_ConflictsContextItem; - - const DirectoryEntry *m_Directory; - FilesOrigin *m_Origin; - QTextCodec *m_UTF8Codec; - -}; - -#endif // MODINFODIALOG_H +#ifndef MODINFODIALOG_H +#define MODINFODIALOG_H + + +#include "modinfo.h" +#include "categories.h" +#include "tutorabledialog.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace Ui { + class ModInfoDialog; +} + +class QFileSystemModel; +class QTreeView; + + +/** + * this is a larger dialog used to visualise information abount the mod. + * @todo this would probably a good place for a plugin-system + **/ +class ModInfoDialog : public TutorableDialog +{ + Q_OBJECT + +public: + + enum ETabs { + TAB_TEXTFILES, + TAB_INIFILES, + TAB_IMAGES, + TAB_ESPS, + TAB_CONFLICTS, + TAB_CATEGORIES, + TAB_NEXUS, + TAB_FILETREE + }; + +public: + + /** + * @brief constructor + * + * @param modInfo info structure about the mod to display + * @param parent parend widget + **/ + explicit ModInfoDialog(ModInfo::Ptr modInfo, const DirectoryEntry *directory, QWidget *parent = 0); + ~ModInfoDialog(); + + /** + * @brief retrieve the (user-modified) version of the mod + * + * @return the (user-modified) version of the mod + **/ + QString getModVersion() const; + + /** + * @brief retrieve the (user-modified) mod id + * + * @return the (user-modified) id of the mod + **/ + const int getModID() const; + + /** + * @brief open the specified tab in the dialog if it's enabled + * + * @param tab the tab to activate + **/ + void openTab(int tab); + +signals: + + void thumbnailClickedSignal(const QString &filename); + void nexusLinkActivated(const QString &link); + void downloadRequest(const QString &link); + void modOpen(const QString &modName, int tab); + void originModified(int originID); + void endorseMod(ModInfo::Ptr nexusID); + +public slots: + + void modDetailsUpdated(bool success); + +private: + + void refreshLists(); + + void addCategories(const CategoryFactory &factory, const std::set &enabledCategories, QTreeWidgetItem *root, int rootLevel); + + void updateVersionColor(); + + void refreshNexusData(int modID); + void activateNexusTab(); + QString getFileCategory(int categoryID); + bool recursiveDelete(const QModelIndex &index); + void deleteFile(const QModelIndex &index); + void openFile(const QModelIndex &index); + void saveIniTweaks(); + void saveCategories(QTreeWidgetItem *currentNode); + void saveCurrentTextFile(); + void saveCurrentIniFile(); + void openTextFile(const QString &fileName); + void openIniFile(const QString &fileName); + bool allowNavigateFromTXT(); + bool allowNavigateFromINI(); + bool hideFile(const QString &oldName); + bool unhideFile(const QString &oldName); + void addCheckedCategories(QTreeWidgetItem *tree); + void refreshPrimaryCategoriesBox(); + +private slots: + + void hideConflictFile(); + void unhideConflictFile(); + + void thumbnailClicked(const QString &fileName); + void linkClicked(const QUrl &url); + + void deleteTriggered(); + void renameTriggered(); + void openTriggered(); + void createDirectoryTriggered(); + void hideTriggered(); + void unhideTriggered(); + + void on_closeButton_clicked(); + void on_saveButton_clicked(); + void on_activateESP_clicked(); + void on_deactivateESP_clicked(); + void on_saveTXTButton_clicked(); + void on_filesWidget_doubleClicked(const QModelIndex &index); + void on_visitNexusLabel_linkActivated(const QString &link); + void on_modIDEdit_editingFinished(); + void on_versionEdit_editingFinished(); + void on_iniFileView_textChanged(); + void on_textFileView_textChanged(); + void on_tabWidget_currentChanged(int index); + void on_primaryCategoryBox_currentIndexChanged(int index); + void on_categoriesTree_itemChanged(QTreeWidgetItem *item, int column); + void on_textFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); + void on_iniFileList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); + void on_iniTweaksList_currentItemChanged(QListWidgetItem *current, QListWidgetItem *previous); + void on_overwriteTree_itemDoubleClicked(QTreeWidgetItem *item, int column); + void on_overwrittenTree_itemDoubleClicked(QTreeWidgetItem *item, int column); + void on_overwriteTree_customContextMenuRequested(const QPoint &pos); + void on_fileTree_customContextMenuRequested(const QPoint &pos); + + void on_refreshButton_clicked(); + + void on_endorseBtn_clicked(); + +private: + + Ui::ModInfoDialog *ui; + + ModInfo::Ptr m_ModInfo; + int m_OriginID; + + QSignalMapper m_ThumbnailMapper; + QString m_RootPath; + + QFileSystemModel *m_FileSystemModel; + QTreeView *m_FileTree; + QModelIndexList m_FileSelection; + + QSettings *m_Settings; + + std::set m_RequestIDs; + bool m_RequestStarted; + + QAction *m_DeleteAction; + QAction *m_RenameAction; + QAction *m_OpenAction; + QAction *m_NewFolderAction; + QAction *m_HideAction; + QAction *m_UnhideAction; + + QTreeWidgetItem *m_ConflictsContextItem; + + const DirectoryEntry *m_Directory; + FilesOrigin *m_Origin; + QTextCodec *m_UTF8Codec; + +}; + +#endif // MODINFODIALOG_H diff --git a/src/modlist.cpp b/src/modlist.cpp index b1d0caf8..aac913c5 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -17,655 +17,652 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include "modlist.h" - -#include "report.h" -#include "utility.h" -#include "messagedialog.h" -#include "installationtester.h" -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -ModList::ModList(QObject *parent) - : QAbstractItemModel(parent), m_Profile(NULL), m_Modified(false), - m_FontMetrics(QFont()) -{ -// setSupportedDragActions(Qt::MoveAction); -} - - -void ModList::setProfile(Profile *profile) -{ - m_Profile = profile; -} - - -int ModList::rowCount(const QModelIndex &parent) const -{ - if (!parent.isValid()) { - return ModInfo::getNumMods(); - } else { - return 0; - } -} - - -int ModList::columnCount(const QModelIndex &) const -{ - return COL_LASTCOLUMN + 1; -} - - -QVariant ModList::getOverwriteData(int column, int role) const -{ - switch (role) { - case Qt::DisplayRole: { - if (column == 0) { - return tr("Overwrite"); - } else { - return QVariant(); - } - } break; - case Qt::CheckStateRole: { - if (column == 0) { - return Qt::Checked; - } else { - return QVariant(); - } - } break; - case Qt::TextAlignmentRole: return QVariant(Qt::AlignCenter | Qt::AlignVCenter); - case Qt::UserRole: return -1; - case Qt::ForegroundRole: return QBrush(Qt::red); - case Qt::ToolTipRole: return tr("This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit)"); - default: return QVariant(); - } -} - - -QString ModList::getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const -{ - switch (flag) { - case ModInfo::FLAG_BACKUP: return tr("Backup"); - case ModInfo::FLAG_INVALID: return tr("No valid game data"); - case ModInfo::FLAG_NOTENDORSED: return tr("Not endorsed yet"); - case ModInfo::FLAG_NOTES: return QString("%1").arg(modInfo->notes().replace("\n", "
    ")); - case ModInfo::FLAG_CONFLICT_OVERWRITE: return tr("Overwrites files"); - case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return tr("Overwritten files"); - case ModInfo::FLAG_CONFLICT_MIXED: return tr("Overwrites & Overwritten"); - case ModInfo::FLAG_CONFLICT_REDUNDANT: return tr("Redundant"); - default: return ""; - } -} - - -QVariant ModList::data(const QModelIndex &modelIndex, int role) const -{ - if (m_Profile == NULL) return QVariant(); - unsigned int modIndex = modelIndex.row(); - int column = modelIndex.column(); - - ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); - if ((role == Qt::DisplayRole) || - (role == Qt::EditRole)) { - if (column == COL_FLAGS) { - return QVariant(); - } else if (column == COL_NAME) { - return modInfo->name(); - } else if (column == COL_VERSION) { - QString version = modInfo->getVersion().canonicalString(); - if (version.isEmpty() && modInfo->canBeUpdated()) { - version = "?"; - } - return version; - } else if (column == COL_PRIORITY) { - int priority = modInfo->getFixedPriority(); - if (priority != INT_MIN) { - return QVariant(); // hide priority for mods where it's fixed - } else { - if ((m_Profile->getModPriority(modIndex) == 0) && (role == Qt::DisplayRole)) { - return tr("min"); - } else if ((m_Profile->getModPriority(modIndex) == m_Profile->numRegularMods() - 1) && - (role == Qt::DisplayRole)) { - return tr("max"); - } else { - //return QString::number(m_Profile->getModPriority(modIndex)); - return m_Profile->getModPriority(modIndex); - } - } - } else if (column == COL_MODID) { - int modID = modInfo->getNexusID(); - if (modID >= 0) { - return modID; - } else { - return QVariant(); - } - } else if (column == COL_CATEGORY) { - int category = modInfo->getPrimaryCategory(); - if (category != -1) { - CategoryFactory &categoryFactory = CategoryFactory::instance(); - try { - return categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(category)); - } catch (const std::exception &e) { - qCritical("failed to retrieve category name: %s", e.what()); - return QString(); - } - } else { - //return tr("None"); - return QVariant(); - } - } else { - return tr("invalid"); - } - } else if ((role == Qt::CheckStateRole) && (column == 0)) { - if (modInfo->canBeEnabled()) { - return m_Profile->modEnabled(modIndex) ? Qt::Checked : Qt::Unchecked; - } else { - return QVariant(); - } - } else if (role == Qt::TextAlignmentRole) { - if (column == COL_NAME) { - if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_CENTER) { - return QVariant(Qt::AlignCenter | Qt::AlignVCenter); - } else { - return QVariant(Qt::AlignLeft | Qt::AlignVCenter); - } - } else if (column == COL_VERSION) { - return QVariant(Qt::AlignRight | Qt::AlignVCenter); - } else { - return QVariant(Qt::AlignCenter | Qt::AlignVCenter); - } - } else if (role == Qt::UserRole) { - return modInfo->getNexusID(); - } else if (role == Qt::FontRole) { - QFont result; - if (column == COL_NAME) { - if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_INVALID) { - result.setItalic(true); - } - } else if (column == COL_VERSION) { - if (modInfo->updateAvailable()) { - result.setWeight(QFont::Bold); - } - } - return result; - } else if (role == Qt::DecorationRole) { - if ((column == COL_VERSION) && - modInfo->updateAvailable() && - modInfo->getNewestVersion().isValid()) { - return QIcon(":/MO/gui/update_available"); - } - return QVariant(); - } else if (role == Qt::ForegroundRole) { - if (column == COL_NAME) { - int highlight = modInfo->getHighlight(); - if (highlight & ModInfo::HIGHLIGHT_IMPORTANT) return QBrush(Qt::darkRed); - else if (highlight & ModInfo::HIGHLIGHT_INVALID) return QBrush(Qt::darkGray); - } else if (column == COL_VERSION) { - if (!modInfo->getNewestVersion().isValid()) { - return QVariant(); - } else if (modInfo->updateAvailable()) { - return QBrush(Qt::red); - } else { - return QBrush(Qt::darkGreen); - } - } - return QVariant(); - } else if (role == Qt::ToolTipRole) { - if (column == COL_FLAGS) { - QString result; - - std::vector flags = modInfo->getFlags(); - - for (auto iter = flags.begin(); iter != flags.end(); ++iter) { - if (result.length() != 0) result += "
    "; - result += getFlagText(*iter, modInfo); - } - - return result; - } else if (column == COL_NAME) { - try { - return modInfo->getDescription(); - } catch (const std::exception &e) { - qCritical("invalid mod description: %s", e.what()); - return QString(); - } - } else if (column == COL_VERSION) { - return tr("installed version: %1, newest version: %2").arg(modInfo->getVersion().canonicalString()).arg(modInfo->getNewestVersion().canonicalString()); - } else if (column == COL_CATEGORY) { - const std::set &categories = modInfo->getCategories(); - std::wostringstream categoryString; - categoryString << ToWString(tr("Categories:
    ")); - CategoryFactory &categoryFactory = CategoryFactory::instance(); - for (std::set::const_iterator catIter = categories.begin(); - catIter != categories.end(); ++catIter) { - if (catIter != categories.begin()) { - categoryString << " , "; - } - try { - categoryString << "" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << ""; - } catch (const std::exception &e) { - qCritical("failed to generate tooltip: %s", e.what()); - return QString(); - } - } - - return ToQString(categoryString.str()); - } else { - return QVariant(); - } - } else { - return QVariant(); - } -} - - -bool ModList::renameMod(int index, const QString &newName) -{ - // before we rename, write back the current profile so we don't lose changes and to ensure - // there is no scheduled asynchronous rewrite anytime soon - m_Profile->writeModlistNow(); - - ModInfo::Ptr modInfo = ModInfo::getByIndex(index); - QString oldName = modInfo->name(); - modInfo->setName(newName); - // this just broke all profiles! The recipient of modRenamed has to do some magic - // to can't write the currently active profile back - emit modRenamed(oldName, newName); - - // invalidate the currently displayed state of this list - notifyChange(-1); - return true; -} - - -bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) -{ - if (m_Profile == NULL) return false; - - if (static_cast(index.row()) >= ModInfo::getNumMods()) { - return false; - } - - if (role == Qt::CheckStateRole) { - bool enabled = value.toInt() == Qt::Checked; - if (m_Profile->modEnabled(index.row()) != enabled) { - m_Profile->setModEnabled(index.row(), enabled); - m_Modified = true; - - emit modlist_changed(index.row()); - } - return true; - } else if (role == Qt::EditRole) { - switch (index.column()) { - case COL_NAME: { - return renameMod(index.row(), value.toString()); - } break; - case COL_PRIORITY: { - bool ok = false; - int newPriority = value.toInt(&ok); - if (ok) { - m_Profile->setModPriority(index.row(), newPriority); - - emit modlist_changed(index.row()); - return true; - } else { - return false; - } - } break; - case COL_MODID: { - ModInfo::Ptr info = ModInfo::getByIndex(index.row()); - bool ok = false; - int newID = value.toInt(&ok); - if (ok) { - info->setNexusID(newID); - emit modlist_changed(index.row()); - return true; - } else { - return false; - } - } break; - case COL_VERSION: { - ModInfo::Ptr info = ModInfo::getByIndex(index.row()); - VersionInfo version(value.toString()); - if (version.isValid()) { - info->setVersion(version); - return true; - } else { - return false; - } - } break; - default: { - qWarning("edit on column \"%s\" not supported", - getColumnName(index.column()).toUtf8().constData()); - return false; - } break; - } - } else { - return false; - } -} - - -ModList::EColumn ModList::getEnabledColumn(int index) const -{ - for (int i = 0; i <= COL_LASTCOLUMN; ++i) { - if (index == 0) { - return static_cast(i); - } else { - --index; - } - } - return ModList::COL_NAME; -} - - -QVariant ModList::headerData(int section, Qt::Orientation orientation, - int role) const -{ - if (orientation == Qt::Horizontal) { - if (role == Qt::DisplayRole) { - return getColumnName(section); - } else if (role == Qt::ToolTipRole) { - return getColumnToolTip(section); - } else if (role == Qt::TextAlignmentRole) { - return QVariant(Qt::AlignCenter); - } else if (role == Qt::SizeHintRole) { - QSize temp = m_FontMetrics.size(Qt::TextSingleLine, getColumnName(section)); - temp.rwidth() += 20; - temp.rheight() += 12; - return temp; - } - } - return QAbstractItemModel::headerData(section, orientation, role); -} - - -Qt::ItemFlags ModList::flags(const QModelIndex &modelIndex) const -{ - Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex); - if (modelIndex.internalId() < 0) { - return Qt::ItemIsEnabled; - } - if (modelIndex.isValid()) { - ModInfo::Ptr modInfo = ModInfo::getByIndex(modelIndex.row()); - if (modInfo->getFixedPriority() == INT_MIN) { - result |= Qt::ItemIsDragEnabled; - result |= Qt::ItemIsUserCheckable; - if ((modelIndex.column() == COL_NAME) || - (modelIndex.column() == COL_PRIORITY) || - (modelIndex.column() == COL_VERSION) || - (modelIndex.column() == COL_MODID)) { - result |= Qt::ItemIsEditable; - } - } - } else { - result |= Qt::ItemIsDropEnabled; - } - return result; -} - - -void ModList::changeModPriority(std::vector sourceIndices, int newPriority) -{ - if (m_Profile == NULL) return; - - emit layoutAboutToBeChanged(); - Profile *profile = m_Profile; - // sort rows to insert by their old priority (ascending) and insert them move them in that order - std::sort(sourceIndices.begin(), sourceIndices.end(), - [profile](const int &LHS, const int &RHS) { - return profile->getModPriority(LHS) < profile->getModPriority(RHS); - }); - - // odd stuff: if any of the dragged sources has priority lower than the destination then the - // target idx is that of the row BELOW the dropped location, otherwise it's the one above. why? - for (std::vector::const_iterator iter = sourceIndices.begin(); - iter != sourceIndices.end(); ++iter) { - if (profile->getModPriority(*iter) < newPriority) { - --newPriority; - break; - } - } - - for (std::vector::const_iterator iter = sourceIndices.begin(); - iter != sourceIndices.end(); ++iter) { - m_Profile->setModPriority(*iter, newPriority); - } - - emit layoutChanged(); - - emit modorder_changed(); -} - - -void ModList::changeModPriority(int sourceIndex, int newPriority) -{ - if (m_Profile == NULL) return; - emit layoutAboutToBeChanged(); - - m_Profile->setModPriority(sourceIndex, newPriority); - - emit layoutChanged(); - - emit modorder_changed(); -} - - -bool ModList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int, const QModelIndex &parent) -{ - if (action == Qt::IgnoreAction) { - return true; - } - - if (m_Profile == NULL) return false; - - try { - QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist"); - QDataStream stream(&encoded, QIODevice::ReadOnly); - std::vector sourceRows; - - while (!stream.atEnd()) { - int sourceRow, col; - QMap roleDataMap; - stream >> sourceRow >> col >> roleDataMap; - if (col == 0) { - sourceRows.push_back(sourceRow); - } - } - - if (row == -1) { - row = parent.row(); - } - - if ((row < 0) || (static_cast(row) >= ModInfo::getNumMods())) { - return false; - } - - int newPriority = 0; - { - if ((row < 0) || (row > static_cast(m_Profile->numRegularMods()))) { - newPriority = m_Profile->numRegularMods() + 1; - } else { - newPriority = m_Profile->getModPriority(row); - } - if (newPriority == -1) { - newPriority = m_Profile->numRegularMods() + 1; - } - } - changeModPriority(sourceRows, newPriority); - } catch (const std::exception &e) { - reportError(tr("drag&drop failed: %1").arg(e.what())); - } - - return false; -} - - -void ModList::removeRowForce(int row) -{ - if (static_cast(row) >= ModInfo::getNumMods()) { - return; - } - if (m_Profile == NULL) return; - - ModInfo::Ptr modInfo = ModInfo::getByIndex(row); - - bool wasEnabled = m_Profile->modEnabled(row); - beginRemoveRows(QModelIndex(), row, row); - - m_Profile->cancelWriteModlist(); // don't write modlist while we're changing it - ModInfo::removeMod(row); - m_Profile->refreshModStatus(); // removes the mod from the status list - m_Profile->writeModlist(); // this ensures the modified list gets written back before new mods can be installed - - endRemoveRows(); - if (wasEnabled) { - emit removeOrigin(modInfo->name()); - } -} - - -void ModList::removeRow(int row, const QModelIndex&) -{ - if (static_cast(row) >= ModInfo::getNumMods()) { - return; - } - if (m_Profile == NULL) return; - - ModInfo::Ptr modInfo = ModInfo::getByIndex(row); - - QMessageBox confirmBox(QMessageBox::Question, tr("Confirm"), tr("Are you sure you want to remove \"%1\"?").arg(modInfo->name()), - QMessageBox::Yes | QMessageBox::No); - - if (confirmBox.exec() == QMessageBox::Yes) { - removeRowForce(row); - } -} - - -void ModList::notifyChange(int row) -{ - if (row < 0) { - beginResetModel(); - endResetModel(); - } else { - emit dataChanged(this->index(row, 0), this->index(row, this->columnCount() - 1)); - } -} - - -QModelIndex ModList::index(int row, int column, const QModelIndex&) const -{ - return createIndex(row, column, row); -} - - -QModelIndex ModList::parent(const QModelIndex&) const -{ - return QModelIndex(); -} - - -QString ModList::getColumnName(int column) -{ - switch (column) { - case COL_FLAGS: return tr("Flags"); - case COL_NAME: return tr("Name"); - case COL_VERSION: return tr("Version"); - case COL_PRIORITY: return tr("Priority"); - case COL_CATEGORY: return tr("Category"); - case COL_MODID: return tr("Nexus ID"); - default: return tr("unknown"); - } -} - - -QString ModList::getColumnToolTip(int column) -{ - switch (column) { - case COL_NAME: return tr("Name of your mods"); - case COL_VERSION: return tr("Version of the mod (if available)"); - case COL_PRIORITY: return tr("Installation priority of your mod. The higher, the more \"important\" it is and thus " - "overwrites files from mods with lower priority."); - case COL_CATEGORY: return tr("Category of the mod."); - case COL_MODID: return tr("Id of the mod as used on Nexus."); - case COL_FLAGS: return tr("Emblemes to highlight things that might require attention."); - default: return tr("unknown"); - } -} - - -bool ModList::eventFilter(QObject *obj, QEvent *event) -{ - if (event->type() == QEvent::ContextMenu) { - QContextMenuEvent *contextEvent = static_cast(event); - QWidget *object = qobject_cast(obj); - if (object != NULL) { - emit requestColumnSelect(object->mapToGlobal(contextEvent->pos())); - - return true; - } - } else if ((event->type() == QEvent::KeyPress) && (m_Profile != NULL)) { - QAbstractItemView *itemView = qobject_cast(obj); - QKeyEvent *keyEvent = static_cast(event); - if ((itemView != NULL) && - (keyEvent->modifiers() == Qt::ControlModifier) && - ((keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down))) { - QItemSelectionModel *selectionModel = itemView->selectionModel(); - const QSortFilterProxyModel *proxyModel = qobject_cast(selectionModel->model()); - int diff = -1; - if (((keyEvent->key() == Qt::Key_Up) && (proxyModel->sortOrder() == Qt::DescendingOrder)) || - ((keyEvent->key() == Qt::Key_Down) && (proxyModel->sortOrder() == Qt::AscendingOrder))) { - diff = 1; - } - QModelIndexList rows = selectionModel->selectedRows(); - if (keyEvent->key() == Qt::Key_Down) { - for (int i = 0; i < rows.size() / 2; ++i) { - rows.swap(i, rows.size() - i - 1); - } - } - foreach (QModelIndex idx, rows) { - if (proxyModel != NULL) { - idx = proxyModel->mapToSource(idx); - } - int newPriority = m_Profile->getModPriority(idx.row()) + diff; - if ((newPriority >= 0) && (newPriority < static_cast(m_Profile->numRegularMods()))) { - m_Profile->setModPriority(idx.row(), newPriority); - notifyChange(idx.row()); - } - } - return true; - } else if (keyEvent->key() == Qt::Key_Delete) { - emit removeSelectedMods(); -/* QItemSelectionModel *selectionModel = itemView->selectionModel(); - const QSortFilterProxyModel *proxyModel = qobject_cast(selectionModel->model()); - QModelIndexList rows = selectionModel->selectedRows(); - foreach (QModelIndex idx, rows) { - if (proxyModel != NULL) { - idx = proxyModel->mapToSource(idx); - } - removeRow(idx.row(), QModelIndex()); - }*/ - return true; - } - } - return QObject::eventFilter(obj, event); -} +#include "modlist.h" + +#include "report.h" +#include "utility.h" +#include "messagedialog.h" +#include "installationtester.h" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +ModList::ModList(QObject *parent) + : QAbstractItemModel(parent), m_Profile(NULL), m_Modified(false), + m_FontMetrics(QFont()) +{ +// setSupportedDragActions(Qt::MoveAction); +} + + +void ModList::setProfile(Profile *profile) +{ + m_Profile = profile; +} + + +int ModList::rowCount(const QModelIndex &parent) const +{ + if (!parent.isValid()) { + return ModInfo::getNumMods(); + } else { + return 0; + } +} + + +int ModList::columnCount(const QModelIndex &) const +{ + return COL_LASTCOLUMN + 1; +} + + +QVariant ModList::getOverwriteData(int column, int role) const +{ + switch (role) { + case Qt::DisplayRole: { + if (column == 0) { + return tr("Overwrite"); + } else { + return QVariant(); + } + } break; + case Qt::CheckStateRole: { + if (column == 0) { + return Qt::Checked; + } else { + return QVariant(); + } + } break; + case Qt::TextAlignmentRole: return QVariant(Qt::AlignCenter | Qt::AlignVCenter); + case Qt::UserRole: return -1; + case Qt::ForegroundRole: return QBrush(Qt::red); + case Qt::ToolTipRole: return tr("This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit)"); + default: return QVariant(); + } +} + + +QString ModList::getFlagText(ModInfo::EFlag flag, ModInfo::Ptr modInfo) const +{ + switch (flag) { + case ModInfo::FLAG_BACKUP: return tr("Backup"); + case ModInfo::FLAG_INVALID: return tr("No valid game data"); + case ModInfo::FLAG_NOTENDORSED: return tr("Not endorsed yet"); + case ModInfo::FLAG_NOTES: return QString("%1").arg(modInfo->notes().replace("\n", "
    ")); + case ModInfo::FLAG_CONFLICT_OVERWRITE: return tr("Overwrites files"); + case ModInfo::FLAG_CONFLICT_OVERWRITTEN: return tr("Overwritten files"); + case ModInfo::FLAG_CONFLICT_MIXED: return tr("Overwrites & Overwritten"); + case ModInfo::FLAG_CONFLICT_REDUNDANT: return tr("Redundant"); + default: return ""; + } +} + + +QVariant ModList::data(const QModelIndex &modelIndex, int role) const +{ + if (m_Profile == NULL) return QVariant(); + unsigned int modIndex = modelIndex.row(); + int column = modelIndex.column(); + + ModInfo::Ptr modInfo = ModInfo::getByIndex(modIndex); + if ((role == Qt::DisplayRole) || + (role == Qt::EditRole)) { + if (column == COL_FLAGS) { + return QVariant(); + } else if (column == COL_NAME) { + return modInfo->name(); + } else if (column == COL_VERSION) { + QString version = modInfo->getVersion().canonicalString(); + if (version.isEmpty() && modInfo->canBeUpdated()) { + version = "?"; + } + return version; + } else if (column == COL_PRIORITY) { + int priority = modInfo->getFixedPriority(); + if (priority != INT_MIN) { + return QVariant(); // hide priority for mods where it's fixed + } else { + if ((m_Profile->getModPriority(modIndex) == 0) && (role == Qt::DisplayRole)) { + return tr("min"); + } else if ((m_Profile->getModPriority(modIndex) == m_Profile->numRegularMods() - 1) && + (role == Qt::DisplayRole)) { + return tr("max"); + } else { + //return QString::number(m_Profile->getModPriority(modIndex)); + return m_Profile->getModPriority(modIndex); + } + } + } else if (column == COL_MODID) { + int modID = modInfo->getNexusID(); + if (modID >= 0) { + return modID; + } else { + return QVariant(); + } + } else if (column == COL_CATEGORY) { + int category = modInfo->getPrimaryCategory(); + if (category != -1) { + CategoryFactory &categoryFactory = CategoryFactory::instance(); + try { + return categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(category)); + } catch (const std::exception &e) { + qCritical("failed to retrieve category name: %s", e.what()); + return QString(); + } + } else { + //return tr("None"); + return QVariant(); + } + } else { + return tr("invalid"); + } + } else if ((role == Qt::CheckStateRole) && (column == 0)) { + if (modInfo->canBeEnabled()) { + return m_Profile->modEnabled(modIndex) ? Qt::Checked : Qt::Unchecked; + } else { + return QVariant(); + } + } else if (role == Qt::TextAlignmentRole) { + if (column == COL_NAME) { + if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_CENTER) { + return QVariant(Qt::AlignCenter | Qt::AlignVCenter); + } else { + return QVariant(Qt::AlignLeft | Qt::AlignVCenter); + } + } else if (column == COL_VERSION) { + return QVariant(Qt::AlignRight | Qt::AlignVCenter); + } else { + return QVariant(Qt::AlignCenter | Qt::AlignVCenter); + } + } else if (role == Qt::UserRole) { + return modInfo->getNexusID(); + } else if (role == Qt::FontRole) { + QFont result; + if (column == COL_NAME) { + if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_INVALID) { + result.setItalic(true); + } + } else if (column == COL_VERSION) { + if (modInfo->updateAvailable()) { + result.setWeight(QFont::Bold); + } + } + return result; + } else if (role == Qt::DecorationRole) { + if ((column == COL_VERSION) && + modInfo->updateAvailable() && + modInfo->getNewestVersion().isValid()) { + return QIcon(":/MO/gui/update_available"); + } + return QVariant(); + } else if (role == Qt::ForegroundRole) { + if (column == COL_NAME) { + int highlight = modInfo->getHighlight(); + if (highlight & ModInfo::HIGHLIGHT_IMPORTANT) return QBrush(Qt::darkRed); + else if (highlight & ModInfo::HIGHLIGHT_INVALID) return QBrush(Qt::darkGray); + } else if (column == COL_VERSION) { + if (!modInfo->getNewestVersion().isValid()) { + return QVariant(); + } else if (modInfo->updateAvailable()) { + return QBrush(Qt::red); + } else { + return QBrush(Qt::darkGreen); + } + } + return QVariant(); + } else if (role == Qt::ToolTipRole) { + if (column == COL_FLAGS) { + QString result; + + std::vector flags = modInfo->getFlags(); + + for (auto iter = flags.begin(); iter != flags.end(); ++iter) { + if (result.length() != 0) result += "
    "; + result += getFlagText(*iter, modInfo); + } + + return result; + } else if (column == COL_NAME) { + try { + return modInfo->getDescription(); + } catch (const std::exception &e) { + qCritical("invalid mod description: %s", e.what()); + return QString(); + } + } else if (column == COL_VERSION) { + return tr("installed version: %1, newest version: %2").arg(modInfo->getVersion().canonicalString()).arg(modInfo->getNewestVersion().canonicalString()); + } else if (column == COL_CATEGORY) { + const std::set &categories = modInfo->getCategories(); + std::wostringstream categoryString; + categoryString << ToWString(tr("Categories:
    ")); + CategoryFactory &categoryFactory = CategoryFactory::instance(); + for (std::set::const_iterator catIter = categories.begin(); + catIter != categories.end(); ++catIter) { + if (catIter != categories.begin()) { + categoryString << " , "; + } + try { + categoryString << "" << ToWString(categoryFactory.getCategoryName(categoryFactory.getCategoryIndex(*catIter))) << ""; + } catch (const std::exception &e) { + qCritical("failed to generate tooltip: %s", e.what()); + return QString(); + } + } + + return ToQString(categoryString.str()); + } else { + return QVariant(); + } + } else { + return QVariant(); + } +} + + +bool ModList::renameMod(int index, const QString &newName) +{ + // before we rename, write back the current profile so we don't lose changes and to ensure + // there is no scheduled asynchronous rewrite anytime soon + m_Profile->writeModlistNow(); + + ModInfo::Ptr modInfo = ModInfo::getByIndex(index); + QString oldName = modInfo->name(); + modInfo->setName(newName); + // this just broke all profiles! The recipient of modRenamed has to do some magic + // to can't write the currently active profile back + emit modRenamed(oldName, newName); + + // invalidate the currently displayed state of this list + notifyChange(-1); + return true; +} + + +bool ModList::setData(const QModelIndex &index, const QVariant &value, int role) +{ + if (m_Profile == NULL) return false; + + if (static_cast(index.row()) >= ModInfo::getNumMods()) { + return false; + } + + if (role == Qt::CheckStateRole) { + bool enabled = value.toInt() == Qt::Checked; + if (m_Profile->modEnabled(index.row()) != enabled) { + m_Profile->setModEnabled(index.row(), enabled); + m_Modified = true; + + emit modlist_changed(index.row()); + } + return true; + } else if (role == Qt::EditRole) { + switch (index.column()) { + case COL_NAME: { + return renameMod(index.row(), value.toString()); + } break; + case COL_PRIORITY: { + bool ok = false; + int newPriority = value.toInt(&ok); + if (ok) { + m_Profile->setModPriority(index.row(), newPriority); + + emit modlist_changed(index.row()); + return true; + } else { + return false; + } + } break; + case COL_MODID: { + ModInfo::Ptr info = ModInfo::getByIndex(index.row()); + bool ok = false; + int newID = value.toInt(&ok); + if (ok) { + info->setNexusID(newID); + emit modlist_changed(index.row()); + return true; + } else { + return false; + } + } break; + case COL_VERSION: { + ModInfo::Ptr info = ModInfo::getByIndex(index.row()); + VersionInfo version(value.toString()); + if (version.isValid()) { + info->setVersion(version); + return true; + } else { + return false; + } + } break; + default: { + qWarning("edit on column \"%s\" not supported", + getColumnName(index.column()).toUtf8().constData()); + return false; + } break; + } + } else { + return false; + } +} + + +ModList::EColumn ModList::getEnabledColumn(int index) const +{ + for (int i = 0; i <= COL_LASTCOLUMN; ++i) { + if (index == 0) { + return static_cast(i); + } else { + --index; + } + } + return ModList::COL_NAME; +} + + +QVariant ModList::headerData(int section, Qt::Orientation orientation, + int role) const +{ + if (orientation == Qt::Horizontal) { + if (role == Qt::DisplayRole) { + return getColumnName(section); + } else if (role == Qt::ToolTipRole) { + return getColumnToolTip(section); + } else if (role == Qt::TextAlignmentRole) { + return QVariant(Qt::AlignCenter); + } else if (role == Qt::SizeHintRole) { + QSize temp = m_FontMetrics.size(Qt::TextSingleLine, getColumnName(section)); + temp.rwidth() += 20; + temp.rheight() += 12; + return temp; + } + } + return QAbstractItemModel::headerData(section, orientation, role); +} + + +Qt::ItemFlags ModList::flags(const QModelIndex &modelIndex) const +{ + Qt::ItemFlags result = QAbstractItemModel::flags(modelIndex); + if (modelIndex.internalId() < 0) { + return Qt::ItemIsEnabled; + } + if (modelIndex.isValid()) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(modelIndex.row()); + if (modInfo->getFixedPriority() == INT_MIN) { + result |= Qt::ItemIsDragEnabled; + result |= Qt::ItemIsUserCheckable; + if ((modelIndex.column() == COL_NAME) || + (modelIndex.column() == COL_PRIORITY) || + (modelIndex.column() == COL_VERSION) || + (modelIndex.column() == COL_MODID)) { + result |= Qt::ItemIsEditable; + } + } + } else { + result |= Qt::ItemIsDropEnabled; + } + return result; +} + + +void ModList::changeModPriority(std::vector sourceIndices, int newPriority) +{ + if (m_Profile == NULL) return; + + emit layoutAboutToBeChanged(); + Profile *profile = m_Profile; + // sort rows to insert by their old priority (ascending) and insert them move them in that order + std::sort(sourceIndices.begin(), sourceIndices.end(), + [profile](const int &LHS, const int &RHS) { + return profile->getModPriority(LHS) < profile->getModPriority(RHS); + }); + + // odd stuff: if any of the dragged sources has priority lower than the destination then the + // target idx is that of the row BELOW the dropped location, otherwise it's the one above. why? + for (std::vector::const_iterator iter = sourceIndices.begin(); + iter != sourceIndices.end(); ++iter) { + if (profile->getModPriority(*iter) < newPriority) { + --newPriority; + break; + } + } + + for (std::vector::const_iterator iter = sourceIndices.begin(); + iter != sourceIndices.end(); ++iter) { + m_Profile->setModPriority(*iter, newPriority); + } + + emit layoutChanged(); + + emit modorder_changed(); +} + + +void ModList::changeModPriority(int sourceIndex, int newPriority) +{ + if (m_Profile == NULL) return; + emit layoutAboutToBeChanged(); + + m_Profile->setModPriority(sourceIndex, newPriority); + + emit layoutChanged(); + + emit modorder_changed(); +} + + +bool ModList::dropMimeData(const QMimeData *mimeData, Qt::DropAction action, int row, int, const QModelIndex &parent) +{ + if (action == Qt::IgnoreAction) { + return true; + } + + if (m_Profile == NULL) return false; + + try { + QByteArray encoded = mimeData->data("application/x-qabstractitemmodeldatalist"); + QDataStream stream(&encoded, QIODevice::ReadOnly); + std::vector sourceRows; + + while (!stream.atEnd()) { + int sourceRow, col; + QMap roleDataMap; + stream >> sourceRow >> col >> roleDataMap; + if (col == 0) { + sourceRows.push_back(sourceRow); + } + } + + if (row == -1) { + row = parent.row(); + } + + if ((row < 0) || (static_cast(row) >= ModInfo::getNumMods())) { + return false; + } + + int newPriority = 0; + { + if ((row < 0) || (row > static_cast(m_Profile->numRegularMods()))) { + newPriority = m_Profile->numRegularMods() + 1; + } else { + newPriority = m_Profile->getModPriority(row); + } + if (newPriority == -1) { + newPriority = m_Profile->numRegularMods() + 1; + } + } + changeModPriority(sourceRows, newPriority); + } catch (const std::exception &e) { + reportError(tr("drag&drop failed: %1").arg(e.what())); + } + + return false; +} + + +void ModList::removeRowForce(int row) +{ + if (static_cast(row) >= ModInfo::getNumMods()) { + return; + } + if (m_Profile == NULL) return; + + ModInfo::Ptr modInfo = ModInfo::getByIndex(row); + + bool wasEnabled = m_Profile->modEnabled(row); + beginRemoveRows(QModelIndex(), row, row); + + m_Profile->cancelWriteModlist(); // don't write modlist while we're changing it + ModInfo::removeMod(row); + m_Profile->refreshModStatus(); // removes the mod from the status list + m_Profile->writeModlist(); // this ensures the modified list gets written back before new mods can be installed + + endRemoveRows(); + if (wasEnabled) { + emit removeOrigin(modInfo->name()); + } +} + + +void ModList::removeRow(int row, const QModelIndex&) +{ + if (static_cast(row) >= ModInfo::getNumMods()) { + return; + } + if (m_Profile == NULL) return; + + ModInfo::Ptr modInfo = ModInfo::getByIndex(row); + + QMessageBox confirmBox(QMessageBox::Question, tr("Confirm"), tr("Are you sure you want to remove \"%1\"?").arg(modInfo->name()), + QMessageBox::Yes | QMessageBox::No); + + if (confirmBox.exec() == QMessageBox::Yes) { + removeRowForce(row); + } +} + + +void ModList::notifyChange(int row) +{ + if (row < 0) { + beginResetModel(); + endResetModel(); + } else { + emit dataChanged(this->index(row, 0), this->index(row, this->columnCount() - 1)); + } +} + + +QModelIndex ModList::index(int row, int column, const QModelIndex&) const +{ + return createIndex(row, column, row); +} + + +QModelIndex ModList::parent(const QModelIndex&) const +{ + return QModelIndex(); +} + + +QString ModList::getColumnName(int column) +{ + switch (column) { + case COL_FLAGS: return tr("Flags"); + case COL_NAME: return tr("Name"); + case COL_VERSION: return tr("Version"); + case COL_PRIORITY: return tr("Priority"); + case COL_CATEGORY: return tr("Category"); + case COL_MODID: return tr("Nexus ID"); + default: return tr("unknown"); + } +} + + +QString ModList::getColumnToolTip(int column) +{ + switch (column) { + case COL_NAME: return tr("Name of your mods"); + case COL_VERSION: return tr("Version of the mod (if available)"); + case COL_PRIORITY: return tr("Installation priority of your mod. The higher, the more \"important\" it is and thus " + "overwrites files from mods with lower priority."); + case COL_CATEGORY: return tr("Category of the mod."); + case COL_MODID: return tr("Id of the mod as used on Nexus."); + case COL_FLAGS: return tr("Emblemes to highlight things that might require attention."); + default: return tr("unknown"); + } +} + + +bool ModList::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::ContextMenu) { + QContextMenuEvent *contextEvent = static_cast(event); + QWidget *object = qobject_cast(obj); + if (object != NULL) { + emit requestColumnSelect(object->mapToGlobal(contextEvent->pos())); + + return true; + } + } else if ((event->type() == QEvent::KeyPress) && (m_Profile != NULL)) { + QAbstractItemView *itemView = qobject_cast(obj); + QKeyEvent *keyEvent = static_cast(event); + if ((itemView != NULL) && + (keyEvent->modifiers() == Qt::ControlModifier) && + ((keyEvent->key() == Qt::Key_Up) || (keyEvent->key() == Qt::Key_Down))) { + QItemSelectionModel *selectionModel = itemView->selectionModel(); + const QSortFilterProxyModel *proxyModel = qobject_cast(selectionModel->model()); + int diff = -1; + if (((keyEvent->key() == Qt::Key_Up) && (proxyModel->sortOrder() == Qt::DescendingOrder)) || + ((keyEvent->key() == Qt::Key_Down) && (proxyModel->sortOrder() == Qt::AscendingOrder))) { + diff = 1; + } + QModelIndexList rows = selectionModel->selectedRows(); + if (keyEvent->key() == Qt::Key_Down) { + for (int i = 0; i < rows.size() / 2; ++i) { + rows.swap(i, rows.size() - i - 1); + } + } + foreach (QModelIndex idx, rows) { + if (proxyModel != NULL) { + idx = proxyModel->mapToSource(idx); + } + int newPriority = m_Profile->getModPriority(idx.row()) + diff; + if ((newPriority >= 0) && (newPriority < static_cast(m_Profile->numRegularMods()))) { + m_Profile->setModPriority(idx.row(), newPriority); + notifyChange(idx.row()); + } + } + return true; + } else if (keyEvent->key() == Qt::Key_Delete) { + QItemSelectionModel *selectionModel = itemView->selectionModel(); + QModelIndexList rows = selectionModel->selectedRows(); + if (rows.count() > 1) { + emit removeSelectedMods(); + } else if (rows.count() == 1) { + removeRow(rows[0].row(), QModelIndex()); + } + return true; + } + } + return QObject::eventFilter(obj, event); +} diff --git a/src/modlistsortproxy.cpp b/src/modlistsortproxy.cpp index de1e3230..bf70f238 100644 --- a/src/modlistsortproxy.cpp +++ b/src/modlistsortproxy.cpp @@ -17,213 +17,218 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include "modlistsortproxy.h" -#include "modinfo.h" -#include "profile.h" -#include "modlist.h" -#include -#include -#include -#include - - -ModListSortProxy::ModListSortProxy(Profile* profile, QObject *parent) - : QSortFilterProxyModel(parent), m_Profile(profile), - m_CategoryFilter(CategoryFactory::CATEGORY_NONE), m_CurrentFilter() -{ - m_EnabledColumns.set(ModList::COL_FLAGS); - m_EnabledColumns.set(ModList::COL_NAME); - m_EnabledColumns.set(ModList::COL_VERSION); - m_EnabledColumns.set(ModList::COL_PRIORITY); - setDynamicSortFilter(true); // this seems to work without dynamicsortfilter - // but I don't know why. This should be necessary -} - - -void ModListSortProxy::setProfile(Profile *profile) -{ - m_Profile = profile; -} - - -void ModListSortProxy::setCategoryFilter(int category) -{ - m_CategoryFilter = category; - this->invalidate(); -} - - -Qt::ItemFlags ModListSortProxy::flags(const QModelIndex &modelIndex) const -{ - Qt::ItemFlags flags = sourceModel()->flags(mapToSource(modelIndex)); - if (sortColumn() != ModList::COL_PRIORITY) { - flags &= ~Qt::ItemIsDragEnabled; - } - return flags; -} - - - -void ModListSortProxy::displayColumnSelection(const QPoint &pos) -{ - QMenu menu; - - for (int i = 0; i <= ModList::COL_LASTCOLUMN; ++i) { - QCheckBox *checkBox = new QCheckBox(&menu); - checkBox->setText(ModList::getColumnName(i)); - checkBox->setChecked(m_EnabledColumns.test(i) ? Qt::Checked : Qt::Unchecked); - QWidgetAction *checkableAction = new QWidgetAction(&menu); - checkableAction->setDefaultWidget(checkBox); - menu.addAction(checkableAction); - } - menu.exec(pos); - int i = 0; - - emit layoutAboutToBeChanged(); - m_EnabledColumns.reset(); - foreach (const QAction *action, menu.actions()) { - const QWidgetAction *widgetAction = qobject_cast(action); - if (widgetAction != NULL) { - const QCheckBox *checkBox = qobject_cast(widgetAction->defaultWidget()); - if (checkBox != NULL) { - m_EnabledColumns.set(i, checkBox->checkState() == Qt::Checked); - } - } - ++i; - } - emit layoutChanged(); -} - - -void ModListSortProxy::enableAllVisible() -{ - if (m_Profile == NULL) return; - - for (int i = 0; i < this->rowCount(); ++i) { - int modID = mapToSource(index(i, 0)).row(); - m_Profile->setModEnabled(modID, true); - } -} - - -void ModListSortProxy::disableAllVisible() -{ - if (m_Profile == NULL) return; - for (int i = 0; i < this->rowCount(); ++i) { - int modID = mapToSource(index(i, 0)).row(); - m_Profile->setModEnabled(modID, false); - } -} - - -bool ModListSortProxy::lessThan(const QModelIndex &left, - const QModelIndex &right) const -{ - int leftIndex = left.internalId(); - int rightIndex = right.internalId(); - ModInfo::Ptr leftMod = ModInfo::getByIndex(leftIndex); - ModInfo::Ptr rightMod = ModInfo::getByIndex(rightIndex); - - bool lt = false; - switch (left.column()) { - case ModList::COL_FLAGS: lt = leftMod->getFlags().size() < rightMod->getFlags().size(); break; - case ModList::COL_NAME: lt = QString::compare(leftMod->name(), rightMod->name(), Qt::CaseInsensitive) < 0; break; - case ModList::COL_CATEGORY: { - if (leftMod->getPrimaryCategory() < 0) lt = false; - else if (rightMod->getPrimaryCategory() < 0) lt = true; - else { - try { - CategoryFactory &categories = CategoryFactory::instance(); - QString leftCatName = categories.getCategoryName(categories.getCategoryIndex(leftMod->getPrimaryCategory())); - QString rightCatName = categories.getCategoryName(categories.getCategoryIndex(rightMod->getPrimaryCategory())); - lt = leftCatName < rightCatName; - } catch (const std::exception &e) { - qCritical("failed to compare categories: %s", e.what()); - } - } - } break; - case ModList::COL_MODID: lt = leftMod->getNexusID() < rightMod->getNexusID(); break; - case ModList::COL_VERSION: lt = leftMod->getVersion() < rightMod->getVersion(); break; - case ModList::COL_PRIORITY: { - if (m_Profile != NULL) { - int leftPrio = leftMod->getFixedPriority(); - int rightPrio = rightMod->getFixedPriority(); - if (leftPrio == INT_MIN) leftPrio = m_Profile->getModPriority(leftIndex); - if (rightPrio == INT_MIN) rightPrio = m_Profile->getModPriority(rightIndex); - - lt = leftPrio < rightPrio; - } else { - lt = leftMod->name() < rightMod->name(); - } - } break; - } - return lt; -} - - -void ModListSortProxy::updateFilter(const QString &filter) -{ - m_CurrentFilter = filter; - invalidateFilter(); -} - - -bool ModListSortProxy::hasConflictFlag(const std::vector &flags) const -{ - foreach (ModInfo::EFlag flag, flags) { - if ((flag == ModInfo::FLAG_CONFLICT_MIXED) || - (flag == ModInfo::FLAG_CONFLICT_OVERWRITE) || - (flag == ModInfo::FLAG_CONFLICT_OVERWRITTEN) || - (flag == ModInfo::FLAG_CONFLICT_REDUNDANT)) { - return true; - } - } - - return false; -} - - -bool ModListSortProxy::filterAcceptsRow(int row, const QModelIndex&) const -{ - ModInfo::Ptr modInfo = ModInfo::getByIndex(row); - - if (!m_CurrentFilter.isEmpty() && - !modInfo->name().contains(m_CurrentFilter, Qt::CaseInsensitive)) { - return false; - } - - bool modEnabled = m_Profile != NULL ? m_Profile->modEnabled(row) : false; - return ((m_CategoryFilter == CategoryFactory::CATEGORY_NONE) || - ((m_CategoryFilter < CategoryFactory::CATEGORY_SPECIAL_FIRST) && (modInfo->categorySet(m_CategoryFilter))) || - ((m_CategoryFilter == CategoryFactory::CATEGORY_SPECIAL_CHECKED) && modEnabled) || - ((m_CategoryFilter == CategoryFactory::CATEGORY_SPECIAL_UNCHECKED) && !modEnabled) || - ((m_CategoryFilter == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) && modInfo->updateAvailable()) || - ((m_CategoryFilter == CategoryFactory::CATEGORY_SPECIAL_NOCATEGORY) && (modInfo->getCategories().size() == 0)) || - ((m_CategoryFilter == CategoryFactory::CATEGORY_SPECIAL_CONFLICT) && (hasConflictFlag(modInfo->getFlags())))); -} - - -/*bool ModListSortProxy::filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const -{ - return m_EnabledColumns.test(source_column); -}*/ - - -bool ModListSortProxy::dropMimeData(const QMimeData *data, Qt::DropAction action, - int row, int column, const QModelIndex &parent) -{ - if ((row == -1) && (column == -1)) { - return this->sourceModel()->dropMimeData(data, action, -1, -1, mapToSource(parent)); - } - // in the regular model, when dropping between rows, the row-value passed to - // the sourceModel is inconsistent between ascending and descending ordering. - // This should fix that - if (sortOrder() == Qt::DescendingOrder) { - --row; - } - - QModelIndex proxyIndex = index(row, column, parent); - QModelIndex sourceIndex = mapToSource(proxyIndex); - return this->sourceModel()->dropMimeData(data, action, sourceIndex.row(), sourceIndex.column(), - sourceIndex.parent()); -} +#include "modlistsortproxy.h" +#include "modinfo.h" +#include "profile.h" +#include "modlist.h" +#include +#include +#include +#include + + +ModListSortProxy::ModListSortProxy(Profile* profile, QObject *parent) + : QSortFilterProxyModel(parent), m_Profile(profile), + m_CategoryFilter(CategoryFactory::CATEGORY_NONE), m_CurrentFilter() +{ + m_EnabledColumns.set(ModList::COL_FLAGS); + m_EnabledColumns.set(ModList::COL_NAME); + m_EnabledColumns.set(ModList::COL_VERSION); + m_EnabledColumns.set(ModList::COL_PRIORITY); + setDynamicSortFilter(true); // this seems to work without dynamicsortfilter + // but I don't know why. This should be necessary +} + + +void ModListSortProxy::setProfile(Profile *profile) +{ + m_Profile = profile; +} + + +void ModListSortProxy::setCategoryFilter(int category) +{ + m_CategoryFilter = category; + this->invalidate(); +} + + +Qt::ItemFlags ModListSortProxy::flags(const QModelIndex &modelIndex) const +{ + Qt::ItemFlags flags = sourceModel()->flags(mapToSource(modelIndex)); + if (sortColumn() != ModList::COL_PRIORITY) { + flags &= ~Qt::ItemIsDragEnabled; + } + return flags; +} + + + +void ModListSortProxy::displayColumnSelection(const QPoint &pos) +{ + QMenu menu; + + for (int i = 0; i <= ModList::COL_LASTCOLUMN; ++i) { + QCheckBox *checkBox = new QCheckBox(&menu); + checkBox->setText(ModList::getColumnName(i)); + checkBox->setChecked(m_EnabledColumns.test(i) ? Qt::Checked : Qt::Unchecked); + QWidgetAction *checkableAction = new QWidgetAction(&menu); + checkableAction->setDefaultWidget(checkBox); + menu.addAction(checkableAction); + } + menu.exec(pos); + int i = 0; + + emit layoutAboutToBeChanged(); + m_EnabledColumns.reset(); + foreach (const QAction *action, menu.actions()) { + const QWidgetAction *widgetAction = qobject_cast(action); + if (widgetAction != NULL) { + const QCheckBox *checkBox = qobject_cast(widgetAction->defaultWidget()); + if (checkBox != NULL) { + m_EnabledColumns.set(i, checkBox->checkState() == Qt::Checked); + } + } + ++i; + } + emit layoutChanged(); +} + + +void ModListSortProxy::enableAllVisible() +{ + if (m_Profile == NULL) return; + + for (int i = 0; i < this->rowCount(); ++i) { + int modID = mapToSource(index(i, 0)).row(); + m_Profile->setModEnabled(modID, true); + } +} + + +void ModListSortProxy::disableAllVisible() +{ + if (m_Profile == NULL) return; + for (int i = 0; i < this->rowCount(); ++i) { + int modID = mapToSource(index(i, 0)).row(); + m_Profile->setModEnabled(modID, false); + } +} + + +bool ModListSortProxy::lessThan(const QModelIndex &left, + const QModelIndex &right) const +{ + int leftIndex = left.internalId(); + int rightIndex = right.internalId(); + ModInfo::Ptr leftMod = ModInfo::getByIndex(leftIndex); + ModInfo::Ptr rightMod = ModInfo::getByIndex(rightIndex); + + bool lt = false; + switch (left.column()) { + case ModList::COL_FLAGS: lt = leftMod->getFlags().size() < rightMod->getFlags().size(); break; + case ModList::COL_NAME: lt = QString::compare(leftMod->name(), rightMod->name(), Qt::CaseInsensitive) < 0; break; + case ModList::COL_CATEGORY: { + if (leftMod->getPrimaryCategory() < 0) lt = false; + else if (rightMod->getPrimaryCategory() < 0) lt = true; + else { + try { + CategoryFactory &categories = CategoryFactory::instance(); + QString leftCatName = categories.getCategoryName(categories.getCategoryIndex(leftMod->getPrimaryCategory())); + QString rightCatName = categories.getCategoryName(categories.getCategoryIndex(rightMod->getPrimaryCategory())); + lt = leftCatName < rightCatName; + } catch (const std::exception &e) { + qCritical("failed to compare categories: %s", e.what()); + } + } + } break; + case ModList::COL_MODID: lt = leftMod->getNexusID() < rightMod->getNexusID(); break; + case ModList::COL_VERSION: lt = leftMod->getVersion() < rightMod->getVersion(); break; + case ModList::COL_PRIORITY: { + if (m_Profile != NULL) { + int leftPrio = leftMod->getFixedPriority(); + int rightPrio = rightMod->getFixedPriority(); + if (leftPrio == INT_MIN) leftPrio = m_Profile->getModPriority(leftIndex); + if (rightPrio == INT_MIN) rightPrio = m_Profile->getModPriority(rightIndex); + + lt = leftPrio < rightPrio; + } else { + lt = leftMod->name() < rightMod->name(); + } + } break; + } + return lt; +} + + +void ModListSortProxy::updateFilter(const QString &filter) +{ + m_CurrentFilter = filter; + invalidateFilter(); +} + + +bool ModListSortProxy::hasConflictFlag(const std::vector &flags) const +{ + foreach (ModInfo::EFlag flag, flags) { + if ((flag == ModInfo::FLAG_CONFLICT_MIXED) || + (flag == ModInfo::FLAG_CONFLICT_OVERWRITE) || + (flag == ModInfo::FLAG_CONFLICT_OVERWRITTEN) || + (flag == ModInfo::FLAG_CONFLICT_REDUNDANT)) { + return true; + } + } + + return false; +} + + +bool ModListSortProxy::filterMatches(ModInfo::Ptr info, bool enabled) const +{ + if (!m_CurrentFilter.isEmpty() && + !info->name().contains(m_CurrentFilter, Qt::CaseInsensitive)) { + return false; + } + + return ((m_CategoryFilter == CategoryFactory::CATEGORY_NONE) || + ((m_CategoryFilter < CategoryFactory::CATEGORY_SPECIAL_FIRST) && (info->categorySet(m_CategoryFilter))) || + ((m_CategoryFilter == CategoryFactory::CATEGORY_SPECIAL_CHECKED) && enabled) || + ((m_CategoryFilter == CategoryFactory::CATEGORY_SPECIAL_UNCHECKED) && !enabled) || + ((m_CategoryFilter == CategoryFactory::CATEGORY_SPECIAL_UPDATEAVAILABLE) && info->updateAvailable()) || + ((m_CategoryFilter == CategoryFactory::CATEGORY_SPECIAL_NOCATEGORY) && (info->getCategories().size() == 0)) || + ((m_CategoryFilter == CategoryFactory::CATEGORY_SPECIAL_CONFLICT) && (hasConflictFlag(info->getFlags())))); +} + + +bool ModListSortProxy::filterAcceptsRow(int row, const QModelIndex&) const +{ + bool modEnabled = m_Profile != NULL ? m_Profile->modEnabled(row) : false; + + return filterMatches(ModInfo::getByIndex(row), modEnabled); +} + + +/*bool ModListSortProxy::filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const +{ + return m_EnabledColumns.test(source_column); +}*/ + + +bool ModListSortProxy::dropMimeData(const QMimeData *data, Qt::DropAction action, + int row, int column, const QModelIndex &parent) +{ + if ((row == -1) && (column == -1)) { + return this->sourceModel()->dropMimeData(data, action, -1, -1, mapToSource(parent)); + } + // in the regular model, when dropping between rows, the row-value passed to + // the sourceModel is inconsistent between ascending and descending ordering. + // This should fix that + if (sortOrder() == Qt::DescendingOrder) { + --row; + } + + QModelIndex proxyIndex = index(row, column, parent); + QModelIndex sourceIndex = mapToSource(proxyIndex); + return this->sourceModel()->dropMimeData(data, action, sourceIndex.row(), sourceIndex.column(), + sourceIndex.parent()); +} diff --git a/src/modlistsortproxy.h b/src/modlistsortproxy.h index aad87f06..f7a9b587 100644 --- a/src/modlistsortproxy.h +++ b/src/modlistsortproxy.h @@ -17,64 +17,66 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#ifndef MODLISTSORTPROXY_H -#define MODLISTSORTPROXY_H - -#include -#include -#include "modlist.h" - -class Profile; - -class ModListSortProxy : public QSortFilterProxyModel -{ - Q_OBJECT - -public: - - explicit ModListSortProxy(Profile *profile, QObject *parent = 0); - - void setProfile(Profile *profile); - - void setCategoryFilter(int category); - - virtual Qt::ItemFlags flags(const QModelIndex &modelIndex) const; - virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, - int row, int column, const QModelIndex &parent); - - /** - * @brief enable all mods visible under the current filter - **/ - void enableAllVisible(); - - /** - * @brief disable all mods visible under the current filter - **/ - void disableAllVisible(); - -public slots: - - void displayColumnSelection(const QPoint &pos); - void updateFilter(const QString &filter); - -protected: - - virtual bool lessThan(const QModelIndex &left, const QModelIndex &right) const; - virtual bool filterAcceptsRow(int row, const QModelIndex &parent) const; -// virtual bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const; - -private: - - bool hasConflictFlag(const std::vector &flags) const; - -private: - - Profile *m_Profile; - - int m_CategoryFilter; - std::bitset m_EnabledColumns; - QString m_CurrentFilter; - -}; - -#endif // MODLISTSORTPROXY_H +#ifndef MODLISTSORTPROXY_H +#define MODLISTSORTPROXY_H + +#include +#include +#include "modlist.h" + +class Profile; + +class ModListSortProxy : public QSortFilterProxyModel +{ + Q_OBJECT + +public: + + explicit ModListSortProxy(Profile *profile, QObject *parent = 0); + + void setProfile(Profile *profile); + + void setCategoryFilter(int category); + + virtual Qt::ItemFlags flags(const QModelIndex &modelIndex) const; + virtual bool dropMimeData(const QMimeData *data, Qt::DropAction action, + int row, int column, const QModelIndex &parent); + + /** + * @brief enable all mods visible under the current filter + **/ + void enableAllVisible(); + + /** + * @brief disable all mods visible under the current filter + **/ + void disableAllVisible(); + + bool filterMatches(ModInfo::Ptr info, bool enabled) const; + +public slots: + + void displayColumnSelection(const QPoint &pos); + void updateFilter(const QString &filter); + +protected: + + virtual bool lessThan(const QModelIndex &left, const QModelIndex &right) const; + virtual bool filterAcceptsRow(int row, const QModelIndex &parent) const; +// virtual bool filterAcceptsColumn(int source_column, const QModelIndex &source_parent) const; + +private: + + bool hasConflictFlag(const std::vector &flags) const; + +private: + + Profile *m_Profile; + + int m_CategoryFilter; + std::bitset m_EnabledColumns; + QString m_CurrentFilter; + +}; + +#endif // MODLISTSORTPROXY_H diff --git a/src/nexusdialog.cpp b/src/nexusdialog.cpp index 770b740c..7046cdd7 100644 --- a/src/nexusdialog.cpp +++ b/src/nexusdialog.cpp @@ -17,307 +17,317 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include "nexusdialog.h" -#include "ui_nexusdialog.h" - -#include "messagedialog.h" -#include "report.h" -#include "json.h" - -#include -#include -#include -#include -#include -#include -#include - - -NexusDialog::NexusDialog(NXMAccessManager *accessManager, QWidget *parent) - : QDialog(parent), ui(new Ui::NexusDialog), - m_AccessManager(accessManager), - m_Tutorial(this, "NexusDialog") -{ - ui->setupUi(this); - - Qt::WindowFlags flags = windowFlags() | Qt::WindowMaximizeButtonHint | Qt::WindowMinimizeButtonHint; - Qt::WindowFlags helpFlag = Qt::WindowContextHelpButtonHint; - flags = flags & (~helpFlag); - setWindowFlags(flags); - - m_Tabs = this->findChild("browserTabWidget"); - - m_View = new NexusView(this); - - initTab(m_View); - - m_LoadProgress = this->findChild("loadProgress"); - - connect(m_Tabs, SIGNAL(tabCloseRequested(int)), this, SLOT(tabCloseRequested(int))); - - m_Tutorial.registerControl(); -} - - -NexusDialog::~NexusDialog() -{ - - delete ui; -} - -void NexusDialog::closeEvent(QCloseEvent *event) -{ -// m_AccessManager->showCookies(); - QDialog::closeEvent(event); -} - -void NexusDialog::initTab(NexusView *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(NexusView*)), this, SLOT(initTab(NexusView*))); - 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*))); - connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(requestNXMDownload(QString))); - - ui->backBtn->setEnabled(false); - ui->fwdBtn->setEnabled(false); - m_Tabs->addTab(newView, tr("new")); - - m_View->settings()->setAttribute(QWebSettings::PluginsEnabled, true); - m_View->settings()->setAttribute(QWebSettings::AutoLoadImages, true); -} - - -void NexusDialog::openInNewTab(const QUrl &url) -{ - NexusView *newView = new NexusView(this); - - initTab(newView); - newView->setUrl(url); -} - - -NexusView *NexusDialog::getCurrentView() -{ - return qobject_cast(m_Tabs->currentWidget()); -} - - -void NexusDialog::urlChanged(const QUrl&) -{ - NexusView *currentView = getCurrentView(); - if (currentView != NULL) { - ui->backBtn->setEnabled(currentView->history()->canGoBack()); - ui->fwdBtn->setEnabled(currentView->history()->canGoForward()); - } -} - - -void NexusDialog::openUrl(const QString &url) -{ - m_Url = url; - if (m_Url.startsWith("www")) { - m_Url.prepend("http://"); - } -} - -void NexusDialog::login(const QString &username, const QString &password) -{ - m_AccessManager->login(username, password); - - connect(m_AccessManager, SIGNAL(loginSuccessful(bool)), this, SLOT(loginFinished(bool))); - connect(m_AccessManager, SIGNAL(loginFailed(QString)), this, SLOT(loginFailed(QString))); -} - - -void NexusDialog::loginFailed(const QString &message) -{ - if (this->isVisible()) { - MessageDialog::showMessage(tr("login failed: %1").arg(message), this); - } -} - - -void NexusDialog::loginFinished(bool necessary) -{ - if (necessary && this->isVisible()) { - MessageDialog::showMessage(tr("login successful"), this); - } - loadNexus(); - emit loginSuccessful(necessary); -} - - -void NexusDialog::loadNexus() -{ - m_View->load(QUrl(m_Url)); -} - - -void NexusDialog::progress(int value) -{ - m_LoadProgress->setValue(value); - m_LoadProgress->setVisible(value != 100); -} - - -void NexusDialog::titleChanged(const QString &title) -{ - NexusView *view = qobject_cast(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 NexusDialog::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 NexusDialog::unsupportedContent(QNetworkReply *reply) -{ - try { - QWebPage *page = qobject_cast(sender()); - if (page == NULL) { - qCritical("sender not a page"); - return; - } - NexusView *view = qobject_cast(page->view()); - if (view == NULL) { - qCritical("no view?"); - return; - } - - int modID = 0; - QString fileName = guessFileName(reply->url().toString()); - qDebug("unsupported: %s - %s", view->url().toString().toUtf8().constData(), reply->url().toString().toUtf8().constData()); - QRegExp sourceExp(QString("http://[a-zA-Z0-9.]*.nexusmods.com/.*"), Qt::CaseInsensitive); - QRegExp modidExp(QString("http://([a-zA-Z0-9]*).nexusmods.com/downloads/file.php\\?id=(\\d+)"), Qt::CaseInsensitive); - QRegExp modidAltExp(QString("http://([a-zA-Z0-9]*).nexusmods.com/mods/(\\d+)"), Qt::CaseInsensitive); - if (sourceExp.indexIn(reply->url().toString()) != -1) { - if (modidExp.indexIn(view->url().toString()) != -1) { - modID = modidExp.cap(2).toInt(); - } else if (modidAltExp.indexIn(view->url().toString()) != -1) { - modID = modidAltExp.cap(2).toInt(); - } else { - modID = view->getLastSeenModID(); - } - } else { - qDebug("not a nexus download: %s", reply->url().toString().toUtf8().constData()); - return; - } - emit requestDownload(reply, modID, fileName); - } catch (const std::exception &e) { - if (isVisible()) { - MessageDialog::showMessage(tr("failed to start download"), this); - } - qCritical("exception downloading unsupported content: %s", e.what()); - } -} - - -void NexusDialog::downloadRequested(const QNetworkRequest &request) -{ - qCritical("download request %s ignored", request.url().toString().toUtf8().constData()); -} - - -void NexusDialog::requestNXMDownload(const QString&) -{ - if (isVisible()) { - MessageDialog::showMessage(tr("Download started"), this); - } -} - - -void NexusDialog::tabCloseRequested(int index) -{ - if (m_Tabs->count() == 1) { - this->close(); - } else { - m_Tabs->widget(index)->deleteLater(); - m_Tabs->removeTab(index); - } -} - - -void NexusDialog::on_browserTabWidget_customContextMenuRequested(const QPoint&) -{ -} - -void NexusDialog::on_backBtn_clicked() -{ - NexusView *currentView = getCurrentView(); - if (currentView != NULL) { - currentView->back(); - } -} - -void NexusDialog::on_fwdBtn_clicked() -{ - NexusView *currentView = getCurrentView(); - if (currentView != NULL) { - currentView->forward(); - } -} - - -void NexusDialog::startSearch() -{ - ui->searchEdit->setFocus(); -} - - -void NexusDialog::on_modIDEdit_returnPressed() -{ - QString url = ToQString(GameInfo::instance().getNexusPage()).append("/downloads/file.php?id=%1").arg(ui->modIDEdit->text()); - NexusView *currentView = getCurrentView(); - if (currentView != NULL) { - currentView->load(QUrl(url)); - } -} - -void NexusDialog::on_searchEdit_returnPressed() -{ - NexusView *currentView = getCurrentView(); - if (currentView != NULL) { - currentView->findText(ui->searchEdit->text(), QWebPage::FindWrapsAroundDocument); - } -} - -void NexusDialog::on_browserTabWidget_currentChanged(QWidget *current) -{ - NexusView *currentView = qobject_cast(current); - if (currentView != NULL) { - ui->backBtn->setEnabled(currentView->history()->canGoBack()); - ui->fwdBtn->setEnabled(currentView->history()->canGoForward()); - } -} - -void NexusDialog::on_refreshBtn_clicked() -{ - getCurrentView()->reload(); -} +#include "nexusdialog.h" +#include "ui_nexusdialog.h" + +#include "messagedialog.h" +#include "report.h" +#include "json.h" + +#include +#include +#include +#include +#include +#include +#include + + +NexusDialog::NexusDialog(NXMAccessManager *accessManager, QWidget *parent) + : QDialog(parent), ui(new Ui::NexusDialog), + m_ModUrlExp(QString("http://([a-zA-Z0-9]*).nexusmods.com/mods/(\\d+)"), Qt::CaseInsensitive), + m_AccessManager(accessManager), + m_Tutorial(this, "NexusDialog") +{ + ui->setupUi(this); + + Qt::WindowFlags flags = windowFlags() | Qt::WindowMaximizeButtonHint | Qt::WindowMinimizeButtonHint; + Qt::WindowFlags helpFlag = Qt::WindowContextHelpButtonHint; + flags = flags & (~helpFlag); + setWindowFlags(flags); + + m_Tabs = this->findChild("browserTabWidget"); + + m_View = new NexusView(this); + + initTab(m_View); + + m_LoadProgress = this->findChild("loadProgress"); + + connect(m_Tabs, SIGNAL(tabCloseRequested(int)), this, SLOT(tabCloseRequested(int))); + m_Tutorial.registerControl(); +} + + +NexusDialog::~NexusDialog() +{ + + delete ui; +} + +void NexusDialog::closeEvent(QCloseEvent *event) +{ +// m_AccessManager->showCookies(); + QDialog::closeEvent(event); +} + +void NexusDialog::initTab(NexusView *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(NexusView*)), this, SLOT(initTab(NexusView*))); + 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*))); + connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(requestNXMDownload(QString))); + + ui->backBtn->setEnabled(false); + ui->fwdBtn->setEnabled(false); + m_Tabs->addTab(newView, tr("new")); + + m_View->settings()->setAttribute(QWebSettings::PluginsEnabled, true); + m_View->settings()->setAttribute(QWebSettings::AutoLoadImages, true); +} + + +void NexusDialog::openInNewTab(const QUrl &url) +{ + NexusView *newView = new NexusView(this); + + initTab(newView); + newView->setUrl(url); +} + + +NexusView *NexusDialog::getCurrentView() +{ + return qobject_cast(m_Tabs->currentWidget()); +} + + +void NexusDialog::urlChanged(const QUrl &url) +{ + NexusView *sendingView = qobject_cast(sender()); + NexusView *currentView = getCurrentView(); + if ((m_ModUrlExp.indexIn(url.toString()) != -1) && + (sendingView == currentView)) { + ui->modIDEdit->setText(m_ModUrlExp.cap(2)); + } + + if (currentView != NULL) { + ui->backBtn->setEnabled(currentView->history()->canGoBack()); + ui->fwdBtn->setEnabled(currentView->history()->canGoForward()); + } +} + + +void NexusDialog::openUrl(const QString &url) +{ + m_Url = url; + if (m_Url.startsWith("www")) { + m_Url.prepend("http://"); + } +} + +void NexusDialog::login(const QString &username, const QString &password) +{ + m_AccessManager->login(username, password); + + connect(m_AccessManager, SIGNAL(loginSuccessful(bool)), this, SLOT(loginFinished(bool))); + connect(m_AccessManager, SIGNAL(loginFailed(QString)), this, SLOT(loginFailed(QString))); +} + + +void NexusDialog::loginFailed(const QString &message) +{ + if (this->isVisible()) { + MessageDialog::showMessage(tr("login failed: %1").arg(message), this); + } +} + + +void NexusDialog::loginFinished(bool necessary) +{ + if (necessary && this->isVisible()) { + MessageDialog::showMessage(tr("login successful"), this); + } + loadNexus(); + emit loginSuccessful(necessary); +} + + +void NexusDialog::loadNexus() +{ + m_View->load(QUrl(m_Url)); +} + + +void NexusDialog::progress(int value) +{ + m_LoadProgress->setValue(value); + m_LoadProgress->setVisible(value != 100); +} + + +void NexusDialog::titleChanged(const QString &title) +{ + NexusView *view = qobject_cast(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 NexusDialog::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 NexusDialog::unsupportedContent(QNetworkReply *reply) +{ + try { + QWebPage *page = qobject_cast(sender()); + if (page == NULL) { + qCritical("sender not a page"); + return; + } + NexusView *view = qobject_cast(page->view()); + if (view == NULL) { + qCritical("no view?"); + return; + } + + int modID = 0; + QString fileName = guessFileName(reply->url().toString()); + qDebug("unsupported: %s - %s", view->url().toString().toUtf8().constData(), reply->url().toString().toUtf8().constData()); + QRegExp sourceExp(QString("http://[a-zA-Z0-9.]*.nexusmods.com/.*"), Qt::CaseInsensitive); + QRegExp modidExp(QString("http://([a-zA-Z0-9]*).nexusmods.com/downloads/file.php\\?id=(\\d+)"), Qt::CaseInsensitive); + QRegExp modidAltExp(QString("http://([a-zA-Z0-9]*).nexusmods.com/mods/(\\d+)"), Qt::CaseInsensitive); + if (sourceExp.indexIn(reply->url().toString()) != -1) { + if (modidExp.indexIn(view->url().toString()) != -1) { + modID = modidExp.cap(2).toInt(); + } else if (modidAltExp.indexIn(view->url().toString()) != -1) { + modID = modidAltExp.cap(2).toInt(); + } else { + modID = view->getLastSeenModID(); + } + } else { + qDebug("not a nexus download: %s", reply->url().toString().toUtf8().constData()); + return; + } + emit requestDownload(reply, modID, fileName); + } catch (const std::exception &e) { + if (isVisible()) { + MessageDialog::showMessage(tr("failed to start download"), this); + } + qCritical("exception downloading unsupported content: %s", e.what()); + } +} + + +void NexusDialog::downloadRequested(const QNetworkRequest &request) +{ + qCritical("download request %s ignored", request.url().toString().toUtf8().constData()); +} + + +void NexusDialog::requestNXMDownload(const QString&) +{ + if (isVisible()) { + MessageDialog::showMessage(tr("Download started"), this); + } +} + + +void NexusDialog::tabCloseRequested(int index) +{ + if (m_Tabs->count() == 1) { + this->close(); + } else { + m_Tabs->widget(index)->deleteLater(); + m_Tabs->removeTab(index); + } +} + + +void NexusDialog::on_browserTabWidget_customContextMenuRequested(const QPoint&) +{ +} + +void NexusDialog::on_backBtn_clicked() +{ + NexusView *currentView = getCurrentView(); + if (currentView != NULL) { + currentView->back(); + } +} + +void NexusDialog::on_fwdBtn_clicked() +{ + NexusView *currentView = getCurrentView(); + if (currentView != NULL) { + currentView->forward(); + } +} + + +void NexusDialog::startSearch() +{ + ui->searchEdit->setFocus(); +} + + +void NexusDialog::on_modIDEdit_returnPressed() +{ + QString url = ToQString(GameInfo::instance().getNexusPage()).append("/downloads/file.php?id=%1").arg(ui->modIDEdit->text()); + NexusView *currentView = getCurrentView(); + if (currentView != NULL) { + currentView->load(QUrl(url)); + } +} + +void NexusDialog::on_searchEdit_returnPressed() +{ + NexusView *currentView = getCurrentView(); + if (currentView != NULL) { + currentView->findText(ui->searchEdit->text(), QWebPage::FindWrapsAroundDocument); + } +} + +void NexusDialog::on_browserTabWidget_currentChanged(QWidget *current) +{ + NexusView *currentView = qobject_cast(current); + if (currentView != NULL) { + ui->backBtn->setEnabled(currentView->history()->canGoBack()); + ui->fwdBtn->setEnabled(currentView->history()->canGoForward()); + } + + if (m_ModUrlExp.indexIn(currentView->url().toString()) != -1) { + ui->modIDEdit->setText(m_ModUrlExp.cap(2)); + } +} + +void NexusDialog::on_refreshBtn_clicked() +{ + getCurrentView()->reload(); +} diff --git a/src/nexusdialog.h b/src/nexusdialog.h index 041b5e7c..fbe99f3d 100644 --- a/src/nexusdialog.h +++ b/src/nexusdialog.h @@ -17,144 +17,145 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#ifndef NEXUSDIALOG_H -#define NEXUSDIALOG_H - -#include "nexusview.h" -#include "nxmaccessmanager.h" -#include "tutorialcontrol.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include - - -namespace Ui { - class NexusDialog; -} - - -/** - * @brief a dialog containing a webbrowser that is intended to browse the nexus network - **/ -class NexusDialog : public QDialog -{ - Q_OBJECT - -public: - - /** - * @brief constructor - * - * @param accessManager the access manager to use for network requests - * @param parent parent widget - **/ - explicit NexusDialog(NXMAccessManager *accessManager, QWidget *parent = 0); - ~NexusDialog(); - - /** - * @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 QString &url); - - /** - * @brief log-in to the nexus page. - * - * After successful login, loadNexus() is automatically called. If the user is already - * logged in, this happens immediately - * - * @param username the user name to log in as - * @param password the user password - **/ - void login(const QString &username, const QString &password); - - /** - * @brief load the page set with openUrl() - **/ - void loadNexus(); - -signals: - - /** - * @brief emitted when the user caused a download - * - * @param reply the network-reply transmitting the file - * @param modID mod id of the file requested - * @param fileName suggested filename - **/ - void requestDownload(QNetworkReply *reply, int modID, const QString &fileName); - - void loginSuccessful(bool necessary); - -protected: - - virtual void closeEvent(QCloseEvent *); - -private slots: - - void loginFailed(const QString &message); - void loginFinished(bool necessary); -// void loginError(QNetworkReply::NetworkError errorCode); -// void loginTimeout(); - - void initTab(NexusView *newView); - void openInNewTab(const QUrl &url); - - void progress(int value); - - void titleChanged(const QString &title); - void requestNXMDownload(const QString &url); - void unsupportedContent(QNetworkReply *reply); - void downloadRequested(const QNetworkRequest &request); - - - void tabCloseRequested(int index); - -// void openByModID(); - void on_browserTabWidget_customContextMenuRequested(const QPoint &pos); - - void urlChanged(const QUrl &url); - - void on_backBtn_clicked(); - - void on_fwdBtn_clicked(); - - void on_modIDEdit_returnPressed(); - - void on_searchEdit_returnPressed(); - - void startSearch(); - - void on_browserTabWidget_currentChanged(QWidget *arg1); - - void on_refreshBtn_clicked(); - -private: - - QString guessFileName(const QString &url); - - NexusView *getCurrentView(); - -private: - - Ui::NexusDialog *ui; - - TutorialControl m_Tutorial; - - QString m_Url; - - QTabWidget *m_Tabs; - NexusView *m_View; - QProgressBar *m_LoadProgress; - NXMAccessManager *m_AccessManager; - -}; - -#endif // NEXUSDIALOG_H +#ifndef NEXUSDIALOG_H +#define NEXUSDIALOG_H + +#include "nexusview.h" +#include "nxmaccessmanager.h" +#include "tutorialcontrol.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + + +namespace Ui { + class NexusDialog; +} + + +/** + * @brief a dialog containing a webbrowser that is intended to browse the nexus network + **/ +class NexusDialog : public QDialog +{ + Q_OBJECT + +public: + + /** + * @brief constructor + * + * @param accessManager the access manager to use for network requests + * @param parent parent widget + **/ + explicit NexusDialog(NXMAccessManager *accessManager, QWidget *parent = 0); + ~NexusDialog(); + + /** + * @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 QString &url); + + /** + * @brief log-in to the nexus page. + * + * After successful login, loadNexus() is automatically called. If the user is already + * logged in, this happens immediately + * + * @param username the user name to log in as + * @param password the user password + **/ + void login(const QString &username, const QString &password); + + /** + * @brief load the page set with openUrl() + **/ + void loadNexus(); + +signals: + + /** + * @brief emitted when the user caused a download + * + * @param reply the network-reply transmitting the file + * @param modID mod id of the file requested + * @param fileName suggested filename + **/ + void requestDownload(QNetworkReply *reply, int modID, const QString &fileName); + + void loginSuccessful(bool necessary); + +protected: + + virtual void closeEvent(QCloseEvent *); + +private slots: + + void loginFailed(const QString &message); + void loginFinished(bool necessary); +// void loginError(QNetworkReply::NetworkError errorCode); +// void loginTimeout(); + + void initTab(NexusView *newView); + void openInNewTab(const QUrl &url); + + void progress(int value); + + void titleChanged(const QString &title); + void requestNXMDownload(const QString &url); + void unsupportedContent(QNetworkReply *reply); + void downloadRequested(const QNetworkRequest &request); + + + void tabCloseRequested(int index); + +// void openByModID(); + void on_browserTabWidget_customContextMenuRequested(const QPoint &pos); + + void urlChanged(const QUrl &url); + + void on_backBtn_clicked(); + + void on_fwdBtn_clicked(); + + void on_modIDEdit_returnPressed(); + + void on_searchEdit_returnPressed(); + + void startSearch(); + + void on_browserTabWidget_currentChanged(QWidget *arg1); + + void on_refreshBtn_clicked(); + +private: + + QString guessFileName(const QString &url); + + NexusView *getCurrentView(); + +private: + + Ui::NexusDialog *ui; + + TutorialControl m_Tutorial; + + QString m_Url; + QRegExp m_ModUrlExp; + + QTabWidget *m_Tabs; + NexusView *m_View; + QProgressBar *m_LoadProgress; + NXMAccessManager *m_AccessManager; + +}; + +#endif // NEXUSDIALOG_H diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index c250a7d2..ad8c529d 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -17,450 +17,459 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include "nexusinterface.h" -#include "nxmaccessmanager.h" -#include "utility.h" -#include "json.h" -#include "selectiondialog.h" -#include -#include - -using QtJson::Json; - - -NexusBridge::NexusBridge() -{ - m_Interface = NexusInterface::instance(); -} - - -void NexusBridge::requestDescription(int modID, QVariant userData, const QString &url) -{ - m_RequestIDs.insert(m_Interface->requestDescription(modID, this, userData, url)); -} - -void NexusBridge::requestFiles(int modID, QVariant userData, const QString &url) -{ - m_RequestIDs.insert(m_Interface->requestFiles(modID, this, userData, url)); -} - -void NexusBridge::requestFileInfo(int modID, int fileID, QVariant userData, const QString &url) -{ - m_RequestIDs.insert(m_Interface->requestFileInfo(modID, fileID, this, userData, url)); -} - -void NexusBridge::requestDownloadURL(int modID, int fileID, QVariant userData, const QString &url) -{ - m_RequestIDs.insert(m_Interface->requestDownloadURL(modID, fileID, this, userData, url)); -} - -void NexusBridge::requestToggleEndorsement(int modID, bool endorse, QVariant userData, const QString &url) -{ - m_RequestIDs.insert(m_Interface->requestToggleEndorsement(modID, endorse, this, userData, url)); -} - -void NexusBridge::nxmDescriptionAvailable(int modID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - emit nxmDescriptionAvailable(modID, userData, resultData); - } -} - -void NexusBridge::nxmFilesAvailable(int modID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - emit nxmFilesAvailable(modID, userData, resultData); - } -} - -void NexusBridge::nxmFileInfoAvailable(int modID, int fileID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - emit nxmFileInfoAvailable(modID, fileID, userData, resultData); - } -} - -void NexusBridge::nxmDownloadURLsAvailable(int modID, int fileID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - emit nxmDownloadURLsAvailable(modID, fileID, userData, resultData); - } -} - -void NexusBridge::nxmEndorsementToggled(int modID, QVariant userData, QVariant resultData, int requestID) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - emit nxmEndorsementToggled(modID, userData, resultData); - } -} - -void NexusBridge::nxmRequestFailed(int modID, QVariant userData, int requestID, const QString &errorMessage) -{ - std::set::iterator iter = m_RequestIDs.find(requestID); - if (iter != m_RequestIDs.end()) { - m_RequestIDs.erase(iter); - emit nxmRequestFailed(modID, userData, errorMessage); - } -} - - -QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0); - - -NexusInterface::NexusInterface() - : m_NMMVersion() -{ - m_AccessManager = new NXMAccessManager(this); - - m_DiskCache = new QNetworkDiskCache(this); - connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); -} - - -NXMAccessManager *NexusInterface::getAccessManager() -{ - return m_AccessManager; -} - - -NexusInterface *NexusInterface::s_Instance = NULL; - - -NexusInterface *NexusInterface::instance() -{ - if (s_Instance == NULL) { - s_Instance = new NexusInterface; - } - return s_Instance; -} - - -void NexusInterface::setCacheDirectory(const QString &directory) -{ - m_DiskCache->setCacheDirectory(directory); - m_AccessManager->setCache(m_DiskCache); -} - - -void NexusInterface::setNMMVersion(const QString &nmmVersion) -{ - m_NMMVersion = nmmVersion; -} - - -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]+).*"); -// std::tr1::match_results result; - QByteArray fileNameUTF8 = fileName.toUtf8(); - std::tr1::cmatch result; - if (std::tr1::regex_search(fileNameUTF8.constData(), result, exp)) { - modName = QString::fromUtf8(result[1].str().c_str()); - modName = modName.replace('_', ' ').trimmed(); - - std::string candidate = result[3].str(); - std::string candidate2 = result[2].str(); - if (candidate2.length() != 0 && (candidate2.find_last_of("VvRr") == std::string::npos)) { - // well, that second match might be an id too... - unsigned offset = strspn(candidate2.c_str(), "-_ "); - if (offset < candidate2.length() && query) { - SelectionDialog selection(tr("Failed to guess mod id for \"%1\", please pick the correct one").arg(fileName)); - QString r2Highlight(fileName); - r2Highlight.insert(result.position(2) + result.length(2), "* ").insert(result.position(2) + offset, " *"); - QString r3Highlight(fileName); - r3Highlight.insert(result.position(3) + result.length(3), "* ").insert(result.position(3), " *"); - - selection.addChoice(candidate.c_str(), r3Highlight, strtol(candidate.c_str(), NULL, 10)); - selection.addChoice(candidate2.c_str() + offset, r2Highlight, abs(strtol(candidate2.c_str() + offset, NULL, 10))); - if (selection.exec() == QDialog::Accepted) { - modID = selection.getChoiceData().toInt(); - } else { - modID = -1; - } - } else { - modID = -1; - } - } else { - modID = strtol(candidate.c_str(), NULL, 10); - } - qDebug("mod id guessed: %s -> %d", qPrintable(fileName), modID); - } else { - modName.clear(); - modID = -1; - } -} - - -int NexusInterface::requestDescription(int modID, QObject *receiver, QVariant userData, const QString &url) -{ - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_DESCRIPTION, userData, url); - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmDescriptionAvailable(int,QVariant,QVariant,int)), - receiver, SLOT(nxmDescriptionAvailable(int,QVariant,QVariant,int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(int,QVariant,int,QString)), - receiver, SLOT(nxmRequestFailed(int,QVariant,int,QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -int NexusInterface::requestUpdates(const std::vector &modIDs, QObject *receiver, QVariant userData, const QString &url) -{ - NXMRequestInfo requestInfo(modIDs, NXMRequestInfo::TYPE_GETUPDATES, userData, url); - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmUpdatesAvailable(std::vector,QVariant,QVariant,int)), - receiver, SLOT(nxmUpdatesAvailable(std::vector,QVariant,QVariant,int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(int,QVariant,int,QString)), - receiver, SLOT(nxmRequestFailed(int,QVariant,int,QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -int NexusInterface::requestFiles(int modID, QObject *receiver, QVariant userData, const QString &url) -{ - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_FILES, userData, url); - m_RequestQueue.enqueue(requestInfo); - connect(this, SIGNAL(nxmFilesAvailable(int,QVariant,QVariant,int)), - receiver, SLOT(nxmFilesAvailable(int,QVariant,QVariant,int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(int,QVariant,int,QString)), - receiver, SLOT(nxmRequestFailed(int,QVariant,int,QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -int NexusInterface::requestFileInfo(int modID, int fileID, QObject *receiver, QVariant userData, const QString &url) -{ - NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_FILEINFO, userData, url); - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmFileInfoAvailable(int,int,QVariant,QVariant,int)), - receiver, SLOT(nxmFileInfoAvailable(int,int,QVariant,QVariant,int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(int,QVariant,int,QString)), - receiver, SLOT(nxmRequestFailed(int,QVariant,int,QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -int NexusInterface::requestDownloadURL(int modID, int fileID, QObject *receiver, QVariant userData, const QString &url) -{ - NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_DOWNLOADURL, userData, url); - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmDownloadURLsAvailable(int,int,QVariant,QVariant,int)), - receiver, SLOT(nxmDownloadURLsAvailable(int,int,QVariant,QVariant,int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(int,QVariant,int,QString)), - receiver, SLOT(nxmRequestFailed(int,QVariant,int,QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -int NexusInterface::requestToggleEndorsement(int modID, bool endorse, QObject *receiver, QVariant userData, const QString &url) -{ - NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, url); - requestInfo.m_Endorse = endorse; - m_RequestQueue.enqueue(requestInfo); - - connect(this, SIGNAL(nxmEndorsementToggled(int,QVariant,QVariant,int)), - receiver, SLOT(nxmEndorsementToggled(int,QVariant,QVariant,int)), Qt::UniqueConnection); - - connect(this, SIGNAL(nxmRequestFailed(int,QVariant,int,QString)), - receiver, SLOT(nxmRequestFailed(int,QVariant,int,QString)), Qt::UniqueConnection); - - nextRequest(); - return requestInfo.m_ID; -} - - -void NexusInterface::nextRequest() -{ - if ((m_ActiveRequest.size() >= MAX_ACTIVE_DOWNLOADS) || (m_RequestQueue.isEmpty())) { - return; - } - - NXMRequestInfo info = m_RequestQueue.dequeue(); - info.m_Timeout = new QTimer(this); - info.m_Timeout->setInterval(60000); - - QString url; - switch (info.m_Type) { - case NXMRequestInfo::TYPE_DESCRIPTION: { - url = QString("%1/Mods/%2/").arg(info.m_URL).arg(info.m_ModID); - } break; - case NXMRequestInfo::TYPE_FILES: { - url = QString("%1/Files/indexfrommod/%2/").arg(info.m_URL).arg(info.m_ModID); - } break; - case NXMRequestInfo::TYPE_FILEINFO: { - url = QString("%1/Files/%2/").arg(info.m_URL).arg(info.m_FileID); - } break; - case NXMRequestInfo::TYPE_DOWNLOADURL: { - url = QString("%1/Files/download/%2").arg(info.m_URL).arg(info.m_FileID); - } break; - case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { - url = QString("%1/Mods/toggleendorsement/%2?lvote=%3").arg(info.m_URL).arg(info.m_ModID).arg(!info.m_Endorse); - } break; - case NXMRequestInfo::TYPE_GETUPDATES: { - QString modIDList = VectorJoin(info.m_ModIDList, ","); - modIDList = "[" + modIDList + "]"; - url = QString("%1/Mods/GetUpdates?ModList=%2").arg(info.m_URL).arg(modIDList); - } break; - } - -/* - /// - /// Finds the mods containing the given search terms. - /// - /// The terms to use to search for mods. - /// Whether the returned mods' names should include all of - /// the given search terms, or any of the terms. - /// The mod info for the mods matching the given search criteria. - [OperationContract] - [WebGet( - BodyStyle = WebMessageBodyStyle.Bare, - UriTemplate = "Mods/?Find&name={p_strModNameSearchString}&type={p_strType}", - ResponseFormat = WebMessageFormat.Json)] - List FindMods(string p_strModNameSearchString, string p_strType); - - - -*/ - - - QNetworkRequest request(url); - request.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml"); - request.setRawHeader("User-Agent", QString("Mod Organizer v0.12.0 (compatible to Nexus Client v%1)").arg(m_NMMVersion).toUtf8()); - - info.m_Reply = m_AccessManager->get(request); - - connect(info.m_Reply, SIGNAL(finished()), this, SLOT(requestFinished())); - connect(info.m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); - connect(info.m_Timeout, SIGNAL(timeout()), this, SLOT(requestTimeout())); - info.m_Timeout->start(); - m_ActiveRequest.push_back(info); -} - - -void NexusInterface::downloadRequestedNXM(const QString &url) -{ - emit requestNXMDownload(url); -} - - -void NexusInterface::requestFinished(std::list::iterator iter) -{ - QNetworkReply *reply = iter->m_Reply; - - if (reply->error() != QNetworkReply::NoError) { - qWarning("request failed: %s", reply->errorString().toUtf8().constData()); - emit nxmRequestFailed(iter->m_ModID, iter->m_UserData, iter->m_ID, reply->errorString()); - } else { - QByteArray data = reply->readAll(); - if (data.isNull() || data.isEmpty() || (strcmp(data.constData(), "null") == 0)) { - QString nexusError(reply->rawHeader("NexusErrorInfo")); - if (nexusError.length() == 0) { - nexusError = tr("empty response"); - } - - emit nxmRequestFailed(iter->m_ModID, iter->m_UserData, iter->m_ID, nexusError); - } else { - bool ok; - QVariant result = Json::parse(data, ok); - if (result.isValid() && ok) { - switch (iter->m_Type) { - case NXMRequestInfo::TYPE_DESCRIPTION: { - emit nxmDescriptionAvailable(iter->m_ModID, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_FILES: { - emit nxmFilesAvailable(iter->m_ModID, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_FILEINFO: { - emit nxmFileInfoAvailable(iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_DOWNLOADURL: { - emit nxmDownloadURLsAvailable(iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_GETUPDATES: { - emit nxmUpdatesAvailable(iter->m_ModIDList, iter->m_UserData, result, iter->m_ID); - } break; - case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { - emit nxmEndorsementToggled(iter->m_ModID, iter->m_UserData, result, iter->m_ID); - } break; - } - } else { - emit nxmRequestFailed(iter->m_ModID, iter->m_UserData, iter->m_ID, tr("invalid response")); - } - } - } -} - - -void NexusInterface::requestFinished() -{ - QNetworkReply *reply = static_cast(sender()); - for (std::list::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) { - if (iter->m_Reply == reply) { - iter->m_Timeout->stop(); - iter->m_Timeout->deleteLater(); - requestFinished(iter); - iter->m_Reply->deleteLater(); - m_ActiveRequest.erase(iter); - nextRequest(); - return; - } - } -} - - -void NexusInterface::requestError(QNetworkReply::NetworkError) -{ - QNetworkReply *reply = qobject_cast(sender()); - if (reply == NULL) { - qWarning("invalid sender type"); - return; - } - - qCritical("request error: %s", reply->errorString().toUtf8().constData()); -} - - -void NexusInterface::requestTimeout() -{ - QTimer *timer = qobject_cast(sender()); - if (timer == NULL) { - qWarning("invalid sender type"); - return; - } - qWarning("request timeout"); - for (std::list::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) { - if (iter->m_Timeout == timer) { - // this abort causes a "request failed" which cleans up the rest - iter->m_Reply->abort(); - return; - } - } -} +#include "nexusinterface.h" +#include "nxmaccessmanager.h" +#include "utility.h" +#include "json.h" +#include "selectiondialog.h" +#include +#include + +using QtJson::Json; + + +NexusBridge::NexusBridge() +{ + m_Interface = NexusInterface::instance(); +} + + +void NexusBridge::requestDescription(int modID, QVariant userData, const QString &url) +{ + m_RequestIDs.insert(m_Interface->requestDescription(modID, this, userData, url)); +} + +void NexusBridge::requestFiles(int modID, QVariant userData, const QString &url) +{ + m_RequestIDs.insert(m_Interface->requestFiles(modID, this, userData, url)); +} + +void NexusBridge::requestFileInfo(int modID, int fileID, QVariant userData, const QString &url) +{ + m_RequestIDs.insert(m_Interface->requestFileInfo(modID, fileID, this, userData, url)); +} + +void NexusBridge::requestDownloadURL(int modID, int fileID, QVariant userData, const QString &url) +{ + m_RequestIDs.insert(m_Interface->requestDownloadURL(modID, fileID, this, userData, url)); +} + +void NexusBridge::requestToggleEndorsement(int modID, bool endorse, QVariant userData, const QString &url) +{ + m_RequestIDs.insert(m_Interface->requestToggleEndorsement(modID, endorse, this, userData, url)); +} + +void NexusBridge::nxmDescriptionAvailable(int modID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit nxmDescriptionAvailable(modID, userData, resultData); + } +} + +void NexusBridge::nxmFilesAvailable(int modID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit nxmFilesAvailable(modID, userData, resultData); + } +} + +void NexusBridge::nxmFileInfoAvailable(int modID, int fileID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit nxmFileInfoAvailable(modID, fileID, userData, resultData); + } +} + +void NexusBridge::nxmDownloadURLsAvailable(int modID, int fileID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit nxmDownloadURLsAvailable(modID, fileID, userData, resultData); + } +} + +void NexusBridge::nxmEndorsementToggled(int modID, QVariant userData, QVariant resultData, int requestID) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit nxmEndorsementToggled(modID, userData, resultData); + } +} + +void NexusBridge::nxmRequestFailed(int modID, QVariant userData, int requestID, const QString &errorMessage) +{ + std::set::iterator iter = m_RequestIDs.find(requestID); + if (iter != m_RequestIDs.end()) { + m_RequestIDs.erase(iter); + emit nxmRequestFailed(modID, userData, errorMessage); + } +} + + +QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0); + + +NexusInterface::NexusInterface() + : m_NMMVersion() +{ + m_AccessManager = new NXMAccessManager(this); + + m_DiskCache = new QNetworkDiskCache(this); + connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString))); +} + + +NXMAccessManager *NexusInterface::getAccessManager() +{ + return m_AccessManager; +} + + +NexusInterface *NexusInterface::s_Instance = NULL; + + +NexusInterface *NexusInterface::instance() +{ + if (s_Instance == NULL) { + s_Instance = new NexusInterface; + } + return s_Instance; +} + + +void NexusInterface::setCacheDirectory(const QString &directory) +{ + m_DiskCache->setCacheDirectory(directory); + m_AccessManager->setCache(m_DiskCache); +} + + +void NexusInterface::setNMMVersion(const QString &nmmVersion) +{ + m_NMMVersion = nmmVersion; +} + + +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 result; + QByteArray fileNameUTF8 = fileName.toUtf8(); + std::tr1::cmatch result; + if (std::tr1::regex_search(fileNameUTF8.constData(), result, exp)) { + modName = QString::fromUtf8(result[1].str().c_str()); + modName = modName.replace('_', ' ').trimmed(); + + std::string candidate = result[3].str(); + std::string candidate2 = result[2].str(); + if (candidate2.length() != 0 && (candidate2.find_last_of("VvRr") == std::string::npos)) { + // well, that second match might be an id too... + unsigned offset = strspn(candidate2.c_str(), "-_ "); + if (offset < candidate2.length() && query) { + SelectionDialog selection(tr("Failed to guess mod id for \"%1\", please pick the correct one").arg(fileName)); + QString r2Highlight(fileName); + r2Highlight.insert(result.position(2) + result.length(2), "* ").insert(result.position(2) + offset, " *"); + QString r3Highlight(fileName); + r3Highlight.insert(result.position(3) + result.length(3), "* ").insert(result.position(3), " *"); + + selection.addChoice(candidate.c_str(), r3Highlight, strtol(candidate.c_str(), NULL, 10)); + selection.addChoice(candidate2.c_str() + offset, r2Highlight, abs(strtol(candidate2.c_str() + offset, NULL, 10))); + if (selection.exec() == QDialog::Accepted) { + modID = selection.getChoiceData().toInt(); + } else { + modID = -1; + } + } else { + modID = -1; + } + } else { + modID = strtol(candidate.c_str(), NULL, 10); + } + qDebug("mod id guessed: %s -> %d", qPrintable(fileName), modID); + } else if (std::tr1::regex_search(fileNameUTF8.constData(), result, simpleexp)) { + qDebug("simple expression matched, using name only"); + modName = QString::fromUtf8(result[1].str().c_str()); + modName = modName.replace('_', ' ').trimmed(); + + modID = -1; + } else { + qDebug("no expression matched!"); + modName.clear(); + modID = -1; + } +} + + +int NexusInterface::requestDescription(int modID, QObject *receiver, QVariant userData, const QString &url) +{ + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_DESCRIPTION, userData, url); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmDescriptionAvailable(int,QVariant,QVariant,int)), + receiver, SLOT(nxmDescriptionAvailable(int,QVariant,QVariant,int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(int,QVariant,int,QString)), + receiver, SLOT(nxmRequestFailed(int,QVariant,int,QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +int NexusInterface::requestUpdates(const std::vector &modIDs, QObject *receiver, QVariant userData, const QString &url) +{ + NXMRequestInfo requestInfo(modIDs, NXMRequestInfo::TYPE_GETUPDATES, userData, url); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmUpdatesAvailable(std::vector,QVariant,QVariant,int)), + receiver, SLOT(nxmUpdatesAvailable(std::vector,QVariant,QVariant,int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(int,QVariant,int,QString)), + receiver, SLOT(nxmRequestFailed(int,QVariant,int,QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +int NexusInterface::requestFiles(int modID, QObject *receiver, QVariant userData, const QString &url) +{ + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_FILES, userData, url); + m_RequestQueue.enqueue(requestInfo); + connect(this, SIGNAL(nxmFilesAvailable(int,QVariant,QVariant,int)), + receiver, SLOT(nxmFilesAvailable(int,QVariant,QVariant,int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(int,QVariant,int,QString)), + receiver, SLOT(nxmRequestFailed(int,QVariant,int,QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +int NexusInterface::requestFileInfo(int modID, int fileID, QObject *receiver, QVariant userData, const QString &url) +{ + NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_FILEINFO, userData, url); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmFileInfoAvailable(int,int,QVariant,QVariant,int)), + receiver, SLOT(nxmFileInfoAvailable(int,int,QVariant,QVariant,int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(int,QVariant,int,QString)), + receiver, SLOT(nxmRequestFailed(int,QVariant,int,QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +int NexusInterface::requestDownloadURL(int modID, int fileID, QObject *receiver, QVariant userData, const QString &url) +{ + NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_DOWNLOADURL, userData, url); + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmDownloadURLsAvailable(int,int,QVariant,QVariant,int)), + receiver, SLOT(nxmDownloadURLsAvailable(int,int,QVariant,QVariant,int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(int,QVariant,int,QString)), + receiver, SLOT(nxmRequestFailed(int,QVariant,int,QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +int NexusInterface::requestToggleEndorsement(int modID, bool endorse, QObject *receiver, QVariant userData, const QString &url) +{ + NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, url); + requestInfo.m_Endorse = endorse; + m_RequestQueue.enqueue(requestInfo); + + connect(this, SIGNAL(nxmEndorsementToggled(int,QVariant,QVariant,int)), + receiver, SLOT(nxmEndorsementToggled(int,QVariant,QVariant,int)), Qt::UniqueConnection); + + connect(this, SIGNAL(nxmRequestFailed(int,QVariant,int,QString)), + receiver, SLOT(nxmRequestFailed(int,QVariant,int,QString)), Qt::UniqueConnection); + + nextRequest(); + return requestInfo.m_ID; +} + + +void NexusInterface::nextRequest() +{ + if ((m_ActiveRequest.size() >= MAX_ACTIVE_DOWNLOADS) || (m_RequestQueue.isEmpty())) { + return; + } + + NXMRequestInfo info = m_RequestQueue.dequeue(); + info.m_Timeout = new QTimer(this); + info.m_Timeout->setInterval(60000); + + QString url; + switch (info.m_Type) { + case NXMRequestInfo::TYPE_DESCRIPTION: { + url = QString("%1/Mods/%2/").arg(info.m_URL).arg(info.m_ModID); + } break; + case NXMRequestInfo::TYPE_FILES: { + url = QString("%1/Files/indexfrommod/%2/").arg(info.m_URL).arg(info.m_ModID); + } break; + case NXMRequestInfo::TYPE_FILEINFO: { + url = QString("%1/Files/%2/").arg(info.m_URL).arg(info.m_FileID); + } break; + case NXMRequestInfo::TYPE_DOWNLOADURL: { + url = QString("%1/Files/download/%2").arg(info.m_URL).arg(info.m_FileID); + } break; + case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { + url = QString("%1/Mods/toggleendorsement/%2?lvote=%3").arg(info.m_URL).arg(info.m_ModID).arg(!info.m_Endorse); + } break; + case NXMRequestInfo::TYPE_GETUPDATES: { + QString modIDList = VectorJoin(info.m_ModIDList, ","); + modIDList = "[" + modIDList + "]"; + url = QString("%1/Mods/GetUpdates?ModList=%2").arg(info.m_URL).arg(modIDList); + } break; + } + +/* + /// + /// Finds the mods containing the given search terms. + /// + /// The terms to use to search for mods. + /// Whether the returned mods' names should include all of + /// the given search terms, or any of the terms. + /// The mod info for the mods matching the given search criteria. + [OperationContract] + [WebGet( + BodyStyle = WebMessageBodyStyle.Bare, + UriTemplate = "Mods/?Find&name={p_strModNameSearchString}&type={p_strType}", + ResponseFormat = WebMessageFormat.Json)] + List FindMods(string p_strModNameSearchString, string p_strType); + + + +*/ + + + QNetworkRequest request(url); + request.setHeader(QNetworkRequest::ContentTypeHeader, "application/xml"); + request.setRawHeader("User-Agent", QString("Mod Organizer v0.12.0 (compatible to Nexus Client v%1)").arg(m_NMMVersion).toUtf8()); + + info.m_Reply = m_AccessManager->get(request); + + connect(info.m_Reply, SIGNAL(finished()), this, SLOT(requestFinished())); + connect(info.m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError))); + connect(info.m_Timeout, SIGNAL(timeout()), this, SLOT(requestTimeout())); + info.m_Timeout->start(); + m_ActiveRequest.push_back(info); +} + + +void NexusInterface::downloadRequestedNXM(const QString &url) +{ + emit requestNXMDownload(url); +} + + +void NexusInterface::requestFinished(std::list::iterator iter) +{ + QNetworkReply *reply = iter->m_Reply; + + if (reply->error() != QNetworkReply::NoError) { + qWarning("request failed: %s", reply->errorString().toUtf8().constData()); + emit nxmRequestFailed(iter->m_ModID, iter->m_UserData, iter->m_ID, reply->errorString()); + } else { + QByteArray data = reply->readAll(); + if (data.isNull() || data.isEmpty() || (strcmp(data.constData(), "null") == 0)) { + QString nexusError(reply->rawHeader("NexusErrorInfo")); + if (nexusError.length() == 0) { + nexusError = tr("empty response"); + } + + emit nxmRequestFailed(iter->m_ModID, iter->m_UserData, iter->m_ID, nexusError); + } else { + bool ok; + QVariant result = Json::parse(data, ok); + if (result.isValid() && ok) { + switch (iter->m_Type) { + case NXMRequestInfo::TYPE_DESCRIPTION: { + emit nxmDescriptionAvailable(iter->m_ModID, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_FILES: { + emit nxmFilesAvailable(iter->m_ModID, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_FILEINFO: { + emit nxmFileInfoAvailable(iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_DOWNLOADURL: { + emit nxmDownloadURLsAvailable(iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_GETUPDATES: { + emit nxmUpdatesAvailable(iter->m_ModIDList, iter->m_UserData, result, iter->m_ID); + } break; + case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: { + emit nxmEndorsementToggled(iter->m_ModID, iter->m_UserData, result, iter->m_ID); + } break; + } + } else { + emit nxmRequestFailed(iter->m_ModID, iter->m_UserData, iter->m_ID, tr("invalid response")); + } + } + } +} + + +void NexusInterface::requestFinished() +{ + QNetworkReply *reply = static_cast(sender()); + for (std::list::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) { + if (iter->m_Reply == reply) { + iter->m_Timeout->stop(); + iter->m_Timeout->deleteLater(); + requestFinished(iter); + iter->m_Reply->deleteLater(); + m_ActiveRequest.erase(iter); + nextRequest(); + return; + } + } +} + + +void NexusInterface::requestError(QNetworkReply::NetworkError) +{ + QNetworkReply *reply = qobject_cast(sender()); + if (reply == NULL) { + qWarning("invalid sender type"); + return; + } + + qCritical("request error: %s", reply->errorString().toUtf8().constData()); +} + + +void NexusInterface::requestTimeout() +{ + QTimer *timer = qobject_cast(sender()); + if (timer == NULL) { + qWarning("invalid sender type"); + return; + } + qWarning("request timeout"); + for (std::list::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) { + if (iter->m_Timeout == timer) { + // this abort causes a "request failed" which cleans up the rest + iter->m_Reply->abort(); + return; + } + } +} diff --git a/src/nexusview.cpp b/src/nexusview.cpp index 1099fff2..c8c7f09d 100644 --- a/src/nexusview.cpp +++ b/src/nexusview.cpp @@ -17,71 +17,102 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include "nexusview.h" - -#include -#include -#include -#include -#include -#include -#include -#include "utility.h" - -NexusView::NexusView(QWidget *parent) - : QWebView(parent), m_LastSeenModID(0) -{ - installEventFilter(this); -// this->pageAction(QWebPage::OpenLinkInNewWindow) - connect(this, SIGNAL(urlChanged(QUrl)), this, SLOT(urlChanged(QUrl))); - - page()->settings()->setMaximumPagesInCache(10); -} - - -void NexusView::urlChanged(const QUrl &url) -{ - QRegExp modidExp(QString("http://([a-zA-Z0-9]*).nexusmods.com/downloads/file.php\\?id=(\\d+)"), Qt::CaseInsensitive); - if (modidExp.indexIn(url.toString()) != -1) { - m_LastSeenModID = modidExp.cap(2).toInt(); - } -} - - -QWebView *NexusView::createWindow(QWebPage::WebWindowType) -{ - NexusView *newView = new NexusView(parentWidget()); - emit initTab(newView); - return newView; -} - - -bool NexusView::eventFilter(QObject *obj, QEvent *event) -{ - if (event->type() == QEvent::ShortcutOverride) { - QKeyEvent *keyEvent = static_cast(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(event); - if (mouseEvent->button() == Qt::MidButton) { - mouseEvent->ignore(); - return true; - } - } else if (event->type() == QEvent::MouseButtonRelease) { - QMouseEvent *mouseEvent = static_cast(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); -} +#include "nexusview.h" + +#include +#include +#include +#include +#include +#include +#include +#include "utility.h" + +NexusView::NexusView(QWidget *parent) + : QWebView(parent), m_LastSeenModID(0) +{ + installEventFilter(this); +// this->pageAction(QWebPage::OpenLinkInNewWindow) + connect(this, SIGNAL(urlChanged(QUrl)), this, SLOT(urlChanged(QUrl))); + + page()->settings()->setMaximumPagesInCache(10); +} + + +void NexusView::urlChanged(const QUrl &url) +{ + QRegExp modidExp(QString("http://([a-zA-Z0-9]*).nexusmods.com/downloads/file.php\\?id=(\\d+)"), Qt::CaseInsensitive); + if (modidExp.indexIn(url.toString()) != -1) { + m_LastSeenModID = modidExp.cap(2).toInt(); + } +} + +void NexusView::openPageExternal() +{ + ::ShellExecuteW(NULL, L"open", ToWString(url().toString()).c_str(), NULL, NULL, SW_SHOWNORMAL); +} + +void NexusView::openLinkExternal() +{ + ::ShellExecuteW(NULL, L"open", ToWString(m_ContextURL.toString()).c_str(), NULL, NULL, SW_SHOWNORMAL); +} + + +QWebView *NexusView::createWindow(QWebPage::WebWindowType) +{ + NexusView *newView = new NexusView(parentWidget()); + emit initTab(newView); + return newView; +} + + +bool NexusView::eventFilter(QObject *obj, QEvent *event) +{ + if (event->type() == QEvent::ShortcutOverride) { + QKeyEvent *keyEvent = static_cast(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(event); + if (mouseEvent->button() == Qt::MidButton) { + mouseEvent->ignore(); + return true; + } + } else if (event->type() == QEvent::MouseButtonRelease) { + QMouseEvent *mouseEvent = static_cast(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); +} + + +void NexusView::contextMenuEvent(QContextMenuEvent *event) +{ + if (!page()->swallowContextMenuEvent(event)) { + QWebHitTestResult r = page()->mainFrame()->hitTestContent(event->pos()); + + QMenu *menu = page()->createStandardContextMenu(); + QAction *openExternalAction = new QAction("Open in external browser", menu); + if (r.linkUrl().isEmpty()) { + connect(openExternalAction, SIGNAL(triggered()), this, SLOT(openPageExternal())); + } else { + m_ContextURL = r.linkUrl(); + connect(openExternalAction, SIGNAL(triggered()), this, SLOT(openLinkExternal())); + } + + menu->addSeparator(); + menu->addAction(openExternalAction); + menu->exec(mapToGlobal(event->pos())); + } +} diff --git a/src/nexusview.h b/src/nexusview.h index f0370d27..680a36f1 100644 --- a/src/nexusview.h +++ b/src/nexusview.h @@ -17,73 +17,79 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#ifndef NEXUSVIEW_H -#define NEXUSVIEW_H - -#include "finddialog.h" - -#include -#include -#include - -/** - * @brief web view used to display a nexus page - **/ -class NexusView : public QWebView -{ - Q_OBJECT - -public: - - explicit NexusView(QWidget *parent = 0); - - /** - * @return last mod id seen in the url - */ - int getLastSeenModID() const { return m_LastSeenModID; } - -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(NexusView *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 slots: - - void urlChanged(const QUrl &url); - -private: - - QString m_FindPattern; - bool m_MiddleClick; - int m_LastSeenModID; - -}; - -#endif // NEXUSVIEW_H +#ifndef NEXUSVIEW_H +#define NEXUSVIEW_H + +#include "finddialog.h" + +#include +#include +#include + +/** + * @brief web view used to display a nexus page + **/ +class NexusView : public QWebView +{ + Q_OBJECT + +public: + + explicit NexusView(QWidget *parent = 0); + + /** + * @return last mod id seen in the url + */ + int getLastSeenModID() const { return m_LastSeenModID; } + +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(NexusView *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); + + virtual void contextMenuEvent(QContextMenuEvent *event); + +private slots: + + void urlChanged(const QUrl &url); + + void openPageExternal(); + void openLinkExternal(); + +private: + + QString m_FindPattern; + bool m_MiddleClick; + int m_LastSeenModID; + QUrl m_ContextURL; + +}; + +#endif // NEXUSVIEW_H diff --git a/src/organizer.pro b/src/organizer.pro index 29c3b5fa..723c1e04 100644 --- a/src/organizer.pro +++ b/src/organizer.pro @@ -78,7 +78,9 @@ SOURCES += \ moapplication.cpp \ profileinputdialog.cpp \ icondelegate.cpp \ - gameinfoimpl.cpp + gameinfoimpl.cpp \ + csvbuilder.cpp \ + savetextasdialog.cpp HEADERS += \ transfersavesdialog.h \ @@ -144,7 +146,9 @@ HEADERS += \ moapplication.h \ profileinputdialog.h \ icondelegate.h \ - gameinfoimpl.h + gameinfoimpl.h \ + csvbuilder.h \ + savetextasdialog.h FORMS += \ transfersavesdialog.ui \ @@ -173,7 +177,8 @@ FORMS += \ categoriesdialog.ui \ baincomplexinstallerdialog.ui \ activatemodsdialog.ui \ - profileinputdialog.ui + profileinputdialog.ui \ + savetextasdialog.ui INCLUDEPATH += ../shared ../archive ../uibase ../bsatk "$(BOOSTPATH)" @@ -233,24 +238,23 @@ DEFINES += UNICODE _UNICODE _CRT_SECURE_NO_WARNINGS NOMINMAX DEFINES += BOOST_DISABLE_ASSERTS NDEBUG #DEFINES += QMLJSDEBUGGER - SRCDIR = $$PWD SRCDIR ~= s,/,$$QMAKE_DIR_SEP,g OUTDIR ~= s,/,$$QMAKE_DIR_SEP,g DSTDIR ~= s,/,$$QMAKE_DIR_SEP,g -QMAKE_POST_LINK += mkdir -p $$quote($$DSTDIR) $$escape_expand(\\n) -QMAKE_POST_LINK += mkdir -p $$quote($$DSTDIR\\dlls) $$escape_expand(\\n) -QMAKE_POST_LINK += xcopy /y /i $$quote($$OUTDIR\\ModOrganizer*.exe) $$quote($$DSTDIR) $$escape_expand(\\n) -QMAKE_POST_LINK += xcopy /y /i $$quote($$OUTDIR\\ModOrganizer*.pdb) $$quote($$DSTDIR) $$escape_expand(\\n) -QMAKE_POST_LINK += xcopy /y /s /i $$quote($$SRCDIR\\stylesheets) $$quote($$DSTDIR)\\stylesheets $$escape_expand(\\n) -QMAKE_POST_LINK += xcopy /y /s /i $$quote($$SRCDIR\\tutorials) $$quote($$DSTDIR)\\tutorials $$escape_expand(\\n) -QMAKE_POST_LINK += xcopy /y /s /i $$quote($$SRCDIR\\*.qm) $$quote($$DSTDIR)\\translations $$escape_expand(\\n) +QMAKE_POST_LINK += xcopy /y /I $$quote($$OUTDIR\\ModOrganizer*.exe) $$quote($$DSTDIR) $$escape_expand(\\n) +QMAKE_POST_LINK += xcopy /y /I $$quote($$OUTDIR\\ModOrganizer*.pdb) $$quote($$DSTDIR) $$escape_expand(\\n) +QMAKE_POST_LINK += xcopy /y /s /I $$quote($$SRCDIR\\stylesheets) $$quote($$DSTDIR)\\stylesheets $$escape_expand(\\n) +QMAKE_POST_LINK += xcopy /y /s /I $$quote($$SRCDIR\\tutorials) $$quote($$DSTDIR)\\tutorials $$escape_expand(\\n) +QMAKE_POST_LINK += xcopy /y /s /I $$quote($$SRCDIR\\*.qm) $$quote($$DSTDIR)\\translations $$escape_expand(\\n) CONFIG(debug, debug|release) { - QMAKE_POST_LINK += copy $$quote($$SRCDIR\\..\\dlls.manifest.debug) $$quote($$DSTDIR)\\dlls\\dlls.manifest $$escape_expand(\\n) + QMAKE_POST_LINK += xcopy /y /s /I $$quote($$SRCDIR\\..\\dlls.*manifest.debug) $$quote($$DSTDIR)\\dlls $$escape_expand(\\n) + QMAKE_POST_LINK += copy /y $$quote($$DSTDIR)\\dlls\\dlls.manifest.debug $$quote($$DSTDIR)\\dlls\\dlls.manifest $$escape_expand(\\n) + QMAKE_POST_LINK += del $$quote($$DSTDIR)\\dlls\\dlls.manifest.debug $$escape_expand(\\n) } else { - QMAKE_POST_LINK += copy $$quote($$SRCDIR\\..\\dlls.manifest) $$quote($$DSTDIR)\\dlls $$escape_expand(\\n) + QMAKE_POST_LINK += xcopy /y /s /I $$quote($$SRCDIR\\..\\dlls.*manifest) $$quote($$DSTDIR)\\dlls $$escape_expand(\\n) } RESOURCES += \ diff --git a/src/savetextasdialog.cpp b/src/savetextasdialog.cpp new file mode 100644 index 00000000..465e3d65 --- /dev/null +++ b/src/savetextasdialog.cpp @@ -0,0 +1,46 @@ +#include "savetextasdialog.h" +#include "ui_savetextasdialog.h" +#include +#include +#include + +SaveTextAsDialog::SaveTextAsDialog(QWidget *parent) + : QDialog(parent), ui(new Ui::SaveTextAsDialog) +{ + ui->setupUi(this); +} + +SaveTextAsDialog::~SaveTextAsDialog() +{ + delete ui; +} + +void SaveTextAsDialog::setText(const QString &text) +{ + ui->textEdit->setPlainText(text); +} + +void SaveTextAsDialog::on_closeBtn_clicked() +{ + this->close(); +} + +void SaveTextAsDialog::on_clipboardBtn_clicked() +{ + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(ui->textEdit->toPlainText()); +} + +void SaveTextAsDialog::on_saveAsBtn_clicked() +{ + QString fileName = QFileDialog::getSaveFileName(this, tr("Save CSV"), QString(), tr("Text Files") + " (*.txt *.csv)"); + if (!fileName.isEmpty()) { + QFile file(fileName); + if (!file.open(QIODevice::WriteOnly)) { + reportError(tr("failed to open \"%1\" for writing").arg(fileName)); + return; + } + + file.write(ui->textEdit->toPlainText().toUtf8()); + } +} diff --git a/src/savetextasdialog.h b/src/savetextasdialog.h new file mode 100644 index 00000000..1a17a2db --- /dev/null +++ b/src/savetextasdialog.h @@ -0,0 +1,31 @@ +#ifndef SAVETEXTASDIALOG_H +#define SAVETEXTASDIALOG_H + +#include + +namespace Ui { +class SaveTextAsDialog; +} + +class SaveTextAsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit SaveTextAsDialog(QWidget *parent = 0); + ~SaveTextAsDialog(); + + void setText(const QString &text); + +private slots: + void on_closeBtn_clicked(); + + void on_clipboardBtn_clicked(); + + void on_saveAsBtn_clicked(); + +private: + Ui::SaveTextAsDialog *ui; +}; + +#endif // SAVETEXTASDIALOG_H diff --git a/src/savetextasdialog.ui b/src/savetextasdialog.ui new file mode 100644 index 00000000..e4834319 --- /dev/null +++ b/src/savetextasdialog.ui @@ -0,0 +1,69 @@ + + + SaveTextAsDialog + + + + 0 + 0 + 631 + 578 + + + + Dialog + + + + + + QTextEdit::NoWrap + + + false + + + + + + + + + Copy To Clipboard + + + + + + + Save As... + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Close + + + + + + + + + + diff --git a/src/shared/directoryentry.cpp b/src/shared/directoryentry.cpp index 748ead99..aa3bce5f 100644 --- a/src/shared/directoryentry.cpp +++ b/src/shared/directoryentry.cpp @@ -17,783 +17,783 @@ You should have received a copy of the GNU General Public License along with Mod Organizer. If not, see . */ -#include "directoryentry.h" -#define WIN32_LEAN_AND_MEAN -#include -#include -#include -#include -#include "error_report.h" -#include "util.h" -#include "windows_error.h" -#include - - - -class OriginConnection { - -public: - - typedef int Index; - static const int INVALID_INDEX = INT_MIN; - -public: - - OriginConnection() - : m_NextID(0) - {} - - FilesOrigin& createOrigin(const std::wstring &originName, const std::wstring &directory, int priority, - boost::shared_ptr fileRegister, boost::shared_ptr originConnection) { - int newID = createID(); - m_Origins[newID] = FilesOrigin(newID, originName, directory, priority, fileRegister, originConnection); - m_OriginsNameMap[originName] = newID; - m_OriginsPriorityMap[priority] = newID; - return m_Origins[newID]; - } - - bool exists(const std::wstring &name) { - std::map::iterator iter = m_OriginsNameMap.find(name); - return iter != m_OriginsNameMap.end(); - } - - FilesOrigin &getByID(Index ID) { - return m_Origins[ID]; - } - - FilesOrigin &getByName(const std::wstring &name) { - std::map::iterator iter = m_OriginsNameMap.find(name); - if (iter != m_OriginsNameMap.end()) { - return m_Origins[iter->second]; - } else { - std::ostringstream stream; - stream << "invalid origin name: " << ToString(name, false); - throw std::runtime_error(stream.str()); - } - } - - void changePriorityLookup(int oldPriority, int newPriority) - { - auto iter = m_OriginsPriorityMap.find(oldPriority); - if (iter != m_OriginsPriorityMap.end()) { - Index idx = iter->second; - m_OriginsPriorityMap.erase(iter); - m_OriginsPriorityMap[newPriority] = idx; - } - } - - void changeNameLookup(const std::wstring &oldName, const std::wstring &newName) - { - auto iter = m_OriginsNameMap.find(oldName); - if (iter != m_OriginsNameMap.end()) { - Index idx = iter->second; - m_OriginsNameMap.erase(iter); - m_OriginsNameMap[newName] = idx; - } else { - log("failed to change name lookup from %ls to %ls", oldName.c_str(), newName.c_str()); - } - } - -private: - - Index createID() { - return m_NextID++; - } - -private: - - Index m_NextID; - - std::map m_Origins; - std::map m_OriginsNameMap; - std::map m_OriginsPriorityMap; - -}; - - -// -// FilesOrigin -// - - -void FilesOrigin::enable(bool enabled) -{ - if (!enabled) { - std::set copy = m_Files; - for (auto iter = copy.begin(); iter != copy.end(); ++iter) { - m_FileRegister->removeOrigin(*iter, m_ID); - } - m_Files.clear(); - } - m_Disabled = !enabled; -} - - -void FilesOrigin::removeFile(FileEntry::Index index) -{ - auto iter = m_Files.find(index); - if (iter != m_Files.end()) { - m_Files.erase(iter); - } -} - - - -std::wstring tail(const std::wstring &source, const size_t count) -{ - if (count >= source.length()) { - return source; - } - - return source.substr(source.length() - count); -} - - -void FilesOrigin::setPriority(int priority) -{ - m_OriginConnection->changePriorityLookup(m_Priority, priority); - - m_Priority = priority; -} - - -void FilesOrigin::setName(const std::wstring &name) -{ - m_OriginConnection->changeNameLookup(m_Name, name); - // change path too - if (tail(m_Path, m_Name.length()) == m_Name) { - m_Path = m_Path.substr(0, m_Path.length() - m_Name.length()).append(name); - } - m_Name = name; -} - -std::vector FilesOrigin::getFiles() const -{ - std::vector result; - - for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) { - result.push_back(m_FileRegister->getFile(*iter)); - } - - return result; -} - - - -// -// FileEntry -// - -void FileEntry::addOrigin(int origin, FILETIME fileTime, const std::wstring &archive) -{ - if (m_Origin == -1) { - m_Origin = origin; - m_FileTime = fileTime; - m_Archive = archive; -// } else if (FilesOrigin::getByID(origin).getPriority() > FilesOrigin::getByID(m_Origin).getPriority()) { - } else if (m_Parent->getOriginByID(origin).getPriority() > m_Parent->getOriginByID(m_Origin).getPriority()) { - if (std::find(m_Alternatives.begin(), m_Alternatives.end(), m_Origin) == m_Alternatives.end()) { - m_Alternatives.push_back(m_Origin); - } - m_Origin = origin; - m_FileTime = fileTime; - m_Archive = archive; - } else { - bool found = false; - if (m_Origin == origin) { - // already an origin - return; - } - for (std::vector::iterator iter = m_Alternatives.begin(); iter != m_Alternatives.end(); ++iter) { - if (*iter == origin) { - // already an origin - return; - } - if (m_Parent->getOriginByID(*iter).getPriority() < m_Parent->getOriginByID(origin).getPriority()) { - m_Alternatives.insert(iter, origin); - found = true; - break; - } - } - if (!found) { - m_Alternatives.push_back(origin); - } - } -} - -bool FileEntry::removeOrigin(int origin) -{ - if (m_Origin == origin) { - if (!m_Alternatives.empty()) { - // find alternative with the highest priority - std::vector::iterator currentIter = m_Alternatives.begin(); - for (std::vector::iterator iter = m_Alternatives.begin(); iter != m_Alternatives.end(); ++iter) { - if ((m_Parent->getOriginByID(*iter).getPriority() > m_Parent->getOriginByID(*currentIter).getPriority()) && - (*iter != origin)) { - currentIter = iter; - } - } - int currentID = *currentIter; - m_Alternatives.erase(currentIter); - - m_Origin = currentID; - - // now we need to update the file time... - std::wstring filePath = getFullPath(); - HANDLE file = ::CreateFile(filePath.c_str(), GENERIC_READ | GENERIC_WRITE, - 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); - if (!::GetFileTime(file, NULL, NULL, &m_FileTime)) { - // maybe this file is in a bsa, but there is no easy way to find out which. User should refresh - // the view to find out - m_Archive = L"bsa?"; - } else { - m_Archive = L""; - } - - ::CloseHandle(file); - - } else { - m_Origin = -1; - return true; - } - } else { - std::vector::iterator newEnd = std::remove(m_Alternatives.begin(), m_Alternatives.end(), origin); - m_Alternatives.erase(newEnd, m_Alternatives.end()); - } - return false; -} - - -// sorted by priority descending -static bool ByOriginPriority(DirectoryEntry *entry, int LHS, int RHS) -{ - return entry->getOriginByID(LHS).getPriority() < entry->getOriginByID(RHS).getPriority(); -} - - -FileEntry::FileEntry(Index index, const std::wstring &name, DirectoryEntry *parent) - : m_Index(index), m_Name(name), m_Parent(parent), m_Origin(-1), m_Archive(L"") -{ -} - - -void FileEntry::sortOrigins() -{ - m_Alternatives.push_back(m_Origin); - std::sort(m_Alternatives.begin(), m_Alternatives.end(), boost::bind(ByOriginPriority, m_Parent, _1, _2)); - m_Origin = m_Alternatives[m_Alternatives.size() - 1]; - m_Alternatives.pop_back(); -} - - -bool FileEntry::recurseParents(std::wstring &path, const DirectoryEntry *parent) const -{ - if (parent == NULL) { - return false; - } else { - // don't append the topmost parent because it is the virtual data-root - if (recurseParents(path, parent->getParent())) { - path.append(L"\\").append(parent->getName()); - } - return true; - } -} - -std::wstring FileEntry::getFullPath() const -{ - std::wstring result; - bool ignore = false; - result = m_Parent->getOriginByID(getOrigin(ignore)).getPath(); //base directory for origin - recurseParents(result, m_Parent); // all intermediate directories - result.append(L"\\").append(m_Name); // the actual filename - return result; -} - -std::wstring FileEntry::getRelativePath() const -{ - std::wstring result; - recurseParents(result, m_Parent); // all intermediate directories - result.append(L"\\").append(m_Name); // the actual filename - return result; -} - - - - - -// -// DirectoryEntry -// -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_FileRegister.reset(new FileRegister(m_OriginConnection)); -} - -DirectoryEntry::DirectoryEntry(const std::wstring &name, DirectoryEntry *parent, int originID, - boost::shared_ptr fileRegister, boost::shared_ptr originConnection) - : m_FileRegister(fileRegister), m_OriginConnection(originConnection), - m_Name(name), m_Parent(parent), m_Populated(false), m_Origin(originID) -{} - - -DirectoryEntry::~DirectoryEntry() -{ - clear(); -} - - -const std::wstring &DirectoryEntry::getName() const -{ - return m_Name; -} - - -void DirectoryEntry::clear() -{ - m_Files.clear(); - for (std::vector::iterator iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { - delete *iter; - } - m_SubDirectories.clear(); -} - - -FilesOrigin &DirectoryEntry::createOrigin(const std::wstring &originName, const std::wstring &directory, int priority) -{ - if (m_OriginConnection->exists(originName)) { - FilesOrigin &origin = m_OriginConnection->getByName(originName); - origin.enable(true); - return origin; - } else { - return m_OriginConnection->createOrigin(originName, directory, priority, m_FileRegister, m_OriginConnection); - } -} - - -void DirectoryEntry::addFromOrigin(const std::wstring &originName, const std::wstring &directory, int priority) -{ - FilesOrigin &origin = createOrigin(originName, directory, priority); - wchar_t *buffer = new wchar_t[MAXPATH_UNICODE + 1]; - memset(buffer, L'\0', MAXPATH_UNICODE + 1); - try { - int offset = _snwprintf(buffer, MAXPATH_UNICODE, L"%ls", directory.c_str()); - addFiles(origin, buffer, offset); - } catch (...) { - delete [] buffer; - buffer = NULL; - } - delete [] buffer; - m_Populated = true; -} - - -void DirectoryEntry::addFromBSA(const std::wstring &originName, std::wstring &directory, const std::wstring &fileName, int priority) -{ - FilesOrigin &origin = createOrigin(originName, directory, priority); - - WIN32_FILE_ATTRIBUTE_DATA fileData; - if (::GetFileAttributesExW(fileName.c_str(), GetFileExInfoStandard, &fileData) == 0) { - throw windows_error("failed to determine file time"); - } - - BSA::Archive archive; - BSA::EErrorCode res = archive.read(ToString(fileName, false).c_str()); - if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) { - std::ostringstream stream; - stream << "invalid bsa file: " << ToString(fileName, false) << " errorcode " << res << " - " << ::GetLastError(); - throw std::runtime_error(stream.str()); - } - size_t namePos = fileName.find_last_of(L"\\/"); - if (namePos == std::wstring::npos) { - namePos = 0; - } else { - ++namePos; - } - - addFiles(origin, archive.getRoot(), fileData.ftLastWriteTime, fileName.substr(namePos)); - m_Populated = true; -} - - -void DirectoryEntry::addFiles(FilesOrigin &origin, wchar_t *buffer, int bufferOffset) -{ - WIN32_FIND_DATAW findData; - - _snwprintf(buffer + bufferOffset, MAXPATH_UNICODE - bufferOffset, L"\\*"); - HANDLE searchHandle = ::FindFirstFileExW(buffer, FindExInfoStandard, &findData, FindExSearchNameMatch, NULL, FIND_FIRST_EX_CASE_SENSITIVE); - if (searchHandle != INVALID_HANDLE_VALUE) { - BOOL result = true; - while (result) { - if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { - if ((wcscmp(findData.cFileName, L".") != 0) && - (wcscmp(findData.cFileName, L"..") != 0)) { - int offset = _snwprintf(buffer + bufferOffset, MAXPATH_UNICODE, L"\\%ls", findData.cFileName); - // recurse into subdirectories - getSubDirectory(findData.cFileName, true, origin.getID())->addFiles(origin, buffer, bufferOffset + offset); - } - } else { - insert(findData.cFileName, origin, findData.ftLastWriteTime, L""); - } - result = ::FindNextFileW(searchHandle, &findData); - } - } - ::FindClose(searchHandle); -} - - -void DirectoryEntry::addFiles(FilesOrigin &origin, BSA::Folder::Ptr archiveFolder, FILETIME &fileTime, const std::wstring &archiveName) -{ - // add files - for (unsigned int fileIdx = 0; fileIdx < archiveFolder->getNumFiles(); ++fileIdx) { - BSA::File::Ptr file = archiveFolder->getFile(fileIdx); - insert(ToWString(file->getName(), true), origin, fileTime, archiveName); - } - - // recurse into subdirectories - for (unsigned int folderIdx = 0; folderIdx < archiveFolder->getNumSubFolders(); ++folderIdx) { - BSA::Folder::Ptr folder = archiveFolder->getSubFolder(folderIdx); - DirectoryEntry *folderEntry = getSubDirectoryRecursive(ToWString(folder->getName(), true), true, origin.getID()); - - folderEntry->addFiles(origin, folder, fileTime, archiveName); - } -} - - -void DirectoryEntry::removeFile(const std::wstring &filePath, int *origin) -{ - size_t pos = filePath.find_first_of(L"\\/"); - if (pos == std::string::npos) { - this->remove(filePath, origin); - } else { - std::wstring dirName = filePath.substr(0, pos); - std::wstring rest = filePath.substr(pos + 1); - DirectoryEntry *entry = getSubDirectoryRecursive(dirName, false); - if (entry != NULL) { - entry->removeFile(rest, origin); - } - } -} - -void DirectoryEntry::removeDirRecursive() -{ - while (!m_Files.empty()) { - m_FileRegister->removeFile(m_Files.begin()->second); - } - - for (auto iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { - (*iter)->removeDirRecursive(); - } - m_SubDirectories.clear(); -} - -void DirectoryEntry::removeDir(const std::wstring &path) -{ - size_t pos = path.find_first_of(L"\\/"); - if (pos == std::string::npos) { - for (auto iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { - if (_wcsicmp((*iter)->getName().c_str(), path.c_str()) == 0) { - (*iter)->removeDirRecursive(); - m_SubDirectories.erase(iter); - break; - } - } - } else { - std::wstring dirName = path.substr(0, pos); - std::wstring rest = path.substr(pos + 1); - DirectoryEntry *entry = getSubDirectoryRecursive(dirName, false); - if (entry != NULL) { - entry->removeDir(rest); - } - } -} - - -void DirectoryEntry::insertFile(const std::wstring &filePath, FilesOrigin &origin, FILETIME fileTime) -{ - size_t pos = filePath.find_first_of(L"\\/"); - if (pos == std::string::npos) { - this->insert(filePath, origin, fileTime, L""); - } else { - std::wstring dirName = filePath.substr(0, pos); - std::wstring rest = filePath.substr(pos + 1); - getSubDirectoryRecursive(dirName, true, origin.getID())->insertFile(rest, origin, fileTime); - } -} - - -void DirectoryEntry::removeFile(FileEntry::Index index) -{ - if (m_Files.size() != 0) { - auto iter = std::find_if(m_Files.begin(), m_Files.end(), - [&index](const std::pair &iter) -> bool { - return iter.second == index; } ); - if (iter != m_Files.end()) { - m_Files.erase(iter); - } else { - log("file \"%ls\" not in directory \"%ls\"", - m_FileRegister->getFile(index)->getName().c_str(), - this->getName().c_str()); - } - } else { - log("file \"%ls\" not in directory \"%ls\", directory empty", - m_FileRegister->getFile(index)->getName().c_str(), - this->getName().c_str()); - } -} - - -int DirectoryEntry::anyOrigin() const -{ - bool ignore; - for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) { - FileEntry *entry = m_FileRegister->getFile(iter->second); - if (!entry->isFromArchive()) { - return entry->getOrigin(ignore); - } - } - - // if we got here, no file directly within this directory is a valid indicator for a mod, thus - // we continue looking in subdirectories - for (std::vector::const_iterator iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { - int res = (*iter)->anyOrigin(); - if (res != -1){ - return res; - } - } - return m_Origin; -} - - -bool DirectoryEntry::originExists(const std::wstring &name) const -{ - return m_OriginConnection->exists(name); -} - - -FilesOrigin &DirectoryEntry::getOriginByID(int ID) const -{ - return m_OriginConnection->getByID(ID); -} - - -FilesOrigin &DirectoryEntry::getOriginByName(const std::wstring &name) const -{ - return m_OriginConnection->getByName(name); -} - - -int DirectoryEntry::getOrigin(const std::wstring &path, bool &archive) -{ - const DirectoryEntry *directory = NULL; - const FileEntry *file = searchFile(path, &directory); - if (file != NULL) { - return file->getOrigin(archive); - } else { - if (directory != NULL) { - return directory->anyOrigin(); - } else { - return -1; - } - } -} - -std::vector DirectoryEntry::getFiles() const -{ - std::vector result; - for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) { - result.push_back(m_FileRegister->getFile(iter->second)); - } - return result; -} - - -const FileEntry *DirectoryEntry::searchFile(const std::wstring &path, const DirectoryEntry **directory) const -{ - if (directory != NULL) { - *directory = NULL; - } - - if ((path.length() == 0) || - (path == L"*")) { - // no file name -> the path ended on a (back-)slash - *directory = this; - - return NULL; - } - - size_t len = path.find_first_of(L"\\/"); - - if (len == std::string::npos) { - // no more path components - auto iter = m_Files.find(path); - if (iter != m_Files.end()) { - return m_FileRegister->getFile(iter->second); - } else if (directory != NULL) { - DirectoryEntry *temp = findSubDirectory(path); - if (temp != NULL) { - *directory = temp; - } - } - } else { - // file is in in a subdirectory, recurse into the matching subdirectory - std::wstring pathComponent = path.substr(0, len); - DirectoryEntry *temp = findSubDirectory(pathComponent); - if (temp != NULL) { - return temp->searchFile(path.substr(len + 1), directory); - } - } - return NULL; -} - -/* -void DirectoryEntry::sortOrigins() -{ - for (std::set::iterator iter = m_Files.begin(); iter != m_Files.end(); ++iter) { - const_cast(*iter).sortOrigins(); - } - for (std::vector::iterator iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { - (*iter)->sortOrigins(); - } -} -*/ - -DirectoryEntry *DirectoryEntry::findSubDirectory(const std::wstring &name) const -{ - for (std::vector::const_iterator iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { - if (_wcsicmp((*iter)->getName().c_str(), name.c_str()) == 0) { - return *iter; - } - } - return NULL; -} - - - -const FileEntry *DirectoryEntry::findFile(const std::wstring &name) -{ - auto iter = m_Files.find(name); - if (iter != m_Files.end()) { - return m_FileRegister->getFile(iter->second); - } else { - return NULL; - } -} - -DirectoryEntry *DirectoryEntry::getSubDirectory(const std::wstring &name, bool create, int originID) -{ - for (std::vector::iterator iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { - if (_wcsicmp((*iter)->getName().c_str(), name.c_str()) == 0) { - return *iter; - } - } - if (create) { - std::vector::iterator iter = m_SubDirectories.insert(m_SubDirectories.end(), - new DirectoryEntry(name, this, originID, m_FileRegister, m_OriginConnection)); - return *iter; - } else { - return NULL; - } -} - - -DirectoryEntry *DirectoryEntry::getSubDirectoryRecursive(const std::wstring &path, bool create, int originID) -{ - if (path.length() == 0) { - // path ended with a backslash? - return this; - } - - size_t pos = path.find_first_of(L"\\/"); - if (pos == std::wstring::npos) { - return getSubDirectory(path, create); - } else { - DirectoryEntry *nextChild = getSubDirectory(path.substr(0, pos), create, originID); - if (nextChild == NULL) { - return NULL; - } else { - return nextChild->getSubDirectoryRecursive(path.substr(pos + 1), create, originID); - } - } -} - - - -FileRegister::FileRegister(boost::shared_ptr originConnection) - : m_OriginConnection(originConnection) -{ -} - -FileEntry::Index FileRegister::generateIndex() -{ - static FileEntry::Index sIndex = 0; - return sIndex++; -} - -bool FileRegister::indexValid(FileEntry::Index index) const -{ - return m_Files.find(index) != m_Files.end(); -} - -FileEntry &FileRegister::createFile(const std::wstring &name, DirectoryEntry *parent) -{ - FileEntry::Index index = generateIndex(); - m_Files[index] = FileEntry(index, name, parent); - return m_Files[index]; -} - - -FileEntry *FileRegister::getFile(FileEntry::Index index) -{ - auto iter = m_Files.find(index); - if (iter != m_Files.end()) { - return &iter->second; - } - return NULL; -} - - -void FileRegister::unregisterFile(FileEntry &file) -{ - bool ignore; - // unregister from origin - int originID = file.getOrigin(ignore); - m_OriginConnection->getByID(originID).removeFile(file.getIndex()); - const std::vector &alternatives = file.getAlternatives(); - for (auto iter = alternatives.begin(); iter != alternatives.end(); ++iter) { - m_OriginConnection->getByID(*iter).removeFile(file.getIndex()); - } - - // unregister from directory - if (file.getParent() != NULL) { - file.getParent()->removeFile(file.getIndex()); - } -} - - -void FileRegister::removeFile(FileEntry::Index index) -{ - auto iter = m_Files.find(index); - if (iter != m_Files.end()) { - unregisterFile(iter->second); - m_Files.erase(index); - } -} - -void FileRegister::removeOrigin(FileEntry::Index index, int originID) -{ - auto iter = m_Files.find(index); - if (iter != m_Files.end()) { - if (iter->second.removeOrigin(originID)) { - unregisterFile(iter->second); - } - } -} - -void FileRegister::sortOrigins() -{ - for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) { - iter->second.sortOrigins(); - } -} +#include "directoryentry.h" +#define WIN32_LEAN_AND_MEAN +#include +#include +#include +#include +#include "error_report.h" +#include "util.h" +#include "windows_error.h" +#include + + + +class OriginConnection { + +public: + + typedef int Index; + static const int INVALID_INDEX = INT_MIN; + +public: + + OriginConnection() + : m_NextID(0) + {} + + FilesOrigin& createOrigin(const std::wstring &originName, const std::wstring &directory, int priority, + boost::shared_ptr fileRegister, boost::shared_ptr originConnection) { + int newID = createID(); + m_Origins[newID] = FilesOrigin(newID, originName, directory, priority, fileRegister, originConnection); + m_OriginsNameMap[originName] = newID; + m_OriginsPriorityMap[priority] = newID; + return m_Origins[newID]; + } + + bool exists(const std::wstring &name) { + std::map::iterator iter = m_OriginsNameMap.find(name); + return iter != m_OriginsNameMap.end(); + } + + FilesOrigin &getByID(Index ID) { + return m_Origins[ID]; + } + + FilesOrigin &getByName(const std::wstring &name) { + std::map::iterator iter = m_OriginsNameMap.find(name); + if (iter != m_OriginsNameMap.end()) { + return m_Origins[iter->second]; + } else { + std::ostringstream stream; + stream << "invalid origin name: " << ToString(name, false); + throw std::runtime_error(stream.str()); + } + } + + void changePriorityLookup(int oldPriority, int newPriority) + { + auto iter = m_OriginsPriorityMap.find(oldPriority); + if (iter != m_OriginsPriorityMap.end()) { + Index idx = iter->second; + m_OriginsPriorityMap.erase(iter); + m_OriginsPriorityMap[newPriority] = idx; + } + } + + void changeNameLookup(const std::wstring &oldName, const std::wstring &newName) + { + auto iter = m_OriginsNameMap.find(oldName); + if (iter != m_OriginsNameMap.end()) { + Index idx = iter->second; + m_OriginsNameMap.erase(iter); + m_OriginsNameMap[newName] = idx; + } else { + log("failed to change name lookup from %ls to %ls", oldName.c_str(), newName.c_str()); + } + } + +private: + + Index createID() { + return m_NextID++; + } + +private: + + Index m_NextID; + + std::map m_Origins; + std::map m_OriginsNameMap; + std::map m_OriginsPriorityMap; + +}; + + +// +// FilesOrigin +// + + +void FilesOrigin::enable(bool enabled) +{ + if (!enabled) { + std::set copy = m_Files; + for (auto iter = copy.begin(); iter != copy.end(); ++iter) { + m_FileRegister->removeOrigin(*iter, m_ID); + } + m_Files.clear(); + } + m_Disabled = !enabled; +} + + +void FilesOrigin::removeFile(FileEntry::Index index) +{ + auto iter = m_Files.find(index); + if (iter != m_Files.end()) { + m_Files.erase(iter); + } +} + + + +std::wstring tail(const std::wstring &source, const size_t count) +{ + if (count >= source.length()) { + return source; + } + + return source.substr(source.length() - count); +} + + +void FilesOrigin::setPriority(int priority) +{ + m_OriginConnection->changePriorityLookup(m_Priority, priority); + + m_Priority = priority; +} + + +void FilesOrigin::setName(const std::wstring &name) +{ + m_OriginConnection->changeNameLookup(m_Name, name); + // change path too + if (tail(m_Path, m_Name.length()) == m_Name) { + m_Path = m_Path.substr(0, m_Path.length() - m_Name.length()).append(name); + } + m_Name = name; +} + +std::vector FilesOrigin::getFiles() const +{ + std::vector result; + + for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) { + result.push_back(m_FileRegister->getFile(*iter)); + } + + return result; +} + + + +// +// FileEntry +// + +void FileEntry::addOrigin(int origin, FILETIME fileTime, const std::wstring &archive) +{ + if (m_Origin == -1) { + m_Origin = origin; + m_FileTime = fileTime; + m_Archive = archive; +// } else if (FilesOrigin::getByID(origin).getPriority() > FilesOrigin::getByID(m_Origin).getPriority()) { + } else if (m_Parent->getOriginByID(origin).getPriority() > m_Parent->getOriginByID(m_Origin).getPriority()) { + if (std::find(m_Alternatives.begin(), m_Alternatives.end(), m_Origin) == m_Alternatives.end()) { + m_Alternatives.push_back(m_Origin); + } + m_Origin = origin; + m_FileTime = fileTime; + m_Archive = archive; + } else { + bool found = false; + if (m_Origin == origin) { + // already an origin + return; + } + for (std::vector::iterator iter = m_Alternatives.begin(); iter != m_Alternatives.end(); ++iter) { + if (*iter == origin) { + // already an origin + return; + } + if (m_Parent->getOriginByID(*iter).getPriority() < m_Parent->getOriginByID(origin).getPriority()) { + m_Alternatives.insert(iter, origin); + found = true; + break; + } + } + if (!found) { + m_Alternatives.push_back(origin); + } + } +} + +bool FileEntry::removeOrigin(int origin) +{ + if (m_Origin == origin) { + if (!m_Alternatives.empty()) { + // find alternative with the highest priority + std::vector::iterator currentIter = m_Alternatives.begin(); + for (std::vector::iterator iter = m_Alternatives.begin(); iter != m_Alternatives.end(); ++iter) { + if ((m_Parent->getOriginByID(*iter).getPriority() > m_Parent->getOriginByID(*currentIter).getPriority()) && + (*iter != origin)) { + currentIter = iter; + } + } + int currentID = *currentIter; + m_Alternatives.erase(currentIter); + + m_Origin = currentID; + + // now we need to update the file time... + std::wstring filePath = getFullPath(); + HANDLE file = ::CreateFile(filePath.c_str(), GENERIC_READ | GENERIC_WRITE, + 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); + if (!::GetFileTime(file, NULL, NULL, &m_FileTime)) { + // maybe this file is in a bsa, but there is no easy way to find out which. User should refresh + // the view to find out + m_Archive = L"bsa?"; + } else { + m_Archive = L""; + } + + ::CloseHandle(file); + + } else { + m_Origin = -1; + return true; + } + } else { + std::vector::iterator newEnd = std::remove(m_Alternatives.begin(), m_Alternatives.end(), origin); + m_Alternatives.erase(newEnd, m_Alternatives.end()); + } + return false; +} + + +// sorted by priority descending +static bool ByOriginPriority(DirectoryEntry *entry, int LHS, int RHS) +{ + return entry->getOriginByID(LHS).getPriority() < entry->getOriginByID(RHS).getPriority(); +} + + +FileEntry::FileEntry(Index index, const std::wstring &name, DirectoryEntry *parent) + : m_Index(index), m_Name(name), m_Parent(parent), m_Origin(-1), m_Archive(L"") +{ +} + + +void FileEntry::sortOrigins() +{ + m_Alternatives.push_back(m_Origin); + std::sort(m_Alternatives.begin(), m_Alternatives.end(), boost::bind(ByOriginPriority, m_Parent, _1, _2)); + m_Origin = m_Alternatives[m_Alternatives.size() - 1]; + m_Alternatives.pop_back(); +} + + +bool FileEntry::recurseParents(std::wstring &path, const DirectoryEntry *parent) const +{ + if (parent == NULL) { + return false; + } else { + // don't append the topmost parent because it is the virtual data-root + if (recurseParents(path, parent->getParent())) { + path.append(L"\\").append(parent->getName()); + } + return true; + } +} + +std::wstring FileEntry::getFullPath() const +{ + std::wstring result; + bool ignore = false; + result = m_Parent->getOriginByID(getOrigin(ignore)).getPath(); //base directory for origin + recurseParents(result, m_Parent); // all intermediate directories + result.append(L"\\").append(m_Name); // the actual filename + return result; +} + +std::wstring FileEntry::getRelativePath() const +{ + std::wstring result; + recurseParents(result, m_Parent); // all intermediate directories + result.append(L"\\").append(m_Name); // the actual filename + return result; +} + + + + + +// +// DirectoryEntry +// +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_FileRegister.reset(new FileRegister(m_OriginConnection)); +} + +DirectoryEntry::DirectoryEntry(const std::wstring &name, DirectoryEntry *parent, int originID, + boost::shared_ptr fileRegister, boost::shared_ptr originConnection) + : m_FileRegister(fileRegister), m_OriginConnection(originConnection), + m_Name(name), m_Parent(parent), m_Populated(false), m_Origin(originID) +{} + + +DirectoryEntry::~DirectoryEntry() +{ + clear(); +} + + +const std::wstring &DirectoryEntry::getName() const +{ + return m_Name; +} + + +void DirectoryEntry::clear() +{ + m_Files.clear(); + for (std::vector::iterator iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { + delete *iter; + } + m_SubDirectories.clear(); +} + + +FilesOrigin &DirectoryEntry::createOrigin(const std::wstring &originName, const std::wstring &directory, int priority) +{ + if (m_OriginConnection->exists(originName)) { + FilesOrigin &origin = m_OriginConnection->getByName(originName); + origin.enable(true); + return origin; + } else { + return m_OriginConnection->createOrigin(originName, directory, priority, m_FileRegister, m_OriginConnection); + } +} + + +void DirectoryEntry::addFromOrigin(const std::wstring &originName, const std::wstring &directory, int priority) +{ + FilesOrigin &origin = createOrigin(originName, directory, priority); + wchar_t *buffer = new wchar_t[MAXPATH_UNICODE + 1]; + memset(buffer, L'\0', MAXPATH_UNICODE + 1); + try { + int offset = _snwprintf(buffer, MAXPATH_UNICODE, L"%ls", directory.c_str()); + addFiles(origin, buffer, offset); + } catch (...) { + delete [] buffer; + buffer = NULL; + } + delete [] buffer; + m_Populated = true; +} + + +void DirectoryEntry::addFromBSA(const std::wstring &originName, std::wstring &directory, const std::wstring &fileName, int priority) +{ + FilesOrigin &origin = createOrigin(originName, directory, priority); + + WIN32_FILE_ATTRIBUTE_DATA fileData; + if (::GetFileAttributesExW(fileName.c_str(), GetFileExInfoStandard, &fileData) == 0) { + throw windows_error("failed to determine file time"); + } + + BSA::Archive archive; + BSA::EErrorCode res = archive.read(ToString(fileName, false).c_str()); + if ((res != BSA::ERROR_NONE) && (res != BSA::ERROR_INVALIDHASHES)) { + std::ostringstream stream; + stream << "invalid bsa file: " << ToString(fileName, false) << " errorcode " << res << " - " << ::GetLastError(); + throw std::runtime_error(stream.str()); + } + size_t namePos = fileName.find_last_of(L"\\/"); + if (namePos == std::wstring::npos) { + namePos = 0; + } else { + ++namePos; + } + + addFiles(origin, archive.getRoot(), fileData.ftLastWriteTime, fileName.substr(namePos)); + m_Populated = true; +} + + +void DirectoryEntry::addFiles(FilesOrigin &origin, wchar_t *buffer, int bufferOffset) +{ + WIN32_FIND_DATAW findData; + + _snwprintf(buffer + bufferOffset, MAXPATH_UNICODE - bufferOffset, L"\\*"); + HANDLE searchHandle = ::FindFirstFileExW(buffer, FindExInfoStandard, &findData, FindExSearchNameMatch, NULL, FIND_FIRST_EX_CASE_SENSITIVE); + if (searchHandle != INVALID_HANDLE_VALUE) { + BOOL result = true; + while (result) { + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) { + if ((wcscmp(findData.cFileName, L".") != 0) && + (wcscmp(findData.cFileName, L"..") != 0)) { + int offset = _snwprintf(buffer + bufferOffset, MAXPATH_UNICODE, L"\\%ls", findData.cFileName); + // recurse into subdirectories + getSubDirectory(findData.cFileName, true, origin.getID())->addFiles(origin, buffer, bufferOffset + offset); + } + } else { + insert(findData.cFileName, origin, findData.ftLastWriteTime, L""); + } + result = ::FindNextFileW(searchHandle, &findData); + } + } + ::FindClose(searchHandle); +} + + +void DirectoryEntry::addFiles(FilesOrigin &origin, BSA::Folder::Ptr archiveFolder, FILETIME &fileTime, const std::wstring &archiveName) +{ + // add files + for (unsigned int fileIdx = 0; fileIdx < archiveFolder->getNumFiles(); ++fileIdx) { + BSA::File::Ptr file = archiveFolder->getFile(fileIdx); + insert(ToWString(file->getName(), true), origin, fileTime, archiveName); + } + + // recurse into subdirectories + for (unsigned int folderIdx = 0; folderIdx < archiveFolder->getNumSubFolders(); ++folderIdx) { + BSA::Folder::Ptr folder = archiveFolder->getSubFolder(folderIdx); + DirectoryEntry *folderEntry = getSubDirectoryRecursive(ToWString(folder->getName(), true), true, origin.getID()); + + folderEntry->addFiles(origin, folder, fileTime, archiveName); + } +} + + +void DirectoryEntry::removeFile(const std::wstring &filePath, int *origin) +{ + size_t pos = filePath.find_first_of(L"\\/"); + if (pos == std::string::npos) { + this->remove(filePath, origin); + } else { + std::wstring dirName = filePath.substr(0, pos); + std::wstring rest = filePath.substr(pos + 1); + DirectoryEntry *entry = getSubDirectoryRecursive(dirName, false); + if (entry != NULL) { + entry->removeFile(rest, origin); + } + } +} + +void DirectoryEntry::removeDirRecursive() +{ + while (!m_Files.empty()) { + m_FileRegister->removeFile(m_Files.begin()->second); + } + + for (auto iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { + (*iter)->removeDirRecursive(); + } + m_SubDirectories.clear(); +} + +void DirectoryEntry::removeDir(const std::wstring &path) +{ + size_t pos = path.find_first_of(L"\\/"); + if (pos == std::string::npos) { + for (auto iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { + if (_wcsicmp((*iter)->getName().c_str(), path.c_str()) == 0) { + (*iter)->removeDirRecursive(); + m_SubDirectories.erase(iter); + break; + } + } + } else { + std::wstring dirName = path.substr(0, pos); + std::wstring rest = path.substr(pos + 1); + DirectoryEntry *entry = getSubDirectoryRecursive(dirName, false); + if (entry != NULL) { + entry->removeDir(rest); + } + } +} + + +void DirectoryEntry::insertFile(const std::wstring &filePath, FilesOrigin &origin, FILETIME fileTime) +{ + size_t pos = filePath.find_first_of(L"\\/"); + if (pos == std::string::npos) { + this->insert(filePath, origin, fileTime, L""); + } else { + std::wstring dirName = filePath.substr(0, pos); + std::wstring rest = filePath.substr(pos + 1); + getSubDirectoryRecursive(dirName, true, origin.getID())->insertFile(rest, origin, fileTime); + } +} + + +void DirectoryEntry::removeFile(FileEntry::Index index) +{ + if (m_Files.size() != 0) { + auto iter = std::find_if(m_Files.begin(), m_Files.end(), + [&index](const std::pair &iter) -> bool { + return iter.second == index; } ); + if (iter != m_Files.end()) { + m_Files.erase(iter); + } else { + log("file \"%ls\" not in directory \"%ls\"", + m_FileRegister->getFile(index)->getName().c_str(), + this->getName().c_str()); + } + } else { + log("file \"%ls\" not in directory \"%ls\", directory empty", + m_FileRegister->getFile(index)->getName().c_str(), + this->getName().c_str()); + } +} + + +int DirectoryEntry::anyOrigin() const +{ + bool ignore; + for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) { + FileEntry *entry = m_FileRegister->getFile(iter->second); + if (!entry->isFromArchive()) { + return entry->getOrigin(ignore); + } + } + + // if we got here, no file directly within this directory is a valid indicator for a mod, thus + // we continue looking in subdirectories + for (std::vector::const_iterator iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { + int res = (*iter)->anyOrigin(); + if (res != -1){ + return res; + } + } + return m_Origin; +} + + +bool DirectoryEntry::originExists(const std::wstring &name) const +{ + return m_OriginConnection->exists(name); +} + + +FilesOrigin &DirectoryEntry::getOriginByID(int ID) const +{ + return m_OriginConnection->getByID(ID); +} + + +FilesOrigin &DirectoryEntry::getOriginByName(const std::wstring &name) const +{ + return m_OriginConnection->getByName(name); +} + + +int DirectoryEntry::getOrigin(const std::wstring &path, bool &archive) +{ + const DirectoryEntry *directory = NULL; + const FileEntry *file = searchFile(path, &directory); + if (file != NULL) { + return file->getOrigin(archive); + } else { + if (directory != NULL) { + return directory->anyOrigin(); + } else { + return -1; + } + } +} + +std::vector DirectoryEntry::getFiles() const +{ + std::vector result; + for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) { + result.push_back(m_FileRegister->getFile(iter->second)); + } + return result; +} + + +const FileEntry *DirectoryEntry::searchFile(const std::wstring &path, const DirectoryEntry **directory) const +{ + if (directory != NULL) { + *directory = NULL; + } + + if ((path.length() == 0) || + (path == L"*")) { + // no file name -> the path ended on a (back-)slash + *directory = this; + + return NULL; + } + + size_t len = path.find_first_of(L"\\/"); + + if (len == std::string::npos) { + // no more path components + auto iter = m_Files.find(path); + if (iter != m_Files.end()) { + return m_FileRegister->getFile(iter->second); + } else if (directory != NULL) { + DirectoryEntry *temp = findSubDirectory(path); + if (temp != NULL) { + *directory = temp; + } + } + } else { + // file is in in a subdirectory, recurse into the matching subdirectory + std::wstring pathComponent = path.substr(0, len); + DirectoryEntry *temp = findSubDirectory(pathComponent); + if (temp != NULL) { + return temp->searchFile(path.substr(len + 1), directory); + } + } + return NULL; +} + +/* +void DirectoryEntry::sortOrigins() +{ + for (std::set::iterator iter = m_Files.begin(); iter != m_Files.end(); ++iter) { + const_cast(*iter).sortOrigins(); + } + for (std::vector::iterator iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { + (*iter)->sortOrigins(); + } +} +*/ + +DirectoryEntry *DirectoryEntry::findSubDirectory(const std::wstring &name) const +{ + for (std::vector::const_iterator iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { + if (_wcsicmp((*iter)->getName().c_str(), name.c_str()) == 0) { + return *iter; + } + } + return NULL; +} + + + +const FileEntry *DirectoryEntry::findFile(const std::wstring &name) +{ + auto iter = m_Files.find(name); + if (iter != m_Files.end()) { + return m_FileRegister->getFile(iter->second); + } else { + return NULL; + } +} + +DirectoryEntry *DirectoryEntry::getSubDirectory(const std::wstring &name, bool create, int originID) +{ + for (std::vector::iterator iter = m_SubDirectories.begin(); iter != m_SubDirectories.end(); ++iter) { + if (_wcsicmp((*iter)->getName().c_str(), name.c_str()) == 0) { + return *iter; + } + } + if (create) { + std::vector::iterator iter = m_SubDirectories.insert(m_SubDirectories.end(), + new DirectoryEntry(name, this, originID, m_FileRegister, m_OriginConnection)); + return *iter; + } else { + return NULL; + } +} + + +DirectoryEntry *DirectoryEntry::getSubDirectoryRecursive(const std::wstring &path, bool create, int originID) +{ + if (path.length() == 0) { + // path ended with a backslash? + return this; + } + + size_t pos = path.find_first_of(L"\\/"); + if (pos == std::wstring::npos) { + return getSubDirectory(path, create); + } else { + DirectoryEntry *nextChild = getSubDirectory(path.substr(0, pos), create, originID); + if (nextChild == NULL) { + return NULL; + } else { + return nextChild->getSubDirectoryRecursive(path.substr(pos + 1), create, originID); + } + } +} + + + +FileRegister::FileRegister(boost::shared_ptr originConnection) + : m_OriginConnection(originConnection) +{ +} + +FileEntry::Index FileRegister::generateIndex() +{ + static FileEntry::Index sIndex = 0; + return sIndex++; +} + +bool FileRegister::indexValid(FileEntry::Index index) const +{ + return m_Files.find(index) != m_Files.end(); +} + +FileEntry &FileRegister::createFile(const std::wstring &name, DirectoryEntry *parent) +{ + FileEntry::Index index = generateIndex(); + m_Files[index] = FileEntry(index, name, parent); + return m_Files[index]; +} + + +FileEntry *FileRegister::getFile(FileEntry::Index index) +{ + auto iter = m_Files.find(index); + if (iter != m_Files.end()) { + return &iter->second; + } + return NULL; +} + + +void FileRegister::unregisterFile(FileEntry &file) +{ + bool ignore; + // unregister from origin + int originID = file.getOrigin(ignore); + m_OriginConnection->getByID(originID).removeFile(file.getIndex()); + const std::vector &alternatives = file.getAlternatives(); + for (auto iter = alternatives.begin(); iter != alternatives.end(); ++iter) { + m_OriginConnection->getByID(*iter).removeFile(file.getIndex()); + } + + // unregister from directory + if (file.getParent() != NULL) { + file.getParent()->removeFile(file.getIndex()); + } +} + + +void FileRegister::removeFile(FileEntry::Index index) +{ + auto iter = m_Files.find(index); + if (iter != m_Files.end()) { + unregisterFile(iter->second); + m_Files.erase(index); + } +} + +void FileRegister::removeOrigin(FileEntry::Index index, int originID) +{ + auto iter = m_Files.find(index); + if (iter != m_Files.end()) { + if (iter->second.removeOrigin(originID)) { + unregisterFile(iter->second); + } + } +} + +void FileRegister::sortOrigins() +{ + for (auto iter = m_Files.begin(); iter != m_Files.end(); ++iter) { + iter->second.sortOrigins(); + } +} diff --git a/src/textviewer.h b/src/textviewer.h deleted file mode 100644 index c0d29607..00000000 --- a/src/textviewer.h +++ /dev/null @@ -1,93 +0,0 @@ -/* -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 . -*/ - -#ifndef TEXTVIEWER_H -#define TEXTVIEWER_H - -#include -#include -#include -#include -#include "loghighlighter.h" - -namespace Ui { - class TextViewer; -} - - -/** - * @brief tabbed text editor - **/ -class TextViewer : public QDialog -{ - Q_OBJECT - -public: - - /** - * @brief constructor - * @param parent parent widget - **/ - explicit TextViewer(QWidget *parent = 0); - - ~TextViewer(); - - /** - * @brief set the description text to inform the user what he's editing - * - * @param description the description to set - **/ - void setDescription(const QString &description); - - /** - * @brief add a new tab with the specified file open - * - * @param fileName name of the file to open - * @param writable if true, the file can be modified - **/ - void addFile(const QString &fileName, bool writable); - -protected: - - void closeEvent(QCloseEvent *event); - bool eventFilter(QObject *obj, QEvent *event); - -private slots: - - void saveFile(); - void modified(); - void patternChanged(QString newPattern); - void findNext(); - -private: - - void saveFile(const QTextEdit *editor); - void find(); - -private: - - Ui::TextViewer *ui; - QTabWidget *m_EditorTabs; - std::set m_Modified; - FindDialog *m_FindDialog; - QString m_FindPattern; - -}; - -#endif // TEXTVIEWER_H diff --git a/src/version.rc b/src/version.rc index c1f85d9f..bc952c2a 100644 --- a/src/version.rc +++ b/src/version.rc @@ -1,7 +1,7 @@ #include "Winver.h" -#define VER_FILEVERSION 0,12,6,0 -#define VER_FILEVERSION_STR 0,12,6,0 +#define VER_FILEVERSION 0,12,7,0 +#define VER_FILEVERSION_STR 0,12,7,0 VS_VERSION_INFO VERSIONINFO FILEVERSION VER_FILEVERSION -- cgit v1.3.1