1
0
Fork 0
mirror of https://github.com/OpenMW/openmw.git synced 2025-10-15 12:56:36 +00:00
openmw/apps/launcher/datafilespage.cpp
elsid d84c81a85d
Check if reload cells aborted before cv wait
If mAbortReloadCells is set to true before the first mReloadCellsMutex
lock the loop can become infinite. Unlikely to happen in practice but
theoretically possible.
2025-08-17 12:44:15 +02:00

1180 lines
41 KiB
C++

#include "datafilespage.hpp"
#include "maindialog.hpp"
#include <QClipboard>
#include <QDebug>
#include <QDesktopServices>
#include <QFileDialog>
#include <QList>
#include <QMessageBox>
#include <QPair>
#include <QProgressDialog>
#include <QPushButton>
#include <QTimer>
#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"
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)
, mDirectoryPickerDialog(new QDialog(this))
, mMainDialog(parent)
, mCfgMgr(cfg)
, mGameSettings(gameSettings)
, mLauncherSettings(launcherSettings)
, mNavMeshToolInvoker(new Process::ProcessInvoker(this))
, mReloadCellsThread(&DataFilesPage::reloadCells, this)
{
ui.setupUi(this);
mDirectoryPicker.setupUi(mDirectoryPickerDialog);
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);
mReloadCellsTimer = new QTimer(this);
mReloadCellsTimer->setSingleShot(true);
mReloadCellsTimer->setInterval(200);
connect(mReloadCellsTimer, &QTimer::timeout, this, &DataFilesPage::onReloadCellsTimerTimeout);
// Call manually to indicate all changes to addon data during startup.
onReloadCellsTimerTimeout();
}
Launcher::DataFilesPage::~DataFilesPage()
{
{
const std::lock_guard lock(mReloadCellsMutex);
mAbortReloadCells = true;
mStartReloadCells.notify_one();
}
mReloadCellsThread.join();
}
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();
buildDataFilesContextMenu();
buildDirectoryPickerContextMenu();
}
void Launcher::DataFilesPage::slotCopySelectedItemsPaths()
{
QClipboard* clipboard = QApplication::clipboard();
QStringList filepaths;
for (QListWidgetItem* item : ui.directoryListWidget->selectedItems())
{
QString path = qvariant_cast<Config::SettingValue>(item->data(Qt::UserRole)).originalRepresentation;
filepaths.push_back(path);
}
if (!filepaths.isEmpty())
{
clipboard->setText(filepaths.join("\n"));
}
}
void Launcher::DataFilesPage::slotOpenSelectedItemsPaths()
{
QListWidgetItem* item = ui.directoryListWidget->currentItem();
QUrl confFolderUrl = QUrl::fromLocalFile(qvariant_cast<Config::SettingValue>(item->data(Qt::UserRole)).value);
QDesktopServices::openUrl(confFolderUrl);
}
void Launcher::DataFilesPage::buildArchiveContextMenu()
{
connect(ui.archiveListWidget, &QListWidget::customContextMenuRequested, this,
&DataFilesPage::slotShowArchiveContextMenu);
mArchiveContextMenu = new QMenu(ui.archiveListWidget);
mArchiveContextMenu->addAction(tr("&Check Selected"), this,
[this]() { setCheckStateForMultiSelectedItems(ui.archiveListWidget, Qt::Checked); });
mArchiveContextMenu->addAction(tr("&Uncheck Selected"), this,
[this]() { setCheckStateForMultiSelectedItems(ui.archiveListWidget, Qt::Unchecked); });
}
void Launcher::DataFilesPage::buildDataFilesContextMenu()
{
connect(ui.directoryListWidget, &QListWidget::customContextMenuRequested, this,
&DataFilesPage::slotShowDataFilesContextMenu);
mDataFilesContextMenu = new QMenu(ui.directoryListWidget);
mDataFilesContextMenu->addAction(
tr("&Copy Path(s) to Clipboard"), this, &Launcher::DataFilesPage::slotCopySelectedItemsPaths);
mDataFilesContextMenu->addAction(
tr("&Open Path in File Explorer"), this, &Launcher::DataFilesPage::slotOpenSelectedItemsPaths);
}
void Launcher::DataFilesPage::buildDirectoryPickerContextMenu()
{
connect(mDirectoryPicker.dirListWidget, &QListWidget::customContextMenuRequested, this,
&DataFilesPage::slotShowDirectoryPickerContextMenu);
mDirectoryPickerMenu = new QMenu(mDirectoryPicker.dirListWidget);
mDirectoryPickerMenu->addAction(tr("&Check Selected"), this,
[this]() { setCheckStateForMultiSelectedItems(mDirectoryPicker.dirListWidget, Qt::Checked); });
mDirectoryPickerMenu->addAction(tr("&Uncheck Selected"), this,
[this]() { setCheckStateForMultiSelectedItems(mDirectoryPicker.dirListWidget, Qt::Unchecked); });
}
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.processPathSettingValue({ dir }));
}
mDataLocal = mGameSettings.getDataLocal();
if (!mDataLocal.isEmpty())
directories.insert(0, { mDataLocal });
const auto& resourcesVfs = mGameSettings.getResourcesVfs();
if (!resourcesVfs.isEmpty())
directories.insert(0, { resourcesVfs });
QIcon containsDataIcon(":/images/openmw-plugin.png");
QProgressDialog progressBar("Adding data directories", {}, 0, static_cast<int>(directories.size()), this);
progressBar.setWindowModality(Qt::WindowModal);
std::unordered_set<QString> visitedDirectories;
for (qsizetype i = 0; i < directories.size(); ++i)
{
progressBar.setValue(static_cast<int>(i));
const Config::SettingValue& currentDir = directories.at(i);
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(containsDataIcon);
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'));
}
progressBar.setValue(progressBar.maximum());
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]);
// entries may be duplicated, e.g. if a content list predated a BSA being added to a non-user config file
if (oldrow < row)
continue;
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"), {}, 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;
}
mDirectoryPicker.dirListWidget->clear();
for (const auto& dir : subdirs)
{
if (!ui.directoryListWidget->findItems(dir, Qt::MatchFixedString).isEmpty())
continue;
QListWidgetItem* newDir = new QListWidgetItem(dir, mDirectoryPicker.dirListWidget);
newDir->setCheckState(Qt::Unchecked);
}
if (mDirectoryPickerDialog->exec() == QDialog::Rejected)
return;
for (int i = 0; i < mDirectoryPicker.dirListWidget->count(); ++i)
{
const auto* dir = mDirectoryPicker.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::showContextMenu(QMenu* menu, QListWidget* list, const QPoint& pos)
{
QPoint globalPos = list->viewport()->mapToGlobal(pos);
menu->exec(globalPos);
}
void Launcher::DataFilesPage::slotShowArchiveContextMenu(const QPoint& pos)
{
showContextMenu(mArchiveContextMenu, ui.archiveListWidget, pos);
}
void Launcher::DataFilesPage::slotShowDataFilesContextMenu(const QPoint& pos)
{
showContextMenu(mDataFilesContextMenu, ui.directoryListWidget, pos);
}
void Launcher::DataFilesPage::slotShowDirectoryPickerContextMenu(const QPoint& pos)
{
showContextMenu(mDirectoryPickerMenu, mDirectoryPicker.dirListWidget, pos);
}
void Launcher::DataFilesPage::setCheckStateForMultiSelectedItems(QListWidget* list, Qt::CheckState checkState)
{
for (QListWidgetItem* selectedItem : list->selectedItems())
{
selectedItem->setCheckState(checkState);
}
}
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()
{
mReloadCellsTimer->start();
}
void Launcher::DataFilesPage::onReloadCellsTimerTimeout()
{
const ContentSelectorModel::ContentFileList items = mSelector->selectedFiles();
QStringList selectedFiles;
for (const ContentSelectorModel::EsmFile* item : items)
selectedFiles.append(item->filePath());
if (mSelectedFiles != selectedFiles)
{
const std::lock_guard lock(mReloadCellsMutex);
mSelectedFiles = std::move(selectedFiles);
mReloadCells = true;
mStartReloadCells.notify_one();
}
}
void Launcher::DataFilesPage::reloadCells()
{
QStringList selectedFiles;
std::unique_lock lock(mReloadCellsMutex);
while (true)
{
if (mAbortReloadCells)
return;
mStartReloadCells.wait(lock);
if (mAbortReloadCells)
return;
if (!std::exchange(mReloadCells, false))
continue;
const QStringList newSelectedFiles = mSelectedFiles;
lock.unlock();
QStringList filteredFiles;
for (const QString& v : newSelectedFiles)
if (QFile::exists(v))
filteredFiles.append(v);
if (selectedFiles != filteredFiles)
{
selectedFiles = std::move(filteredFiles);
CellNameLoader cellNameLoader;
QSet<QString> set = cellNameLoader.getCellNames(selectedFiles);
QStringList cellNamesList(set.begin(), set.end());
std::sort(cellNamesList.begin(), cellNamesList.end());
emit signalLoadedCellsChanged(std::move(cellNamesList));
}
lock.lock();
}
}
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);
}