/*
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 "modinfodialogesps.h"
#include "modinfodialogconflicts.h"
#include "modinfodialogcategories.h"
#include "modinfodialognexus.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace MOBase;
using namespace MOShared;
const int max_scan_for_context_menu = 50;
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;
}
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;
}
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);
}
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_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 = createTabs();
for (std::size_t i=0; i(i));
});
}
this->setWindowTitle(modInfo->name());
this->setWindowModality(Qt::WindowModal);
m_RootPath = modInfo->absolutePath();
ui->commentsEdit->setText(modInfo->comments());
ui->notesEdit->setText(modInfo->notes());
//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;
}
}
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, true);
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_ESPS, false);
ui->tabWidget->setTabEnabled(TAB_CONFLICTS, true);
ui->tabWidget->setTabEnabled(TAB_CATEGORIES, false);
ui->tabWidget->setTabEnabled(TAB_NEXUS, false);
ui->tabWidget->setTabEnabled(TAB_FILETREE, false);
ui->tabWidget->setTabEnabled(TAB_NOTES, false);
} else {
ui->tabWidget->setTabEnabled(TAB_TEXTFILES, true);
ui->tabWidget->setTabEnabled(TAB_INIFILES, true);
ui->tabWidget->setTabEnabled(TAB_IMAGES, true);
ui->tabWidget->setTabEnabled(TAB_ESPS, true);
ui->tabWidget->setTabEnabled(TAB_CONFLICTS, true);
ui->tabWidget->setTabEnabled(TAB_CATEGORIES, true);
ui->tabWidget->setTabEnabled(TAB_NEXUS, true);
initFiletree(modInfo);
}
// activate first enabled tab
for (int i = 0; i < ui->tabWidget->count(); ++i) {
if (ui->tabWidget->isTabEnabled(i)) {
ui->tabWidget->setCurrentIndex(i);
break;
}
}
for (auto& tab : m_tabs) {
tab->setMod(m_ModInfo, m_Origin);
}
}
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());
delete ui;
}
template
std::vector> createTabsImpl(
OrganizerCore& oc, PluginContainer& plugin,
ModInfoDialog* self, Ui::ModInfoDialog* ui)
{
std::vector> v;
(v.push_back(std::make_unique(oc, plugin, self, ui)), ...);
return v;
}
std::vector> ModInfoDialog::createTabs()
{
return createTabsImpl<
TextFilesTab, IniFilesTab, ImagesTab, ESPsTab,
ConflictsTab, CategoriesTab, NexusTab>(
*m_OrganizerCore, *m_PluginContainer, this, ui);
}
int ModInfoDialog::exec()
{
refreshLists();
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());
for (const auto& tab : m_tabs) {
tab->saveState(s);
}
}
void ModInfoDialog::restoreState(const Settings& s)
{
restoreTabState(s.directInterface().value("mod_info_tabs").toByteArray());
for (const auto& tab : m_tabs) {
tab->restoreState(s);
}
}
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);
}
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;
}
void ModInfoDialog::refreshLists()
{
for (auto& tab : m_tabs) {
tab->update();
}
refreshFiles();
}
void ModInfoDialog::refreshFiles()
{
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;
}
}
}
}
}
void ModInfoDialog::on_closeButton_clicked()
{
for (auto& tab : m_tabs) {
if (!tab->canClose()) {
return;
}
}
close();
}
void ModInfoDialog::openTab(int tab)
{
QTabWidget *tabWidget = findChild("tabWidget");
if (tabWidget->isTabEnabled(tab)) {
tabWidget->setCurrentIndex(tab);
}
}
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::on_tabWidget_currentChanged(int index)
{
}
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_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();
}