mirror of
https://github.com/OpenMW/openmw.git
synced 2025-01-16 18:59:57 +00:00
9280957581
If directory path is a symlink it should be showed and written to config files as is. Between launcher runs the resulting canonical path may be different so the resolved path becomes outdated.
899 lines
31 KiB
C++
899 lines
31 KiB
C++
#include "datafilespage.hpp"
|
|
#include "maindialog.hpp"
|
|
|
|
#include <QDebug>
|
|
#include <QFileDialog>
|
|
#include <QMessageBox>
|
|
#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/settings.hpp>
|
|
#include <components/vfs/bsaarchive.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)
|
|
{
|
|
QStringList fileFilter{ "*.esm", "*.esp", "*.omwaddon", "*.bsa" };
|
|
QStringList dirFilter{ "bookart", "icons", "meshes", "music", "sound", "textures" };
|
|
|
|
QDir currentDir(path);
|
|
if (!currentDir.entryInfoList(fileFilter, QDir::Files).empty()
|
|
|| !currentDir.entryInfoList(dirFilter, QDir::Dirs | QDir::NoDotAndDotDot).empty())
|
|
dirs.push_back(currentDir.canonicalPath());
|
|
|
|
for (const auto& subdir : currentDir.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot))
|
|
contentSubdirs(subdir.canonicalFilePath(), dirs);
|
|
}
|
|
}
|
|
|
|
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::Manager::getUInt64("max navmeshdb file size", "Navigator") / (1024 * 1024);
|
|
}
|
|
}
|
|
}
|
|
|
|
Launcher::DataFilesPage::DataFilesPage(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");
|
|
mSelector->setEncoding(encoding);
|
|
|
|
QStringList languages;
|
|
languages << tr("English") << tr("French") << tr("German") << tr("Italian") << tr("Polish") << tr("Russian")
|
|
<< tr("Spanish");
|
|
|
|
mSelector->languageBox()->addItems(languages);
|
|
|
|
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->moveDirectory(-1); });
|
|
connect(ui.directoryDownButton, &QPushButton::released, this, [this]() { this->moveDirectory(1); });
|
|
connect(ui.directoryRemoveButton, &QPushButton::released, this, [this]() { this->removeDirectory(); });
|
|
connect(ui.archiveUpButton, &QPushButton::released, this, [this]() { this->moveArchive(-1); });
|
|
connect(ui.archiveDownButton, &QPushButton::released, this, [this]() { this->moveArchive(1); });
|
|
connect(
|
|
ui.directoryListWidget->model(), &QAbstractItemModel::rowsMoved, this, [this]() { this->sortDirectories(); });
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
|
|
const int index = mSelector->languageBox()->findText(mLauncherSettings.getLanguage());
|
|
if (index != -1)
|
|
mSelector->languageBox()->setCurrentIndex(index);
|
|
|
|
return true;
|
|
}
|
|
|
|
void Launcher::DataFilesPage::populateFileViews(const QString& contentModelName)
|
|
{
|
|
mSelector->clearFiles();
|
|
ui.archiveListWidget->clear();
|
|
ui.directoryListWidget->clear();
|
|
|
|
QStringList directories = mLauncherSettings.getDataDirectoryList(contentModelName);
|
|
if (directories.isEmpty())
|
|
directories = mGameSettings.getDataDirs();
|
|
|
|
mDataLocal = mGameSettings.getDataLocal();
|
|
if (!mDataLocal.isEmpty())
|
|
directories.insert(0, mDataLocal);
|
|
|
|
const auto& globalDataDir = mGameSettings.getGlobalDataDir();
|
|
if (!globalDataDir.empty())
|
|
directories.insert(0, Files::pathToQString(globalDataDir));
|
|
|
|
std::unordered_set<QString> visitedDirectories;
|
|
for (const QString& currentDir : directories)
|
|
{
|
|
// normalize user supplied directories: resolve symlink, convert to native separator, make absolute
|
|
const QString canonicalDirPath = QDir(QDir::cleanPath(currentDir)).canonicalPath();
|
|
|
|
if (!visitedDirectories.insert(canonicalDirPath).second)
|
|
continue;
|
|
|
|
// add new achives files presents in current directory
|
|
addArchivesFromDir(currentDir);
|
|
|
|
QString tooltip;
|
|
|
|
// add content files presents in current directory
|
|
mSelector->addFiles(currentDir, mNewDataDirs.contains(canonicalDirPath));
|
|
|
|
// add current directory to list
|
|
ui.directoryListWidget->addItem(currentDir);
|
|
auto row = ui.directoryListWidget->count() - 1;
|
|
auto* item = ui.directoryListWidget->item(row);
|
|
|
|
// Display new content with green background
|
|
if (mNewDataDirs.contains(canonicalDirPath))
|
|
{
|
|
tooltip += "Will be added to the current profile\n";
|
|
item->setBackground(Qt::green);
|
|
item->setForeground(Qt::black);
|
|
}
|
|
|
|
// deactivate data-local and global data directory: they are always included
|
|
if (currentDir == mDataLocal || Files::pathFromQString(currentDir) == globalDataDir)
|
|
{
|
|
auto flags = item->flags();
|
|
item->setFlags(flags & ~(Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | Qt::ItemIsEnabled));
|
|
}
|
|
|
|
// Add a "data file" icon if the directory contains a content file
|
|
if (mSelector->containsDataFiles(currentDir))
|
|
{
|
|
item->setIcon(QIcon(":/images/openmw-plugin.png"));
|
|
tooltip += "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(ui.directoryListWidget->palette().base().color());
|
|
auto emptyIcon = QIcon(pixmap);
|
|
item->setIcon(emptyIcon);
|
|
}
|
|
item->setToolTip(tooltip);
|
|
}
|
|
mSelector->sortFiles();
|
|
|
|
QStringList selectedArchives = mLauncherSettings.getArchiveList(contentModelName);
|
|
if (selectedArchives.isEmpty())
|
|
selectedArchives = mGameSettings.getArchiveList();
|
|
|
|
// sort and tick BSA according to profile
|
|
int row = 0;
|
|
for (const auto& archive : selectedArchives)
|
|
{
|
|
const auto match = ui.archiveListWidget->findItems(archive, Qt::MatchExactly);
|
|
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);
|
|
row++;
|
|
}
|
|
|
|
PathIterator pathIterator(directories);
|
|
|
|
mSelector->setProfileContent(filesInProfile(contentModelName, pathIterator));
|
|
}
|
|
|
|
QStringList Launcher::DataFilesPage::filesInProfile(const QString& profileName, PathIterator& pathIterator)
|
|
{
|
|
QStringList files = mLauncherSettings.getContentListFiles(profileName);
|
|
QStringList filepaths;
|
|
|
|
for (const QString& file : files)
|
|
{
|
|
QString filepath = pathIterator.findFirstPath(file);
|
|
|
|
if (!filepath.isEmpty())
|
|
filepaths << filepath;
|
|
}
|
|
|
|
return filepaths;
|
|
}
|
|
|
|
void Launcher::DataFilesPage::saveSettings(const QString& profile)
|
|
{
|
|
if (const int value = ui.navMeshMaxSizeSpinBox->value(); value != getMaxNavMeshDbFileSizeMiB())
|
|
Settings::Manager::setUInt64(
|
|
"max navmeshdb file size", "Navigator", static_cast<std::uint64_t>(std::max(0, 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());
|
|
}
|
|
mLauncherSettings.setContentList(profileName, dirList, selectedArchivePaths(), fileNames);
|
|
mGameSettings.setContentList(dirList, selectedArchivePaths(), fileNames);
|
|
|
|
QString language(mSelector->languageBox()->currentText());
|
|
|
|
mLauncherSettings.setLanguage(language);
|
|
|
|
if (language == QLatin1String("Polish"))
|
|
{
|
|
mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1250"));
|
|
}
|
|
else if (language == QLatin1String("Russian"))
|
|
{
|
|
mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1251"));
|
|
}
|
|
else
|
|
{
|
|
mGameSettings.setValue(QLatin1String("encoding"), QLatin1String("win1252"));
|
|
}
|
|
}
|
|
|
|
QStringList Launcher::DataFilesPage::selectedDirectoriesPaths() const
|
|
{
|
|
QStringList dirList;
|
|
for (int i = 0; i < ui.directoryListWidget->count(); ++i)
|
|
{
|
|
if (ui.directoryListWidget->item(i)->flags() & Qt::ItemIsEnabled)
|
|
dirList.append(ui.directoryListWidget->item(i)->text());
|
|
}
|
|
return dirList;
|
|
}
|
|
|
|
QStringList Launcher::DataFilesPage::selectedArchivePaths(bool all) const
|
|
{
|
|
QStringList archiveList;
|
|
for (int i = 0; i < ui.archiveListWidget->count(); ++i)
|
|
{
|
|
const auto* item = ui.archiveListWidget->item(i);
|
|
const auto archive = ui.archiveListWidget->item(i)->text();
|
|
|
|
if (all || item->checkState() == Qt::Checked)
|
|
archiveList.append(item->text());
|
|
}
|
|
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)
|
|
{
|
|
QFile file(item->filePath());
|
|
if (file.exists())
|
|
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;
|
|
|
|
mLauncherSettings.setContentList(profile, selectedDirectoriesPaths(), selectedArchivePaths(), 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);
|
|
}
|
|
|
|
QString Launcher::DataFilesPage::selectDirectory()
|
|
{
|
|
QFileDialog fileDialog(this);
|
|
fileDialog.setFileMode(QFileDialog::Directory);
|
|
fileDialog.setOptions(QFileDialog::Option::ShowDirsOnly | QFileDialog::Option::ReadOnly);
|
|
|
|
if (fileDialog.exec() == QDialog::Rejected)
|
|
return {};
|
|
|
|
return QDir(fileDialog.selectedFiles()[0]).canonicalPath();
|
|
}
|
|
|
|
void Launcher::DataFilesPage::addSubdirectories(bool append)
|
|
{
|
|
int selectedRow = append ? ui.directoryListWidget->count() : ui.directoryListWidget->currentRow();
|
|
|
|
if (selectedRow == -1)
|
|
return;
|
|
|
|
const auto rootDir = selectDirectory();
|
|
if (rootDir.isEmpty())
|
|
return;
|
|
|
|
QStringList subdirs;
|
|
contentSubdirs(rootDir, subdirs);
|
|
|
|
if (subdirs.empty())
|
|
{
|
|
// we didn't find anything that looks like a content directory, add directory selected by user
|
|
if (ui.directoryListWidget->findItems(rootDir, Qt::MatchFixedString).isEmpty())
|
|
{
|
|
ui.directoryListWidget->addItem(rootDir);
|
|
mNewDataDirs.push_back(rootDir);
|
|
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());
|
|
mNewDataDirs.push_back(dir->text());
|
|
}
|
|
}
|
|
|
|
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::moveDirectory(int step)
|
|
{
|
|
int selectedRow = ui.directoryListWidget->currentRow();
|
|
int newRow = selectedRow + step;
|
|
if (selectedRow == -1 || newRow < 0 || newRow > ui.directoryListWidget->count() - 1)
|
|
return;
|
|
|
|
if (!(ui.directoryListWidget->item(newRow)->flags() & Qt::ItemIsEnabled))
|
|
return;
|
|
|
|
const auto item = ui.directoryListWidget->takeItem(selectedRow);
|
|
ui.directoryListWidget->insertItem(newRow, item);
|
|
ui.directoryListWidget->setCurrentRow(newRow);
|
|
}
|
|
|
|
void Launcher::DataFilesPage::removeDirectory()
|
|
{
|
|
for (const auto& path : ui.directoryListWidget->selectedItems())
|
|
ui.directoryListWidget->takeItem(ui.directoryListWidget->row(path));
|
|
refreshDataFilesView();
|
|
}
|
|
|
|
void Launcher::DataFilesPage::moveArchive(int step)
|
|
{
|
|
int selectedRow = ui.archiveListWidget->currentRow();
|
|
int newRow = selectedRow + step;
|
|
if (selectedRow == -1 || newRow < 0 || newRow > ui.archiveListWidget->count() - 1)
|
|
return;
|
|
|
|
const auto* item = ui.archiveListWidget->takeItem(selectedRow);
|
|
|
|
addArchive(item->text(), item->checkState(), newRow);
|
|
ui.archiveListWidget->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);
|
|
if (mKnownArchives.filter(name).isEmpty()) // XXX why contains doesn't work here ???
|
|
{
|
|
ui.archiveListWidget->item(row)->setBackground(Qt::green);
|
|
ui.archiveListWidget->item(row)->setForeground(Qt::black);
|
|
}
|
|
}
|
|
|
|
void Launcher::DataFilesPage::addArchivesFromDir(const QString& path)
|
|
{
|
|
QDir dir(path, "*.bsa");
|
|
|
|
for (const auto& fileinfo : dir.entryInfoList())
|
|
{
|
|
const auto absPath = fileinfo.absoluteFilePath();
|
|
if (Bsa::BSAFile::detectVersion(Files::pathFromQString(absPath)) == Bsa::BSAVER_UNKNOWN)
|
|
continue;
|
|
|
|
const auto fileName = fileinfo.fileName();
|
|
const auto currentList = selectedArchivePaths(true);
|
|
|
|
if (!currentList.contains(fileName, Qt::CaseInsensitive))
|
|
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;
|
|
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
|
QSet<QString> set = cellNameLoader.getCellNames(selectedFiles);
|
|
QStringList cellNamesList(set.begin(), set.end());
|
|
#else
|
|
QStringList cellNamesList = QStringList::fromSet(cellNameLoader.getCellNames(selectedFiles));
|
|
#endif
|
|
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);
|
|
}
|