1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-01-16 18:29:55 +00:00
openmw/apps/launcher/datafilespage.cpp
AnyOldName3 c2b383ea92 Store original representation of paths in content lists
Also compare against existing content lists in a more forgiving way.

The first improvement makes it possible to use relative paths in openmw.cfg without the launcher canonicalising them.
This was really annoying if you used a relative path on purpose.
It also stops the launcher converting all paths to Qt's convention, where forward slashes are used on Windows even though they're not native.
The engine doesn't care, so you could always put either in the config file, but the launcher wouldn't stand for that, and would make them match.

To make this work, we need to store a path's originalRepresentation in the content list, compare paths loaded from openmw.cfg based on their originalRepresentation, and convert paths from originalRepresentation to absolute value when loading them from a content list.

The second improvement means that paths that are equivalent, but expressed differently (e.g. mismatched case on Windows, mismatched separators on Windows, or mild differences like unnecessary `./`es and doubled separators) don't trigger the creation of a new effectively-identical content list.

To make this work, we had to switch the comparison to lexicaly normalise the path first.
It could only be lexical normalisation as originalRepresentation might be absolute, relative, or absolute-but-based-on-a-path-slug, and we didn't want slugs to break things or relative paths to count as equivalent to absolute ones that refer to the same file.
The comparison is case-insensitive on Windows, and case-sensitive elsewhere.
This isn't strictly right, as you can have case-sensitive things mounted on Windows or tell a Linux directory to be case-insensitive, but we can't tell when that might happen based on a lexical path as it depends on real directory properties (and might differ for different parts of the path, which is too much hassle to support).
2024-10-25 00:49:59 +01:00

1064 lines
38 KiB
C++

