diff --git a/src/downloadlist.cpp b/src/downloadlist.cpp index f13cdef14..1e31973d6 100644 --- a/src/downloadlist.cpp +++ b/src/downloadlist.cpp @@ -40,7 +40,7 @@ int DownloadList::rowCount(const QModelIndex&) const int DownloadList::columnCount(const QModelIndex&) const { - return 3; + return 4; } @@ -63,6 +63,7 @@ QVariant DownloadList::headerData(int section, Qt::Orientation orientation, int switch (section) { case COL_NAME: return tr("Name"); case COL_FILETIME: return tr("Filetime"); + case COL_SIZE: return tr("Size"); default: return tr("Done"); } } else { diff --git a/src/downloadlist.h b/src/downloadlist.h index 2316dddce..e8833f0f6 100644 --- a/src/downloadlist.h +++ b/src/downloadlist.h @@ -39,7 +39,8 @@ class DownloadList : public QAbstractTableModel enum EColumn { COL_NAME = 0, COL_FILETIME, - COL_STATUS + COL_STATUS, + COL_SIZE }; public: diff --git a/src/downloadlistsortproxy.cpp b/src/downloadlistsortproxy.cpp index 2780f9739..f791617ae 100644 --- a/src/downloadlistsortproxy.cpp +++ b/src/downloadlistsortproxy.cpp @@ -47,6 +47,8 @@ bool DownloadListSortProxy::lessThan(const QModelIndex &left, return m_Manager->getFileTime(leftIndex) < m_Manager->getFileTime(rightIndex); } else if (left.column() == DownloadList::COL_STATUS) { return m_Manager->getState(leftIndex) < m_Manager->getState(rightIndex); + } else if(left.column() == DownloadList::COL_SIZE){ + return m_Manager->getFileSize(leftIndex) < m_Manager->getFileSize(rightIndex); } else { return leftIndex < rightIndex; } diff --git a/src/downloadlistwidget.cpp b/src/downloadlistwidget.cpp index 2af74cc26..ad694107a 100644 --- a/src/downloadlistwidget.cpp +++ b/src/downloadlistwidget.cpp @@ -82,11 +82,30 @@ void DownloadListWidgetDelegate::drawCache(QPainter *painter, const QStyleOption { QRect rect = option.rect; rect.setLeft(0); - rect.setWidth(m_View->columnWidth(0) + m_View->columnWidth(1) + m_View->columnWidth(2)); + rect.setWidth(m_View->columnWidth(0) + m_View->columnWidth(1) + m_View->columnWidth(2) + m_View->columnWidth(3)); painter->drawPixmap(rect, cache); } +QString DownloadListWidgetDelegate::sizeFormat(quint64 size) const +{ + qreal calc = size; + QStringList list; + list << "KB" << "MB" << "GB" << "TB"; + + QStringListIterator i(list); + QString unit("byte(s)"); + + while (calc >= 1024.0 && i.hasNext()) + { + unit = i.next(); + calc /= 1024.0; + } + + return QString().setNum(calc, 'f', 2) + " " + unit; +} + + void DownloadListWidgetDelegate::paintPendingDownload(int downloadIndex) const { std::tuple nexusids = m_Manager->getPendingDownload(downloadIndex); @@ -106,9 +125,9 @@ void DownloadListWidgetDelegate::paintRegularDownload(int downloadIndex) const name.append("..."); } m_NameLabel->setText(name); - m_SizeLabel->setText(QString::number(m_Manager->getFileSize(downloadIndex) / 1024)); + m_SizeLabel->setText(sizeFormat(m_Manager->getFileSize(downloadIndex) )); DownloadManager::DownloadState state = m_Manager->getState(downloadIndex); - if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR)) { + if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR) || (state == DownloadManager::STATE_PAUSING)) { QPalette labelPalette; m_InstallLabel->setVisible(true); m_Progress->setVisible(false); @@ -174,7 +193,7 @@ void DownloadListWidgetDelegate::paint(QPainter *painter, const QStyleOptionView return; } - m_ItemWidget->resize(QSize(m_View->columnWidth(0) + m_View->columnWidth(1) + m_View->columnWidth(2), option.rect.height())); + m_ItemWidget->resize(QSize(m_View->columnWidth(0) + m_View->columnWidth(1) + m_View->columnWidth(2) + m_View->columnWidth(3), option.rect.height())); int downloadIndex = index.data().toInt(); @@ -226,7 +245,11 @@ void DownloadListWidgetDelegate::issueQueryInfo() void DownloadListWidgetDelegate::issueDelete() { - emit removeDownload(m_ContextRow, true); + if (QMessageBox::question(nullptr, tr("Delete Files?"), + tr("This will permanently delete the selected download."), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + emit removeDownload(m_ContextRow, true); + } } void DownloadListWidgetDelegate::issueRemoveFromView() @@ -236,7 +259,22 @@ void DownloadListWidgetDelegate::issueRemoveFromView() void DownloadListWidgetDelegate::issueRestoreToView() { - emit restoreDownload(m_ContextRow); + emit restoreDownload(m_ContextRow); +} + +void DownloadListWidgetDelegate::issueRestoreToViewAll() +{ + emit restoreDownload(-1); +} + +void DownloadListWidgetDelegate::issueVisitOnNexus() +{ + emit visitOnNexus(m_ContextRow); +} + +void DownloadListWidgetDelegate::issueOpenInDownloadsFolder() +{ + emit openInDownloadsFolder(m_ContextRow); } void DownloadListWidgetDelegate::issueCancel() @@ -256,7 +294,7 @@ void DownloadListWidgetDelegate::issueResume() void DownloadListWidgetDelegate::issueDeleteAll() { - if (QMessageBox::question(nullptr, tr("Are you sure?"), + if (QMessageBox::question(nullptr, tr("Delete Files?"), tr("This will remove all finished downloads from this list and from disk."), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { emit removeDownload(-1, true); @@ -265,13 +303,22 @@ void DownloadListWidgetDelegate::issueDeleteAll() void DownloadListWidgetDelegate::issueDeleteCompleted() { - if (QMessageBox::question(nullptr, tr("Are you sure?"), + if (QMessageBox::question(nullptr, tr("Delete Files?"), tr("This will remove all installed downloads from this list and from disk."), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { emit removeDownload(-2, true); } } +void DownloadListWidgetDelegate::issueDeleteUninstalled() +{ + if (QMessageBox::question(nullptr, tr("Delete Files?"), + tr("This will remove all uninstalled downloads from this list and from disk."), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + emit removeDownload(-3, true); + } +} + void DownloadListWidgetDelegate::issueRemoveFromViewAll() { if (QMessageBox::question(nullptr, tr("Are you sure?"), @@ -290,6 +337,15 @@ void DownloadListWidgetDelegate::issueRemoveFromViewCompleted() } } +void DownloadListWidgetDelegate::issueRemoveFromViewUninstalled() +{ + if (QMessageBox::question(nullptr, tr("Are you sure?"), + tr("This will remove all uninstalled downloads from this list (but NOT from disk)."), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + emit removeDownload(-3, false); + } +} + bool DownloadListWidgetDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) { @@ -298,7 +354,7 @@ bool DownloadListWidgetDelegate::editorEvent(QEvent *event, QAbstractItemModel * QModelIndex sourceIndex = qobject_cast(model)->mapToSource(index); if (m_Manager->getState(sourceIndex.row()) >= DownloadManager::STATE_READY) { emit installDownload(sourceIndex.row()); - } else if (m_Manager->getState(sourceIndex.row()) >= DownloadManager::STATE_PAUSED) { + } else if ((m_Manager->getState(sourceIndex.row()) >= DownloadManager::STATE_PAUSED) || (m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_PAUSING)) { emit resumeDownload(sourceIndex.row()); } return true; @@ -315,7 +371,14 @@ bool DownloadListWidgetDelegate::editorEvent(QEvent *event, QAbstractItemModel * menu.addAction(tr("Install"), this, SLOT(issueInstall())); if (m_Manager->isInfoIncomplete(m_ContextRow)) { menu.addAction(tr("Query Info"), this, SLOT(issueQueryInfo())); + }else { + menu.addAction(tr("Visit on Nexus"), this,SLOT(issueVisitOnNexus())); } + + menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder())); + + menu.addSeparator(); + menu.addAction(tr("Delete"), this, SLOT(issueDelete())); if (hidden) { menu.addAction(tr("Un-Hide"), this, SLOT(issueRestoreToView())); @@ -325,20 +388,30 @@ bool DownloadListWidgetDelegate::editorEvent(QEvent *event, QAbstractItemModel * } else if (state == DownloadManager::STATE_DOWNLOADING){ menu.addAction(tr("Cancel"), this, SLOT(issueCancel())); menu.addAction(tr("Pause"), this, SLOT(issuePause())); - } else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR)) { - menu.addAction(tr("Remove"), this, SLOT(issueDelete())); + menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder())); + } else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR) || (state == DownloadManager::STATE_PAUSING)) { + menu.addAction(tr("Delete"), this, SLOT(issueDelete())); menu.addAction(tr("Resume"), this, SLOT(issueResume())); + menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder())); } menu.addSeparator(); } menu.addAction(tr("Delete Installed..."), this, SLOT(issueDeleteCompleted())); + menu.addAction(tr("Delete Uninstalled..."), this, SLOT(issueDeleteUninstalled())); menu.addAction(tr("Delete All..."), this, SLOT(issueDeleteAll())); - if (!hidden) { - menu.addSeparator(); - menu.addAction(tr("Hide Installed..."), this, SLOT(issueRemoveFromViewCompleted())); - menu.addAction(tr("Hide All..."), this, SLOT(issueRemoveFromViewAll())); - } + + if (!hidden) { + menu.addSeparator(); + menu.addAction(tr("Hide Installed..."), this, SLOT(issueRemoveFromViewCompleted())); + menu.addAction(tr("Hide Uninstalled..."), this, SLOT(issueRemoveFromViewUninstalled())); + menu.addAction(tr("Hide All..."), this, SLOT(issueRemoveFromViewAll())); + } + if (hidden) { + menu.addSeparator(); + menu.addAction(tr("Un-Hide All..."), this, SLOT(issueRestoreToViewAll())); + } + menu.exec(mouseEvent->globalPos()); event->accept(); diff --git a/src/downloadlistwidget.h b/src/downloadlistwidget.h index c1dfe4cd5..2dd73e731 100644 --- a/src/downloadlistwidget.h +++ b/src/downloadlistwidget.h @@ -71,14 +71,18 @@ class DownloadListWidgetDelegate : public QItemDelegate void cancelDownload(int index); void pauseDownload(int index); void resumeDownload(int index); + void visitOnNexus(int index); + void openInDownloadsFolder(int index); protected: + QString sizeFormat(quint64 size) const; bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index); private: + void drawCache(QPainter *painter, const QStyleOptionViewItem &option, const QPixmap &cache) const; private slots: @@ -87,13 +91,18 @@ private slots: void issueDelete(); void issueRemoveFromView(); void issueRestoreToView(); + void issueRestoreToViewAll(); + void issueVisitOnNexus(); + void issueOpenInDownloadsFolder(); void issueCancel(); void issuePause(); void issueResume(); void issueDeleteAll(); void issueDeleteCompleted(); + void issueDeleteUninstalled(); void issueRemoveFromViewAll(); void issueRemoveFromViewCompleted(); + void issueRemoveFromViewUninstalled(); void issueQueryInfo(); void stateChanged(int row, DownloadManager::DownloadState); diff --git a/src/downloadlistwidget.ui b/src/downloadlistwidget.ui index 7a6ce8ba2..9e2385092 100644 --- a/src/downloadlistwidget.ui +++ b/src/downloadlistwidget.ui @@ -86,6 +86,9 @@ KB + + + false diff --git a/src/downloadlistwidgetcompact.cpp b/src/downloadlistwidgetcompact.cpp index 898d400ae..663a224ea 100644 --- a/src/downloadlistwidgetcompact.cpp +++ b/src/downloadlistwidgetcompact.cpp @@ -81,18 +81,35 @@ void DownloadListWidgetCompactDelegate::drawCache(QPainter *painter, const QStyl { QRect rect = option.rect; rect.setLeft(0); - rect.setWidth(m_View->columnWidth(0) + m_View->columnWidth(1) + m_View->columnWidth(2)); + rect.setWidth(m_View->columnWidth(0) + m_View->columnWidth(1) + m_View->columnWidth(2) + m_View->columnWidth(3)); painter->drawPixmap(rect, cache); } +QString DownloadListWidgetCompactDelegate::sizeFormat(quint64 size) const +{ + qreal calc = size; + QStringList list; + list << "KB" << "MB" << "GB" << "TB"; + + QStringListIterator i(list); + QString unit("byte(s)"); + + while (calc >= 1024.0 && i.hasNext()) + { + unit = i.next(); + calc /= 1024.0; + } + + return QString().setNum(calc, 'f', 2) + " " + unit; +} void DownloadListWidgetCompactDelegate::paintPendingDownload(int downloadIndex) const { std::tuple nexusids = m_Manager->getPendingDownload(downloadIndex); m_NameLabel->setText(tr("< game %1 mod %2 file %3 >").arg(std::get<0>(nexusids)).arg(std::get<1>(nexusids)).arg(std::get<2>(nexusids))); - if (m_SizeLabel != nullptr) { - m_SizeLabel->setText("???"); - } + //if (m_SizeLabel != nullptr) { + // m_SizeLabel->setText("???"); + //} m_DoneLabel->setVisible(true); m_DoneLabel->setText(tr("Pending")); m_Progress->setVisible(false); @@ -110,11 +127,15 @@ void DownloadListWidgetCompactDelegate::paintRegularDownload(int downloadIndex) DownloadManager::DownloadState state = m_Manager->getState(downloadIndex); - if ((m_SizeLabel != nullptr) && (state >= DownloadManager::STATE_READY)) { - m_SizeLabel->setText(QString::number(m_Manager->getFileSize(downloadIndex) / 1048576)); + if (m_SizeLabel != nullptr) { + m_SizeLabel->setText(sizeFormat(m_Manager->getFileSize(downloadIndex)) + " "); + m_SizeLabel->setVisible(true); } + //else { + // m_SizeLabel->setVisible(false); + //} - if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR)) { + if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR) || (state == DownloadManager::STATE_PAUSING)) { m_DoneLabel->setVisible(true); m_Progress->setVisible(false); m_DoneLabel->setText(QString("%1").arg(tr("Paused"))); @@ -153,7 +174,7 @@ void DownloadListWidgetCompactDelegate::paint(QPainter *painter, const QStyleOpt return; } - m_ItemWidget->resize(QSize(m_View->columnWidth(0) + m_View->columnWidth(1) + m_View->columnWidth(2), option.rect.height())); + m_ItemWidget->resize(QSize(m_View->columnWidth(0) + m_View->columnWidth(1) + m_View->columnWidth(2) + m_View->columnWidth(3), option.rect.height())); if (index.row() % 2 == 1) { m_ItemWidget->setBackgroundRole(QPalette::AlternateBase); } else { @@ -209,7 +230,11 @@ void DownloadListWidgetCompactDelegate::issueQueryInfo() void DownloadListWidgetCompactDelegate::issueDelete() { - emit removeDownload(m_ContextIndex.row(), true); + if (QMessageBox::question(nullptr, tr("Are you sure?"), + tr("This will permanently delete the selected download."), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + emit removeDownload(m_ContextIndex.row(), true); + } } void DownloadListWidgetCompactDelegate::issueRemoveFromView() @@ -217,11 +242,27 @@ void DownloadListWidgetCompactDelegate::issueRemoveFromView() emit removeDownload(m_ContextIndex.row(), false); } +void DownloadListWidgetCompactDelegate::issueVisitOnNexus() +{ + emit visitOnNexus(m_ContextIndex.row()); +} + +void DownloadListWidgetCompactDelegate::issueOpenInDownloadsFolder() +{ + emit openInDownloadsFolder(m_ContextIndex.row()); +} + void DownloadListWidgetCompactDelegate::issueRestoreToView() { - emit restoreDownload(m_ContextIndex.row()); + emit restoreDownload(m_ContextIndex.row()); } +void DownloadListWidgetCompactDelegate::issueRestoreToViewAll() +{ + emit restoreDownload(-1); +} + + void DownloadListWidgetCompactDelegate::issueCancel() { emit cancelDownload(m_ContextIndex.row()); @@ -255,6 +296,15 @@ void DownloadListWidgetCompactDelegate::issueDeleteCompleted() } } +void DownloadListWidgetCompactDelegate::issueDeleteUninstalled() +{ + if (QMessageBox::question(nullptr, tr("Are you sure?"), + tr("This will remove all uninstalled downloads from this list and from disk."), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + emit removeDownload(-3, true); + } +} + void DownloadListWidgetCompactDelegate::issueRemoveFromViewAll() { if (QMessageBox::question(nullptr, tr("Are you sure?"), @@ -273,6 +323,15 @@ void DownloadListWidgetCompactDelegate::issueRemoveFromViewCompleted() } } +void DownloadListWidgetCompactDelegate::issueRemoveFromViewUninstalled() +{ + if (QMessageBox::question(nullptr, tr("Are you sure?"), + tr("This will permanently remove all uninstalled downloads from this list (but NOT from disk)."), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { + emit removeDownload(-3, false); + } +} + bool DownloadListWidgetCompactDelegate::editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index) @@ -282,7 +341,7 @@ bool DownloadListWidgetCompactDelegate::editorEvent(QEvent *event, QAbstractItem QModelIndex sourceIndex = qobject_cast(model)->mapToSource(index); if (m_Manager->getState(sourceIndex.row()) >= DownloadManager::STATE_READY) { emit installDownload(sourceIndex.row()); - } else if (m_Manager->getState(sourceIndex.row()) >= DownloadManager::STATE_PAUSED) { + } else if ((m_Manager->getState(sourceIndex.row()) >= DownloadManager::STATE_PAUSED) || (m_Manager->getState(sourceIndex.row()) == DownloadManager::STATE_PAUSING)) { emit resumeDownload(sourceIndex.row()); } return true; @@ -299,7 +358,11 @@ bool DownloadListWidgetCompactDelegate::editorEvent(QEvent *event, QAbstractItem menu.addAction(tr("Install"), this, SLOT(issueInstall())); if (m_Manager->isInfoIncomplete(m_ContextIndex.row())) { menu.addAction(tr("Query Info"), this, SLOT(issueQueryInfo())); + }else { + menu.addAction(tr("Visit on Nexus"), this, SLOT(issueVisitOnNexus())); } + menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder())); + menu.addSeparator(); menu.addAction(tr("Delete"), this, SLOT(issueDelete())); if (hidden) { menu.addAction(tr("Un-Hide"), this, SLOT(issueRestoreToView())); @@ -309,20 +372,27 @@ bool DownloadListWidgetCompactDelegate::editorEvent(QEvent *event, QAbstractItem } else if (state == DownloadManager::STATE_DOWNLOADING){ menu.addAction(tr("Cancel"), this, SLOT(issueCancel())); menu.addAction(tr("Pause"), this, SLOT(issuePause())); - } else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR)) { + menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder())); + } else if ((state == DownloadManager::STATE_PAUSED) || (state == DownloadManager::STATE_ERROR) || (state == DownloadManager::STATE_PAUSING)) { menu.addAction(tr("Remove"), this, SLOT(issueDelete())); menu.addAction(tr("Resume"), this, SLOT(issueResume())); + menu.addAction(tr("Show in Folder"), this, SLOT(issueOpenInDownloadsFolder())); } - menu.addSeparator(); } menu.addAction(tr("Delete Installed..."), this, SLOT(issueDeleteCompleted())); + menu.addAction(tr("Delete Uninstalled..."), this, SLOT(issueDeleteUninstalled())); menu.addAction(tr("Delete All..."), this, SLOT(issueDeleteAll())); if (!hidden) { menu.addSeparator(); menu.addAction(tr("Hide Installed..."), this, SLOT(issueRemoveFromViewCompleted())); + menu.addAction(tr("Hide Uninstalled..."), this, SLOT(issueRemoveFromViewUninstalled())); menu.addAction(tr("Hide All..."), this, SLOT(issueRemoveFromViewAll())); } + if (hidden) { + menu.addSeparator(); + menu.addAction(tr("Un-Hide All..."), this, SLOT(issueRestoreToViewAll())); + } menu.exec(mouseEvent->globalPos()); event->accept(); @@ -335,4 +405,3 @@ bool DownloadListWidgetCompactDelegate::editorEvent(QEvent *event, QAbstractItem return QItemDelegate::editorEvent(event, model, option, index); } - diff --git a/src/downloadlistwidgetcompact.h b/src/downloadlistwidgetcompact.h index bf855d5f7..b1b3c6176 100644 --- a/src/downloadlistwidgetcompact.h +++ b/src/downloadlistwidgetcompact.h @@ -35,7 +35,7 @@ class DownloadListWidgetCompact; class DownloadListWidgetCompact : public QWidget { Q_OBJECT - + public: explicit DownloadListWidgetCompact(QWidget *parent = 0); ~DownloadListWidgetCompact(); @@ -69,9 +69,12 @@ class DownloadListWidgetCompactDelegate : public QItemDelegate void cancelDownload(int index); void pauseDownload(int index); void resumeDownload(int index); + void visitOnNexus(int index); + void openInDownloadsFolder(int index); protected: + QString sizeFormat(quint64 size) const; bool editorEvent(QEvent *event, QAbstractItemModel *model, const QStyleOptionViewItem &option, const QModelIndex &index); @@ -87,13 +90,18 @@ private slots: void issueDelete(); void issueRemoveFromView(); void issueRestoreToView(); + void issueRestoreToViewAll(); + void issueVisitOnNexus(); + void issueOpenInDownloadsFolder(); void issueCancel(); void issuePause(); void issueResume(); void issueDeleteAll(); void issueDeleteCompleted(); + void issueDeleteUninstalled(); void issueRemoveFromViewAll(); void issueRemoveFromViewCompleted(); + void issueRemoveFromViewUninstalled(); void issueQueryInfo(); void stateChanged(int row, DownloadManager::DownloadState); @@ -119,4 +127,3 @@ private slots: }; #endif // DOWNLOADLISTWIDGETCOMPACT_H - diff --git a/src/downloadlistwidgetcompact.ui b/src/downloadlistwidgetcompact.ui index b43f1fb35..a3fa958c4 100644 --- a/src/downloadlistwidgetcompact.ui +++ b/src/downloadlistwidgetcompact.ui @@ -19,7 +19,7 @@ true - + 2 @@ -58,18 +58,25 @@ - + Qt::Horizontal - 40 + 10 20 + + + + + + + diff --git a/src/downloadmanager.cpp b/src/downloadmanager.cpp index bfc4de3ea..33899f465 100644 --- a/src/downloadmanager.cpp +++ b/src/downloadmanager.cpp @@ -36,6 +36,7 @@ along with Mod Organizer. If not, see . #include #include #include +#include #include #include #include @@ -54,6 +55,7 @@ using namespace MOBase; static const char UNFINISHED[] = ".unfinished"; unsigned int DownloadManager::DownloadInfo::s_NextDownloadID = 1U; +int DownloadManager::m_DirWatcherDisabler = 0; DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createNew(const ModRepositoryFileInfo *fileInfo, const QStringList &URLs) @@ -62,7 +64,7 @@ DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createNew(const Mo info->m_DownloadID = s_NextDownloadID++; info->m_StartTime.start(); info->m_PreResumeSize = 0LL; - info->m_Progress = std::make_pair(0, "0 bytes/sec"); + info->m_Progress = std::make_pair(0, " 0.0 Bytes/s "); info->m_ResumePos = 0; info->m_FileInfo = new ModRepositoryFileInfo(*fileInfo); info->m_Urls = URLs; @@ -70,6 +72,7 @@ DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createNew(const Mo info->m_Tries = AUTOMATIC_RETRIES; info->m_State = STATE_STARTED; info->m_TaskProgressId = TaskProgressManager::instance().getId(); + info->m_Reply = nullptr; return info; } @@ -136,10 +139,30 @@ DownloadManager::DownloadInfo *DownloadManager::DownloadInfo::createFromMeta(con info->m_FileInfo->fileCategory = metaFile.value("fileCategory", 0).toInt(); info->m_FileInfo->repository = metaFile.value("repository", "Nexus").toString(); info->m_FileInfo->userData = metaFile.value("userData").toMap(); + info->m_Reply = nullptr; return info; } +void DownloadManager::startDisableDirWatcher() +{ + DownloadManager::m_DirWatcherDisabler++; +} + + +void DownloadManager::endDisableDirWatcher() +{ + if (DownloadManager::m_DirWatcherDisabler > 0) + { + if (DownloadManager::m_DirWatcherDisabler == 1) + QCoreApplication::processEvents(); + DownloadManager::m_DirWatcherDisabler--; + } + else { + DownloadManager::m_DirWatcherDisabler = 0; + } +} + void DownloadManager::DownloadInfo::setName(QString newName, bool renameFile) { QString oldMetaFileName = QString("%1.meta").arg(m_FileName); @@ -182,6 +205,9 @@ DownloadManager::DownloadManager(NexusInterface *nexusInterface, QObject *parent m_DateExpression("/Date\\((\\d+)\\)/") { connect(&m_DirWatcher, SIGNAL(directoryChanged(QString)), this, SLOT(directoryChanged(QString))); + m_TimeoutTimer.setSingleShot(false); + //connect(&m_TimeoutTimer, SIGNAL(timeout()), this, SLOT(checkDownloadTimeout())); + m_TimeoutTimer.start(5 * 1000); } @@ -204,8 +230,20 @@ bool DownloadManager::downloadsInProgress() return false; } +bool DownloadManager::downloadsInProgressNoPause() +{ + for (QVector::iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter) { + if ((*iter)->m_State < STATE_READY && (*iter)->m_State != STATE_PAUSED) { + return true; + } + } + return false; +} + + void DownloadManager::pauseAll() { + // first loop: pause all downloads for (int i = 0; i < m_ActiveDownloads.count(); ++i) { if (m_ActiveDownloads[i]->m_State < STATE_READY) { @@ -233,6 +271,7 @@ void DownloadManager::pauseAll() ::Sleep(100); } } + } @@ -271,9 +310,17 @@ void DownloadManager::setPluginContainer(PluginContainer *pluginContainer) m_NexusInterface->setPluginContainer(pluginContainer); } + + + + + void DownloadManager::refreshList() { try { + //avoid triggering other refreshes + startDisableDirWatcher(); + int downloadsBefore = m_ActiveDownloads.size(); // remove finished downloads @@ -330,10 +377,14 @@ void DownloadManager::refreshList() } } - if (m_ActiveDownloads.size() != downloadsBefore) { + //if (m_ActiveDownloads.size() != downloadsBefore) { qDebug("downloads after refresh: %d", m_ActiveDownloads.size()); - } + //} emit update(-1); + + //let watcher trigger refreshes again + endDisableDirWatcher(); + } catch (const std::bad_alloc&) { reportError(tr("Memory allocation error (in refreshing directory).")); } @@ -351,6 +402,7 @@ bool DownloadManager::addDownload(const QStringList &URLs, QString gameName, QUrl preferredUrl = QUrl::fromEncoded(URLs.first().toLocal8Bit()); qDebug("selected download url: %s", qPrintable(preferredUrl.toString())); QNetworkRequest request(preferredUrl); + request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent()); return addDownload(m_NexusInterface->getAccessManager()->get(request), URLs, fileName, gameName, modID, fileID, fileInfo); } @@ -382,7 +434,10 @@ bool DownloadManager::addDownload(QNetworkReply *reply, const QStringList &URLs, baseName = dispoName; } } + + startDisableDirWatcher(); newDownload->setName(getDownloadFileName(baseName), false); + endDisableDirWatcher(); startDownload(reply, newDownload, false); // emit update(-1); @@ -452,7 +507,9 @@ void DownloadManager::startDownload(QNetworkReply *reply, DownloadInfo *newDownl else setState(newDownload, STATE_CANCELING); } else { + startDisableDirWatcher(); newDownload->setName(getDownloadFileName(newDownload->m_FileName, true), true); + endDisableDirWatcher(); if (newDownload->m_State == STATE_PAUSED) resumeDownload(indexByName(newDownload->m_FileName)); else @@ -524,6 +581,9 @@ void DownloadManager::addNXMDownload(const QString &url) void DownloadManager::removeFile(int index, bool deleteFile) { + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + if (index >= m_ActiveDownloads.size()) { throw MyException(tr("remove: invalid download index %1").arg(index)); } @@ -534,6 +594,7 @@ void DownloadManager::removeFile(int index, bool deleteFile) (download->m_State == STATE_DOWNLOADING)) { // shouldn't have been possible qCritical("tried to remove active download"); + endDisableDirWatcher(); return; } @@ -544,6 +605,7 @@ void DownloadManager::removeFile(int index, bool deleteFile) if (deleteFile) { if (!shellDelete(QStringList(filePath), true)) { reportError(tr("failed to delete %1").arg(filePath)); + endDisableDirWatcher(); return; } @@ -553,8 +615,11 @@ void DownloadManager::removeFile(int index, bool deleteFile) } } else { QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat); - metaSettings.setValue("removed", true); + if(!download->m_Hidden) + metaSettings.setValue("removed", true); } + + endDisableDirWatcher(); } class LessThanWrapper @@ -591,34 +656,64 @@ void DownloadManager::refreshAlphabeticalTranslation() void DownloadManager::restoreDownload(int index) { - if ((index < 0) || (index >= m_ActiveDownloads.size())) { - throw MyException(tr("restore: invalid download index: %1").arg(index)); - } - DownloadInfo *download = m_ActiveDownloads.at(index); - download->m_Hidden = false; + if (index < 0) { + DownloadState minState = STATE_READY ; + index = 0; - QString filePath = m_OutputDirectory + "/" + download->m_FileName; + for (QVector::const_iterator iter = m_ActiveDownloads.begin(); iter != m_ActiveDownloads.end(); ++iter ) { - QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat); - metaSettings.setValue("removed", false); + if ((*iter)->m_State >= minState) { + restoreDownload(index); + } + index++; + } + } + else { + if (index >= m_ActiveDownloads.size()) { + throw MyException(tr("restore: invalid download index: %1").arg(index)); + } + + DownloadInfo *download = m_ActiveDownloads.at(index); + if (download->m_Hidden) { + download->m_Hidden = false; + + QString filePath = m_OutputDirectory + "/" + download->m_FileName; + + //avoid dirWatcher triggering refreshes + startDisableDirWatcher(); + QSettings metaSettings(filePath.append(".meta"), QSettings::IniFormat); + metaSettings.setValue("removed", false); + + endDisableDirWatcher(); + } + } } void DownloadManager::removeDownload(int index, bool deleteFile) { try { + //avoid dirWatcher triggering refreshes + startDisableDirWatcher(); + emit aboutToUpdate(); if (index < 0) { - DownloadState minState = index == -1 ? STATE_READY : STATE_INSTALLED; + DownloadState minState; + if (index == -3) { + minState = STATE_UNINSTALLED; + } + else + minState = index == -1 ? STATE_READY : STATE_INSTALLED; + 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); - QCoreApplication::processEvents(); + //QCoreApplication::processEvents(); } else { ++iter; @@ -628,6 +723,8 @@ void DownloadManager::removeDownload(int index, bool deleteFile) } else { if (index >= m_ActiveDownloads.size()) { reportError(tr("remove: invalid download index %1").arg(index)); + //emit update(-1); + endDisableDirWatcher(); return; } @@ -636,9 +733,11 @@ void DownloadManager::removeDownload(int index, bool deleteFile) m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); } emit update(-1); + endDisableDirWatcher(); } catch (const std::exception &e) { qCritical("failed to remove download: %s", e.what()); } + refreshList(); } @@ -695,13 +794,20 @@ void DownloadManager::resumeDownloadInt(int index) DownloadInfo *info = m_ActiveDownloads[index]; // Check for finished download; - if (info->m_TotalSize <= info->m_Output.size()) { + if (info->m_TotalSize <= info->m_Output.size() && info->m_Reply != nullptr + && info->m_Reply->isOpen() && info->m_Reply->isFinished() && info->m_State != STATE_ERROR) { setState(info, STATE_DOWNLOADING); downloadFinished(index); return; } - if (info->isPausedState()) { + if (info->isPausedState() || info->m_State == STATE_PAUSING) { + if (info->m_State == STATE_PAUSING) { + if (info->m_Output.isOpen()) { + info->m_Output.write(info->m_Reply->readAll()); + setState(info, STATE_PAUSED); + } + } if ((info->m_Urls.size() == 0) || ((info->m_Urls.size() == 1) && (info->m_Urls[0].size() == 0))) { emit showMessage(tr("No known download urls. Sorry, this download can't be resumed.")); @@ -712,15 +818,18 @@ void DownloadManager::resumeDownloadInt(int index) } qDebug("request resume from url %s", qPrintable(info->currentURL())); QNetworkRequest request(QUrl::fromEncoded(info->currentURL().toLocal8Bit())); - info->m_ResumePos = info->m_Output.size(); + request.setHeader(QNetworkRequest::UserAgentHeader, m_NexusInterface->getAccessManager()->userAgent()); + if (info->m_State != STATE_ERROR) { + info->m_ResumePos = info->m_Output.size(); + QByteArray rangeHeader = "bytes=" + QByteArray::number(info->m_ResumePos) + "-"; + request.setRawHeader("Range", rangeHeader); + } std::get<0>(info->m_SpeedDiff) = 0; std::get<1>(info->m_SpeedDiff) = 0; std::get<2>(info->m_SpeedDiff) = 0; std::get<3>(info->m_SpeedDiff) = 0; std::get<4>(info->m_SpeedDiff) = 0; qDebug("resume at %lld bytes", info->m_ResumePos); - QByteArray rangeHeader = "bytes=" + QByteArray::number(info->m_ResumePos) + "-"; - request.setRawHeader("Range", rangeHeader); startDownload(m_NexusInterface->getAccessManager()->get(request), info, true); } emit update(index); @@ -790,6 +899,59 @@ void DownloadManager::queryInfo(int index) setState(info, STATE_FETCHINGMODINFO); } +void DownloadManager::visitOnNexus(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("VisitNexus: invalid download index %1").arg(index)); + return; + } + DownloadInfo *info = m_ActiveDownloads[index]; + + if (info->m_FileInfo->repository != "Nexus") { + qWarning("Visiting mod page is currently only possible with Nexus"); + return; + } + + if (info->m_State < DownloadManager::STATE_READY) { + // UI shouldn't allow this + return; + } + int modID = info->m_FileInfo->modID; + + QString gameName = info->m_FileInfo->gameName; + if (modID > 0) { + QDesktopServices::openUrl(QUrl(m_NexusInterface->getModURL(modID, gameName))); + } + else { + emit showMessage(tr("Nexus ID for this Mod is unknown")); + } +} + +void DownloadManager::openInDownloadsFolder(int index) +{ + if ((index < 0) || (index >= m_ActiveDownloads.size())) { + reportError(tr("VisitNexus: invalid download index %1").arg(index)); + return; + } + QString params = "/select,\""; + QDir path = QDir(m_OutputDirectory); + if (path.exists(getFileName(index))) { + params = params + QDir::toNativeSeparators(getFilePath(index)) + "\""; + + ::ShellExecuteW(nullptr, nullptr, L"explorer", ToWString(params).c_str(), nullptr, SW_SHOWNORMAL); + return; + } + else if (path.exists(getFileName(index) + ".unfinished")) { + params = params + QDir::toNativeSeparators(getFilePath(index)+".unfinished") + "\""; + + ::ShellExecuteW(nullptr, nullptr, L"explorer", ToWString(params).c_str(), nullptr, SW_SHOWNORMAL); + return; + } + + ::ShellExecuteW(nullptr, L"explore", ToWString(QDir::toNativeSeparators(m_OutputDirectory)).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + return; +} + int DownloadManager::numTotalDownloads() const { @@ -960,11 +1122,16 @@ void DownloadManager::markInstalled(int index) throw MyException(tr("mark installed: invalid download index %1").arg(index)); } + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + DownloadInfo *info = m_ActiveDownloads.at(index); QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); metaFile.setValue("installed", true); metaFile.setValue("uninstalled", false); + endDisableDirWatcher(); + setState(m_ActiveDownloads.at(index), STATE_INSTALLED); } @@ -976,10 +1143,15 @@ void DownloadManager::markInstalled(QString fileName) } else { DownloadInfo *info = getDownloadInfo(fileName); if (info != nullptr) { + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); metaFile.setValue("installed", true); metaFile.setValue("uninstalled", false); delete info; + + endDisableDirWatcher(); } } } @@ -995,10 +1167,15 @@ void DownloadManager::markUninstalled(int index) throw MyException(tr("mark uninstalled: invalid download index %1").arg(index)); } + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + DownloadInfo *info = m_ActiveDownloads.at(index); QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); metaFile.setValue("uninstalled", true); + endDisableDirWatcher(); + setState(m_ActiveDownloads.at(index), STATE_UNINSTALLED); } @@ -1012,9 +1189,15 @@ void DownloadManager::markUninstalled(QString fileName) QString filePath = QDir::fromNativeSeparators(m_OutputDirectory) + "/" + fileName; DownloadInfo *info = getDownloadInfo(filePath); if (info != nullptr) { + + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + QSettings metaFile(info->m_Output.fileName() + ".meta", QSettings::IniFormat); metaFile.setValue("uninstalled", true); delete info; + + endDisableDirWatcher(); } } } @@ -1109,6 +1292,7 @@ void DownloadManager::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) try { DownloadInfo *info = findDownload(this->sender(), &index); if (info != nullptr) { + info->m_HasData = true; if (info->m_State == STATE_CANCELING) { setState(info, STATE_CANCELED); } else if (info->m_State == STATE_PAUSING) { @@ -1134,19 +1318,19 @@ void DownloadManager::downloadProgress(qint64 bytesReceived, qint64 bytesTotal) double speed = (std::get<4>(info->m_SpeedDiff) * 1000.0) / (5 * 1000); QString unit; - if (speed < 1024) { - unit = "bytes/sec"; + if (speed < 1000) { + unit = "Bytes/s"; } - else if (speed < 1024 * 1024) { + else if (speed < 1000*1024) { speed /= 1024; - unit = "kB/s"; + unit = "KB/s"; } else { speed /= 1024 * 1024; unit = "MB/s"; } - info->m_Progress.second = QString::fromLatin1("%1% - %2 %3").arg(info->m_Progress.first).arg(speed, 3, 'f', 1).arg(unit); + info->m_Progress.second = QString::fromLatin1("%1% - %2 %3").arg(info->m_Progress.first).arg(speed, 8, 'f', 1,' ').arg(unit, -8, ' '); TaskProgressManager::instance().updateProgress(info->m_TaskProgressId, bytesReceived, bytesTotal); emit update(index); @@ -1173,6 +1357,9 @@ void DownloadManager::downloadReadyRead() void DownloadManager::createMetaFile(DownloadInfo *info) { + //Avoid triggering refreshes from DirWatcher + startDisableDirWatcher(); + QSettings metaFile(QString("%1.meta").arg(info->m_Output.fileName()), QSettings::IniFormat); metaFile.setValue("gameName", info->m_FileInfo->gameName); metaFile.setValue("modID", info->m_FileInfo->modID); @@ -1194,6 +1381,7 @@ void DownloadManager::createMetaFile(DownloadInfo *info) (info->m_State == DownloadManager::STATE_ERROR)); metaFile.setValue("removed", info->m_Hidden); + endDisableDirWatcher(); // slightly hackish... for (int i = 0; i < m_ActiveDownloads.size(); ++i) { if (m_ActiveDownloads[i] == info) { @@ -1491,7 +1679,7 @@ void DownloadManager::downloadFinished(int index) if (info != nullptr) { QNetworkReply *reply = info->m_Reply; QByteArray data; - if (reply->isOpen()) { + if (reply->isOpen() && info->m_HasData) { data = reply->readAll(); info->m_Output.write(data); } @@ -1506,39 +1694,35 @@ void DownloadManager::downloadFinished(int index) emit showMessage(tr("Warning: Content type is: %1").arg(reply->header(QNetworkRequest::ContentTypeHeader).toString())); if ((info->m_Output.size() == 0) || ((reply->error() != QNetworkReply::NoError) - && (reply->error() != QNetworkReply::OperationCanceledError) - && (reply->error() == QNetworkReply::UnknownContentError && (info->m_Output.size() != reply->header(QNetworkRequest::ContentLengthHeader).toLongLong())))) { + && (reply->error() != QNetworkReply::OperationCanceledError))) { if (reply->error() == QNetworkReply::UnknownContentError) emit showMessage(tr("Download header content length: %1 downloaded file size: %2").arg(reply->header(QNetworkRequest::ContentLengthHeader).toLongLong()).arg(info->m_Output.size())); if (info->m_Tries == 0) { emit showMessage(tr("Download failed: %1 (%2)").arg(reply->errorString()).arg(reply->error())); } error = true; - setState(info, STATE_PAUSING); + setState(info, STATE_ERROR); } } if (info->m_State == STATE_CANCELING) { setState(info, STATE_CANCELED); } else if (info->m_State == STATE_PAUSING) { - if (info->m_Output.isOpen()) { + if (info->m_Output.isOpen() && info->m_HasData) { info->m_Output.write(info->m_Reply->readAll()); } - - if (error) { - setState(info, STATE_ERROR); - } else { - setState(info, STATE_PAUSED); - } + setState(info, STATE_PAUSED); } - if (info->m_State == STATE_CANCELED) { + if (info->m_State == STATE_CANCELED || (info->m_Tries == 0 && error)) { emit aboutToUpdate(); info->m_Output.remove(); delete info; m_ActiveDownloads.erase(m_ActiveDownloads.begin() + index); + if (error) + emit showMessage(tr("We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers.")); emit update(-1); - } else if (info->isPausedState()) { + } else if (info->isPausedState() || info->m_State == STATE_PAUSING) { info->m_Output.close(); createMetaFile(info); emit update(index); @@ -1567,11 +1751,14 @@ void DownloadManager::downloadFinished(int index) QString newName = getFileNameFromNetworkReply(reply); QString oldName = QFileInfo(info->m_Output).fileName(); + + startDisableDirWatcher(); if (!newName.isEmpty() && (oldName.isEmpty())) { info->setName(getDownloadFileName(newName), true); } else { info->setName(m_OutputDirectory + "/" + info->m_FileName, true); // don't rename but remove the ".unfinished" extension } + endDisableDirWatcher(); if (!isNexus) { setState(info, STATE_READY); @@ -1611,7 +1798,9 @@ void DownloadManager::metaDataChanged() if (info != nullptr) { QString newName = getFileNameFromNetworkReply(info->m_Reply); if (!newName.isEmpty() && (info->m_FileName.isEmpty())) { + startDisableDirWatcher(); info->setName(getDownloadFileName(newName), true); + endDisableDirWatcher(); refreshAlphabeticalTranslation(); if (!info->m_Output.isOpen() && !info->m_Output.open(QIODevice::WriteOnly | QIODevice::Append)) { reportError(tr("failed to re-open %1").arg(info->m_FileName)); @@ -1625,10 +1814,24 @@ void DownloadManager::metaDataChanged() void DownloadManager::directoryChanged(const QString&) { - refreshList(); + if(DownloadManager::m_DirWatcherDisabler==0) + refreshList(); } void DownloadManager::managedGameChanged(MOBase::IPluginGame const *managedGame) { m_ManagedGame = managedGame; } + +void DownloadManager::checkDownloadTimeout() +{ + for (int i = 0; i < m_ActiveDownloads.size(); ++i) { + if (m_ActiveDownloads[i]->m_StartTime.elapsed() - std::get<3>(m_ActiveDownloads[i]->m_SpeedDiff) > 5 * 1000 && + m_ActiveDownloads[i]->m_State == STATE_DOWNLOADING && m_ActiveDownloads[i]->m_Reply != nullptr && + m_ActiveDownloads[i]->m_Reply->isOpen()) { + pauseDownload(i); + downloadFinished(i); + resumeDownload(i); + } + } +} diff --git a/src/downloadmanager.h b/src/downloadmanager.h index 98f5e4687..136ecf2aa 100644 --- a/src/downloadmanager.h +++ b/src/downloadmanager.h @@ -29,6 +29,7 @@ along with Mod Organizer. If not, see . #include #include #include +#include #include #include #include @@ -77,6 +78,7 @@ class DownloadManager : public MOBase::IDownloadManager qint64 m_PreResumeSize; std::pair m_Progress; std::tuple m_SpeedDiff; + bool m_HasData; DownloadState m_State; int m_CurrentUrl; QStringList m_Urls; @@ -114,7 +116,7 @@ class DownloadManager : public MOBase::IDownloadManager private: static unsigned int s_NextDownloadID; private: - DownloadInfo() : m_TotalSize(0), m_ReQueried(false), m_Hidden(false), m_SpeedDiff(std::tuple(0,0,0,0,0)) {} + DownloadInfo() : m_TotalSize(0), m_ReQueried(false), m_Hidden(false), m_SpeedDiff(std::tuple(0,0,0,0,0)), m_HasData(false) {} }; public: @@ -136,6 +138,13 @@ class DownloadManager : public MOBase::IDownloadManager **/ bool downloadsInProgress(); + /** + * @brief determine if a download is currently in progress, does not count paused ones. + * + * @return true if there is currently a download in progress (that is not paused already). + **/ + bool downloadsInProgressNoPause(); + /** * @brief set the output directory to write to * @@ -143,6 +152,17 @@ class DownloadManager : public MOBase::IDownloadManager **/ void setOutputDirectory(const QString &outputDirectory); + /** + * @brief disables feedback from the downlods fileSystemWhatcher untill disableDownloadsWatcherEnd() is called + * + **/ + static void startDisableDirWatcher(); + + /** + * @brief re-enables feedback from the downlods fileSystemWhatcher after disableDownloadsWatcherStart() was called + **/ + static void endDisableDirWatcher(); + /** * @return current download directory **/ @@ -423,6 +443,10 @@ public slots: void queryInfo(int index); + void visitOnNexus(int index); + + void openInDownloadsFolder(int index); + void nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); void nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID); @@ -443,6 +467,7 @@ private slots: void downloadError(QNetworkReply::NetworkError error); void metaDataChanged(); void directoryChanged(const QString &dirctory); + void checkDownloadTimeout(); private: @@ -517,6 +542,12 @@ private slots: QFileSystemWatcher m_DirWatcher; + //The dirWatcher is actually triggering off normal Mo operations such as deleting downloads or editing .meta files + //so it needs to be disabled during operations that are known to cause the creation or deletion of files in the Downloads folder. + //Notably using QSettings to edit a file creates a temporarily .lock file that causes the Watcher to trigger multiple listRefreshes freezing the ui. + static int m_DirWatcherDisabler; + + std::map m_DownloadFails; bool m_ShowHidden; @@ -524,6 +555,8 @@ private slots: QRegExp m_DateExpression; MOBase::IPluginGame const *m_ManagedGame; + + QTimer m_TimeoutTimer; }; diff --git a/src/instancemanager.cpp b/src/instancemanager.cpp index 43ae152b3..1c6542e88 100644 --- a/src/instancemanager.cpp +++ b/src/instancemanager.cpp @@ -131,11 +131,12 @@ QString InstanceManager::manageInstances(const QStringList &instanceList) const QString InstanceManager::queryInstanceName(const QStringList &instanceList) const { QString instanceId; + QString dialogText; while (instanceId.isEmpty()) { QInputDialog dialog; - dialog.setWindowTitle(QObject::tr("Enter a Name for the new Instance")); - dialog.setLabelText(QObject::tr("Enter a new name or select one from the sugested list (only letters and numbers allowed):")); + dialog.setWindowTitle(QObject::tr("Enter a Name for the new Instance")); + dialog.setLabelText(QObject::tr("Enter a new name or select one from the suggested list:")); // would be neat if we could take the names from the game plugins but // the required initialization order requires the ini file to be // available *before* we load plugins @@ -146,7 +147,17 @@ QString InstanceManager::queryInstanceName(const QStringList &instanceList) cons if (dialog.exec() == QDialog::Rejected) { throw MOBase::MyException(QObject::tr("Canceled")); } - instanceId = dialog.textValue().replace(QRegExp("[^0-9a-zA-Z ]"), ""); + dialogText = dialog.textValue(); + instanceId = sanitizeInstanceName(dialogText); + if (instanceId != dialogText) { + if (QMessageBox::question( nullptr, + QObject::tr("Invalid instance name"), + QObject::tr("The instance name \"%1\" is invalid. Use the name \"%2\" instead?").arg(dialogText,instanceId), + QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) { + instanceId=""; + continue; + } + } bool alreadyExists=false; for (const QString &instance : instanceList) { @@ -296,3 +307,21 @@ QString InstanceManager::determineDataPath() } } + +QString InstanceManager::sanitizeInstanceName(const QString &name) const +{ + QString new_name = name; + + // Restrict the allowed characters + new_name = new_name.remove(QRegExp("[^A-Za-z0-9 _=+;!@#$%^'\\-\\.\\[\\]\\{\\}\\(\\)]")); + + // Don't end in spaces and periods + new_name = new_name.remove(QRegExp("\\.*$")); + new_name = new_name.remove(QRegExp(" *$")); + + // Recurse until stuff stops changing + if (new_name != name) { + return sanitizeInstanceName(new_name); + } + return new_name; +} \ No newline at end of file diff --git a/src/instancemanager.h b/src/instancemanager.h index adedd78f9..4efa6f033 100644 --- a/src/instancemanager.h +++ b/src/instancemanager.h @@ -50,6 +50,7 @@ class InstanceManager { QString manageInstances(const QStringList &instanceList) const; + QString sanitizeInstanceName(const QString &name) const; void setCurrentInstance(const QString &name); QString queryInstanceName(const QStringList &instanceList) const; diff --git a/src/main.cpp b/src/main.cpp index 9c30a1c61..bfc3b926c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -446,11 +446,7 @@ static void preloadSsl() static QString getVersionDisplayString() { - VS_FIXEDFILEINFO version = GetFileVersion(ToWString(QApplication::applicationFilePath())); - return VersionInfo(version.dwFileVersionMS >> 16, - version.dwFileVersionMS & 0xFFFF, - version.dwFileVersionLS >> 16, - version.dwFileVersionLS & 0xFFFF).displayString(); + return createVersionInfo().displayString(); } int runApplication(MOApplication &application, SingleInstance &instance, diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 2830c776a..f6cfbfbd2 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -242,7 +242,28 @@ MainWindow::MainWindow(QSettings &initSettings updateProblemsButton(); - updateToolBar(); + // Setup toolbar + QWidget *spacer = new QWidget(ui->toolBar); + spacer->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred); + QWidget *widget = ui->toolBar->widgetForAction(ui->actionTool); + QToolButton *toolBtn = qobject_cast(widget); + + if (toolBtn->menu() == nullptr) { + actionToToolButton(ui->actionTool); + } + + actionToToolButton(ui->actionHelp); + createHelpWidget(); + + for (QAction *action : ui->toolBar->actions()) { + if (action->isSeparator()) { + // insert spacers + ui->toolBar->insertWidget(action, spacer); + m_Sep = action; + // m_Sep would only use the last seperator anyway, and we only have the one anyway? + break; + } + } TaskProgressManager::instance().tryCreateTaskbar(); @@ -379,7 +400,7 @@ MainWindow::MainWindow(QSettings &initSettings new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Enter), this, SLOT(openExplorer_activated())); new QShortcut(QKeySequence(Qt::CTRL + Qt::Key_Return), this, SLOT(openExplorer_activated())); - + new QShortcut(QKeySequence::Refresh, this, SLOT(refreshProfile_activated())); @@ -560,41 +581,22 @@ void MainWindow::updateToolBar() for (QAction *action : ui->toolBar->actions()) { if (action->objectName().startsWith("custom__")) { ui->toolBar->removeAction(action); + action->deleteLater(); } } - 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() == nullptr) { - actionToToolButton(ui->actionTool); - } - - actionToToolButton(ui->actionHelp); - createHelpWidget(); - - for (QAction *action : ui->toolBar->actions()) { - if (action->isSeparator()) { - // insert spacers - ui->toolBar->insertWidget(action, spacer); - - std::vector::iterator begin, end; - m_OrganizerCore.executablesList()->getExecutables(begin, end); - for (auto iter = begin; iter != end; ++iter) { - if (iter->isShownOnToolbar()) { - QAction *exeAction = new QAction(iconForExecutable(iter->m_BinaryInfo.filePath()), - iter->m_Title, - ui->toolBar); - 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); - } + std::vector::iterator begin, end; + m_OrganizerCore.executablesList()->getExecutables(begin, end); + for (auto iter = begin; iter != end; ++iter) { + if (iter->isShownOnToolbar()) { + QAction *exeAction = new QAction(iconForExecutable(iter->m_BinaryInfo.filePath()), + iter->m_Title, + ui->toolBar); + exeAction->setObjectName(QString("custom__") + iter->m_Title); + if (!connect(exeAction, SIGNAL(triggered()), this, SLOT(startExeAction()))) { + qDebug("failed to connect trigger?"); } + ui->toolBar->insertAction(m_Sep, exeAction); } } } @@ -881,7 +883,7 @@ void MainWindow::closeEvent(QCloseEvent* event) { m_closing = true; - if (m_OrganizerCore.downloadManager()->downloadsInProgress()) { + if (m_OrganizerCore.downloadManager()->downloadsInProgressNoPause()) { if (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) { @@ -2334,9 +2336,11 @@ void MainWindow::removeMod_clicked() 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 + DownloadManager::startDisableDirWatcher(); for (QString name : modNames) { m_OrganizerCore.modList()->removeRowForce(ModInfo::getIndex(name), QModelIndex()); } + DownloadManager::endDisableDirWatcher(); } } else { m_OrganizerCore.modList()->removeRow(m_ContextRow, QModelIndex()); @@ -2498,7 +2502,7 @@ void MainWindow::displayModInformation(ModInfo::Ptr modInfo, unsigned int index, connect(&dialog, SIGNAL(modOpenPrev(int)), this, SLOT(modOpenPrev(int)), Qt::QueuedConnection); connect(&dialog, SIGNAL(originModified(int)), this, SLOT(originModified(int))); connect(&dialog, SIGNAL(endorseMod(ModInfo::Ptr)), this, SLOT(endorseMod(ModInfo::Ptr))); - + //Open the tab first if we want to use the standard indexes of the tabs. if (tab != -1) { dialog.openTab(tab); @@ -2615,20 +2619,44 @@ void MainWindow::displayModInformation(int row, int tab) void MainWindow::ignoreMissingData_clicked() { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - QDir(info->absolutePath()).mkdir("textures"); - info->testValid(); - connect(this, SIGNAL(modListDataChanged(QModelIndex,QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex,QModelIndex))); + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + int row_idx = idx.data(Qt::UserRole + 1).toInt(); + ModInfo::Ptr info = ModInfo::getByIndex(row_idx); + QDir(info->absolutePath()).mkdir("textures"); + info->testValid(); + connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); + + emit modListDataChanged(m_OrganizerCore.modList()->index(row_idx, 0), m_OrganizerCore.modList()->index(row_idx, m_OrganizerCore.modList()->columnCount() - 1)); + } + } else { + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + QDir(info->absolutePath()).mkdir("textures"); + info->testValid(); + connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); - emit modListDataChanged(m_OrganizerCore.modList()->index(m_ContextRow, 0), m_OrganizerCore.modList()->index(m_ContextRow, m_OrganizerCore.modList()->columnCount() - 1)); + emit modListDataChanged(m_OrganizerCore.modList()->index(m_ContextRow, 0), m_OrganizerCore.modList()->index(m_ContextRow, m_OrganizerCore.modList()->columnCount() - 1)); + } } void MainWindow::markConverted_clicked() { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - info->markConverted(true); - connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); - emit modListDataChanged(m_OrganizerCore.modList()->index(m_ContextRow, 0), m_OrganizerCore.modList()->index(m_ContextRow, m_OrganizerCore.modList()->columnCount() - 1)); + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + int row_idx = idx.data(Qt::UserRole + 1).toInt(); + ModInfo::Ptr info = ModInfo::getByIndex(row_idx); + info->markConverted(true); + connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); + emit modListDataChanged(m_OrganizerCore.modList()->index(row_idx, 0), m_OrganizerCore.modList()->index(row_idx, m_OrganizerCore.modList()->columnCount() - 1)); + } + } else { + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + info->markConverted(true); + connect(this, SIGNAL(modListDataChanged(QModelIndex, QModelIndex)), m_OrganizerCore.modList(), SIGNAL(dataChanged(QModelIndex, QModelIndex))); + emit modListDataChanged(m_OrganizerCore.modList()->index(m_ContextRow, 0), m_OrganizerCore.modList()->index(m_ContextRow, m_OrganizerCore.modList()->columnCount() - 1)); + } } @@ -2655,9 +2683,17 @@ void MainWindow::visitWebPage_clicked() void MainWindow::openExplorer_clicked() { - ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); - - ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + ::ShellExecuteW(nullptr, L"explore", ToWString(info->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } + } + else { + ModInfo::Ptr modInfo = ModInfo::getByIndex(m_ContextRow); + ::ShellExecuteW(nullptr, L"explore", ToWString(modInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); + } } void MainWindow::openExplorer_activated() @@ -2685,7 +2721,7 @@ void MainWindow::openExplorer_activated() QModelIndex idx = selection->currentIndex(); QString fileName = idx.data().toString(); - + ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); std::vector flags = modInfo->getFlags(); @@ -2825,13 +2861,83 @@ void MainWindow::on_modList_doubleClicked(const QModelIndex &index) return; } + Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers(); + if (modifiers.testFlag(Qt::ControlModifier)) { + try { + m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); + openExplorer_clicked(); + // 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()); + } + } + else { + try { + m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); + displayModInformation(sourceIdx.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()); + } + } +} + +void MainWindow::on_espList_doubleClicked(const QModelIndex &index) +{ + if (!index.isValid()) { + return; + } + + if (m_OrganizerCore.pluginList()->timeElapsedSinceLastChecked() <= QApplication::doubleClickInterval()) { + // don't interpret double click if we only just checked a plugin + return; + } + + QModelIndex sourceIdx = mapToModel(m_OrganizerCore.pluginList(), index); + if (!sourceIdx.isValid()) { + return; + } try { - m_ContextRow = m_ModListSortProxy->mapToSource(index).row(); - displayModInformation(sourceIdx.row()); - // workaround to cancel the editor that might have opened because of - // selection-click - ui->modList->closePersistentEditor(index); - } catch (const std::exception &e) { + + QItemSelectionModel *selection = ui->espList->selectionModel(); + + if (selection->hasSelection() && selection->selectedRows().count() == 1) { + + QModelIndex idx = selection->currentIndex(); + QString fileName = idx.data().toString(); + + if (ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName)) == UINT_MAX) + return; + + ModInfo::Ptr modInfo = ModInfo::getByIndex(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + std::vector flags = modInfo->getFlags(); + + if (modInfo->isRegular() || (std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end())) { + + Qt::KeyboardModifiers modifiers = QApplication::queryKeyboardModifiers(); + if (modifiers.testFlag(Qt::ControlModifier)) { + openExplorer_activated(); + // workaround to cancel the editor that might have opened because of + // selection-click + ui->espList->closePersistentEditor(index); + } + else { + + displayModInformation(ModInfo::getIndex(m_OrganizerCore.pluginList()->origin(fileName))); + // workaround to cancel the editor that might have opened because of + // selection-click + ui->espList->closePersistentEditor(index); + } + } + } + } + catch (const std::exception &e) { reportError(e.what()); } } @@ -3072,14 +3178,32 @@ void MainWindow::changeVersioningScheme() { } void MainWindow::ignoreUpdate() { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - info->ignoreUpdate(true); + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + info->ignoreUpdate(true); + } + } + else { + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + info->ignoreUpdate(true); + } } void MainWindow::unignoreUpdate() { - ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); - info->ignoreUpdate(false); + QItemSelectionModel *selection = ui->modList->selectionModel(); + if (selection->hasSelection() && selection->selectedRows().count() > 1) { + for (QModelIndex idx : selection->selectedRows()) { + ModInfo::Ptr info = ModInfo::getByIndex(idx.data(Qt::UserRole + 1).toInt()); + info->ignoreUpdate(false); + } + } + else { + ModInfo::Ptr info = ModInfo::getByIndex(m_ContextRow); + info->ignoreUpdate(false); + } } void MainWindow::addPrimaryCategoryCandidates(QMenu *primaryCategoryMenu, @@ -3153,6 +3277,13 @@ void MainWindow::openInstallFolder() ::ShellExecuteW(nullptr, L"explore", ToWString(qApp->applicationDirPath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); } +void MainWindow::openPluginsFolder() +{ + QString pluginsPath = QCoreApplication::applicationDirPath() + "/" + ToQString(AppConfig::pluginPath()); + ::ShellExecuteW(nullptr, L"explore", ToWString(pluginsPath).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +} + + void MainWindow::openProfileFolder() { ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.currentProfile()->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); @@ -3163,6 +3294,11 @@ void MainWindow::openDownloadsFolder() ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getDownloadDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); } +void MainWindow::openModsFolder() +{ + ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.settings().getModDirectory()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +} + void MainWindow::openGameFolder() { ::ShellExecuteW(nullptr, L"explore", ToWString(m_OrganizerCore.managedGame()->gameDirectory().absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); @@ -3356,6 +3492,8 @@ QMenu *MainWindow::openFolderMenu() FolderMenu->addAction(tr("Open Instance folder"), this, SLOT(openInstanceFolder())); + FolderMenu->addAction(tr("Open Mods folder"), this, SLOT(openModsFolder())); + FolderMenu->addAction(tr("Open Profile folder"), this, SLOT(openProfileFolder())); FolderMenu->addAction(tr("Open Downloads folder"), this, SLOT(openDownloadsFolder())); @@ -3364,15 +3502,9 @@ QMenu *MainWindow::openFolderMenu() FolderMenu->addAction(tr("Open MO2 Install folder"), this, SLOT(openInstallFolder())); - FolderMenu->addAction(tr("Open MO2 Logs folder"), this, SLOT(openLogsFolder())); - - - - - - - + FolderMenu->addAction(tr("Open MO2 Plugins folder"), this, SLOT(openPluginsFolder())); + FolderMenu->addAction(tr("Open MO2 Logs folder"), this, SLOT(openLogsFolder())); return FolderMenu; @@ -4199,12 +4331,16 @@ void MainWindow::on_actionUpdate_triggered() void MainWindow::on_actionEndorseMO_triggered() { + // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now + IPluginGame * game = m_OrganizerCore.getGame("skyrimse"); + if (!game) return; + if (QMessageBox::question(this, tr("Endorse Mod Organizer"), tr("Do you want to endorse Mod Organizer on %1 now?").arg( - NexusInterface::instance(&m_PluginContainer)->getGameURL(m_OrganizerCore.managedGame()->gameShortName())), + NexusInterface::instance(&m_PluginContainer)->getGameURL(game->gameShortName())), QMessageBox::Yes | QMessageBox::No) == QMessageBox::Yes) { NexusInterface::instance(&m_PluginContainer)->requestToggleEndorsement( - m_OrganizerCore.managedGame()->gameShortName(), m_OrganizerCore.managedGame()->nexusModOrganizerID(), true, this, QVariant(), QString()); + game->gameShortName(), game->nexusModOrganizerID(), true, this, QVariant(), QString()); } } @@ -4231,11 +4367,13 @@ void MainWindow::updateDownloadListDelegate() connect(ui->downloadFilterEdit, SIGNAL(textChanged(QString)), this, SLOT(downloadFilterChanged(QString))); ui->downloadView->setModel(sortProxy); - ui->downloadView->sortByColumn(1, Qt::DescendingOrder); - ui->downloadView->header()->resizeSections(QHeaderView::Fixed); + //ui->downloadView->sortByColumn(1, Qt::DescendingOrder); + ui->downloadView->header()->resizeSections(QHeaderView::Stretch); connect(ui->downloadView->itemDelegate(), SIGNAL(installDownload(int)), &m_OrganizerCore, SLOT(installDownload(int))); connect(ui->downloadView->itemDelegate(), SIGNAL(queryInfo(int)), m_OrganizerCore.downloadManager(), SLOT(queryInfo(int))); + connect(ui->downloadView->itemDelegate(), SIGNAL(visitOnNexus(int)), m_OrganizerCore.downloadManager(), SLOT(visitOnNexus(int))); + connect(ui->downloadView->itemDelegate(), SIGNAL(openInDownloadsFolder(int)), m_OrganizerCore.downloadManager(), SLOT(openInDownloadsFolder(int))); connect(ui->downloadView->itemDelegate(), SIGNAL(removeDownload(int, bool)), m_OrganizerCore.downloadManager(), SLOT(removeDownload(int, bool))); connect(ui->downloadView->itemDelegate(), SIGNAL(restoreDownload(int)), m_OrganizerCore.downloadManager(), SLOT(restoreDownload(int))); connect(ui->downloadView->itemDelegate(), SIGNAL(cancelDownload(int)), m_OrganizerCore.downloadManager(), SLOT(cancelDownload(int))); @@ -4267,9 +4405,13 @@ void MainWindow::nxmUpdatesAvailable(const std::vector &modIDs, QVariant us QVariantList resultList = resultData.toList(); for (auto iter = resultList.begin(); iter != resultList.end(); ++iter) { QVariantMap result = iter->toMap(); - if (result["id"].toInt() == m_OrganizerCore.managedGame()->nexusModOrganizerID() - && result["game_id"].toInt() == m_OrganizerCore.managedGame()->nexusGameID()) { - if (!result["voted_by_user"].toBool()) { + // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now + IPluginGame * game = m_OrganizerCore.getGame("skyrimse"); + if (game + && result["id"].toInt() == game->nexusModOrganizerID() + && result["game_id"].toInt() == game->nexusGameID()) { + if (result["voted_by_user"].type() != QVariant::Invalid && + !result["voted_by_user"].toBool()) { ui->actionEndorseMO->setVisible(true); } } else { @@ -4293,7 +4435,8 @@ void MainWindow::nxmUpdatesAvailable(const std::vector &modIDs, QVariant us (*iter)->setNewestVersion(result["version"].toString()); (*iter)->setNexusDescription(result["description"].toString()); if (NexusInterface::instance(&m_PluginContainer)->getAccessManager()->loggedIn() && - result.contains("voted_by_user")) { + result.contains("voted_by_user") && + result["voted_by_user"].type() != QVariant::Invalid) { // don't use endorsement info if we're not logged in or if the response doesn't contain it (*iter)->setIsEndorsed(result["voted_by_user"].toBool()); } @@ -4319,7 +4462,7 @@ void MainWindow::nxmEndorsementToggled(QString, int, QVariant, QVariant resultDa { if (resultData.toBool()) { ui->actionEndorseMO->setVisible(false); - QMessageBox::question(this, tr("Thank you!"), tr("Thank you for your endorsement!")); + QMessageBox::information(this, tr("Thank you!"), tr("Thank you for your endorsement!")); } if (!disconnect(sender(), SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), @@ -4637,7 +4780,7 @@ void MainWindow::on_espList_customContextMenuRequested(const QPoint &pos) menu.addAction(tr("Unlock load order"), this, SLOT(unlockESPIndex())); } if (hasUnlocked) { - menu.addAction(tr("Lock load order"), this, SLOT(f())); + menu.addAction(tr("Lock load order"), this, SLOT(lockESPIndex())); } try { @@ -4965,7 +5108,7 @@ bool MainWindow::createBackup(const QString &filePath, const QDateTime &time) QString outPath = filePath + "." + time.toString(PATTERN_BACKUP_DATE); if (shellCopy(QStringList(filePath), QStringList(outPath), this)) { QFileInfo fileInfo(filePath); - removeOldFiles(fileInfo.absolutePath(), fileInfo.fileName() + PATTERN_BACKUP_GLOB, 3, QDir::Name); + removeOldFiles(fileInfo.absolutePath(), fileInfo.fileName() + PATTERN_BACKUP_GLOB, 10, QDir::Name); return true; } else { return false; diff --git a/src/mainwindow.h b/src/mainwindow.h index bbff0d938..6cf833011 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -304,6 +304,8 @@ private slots: Ui::MainWindow *ui; + QAction *m_Sep; // Executable Shortcuts are added after this. Non owning. + bool m_WasVisible; MOBase::TutorialControl m_Tutorial; @@ -498,7 +500,9 @@ private slots: void openInstanceFolder(); void openLogsFolder(); void openInstallFolder(); + void openPluginsFolder(); void openDownloadsFolder(); + void openModsFolder(); void openProfileFolder(); void openGameFolder(); void openMyGamesFolder(); @@ -571,6 +575,7 @@ private slots: // ui slots void on_executablesListBox_currentIndexChanged(int index); void on_modList_customContextMenuRequested(const QPoint &pos); void on_modList_doubleClicked(const QModelIndex &index); + void on_espList_doubleClicked(const QModelIndex &index); void on_profileBox_currentIndexChanged(int index); void on_savegameList_customContextMenuRequested(const QPoint &pos); void on_startButton_clicked(); diff --git a/src/mainwindow.ui b/src/mainwindow.ui index e0aa6f36e..cf0f2aa41 100644 --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -96,9 +96,9 @@
- - false - + + false + 0 @@ -194,7 +194,7 @@ - + Pick a module collection @@ -248,7 +248,7 @@ p, li { white-space: pre-wrap; } 16 - + @@ -265,16 +265,6 @@ p, li { white-space: pre-wrap; } - - - - - - - - - - Restore Backup... @@ -509,7 +499,7 @@ p, li { white-space: pre-wrap; } - + Qt::Horizontal @@ -521,7 +511,7 @@ p, li { white-space: pre-wrap; } - + @@ -536,12 +526,12 @@ p, li { white-space: pre-wrap; } 22 - + 95 0 - + false @@ -567,7 +557,7 @@ p, li { white-space: pre-wrap; } - + Qt::Horizontal @@ -579,14 +569,14 @@ p, li { white-space: pre-wrap; } - - - + + + 220 0 - + Qt::ClickFocus @@ -608,13 +598,13 @@ p, li { white-space: pre-wrap; } - - + + 220 0 - + Namefilter @@ -835,12 +825,12 @@ p, li { white-space: pre-wrap; } - - true - + + true + Sort - + :/MO/gui/sort:/MO/gui/sort @@ -954,6 +944,9 @@ p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'MS Shell Dlg 2'; font-size:8.25pt; font-weight:400; font-style:normal;"> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:8pt;">This list contains the esps, esms, and esls contained in the active mods. These require their own load order. Use drag&amp;drop to modify this load order. Please note that MO will only save the load order for mods that are active/checked.<br />There is a great tool named &quot;BOSS&quot; to automatically sort these files.</span></p></body></html> + + QAbstractItemView::EditKeyPressed|QAbstractItemView::SelectedClicked + true @@ -987,6 +980,9 @@ p, li { white-space: pre-wrap; } true + + false + false @@ -1009,15 +1005,12 @@ p, li { white-space: pre-wrap; } - - - + + false + Archives - - true - 6 @@ -1032,17 +1025,7 @@ p, li { white-space: pre-wrap; } 6 - - - - - - - - - - - + @@ -1072,50 +1055,24 @@ p, li { white-space: pre-wrap; } BSAs checked here are loaded in such a way that your installation order is obeyed properly. - - QAbstractItemView::NoEditTriggers - false - + false - + false - - QAbstractItemView::NoDragDrop - - - Qt::IgnoreAction - - - QAbstractItemView::SingleSelection - - - QAbstractItemView::SelectRows - - + 20 - + true - + 1 - - false - - - 200 - - - - File - - @@ -1286,7 +1243,7 @@ p, li { white-space: pre-wrap; } Qt::ScrollBarAlwaysOn - Qt::ScrollBarAlwaysOff + Qt::ScrollBarAsNeeded true @@ -1319,7 +1276,10 @@ p, li { white-space: pre-wrap; } true - 100 + 50 + + + 15 true @@ -1601,9 +1561,6 @@ Right now this has very limited functionality Copy Log to Clipboard - - Ctrl+C - diff --git a/src/modinfo.cpp b/src/modinfo.cpp index bd4c12543..91f3c1a17 100644 --- a/src/modinfo.cpp +++ b/src/modinfo.cpp @@ -102,6 +102,7 @@ QString ModInfo::getContentTypeName(int contentType) case CONTENT_SKSE: return tr("Script Extender"); case CONTENT_SKYPROC: return tr("SkyProc Tools"); case CONTENT_MCM: return tr("MCM Data"); + case CONTENT_INI: return tr("INI files"); default: throw MyException(tr("invalid content type %1").arg(contentType)); } } @@ -286,9 +287,9 @@ int ModInfo::checkAllForUpdate(PluginContainer *pluginContainer, QObject *receiv int result = 0; std::vector modIDs; - //I ought to store this, it's used elsewhere - IPluginGame const *game = qApp->property("managed_game").value(); - if (game->nexusModOrganizerID()) { + // Normally this would be the managed game but MO2 is only uploaded to the Skyrim SE site right now + IPluginGame const *game = pluginContainer->managedGame("Skyrim Special Edition"); + if (game && game->nexusModOrganizerID()) { modIDs.push_back(game->nexusModOrganizerID()); checkChunkForUpdate(pluginContainer, modIDs, receiver, game->gameShortName()); modIDs.clear(); diff --git a/src/modinfo.h b/src/modinfo.h index 001a78dc4..4cdd7bcf8 100644 --- a/src/modinfo.h +++ b/src/modinfo.h @@ -84,10 +84,11 @@ class ModInfo : public QObject, public MOBase::IModInterface CONTENT_SCRIPT, CONTENT_SKSE, CONTENT_SKYPROC, - CONTENT_MCM + CONTENT_MCM, + CONTENT_INI }; - static const int NUM_CONTENT_TYPES = CONTENT_MCM + 1; + static const int NUM_CONTENT_TYPES = CONTENT_INI + 1; enum EHighlight { HIGHLIGHT_NONE = 0, diff --git a/src/modinforegular.cpp b/src/modinforegular.cpp index c19294f11..5b6ddcda6 100644 --- a/src/modinforegular.cpp +++ b/src/modinforegular.cpp @@ -466,6 +466,10 @@ std::vector ModInfoRegular::getContents() const if (dir.entryList(QStringList() << "*.bsa" << "*.ba2").size() > 0) { m_CachedContent.push_back(CONTENT_BSA); } + //use >1 for ini files since there is meta.ini in all mods already. + if (dir.entryList(QStringList() << "*.ini").size() > 1) { + m_CachedContent.push_back(CONTENT_INI); + } ScriptExtender *extender = qApp->property("managed_game") .value() diff --git a/src/modlist.cpp b/src/modlist.cpp index 2d58081df..0d084c224 100644 --- a/src/modlist.cpp +++ b/src/modlist.cpp @@ -73,6 +73,7 @@ ModList::ModList(PluginContainer *pluginContainer, QObject *parent) m_ContentIcons[ModInfo::CONTENT_SOUND] = std::make_tuple(":/MO/gui/content/sound", tr("Sound or Music")); m_ContentIcons[ModInfo::CONTENT_TEXTURE] = std::make_tuple(":/MO/gui/content/texture", tr("Textures")); m_ContentIcons[ModInfo::CONTENT_MCM] = std::make_tuple(":/MO/gui/content/menu", tr("MCM Configuration")); + m_ContentIcons[ModInfo::CONTENT_INI] = std::make_tuple(":/MO/gui/content/inifile", tr("INI files")); m_LastCheck.start(); } @@ -353,7 +354,6 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const 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 (highlight & ModInfo::HIGHLIGHT_PLUGIN) return QBrush(Qt::darkBlue); } else if (column == COL_VERSION) { if (!modInfo->getNewestVersion().isValid()) { return QVariant(); @@ -367,11 +367,11 @@ QVariant ModList::data(const QModelIndex &modelIndex, int role) const } else if ((role == Qt::BackgroundRole) || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) { if (modInfo->getHighlight() & ModInfo::HIGHLIGHT_PLUGIN) { - return QColor(0, 0, 255, 32); + return QColor(0, 0, 255, 64); } else if (m_Overwrite.find(modIndex) != m_Overwrite.end()) { - return QColor(0, 255, 0, 32); + return QColor(0, 255, 0, 64); } else if (m_Overwritten.find(modIndex) != m_Overwritten.end()) { - return QColor(255, 0, 0, 32); + return QColor(255, 0, 0, 64); } else { return QVariant(); } @@ -699,7 +699,9 @@ void ModList::modInfoChanged(ModInfo::Ptr info) int row = ModInfo::getIndex(info->name()); info->testValid(); + emit aboutToChangeData(); emit dataChanged(index(row, 0), index(row, columnCount())); + emit postDataChanged(); } else { qCritical("modInfoChanged not called after modInfoAboutToChange"); } @@ -996,6 +998,15 @@ bool ModList::removeRows(int row, int count, const QModelIndex &parent) bool success = false; + if (count == 1) { + ModInfo::Ptr modInfo = ModInfo::getByIndex(row); + std::vector flags = modInfo->getFlags(); + if ((std::find(flags.begin(), flags.end(), ModInfo::FLAG_OVERWRITE) != flags.end()) && (QDir(modInfo->absolutePath()).count() > 2)) { + emit clearOverwrite(); + success = true; + } + } + for (int i = 0; i < count; ++i) { ModInfo::Ptr modInfo = ModInfo::getByIndex(row + i); if (!modInfo->isRegular()) { @@ -1107,6 +1118,7 @@ QString ModList::getColumnToolTip(int column) "Script Extender plugins" "SkyProc Patcher" "Mod Configuration Menu" + "INI files" ""); case COL_INSTALLTIME: return tr("Time this mod was installed"); default: return tr("unknown"); @@ -1215,6 +1227,7 @@ bool ModList::eventFilter(QObject *obj, QEvent *event) } else if (keyEvent->key() == Qt::Key_Space) { return toggleSelection(itemView); } + return QAbstractItemModel::eventFilter(obj, event); } return QAbstractItemModel::eventFilter(obj, event); } diff --git a/src/modlist.h b/src/modlist.h index b5f18e988..2db98bd1d 100644 --- a/src/modlist.h +++ b/src/modlist.h @@ -244,6 +244,11 @@ public slots: */ void fileMoved(const QString &relativePath, const QString &oldOriginName, const QString &newOriginName); + /** + * @brief emitted to have the overwrite folder cleared + */ + void clearOverwrite(); + void aboutToChangeData(); void postDataChanged(); diff --git a/src/modlistsortproxy.cpp b/src/modlistsortproxy.cpp index 435967c9b..88084ae19 100644 --- a/src/modlistsortproxy.cpp +++ b/src/modlistsortproxy.cpp @@ -208,6 +208,16 @@ bool ModListSortProxy::lessThan(const QModelIndex &left, if (leftTime != rightTime) return leftTime < rightTime; } break; + case ModList::COL_GAME: { + if (leftMod->getGameName() != rightMod->getGameName()) { + lt = leftMod->getGameName() < rightMod->getGameName(); + } + else { + int comp = QString::compare(leftMod->name(), rightMod->name(), Qt::CaseInsensitive); + if (comp != 0) + lt = comp < 0; + } + } break; case ModList::COL_PRIORITY: { // nop, already compared by priority } break; diff --git a/src/nexusinterface.cpp b/src/nexusinterface.cpp index d2a52c7bf..e97e98006 100644 --- a/src/nexusinterface.cpp +++ b/src/nexusinterface.cpp @@ -147,10 +147,7 @@ QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0); NexusInterface::NexusInterface(PluginContainer *pluginContainer) : m_NMMVersion(), m_PluginContainer(pluginContainer) { - VS_FIXEDFILEINFO version = GetFileVersion(ToWString(QApplication::applicationFilePath())); - m_MOVersion = VersionInfo(version.dwFileVersionMS >> 16, - version.dwFileVersionMS & 0xFFFF, - version.dwFileVersionLS >> 16); + m_MOVersion = createVersionInfo(); m_AccessManager = new NXMAccessManager(this, m_MOVersion.displayString()); m_DiskCache = new QNetworkDiskCache(this); diff --git a/src/nxmaccessmanager.cpp b/src/nxmaccessmanager.cpp index 05016e8f9..426c8b9cc 100644 --- a/src/nxmaccessmanager.cpp +++ b/src/nxmaccessmanager.cpp @@ -224,12 +224,12 @@ void NXMAccessManager::login(const QString &username, const QString &password) QString NXMAccessManager::userAgent(const QString &subModule) const { QStringList comments; - comments << "compatible to Nexus Client v" + m_NMMVersion; + comments << "Nexus Client v" + m_NMMVersion; if (!subModule.isEmpty()) { comments << "module: " + subModule; } - return QString("Mod Organizer v%1 (%2)").arg(m_MOVersion, comments.join("; ")); + return QString("Mod Organizer/%1 (%2)").arg(m_MOVersion, comments.join("; ")); } diff --git a/src/organizer_en.ts b/src/organizer_en.ts index 0a604b277..01a4abcbf 100644 --- a/src/organizer_en.ts +++ b/src/organizer_en.ts @@ -303,16 +303,21 @@ p, li { white-space: pre-wrap; } + Size + + + + Done - + Information missing, please select "Query Info" from the context menu to re-retrieve. - + pending download @@ -326,27 +331,27 @@ p, li { white-space: pre-wrap; } - - - + + + Done - Double Click to install - - + + Paused - Double Click to resume - - + + Installed - Double Click to re-install - - + + Uninstalled - Double Click to re-install @@ -360,7 +365,7 @@ p, li { white-space: pre-wrap; } - + Done @@ -368,528 +373,634 @@ p, li { white-space: pre-wrap; } DownloadListWidgetCompactDelegate - + < game %1 mod %2 file %3 > - + Pending - + Paused - + Fetching Info 1 - + Fetching Info 2 - + Installed - + Uninstalled - + Done - - - - + + + + + + + Are you sure? - + + This will permanently delete the selected download. + + + + This will remove all finished downloads from this list and from disk. - + This will remove all installed downloads from this list and from disk. - + + This will remove all uninstalled downloads from this list and from disk. + + + + This will permanently remove all finished downloads from this list (but NOT from disk). - + This will permanently remove all installed downloads from this list (but NOT from disk). - + + This will permanently remove all uninstalled downloads from this list (but NOT from disk). + + + + Install - + Query Info - + + Visit on Nexus + + + + + + + Show in Folder + + + + Delete - + Un-Hide - + Hide - + Cancel - + Pause - + Remove - + Resume - + Delete Installed... - + + Delete Uninstalled... + + + + Delete All... - + Hide Installed... - + + Hide Uninstalled... + + + + Hide All... + + + Un-Hide All... + + DownloadListWidgetDelegate - + < game %1 mod %2 file %3 > - + Pending - + Fetching Info 1 - + Fetching Info 2 - - - - + + + Are you sure? - + This will remove all finished downloads from this list and from disk. - + + + + + Delete Files? + + + + + This will permanently delete the selected download. + + + + This will remove all installed downloads from this list and from disk. - + + This will remove all uninstalled downloads from this list and from disk. + + + + This will remove all finished downloads from this list (but NOT from disk). - + This will remove all installed downloads from this list (but NOT from disk). - + + This will remove all uninstalled downloads from this list (but NOT from disk). + + + + Install - + Query Info - + + Visit on Nexus + + + + + + + Show in Folder + + + + + Delete - + Un-Hide - + Hide - + Cancel - + Pause - - Remove + + Resume - - Resume + + Delete Installed... - - Delete Installed... + + Delete Uninstalled... - + Delete All... - + Hide Installed... - + + Hide Uninstalled... + + + + Hide All... + + + Un-Hide All... + + DownloadManager - + failed to rename "%1" to "%2" - + Memory allocation error (in refreshing directory). - + failed to download %1: could not open output file: %2 - + Download again? - + 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. - + Wrong Game - + The download link is for a mod for "%1" but this instance of MO has been set up for "%2". - - + + Already Started - + A download for this mod file has already been queued. - + There is already a download started for this file (mod: %1, file: %2). - - + + remove: invalid download index %1 - + failed to delete %1 - + failed to delete meta file for %1 - + restore: invalid download index: %1 - + cancel: invalid download index %1 - + pause: invalid download index %1 - + resume: invalid download index %1 - + resume (int): invalid download index %1 - + No known download urls. Sorry, this download can't be resumed. - + query: invalid download index %1 - + Please enter the nexus mod id - + Mod ID: - + Please select the source game code for %1 - + + + VisitNexus: invalid download index %1 + + + + + Nexus ID for this Mod is unknown + + + + get pending: invalid download index %1 - + get path: invalid download index %1 - + Main - + Update - + Optional - + Old - + Misc - + Unknown - + display name: invalid download index %1 - + file name: invalid download index %1 - + file time: invalid download index %1 - + file size: invalid download index %1 - + progress: invalid download index %1 - + state: invalid download index %1 - + infocomplete: invalid download index %1 - - + + mod id: invalid download index %1 - + ishidden: invalid download index %1 - + file info: invalid download index %1 - + mark installed: invalid download index %1 - + mark uninstalled: invalid download index %1 - + Memory allocation error (in processing progress event). - + Memory allocation error (in processing downloaded data). - + Information updated - - + + No matching file found on Nexus! Maybe this file is no longer available or it was renamed? - + No file on Nexus matches the selected file by name. Please manually choose the correct one. - + No download server available. Please try again later. - + Failed to request file info from nexus: %1 - + Warning: Content type is: %1 - + Download header content length: %1 downloaded file size: %2 - + Download failed: %1 (%2) - + + We were unable to download the file due to errors after four retries. There may be an issue with the Nexus servers. + + + + failed to re-open %1 @@ -1256,7 +1367,6 @@ p, li { white-space: pre-wrap; } None of the available installer plugins were able to handle that archive. This is likely due to a corrupted or incompatible download or unrecognized archive format. - None of the available installer plugins were able to handle that archive @@ -1397,7 +1507,7 @@ This is likely due to a corrupted or incompatible download or unrecognized archi MainWindow - + Categories @@ -1462,61 +1572,61 @@ p, li { white-space: pre-wrap; } - - + + Restore Backup... - - + + Create Backup - + List of available mods. - + This is a list of installed mods. Use the checkboxes to activate/deactivate mods and drag & drop mods to change their "installation" orders. - + Filter - + Clear all Filters - + No groups - + Nexus IDs - - - + + + Namefilter - + Pick a program to run. - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1526,12 +1636,12 @@ p, li { white-space: pre-wrap; } - + Run program - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1540,17 +1650,17 @@ p, li { white-space: pre-wrap; } - + Run - + Create a shortcut in your start menu or on the desktop to the specified program - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1559,27 +1669,27 @@ p, li { white-space: pre-wrap; } - + Shortcut - + Plugins - + Sort - + List of available esp/esm files - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1588,27 +1698,27 @@ p, li { white-space: pre-wrap; } - + Archives - + <html><head/><body><p>BSAs / BA2s are bundles of game assets (textures, scripts, etc.). By default, the engine loads these bundles in a separate step from loose files. <p>Their load order is specified by the priority of the corresponding plugin (right pane, plugins tab).</p><p>If there is a matching plugin, the game will load them no matter what.</p></body></html> - + <html><head/><body><p>Currently detected archives. (<a href="#"><span style=" text-decoration: underline; color:#0000ff;">What is an archive?</span></a>)</p></body></html> - + List of available BS Archives. Archives not checked here are not managed by MO and ignore installation order. - + BSA files are archives (comparable to .zip files) that contain data assets (meshes, textures, ...) to be used by the game. As such they "compete" with loose files in your data directory over which is loaded. By default, BSAs that share their base name with an enabled ESP (i.e. plugin.esp and plugin.bsa) are automatically loaded and will have precedence over all loose files, the installation order you set up to the left is then ignored! @@ -1616,61 +1726,60 @@ p, li { white-space: pre-wrap; } - - + File - + Data - + refresh data-directory overview - + Refresh the overview. This may take a moment. - - - + + + Refresh - + This is an overview of your data directory as visible to the game (and tools). - + Mod - - + + Filter the above list so that only conflicts are displayed. - + Show only conflicts - + Saves - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -1681,155 +1790,155 @@ p, li { white-space: pre-wrap; } - + Downloads - + This is a list of mods you downloaded from Nexus. Double click one to install it. You can also drag an archive into here. - + Show Hidden - + Tool Bar - + Install Mod - + Install &Mod - + Install a new mod from an archive - + Ctrl+M - + Profiles - + &Profiles - + Configure Profiles - + Ctrl+P - + Executables - + &Executables - + Configure the executables that can be started through Mod Organizer - + Ctrl+E - - + + Tools - + &Tools - + Ctrl+I - + Settings - + &Settings - + Configure settings and workarounds - + Ctrl+S - + Nexus - + Search nexus network for more mods - + Ctrl+N - - + + Update - + Mod Organizer is up-to-date - - + + No Problems - + This button will be highlighted if MO discovered potential problems in your setup and provide tips on how to fix them. !Work in progress! @@ -1837,730 +1946,740 @@ Right now this has very limited functionality - - + + Help - + Ctrl+H - + Endorse MO - - + + Endorse Mod Organizer - + Copy Log to Clipboard - + Ctrl+C - + Change Game - + Open the Instance selection dialog to manage a different Game - + Toolbar - + Desktop - + Start Menu - + There is no supported sort mechanism for this game. You will probably have to use a third-party tool. - + Problems - + There are potential problems with your setup - + Everything seems to be in order - + Help on UI - + Documentation Wiki - + Report Issue - + Tutorials - + About - + About Qt - + Name - + Please enter a name for the new profile - + failed to create profile: %1 - + Show tutorial? - + 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. - + Downloads in progress - + There are still downloads in progress, do you really want to quit? - + Plugin "%1" failed: %2 - + Plugin "%1" failed - + Browse Mod Page - + Also in: <br> - + No conflict - + <Edit...> - + This bsa is enabled in the ini file so it may be required! - + Activating Network Proxy - + Notice: Your current MO version (%1) is lower than the previous version (%2).<br>The GUI may not downgrade gracefully, so you may experience oddities.<br>However, there should be no serious issues. - + Choose Mod - + Mod Archive - + Start Tutorial? - + You're about to start a tutorial. For technical reasons it's not possible to end the tutorial early. Continue? - + failed to spawn notepad.exe: %1 - + failed to change origin name: %1 - + failed to move "%1" from mod "%2" to "%3": %4 - + <Contains %1> - + <Checked> - + <Unchecked> - + <Update> - + <Managed by MO> - + <Managed outside MO> - + <No category> - + <Conflicted> - + <Not Endorsed> - + failed to rename mod: %1 - + Overwrite? - + This will replace the existing mod "%1". Continue? - + failed to remove mod "%1" - - - + + + failed to rename "%1" to "%2" - - - - + + + + Confirm - + Remove the following mods?<br><ul>%1</ul> - + failed to remove mod: %1 - - + + Failed - + Installation file no longer exists - + Mods installed with old versions of MO can't be reinstalled in this way. - + You need to be logged in with Nexus to resume a download - - + + You need to be logged in with Nexus to endorse - + Failed to display overwrite dialog: %1 - + Nexus ID for this Mod is unknown - + Web page for this mod is unknown - - - + + + Create Mod... - + This will create an empty mod. Please enter a name: - - + + A mod with this name already exists - + This will move all files from overwrite into a new, regular mod. Please enter a name: - - + + Are you sure? - + About to recursively delete: - + Not logged in, endorsement information will be wrong - + Continue? - + The versioning scheme decides which version is considered newer than another. This function will guess the versioning scheme under the assumption that the installed version is outdated. - - + + Sorry - + I don't know a versioning scheme where %1 is newer than %2. - + Really enable all visible mods? - + Really disable all visible mods? - + Export to csv - + CSV (Comma Separated Values) is a format that can be imported in programs like Excel to create a spreadsheet. You can also use online editors and converters instead. - + Select what mods you want export: - + All installed mods - + Only active (checked) mods from your current profile - + All currently visible mods in the mod list - + Choose what Columns to export: - + Mod_Priority - + Mod_Name - + Mod_Status - + Primary_Category - + Nexus_ID - + Mod_Nexus_URL - + Mod_Version - + Install_Date - + Download_File_Name - + export failed: %1 - + Open Game folder - + Open MyGames folder - + Open Instance folder - + + Open Mods folder + + + + Open Profile folder - + Open Downloads folder - + Open MO2 Install folder - + + Open MO2 Plugins folder + + + + Open MO2 Logs folder - + Install Mod... - + Create empty mod - + Enable all visible - + Disable all visible - + Check all for update - + Export to csv... - + All Mods - + Sync to Mods... - + Clear Overwrite... - - + + Open in explorer - + Restore Backup - + Remove Backup... - + Change Categories - + Primary Category - + Change versioning scheme - + Un-ignore update - + Ignore update - + Rename Mod... - + Reinstall Mod - + Remove Mod... - + Un-Endorse - - + + Endorse - + Won't endorse - + Endorsement state unknown - + Ignore missing data - + Mark as converted/working - + Visit on Nexus - + Visit web page - + Information... - - + + Exception: - - + + Unknown exception - + <All> - + <Multiple> - + %1 more - + Are you sure you want to remove the following %n save(s)?<br><ul>%1</ul><br>Removed saves will be sent to the Recycle Bin. @@ -2568,12 +2687,12 @@ You can also use online editors and converters instead. - + Enable Mods... - + Delete %n save(s) @@ -2581,319 +2700,319 @@ You can also use online editors and converters instead. - + failed to remove %1 - + failed to create %1 - + Can't change download directory while downloads are in progress! - + failed to write to file %1 - + %1 written - + Select binary - + Binary - + Enter Name - + Please enter a name for the executable - + Not an executable - + This is not a recognized executable. - - + + Replace file? - + There already is a hidden version of this file. Replace it? - - + + File operation failed - - + + Failed to remove "%1". Maybe you lack the required file permissions? - + There already is a visible version of this file. Replace it? - + file not found: %1 - + failed to generate preview for %1 - + Sorry, can't preview anything. This function currently does not support extracting from bsas. - + Update available - + Open/Execute - + Add as Executable - + Preview - + Un-Hide - + Hide - + Write To File... - + Do you want to endorse Mod Organizer on %1 now? - + Thank you! - + Thank you for your endorsement! - + Request to Nexus failed: %1 - - + + failed to read %1: %2 - - + + Error - + failed to extract %1 (errorcode %2) - + Extract BSA - + This archive contains invalid hashes. Some files may be broken. - + Extract... - + This will restart MO, continue? - + Edit Categories... - + Deselect filter - + Remove - + Enable all - + Disable all - + Unlock load order - + Lock load order - + depends on missing "%1" - + incompatible with "%1" - + Please wait while LOOT is running - + loot failed. Exit code was: %1 - + failed to start loot - + failed to run loot: %1 - + Errors occured - + Backup of load order created - + Choose backup to restore - + No Backups - + There are no backups to restore - - + + Restore failed - - + + Failed to restore the backup. Errorcode: %1 - + Backup of modlist created - + A file with the same name has already been downloaded. What would you like to do? - + Overwrite - + Rename new file - + Ignore file @@ -2961,16 +3080,21 @@ You can also use online editors and converters instead. + INI files + + + + invalid content type %1 - + invalid mod index %1 - + remove: invalid mod index %1 @@ -3606,12 +3730,12 @@ p, li { white-space: pre-wrap; } - + %1 contains no esp/esm/esl and no asset (textures, meshes, interface, ...) directory - + Categories: <br> @@ -3669,193 +3793,198 @@ p, li { white-space: pre-wrap; } - + + INI files + + + + This entry contains files that have been created inside the virtual data tree (i.e. by the construction kit) - + Backup - + No valid game data - + Not endorsed yet - + Overwrites files - + Overwritten files - + Overwrites & Overwritten - + Redundant - + Alternate game source - + Non-MO - + invalid - + installed version: "%1", newest version: "%2" - + The newest version on Nexus seems to be older than the one you have installed. This could either mean the version you have has been withdrawn (i.e. due to a bug) or the author uses a non-standard versioning scheme and that newest version is actually newer. Either way you may want to "upgrade". - + Categories: <br> - + Invalid name - + Name is already in use by another mod - + drag&drop failed: %1 - + Confirm - + Are you sure you want to remove "%1"? - + Flags - + Content - + Mod Name - + Version - + Priority - + Category - + Source Game - + Nexus ID - + Installation - - + + unknown - + Name of your mods - + Version of the mod (if available) - + Installation priority of your mod. The higher, the more "important" it is and thus overwrites files from mods with lower priority. - + Category of the mod. - + The source game which was the origin of this mod. - + Id of the mod as used on Nexus. - + Emblemes to highlight things that might require attention. - - Depicts the content of the mod:<br><table cellspacing=7><tr><td><img src=":/MO/gui/content/plugin" width=32/></td><td>Game plugins (esp/esm/esl)</td></tr><tr><td><img src=":/MO/gui/content/interface" width=32/></td><td>Interface</td></tr><tr><td><img src=":/MO/gui/content/mesh" width=32/></td><td>Meshes</td></tr><tr><td><img src=":/MO/gui/content/bsa" width=32/></td><td>BSA</td></tr><tr><td><img src=":/MO/gui/content/texture" width=32/></td><td>Textures</td></tr><tr><td><img src=":/MO/gui/content/sound" width=32/></td><td>Sounds</td></tr><tr><td><img src=":/MO/gui/content/music" width=32/></td><td>Music</td></tr><tr><td><img src=":/MO/gui/content/string" width=32/></td><td>Strings</td></tr><tr><td><img src=":/MO/gui/content/script" width=32/></td><td>Scripts (Papyrus)</td></tr><tr><td><img src=":/MO/gui/content/skse" width=32/></td><td>Script Extender plugins</td></tr><tr><td><img src=":/MO/gui/content/skyproc" width=32/></td><td>SkyProc Patcher</td></tr><tr><td><img src=":/MO/gui/content/menu" width=32/></td><td>Mod Configuration Menu</td></tr></table> + + Depicts the content of the mod:<br><table cellspacing=7><tr><td><img src=":/MO/gui/content/plugin" width=32/></td><td>Game plugins (esp/esm/esl)</td></tr><tr><td><img src=":/MO/gui/content/interface" width=32/></td><td>Interface</td></tr><tr><td><img src=":/MO/gui/content/mesh" width=32/></td><td>Meshes</td></tr><tr><td><img src=":/MO/gui/content/bsa" width=32/></td><td>BSA</td></tr><tr><td><img src=":/MO/gui/content/texture" width=32/></td><td>Textures</td></tr><tr><td><img src=":/MO/gui/content/sound" width=32/></td><td>Sounds</td></tr><tr><td><img src=":/MO/gui/content/music" width=32/></td><td>Music</td></tr><tr><td><img src=":/MO/gui/content/string" width=32/></td><td>Strings</td></tr><tr><td><img src=":/MO/gui/content/script" width=32/></td><td>Scripts (Papyrus)</td></tr><tr><td><img src=":/MO/gui/content/skse" width=32/></td><td>Script Extender plugins</td></tr><tr><td><img src=":/MO/gui/content/skyproc" width=32/></td><td>SkyProc Patcher</td></tr><tr><td><img src=":/MO/gui/content/menu" width=32/></td><td>Mod Configuration Menu</td></tr><tr><td><img src=":/MO/gui/content/inifile" width=32/></td><td>INI files</td></tr></table> - + Time this mod was installed @@ -3863,7 +3992,7 @@ p, li { white-space: pre-wrap; } ModListSortProxy - + Drag&Drop is only supported when sorting by priority @@ -3933,17 +4062,17 @@ p, li { white-space: pre-wrap; } NexusInterface - + Failed to guess mod id for "%1", please pick the correct one - + empty response - + invalid response @@ -3951,189 +4080,201 @@ p, li { white-space: pre-wrap; } OrganizerCore - - + + Failed to write settings - + An error occured trying to update MO settings to %1: %2 - + File is write protected - + Invalid file format (probably a bug) - + Unknown error %1 - + An error occured trying to write back MO settings to %1: %2 - - + + Download started - + Download failed - - + + Installation successful - - + + Configure Mod - - + + This mod contains ini tweaks. Do you want to configure them now? - - + + mod "%1" not found - - + + Installation cancelled - - + + The mod was not installed completely. - + Executable "%1" not found - + Start Steam? - + Steam is required to be running already to correctly start the game. Should MO try to start steam now? - + Error - + + Windows Event Log Error + + + + + The Windows Event Log service is disabled and/or not running. This prevents USVFS from running properly. Your mods may not be working in the executable that you are launching. Note that you may have to restart MO and/or your PC after the service is fixed. + +Continue launching %1? + + + + No profile set - + Failed to refresh list of esps: %1 - + Multiple esps/esls activated, please check that they don't conflict. - + Download? - + A download has been started but no installed page plugin recognizes it. If you download anyway no information (i.e. version) will be associated with the download. Continue? - + failed to update mod list: %1 - - + + login successful - + Login failed - + Login failed, try again? - + login failed: %1. Download will not be associated with an account - + login failed: %1 - + login failed: %1. You need to log-in with Nexus to update MO. - + MO1 "Script Extender" load mechanism has left hook.dll in your game folder - - + + Description missing - + <a href="%1">hook.dll</a> has been found in your game folder (right click to copy the full path). This is most likely a leftover of setting the ModOrganizer 1 load mechanism to "Script Extender", in which case you must remove this file either by changing the load mechanism in ModOrganizer 1 or manually removing the file, otherwise the game is likely to crash and burn. - + failed to save load order: %1 - + The designated write target "%1" is not enabled. @@ -4146,7 +4287,12 @@ Continue? - + + Open in Explorer + + + + You can use drag&drop to move files and directories to regular mods. @@ -4234,120 +4380,135 @@ Continue? PluginList - + Name - + Priority - + Mod Index - + Flags - - + + unknown - + Name of your mods - + Load priority of your mod. The higher, the more "important" it is and thus overwrites data from plugins with lower priority. - + The modindex determines the formids of objects originating from this mods. - + failed to update esp info for file %1 (source id: %2), error: %3 - + esp not found: %1 - - + + Confirm - + Really enable all plugins? - + Really disable all plugins? - + The file containing locked plugin indices is broken - - + + <b>Origin</b>: %1 - + <br><b><i>This plugin can't be disabled (enforced by the game).</i></b> - + Author - + Description - + Missing Masters - + Enabled Masters - - There is an ini file connected to this esp. Its settings will be added to your game settings, overwriting in case of conflicts. + + Loads Archives + + + + + There are Archives connected to this plugin. Their assets will be added to your game, overwriting in case of conflicts following the plugin order. Loose files will always overwrite assets from Archives. (This flag only checks for Archives from the same mod as the plugin) - + + Loads INI settings + + + + + There is an ini file connected to this plugin. Its settings will be added to your game settings, overwriting in case of conflicts. + + + + This ESP is flagged as an ESL. It will adhere to the ESP load order but the records will be loaded in ESL space. - + failed to restore load order for %1 @@ -4703,7 +4864,7 @@ p, li { white-space: pre-wrap; } - + Error @@ -4865,84 +5026,94 @@ If the folder was still in use, restart MO and try again. - + Enter a Name for the new Instance - - Enter a new name or select one from the sugested list (only letters and numbers allowed): + + Enter a new name or select one from the suggested list: - - + + Canceled - + + Invalid instance name + + + + + The instance name "%1" is invalid. Use the name "%2" instead? + + + + The instance "%1" already exists. - + Please choose a different instance name, like: "%1 1" . - + Choose Instance - + Each Instance is a full set of MO data files (mods, downloads, profiles, configuration, ...). You can use multiple instances for different games. Instances are stored in Appdata and can be accessed by all MO installations. If your MO folder is writable, you can also store a single instance locally (called a Portable install, and all the MO data files will be inside the installation folder). - + New - + Create a new instance. - + Portable - + Use MO folder for data. - + Manage Instances - + Delete an Instance. - + failed to create %1 - + Data directory created - + New data directory created at %1. If you don't want to store a lot of data there, reconfigure the storage directories via settings. @@ -5012,7 +5183,7 @@ If the folder was still in use, restart MO and try again. - + Failed to create "%1". Your user account probably lacks permission. @@ -5038,49 +5209,49 @@ If the folder was still in use, restart MO and try again. - + Please select the game edition you have (MO can't start the game correctly if this is set incorrectly!) - + failed to start shortcut: %1 - + failed to start application: %1 - - + + Mod Organizer - + An instance of Mod Organizer is already running - + Failed to set up instance - + Please use "Help" from the toolbar to get usage instructions to all elements - - + + <Manage...> - + failed to parse profile %1: %2 @@ -5116,12 +5287,12 @@ If the folder was still in use, restart MO and try again. - + failed to access %1 - + failed to set file time %1 @@ -5131,12 +5302,12 @@ If the folder was still in use, restart MO and try again. - + Script Extender - + Proxy DLL @@ -5281,58 +5452,58 @@ Start elevated anyway? (you will be asked if you want to allow ModOrganizer.exe - + New update available (%1) - + Do you want to install update? All your mods and setup will be left untouched. Select Show Details option to see the full change-log. - + Install - + Download failed - + Failed to find correct download, please try again later. - + Update - + Download in progress - + Download failed: %1 - + Failed to install update: %1 - + Failed to start %1: %2 - + Error @@ -5356,12 +5527,12 @@ Select Show Details option to see the full change-log. - + Error - + Failed to create "%1", you may not have the necessary permission. path remains unchanged. @@ -5488,81 +5659,86 @@ If you use pre-releases, never contact me directly by e-mail or via private mess - + Base Directory - - - + + + ... - + Overwrite - + Caches - + Downloads - + Directory where mods are stored. - + Directory where mods are stored. Please note that changing this will break all associations of profiles with mods that don't exist in the new location (with the same name). - - + + Directory where downloads are stored. - + Mods - + Profiles - + + Managed Game + + + + Use %BASE_DIR% to refer to the Base Directory. - + Important: All directories have to be writeable! - - + + Nexus - + Allows automatic log-in when the Nexus-Page for the game is clicked. - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -5571,144 +5747,144 @@ p, li { white-space: pre-wrap; } - + If checked and if correct credentials are entered below, log-in to Nexus (for browsing and downloading) is automatic. - + Automatically Log-In to Nexus - - + + Username - - + + Password - + Remove cache and cookies. Forces a new login. - + Clear Cache - + Disable automatic internet features - + Disable automatic internet features. This does not affect features that are explicitly invoked by the user (like checking mods for updates, endorsing, opening the web browser) - + Offline Mode - + Use a proxy for network connections. - + Use a proxy for network connections. This uses the system-wide settings which can be configured in Internet Explorer. Please note that MO will start up a few seconds slower on some systems when using a proxy. - + Use HTTP Proxy (Uses System Settings) - + Associate with "Download with manager" links - + Known Servers (updated on download) - + Preferred Servers (Drag & Drop) - + Steam - + If you save your steam user ID and password here, they will be used when logging into steam. Note, however, your password will be stored unencrypted, so make sure your computer is secure. - + Plugins - + Author: - + Version: - + Description: - + Key - + Value - + Blacklisted Plugins (use <del> to remove): - + Workarounds - + Steam App ID - + The Steam AppID for your game - + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><style type="text/css"> p, li { white-space: pre-wrap; } @@ -5724,17 +5900,17 @@ p, li { white-space: pre-wrap; } - + Load Mechanism - + Select loading mechanism. See help for details. - + Mod Organizer needs a dll to be injected into the game so all mods are visible to it. There are several means to do this: *Mod Organizer* (default) In this mode the Mod Organizer itself injects the dll. The disadvantage is that you always have to start the game through MO or a link created by it. @@ -5745,17 +5921,17 @@ If you use the Steam version of Oblivion the default will NOT work. In this case - + NMM Version - + The Version of Nexus Mod Manager to impersonate. - + Mod Organizer uses an API provided by the Nexus to provide features like checking for updates and downloading files. Unfortunately this API has not been made available officially to third party tools like MO so we have to impersonate the Nexus Mod Manager to be allowed in. On top of this Nexus has used the client identification to lock out outdated versions of NMM to force users to update. This means that MO also needs to impersonate the new version of NMM even if MO doesn't need an update. Therefore you can configure the version to identify as here. Please note that MO does identify itself as MO to the webserver, it's not lying about what it is. It is merely adding a "compatible" NMM version to the user agent. @@ -5764,44 +5940,44 @@ tl;dr-version: If Nexus-features don't work, insert the current version num - + Enforces that inactive ESPs and ESMs are never loaded. - + It seems that the Games occasionally load ESP or ESM files even if they haven't been activated as plugins. I don't yet know what the circumstances are, but user reports imply it is in some cases unwanted. If this is checked, ESPs and ESMs not checked in the List are invisible to the game and can not be loaded. - + Hide inactive ESPs/ESMs - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) - + If checked, files (i.e. esps, esms and bsas) belonging to the core game can not be disabled in the UI. (default: on) Uncheck this if you want to use Mod Organizer with total conversions (like Nehrim) but be aware that the game will crash if required files are not enabled. - + Force-enable game files - + Disable this to no longer display mods installed outside MO in the mod list (left pane). Assets from those mods will then be treated as having lowest mod priority together with the original game content. - + By default Mod Organizer will display esp+bsa bundles installed with foreign tools as mods (left pane). This allows you to control their priority in relation to other mods. This is particularly useful if you also use Steam Workshop to install mods. However, if you installed loose file mods outside MO which conflict with BSAs also installed outside MO those conflicts can't be resolved correctly. @@ -5809,44 +5985,44 @@ If you disable this feature, MO will only display official DLCs this way. Please - + Display mods installed outside MO - - + + For Skyrim, this can be used instead of Archive Invalidation. It should make AI redundant for all Profiles. For the other games this is not a sufficient replacement for AI! - + Back-date BSAs - + These are workarounds for problems with Mod Organizer. Please make sure you read the help text before changing anything here. - + Diagnostics - + Log Level - + Decides the amount of data printed to "ModOrganizer.log" - + Decides the amount of data printed to "ModOrganizer.log". "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regluar use. On the "Error" level the log file usually remains empty. @@ -5854,37 +6030,37 @@ For the other games this is not a sufficient replacement for AI! - + Debug - + Info (recommended) - + Warning - + Error - + Crash Dumps - + Decides which type of crash dumps are collected when injected processes crash. - + Decides which type of crash dumps are collected when injected processes crash. "None" Disables the generation of crash dumps by MO. @@ -5895,37 +6071,37 @@ For the other games this is not a sufficient replacement for AI! - + None - + Mini (recommended) - + Data - + Full - + Max Dumps To Keep - + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. Set "Crash Dumps" above to None to disable crash dump collection. @@ -5933,7 +6109,7 @@ For the other games this is not a sufficient replacement for AI! - + Logs and crash dumps are stored under your current instance in the <a href="LOGS_FULL_PATH">LOGS_DIR</a> and <a href="DUMPS_FULL_PATH">DUMPS_DIR</a> folders. @@ -5943,7 +6119,7 @@ For the other games this is not a sufficient replacement for AI! - + Hint: right click link and copy link location diff --git a/src/organizercore.cpp b/src/organizercore.cpp index 35486f984..74ec752e7 100644 --- a/src/organizercore.cpp +++ b/src/organizercore.cpp @@ -186,6 +186,86 @@ QStringList toStringList(InputIterator current, InputIterator end) return result; } +bool checkService() +{ + SC_HANDLE serviceManagerHandle = NULL; + SC_HANDLE serviceHandle = NULL; + LPSERVICE_STATUS_PROCESS serviceStatus = NULL; + LPQUERY_SERVICE_CONFIG serviceConfig = NULL; + bool serviceRunning = true; + + DWORD bytesNeeded; + + try { + serviceManagerHandle = OpenSCManager(NULL, NULL, SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG); + if (!serviceManagerHandle) { + qWarning("failed to open service manager (query status) (error %d)", GetLastError()); + throw 1; + } + + serviceHandle = OpenService(serviceManagerHandle, L"EventLog", SERVICE_QUERY_STATUS | SERVICE_QUERY_CONFIG); + if (!serviceHandle) { + qWarning("failed to open EventLog service (query status) (error %d)", GetLastError()); + throw 2; + } + + if (QueryServiceConfig(serviceHandle, NULL, 0, &bytesNeeded) + || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) { + qWarning("failed to get size of service config (error %d)", GetLastError()); + throw 3; + } + + DWORD serviceConfigSize = bytesNeeded; + serviceConfig = (LPQUERY_SERVICE_CONFIG)LocalAlloc(LMEM_FIXED, serviceConfigSize); + if (!QueryServiceConfig(serviceHandle, serviceConfig, serviceConfigSize, &bytesNeeded)) { + qWarning("failed to query service config (error %d)", GetLastError()); + throw 4; + } + + if (serviceConfig->dwStartType == SERVICE_DISABLED) { + qCritical("Windows Event Log service is disabled!"); + serviceRunning = false; + } + + if (QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, NULL, 0, &bytesNeeded) + || (GetLastError() != ERROR_INSUFFICIENT_BUFFER)) { + qWarning("failed to get size of service status (error %d)", GetLastError()); + throw 5; + } + + DWORD serviceStatusSize = bytesNeeded; + serviceStatus = (LPSERVICE_STATUS_PROCESS)LocalAlloc(LMEM_FIXED, serviceStatusSize); + if (!QueryServiceStatusEx(serviceHandle, SC_STATUS_PROCESS_INFO, (LPBYTE)serviceStatus, serviceStatusSize, &bytesNeeded)) { + qWarning("failed to query service status (error %d)", GetLastError()); + throw 6; + } + + if (serviceStatus->dwCurrentState != SERVICE_RUNNING) { + qCritical("Windows Event Log service is not running"); + serviceRunning = false; + } + } + catch (int e) { + UNUSED_VAR(e); + serviceRunning = false; + } + + if (serviceStatus) { + LocalFree(serviceStatus); + } + if (serviceConfig) { + LocalFree(serviceConfig); + } + if (serviceHandle) { + CloseServiceHandle(serviceHandle); + } + if (serviceManagerHandle) { + CloseServiceHandle(serviceManagerHandle); + } + + return serviceRunning; +} + OrganizerCore::OrganizerCore(const QSettings &initSettings) : m_UserInterface(nullptr) , m_PluginContainer(nullptr) @@ -472,6 +552,8 @@ void OrganizerCore::setUserInterface(IUserInterface *userInterface, SLOT(modRemoved(QString))); connect(&m_ModList, SIGNAL(removeSelectedMods()), widget, SLOT(removeMod_clicked())); + connect(&m_ModList, SIGNAL(clearOverwrite()), widget, + SLOT(clearOverwrite())); connect(&m_ModList, SIGNAL(requestColumnSelect(QPoint)), widget, SLOT(displayColumnSelection(QPoint))); connect(&m_ModList, SIGNAL(fileMoved(QString, QString, QString)), widget, @@ -1190,6 +1272,11 @@ HANDLE OrganizerCore::spawnBinaryProcess(const QFileInfo &binary, ToWString(m_Settings.getSteamAppID()).c_str()); } + QWidget *window = qApp->activeWindow(); + if ((window != nullptr) && (!window->isVisible())) { + window = nullptr; + } + // This could possibly be extracted somewhere else but it's probably for when // we have more than one provider of game registration. if ((QFileInfo( @@ -1200,16 +1287,12 @@ HANDLE OrganizerCore::spawnBinaryProcess(const QFileInfo &binary, .exists()) && (m_Settings.getLoadMechanism() == LoadMechanism::LOAD_MODORGANIZER)) { if (!testForSteam()) { - QWidget *window = qApp->activeWindow(); - if ((window != nullptr) && (!window->isVisible())) { - window = nullptr; - } if (QuestionBoxMemory::query(window, "steamQuery", binary.fileName(), 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(qApp->activeWindow()); + startSteam(window); } } } @@ -1229,10 +1312,24 @@ HANDLE OrganizerCore::spawnBinaryProcess(const QFileInfo &binary, try { m_USVFS.updateMapping(fileMapping(profileName, customOverwrite)); } catch (const std::exception &e) { - QMessageBox::warning(qApp->activeWindow(), tr("Error"), e.what()); + QMessageBox::warning(window, tr("Error"), e.what()); return INVALID_HANDLE_VALUE; } + // Check if the Windows Event Logging service is running. For some reason, this seems to be + // critical to the successful running of usvfs. + if (!checkService()) { + if (QuestionBoxMemory::query(window, QString("eventLogService"), binary.fileName(), + tr("Windows Event Log Error"), + tr("The Windows Event Log service is disabled and/or not running. This prevents" + " USVFS from running properly. Your mods may not be working in the executable" + " that you are launching. Note that you may have to restart MO and/or your PC" + " after the service is fixed.\n\nContinue launching %1?").arg(binary.fileName()), + QDialogButtonBox::Yes | QDialogButtonBox::No) == QDialogButtonBox::No) { + return INVALID_HANDLE_VALUE; + } + } + QString modsPath = settings().getModDirectory(); // Check if this a request with either an executable or a working directory under our mods folder @@ -2204,4 +2301,4 @@ std::vector OrganizerCore::fileMapping( result.insert(result.end(), subRes.begin(), subRes.end()); } return result; -} +} \ No newline at end of file diff --git a/src/overwriteinfodialog.cpp b/src/overwriteinfodialog.cpp index 3bb67354c..1fe3f6453 100644 --- a/src/overwriteinfodialog.cpp +++ b/src/overwriteinfodialog.cpp @@ -261,6 +261,10 @@ void OverwriteInfoDialog::createDirectoryTriggered() ui->filesView->edit(newIndex); } +void OverwriteInfoDialog::on_explorerButton_clicked() +{ + ::ShellExecuteW(nullptr, L"explore", ToWString(m_ModInfo->absolutePath()).c_str(), nullptr, nullptr, SW_SHOWNORMAL); +} void OverwriteInfoDialog::on_filesView_customContextMenuRequested(const QPoint &pos) { diff --git a/src/overwriteinfodialog.h b/src/overwriteinfodialog.h index 7d44c9e80..4b731736d 100644 --- a/src/overwriteinfodialog.h +++ b/src/overwriteinfodialog.h @@ -56,6 +56,7 @@ private slots: void openTriggered(); void createDirectoryTriggered(); + void on_explorerButton_clicked(); void on_filesView_customContextMenuRequested(const QPoint &pos); private: diff --git a/src/overwriteinfodialog.ui b/src/overwriteinfodialog.ui index 5e5986ca7..8a09ce590 100644 --- a/src/overwriteinfodialog.ui +++ b/src/overwriteinfodialog.ui @@ -14,6 +14,13 @@ Overwrite + + + + Open in Explorer + + + diff --git a/src/pluginlist.cpp b/src/pluginlist.cpp index a6ae8fa76..323cd98fd 100644 --- a/src/pluginlist.cpp +++ b/src/pluginlist.cpp @@ -72,11 +72,21 @@ static bool ByDate(const PluginList::ESPInfo& LHS, const PluginList::ESPInfo& RH return QFileInfo(LHS.m_FullPath).lastModified() < QFileInfo(RHS.m_FullPath).lastModified(); } +static QString TruncateString(const QString& text) { + QString new_text = text; + if (new_text.length() > 1024) { + new_text.truncate(1024); + new_text += "..."; + } + return new_text; +} + PluginList::PluginList(QObject *parent) : QAbstractItemModel(parent) , m_FontMetrics(QFont()) { connect(this, SIGNAL(writePluginsList()), this, SLOT(generatePluginIndexes())); + m_LastCheck.start(); } PluginList::~PluginList() @@ -181,9 +191,17 @@ void PluginList::refresh(const QString &profileName try { FilesOrigin &origin = baseDirectory.getOriginByID(current->getOrigin(archive)); + QString iniPath = QFileInfo(filename).baseName() + ".ini"; bool hasIni = baseDirectory.findFile(ToWString(iniPath)).get() != nullptr; + std::set loadedArchives; + QString originPath = QString::fromWCharArray(origin.getPath().c_str()); + QDir dir(QDir::toNativeSeparators(originPath)); + for (QString filename : dir.entryList(QStringList() << QFileInfo(filename).baseName() + "*.bsa" << QFileInfo(filename).baseName() + "*.ba2")) { + loadedArchives.insert(filename); + } + QString originName = ToQString(origin.getName()); unsigned int modIndex = ModInfo::getIndex(originName); if (modIndex != UINT_MAX) { @@ -191,7 +209,7 @@ void PluginList::refresh(const QString &profileName originName = modInfo->name(); } - m_ESPs.push_back(ESPInfo(filename, forceEnabled, originName, ToQString(current->getFullPath()), hasIni)); + m_ESPs.push_back(ESPInfo(filename, forceEnabled, originName, ToQString(current->getFullPath()), hasIni, loadedArchives)); m_ESPs.rbegin()->m_Priority = -1; } catch (const std::exception &e) { reportError(tr("failed to update esp info for file %1 (source id: %2), error: %3").arg(filename).arg(current->getOrigin(archive)).arg(e.what())); @@ -575,6 +593,11 @@ void PluginList::disconnectSlots() { m_PluginStateChanged.disconnect_all_slots(); } +int PluginList::timeElapsedSinceLastChecked() const +{ + return m_LastCheck.elapsed(); +} + QStringList PluginList::pluginNames() const { QStringList result; @@ -838,7 +861,7 @@ QVariant PluginList::data(const QModelIndex &modelIndex, int role) const } else if (role == Qt::BackgroundRole || (role == ViewMarkingScrollBar::DEFAULT_ROLE)) { if (m_ESPs[index].m_ModSelected) { - return QColor(0, 0, 255, 32); + return QColor(0, 0, 255, 64); } else { return QVariant(); } @@ -873,23 +896,30 @@ QVariant PluginList::data(const QModelIndex &modelIndex, int role) const } else { QString text = tr("Origin: %1").arg(m_ESPs[index].m_OriginName); if (m_ESPs[index].m_Author.size() > 0) { - text += "
" + tr("Author") + ": " + m_ESPs[index].m_Author; + text += "
" + tr("Author") + ": " + TruncateString(m_ESPs[index].m_Author); } if (m_ESPs[index].m_Description.size() > 0) { - text += "
" + tr("Description") + ": " + m_ESPs[index].m_Description; + text += "
" + tr("Description") + ": " + TruncateString(m_ESPs[index].m_Description); } if (m_ESPs[index].m_MasterUnset.size() > 0) { - text += "
" + tr("Missing Masters") + ": " + SetJoin(m_ESPs[index].m_MasterUnset, ", ") + ""; + text += "
" + tr("Missing Masters") + ": " + TruncateString(SetJoin(m_ESPs[index].m_MasterUnset, ", ")) + ""; } std::set enabledMasters; std::set_difference(m_ESPs[index].m_Masters.begin(), m_ESPs[index].m_Masters.end(), m_ESPs[index].m_MasterUnset.begin(), m_ESPs[index].m_MasterUnset.end(), std::inserter(enabledMasters, enabledMasters.end())); if (!enabledMasters.empty()) { - text += "
" + tr("Enabled Masters") + ": " + SetJoin(enabledMasters, ", "); + text += "
" + tr("Enabled Masters") + ": " + TruncateString(SetJoin(enabledMasters, ", ")); + } + if (!m_ESPs[index].m_Archives.empty()) { + text += "
" + tr("Loads Archives") + ": " + TruncateString(SetJoin(m_ESPs[index].m_Archives, ", ")); + text += "
" + tr("There are Archives connected to this plugin. " + "Their assets will be added to your game, overwriting in case of conflicts following the plugin order. " + "Loose files will always overwrite assets from Archives. (This flag only checks for Archives from the same mod as the plugin)"); } if (m_ESPs[index].m_HasIni) { - text += "
" + tr("There is an ini file connected to this esp. " + text += "
" + tr("Loads INI settings") + ": "; + text += "
" + tr("There is an ini file connected to this plugin. " "Its settings will be added to your game settings, overwriting in case of conflicts."); } if (m_ESPs[index].m_IsLightFlagged && !m_ESPs[index].m_IsLight) { @@ -917,6 +947,9 @@ QVariant PluginList::data(const QModelIndex &modelIndex, int role) const if (m_ESPs[index].m_HasIni) { result.append(":/MO/gui/attachment"); } + if (!m_ESPs[index].m_Archives.empty()) { + result.append(":/MO/gui/archive_conflict_neutral"); + } if (m_ESPs[index].m_IsLightFlagged && !m_ESPs[index].m_IsLight) { result.append(":/MO/gui/awaiting"); } @@ -936,6 +969,7 @@ bool PluginList::setData(const QModelIndex &modIndex, const QVariant &value, int if (role == Qt::CheckStateRole) { m_ESPs[modIndex.row()].m_Enabled = value.toInt() == Qt::Checked || m_ESPs[modIndex.row()].m_ForceEnabled; + m_LastCheck.restart(); emit dataChanged(modIndex, modIndex); refreshLoadOrder(); @@ -1228,9 +1262,9 @@ bool PluginList::eventFilter(QObject *obj, QEvent *event) PluginList::ESPInfo::ESPInfo(const QString &name, bool enabled, const QString &originName, const QString &fullPath, - bool hasIni) + bool hasIni, std::set archives) : m_Name(name), m_FullPath(fullPath), m_Enabled(enabled), m_ForceEnabled(enabled), - m_Priority(0), m_LoadOrder(-1), m_OriginName(originName), m_HasIni(hasIni), m_ModSelected(false) + m_Priority(0), m_LoadOrder(-1), m_OriginName(originName), m_HasIni(hasIni), m_Archives(archives), m_ModSelected(false) { try { ESP::File file(ToWString(fullPath)); diff --git a/src/pluginlist.h b/src/pluginlist.h index f6745aa84..b8e35c32d 100644 --- a/src/pluginlist.h +++ b/src/pluginlist.h @@ -28,6 +28,7 @@ namespace MOBase { class IPluginGame; } #include #include #include +#include #include #pragma warning(push) @@ -192,6 +193,8 @@ class PluginList : public QAbstractItemModel, public MOBase::IPluginList */ int enabledCount() const; + int timeElapsedSinceLastChecked() const; + QString getName(int index) const { return m_ESPs.at(index).m_Name; } int getPriority(int index) const { return m_ESPs.at(index).m_Priority; } QString getIndexPriority(int index) const; @@ -273,11 +276,12 @@ public slots: void writePluginsList(); + private: struct ESPInfo { - ESPInfo(const QString &name, bool enabled, const QString &originName, const QString &fullPath, bool hasIni); + ESPInfo(const QString &name, bool enabled, const QString &originName, const QString &fullPath, bool hasIni, std::set archives); QString m_Name; QString m_FullPath; bool m_Enabled; @@ -294,6 +298,7 @@ public slots: QString m_Author; QString m_Description; bool m_HasIni; + std::set m_Archives; std::set m_Masters; mutable std::set m_MasterUnset; bool operator < (const ESPInfo& str) const @@ -346,6 +351,8 @@ public slots: QTemporaryFile m_TempFile; + QTime m_LastCheck; + const MOBase::IPluginGame *m_GamePlugin; }; diff --git a/src/resources.qrc b/src/resources.qrc index a18baf45d..f3459ea7d 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -93,6 +93,7 @@ resources/contents/conversation.png resources/contents/locked-chest.png resources/contents/config.png + resources/contents/feather-and-scroll.png qt.conf diff --git a/src/resources/contents/feather-and-scroll.png b/src/resources/contents/feather-and-scroll.png new file mode 100644 index 000000000..f82694ca4 Binary files /dev/null and b/src/resources/contents/feather-and-scroll.png differ diff --git a/src/selfupdater.cpp b/src/selfupdater.cpp index d671bafcb..2b051b09a 100644 --- a/src/selfupdater.cpp +++ b/src/selfupdater.cpp @@ -102,12 +102,7 @@ SelfUpdater::SelfUpdater(NexusInterface *nexusInterface) throw MyException(InstallationManager::getErrorString(m_ArchiveHandler->getLastError())); } - VS_FIXEDFILEINFO version = GetFileVersion(ToWString(QApplication::applicationFilePath())); - - m_MOVersion = VersionInfo(version.dwFileVersionMS >> 16, - version.dwFileVersionMS & 0xFFFF, - version.dwFileVersionLS >> 16, - version.dwFileVersionLS & 0xFFFF); + m_MOVersion = createVersionInfo(); } diff --git a/src/settings.cpp b/src/settings.cpp index 2bcf2d02f..db4ea5651 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -712,9 +712,10 @@ Settings::PathsTab::PathsTab(Settings *parent, SettingsDialog &dialog) , m_cacheDirEdit(m_dialog.findChild("cacheDirEdit")) , m_profilesDirEdit(m_dialog.findChild("profilesDirEdit")) , m_overwriteDirEdit(m_dialog.findChild("overwriteDirEdit")) + , m_managedGameDirEdit(m_dialog.findChild("managedGameDirEdit")) { m_baseDirEdit->setText(m_parent->getBaseDirectory()); - + m_managedGameDirEdit->setText(m_parent->m_GamePlugin->gameDirectory().absoluteFilePath(m_parent->m_GamePlugin->binaryName())); QString basePath = parent->getBaseDirectory(); QDir baseDir(basePath); for (const auto &dir : { diff --git a/src/settings.h b/src/settings.h index 76ab55daa..c49edfcbe 100644 --- a/src/settings.h +++ b/src/settings.h @@ -403,6 +403,7 @@ public slots: QLineEdit *m_cacheDirEdit; QLineEdit *m_profilesDirEdit; QLineEdit *m_overwriteDirEdit; + QLineEdit *m_managedGameDirEdit; }; class DiagnosticsTab : public SettingsTab diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp index 44fc9b5e5..058d082a6 100644 --- a/src/settingsdialog.cpp +++ b/src/settingsdialog.cpp @@ -241,7 +241,7 @@ void SettingsDialog::deleteBlacklistItem() void SettingsDialog::on_associateButton_clicked() { - Settings::instance().registerAsNXMHandler(false); + Settings::instance().registerAsNXMHandler(true); } void SettingsDialog::on_clearCacheButton_clicked() diff --git a/src/settingsdialog.ui b/src/settingsdialog.ui index 85688cb8f..374e1b84e 100644 --- a/src/settingsdialog.ui +++ b/src/settingsdialog.ui @@ -17,7 +17,7 @@ - 0 + 1 @@ -170,26 +170,23 @@ If you use pre-releases, never contact me directly by e-mail or via private mess - - + + - Base Directory + ... - - + + - ... + ... - - - @@ -197,12 +194,8 @@ If you use pre-releases, never contact me directly by e-mail or via private mess - - - - Overwrite - - + + @@ -211,37 +204,57 @@ If you use pre-releases, never contact me directly by e-mail or via private mess
- - + + - ... + Overwrite + + + + + + + Directory where downloads are stored. + + + Directory where downloads are stored. - - + + - Downloads + ... - - + + - ... + Downloads - - - - Directory where mods are stored. + + + + Qt::Vertical - - Directory where mods are stored. Please note that changing this will break all associations of profiles with mods that don't exist in the new location (with the same name). + + + 20 + 40 + + + + + + + + Profiles @@ -258,20 +271,16 @@ If you use pre-releases, never contact me directly by e-mail or via private mess - - + + + + + - Directory where downloads are stored. + Directory where mods are stored. - Directory where downloads are stored. - - - - - - - Mods + Directory where mods are stored. Please note that changing this will break all associations of profiles with mods that don't exist in the new location (with the same name). @@ -282,18 +291,25 @@ If you use pre-releases, never contact me directly by e-mail or via private mess - - + + + + false + + + true + + - - + + - Profiles + Mods - - + + Qt::Vertical @@ -305,15 +321,29 @@ If you use pre-releases, never contact me directly by e-mail or via private mess + + + + Managed Game + + + + + + + Base Directory + + + + + + + Use %BASE_DIR% to refer to the Base Directory. + + +
- - - - Use %BASE_DIR% to refer to the Base Directory. - - - @@ -336,7 +366,7 @@ If you use pre-releases, never contact me directly by e-mail or via private mess
- + Nexus @@ -890,8 +920,8 @@ tl;dr-version: If Nexus-features don't work, insert the current version number o
- - false + + false Enforces that inactive ESPs and ESMs are never loaded. @@ -986,187 +1016,187 @@ For the other games this is not a sufficient replacement for AI!
- - Diagnostics - - - - - - - - Log Level - - - - - - - Decides the amount of data printed to "ModOrganizer.log" - - - + + Diagnostics + + + + + + + + Log Level + + + + + + + Decides the amount of data printed to "ModOrganizer.log" + + + Decides the amount of data printed to "ModOrganizer.log". "Debug" produces very useful information for finding problems. There is usually no noteworthy performance impact but the file may become rather large. If this is a problem you may prefer the "Info" level for regluar use. On the "Error" level the log file usually remains empty. - - - - Debug - - - - - Info (recommended) - - - - - Warning - - - - - Error - - - - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - - - Crash Dumps - - - - - - - Decides which type of crash dumps are collected when injected processes crash. - - - + + + + Debug + + + + + Info (recommended) + + + + + Warning + + + + + Error + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Crash Dumps + + + + + + + Decides which type of crash dumps are collected when injected processes crash. + + + Decides which type of crash dumps are collected when injected processes crash. "None" Disables the generation of crash dumps by MO. "Mini" Default level which generates small dumps (only stack traces). "Data" Much larger dumps with additional information which may be need (also data segments). "Full" Even larger dumps with a full memory dump of the process. - - - - None - - - - - Mini (recommended) - - - - - Data - - - - - Full - - - - - - - - - - - - Max Dumps To Keep - - - - - - - Qt::Horizontal - - - - 60 - 20 - - - - - - - - Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - - - + + + + None + + + + + Mini (recommended) + + + + + Data + + + + + Full + + + + + + + + + + + + Max Dumps To Keep + + + + + + + Qt::Horizontal + + + + 60 + 20 + + + + + + + + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. + + + Maximum number of crash dumps to keep on disk. Use 0 for unlimited. - Set "Crash Dumps" above to None to disable crash dump collection. + Set "Crash Dumps" above to None to disable crash dump collection. - - - - - - - - - + + + + + + + + + Hint: right click link and copy link location + + + Logs and crash dumps are stored under your current instance in the <a href="LOGS_FULL_PATH">LOGS_DIR</a> and <a href="DUMPS_FULL_PATH">DUMPS_DIR</a> folders. Sending logs and/or crash dumps to the developers can help investigate issues. It is recommended to compress large log and dmp files before sending. - - - true - - - Hint: right click link and copy link location - - - - - - - Qt::Vertical - - - QSizePolicy::Expanding - - - - 20 - 232 - - - - - - + + + true + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 232 + + + + +
+ diff --git a/src/shared/util.cpp b/src/shared/util.cpp index 5491a9e6c..102565f50 100644 --- a/src/shared/util.cpp +++ b/src/shared/util.cpp @@ -27,6 +27,7 @@ along with Mod Organizer. If not, see . #include #include #include +#include namespace MOShared { @@ -174,5 +175,78 @@ VS_FIXEDFILEINFO GetFileVersion(const std::wstring &fileName) } } +std::wstring GetFileVersionString(const std::wstring &fileName) +{ + DWORD handle = 0UL; + DWORD size = ::GetFileVersionInfoSizeW(fileName.c_str(), &handle); + if (size == 0) { + throw windows_error("failed to determine file version info size"); + } + + boost::scoped_array buffer(new char[size]); + try { + handle = 0UL; + if (!::GetFileVersionInfoW(fileName.c_str(), handle, size, buffer.get())) { + throw windows_error("failed to determine file version info"); + } + + LPVOID strBuffer = nullptr; + UINT strLength = 0; + if (!::VerQueryValue(buffer.get(), L"\\StringFileInfo\\040904B0\\ProductVersion", &strBuffer, &strLength)) { + throw windows_error("failed to determine file version"); + } + + return std::wstring((LPCTSTR)strBuffer); + } + catch (...) { + throw; + } +} + +MOBase::VersionInfo createVersionInfo() +{ + VS_FIXEDFILEINFO version = GetFileVersion(QApplication::applicationFilePath().toStdWString()); + + if (version.dwFileFlags | VS_FF_PRERELEASE) + { + // Pre-release builds need annotating + QString versionString = QString::fromStdWString(GetFileVersionString(QApplication::applicationFilePath().toStdWString())); + + // The pre-release flag can be set without the string specifying what type of pre-release + bool noLetters = true; + for (QChar character : versionString) + { + if (character.isLetter()) + { + noLetters = false; + break; + } + } + + if (noLetters) + { + // Default to pre-alpha when release type is unspecified + return MOBase::VersionInfo(version.dwFileVersionMS >> 16, + version.dwFileVersionMS & 0xFFFF, + version.dwFileVersionLS >> 16, + version.dwFileVersionLS & 0xFFFF, + MOBase::VersionInfo::RELEASE_PREALPHA); + } + else + { + // Trust the string to make sense + return MOBase::VersionInfo(versionString); + } + } + else + { + // Non-pre-release builds just need their version numbers reading + return MOBase::VersionInfo(version.dwFileVersionMS >> 16, + version.dwFileVersionMS & 0xFFFF, + version.dwFileVersionLS >> 16, + version.dwFileVersionLS & 0xFFFF); + } +} + } // namespace MOShared diff --git a/src/shared/util.h b/src/shared/util.h index 1e4980597..1fdfb0891 100644 --- a/src/shared/util.h +++ b/src/shared/util.h @@ -25,6 +25,8 @@ along with Mod Organizer. If not, see . #define WIN32_LEAN_AND_MEAN #include +#include + namespace MOShared { /// Test if a file (or directory) by the specified name exists @@ -45,6 +47,8 @@ std::wstring ToLower(const std::wstring &text); bool CaseInsensitiveEqual(const std::wstring &lhs, const std::wstring &rhs); VS_FIXEDFILEINFO GetFileVersion(const std::wstring &fileName); +std::wstring GetFileVersionString(const std::wstring &fileName); +MOBase::VersionInfo createVersionInfo(); } // namespace MOShared diff --git a/src/version.rc b/src/version.rc index 083cbfb01..39565d70c 100644 --- a/src/version.rc +++ b/src/version.rc @@ -1,13 +1,16 @@ #include "Winver.h" -#define VER_FILEVERSION 2,1,3 -#define VER_FILEVERSION_STR "2.1.3\0" +// If VS_FF_PRERELEASE is not set, MO labels the build as a release and uses VER_FILEVERSION to determine version number. +// Otherwise, if letters are used in VER_FILEVERSION_STR, uses the full MOBase::VersionInfo parser +// Otherwise, uses the numbers from VER_FILEVERSION and sets the release type as pre-alpha +#define VER_FILEVERSION 2,1,4 +#define VER_FILEVERSION_STR "2.1.4\0" VS_VERSION_INFO VERSIONINFO FILEVERSION VER_FILEVERSION PRODUCTVERSION VER_FILEVERSION FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -FILEFLAGS (0) +FILEFLAGS VS_FF_PRERELEASE FILEOS VOS__WINDOWS32 FILETYPE VFT_APP FILESUBTYPE (0)