/*
Copyright (C) 2012 Sebastian Herbord. All rights reserved.
This file is part of Mod Organizer.
Mod Organizer is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Mod Organizer is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with Mod Organizer. If not, see .
*/
#include "modinfodialog.h"
#include "ui_modinfodialog.h"
#include "descriptionpage.h"
#include "mainwindow.h"
#include "modidlineedit.h"
#include "iplugingame.h"
#include "nexusinterface.h"
#include "report.h"
#include "utility.h"
#include "messagedialog.h"
#include "bbcode.h"
#include "questionboxmemory.h"
#include "settings.h"
#include "categories.h"
#include "organizercore.h"
#include "pluginlistsortproxy.h"
#include "previewgenerator.h"
#include "previewdialog.h"
#include "texteditor.h"
#include "modinfodialogtextfiles.h"
#include "modinfodialogimages.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace MOBase;
using namespace MOShared;
std::vector> ModInfoDialogTab::createTabs(
Ui::ModInfoDialog* ui)
{
std::vector> v;
v.push_back(std::make_unique(ui));
v.push_back(std::make_unique(ui));
v.push_back(std::make_unique(ui));
return v;
}
bool ModInfoDialogTab::canClose()
{
return true;
}
class ModFileListWidget : public QListWidgetItem {
friend bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS);
public:
ModFileListWidget(const QString &text, int sortValue, QListWidget *parent = 0)
: QListWidgetItem(text, parent, QListWidgetItem::UserType + 1), m_SortValue(sortValue) {}
private:
int m_SortValue;
};
static bool operator<(const ModFileListWidget &LHS, const ModFileListWidget &RHS)
{
return LHS.m_SortValue < RHS.m_SortValue;
}
// if there are more than 50 selected items in the conflict tree or filetree,
// don't bother checking whether menu items apply to them, just show all of them
const int max_scan_for_context_menu = 50;
class ElideLeftDelegate : public QStyledItemDelegate
{
public:
using QStyledItemDelegate::QStyledItemDelegate;
protected:
void initStyleOption(QStyleOptionViewItem *option, const QModelIndex &index) const
{
QStyledItemDelegate::initStyleOption(option, index);
option->textElideMode = Qt::ElideLeft;
}
};
bool canPreviewFile(
PluginContainer* pluginContainer, bool isArchive, const QString& filename)
{
if (isArchive) {
return false;
}
const auto ext = QFileInfo(filename).suffix();
return pluginContainer->previewGenerator().previewSupported(ext);
}
bool canOpenFile(bool isArchive, const QString&)
{
// can open anything as long as it's not in an archive
return !isArchive;
}
bool canHideFile(bool isArchive, const QString& filename)
{
if (isArchive) {
// can't hide files from archives
return false;
}
if (filename.endsWith(ModInfo::s_HiddenExt)) {
// already hidden
return false;
}
return true;
}
bool canUnhideFile(bool isArchive, const QString& filename)
{
if (isArchive) {
// can't unhide files from archives
return false;
}
if (!filename.endsWith(ModInfo::s_HiddenExt)) {
// already visible
return false;
}
return true;
}
int naturalCompare(const QString& a, const QString& b)
{
static QCollator c = []{
QCollator c;
c.setNumericMode(true);
c.setCaseSensitivity(Qt::CaseInsensitive);
return c;
}();
return c.compare(a, b);
}
class ConflictItem : public QTreeWidgetItem
{
public:
static const int FILENAME_USERROLE = Qt::UserRole + 1;
static const int ALT_ORIGIN_USERROLE = Qt::UserRole + 2;
static const int ARCHIVE_USERROLE = Qt::UserRole + 3;
static const int INDEX_USERROLE = Qt::UserRole + 4;
static const int HAS_ALTS_USERROLE = Qt::UserRole + 5;
ConflictItem(
QStringList columns, FileEntry::Index index, const QString& fileName,
bool hasAltOrigins, const QString& altOrigin, bool archive)
: QTreeWidgetItem(columns)
{
setData(0, FILENAME_USERROLE, fileName);
setData(0, ALT_ORIGIN_USERROLE, altOrigin);
setData(0, ARCHIVE_USERROLE, archive);
setData(0, INDEX_USERROLE, index);
setData(0, HAS_ALTS_USERROLE, hasAltOrigins);
if (archive) {
QFont f = font(0);
f.setItalic(true);
for (int i=0; i);
return data(0, INDEX_USERROLE).toUInt();
}
bool canHide() const
{
return canHideFile(isArchive(), fileName());
}
bool canUnhide() const
{
return canUnhideFile(isArchive(), fileName());
}
bool canOpen() const
{
return canOpenFile(isArchive(), fileName());
}
bool canPreview(PluginContainer* pluginContainer) const
{
return canPreviewFile(pluginContainer, isArchive(), fileName());
}
bool operator<(const QTreeWidgetItem& other) const
{
const int column = treeWidget()->sortColumn();
if (column >= columnCount() || column >= other.columnCount()) {
// shouldn't happen
qWarning().nospace() << "ConflictItem::operator<() mistmatch in column count";
return false;
}
return (naturalCompare(text(column), other.text(column)) < 0);
}
};
ModInfoDialog::ModInfoDialog(ModInfo::Ptr modInfo, const DirectoryEntry *directory, bool unmanaged, OrganizerCore *organizerCore, PluginContainer *pluginContainer, QWidget *parent)
: TutorableDialog("ModInfoDialog", parent), ui(new Ui::ModInfoDialog), m_ModInfo(modInfo),
m_RequestStarted(false),
m_NewFolderAction(nullptr), m_OpenAction(nullptr), m_PreviewAction(nullptr),
m_RenameAction(nullptr), m_DeleteAction(nullptr), m_HideAction(nullptr),
m_UnhideAction(nullptr), m_Directory(directory), m_Origin(nullptr),
m_OrganizerCore(organizerCore), m_PluginContainer(pluginContainer)
{
ui->setupUi(this);
m_tabs = ModInfoDialogTab::createTabs(ui);
this->setWindowTitle(modInfo->name());
this->setWindowModality(Qt::WindowModal);
m_RootPath = modInfo->absolutePath();
QString metaFileName = m_RootPath.mid(0).append("/meta.ini");
m_Settings = new QSettings(metaFileName, QSettings::IniFormat);
QLineEdit *modIDEdit = findChild("modIDEdit");
ui->modIDEdit->setValidator(new QIntValidator(modIDEdit));
ui->modIDEdit->setText(QString("%1").arg(modInfo->getNexusID()));
connect(ui->modIDEdit, SIGNAL(linkClicked(QString)), this, SLOT(linkClicked(QString)));
QString gameName = modInfo->getGameName();
ui->sourceGameEdit->addItem(organizerCore->managedGame()->gameName(), organizerCore->managedGame()->gameShortName());
if (organizerCore->managedGame()->validShortNames().size() == 0) {
ui->sourceGameEdit->setDisabled(true);
} else {
for (auto game : pluginContainer->plugins()) {
for (QString gameName : organizerCore->managedGame()->validShortNames()) {
if (game->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) {
ui->sourceGameEdit->addItem(game->gameName(), game->gameShortName());
break;
}
}
}
}
ui->sourceGameEdit->setCurrentIndex(ui->sourceGameEdit->findData(gameName));
ui->commentsEdit->setText(modInfo->comments());
ui->notesEdit->setText(modInfo->notes());
ui->descriptionView->setPage(new DescriptionPage());
connect(m_ModInfo.data(), SIGNAL(modDetailsUpdated(bool)), this, SLOT(modDetailsUpdated(bool)));
connect(ui->descriptionView->page(), SIGNAL(linkClicked(QUrl)), this, SLOT(linkClicked(QUrl)));
//TODO: No easy way to delegate links
//ui->descriptionView->page()->acceptNavigationRequest(QWebEnginePage::DelegateAllLinks);
new QShortcut(QKeySequence::Delete, this, SLOT(delete_activated()));
if (directory->originExists(ToWString(modInfo->name()))) {
m_Origin = &directory->getOriginByName(ToWString(modInfo->name()));
if (m_Origin->isDisabled()) {
m_Origin = nullptr;
}
}
// refresh everything but the conflict lists, which are done in exec() because
// they depend on restoring the state to some widgets; this refresh has to be
// done here because some of the checks below depend on the ui to decide which
// tabs to enable
refreshFiles();
if (modInfo->hasFlag(ModInfo::FLAG_SEPARATOR))
{
ui->tabWidget->setTabEnabled(TAB_TEXTFILES, false);
ui->tabWidget->setTabEnabled(TAB_INIFILES, false);
ui->tabWidget->setTabEnabled(TAB_IMAGES, false);
ui->tabWidget->setTabEnabled(TAB_ESPS, false);
ui->tabWidget->setTabEnabled(TAB_CONFLICTS, false);
//ui->tabWidget->setTabEnabled(TAB_CATEGORIES, false);
addCategories(CategoryFactory::instance(), modInfo->getCategories(), ui->categoriesTree->invisibleRootItem(), 0);
refreshPrimaryCategoriesBox();
ui->tabWidget->setTabEnabled(TAB_NEXUS, false);
//ui->tabWidget->setTabEnabled(TAB_NOTES, false);
ui->tabWidget->setTabEnabled(TAB_FILETREE, false);
}
else if (unmanaged)
{
ui->tabWidget->setTabEnabled(TAB_TEXTFILES, false);
ui->tabWidget->setTabEnabled(TAB_INIFILES, false);
ui->tabWidget->setTabEnabled(TAB_IMAGES, false);
ui->tabWidget->setTabEnabled(TAB_CATEGORIES, false);
ui->tabWidget->setTabEnabled(TAB_NEXUS, false);
ui->tabWidget->setTabEnabled(TAB_FILETREE, false);
ui->tabWidget->setTabEnabled(TAB_NOTES, false);
ui->tabWidget->setTabEnabled(TAB_ESPS, false);
} else {
ui->tabWidget->setTabEnabled(TAB_TEXTFILES, true);
ui->tabWidget->setTabEnabled(TAB_INIFILES, true);
ui->tabWidget->setTabEnabled(TAB_IMAGES, true);
initFiletree(modInfo);
addCategories(CategoryFactory::instance(), modInfo->getCategories(), ui->categoriesTree->invisibleRootItem(), 0);
refreshPrimaryCategoriesBox();
}
ui->tabWidget->setTabEnabled(TAB_CONFLICTS, m_Origin != nullptr);
ui->endorseBtn->setVisible(Settings::instance().endorsementIntegration());
ui->endorseBtn->setEnabled((m_ModInfo->endorsedState() == ModInfo::ENDORSED_FALSE) ||
(m_ModInfo->endorsedState() == ModInfo::ENDORSED_NEVER));
// activate first enabled tab
for (int i = 0; i < ui->tabWidget->count(); ++i) {
if (ui->tabWidget->isTabEnabled(i)) {
ui->tabWidget->setCurrentIndex(i);
break;
}
}
if (ui->tabWidget->currentIndex() == TAB_NEXUS) {
activateNexusTab();
}
m_overwriteExpander.set(ui->overwriteExpander, ui->overwriteTree, true);
m_overwrittenExpander.set(ui->overwrittenExpander, ui->overwrittenTree, true);
m_nonconflictExpander.set(ui->noConflictExpander, ui->noConflictTree);
m_advancedConflictFilter.set(ui->conflictsAdvancedFilter);
m_advancedConflictFilter.changed = [&]{ refreshConflictLists(false, true); };
// left-elide the overwrites column so that the nearest are visible
ui->conflictsAdvancedList->setItemDelegateForColumn(
0, new ElideLeftDelegate(ui->conflictsAdvancedList));
// left-elide the file column to see filenames
ui->conflictsAdvancedList->setItemDelegateForColumn(
1, new ElideLeftDelegate(ui->conflictsAdvancedList));
// don't elide the overwritten by column so that the nearest are visible
connect(ui->conflictsAdvancedShowNoConflict, &QCheckBox::clicked, [&] {
refreshConflictLists(false, true);
});
connect(ui->conflictsAdvancedShowAll, &QRadioButton::clicked, [&] {
refreshConflictLists(false, true);
});
connect(ui->conflictsAdvancedShowNearest, &QRadioButton::clicked, [&] {
refreshConflictLists(false, true);
});
}
ModInfoDialog::~ModInfoDialog()
{
m_ModInfo->setComments(ui->commentsEdit->text());
//Avoid saving html stump if notes field is empty.
if (ui->notesEdit->toPlainText().isEmpty())
m_ModInfo->setNotes(ui->notesEdit->toPlainText());
else
m_ModInfo->setNotes(ui->notesEdit->toHtml());
saveCategories(ui->categoriesTree->invisibleRootItem());
delete ui->descriptionView->page();
delete ui->descriptionView;
delete ui;
delete m_Settings;
}
int ModInfoDialog::exec()
{
// no need to refresh the other stuff, that was done in the constructor
refreshConflictLists(true, true);
return TutorableDialog::exec();
}
void ModInfoDialog::initFiletree(ModInfo::Ptr modInfo)
{
ui->fileTree = findChild("fileTree");
m_FileSystemModel = new QFileSystemModel(this);
m_FileSystemModel->setReadOnly(false);
m_FileSystemModel->setRootPath(m_RootPath);
ui->fileTree->setModel(m_FileSystemModel);
ui->fileTree->setRootIndex(m_FileSystemModel->index(m_RootPath));
ui->fileTree->setColumnWidth(0, 300);
m_NewFolderAction = new QAction(tr("&New Folder"), ui->fileTree);
m_OpenAction = new QAction(tr("&Open"), ui->fileTree);
m_PreviewAction = new QAction(tr("&Preview"), ui->fileTree);
m_RenameAction = new QAction(tr("&Rename"), ui->fileTree);
m_DeleteAction = new QAction(tr("&Delete"), ui->fileTree);
m_HideAction = new QAction(tr("&Hide"), ui->fileTree);
m_UnhideAction = new QAction(tr("&Unhide"), ui->fileTree);
connect(m_NewFolderAction, SIGNAL(triggered()), this, SLOT(createDirectoryTriggered()));
connect(m_OpenAction, SIGNAL(triggered()), this, SLOT(openTriggered()));
connect(m_PreviewAction, SIGNAL(triggered()), this, SLOT(previewTriggered()));
connect(m_RenameAction, SIGNAL(triggered()), this, SLOT(renameTriggered()));
connect(m_DeleteAction, SIGNAL(triggered()), this, SLOT(deleteTriggered()));
connect(m_HideAction, SIGNAL(triggered()), this, SLOT(hideTriggered()));
connect(m_UnhideAction, SIGNAL(triggered()), this, SLOT(unhideTriggered()));
}
int ModInfoDialog::tabIndex(const QString &tabId)
{
for (int i = 0; i < ui->tabWidget->count(); ++i) {
if (ui->tabWidget->widget(i)->objectName() == tabId) {
return i;
}
}
return -1;
}
void ModInfoDialog::saveState(Settings& s) const
{
s.directInterface().setValue("mod_info_tabs", saveTabState());
s.directInterface().setValue("mod_info_conflicts", saveConflictsState());
s.directInterface().setValue(
"mod_info_conflicts_overwrite",
ui->overwriteTree->header()->saveState());
s.directInterface().setValue(
"mod_info_conflicts_noconflict",
ui->noConflictTree->header()->saveState());
s.directInterface().setValue(
"mod_info_conflicts_overwritten",
ui->overwrittenTree->header()->saveState());
s.directInterface().setValue(
"mod_info_advanced_conflicts",
ui->conflictsAdvancedList->header()->saveState());
}
void ModInfoDialog::restoreState(const Settings& s)
{
restoreTabState(s.directInterface().value("mod_info_tabs").toByteArray());
restoreConflictsState(s.directInterface().value("mod_info_conflicts").toByteArray());
ui->overwriteTree->header()->restoreState(
s.directInterface().value("mod_info_conflicts_overwrite").toByteArray());
ui->noConflictTree->header()->restoreState(
s.directInterface().value("mod_info_conflicts_noconflict").toByteArray());
ui->overwrittenTree->header()->restoreState(
s.directInterface().value("mod_info_conflicts_overwritten").toByteArray());
ui->conflictsAdvancedList->header()->restoreState(
s.directInterface().value("mod_info_advanced_conflicts").toByteArray());
}
void ModInfoDialog::restoreTabState(const QByteArray &state)
{
QDataStream stream(state);
int count = 0;
stream >> count;
QStringList tabIds;
// first, only determine the new mapping
for (int newPos = 0; newPos < count; ++newPos) {
QString tabId;
stream >> tabId;
tabIds.append(tabId);
int oldPos = tabIndex(tabId);
if (oldPos != -1) {
m_RealTabPos[newPos] = oldPos;
} else {
m_RealTabPos[newPos] = newPos;
}
}
// then actually move the tabs
QTabBar *tabBar = ui->tabWidget->findChild("qt_tabwidget_tabbar"); // magic name = bad
ui->tabWidget->blockSignals(true);
for (int newPos = 0; newPos < count; ++newPos) {
QString tabId = tabIds.at(newPos);
int oldPos = tabIndex(tabId);
tabBar->moveTab(oldPos, newPos);
}
ui->tabWidget->blockSignals(false);
}
void ModInfoDialog::restoreConflictsState(const QByteArray &state)
{
QDataStream stream(state);
bool overwriteExpanded = false;
bool overwrittenExpanded = false;
bool noConflictExpanded = false;
stream >> overwriteExpanded >> overwrittenExpanded >> noConflictExpanded;
if (stream.status() == QDataStream::Ok) {
m_overwriteExpander.toggle(overwriteExpanded);
m_overwrittenExpander.toggle(overwrittenExpanded);
m_nonconflictExpander.toggle(noConflictExpanded);
}
int index = 0;
bool noConflictChecked = false;
bool showAllChecked = false;
bool showNearestChecked = false;
stream >> index >> noConflictChecked >> showAllChecked >> showNearestChecked;
if (stream.status() == QDataStream::Ok) {
ui->tabConflictsTabs->setCurrentIndex(index);
ui->conflictsAdvancedShowNoConflict->setChecked(noConflictChecked);
ui->conflictsAdvancedShowAll->setChecked(showAllChecked);
ui->conflictsAdvancedShowNearest->setChecked(showNearestChecked);
}
}
QByteArray ModInfoDialog::saveTabState() const
{
QByteArray result;
QDataStream stream(&result, QIODevice::WriteOnly);
stream << ui->tabWidget->count();
for (int i = 0; i < ui->tabWidget->count(); ++i) {
stream << ui->tabWidget->widget(i)->objectName();
}
return result;
}
QByteArray ModInfoDialog::saveConflictsState() const
{
QByteArray result;
QDataStream stream(&result, QIODevice::WriteOnly);
stream
<< m_overwriteExpander.opened()
<< m_overwrittenExpander.opened()
<< m_nonconflictExpander.opened()
<< ui->tabConflictsTabs->currentIndex()
<< ui->conflictsAdvancedShowNoConflict->isChecked()
<< ui->conflictsAdvancedShowAll->isChecked()
<< ui->conflictsAdvancedShowNearest->isChecked();
return result;
}
void ModInfoDialog::refreshLists()
{
refreshConflictLists(true, true);
refreshFiles();
}
void ModInfoDialog::refreshConflictLists(
bool refreshGeneral, bool refreshAdvanced)
{
int numNonConflicting = 0;
int numOverwrite = 0;
int numOverwritten = 0;
if (refreshGeneral) {
ui->overwriteTree->clear();
ui->overwrittenTree->clear();
ui->noConflictTree->clear();
}
if (refreshAdvanced) {
ui->conflictsAdvancedList->clear();
}
if (m_Origin != nullptr) {
std::vector files = m_Origin->getFiles();
for (const auto& file : m_Origin->getFiles()) {
const QString relativeName = QDir::fromNativeSeparators(ToQString(file->getRelativePath()));
const QString fileName = relativeName.mid(0).prepend(m_RootPath);
bool archive = false;
const int fileOrigin = file->getOrigin(archive);
const auto& alternatives = file->getAlternatives();
if (refreshGeneral) {
if (fileOrigin == m_Origin->getID()) {
if (!alternatives.empty()) {
ui->overwriteTree->addTopLevelItem(createOverwriteItem(
file->getIndex(), archive, fileName, relativeName, alternatives));
++numOverwrite;
} else {
// otherwise, put the file in the noconflict tree
ui->noConflictTree->addTopLevelItem(createNoConflictItem(
file->getIndex(), archive, fileName, relativeName));
++numNonConflicting;
}
} else {
ui->overwrittenTree->addTopLevelItem(createOverwrittenItem(
file->getIndex(), fileOrigin, archive, fileName, relativeName));
++numOverwritten;
}
}
if (refreshAdvanced) {
auto* advancedItem = createAdvancedConflictItem(
file->getIndex(), fileOrigin, archive,
fileName, relativeName, alternatives);
if (advancedItem) {
ui->conflictsAdvancedList->addTopLevelItem(advancedItem);
}
}
}
}
if (refreshGeneral) {
ui->overwriteCount->display(numOverwrite);
ui->overwrittenCount->display(numOverwritten);
ui->noConflictCount->display(numNonConflicting);
}
}
QTreeWidgetItem* ModInfoDialog::createOverwriteItem(
FileEntry::Index index, bool archive,
const QString& fileName, const QString& relativeName,
const FileEntry::AlternativesVector& alternatives)
{
QString altString;
for (const auto& alt : alternatives) {
if (!altString.isEmpty()) {
altString += ", ";
}
altString += ToQString(m_Directory->getOriginByID(alt.first).getName());
}
QStringList fields(relativeName);
fields.append(altString);
const auto origin = ToQString(m_Directory->getOriginByID(alternatives.back().first).getName());
return new ConflictItem(fields, index, fileName, true, origin, archive);
}
QTreeWidgetItem* ModInfoDialog::createNoConflictItem(
FileEntry::Index index, bool archive,
const QString& fileName, const QString& relativeName)
{
return new ConflictItem({relativeName}, index, fileName, false, "", archive);
}
QTreeWidgetItem* ModInfoDialog::createOverwrittenItem(
FileEntry::Index index, int fileOrigin, bool archive,
const QString& fileName, const QString& relativeName)
{
const FilesOrigin &realOrigin = m_Directory->getOriginByID(fileOrigin);
QStringList fields(relativeName);
fields.append(ToQString(realOrigin.getName()));
return new ConflictItem(
fields, index, fileName, true, ToQString(realOrigin.getName()), archive);
}
QTreeWidgetItem* ModInfoDialog::createAdvancedConflictItem(
FileEntry::Index index,int fileOrigin, bool archive,
const QString& fileName, const QString& relativeName,
const MOShared::FileEntry::AlternativesVector& alternatives)
{
QString before, after;
if (!alternatives.empty()) {
int beforePrio = 0;
int afterPrio = std::numeric_limits::max();
for (const auto& alt : alternatives)
{
const auto altOrigin = m_Directory->getOriginByID(alt.first);
if (ui->conflictsAdvancedShowAll->isChecked()) {
// fills 'before' and 'after' with all the alternatives that come
// before and after this mod in terms of priority
if (altOrigin.getPriority() < m_Origin->getPriority()) {
// add all the mods having a lower priority than this one
if (!before.isEmpty()) {
before += ", ";
}
before += ToQString(altOrigin.getName());
} else if (altOrigin.getPriority() > m_Origin->getPriority()) {
// add all the mods having a higher priority than this one
if (!after.isEmpty()) {
after += ", ";
}
after += ToQString(altOrigin.getName());
}
} else {
// keep track of the nearest mods that come before and after this one
// in terms of priority
if (altOrigin.getPriority() < m_Origin->getPriority()) {
// the alternative has a lower priority than this mod
if (altOrigin.getPriority() > beforePrio) {
// the alternative has a higher priority and therefore is closer
// to this mod, use it
before = ToQString(altOrigin.getName());
beforePrio = altOrigin.getPriority();
}
}
if (altOrigin.getPriority() > m_Origin->getPriority()) {
// the alternative has a higher priority than this mod
if (altOrigin.getPriority() < afterPrio) {
// the alternative has a lower priority and there is closer
// to this mod, use it
after = ToQString(altOrigin.getName());
afterPrio = altOrigin.getPriority();
}
}
}
}
// the primary origin is never in the list of alternatives, so it has to
// be handled separately
//
// if 'after' is not empty, it means at least one alternative with a higher
// priority than this mod was found; if the user only wants to see the
// nearest mods, it's not worth checking for the primary origin because it
// will always have a higher priority than the alternatives (or it wouldn't
// be the primary)
if (after.isEmpty() || ui->conflictsAdvancedShowAll->isChecked()) {
FilesOrigin &realOrigin = m_Directory->getOriginByID(fileOrigin);
// if no mods overwrite this file, the primary origin is the same as this
// mod, so ignore that
if (realOrigin.getID() != m_Origin->getID()) {
if (!after.isEmpty()) {
after += ", ";
}
after += ToQString(realOrigin.getName());
}
}
}
bool hasAlts = !before.isEmpty() || !after.isEmpty();
if (!ui->conflictsAdvancedShowNoConflict->isChecked()) {
// if both before and after are empty, it means this file has no conflicts
// at all, only display it if the user wants it
if (!hasAlts) {
return nullptr;
}
}
bool matched = m_advancedConflictFilter.matches([&](auto&& what) {
return
before.contains(what, Qt::CaseInsensitive) ||
relativeName.contains(what, Qt::CaseInsensitive) ||
after.contains(what, Qt::CaseInsensitive);
});
if (!matched) {
return nullptr;
}
return new ConflictItem(
{before, relativeName, after}, index, fileName, hasAlts, "", archive);
}
void ModInfoDialog::refreshFiles()
{
// clearing
for (auto& tab : m_tabs) {
tab->clear();
}
ui->inactiveESPList->clear();
ui->activeESPList->clear();
if (m_RootPath.length() > 0) {
QDirIterator dirIterator(m_RootPath, QDir::Files, QDirIterator::Subdirectories);
while (dirIterator.hasNext()) {
QString fileName = dirIterator.next();
for (auto& tab : m_tabs) {
if (tab->feedFile(m_RootPath, fileName)) {
break;
}
}
if (fileName.endsWith(".esp", Qt::CaseInsensitive) ||
fileName.endsWith(".esm", Qt::CaseInsensitive) ||
fileName.endsWith(".esl", Qt::CaseInsensitive)) {
QString relativePath = fileName.mid(m_RootPath.length() + 1);
if (relativePath.contains('/')) {
QFileInfo fileInfo(fileName);
QListWidgetItem *newItem = new QListWidgetItem(fileInfo.fileName());
newItem->setData(Qt::UserRole, relativePath);
ui->inactiveESPList->addItem(newItem);
} else {
ui->activeESPList->addItem(relativePath);
}
}
}
}
ui->tabWidget->setTabEnabled(TAB_ESPS, (ui->inactiveESPList->count() != 0) || (ui->activeESPList->count() != 0));
}
void ModInfoDialog::addCategories(const CategoryFactory &factory, const std::set &enabledCategories, QTreeWidgetItem *root, int rootLevel)
{
for (int i = 0; i < static_cast(factory.numCategories()); ++i) {
if (factory.getParentID(i) != rootLevel) {
continue;
}
int categoryID = factory.getCategoryID(i);
QTreeWidgetItem *newItem
= new QTreeWidgetItem(QStringList(factory.getCategoryName(i)));
newItem->setFlags(newItem->flags() | Qt::ItemIsUserCheckable);
newItem->setCheckState(0, enabledCategories.find(categoryID)
!= enabledCategories.end()
? Qt::Checked
: Qt::Unchecked);
newItem->setData(0, Qt::UserRole, categoryID);
if (factory.hasChildren(i)) {
addCategories(factory, enabledCategories, newItem, categoryID);
}
root->addChild(newItem);
}
}
void ModInfoDialog::saveCategories(QTreeWidgetItem *currentNode)
{
for (int i = 0; i < currentNode->childCount(); ++i) {
QTreeWidgetItem *childNode = currentNode->child(i);
m_ModInfo->setCategory(childNode->data(0, Qt::UserRole).toInt(), childNode->checkState(0));
saveCategories(childNode);
}
}
void ModInfoDialog::on_closeButton_clicked()
{
for (auto& tab : m_tabs) {
if (!tab->canClose()) {
return;
}
}
close();
}
QString ModInfoDialog::getModVersion() const
{
return m_Settings->value("version", "").toString();
}
const int ModInfoDialog::getModID() const
{
return m_Settings->value("modid", 0).toInt();
}
void ModInfoDialog::openTab(int tab)
{
QTabWidget *tabWidget = findChild("tabWidget");
if (tabWidget->isTabEnabled(tab)) {
tabWidget->setCurrentIndex(tab);
}
}
void ModInfoDialog::on_activateESP_clicked()
{
QListWidget *activeESPList = findChild("activeESPList");
QListWidget *inactiveESPList = findChild("inactiveESPList");
int selectedRow = inactiveESPList->currentRow();
if (selectedRow < 0) {
return;
}
QListWidgetItem *selectedItem = inactiveESPList->takeItem(selectedRow);
QDir root(m_RootPath);
bool renamed = false;
while (root.exists(selectedItem->text())) {
bool okClicked = false;
QString newName = QInputDialog::getText(this, tr("File Exists"), tr("A file with that name exists, please enter a new one"), QLineEdit::Normal, selectedItem->text(), &okClicked);
if (!okClicked) {
inactiveESPList->insertItem(selectedRow, selectedItem);
return;
} else if (newName.size() > 0) {
selectedItem->setText(newName);
renamed = true;
}
}
if (root.rename(selectedItem->data(Qt::UserRole).toString(), selectedItem->text())) {
activeESPList->addItem(selectedItem);
if (renamed) {
selectedItem->setData(Qt::UserRole, QVariant());
}
} else {
inactiveESPList->insertItem(selectedRow, selectedItem);
reportError(tr("failed to move file"));
}
}
void ModInfoDialog::on_deactivateESP_clicked()
{
QListWidget *activeESPList = findChild("activeESPList");
QListWidget *inactiveESPList = findChild("inactiveESPList");
int selectedRow = activeESPList->currentRow();
if (selectedRow < 0) {
return;
}
QDir root(m_RootPath);
QListWidgetItem *selectedItem = activeESPList->takeItem(selectedRow);
// if we moved the file from optional to active in this session, we move the file back to
// where it came from. Otherwise, it is moved to the new folder "optional"
if (selectedItem->data(Qt::UserRole).isNull()) {
selectedItem->setData(Qt::UserRole, QString("optional/") + selectedItem->text());
if (!root.exists("optional")) {
if (!root.mkdir("optional")) {
reportError(tr("failed to create directory \"optional\""));
activeESPList->insertItem(selectedRow, selectedItem);
return;
}
}
}
if (root.rename(selectedItem->text(), selectedItem->data(Qt::UserRole).toString())) {
inactiveESPList->addItem(selectedItem);
} else {
activeESPList->insertItem(selectedRow, selectedItem);
}
}
void ModInfoDialog::on_visitNexusLabel_linkActivated(const QString &link)
{
emit linkActivated(link);
}
void ModInfoDialog::linkClicked(const QUrl &url)
{
//Ideally we'd ask the mod for the game and the web service then pass the game
//and URL to the web service
if (NexusInterface::instance(m_PluginContainer)->isURLGameRelated(url)) {
emit linkActivated(url.toString());
} else {
shell::OpenLink(url);
}
}
void ModInfoDialog::linkClicked(QString url)
{
emit linkActivated(url);
}
void ModInfoDialog::refreshNexusData(int modID)
{
if ((!m_RequestStarted) && (modID > 0)) {
m_RequestStarted = true;
m_ModInfo->updateNXMInfo();
MessageDialog::showMessage(tr("Info requested, please wait"), this);
}
}
QString ModInfoDialog::getFileCategory(int categoryID)
{
switch (categoryID) {
case 1: return tr("Main");
case 2: return tr("Update");
case 3: return tr("Optional");
case 4: return tr("Old");
case 5: return tr("Miscellaneous");
case 6: return tr("Deleted");
default: return tr("Unknown");
}
}
void ModInfoDialog::updateVersionColor()
{
// QPalette versionColor;
if (m_ModInfo->getVersion() != m_ModInfo->getNewestVersion()) {
ui->versionEdit->setStyleSheet("color: red");
// versionColor.setColor(QPalette::Text, Qt::red);
ui->versionEdit->setToolTip(tr("Current Version: %1").arg(m_ModInfo->getNewestVersion().canonicalString()));
} else {
ui->versionEdit->setStyleSheet("color: green");
// versionColor.setColor(QPalette::Text, Qt::green);
ui->versionEdit->setToolTip(tr("No update available"));
}
// ui->versionEdit->setPalette(versionColor);
}
void ModInfoDialog::modDetailsUpdated(bool success)
{
QString nexusDescription = m_ModInfo->getNexusDescription();
QString descriptionAsHTML = ""
""
"%1"
"";
if (!nexusDescription.isEmpty()) {
descriptionAsHTML = descriptionAsHTML.arg(BBCode::convertToHTML(nexusDescription));
} else {
descriptionAsHTML = descriptionAsHTML.arg(tr("
Uh oh!
Sorry, there is no description available for this mod. :(
"));
}
ui->descriptionView->page()->setHtml(descriptionAsHTML);
updateVersionColor();
}
void ModInfoDialog::activateNexusTab()
{
QLineEdit *modIDEdit = findChild("modIDEdit");
int modID = modIDEdit->text().toInt();
if (modID > 0) {
QString nexusLink = NexusInterface::instance(m_PluginContainer)->getModURL(modID, m_ModInfo->getGameName());
QLabel *visitNexusLabel = findChild("visitNexusLabel");
visitNexusLabel->setText(tr("Visit on Nexus").arg(nexusLink));
visitNexusLabel->setToolTip(nexusLink);
m_ModInfo->setURL(nexusLink);
if (m_ModInfo->getNexusDescription().isEmpty() || QDateTime::currentDateTimeUtc() >= m_ModInfo->getLastNexusQuery().addDays(1)) {
refreshNexusData(modID);
} else {
modDetailsUpdated(true);
}
} else
modDetailsUpdated(true);
QLineEdit *versionEdit = findChild("versionEdit");
QString currentVersion = m_Settings->value("version", "???").toString();
versionEdit->setText(currentVersion);
ui->customUrlLineEdit->setText(m_ModInfo->getURL());
}
void ModInfoDialog::on_tabWidget_currentChanged(int index)
{
if (index == TAB_NEXUS || m_RealTabPos[index] == TAB_NEXUS) {
activateNexusTab();
}
}
void ModInfoDialog::on_modIDEdit_editingFinished()
{
int oldID = m_Settings->value("modid", 0).toInt();
int modID = ui->modIDEdit->text().toInt();
if (oldID != modID){
m_ModInfo->setNexusID(modID);
ui->descriptionView->page()->setHtml("");
if (modID != 0) {
m_RequestStarted = false;
refreshNexusData(modID);
}
}
}
void ModInfoDialog::on_sourceGameEdit_currentIndexChanged(int)
{
for (auto game : m_PluginContainer->plugins()) {
if (game->gameName() == ui->sourceGameEdit->currentText()) {
m_ModInfo->setGameName(game->gameShortName());
return;
}
}
}
void ModInfoDialog::on_versionEdit_editingFinished()
{
VersionInfo version(ui->versionEdit->text());
m_ModInfo->setVersion(version);
updateVersionColor();
}
void ModInfoDialog::on_customUrlLineEdit_editingFinished()
{
m_ModInfo->setURL(ui->customUrlLineEdit->text());
}
bool ModInfoDialog::recursiveDelete(const QModelIndex &index)
{
for (int childRow = 0; childRow < m_FileSystemModel->rowCount(index); ++childRow) {
QModelIndex childIndex = m_FileSystemModel->index(childRow, 0, index);
if (m_FileSystemModel->isDir(childIndex)) {
if (!recursiveDelete(childIndex)) {
qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData());
return false;
}
} else {
if (!m_FileSystemModel->remove(childIndex)) {
qCritical("failed to delete %s", m_FileSystemModel->fileName(childIndex).toUtf8().constData());
return false;
}
}
}
if (!m_FileSystemModel->remove(index)) {
qCritical("failed to delete %s", m_FileSystemModel->fileName(index).toUtf8().constData());
return false;
}
return true;
}
void ModInfoDialog::on_openInExplorerButton_clicked()
{
shell::ExploreFile(m_ModInfo->absolutePath());
}
void ModInfoDialog::deleteFile(const QModelIndex &index)
{
bool res = m_FileSystemModel->isDir(index) ? recursiveDelete(index)
: m_FileSystemModel->remove(index);
if (!res) {
QString fileName = m_FileSystemModel->fileName(index);
reportError(tr("Failed to delete %1").arg(fileName));
}
}
void ModInfoDialog::delete_activated()
{
if (ui->fileTree->hasFocus()) {
QItemSelectionModel *selection = ui->fileTree->selectionModel();
if (selection->hasSelection() && selection->selectedRows().count() >= 1) {
if (selection->selectedRows().count() == 0) {
return;
}
else if (selection->selectedRows().count() == 1) {
QString fileName = m_FileSystemModel->fileName(selection->selectedRows().at(0));
if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete \"%1\"?").arg(fileName),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
return;
}
}
else {
if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete the selected files?"),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
return;
}
}
foreach(QModelIndex index, selection->selectedRows()) {
deleteFile(index);
}
}
}
}
void ModInfoDialog::deleteTriggered()
{
if (m_FileSelection.count() == 0) {
return;
} else if (m_FileSelection.count() == 1) {
QString fileName = m_FileSystemModel->fileName(m_FileSelection.at(0));
if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete \"%1\"?").arg(fileName),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
return;
}
} else {
if (QMessageBox::question(this, tr("Confirm"), tr("Are you sure you want to delete the selected files?"),
QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes) {
return;
}
}
foreach(QModelIndex index, m_FileSelection) {
deleteFile(index);
}
}
void ModInfoDialog::renameTriggered()
{
QModelIndex selection = m_FileSelection.at(0);
QModelIndex index = selection.sibling(selection.row(), 0);
if (!index.isValid() || m_FileSystemModel->isReadOnly()) {
return;
}
ui->fileTree->edit(index);
}
void ModInfoDialog::hideTriggered()
{
changeFiletreeVisibility(false);
}
void ModInfoDialog::unhideTriggered()
{
changeFiletreeVisibility(true);
}
void ModInfoDialog::changeFiletreeVisibility(bool visible)
{
bool changed = false;
bool stop = false;
qDebug().nospace()
<< (visible ? "unhiding" : "hiding") << " "
<< m_FileSelection.size() << " filetree files";
QFlags flags =
(visible ? FileRenamer::UNHIDE : FileRenamer::HIDE);
if (m_FileSelection.size() > 1) {
flags |= FileRenamer::MULTIPLE;
}
FileRenamer renamer(this, flags);
for (const auto& index : m_FileSelection) {
if (stop) {
break;
}
const QString path = m_FileSystemModel->filePath(index);
auto result = FileRenamer::RESULT_CANCEL;
if (visible) {
if (!canUnhideFile(false, path)) {
qDebug().nospace() << "cannot unhide " << path << ", skipping";
continue;
}
result = unhideFile(renamer, path);
} else {
if (!canHideFile(false, path)) {
qDebug().nospace() << "cannot hide " << path << ", skipping";
continue;
}
result = hideFile(renamer, path);
}
switch (result) {
case FileRenamer::RESULT_OK: {
// will trigger a refresh at the end
changed = true;
break;
}
case FileRenamer::RESULT_SKIP: {
// nop
break;
}
case FileRenamer::RESULT_CANCEL: {
// stop right now, but make sure to refresh if needed
stop = true;
break;
}
}
}
qDebug().nospace() << (visible ? "unhiding" : "hiding") << " filetree files done";
if (changed) {
qDebug().nospace() << "triggering refresh";
if (m_Origin) {
emit originModified(m_Origin->getID());
}
refreshLists();
}
}
void ModInfoDialog::openTriggered()
{
if (m_FileSelection.size() == 1) {
const auto index = m_FileSelection.at(0);
if (!index.isValid()) {
return;
}
QString fileName = m_FileSystemModel->filePath(index);
shell::OpenFile(fileName);
}
}
void ModInfoDialog::previewTriggered()
{
if (m_FileSelection.size() == 1) {
const auto index = m_FileSelection.at(0);
if (!index.isValid()) {
return;
}
QString fileName = m_FileSystemModel->filePath(index);
m_OrganizerCore->previewFile(this, m_ModInfo->name(), fileName);
}
}
void ModInfoDialog::createDirectoryTriggered()
{
QModelIndex selection = m_FileSelection.at(0);
QModelIndex index = m_FileSystemModel->isDir(selection) ? selection
: selection.parent();
index = index.sibling(index.row(), 0);
QString name = tr("New Folder");
QString path = m_FileSystemModel->filePath(index).append("/");
QModelIndex existingIndex = m_FileSystemModel->index(path + name);
int suffix = 1;
while (existingIndex.isValid()) {
name = tr("New Folder") + QString::number(suffix++);
existingIndex = m_FileSystemModel->index(path + name);
}
QModelIndex newIndex = m_FileSystemModel->mkdir(index, name);
if (!newIndex.isValid()) {
reportError(tr("Failed to create \"%1\"").arg(name));
return;
}
ui->fileTree->setCurrentIndex(newIndex);
ui->fileTree->edit(newIndex);
}
void ModInfoDialog::on_fileTree_customContextMenuRequested(const QPoint &pos)
{
QItemSelectionModel *selectionModel = ui->fileTree->selectionModel();
m_FileSelection = selectionModel->selectedRows(0);
QMenu menu(ui->fileTree);
menu.addAction(m_NewFolderAction);
if (selectionModel->hasSelection()) {
bool enableOpen = true;
bool enablePreview = true;
bool enableRename = true;
bool enableDelete = true;
bool enableHide = true;
bool enableUnhide = true;
if (m_FileSelection.size() == 1) {
// single selection
// only enable open action if a file is selected
bool hasFiles = false;
foreach(QModelIndex idx, m_FileSelection) {
if (m_FileSystemModel->fileInfo(idx).isFile()) {
hasFiles = true;
break;
}
}
if (!hasFiles) {
enableOpen = false;
enablePreview = false;
}
const QString fileName = m_FileSystemModel->fileName(m_FileSelection.at(0));
if (!canPreviewFile(m_PluginContainer, false, fileName)) {
enablePreview = false;
}
if (!canHideFile(false, fileName)) {
enableHide = false;
}
if (!canUnhideFile(false, fileName)) {
enableUnhide = false;
}
} else {
// this is a multiple selection, don't show open action so users don't open
// a thousand files
enableOpen = false;
enablePreview = false;
enableRename = false;
if (m_FileSelection.size() < max_scan_for_context_menu) {
// if the number of selected items is low, checking them to accurately
// show the menu items is worth it
enableHide = false;
enableUnhide = false;
for (const auto& index : m_FileSelection) {
const QString fileName = m_FileSystemModel->fileName(index);
if (canHideFile(false, fileName)) {
enableHide = true;
}
if (canUnhideFile(false, fileName)) {
enableUnhide = true;
}
if (enableHide && enableUnhide) {
// found both, no need to check more
break;
}
}
}
}
if (enableOpen) {
menu.addAction(m_OpenAction);
}
if (enablePreview) {
menu.addAction(m_PreviewAction);
}
if (enableRename) {
menu.addAction(m_RenameAction);
}
if (enableDelete) {
menu.addAction(m_DeleteAction);
}
if (enableHide) {
menu.addAction(m_HideAction);
}
if (enableUnhide) {
menu.addAction(m_UnhideAction);
}
} else {
m_FileSelection.clear();
m_FileSelection.append(m_FileSystemModel->index(m_FileSystemModel->rootPath(), 0));
}
menu.exec(ui->fileTree->viewport()->mapToGlobal(pos));
}
void ModInfoDialog::on_categoriesTree_itemChanged(QTreeWidgetItem *item, int)
{
QTreeWidgetItem *parent = item->parent();
while ((parent != nullptr) && ((parent->flags() & Qt::ItemIsUserCheckable) != 0) && (parent->checkState(0) == Qt::Unchecked)) {
parent->setCheckState(0, Qt::Checked);
parent = parent->parent();
}
refreshPrimaryCategoriesBox();
}
void ModInfoDialog::addCheckedCategories(QTreeWidgetItem *tree)
{
for (int i = 0; i < tree->childCount(); ++i) {
QTreeWidgetItem *child = tree->child(i);
if (child->checkState(0) == Qt::Checked) {
ui->primaryCategoryBox->addItem(child->text(0), child->data(0, Qt::UserRole));
addCheckedCategories(child);
}
}
}
void ModInfoDialog::refreshPrimaryCategoriesBox()
{
ui->primaryCategoryBox->clear();
int primaryCategory = m_ModInfo->getPrimaryCategory();
addCheckedCategories(ui->categoriesTree->invisibleRootItem());
for (int i = 0; i < ui->primaryCategoryBox->count(); ++i) {
if (ui->primaryCategoryBox->itemData(i).toInt() == primaryCategory) {
ui->primaryCategoryBox->setCurrentIndex(i);
break;
}
}
}
void ModInfoDialog::on_primaryCategoryBox_currentIndexChanged(int index)
{
if (index != -1) {
m_ModInfo->setPrimaryCategory(ui->primaryCategoryBox->itemData(index).toInt());
}
}
void ModInfoDialog::on_overwriteTree_itemDoubleClicked(QTreeWidgetItem *item, int)
{
if (auto* ci=dynamic_cast(item)) {
const auto origin = ci->altOrigin();
if (!origin.isEmpty()) {
close();
emit modOpen(origin, TAB_CONFLICTS);
}
}
}
FileRenamer::RenameResults ModInfoDialog::hideFile(FileRenamer& renamer, const QString &oldName)
{
const QString newName = oldName + ModInfo::s_HiddenExt;
return renamer.rename(oldName, newName);
}
FileRenamer::RenameResults ModInfoDialog::unhideFile(FileRenamer& renamer, const QString &oldName)
{
QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length());
return renamer.rename(oldName, newName);
}
void ModInfoDialog::changeConflictItemsVisibility(
const QList& items, bool visible)
{
bool changed = false;
bool stop = false;
qDebug().nospace()
<< (visible ? "unhiding" : "hiding") << " "
<< items.size() << " conflict files";
QFlags flags =
(visible ? FileRenamer::UNHIDE : FileRenamer::HIDE);
if (items.size() > 1) {
flags |= FileRenamer::MULTIPLE;
}
FileRenamer renamer(this, flags);
for (const auto* item : items) {
if (stop) {
break;
}
const auto* ci = dynamic_cast(item);
if (!ci) {
continue;
}
auto result = FileRenamer::RESULT_CANCEL;
if (visible) {
if (!ci->canUnhide()) {
qDebug().nospace() << "cannot unhide " << item->text(0) << ", skipping";
continue;
}
result = unhideFile(renamer, ci->fileName());
} else {
if (!ci->canHide()) {
qDebug().nospace() << "cannot hide " << item->text(0) << ", skipping";
continue;
}
result = hideFile(renamer, ci->fileName());
}
switch (result) {
case FileRenamer::RESULT_OK: {
// will trigger a refresh at the end
changed = true;
break;
}
case FileRenamer::RESULT_SKIP: {
// nop
break;
}
case FileRenamer::RESULT_CANCEL: {
// stop right now, but make sure to refresh if needed
stop = true;
break;
}
}
}
qDebug().nospace() << (visible ? "unhiding" : "hiding") << " conflict files done";
if (changed) {
qDebug().nospace() << "triggering refresh";
if (m_Origin) {
emit originModified(m_Origin->getID());
}
refreshLists();
}
}
void ModInfoDialog::openConflictItems(const QList& items)
{
// the menu item is only shown for a single selection, but handle all of them
// in case this changes
for (auto* item : items) {
if (auto* ci=dynamic_cast(item)) {
m_OrganizerCore->executeFileVirtualized(this, ci->fileName());
}
}
}
void ModInfoDialog::previewConflictItems(const QList& items)
{
// the menu item is only shown for a single selection, but handle all of them
// in case this changes
for (auto* item : items) {
if (auto* ci=dynamic_cast(item)) {
m_OrganizerCore->previewFileWithAlternatives(this, ci->fileName());
}
}
}
void ModInfoDialog::on_overwriteTree_customContextMenuRequested(const QPoint &pos)
{
showConflictMenu(pos, ui->overwriteTree);
}
void ModInfoDialog::on_overwrittenTree_customContextMenuRequested(const QPoint &pos)
{
showConflictMenu(pos, ui->overwrittenTree);
}
void ModInfoDialog::on_noConflictTree_customContextMenuRequested(const QPoint &pos)
{
showConflictMenu(pos, ui->noConflictTree);
}
void ModInfoDialog::on_conflictsAdvancedList_customContextMenuRequested(const QPoint &pos)
{
showConflictMenu(pos, ui->conflictsAdvancedList);
}
void ModInfoDialog::showConflictMenu(const QPoint &pos, QTreeWidget* tree)
{
auto actions = createConflictMenuActions(tree->selectedItems());
QMenu menu;
// open
if (actions.open) {
connect(actions.open, &QAction::triggered, [&]{
openConflictItems(tree->selectedItems());
});
menu.addAction(actions.open);
}
// preview
if (actions.preview) {
connect(actions.preview, &QAction::triggered, [&]{
previewConflictItems(tree->selectedItems());
});
menu.addAction(actions.preview);
}
// hide
if (actions.hide) {
connect(actions.hide, &QAction::triggered, [&]{
changeConflictItemsVisibility(tree->selectedItems(), false);
});
menu.addAction(actions.hide);
}
// unhide
if (actions.unhide) {
connect(actions.unhide, &QAction::triggered, [&]{
changeConflictItemsVisibility(tree->selectedItems(), true);
});
menu.addAction(actions.unhide);
}
// goto
if (actions.gotoMenu) {
menu.addMenu(actions.gotoMenu);
for (auto* a : actions.gotoActions) {
connect(a, &QAction::triggered, [&, name=a->text()]{
close();
emit modOpen(name, TAB_CONFLICTS);
});
actions.gotoMenu->addAction(a);
}
}
if (!menu.isEmpty()) {
menu.exec(tree->viewport()->mapToGlobal(pos));
}
}
ModInfoDialog::ConflictActions ModInfoDialog::createConflictMenuActions(
const QList& selection)
{
if (selection.empty()) {
return {};
}
bool enableHide = true;
bool enableUnhide = true;
bool enableOpen = true;
bool enablePreview = true;
bool enableGoto = true;
if (selection.size() == 1) {
// this is a single selection
const auto* ci = dynamic_cast(selection[0]);
if (!ci) {
return {};
}
enableHide = ci->canHide();
enableUnhide = ci->canUnhide();
enableOpen = ci->canOpen();
enablePreview = ci->canPreview(m_PluginContainer);
enableGoto = ci->hasAlts();
}
else {
// this is a multiple selection, don't show open/preview so users don't open
// a thousand files
enableOpen = false;
enablePreview = false;
// don't bother with this on multiple selection, at least for now
enableGoto = false;
if (selection.size() < max_scan_for_context_menu) {
// if the number of selected items is low, checking them to accurately
// show the menu items is worth it
enableHide = false;
enableUnhide = false;
for (const auto* item : selection) {
if (const auto* ci=dynamic_cast(item)) {
if (ci->canHide()) {
enableHide = true;
}
if (ci->canUnhide()) {
enableUnhide = true;
}
if (enableHide && enableUnhide && enableGoto) {
// found all, no need to check more
break;
}
}
}
}
}
ConflictActions actions;
actions.hide = new QAction(tr("Hide"), this);
actions.hide->setEnabled(enableHide);
// note that it is possible for hidden files to appear if they override other
// hidden files from another mod
actions.unhide = new QAction(tr("Unhide"), this);
actions.unhide->setEnabled(enableUnhide);
actions.open = new QAction(tr("Open/Execute"), this);
actions.open->setEnabled(enableOpen);
actions.preview = new QAction(tr("Preview"), this);
actions.preview->setEnabled(enablePreview);
actions.gotoMenu = new QMenu(tr("Go to..."), this);
actions.gotoMenu->setEnabled(enableGoto);
if (enableGoto) {
actions.gotoActions = createGotoActions(selection);
}
return actions;
}
std::vector ModInfoDialog::createGotoActions(const QList& selection)
{
if (!m_Origin || selection.size() != 1) {
return {};
}
const auto* item = dynamic_cast(selection[0]);
if (!item) {
return {};
}
auto file = m_Origin->findFile(item->fileIndex());
if (!file) {
return {};
}
std::vector mods;
// add all alternatives
for (const auto& alt : file->getAlternatives()) {
const auto& o = m_Directory->getOriginByID(alt.first);
if (o.getID() != m_Origin->getID()) {
mods.push_back(ToQString(o.getName()));
}
}
// add the real origin if different from this mod
const FilesOrigin& realOrigin = m_Directory->getOriginByID(file->getOrigin());
if (realOrigin.getID() != m_Origin->getID()) {
mods.push_back(ToQString(realOrigin.getName()));
}
std::sort(mods.begin(), mods.end(), [](const auto& a, const auto& b) {
return (QString::localeAwareCompare(a, b) < 0);
});
std::vector actions;
for (const auto& name : mods) {
actions.push_back(new QAction(name, this));
}
return actions;
}
void ModInfoDialog::on_overwrittenTree_itemDoubleClicked(QTreeWidgetItem *item, int)
{
if (const auto* ci=dynamic_cast(item)) {
const auto origin = ci->altOrigin();
if (!origin.isEmpty()) {
close();
emit modOpen(origin, TAB_CONFLICTS);
}
}
}
void ModInfoDialog::on_refreshButton_clicked()
{
if (m_ModInfo->getNexusID() > 0) {
QLineEdit *modIDEdit = findChild("modIDEdit");
int modID = modIDEdit->text().toInt();
refreshNexusData(modID);
} else
qInfo("Mod has no valid Nexus ID, info can't be updated.");
}
void ModInfoDialog::on_endorseBtn_clicked()
{
emit endorseMod(m_ModInfo);
}
void ModInfoDialog::on_nextButton_clicked()
{
int currentTab = ui->tabWidget->currentIndex();
int tab = m_RealTabPos[currentTab];
emit modOpenNext(tab);
this->accept();
}
void ModInfoDialog::on_prevButton_clicked()
{
int currentTab = ui->tabWidget->currentIndex();
int tab = m_RealTabPos[currentTab];
emit modOpenPrev(tab);
this->accept();
}