#include "datafilespage.hpp"
#include "maindialog.hpp"
#include <QDebug>
#include <QFileDialog>
#include <QList>
#include <QMessageBox>
#include <QPair>
#include <QPushButton>
#include <algorithm>
#include <mutex>
#include <thread>
#include <unordered_set>
#include <apps/launcher/utils/cellnameloader.hpp>
#include <components/files/configurationmanager.hpp>
#include <components/contentselector/model/esmfile.hpp>
#include <components/contentselector/view/contentselector.hpp>
#include <components/config/gamesettings.hpp>
#include <components/config/launchersettings.hpp>
#include <components/bsa/compressedbsafile.hpp>
#include <components/debug/debuglog.hpp>
#include <components/files/qtconversion.hpp>
#include <components/misc/strings/conversion.hpp>
#include <components/navmeshtool/protocol.hpp>
#include <components/settings/values.hpp>
#include <components/vfs/bsaarchive.hpp>
#include <components/vfs/qtconversion.hpp>
#include "utils/profilescombobox.hpp"
#include "utils/textinputdialog.hpp"
#include "ui_directorypicker.h"
const char* Launcher::DataFilesPage::mDefaultContentListName = "Default";
namespace
{
void contentSubdirs(const QString& path, QStringList& dirs)
{
static const QStringList fileFilter{
"*.esm",
"*.esp",
"*.bsa",
"*.ba2",
"*.omwgame",
"*.omwaddon",
"*.omwscripts",
};
static const QStringList dirFilter{
"animations",
"bookart",
"fonts",
"icons",
"interface",
"l10n",
"meshes",
"music",
"mygui",
"scripts",
"shaders",
"sound",
"splash",
"strings",
"textures",
"trees",
"video",
};
QDir currentDir(path);
if (!currentDir.entryInfoList(fileFilter, QDir::Files).empty()
|| !currentDir.entryInfoList(dirFilter, QDir::Dirs | QDir::NoDotAndDotDot).empty())
{
dirs.push_back(currentDir.canonicalPath());
return;
}
for (const auto& subdir : currentDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot))
contentSubdirs(subdir.canonicalFilePath(), dirs);
}
QList<QPair<int, QListWidgetItem*>> sortedSelectedItems(QListWidget* list, bool reverse = false)
{
QList<QPair<int, QListWidgetItem*>> sortedItems;
for (QListWidgetItem* item : list->selectedItems())
sortedItems.append(qMakePair(list->row(item), item));
if (reverse)
std::sort(sortedItems.begin(), sortedItems.end(), [](auto a, auto b) { return a.first > b.first; });
else
std::sort(sortedItems.begin(), sortedItems.end(), [](auto a, auto b) { return a.first < b.first; });
return sortedItems;
}
}
namespace Launcher
{
namespace
{
struct HandleNavMeshToolMessage
{
int mCellsCount;
int mExpectedMaxProgress;
int mMaxProgress;
int mProgress;
HandleNavMeshToolMessage operator()(NavMeshTool::ExpectedCells&& message) const
{
return HandleNavMeshToolMessage{ static_cast<int>(message.mCount), mExpectedMaxProgress,
static_cast<int>(message.mCount) * 100, mProgress };
}
HandleNavMeshToolMessage operator()(NavMeshTool::ProcessedCells&& message) const
{
return HandleNavMeshToolMessage{ mCellsCount, mExpectedMaxProgress, mMaxProgress,
std::max(mProgress, static_cast<int>(message.mCount)) };
}
HandleNavMeshToolMessage operator()(NavMeshTool::ExpectedTiles&& message) const
{
const int expectedMaxProgress = mCellsCount + static_cast<int>(message.mCount);
return HandleNavMeshToolMessage{ mCellsCount, expectedMaxProgress,
std::max(mMaxProgress, expectedMaxProgress), mProgress };
}
HandleNavMeshToolMessage operator()(NavMeshTool::GeneratedTiles&& message) const
{
int progress = mCellsCount + static_cast<int>(message.mCount);
if (mExpectedMaxProgress < mMaxProgress)
progress += static_cast<int>(std::round((mMaxProgress - mExpectedMaxProgress)
* (static_cast<float>(progress) / static_cast<float>(mExpectedMaxProgress))));
return HandleNavMeshToolMessage{ mCellsCount, mExpectedMaxProgress, mMaxProgress,
std::max(mProgress, progress) };
}
};
int getMaxNavMeshDbFileSizeMiB()
{
return Settings::navigator().mMaxNavmeshdbFileSize / (1024 * 1024);
}
}
}
Launcher::DataFilesPage::DataFilesPage(const Files::ConfigurationManager& cfg, Config::GameSettings& gameSettings,
Config::LauncherSettings& launcherSettings, MainDialog* parent)
: QWidget(parent)
, mMainDialog(parent)
, mCfgMgr(cfg)
, mGameSettings(gameSettings)
, mLauncherSettings(launcherSettings)
, mNavMeshToolInvoker(new Process::ProcessInvoker(this))
{
ui.setupUi(this);
setObjectName("DataFilesPage");
mSelector = new ContentSelectorView::ContentSelector(ui.contentSelectorWidget, /*showOMWScripts=*/true);
const QString encoding = mGameSettings.value("encoding", { "win1252" }).value;
mSelector->setEncoding(encoding);
QVector<std::pair<QString, QString>> languages = { { "English", tr("English") }, { "French", tr("French") },
{ "German", tr("German") }, { "Italian", tr("Italian") }, { "Polish", tr("Polish") },
{ "Russian", tr("Russian") }, { "Spanish", tr("Spanish") } };
for (auto lang : languages)
{
mSelector->languageBox()->addItem(lang.second, lang.first);
}
mNewProfileDialog = new TextInputDialog(tr("New Content List"), tr("Content List name:"), this);
mCloneProfileDialog = new TextInputDialog(tr("Clone Content List"), tr("Content List name:"), this);
connect(mNewProfileDialog->lineEdit(), &LineEdit::textChanged, this, &DataFilesPage::updateNewProfileOkButton);
connect(mCloneProfileDialog->lineEdit(), &LineEdit::textChanged, this, &DataFilesPage::updateCloneProfileOkButton);
connect(ui.directoryAddSubdirsButton, &QPushButton::released, this, [this]() { this->addSubdirectories(true); });
connect(ui.directoryInsertButton, &QPushButton::released, this, [this]() { this->addSubdirectories(false); });
connect(ui.directoryUpButton, &QPushButton::released, this,
[this]() { this->moveSources(ui.directoryListWidget, -1); });
connect(ui.directoryDownButton, &QPushButton::released, this,
[this]() { this->moveSources(ui.directoryListWidget, 1); });
connect(ui.directoryRemoveButton, &QPushButton::released, this, &DataFilesPage::removeDirectory);
connect(
ui.archiveUpButton, &QPushButton::released, this, [this]() { this->moveSources(ui.archiveListWidget, -1); });
connect(
ui.archiveDownButton, &QPushButton::released, this, [this]() { this->moveSources(ui.archiveListWidget, 1); });
connect(ui.directoryListWidget->model(), &QAbstractItemModel::rowsMoved, this, &DataFilesPage::sortDirectories);
connect(ui.archiveListWidget->model(), &QAbstractItemModel::rowsMoved, this, &DataFilesPage::sortArchives);
buildView();
loadSettings();
// Connect signal and slot after the settings have been loaded. We only care about the user changing
// the addons and don't want to get signals of the system doing it during startup.
connect(mSelector, &ContentSelectorView::ContentSelector::signalAddonDataChanged, this,
&DataFilesPage::slotAddonDataChanged);
// Call manually to indicate all changes to addon data during startup.
slotAddonDataChanged();
}
void Launcher::DataFilesPage::buildView()
{
QToolButton* refreshButton = mSelector->refreshButton();
// tool buttons
ui.newProfileButton->setToolTip("Create a new Content List");
ui.cloneProfileButton->setToolTip("Clone the current Content List");
ui.deleteProfileButton->setToolTip("Delete an existing Content List");
// combo box
ui.profilesComboBox->addItem(mDefaultContentListName);
ui.profilesComboBox->setPlaceholderText(QString("Select a Content List..."));
ui.profilesComboBox->setCurrentIndex(ui.profilesComboBox->findText(QLatin1String(mDefaultContentListName)));
// Add the actions to the toolbuttons
ui.newProfileButton->setDefaultAction(ui.newProfileAction);
ui.cloneProfileButton->setDefaultAction(ui.cloneProfileAction);
ui.deleteProfileButton->setDefaultAction(ui.deleteProfileAction);
refreshButton->setDefaultAction(ui.refreshDataFilesAction);
// establish connections
connect(ui.profilesComboBox, qOverload<int>(&::ProfilesComboBox::currentIndexChanged), this,
&DataFilesPage::slotProfileChanged);
connect(ui.profilesComboBox, &::ProfilesComboBox::profileRenamed, this, &DataFilesPage::slotProfileRenamed);
connect(ui.profilesComboBox, qOverload<const QString&, const QString&>(&::ProfilesComboBox::signalProfileChanged),
this, &DataFilesPage::slotProfileChangedByUser);
connect(ui.refreshDataFilesAction, &QAction::triggered, this, &DataFilesPage::slotRefreshButtonClicked);
connect(ui.updateNavMeshButton, &QPushButton::clicked, this, &DataFilesPage::startNavMeshTool);
connect(ui.cancelNavMeshButton, &QPushButton::clicked, this, &DataFilesPage::killNavMeshTool);
connect(mNavMeshToolInvoker->getProcess(), &QProcess::readyReadStandardOutput, this,
&DataFilesPage::readNavMeshToolStdout);
connect(mNavMeshToolInvoker->getProcess(), &QProcess::readyReadStandardError, this,
&DataFilesPage::readNavMeshToolStderr);
connect(mNavMeshToolInvoker->getProcess(), qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
&DataFilesPage::navMeshToolFinished);
buildArchiveContextMenu();
}
void Launcher::DataFilesPage::buildArchiveContextMenu()
{
connect(ui.archiveListWidget, &QListWidget::customContextMenuRequested, this,
&DataFilesPage::slotShowArchiveContextMenu);
mArchiveContextMenu = new QMenu(ui.archiveListWidget);
mArchiveContextMenu->addAction(tr("&Check Selected"), this, SLOT(slotCheckMultiSelectedItems()));
mArchiveContextMenu->addAction(tr("&Uncheck Selected"), this, SLOT(slotUncheckMultiSelectedItems()));
}
bool Launcher::DataFilesPage::loadSettings()
{
ui.navMeshMaxSizeSpinBox->setValue(getMaxNavMeshDbFileSizeMiB());
QStringList profiles = mLauncherSettings.getContentLists();
QString currentProfile = mLauncherSettings.getCurrentContentListName();
qDebug() << "The current profile is: " << currentProfile;
for (const QString& item : profiles)
addProfile(item, false);
// Hack: also add the current profile
if (!currentProfile.isEmpty())
addProfile(currentProfile, true);
auto language = mLauncherSettings.getLanguage();
for (int i = 0; i < mSelector->languageBox()->count(); ++i)
{
QString languageItem = mSelector->languageBox()->itemData(i).toString();
if (language == languageItem)
{
mSelector->languageBox()->setCurrentIndex(i);
break;
}
}
return true;
}
void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName)
{
mSelector->clearFiles();
ui.archiveListWidget->clear();
ui.directoryListWidget->clear();
QList<Config::SettingValue> directories = mGameSettings.getDataDirs();
QStringList contentModelDirectories = mLauncherSettings.getDataDirectoryList(contentModelName);
if (!contentModelDirectories.isEmpty())
{
directories.erase(std::remove_if(directories.begin(), directories.end(),
[&](const Config::SettingValue& dir) { return mGameSettings.isUserSetting(dir); }),
directories.end());
for (const auto& dir : contentModelDirectories)
directories.push_back(mGameSettings.procesPathSettingValue({ dir }));
}
mDataLocal = mGameSettings.getDataLocal();
if (!mDataLocal.isEmpty())
directories.insert(0, { mDataLocal });
const auto& resourcesVfs = mGameSettings.getResourcesVfs();
if (!resourcesVfs.isEmpty())
directories.insert(0, { resourcesVfs });
std::unordered_set<QString> visitedDirectories;
for (const Config::SettingValue& currentDir : directories)
{
if (!visitedDirectories.insert(currentDir.value).second)
continue;
// add new achives files presents in current directory
addArchivesFromDir(currentDir.value);
QStringList tooltip;
// add content files presents in current directory
mSelector->addFiles(currentDir.value, mNewDataDirs.contains(currentDir.value));
// add current directory to list
ui.directoryListWidget->addItem(currentDir.originalRepresentation);
auto row = ui.directoryListWidget->count() - 1;
auto* item = ui.directoryListWidget->item(row);
item->setData(Qt::UserRole, QVariant::fromValue(currentDir));
if (currentDir.value != currentDir.originalRepresentation)
tooltip << tr("Resolved as %1").arg(currentDir.value);
// Display new content with custom formatting
if (mNewDataDirs.contains(currentDir.value))
{
tooltip << tr("Will be added to the current profile");
QFont font = item->font();
font.setBold(true);
font.setItalic(true);
item->setFont(font);
}
// deactivate data-local and resources/vfs: they are always included
// same for ones from non-user config files
if (currentDir.value == mDataLocal || currentDir.value == resourcesVfs
|| !mGameSettings.isUserSetting(currentDir))
{
auto flags = item->flags();
item->setFlags(flags & ~(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled));
if (currentDir.value == mDataLocal)
tooltip << tr("This is the data-local directory and cannot be disabled");
else if (currentDir.value == resourcesVfs)
tooltip << tr("This directory is part of OpenMW and cannot be disabled");
else
tooltip << tr("This directory is enabled in an openmw.cfg other than the user one");
}
// Add a "data file" icon if the directory contains a content file
if (mSelector->containsDataFiles(currentDir.value))
{
item->setIcon(QIcon(":/images/openmw-plugin.png"));
tooltip << tr("Contains content file(s)");
}
else
{
// Pad to correct vertical alignment
QPixmap pixmap(QSize(200, 200)); // Arbitrary big number, will be scaled down to widget size
pixmap.fill(QColor(0, 0, 0, 0));
auto emptyIcon = QIcon(pixmap);
item->setIcon(emptyIcon);
}
item->setToolTip(tooltip.join('\n'));
}
mSelector->sortFiles();
QList<Config::SettingValue> selectedArchives = mGameSettings.getArchiveList();
QStringList contentModelSelectedArchives = mLauncherSettings.getArchiveList(contentModelName);
if (!contentModelSelectedArchives.isEmpty())
{
selectedArchives.erase(std::remove_if(selectedArchives.begin(), selectedArchives.end(),
[&](const Config::SettingValue& dir) { return mGameSettings.isUserSetting(dir); }),
selectedArchives.end());
for (const auto& dir : contentModelSelectedArchives)
selectedArchives.push_back({ dir });
}
// sort and tick BSA according to profile
int row = 0;
for (const auto& archive : selectedArchives)
{
const auto match = ui.archiveListWidget->findItems(archive.value, Qt::MatchFixedString);
if (match.isEmpty())
continue;
const auto name = match[0]->text();
const auto oldrow = ui.archiveListWidget->row(match[0]);
ui.archiveListWidget->takeItem(oldrow);
ui.archiveListWidget->insertItem(row, name);
ui.archiveListWidget->item(row)->setCheckState(Qt::Checked);
ui.archiveListWidget->item(row)->setData(Qt::UserRole, QVariant::fromValue(archive));
if (!mGameSettings.isUserSetting(archive))
{
auto flags = ui.archiveListWidget->item(row)->flags();
ui.archiveListWidget->item(row)->setFlags(
flags & ~(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled));
ui.archiveListWidget->item(row)->setToolTip(
tr("This archive is enabled in an openmw.cfg other than the user one"));
}
row++;
}
QStringList nonUserContent;
for (const auto& content : mGameSettings.getContentList())
{
if (!mGameSettings.isUserSetting(content))
nonUserContent.push_back(content.value);
}
mSelector->setNonUserContent(nonUserContent);
mSelector->setProfileContent(mLauncherSettings.getContentListFiles(contentModelName));
}
void Launcher::DataFilesPage::saveSettings(const QString& profile)
{
Settings::navigator().mMaxNavmeshdbFileSize.set(
static_cast<std::uint64_t>(std::max(0, ui.navMeshMaxSizeSpinBox->value())) * 1024 * 1024);
QString profileName = profile;
if (profileName.isEmpty())
profileName = ui.profilesComboBox->currentText();
// retrieve the data paths
auto dirList = selectedDirectoriesPaths();
// retrieve the files selected for the profile
ContentSelectorModel::ContentFileList items = mSelector->selectedFiles();
// set the value of the current profile (not necessarily the profile being saved!)
mLauncherSettings.setCurrentContentListName(ui.profilesComboBox->currentText());
QStringList fileNames;
for (const ContentSelectorModel::EsmFile* item : items)
{
fileNames.append(item->fileName());
}
QStringList dirNames;
for (const auto& dir : dirList)
{
if (mGameSettings.isUserSetting(dir))
dirNames.push_back(dir.originalRepresentation);
}
QStringList archiveNames;
for (const auto& archive : selectedArchivePaths())
{
if (mGameSettings.isUserSetting(archive))
archiveNames.push_back(archive.originalRepresentation);
}
mLauncherSettings.setContentList(profileName, dirNames, archiveNames, fileNames);
mGameSettings.setContentList(dirList, selectedArchivePaths(), fileNames);
QString language(mSelector->languageBox()->currentData().toString());
mLauncherSettings.setLanguage(language);
if (language == QLatin1String("Polish"))
{
mGameSettings.setValue(QLatin1String("encoding"), { "win1250" });
}
else if (language == QLatin1String("Russian"))
{
mGameSettings.setValue(QLatin1String("encoding"), { "win1251" });
}
else
{
mGameSettings.setValue(QLatin1String("encoding"), { "win1252" });
}
}
QList<Config::SettingValue> Launcher::DataFilesPage::selectedDirectoriesPaths() const
{
QList<Config::SettingValue> dirList;
for (int i = 0; i < ui.directoryListWidget->count(); ++i)
{
const QListWidgetItem* item = ui.directoryListWidget->item(i);
if (item->flags() & Qt::ItemIsEnabled)
dirList.append(qvariant_cast<Config::SettingValue>(item->data(Qt::UserRole)));
}
return dirList;
}
QList<Config::SettingValue> Launcher::DataFilesPage::selectedArchivePaths() const
{
QList<Config::SettingValue> archiveList;
for (int i = 0; i < ui.archiveListWidget->count(); ++i)
{
const QListWidgetItem* item = ui.archiveListWidget->item(i);
if (item->checkState() == Qt::Checked)
archiveList.append(qvariant_cast<Config::SettingValue>(item->data(Qt::UserRole)));
}
return archiveList;
}
QStringList Launcher::DataFilesPage::selectedFilePaths() const
{
// retrieve the files selected for the profile
ContentSelectorModel::ContentFileList items = mSelector->selectedFiles();
QStringList filePaths;
for (const ContentSelectorModel::EsmFile* item : items)
if (QFile::exists(item->filePath()))
filePaths.append(item->filePath());
return filePaths;
}
void Launcher::DataFilesPage::removeProfile(const QString& profile)
{
mLauncherSettings.removeContentList(profile);
}
QAbstractItemModel* Launcher::DataFilesPage::profilesModel() const
{
return ui.profilesComboBox->model();
}
int Launcher::DataFilesPage::profilesIndex() const
{
return ui.profilesComboBox->currentIndex();
}
void Launcher::DataFilesPage::setProfile(int index, bool savePrevious)
{
if (index >= -1 && index < ui.profilesComboBox->count())
{
QString previous = mPreviousProfile;
QString current = ui.profilesComboBox->itemText(index);
mPreviousProfile = current;
setProfile(previous, current, savePrevious);
}
}
void Launcher::DataFilesPage::setProfile(const QString& previous, const QString& current, bool savePrevious)
{
// abort if no change (poss. duplicate signal)
if (previous == current)
return;
if (!previous.isEmpty() && savePrevious)
saveSettings(previous);
ui.profilesComboBox->setCurrentProfile(ui.profilesComboBox->findText(current));
mNewDataDirs.clear();
mKnownArchives.clear();
populateFileViews(current);
// save list of "old" bsa to be able to display "new" bsa in a different colour
for (int i = 0; i < ui.archiveListWidget->count(); ++i)
{
auto* item = ui.archiveListWidget->item(i);
mKnownArchives.push_back(item->text());
}
checkForDefaultProfile();
}
void Launcher::DataFilesPage::slotProfileDeleted(const QString& item)
{
removeProfile(item);
}
void Launcher::DataFilesPage::refreshDataFilesView()
{
QString currentProfile = ui.profilesComboBox->currentText();
saveSettings(currentProfile);
populateFileViews(currentProfile);
}
void Launcher::DataFilesPage::slotRefreshButtonClicked()
{
refreshDataFilesView();
}
void Launcher::DataFilesPage::slotProfileChangedByUser(const QString& previous, const QString& current)
{
setProfile(previous, current, true);
emit signalProfileChanged(ui.profilesComboBox->findText(current));
}
void Launcher::DataFilesPage::slotProfileRenamed(const QString& previous, const QString& current)
{
if (previous.isEmpty())
return;
// Save the new profile name
saveSettings();
// Remove the old one
removeProfile(previous);
loadSettings();
}
void Launcher::DataFilesPage::slotProfileChanged(int index)
{
// in case the event was triggered externally
if (ui.profilesComboBox->currentIndex() != index)
ui.profilesComboBox->setCurrentIndex(index);
setProfile(index, true);
}
void Launcher::DataFilesPage::on_newProfileAction_triggered()
{
if (mNewProfileDialog->exec() != QDialog::Accepted)
return;
QString profile = mNewProfileDialog->lineEdit()->text();
if (profile.isEmpty())
return;
saveSettings();
mLauncherSettings.setCurrentContentListName(profile);
addProfile(profile, true);
}
void Launcher::DataFilesPage::addProfile(const QString& profile, bool setAsCurrent)
{
if (profile.isEmpty())
return;
if (ui.profilesComboBox->findText(profile) == -1)
ui.profilesComboBox->addItem(profile);
if (setAsCurrent)
setProfile(ui.profilesComboBox->findText(profile), false);
}
void Launcher::DataFilesPage::on_cloneProfileAction_triggered()
{
if (mCloneProfileDialog->exec() != QDialog::Accepted)
return;
QString profile = mCloneProfileDialog->lineEdit()->text();
if (profile.isEmpty())
return;
const auto& dirList = selectedDirectoriesPaths();
QStringList dirNames;
for (const auto& dir : dirList)
{
if (mGameSettings.isUserSetting(dir))
dirNames.push_back(dir.originalRepresentation);
}
QStringList archiveNames;
for (const auto& archive : selectedArchivePaths())
{
if (mGameSettings.isUserSetting(archive))
archiveNames.push_back(archive.originalRepresentation);
}
mLauncherSettings.setContentList(profile, dirNames, archiveNames, selectedFilePaths());
addProfile(profile, true);
}
void Launcher::DataFilesPage::on_deleteProfileAction_triggered()
{
QString profile = ui.profilesComboBox->currentText();
if (profile.isEmpty())
return;
if (!showDeleteMessageBox(profile))
return;
// this should work since the Default profile can't be deleted and is always index 0
int next = ui.profilesComboBox->currentIndex() - 1;
// changing the profile forces a reload of plugin file views.
ui.profilesComboBox->setCurrentIndex(next);
removeProfile(profile);
ui.profilesComboBox->removeItem(ui.profilesComboBox->findText(profile));
checkForDefaultProfile();
}
void Launcher::DataFilesPage::updateNewProfileOkButton(const QString& text)
{
// We do this here because we need the profiles combobox text
mNewProfileDialog->setOkButtonEnabled(!text.isEmpty() && ui.profilesComboBox->findText(text) == -1);
}
void Launcher::DataFilesPage::updateCloneProfileOkButton(const QString& text)
{
// We do this here because we need the profiles combobox text
mCloneProfileDialog->setOkButtonEnabled(!text.isEmpty() && ui.profilesComboBox->findText(text) == -1);
}
void Launcher::DataFilesPage::addSubdirectories(bool append)
{
int selectedRow = -1;
if (append)
{
selectedRow = ui.directoryListWidget->count();
}
else
{
const QList<QPair<int, QListWidgetItem*>> sortedItems = sortedSelectedItems(ui.directoryListWidget);
if (!sortedItems.isEmpty())
selectedRow = sortedItems.first().first;
}
if (selectedRow == -1)
return;
QString rootPath = QFileDialog::getExistingDirectory(
this, tr("Select Directory"), QDir::homePath(), QFileDialog::ShowDirsOnly | QFileDialog::Option::ReadOnly);
if (rootPath.isEmpty())
return;
const QDir rootDir(rootPath);
rootPath = rootDir.canonicalPath();
QStringList subdirs;
contentSubdirs(rootPath, subdirs);
// Always offer to append the root directory just in case
if (subdirs.isEmpty() || subdirs[0] != rootPath)
subdirs.prepend(rootPath);
else if (subdirs.size() == 1)
{
// We didn't find anything else that looks like a content directory
// Automatically add the directory selected by user
if (!ui.directoryListWidget->findItems(rootPath, Qt::MatchFixedString).isEmpty())
return;
ui.directoryListWidget->insertItem(selectedRow, rootPath);
auto* item = ui.directoryListWidget->item(selectedRow);
item->setData(Qt::UserRole, QVariant::fromValue(Config::SettingValue{ rootPath }));
mNewDataDirs.push_back(rootPath);
refreshDataFilesView();
return;
}
QDialog dialog;
Ui::SelectSubdirs select;
select.setupUi(&dialog);
for (const auto& dir : subdirs)
{
if (!ui.directoryListWidget->findItems(dir, Qt::MatchFixedString).isEmpty())
continue;
const auto lastRow = select.dirListWidget->count();
select.dirListWidget->addItem(dir);
select.dirListWidget->item(lastRow)->setCheckState(Qt::Unchecked);
}
dialog.show();
if (dialog.exec() == QDialog::Rejected)
return;
for (int i = 0; i < select.dirListWidget->count(); ++i)
{
const auto* dir = select.dirListWidget->item(i);
if (dir->checkState() == Qt::Checked)
{
ui.directoryListWidget->insertItem(selectedRow, dir->text());
auto* item = ui.directoryListWidget->item(selectedRow);
item->setData(Qt::UserRole, QVariant::fromValue(Config::SettingValue{ dir->text() }));
mNewDataDirs.push_back(dir->text());
++selectedRow;
}
}
refreshDataFilesView();
}
void Launcher::DataFilesPage::sortDirectories()
{
// Ensure disabled entries (aka default directories) are always at the top.
for (auto i = 1; i < ui.directoryListWidget->count(); ++i)
{
if (!(ui.directoryListWidget->item(i)->flags() & Qt::ItemIsEnabled)
&& (ui.directoryListWidget->item(i - 1)->flags() & Qt::ItemIsEnabled))
{
const auto item = ui.directoryListWidget->takeItem(i);
ui.directoryListWidget->insertItem(i - 1, item);
ui.directoryListWidget->setCurrentRow(i);
}
}
}
void Launcher::DataFilesPage::sortArchives()
{
// Ensure disabled entries (aka ones from non-user config files) are always at the top.
for (auto i = 1; i < ui.archiveListWidget->count(); ++i)
{
if (!(ui.archiveListWidget->item(i)->flags() & Qt::ItemIsEnabled)
&& (ui.archiveListWidget->item(i - 1)->flags() & Qt::ItemIsEnabled))
{
const auto item = ui.archiveListWidget->takeItem(i);
ui.archiveListWidget->insertItem(i - 1, item);
ui.archiveListWidget->setCurrentRow(i);
}
}
}
void Launcher::DataFilesPage::removeDirectory()
{
for (const auto& path : ui.directoryListWidget->selectedItems())
ui.directoryListWidget->takeItem(ui.directoryListWidget->row(path));
refreshDataFilesView();
}
void Launcher::DataFilesPage::slotShowArchiveContextMenu(const QPoint& pos)
{
QPoint globalPos = ui.archiveListWidget->viewport()->mapToGlobal(pos);
mArchiveContextMenu->exec(globalPos);
}
void Launcher::DataFilesPage::setCheckStateForMultiSelectedItems(bool checked)
{
Qt::CheckState checkState = checked ? Qt::Checked : Qt::Unchecked;
for (QListWidgetItem* selectedItem : ui.archiveListWidget->selectedItems())
{
selectedItem->setCheckState(checkState);
}
}
void Launcher::DataFilesPage::slotUncheckMultiSelectedItems()
{
setCheckStateForMultiSelectedItems(false);
}
void Launcher::DataFilesPage::slotCheckMultiSelectedItems()
{
setCheckStateForMultiSelectedItems(true);
}
void Launcher::DataFilesPage::moveSources(QListWidget* sourceList, int step)
{
const QList<QPair<int, QListWidgetItem*>> sortedItems = sortedSelectedItems(sourceList, step > 0);
for (const auto& i : sortedItems)
{
int selectedRow = sourceList->row(i.second);
int newRow = selectedRow + step;
if (selectedRow == -1 || newRow < 0 || newRow > sourceList->count() - 1)
break;
if (!(sourceList->item(newRow)->flags() & Qt::ItemIsEnabled))
break;
const auto item = sourceList->takeItem(selectedRow);
sourceList->insertItem(newRow, item);
sourceList->setCurrentRow(newRow);
}
}
void Launcher::DataFilesPage::addArchive(const QString& name, Qt::CheckState selected, int row)
{
if (row == -1)
row = ui.archiveListWidget->count();
ui.archiveListWidget->insertItem(row, name);
ui.archiveListWidget->item(row)->setCheckState(selected);
ui.archiveListWidget->item(row)->setData(Qt::UserRole, QVariant::fromValue(Config::SettingValue{ name }));
if (mKnownArchives.filter(name).isEmpty()) // XXX why contains doesn't work here ???
{
auto item = ui.archiveListWidget->item(row);
QFont font = item->font();
font.setBold(true);
font.setItalic(true);
item->setFont(font);
}
}
void Launcher::DataFilesPage::addArchivesFromDir(const QString& path)
{
QStringList archiveFilter{ "*.bsa", "*.ba2" };
QDir dir(path);
std::unordered_set<VFS::Path::Normalized, VFS::Path::Hash> archives;
for (int i = 0; i < ui.archiveListWidget->count(); ++i)
archives.insert(VFS::Path::normalizedFromQString(ui.archiveListWidget->item(i)->text()));
for (const auto& fileinfo : dir.entryInfoList(archiveFilter))
{
const auto absPath = fileinfo.absoluteFilePath();
if (Bsa::BSAFile::detectVersion(Files::pathFromQString(absPath)) == Bsa::BsaVersion::Unknown)
continue;
const auto fileName = fileinfo.fileName();
if (archives.insert(VFS::Path::normalizedFromQString(fileName)).second)
addArchive(fileName, Qt::Unchecked);
}
}
void Launcher::DataFilesPage::checkForDefaultProfile()
{
// don't allow deleting "Default" profile
bool success = (ui.profilesComboBox->currentText() != mDefaultContentListName);
ui.deleteProfileAction->setEnabled(success);
ui.profilesComboBox->setEditEnabled(success);
}
bool Launcher::DataFilesPage::showDeleteMessageBox(const QString& text)
{
QMessageBox msgBox(this);
msgBox.setWindowTitle(tr("Delete Content List"));
msgBox.setIcon(QMessageBox::Warning);
msgBox.setStandardButtons(QMessageBox::Cancel);
msgBox.setText(tr("Are you sure you want to delete <b>%1</b>?").arg(text));
QAbstractButton* deleteButton = msgBox.addButton(tr("Delete"), QMessageBox::ActionRole);
msgBox.exec();
return (msgBox.clickedButton() == deleteButton);
}
void Launcher::DataFilesPage::slotAddonDataChanged()
{
QStringList selectedFiles = selectedFilePaths();
if (previousSelectedFiles != selectedFiles)
{
previousSelectedFiles = selectedFiles;
// Loading cells for core Morrowind + Expansions takes about 0.2 seconds, which is enough to cause a
// barely perceptible UI lag. Splitting into its own thread to alleviate that.
std::thread loadCellsThread(&DataFilesPage::reloadCells, this, selectedFiles);
loadCellsThread.detach();
}
}
// Mutex lock to run reloadCells synchronously.
static std::mutex reloadCellsMutex;
void Launcher::DataFilesPage::reloadCells(QStringList selectedFiles)
{
// Use a mutex lock so that we can prevent two threads from executing the rest of this code at the same time
// Based on https://stackoverflow.com/a/5429695/531762
std::unique_lock<std::mutex> lock(reloadCellsMutex);
// The following code will run only if there is not another thread currently running it
CellNameLoader cellNameLoader;
QSet<QString> set = cellNameLoader.getCellNames(selectedFiles);
QStringList cellNamesList(set.begin(), set.end());
std::sort(cellNamesList.begin(), cellNamesList.end());
emit signalLoadedCellsChanged(cellNamesList);
}
void Launcher::DataFilesPage::startNavMeshTool()
{
mMainDialog->writeSettings();
ui.navMeshLogPlainTextEdit->clear();
ui.navMeshProgressBar->setValue(0);
ui.navMeshProgressBar->setMaximum(1);
ui.navMeshProgressBar->resetFormat();
mNavMeshToolProgress = NavMeshToolProgress{};
QStringList arguments({ "--write-binary-log" });
if (ui.navMeshRemoveUnusedTilesCheckBox->checkState() == Qt::Checked)
arguments.append("--remove-unused-tiles");
if (!mNavMeshToolInvoker->startProcess(QLatin1String("openmw-navmeshtool"), arguments))
return;
ui.cancelNavMeshButton->setEnabled(true);
ui.navMeshProgressBar->setEnabled(true);
}
void Launcher::DataFilesPage::killNavMeshTool()
{
mNavMeshToolInvoker->killProcess();
}
void Launcher::DataFilesPage::readNavMeshToolStderr()
{
updateNavMeshProgress(4096);
}
void Launcher::DataFilesPage::updateNavMeshProgress(int minDataSize)
{
if (!mNavMeshToolProgress.mEnabled)
return;
QProcess& process = *mNavMeshToolInvoker->getProcess();
mNavMeshToolProgress.mMessagesData.append(process.readAllStandardError());
if (mNavMeshToolProgress.mMessagesData.size() < minDataSize)
return;
const std::byte* const begin = reinterpret_cast<const std::byte*>(mNavMeshToolProgress.mMessagesData.constData());
const std::byte* const end = begin + mNavMeshToolProgress.mMessagesData.size();
const std::byte* position = begin;
HandleNavMeshToolMessage handle{
mNavMeshToolProgress.mCellsCount,
mNavMeshToolProgress.mExpectedMaxProgress,
ui.navMeshProgressBar->maximum(),
ui.navMeshProgressBar->value(),
};
try
{
while (true)
{
NavMeshTool::Message message;
const std::byte* const nextPosition = NavMeshTool::deserialize(position, end, message);
if (nextPosition == position)
break;
position = nextPosition;
handle = std::visit(handle, NavMeshTool::decode(message));
}
}
catch (const std::exception& e)
{
Log(Debug::Error) << "Failed to deserialize navmeshtool message: " << e.what();
mNavMeshToolProgress.mEnabled = false;
ui.navMeshProgressBar->setFormat("Failed to update progress: " + QString(e.what()));
}
if (position != begin)
mNavMeshToolProgress.mMessagesData = mNavMeshToolProgress.mMessagesData.mid(position - begin);
mNavMeshToolProgress.mCellsCount = handle.mCellsCount;
mNavMeshToolProgress.mExpectedMaxProgress = handle.mExpectedMaxProgress;
ui.navMeshProgressBar->setMaximum(handle.mMaxProgress);
ui.navMeshProgressBar->setValue(handle.mProgress);
}
void Launcher::DataFilesPage::readNavMeshToolStdout()
{
QProcess& process = *mNavMeshToolInvoker->getProcess();
QByteArray& logData = mNavMeshToolProgress.mLogData;
logData.append(process.readAllStandardOutput());
const int lineEnd = logData.lastIndexOf('\n');
if (lineEnd == -1)
return;
const int size = logData.size() >= lineEnd && logData[lineEnd - 1] == '\r' ? lineEnd - 1 : lineEnd;
ui.navMeshLogPlainTextEdit->appendPlainText(QString::fromUtf8(logData.data(), size));
logData = logData.mid(lineEnd + 1);
}
void Launcher::DataFilesPage::navMeshToolFinished(int exitCode, QProcess::ExitStatus exitStatus)
{
updateNavMeshProgress(0);
ui.navMeshLogPlainTextEdit->appendPlainText(
QString::fromUtf8(mNavMeshToolInvoker->getProcess()->readAllStandardOutput()));
if (exitCode == 0 && exitStatus == QProcess::ExitStatus::NormalExit)
{
ui.navMeshProgressBar->setValue(ui.navMeshProgressBar->maximum());
ui.navMeshProgressBar->resetFormat();
}
ui.cancelNavMeshButton->setEnabled(false);
ui.navMeshProgressBar->setEnabled(false);
}