Something went wrong. Try again.
OpenMW is an open implementation of the Morrowind engine
Something went wrong. Try again.
C++
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265#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 static_cast<int>(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");
ui.directoryListWidget->installEventFilter(this);
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), mLauncherSettings.getUserFilesInOrder(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, mSelector->userFilesInOrder()); 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(), mSelector->userFilesInOrder()); 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);}
bool Launcher::DataFilesPage::eventFilter(QObject* obj, QEvent* event){ if (obj != ui.directoryListWidget) return QWidget::eventFilter(obj, event);
if (event->type() == QEvent::DragEnter || event->type() == QEvent::DragMove) { auto* dragDropEvent = static_cast<QDropEvent*>(event); const QMimeData* mimeData = dragDropEvent->mimeData();
// may be dragging multiple files and folders together if (mimeData->hasUrls()) { for (const QUrl& url : mimeData->urls()) { if (!url.isLocalFile()) continue;
QFileInfo fileInfo(url.toLocalFile());
// accept if any of it is a folder if (fileInfo.isDir()) { dragDropEvent->acceptProposedAction(); return true; } } } } else if (event->type() == QEvent::Drop) { QDropEvent* dropEvent = static_cast<QDropEvent*>(event); const QMimeData* mimeData = dropEvent->mimeData();
// may be dragging multiple files and folders together if (mimeData->hasUrls()) { QStringList droppedDirs;
for (const QUrl& url : mimeData->urls()) { if (!url.isLocalFile()) continue;
QFileInfo fileInfo(url.toLocalFile());
// only append folders if (fileInfo.isDir()) { QString canonicalPath = fileInfo.canonicalFilePath();
if (ui.directoryListWidget->findItems(canonicalPath, Qt::MatchFixedString).isEmpty()) { droppedDirs.append(canonicalPath); } } }
if (!droppedDirs.isEmpty()) { for (const QString& dirPath : droppedDirs) { ui.directoryListWidget->addItem(dirPath); auto* item = ui.directoryListWidget->item(ui.directoryListWidget->count() - 1); item->setData(Qt::UserRole, QVariant::fromValue(Config::SettingValue{ dirPath })); mNewDataDirs.push_back(dirPath); }
refreshDataFilesView(); }
dropEvent->acceptProposedAction(); return true; } }
return QWidget::eventFilter(obj, event);}