/*
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 "modinfodialogcategories.h"
#include "modinfodialogconflicts.h"
#include "modinfodialogesps.h"
#include "modinfodialogfiletree.h"
#include "modinfodialogimages.h"
#include "modinfodialognexus.h"
#include "modinfodialogtextfiles.h"
#include "modlistview.h"
#include "organizercore.h"
#include "plugincontainer.h"
#include "shared/directoryentry.h"
#include "shared/filesorigin.h"
#include "ui_modinfodialog.h"
#include
using namespace MOBase;
using namespace MOShared;
namespace fs = std::filesystem;
const int max_scan_for_context_menu = 50;
bool canPreviewFile(const PluginContainer& pluginContainer, bool isArchive,
const QString& filename)
{
const auto ext = QFileInfo(filename).suffix().toLower();
return pluginContainer.previewGenerator().previewSupported(ext, isArchive);
}
bool isExecutableFilename(const QString& filename)
{
static const std::set exeExtensions = {"exe", "cmd", "bat"};
const auto ext = QFileInfo(filename).suffix().toLower();
return exeExtensions.contains(ext);
}
bool canRunFile(bool isArchive, const QString& filename)
{
// can run executables that are not archives
return !isArchive && isExecutableFilename(filename);
}
bool canOpenFile(bool isArchive, const QString& filename)
{
// can open non-executables that are not archives
return !isArchive && !isExecutableFilename(filename);
}
bool canExploreFile(bool isArchive, const QString&)
{
// can explore 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, Qt::CaseInsensitive)) {
// 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, Qt::CaseInsensitive)) {
// already visible
return false;
}
return true;
}
FileRenamer::RenameResults hideFile(FileRenamer& renamer, const QString& oldName)
{
const QString newName = oldName + ModInfo::s_HiddenExt;
return renamer.rename(oldName, newName);
}
FileRenamer::RenameResults unhideFile(FileRenamer& renamer, const QString& oldName)
{
QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length());
return renamer.rename(oldName, newName);
}
FileRenamer::RenameResults restoreHiddenFilesRecursive(FileRenamer& renamer,
const QString& targetDir)
{
FileRenamer::RenameResults results = FileRenamer::RESULT_OK;
QDir currentDir = targetDir;
for (QString hiddenFile :
currentDir.entryList((QStringList() << "*" + ModInfo::s_HiddenExt),
QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot)) {
QString oldName = currentDir.absoluteFilePath(hiddenFile);
QString newName = oldName.left(oldName.length() - ModInfo::s_HiddenExt.length());
auto partialResult = renamer.rename(oldName, newName);
if (partialResult == FileRenamer::RESULT_CANCEL) {
return FileRenamer::RESULT_CANCEL;
}
if (partialResult == FileRenamer::RESULT_SKIP) {
results = FileRenamer::RESULT_SKIP;
}
}
for (QString dirName : currentDir.entryList(QDir::AllDirs | QDir::NoDotAndDotDot)) {
const QString dirPath = currentDir.absoluteFilePath(dirName);
// recurse on childrend directories
auto partialResult = restoreHiddenFilesRecursive(renamer, dirPath);
if (partialResult == FileRenamer::RESULT_CANCEL) {
return FileRenamer::RESULT_CANCEL;
}
if (partialResult == FileRenamer::RESULT_SKIP) {
results = FileRenamer::RESULT_SKIP;
}
}
return results;
}
ModInfoDialog::TabInfo::TabInfo(std::unique_ptr tab)
: tab(std::move(tab)), realPos(-1), widget(nullptr)
{}
bool ModInfoDialog::TabInfo::isVisible() const
{
return (realPos != -1);
}
ModInfoDialog::ModInfoDialog(OrganizerCore& core, PluginContainer& plugin,
ModInfo::Ptr mod, ModListView* modListView,
QWidget* parent)
: TutorableDialog("ModInfoDialog", parent), ui(new Ui::ModInfoDialog), m_core(core),
m_plugin(plugin), m_modListView(modListView), m_initialTab(ModInfoTabIDs::None),
m_arrangingTabs(false)
{
ui->setupUi(this);
{
auto* sc = new QShortcut(QKeySequence::Delete, this);
connect(sc, &QShortcut::activated, [&] {
onDeleteShortcut();
});
}
{
auto* sc = new QShortcut(QKeySequence::MoveToNextPage, this);
connect(sc, &QShortcut::activated, [&] {
onNextMod();
});
}
{
auto* sc = new QShortcut(QKeySequence::MoveToPreviousPage, this);
connect(sc, &QShortcut::activated, [&] {
onPreviousMod();
});
}
setMod(mod);
createTabs();
connect(ui->tabWidget, &QTabWidget::currentChanged, [&] {
onTabSelectionChanged();
});
connect(ui->tabWidget->tabBar(), &QTabBar::tabMoved, [&] {
onTabMoved();
});
connect(ui->close, &QPushButton::clicked, [&] {
onCloseButton();
});
connect(ui->previousMod, &QPushButton::clicked, [&] {
onPreviousMod();
});
connect(ui->nextMod, &QPushButton::clicked, [&] {
onNextMod();
});
}
ModInfoDialog::~ModInfoDialog() = default;
template
std::unique_ptr createTab(ModInfoDialog& d, ModInfoTabIDs id)
{
return std::make_unique(ModInfoDialogTabContext(
d.m_core, d.m_plugin, &d, d.ui.get(), id, d.m_mod, d.getOrigin()));
}
void ModInfoDialog::createTabs()
{
m_tabs.clear();
m_tabs.push_back(createTab(*this, ModInfoTabIDs::TextFiles));
m_tabs.push_back(createTab(*this, ModInfoTabIDs::IniFiles));
m_tabs.push_back(createTab(*this, ModInfoTabIDs::Images));
m_tabs.push_back(createTab(*this, ModInfoTabIDs::Esps));
m_tabs.push_back(createTab(*this, ModInfoTabIDs::Conflicts));
m_tabs.push_back(createTab(*this, ModInfoTabIDs::Categories));
m_tabs.push_back(createTab(*this, ModInfoTabIDs::Nexus));
m_tabs.push_back(createTab(*this, ModInfoTabIDs::Notes));
m_tabs.push_back(createTab(*this, ModInfoTabIDs::Filetree));
// check for tabs in the ui not having a corresponding tab in the list
int count = ui->tabWidget->count();
if (count < 0 || count > static_cast(m_tabs.size())) {
log::error("mod info dialog has more tabs than expected");
count = static_cast(m_tabs.size());
}
// for each tab in the widget; connects the widgets with the tab objects
//
for (int i = 0; i < count; ++i) {
auto& tabInfo = m_tabs[static_cast(i)];
// remembering tab information so tabs can be removed and re-added
tabInfo.widget = ui->tabWidget->widget(i);
tabInfo.caption = ui->tabWidget->tabText(i);
tabInfo.icon = ui->tabWidget->tabIcon(i);
connect(tabInfo.tab.get(), &ModInfoDialogTab::originModified, [this](int originID) {
onOriginModified(originID);
});
connect(tabInfo.tab.get(), &ModInfoDialogTab::modOpen, [&](const QString& name) {
setMod(name);
update();
});
connect(tabInfo.tab.get(), &ModInfoDialogTab::hasDataChanged, [&] {
setTabsColors();
});
connect(tabInfo.tab.get(), &ModInfoDialogTab::wantsFocus,
[&, id = tabInfo.tab->tabID()] {
switchToTab(id);
});
}
}
int ModInfoDialog::exec()
{
GeometrySaver gs(Settings::instance(), this);
restoreState();
// whether to select the first tab; if the main window requested a specific
// tab, it is selected when encountered in update()
const auto noCustomTabRequested = (m_initialTab == ModInfoTabIDs::None);
const auto requestedTab = m_initialTab;
update(true);
if (noCustomTabRequested) {
m_core.settings().widgets().restoreIndex(ui->tabWidget);
}
const int r = TutorableDialog::exec();
saveState();
return r;
}
void ModInfoDialog::setMod(ModInfo::Ptr mod)
{
Q_ASSERT(mod);
m_mod = mod;
// resetting the first activation flag so selecting tabs will trigger it
// again
for (auto& tabInfo : m_tabs) {
tabInfo.tab->resetFirstActivation();
}
}
void ModInfoDialog::setMod(const QString& name)
{
unsigned int index = ModInfo::getIndex(name);
if (index == UINT_MAX) {
log::error("failed to resolve mod name {}", name);
return;
}
auto mod = ModInfo::getByIndex(index);
if (!mod) {
log::error("mod by index {} is null", index);
return;
}
setMod(mod);
}
void ModInfoDialog::selectTab(ModInfoTabIDs id)
{
if (!isVisible()) {
// can't select a tab if the dialog hasn't been properly updated yet
m_initialTab = id;
return;
}
switchToTab(id);
}
ModInfoDialog::TabInfo* ModInfoDialog::currentTab()
{
const auto index = ui->tabWidget->currentIndex();
if (index < 0) {
return nullptr;
}
// looking for the actual tab at that position
for (auto& tabInfo : m_tabs) {
if (tabInfo.realPos == index) {
return &tabInfo;
}
}
return nullptr;
}
void ModInfoDialog::update(bool firstTime)
{
// remembering the current selection, will be restored if the tab still
// exists
const int oldTab = ui->tabWidget->currentIndex();
setWindowTitle(m_mod->name());
// rebuilding the tab widget if needed depending on what tabs are valid for
// the current mod
setTabsVisibility(firstTime);
// updating the data in all tabs
updateTabs();
// switching to the initial tab, if any
if (m_initialTab != ModInfoTabIDs::None) {
switchToTab(m_initialTab);
m_initialTab = ModInfoTabIDs::None;
}
if (ui->tabWidget->currentIndex() == oldTab) {
if (auto* tabInfo = currentTab()) {
// activated() has to be fired manually because the tab index hasn't been
// changed
tabInfo->tab->activated();
} else {
log::error("tab index {} not found", oldTab);
}
}
}
void ModInfoDialog::setTabsVisibility(bool firstTime)
{
// this flag is picked up by onTabSelectionChanged() to avoid triggering
// activation events while moving tabs around
QScopedValueRollback arrangingTabs(m_arrangingTabs, true);
// one bool per tab to indicate whether the tab should be visible
std::vector visibility(m_tabs.size());
bool changed = false;
for (std::size_t i = 0; i < m_tabs.size(); ++i) {
const auto& tabInfo = m_tabs[i];
bool visible = true;
// a tab is visible if it can handle the current mod
if (m_mod->hasFlag(ModInfo::FLAG_FOREIGN)) {
visible = tabInfo.tab->canHandleUnmanaged();
} else if (m_mod->hasFlag(ModInfo::FLAG_SEPARATOR)) {
visible = tabInfo.tab->canHandleSeparators();
}
// if the visibility of this tab is changing, set changed to true because
// the tabs have to be rebuilt
const auto currentlyVisible = (ui->tabWidget->indexOf(tabInfo.widget) != -1);
if (visible != currentlyVisible) {
changed = true;
}
visibility[i] = visible;
}
// the tabs have to be rebuilt the first time the dialog is shown, or when
// the visibility of any tab has changed
if (!firstTime && !changed) {
return;
}
// save the current order (if necessary) because some tabs will be removed and
// others added
if (!firstTime) {
// but don't do it the first time visibility is set because the tabs are
// in the default order, which will clobber the current settings
saveTabOrder();
}
// remember selection, if any
auto sel = ModInfoTabIDs::None;
if (const auto* tabInfo = currentTab()) {
sel = tabInfo->tab->tabID();
}
// removes all tabs and re-adds the visible ones
reAddTabs(visibility, sel);
}
void ModInfoDialog::reAddTabs(const std::vector& visibility, ModInfoTabIDs sel)
{
Q_ASSERT(visibility.size() == m_tabs.size());
// ordered tab names from settings
const auto orderedNames = m_core.settings().geometry().modInfoTabOrder();
// whether the tabs can be sorted
//
// if the object name of a tab widget is not found in orderedNames, the list
// cannot be sorted safely; if the list is empty, it's probably a first run
// and there's nothing to sort
bool canSort = !orderedNames.empty();
// gathering visible tabs
std::vector visibleTabs;
for (std::size_t i = 0; i < m_tabs.size(); ++i) {
if (!visibility[i]) {
// this tab is not visible, skip it
continue;
}
// this tab is visible
visibleTabs.push_back(&m_tabs[i]);
if (canSort) {
// make sure the widget object name is found in the list
const auto objectName = m_tabs[i].widget->objectName();
auto itor = std::find(orderedNames.begin(), orderedNames.end(), objectName);
if (itor == orderedNames.end()) {
// this shouldn't happen, it means there's a tab in the UI that's no
// in the list
log::error("can't sort tabs, '{}' not found", objectName);
canSort = false;
}
}
}
// sorting tabs
if (canSort) {
std::sort(visibleTabs.begin(), visibleTabs.end(), [&](auto&& a, auto&& b) {
// looking the names in the ordered list
auto aItor =
std::find(orderedNames.begin(), orderedNames.end(), a->widget->objectName());
auto bItor =
std::find(orderedNames.begin(), orderedNames.end(), b->widget->objectName());
// this shouldn't happen, it was checked above
Q_ASSERT(aItor != orderedNames.end() && bItor != orderedNames.end());
return (aItor < bItor);
});
}
// removing all tabs
ui->tabWidget->clear();
// reset real positions
for (auto& tabInfo : m_tabs) {
tabInfo.realPos = -1;
}
// add visible tabs
for (std::size_t i = 0; i < visibleTabs.size(); ++i) {
auto& tabInfo = *visibleTabs[i];
// remembering real position
tabInfo.realPos = static_cast(i);
// adding tab
ui->tabWidget->addTab(tabInfo.widget, tabInfo.icon, tabInfo.caption);
// selecting
if (tabInfo.tab->tabID() == sel) {
ui->tabWidget->setCurrentIndex(static_cast(i));
}
}
}
void ModInfoDialog::updateTabs(bool becauseOriginChanged)
{
auto* origin = getOrigin();
// list of tabs that should be updated
std::vector interestedTabs;
for (auto& tabInfo : m_tabs) {
// don't touch invisible tabs
if (!tabInfo.isVisible()) {
continue;
}
// updateTabs() is also called from onOriginModified() to update all the
// tabs that depend on the origin; if updateTabs() is called because the
// origin changed, but the tab doesn't use origin files, it can be safely
// skipped
//
// this happens for tabs like notes and categories, which don't need to
// be updated when files change
if (becauseOriginChanged && !tabInfo.tab->usesOriginFiles()) {
continue;
}
// this tab should be updated
interestedTabs.push_back(&tabInfo);
}
for (auto* tabInfo : interestedTabs) {
// set the current mod
tabInfo->tab->setMod(m_mod, origin);
// clear
tabInfo->tab->clear();
}
// feed all the files from the filesystem
feedFiles(interestedTabs);
// call update() on all tabs
for (auto* tabInfo : interestedTabs) {
tabInfo->tab->update();
}
// update the text colours
setTabsColors();
}
void ModInfoDialog::feedFiles(std::vector& interestedTabs)
{
const auto rootPath =
m_mod->absolutePath() + (m_core.managedGame()->modDataDirectory().isEmpty()
? ""
: "/" + m_core.managedGame()->modDataDirectory());
if (rootPath.isEmpty()) {
return;
}
if (fs::exists(rootPath.toStdWString())) {
const fs::path fsPath(rootPath.toStdWString());
for (const auto& entry : fs::recursive_directory_iterator(fsPath)) {
if (!entry.is_regular_file()) {
// skip directories
continue;
}
const auto filePath = QString::fromStdWString(entry.path().native());
// for each tab
for (auto* tabInfo : interestedTabs) {
if (tabInfo->tab->feedFile(rootPath, filePath)) {
break;
}
}
}
}
}
void ModInfoDialog::setTabsColors()
{
const auto p = m_modListView->parentWidget()->palette();
for (const auto& tabInfo : m_tabs) {
if (!tabInfo.isVisible()) {
// don't bother with invisible tabs
continue;
}
const QColor color = tabInfo.tab->hasData()
? QColor::Invalid
: p.color(QPalette::Disabled, QPalette::WindowText);
ui->tabWidget->tabBar()->setTabTextColor(tabInfo.realPos, color);
}
}
void ModInfoDialog::switchToTab(ModInfoTabIDs id)
{
// look a tab with the given id
for (const auto& tabInfo : m_tabs) {
if (tabInfo.tab->tabID() == id) {
// use realPos to select the proper tab in the widget
ui->tabWidget->setCurrentIndex(tabInfo.realPos);
return;
}
}
// this could happen if the tab is not visible right now
log::debug("can't switch to tab ID {}, not available", static_cast(id));
}
MOShared::FilesOrigin* ModInfoDialog::getOrigin()
{
auto* ds = m_core.directoryStructure();
if (!ds->originExists(m_mod->name().toStdWString())) {
return nullptr;
}
auto* origin = &ds->getOriginByName(m_mod->name().toStdWString());
if (origin->isDisabled()) {
return nullptr;
}
return origin;
}
void ModInfoDialog::saveState() const
{
saveTabOrder();
// save state for each tab
for (const auto& tabInfo : m_tabs) {
tabInfo.tab->saveState(m_core.settings());
}
}
void ModInfoDialog::restoreState()
{
// tab order is not restored here, it will be picked up if tabs have to be
// removed and re-added
// restore state for each tab
for (const auto& tabInfo : m_tabs) {
tabInfo.tab->restoreState(m_core.settings());
}
}
void ModInfoDialog::saveTabOrder() const
{
if (static_cast(m_tabs.size()) != ui->tabWidget->count()) {
// only save tab state when all tabs are visible
//
// if not all tabs are visible, it becomes very difficult to figure out in
// what order the user wants these tabs to be, so just avoid saving it
// completely
//
// this means that reordering tabs when not all tabs are visible is not
// saved, but it's better than breaking everything
return;
}
QString names;
for (int i = 0; i < ui->tabWidget->count(); ++i) {
if (!names.isEmpty()) {
names += " ";
}
names += ui->tabWidget->widget(i)->objectName();
}
m_core.settings().geometry().setModInfoTabOrder(names);
// save last opened index
m_core.settings().widgets().saveIndex(ui->tabWidget);
}
void ModInfoDialog::onOriginModified(int originID)
{
// tell the main window the origin changed
emit originModified(originID);
// update tabs that depend on the origin
updateTabs(true);
}
void ModInfoDialog::onDeleteShortcut()
{
// forward the request to the current tab
if (auto* tabInfo = currentTab()) {
tabInfo->tab->deleteRequested();
}
}
void ModInfoDialog::closeEvent(QCloseEvent* e)
{
if (tryClose()) {
e->accept();
} else {
e->ignore();
}
}
void ModInfoDialog::onCloseButton()
{
if (tryClose()) {
close();
}
}
bool ModInfoDialog::tryClose()
{
// cancel the close if any tab returns false; for example. this can happen if
// a tab has unsaved content, pops a confirmation dialog, and the user clicks
// cancel
for (auto& tabInfo : m_tabs) {
if (!tabInfo.tab->canClose()) {
return false;
}
}
return true;
}
void ModInfoDialog::onTabSelectionChanged()
{
if (m_arrangingTabs) {
// this can be fired while re-arranging tabs, which happens before mods
// are given to tabs, and might trigger first activation, which breaks all
// sorts of things
return;
}
// this will call firstActivation() on the tab if needed
if (auto* tabInfo = currentTab()) {
tabInfo->tab->activated();
}
}
void ModInfoDialog::onTabMoved()
{
// reset
for (auto& tabInfo : m_tabs) {
tabInfo.realPos = -1;
}
// for each tab in the widget
for (int i = 0; i < ui->tabWidget->count(); ++i) {
const auto* w = ui->tabWidget->widget(i);
bool found = false;
// find the corresponding tab info
for (auto& tabInfo : m_tabs) {
if (tabInfo.widget == w) {
tabInfo.realPos = i;
found = true;
break;
}
}
if (!found) {
log::error("unknown tab at index {}", i);
}
}
}
void ModInfoDialog::onNextMod()
{
auto index = m_modListView->nextMod(ModInfo::getIndex(m_mod->name()));
if (!index) {
return;
}
auto mod = ModInfo::getByIndex(*index);
if (!mod || mod == m_mod) {
return;
}
setMod(mod);
update();
emit modChanged(*index);
}
void ModInfoDialog::onPreviousMod()
{
auto index = m_modListView->prevMod(ModInfo::getIndex(m_mod->name()));
if (!index) {
return;
}
auto mod = ModInfo::getByIndex(*index);
setMod(mod);
update();
emit modChanged(*index);
}